summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--benchmarks/src/benchmarks/regression/MathBenchmark.java2
-rw-r--r--harmony-tests/src/test/java/org/apache/harmony/tests/java/text/CollationElementIteratorTest.java12
-rw-r--r--harmony-tests/src/test/java/org/apache/harmony/tests/java/text/RuleBasedCollatorTest.java4
-rw-r--r--luni/src/main/java/java/nio/ByteBuffer.java6
-rw-r--r--luni/src/main/java/java/util/GregorianCalendar.java11
-rw-r--r--luni/src/main/java/java/util/Locale.java347
-rw-r--r--luni/src/main/java/libcore/icu/ICU.java14
-rw-r--r--luni/src/main/java/libcore/util/ZoneInfo.java661
-rw-r--r--luni/src/main/java/libcore/util/ZoneInfoDB.java6
-rw-r--r--luni/src/main/native/libcore_icu_ICU.cpp51
-rw-r--r--luni/src/test/java/libcore/icu/ICUTest.java6
-rw-r--r--luni/src/test/java/libcore/java/nio/BufferTest.java32
-rw-r--r--luni/src/test/java/libcore/java/util/LocaleTest.java54
13 files changed, 1050 insertions, 156 deletions
diff --git a/benchmarks/src/benchmarks/regression/MathBenchmark.java b/benchmarks/src/benchmarks/regression/MathBenchmark.java
index 2227c7f..19b2162 100644
--- a/benchmarks/src/benchmarks/regression/MathBenchmark.java
+++ b/benchmarks/src/benchmarks/regression/MathBenchmark.java
@@ -302,7 +302,7 @@ public class MathBenchmark extends SimpleBenchmark {
public long timeMinL(int reps) {
long result = l;
for (int rep = 0; rep < reps; ++rep) {
- Math.min(l, l);
+ result = Math.min(l, l);
}
return result;
}
diff --git a/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/CollationElementIteratorTest.java b/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/CollationElementIteratorTest.java
index 0ca489c..081b446 100644
--- a/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/CollationElementIteratorTest.java
+++ b/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/CollationElementIteratorTest.java
@@ -125,7 +125,8 @@ public class CollationElementIteratorTest extends TestCase {
public void testGetMaxExpansion() {
String text = "cha";
- RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance(new Locale("es", "", "TRADITIONAL"));
+ RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance(
+ Locale.forLanguageTag("es-u-co-trad"));
CollationElementIterator iterator = rbColl.getCollationElementIterator(text);
int order = iterator.next();
while (order != CollationElementIterator.NULLORDER) {
@@ -177,7 +178,8 @@ public class CollationElementIteratorTest extends TestCase {
}
public void testSetOffset() {
- RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance(new Locale("es", "", "TRADITIONAL"));
+ RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance(
+ Locale.forLanguageTag("es-u-co-trad"));
String text = "cha";
CollationElementIterator iterator = rbColl.getCollationElementIterator(text);
iterator.setOffset(0);
@@ -189,7 +191,8 @@ public class CollationElementIteratorTest extends TestCase {
}
public void testSetTextString() {
- RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance(new Locale("es", "", "TRADITIONAL"));
+ RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance(
+ Locale.forLanguageTag("es-u-co-trad"));
String text = "caa";
CollationElementIterator iterator = rbColl.getCollationElementIterator(text);
iterator.setOffset(0);
@@ -208,7 +211,8 @@ public class CollationElementIteratorTest extends TestCase {
}
public void testSetTextCharacterIterator() {
- RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance(new Locale("es", "", "TRADITIONAL"));
+ RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance(
+ Locale.forLanguageTag("es-u-co-trad"));
String text = "caa";
CollationElementIterator iterator = rbColl.getCollationElementIterator(text);
iterator.setOffset(1);
diff --git a/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/RuleBasedCollatorTest.java b/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/RuleBasedCollatorTest.java
index f5a8057..906857b 100644
--- a/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/RuleBasedCollatorTest.java
+++ b/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/RuleBasedCollatorTest.java
@@ -105,7 +105,7 @@ public class RuleBasedCollatorTest extends TestCase {
public void testGetCollationElementIteratorString() throws Exception {
{
- Locale locale = new Locale("es", "", "TRADITIONAL");
+ Locale locale = Locale.forLanguageTag("es-u-co-trad");
RuleBasedCollator coll = (RuleBasedCollator) Collator.getInstance(locale);
String source = "cha";
CollationElementIterator iterator = coll.getCollationElementIterator(source);
@@ -147,7 +147,7 @@ public class RuleBasedCollatorTest extends TestCase {
public void testGetCollationElementIteratorCharacterIterator() throws Exception {
{
- Locale locale = new Locale("es", "", "TRADITIONAL");
+ Locale locale = Locale.forLanguageTag("es-u-co-trad");
RuleBasedCollator coll = (RuleBasedCollator) Collator.getInstance(locale);
String text = "cha";
StringCharacterIterator source = new StringCharacterIterator(text);
diff --git a/luni/src/main/java/java/nio/ByteBuffer.java b/luni/src/main/java/java/nio/ByteBuffer.java
index 5873590..61093fa 100644
--- a/luni/src/main/java/java/nio/ByteBuffer.java
+++ b/luni/src/main/java/java/nio/ByteBuffer.java
@@ -69,7 +69,11 @@ public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer
if (capacity < 0) {
throw new IllegalArgumentException("capacity < 0: " + capacity);
}
- return new DirectByteBuffer(MemoryBlock.allocate(capacity), capacity, 0, false, null);
+ // Ensure alignment by 8.
+ MemoryBlock memoryBlock = MemoryBlock.allocate(capacity + 7);
+ long address = memoryBlock.toLong();
+ long alignedAddress = (address + 7) & ~(long)7;
+ return new DirectByteBuffer(memoryBlock, capacity, (int)(alignedAddress - address), false, null);
}
/**
diff --git a/luni/src/main/java/java/util/GregorianCalendar.java b/luni/src/main/java/java/util/GregorianCalendar.java
index 71b79dd..be96684 100644
--- a/luni/src/main/java/java/util/GregorianCalendar.java
+++ b/luni/src/main/java/java/util/GregorianCalendar.java
@@ -331,7 +331,16 @@ public class GregorianCalendar extends Calendar {
setTimeInMillis(System.currentTimeMillis());
}
- GregorianCalendar(boolean ignored) {
+ /**
+ * A minimum-cost constructor that does not initialize the current time or perform any date
+ * calculations. For use internally when the time will be set later. Other constructors, such as
+ * {@link GregorianCalendar#GregorianCalendar()}, set the time to the current system clock
+ * and recalculate the fields incurring unnecessary cost when the time or fields will be set
+ * later.
+ *
+ * @hide used internally
+ */
+ public GregorianCalendar(boolean ignored) {
super(TimeZone.getDefault());
setFirstDayOfWeek(SUNDAY);
setMinimalDaysInFirstWeek(1);
diff --git a/luni/src/main/java/java/util/Locale.java b/luni/src/main/java/java/util/Locale.java
index a3eaf21..e0582dc 100644
--- a/luni/src/main/java/java/util/Locale.java
+++ b/luni/src/main/java/java/util/Locale.java
@@ -270,6 +270,11 @@ public final class Locale implements Cloneable, Serializable {
public static final char UNICODE_LOCALE_EXTENSION = 'u';
/**
+ * ISO 639-3 generic code for undetermined languages.
+ */
+ private static final String UNDETERMINED_LANGUAGE = "und";
+
+ /**
* The current default locale. It is temporarily assigned to US because we
* need a default locale to lookup the real default locale.
*/
@@ -340,18 +345,22 @@ public final class Locale implements Cloneable, Serializable {
* @throws IllformedLocaleException if the language was invalid.
*/
public Builder setLanguage(String language) {
- this.language = normalizeAndValidateLanguage(language);
+ this.language = normalizeAndValidateLanguage(language, true /* strict */);
return this;
}
- private static String normalizeAndValidateLanguage(String language) {
+ private static String normalizeAndValidateLanguage(String language, boolean strict) {
if (language == null || language.isEmpty()) {
return "";
}
final String lowercaseLanguage = language.toLowerCase(Locale.ROOT);
if (!isValidBcp47Alpha(lowercaseLanguage, 2, 3)) {
- throw new IllformedLocaleException("Invalid language: " + language);
+ if (strict) {
+ throw new IllformedLocaleException("Invalid language: " + language);
+ } else {
+ return UNDETERMINED_LANGUAGE;
+ }
}
return lowercaseLanguage;
@@ -377,7 +386,7 @@ public final class Locale implements Cloneable, Serializable {
return this;
}
- final Locale fromIcu = ICU.forLanguageTag(languageTag, true /* strict */);
+ final Locale fromIcu = forLanguageTag(languageTag, true /* strict */);
// When we ask ICU for strict parsing, it might return a null locale
// if the language tag is malformed.
if (fromIcu == null) {
@@ -400,11 +409,11 @@ public final class Locale implements Cloneable, Serializable {
* @throws IllformedLocaleException if {@code} region is invalid.
*/
public Builder setRegion(String region) {
- this.region = normalizeAndValidateRegion(region);
+ this.region = normalizeAndValidateRegion(region, true /* strict */);
return this;
}
- private static String normalizeAndValidateRegion(String region) {
+ private static String normalizeAndValidateRegion(String region, boolean strict) {
if (region == null || region.isEmpty()) {
return "";
}
@@ -412,7 +421,11 @@ public final class Locale implements Cloneable, Serializable {
final String uppercaseRegion = region.toUpperCase(Locale.ROOT);
if (!isValidBcp47Alpha(uppercaseRegion, 2, 2) &&
!isUnM49AreaCode(uppercaseRegion)) {
- throw new IllformedLocaleException("Invalid region: " + region);
+ if (strict) {
+ throw new IllformedLocaleException("Invalid region: " + region);
+ } else {
+ return "";
+ }
}
return uppercaseRegion;
@@ -453,27 +466,32 @@ public final class Locale implements Cloneable, Serializable {
String[] subTags = normalizedVariant.split("_");
for (String subTag : subTags) {
- // The BCP-47 spec states that :
- // - Subtags can be between [5, 8] alphanumeric chars in length.
- // - Subtags that start with a number are allowed to be 4 chars in length.
- if (subTag.length() >= 5 && subTag.length() <= 8) {
- if (!isAsciiAlphaNum(subTag)) {
- throw new IllformedLocaleException("Invalid variant: " + variant);
- }
- } else if (subTag.length() == 4) {
- final char firstChar = subTag.charAt(0);
- if (!(firstChar >= '0' && firstChar <= '9') || !isAsciiAlphaNum(subTag)) {
- throw new IllformedLocaleException("Invalid variant: " + variant);
- }
- } else {
+ if (!isValidVariantSubtag(subTag)) {
throw new IllformedLocaleException("Invalid variant: " + variant);
}
}
-
return normalizedVariant;
}
+ private static boolean isValidVariantSubtag(String subTag) {
+ // The BCP-47 spec states that :
+ // - Subtags can be between [5, 8] alphanumeric chars in length.
+ // - Subtags that start with a number are allowed to be 4 chars in length.
+ if (subTag.length() >= 5 && subTag.length() <= 8) {
+ if (isAsciiAlphaNum(subTag)) {
+ return true;
+ }
+ } else if (subTag.length() == 4) {
+ final char firstChar = subTag.charAt(0);
+ if ((firstChar >= '0' && firstChar <= '9') && isAsciiAlphaNum(subTag)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* Sets the locale script. If {@code script} is {@code null} or empty,
* the previous value is cleared.
@@ -489,17 +507,24 @@ public final class Locale implements Cloneable, Serializable {
* @throws IllformedLocaleException if {@code script} is invalid.
*/
public Builder setScript(String script) {
+ this.script = normalizeAndValidateScript(script, true /* strict */);
+ return this;
+ }
+
+ private static String normalizeAndValidateScript(String script, boolean strict) {
if (script == null || script.isEmpty()) {
- this.script = "";
- return this;
+ return "";
}
if (!isValidBcp47Alpha(script, 4, 4)) {
- throw new IllformedLocaleException("Invalid script: " + script);
+ if (strict) {
+ throw new IllformedLocaleException("Invalid script: " + script);
+ } else {
+ return "";
+ }
}
- this.script = titleCaseAsciiWord(script);
- return this;
+ return titleCaseAsciiWord(script);
}
/**
@@ -795,7 +820,7 @@ public final class Locale implements Cloneable, Serializable {
throw new NullPointerException("languageTag == null");
}
- return ICU.forLanguageTag(languageTag, false /* strict */);
+ return forLanguageTag(languageTag, false /* strict */);
}
private transient String countryCode;
@@ -1005,9 +1030,9 @@ public final class Locale implements Cloneable, Serializable {
return "";
}
- try {
- Builder.normalizeAndValidateRegion(countryCode);
- } catch (IllformedLocaleException ex) {
+ final String normalizedRegion = Builder.normalizeAndValidateRegion(
+ countryCode, false /* strict */);
+ if (normalizedRegion.isEmpty()) {
return countryCode;
}
@@ -1041,9 +1066,9 @@ public final class Locale implements Cloneable, Serializable {
// display language for the indeterminate language code.
//
// Sigh... ugh... and what not.
- try {
- Builder.normalizeAndValidateLanguage(languageCode);
- } catch (IllformedLocaleException ex) {
+ final String normalizedLanguage = Builder.normalizeAndValidateLanguage(
+ languageCode, false /* strict */);
+ if (UNDETERMINED_LANGUAGE.equals(normalizedLanguage)) {
return languageCode;
}
@@ -1331,17 +1356,8 @@ public final class Locale implements Cloneable, Serializable {
// in the builder, but we must use hyphens in the BCP-47 language tag.
variant = variantCode.replace('_', '-');
} else {
- try {
- language = Builder.normalizeAndValidateLanguage(languageCode);
- } catch (IllformedLocaleException ilfe) {
- // Ignored, continue processing with "".
- }
-
- try {
- region = Builder.normalizeAndValidateRegion(countryCode);
- } catch (IllformedLocaleException ilfe) {
- // Ignored, continue processing with "".
- }
+ language = Builder.normalizeAndValidateLanguage(languageCode, false /* strict */);
+ region = Builder.normalizeAndValidateRegion(countryCode, false /* strict */);
try {
variant = Builder.normalizeAndValidateVariant(variantCode);
@@ -1358,7 +1374,7 @@ public final class Locale implements Cloneable, Serializable {
}
if (language.isEmpty()) {
- language = "und";
+ language = UNDETERMINED_LANGUAGE;
}
if ("no".equals(language) && "NO".equals(region) && "NY".equals(variant)) {
@@ -1497,7 +1513,7 @@ public final class Locale implements Cloneable, Serializable {
private static String concatenateRange(String[] array, int start, int end) {
StringBuilder builder = new StringBuilder(32);
for (int i = start; i < end; ++i) {
- if (i != 0) {
+ if (i != start) {
builder.append('-');
}
builder.append(array[i]);
@@ -1922,8 +1938,10 @@ public final class Locale implements Cloneable, Serializable {
while (true) {
final Map.Entry<String, String> keyWord = keywordsIterator.next();
sb.append(keyWord.getKey());
- sb.append('-');
- sb.append(keyWord.getValue());
+ if (!keyWord.getValue().isEmpty()) {
+ sb.append('-');
+ sb.append(keyWord.getValue());
+ }
if (keywordsIterator.hasNext()) {
sb.append('-');
} else {
@@ -1970,6 +1988,8 @@ public final class Locale implements Cloneable, Serializable {
if (subtagsForKeyword.size() > 0) {
keywords.put(lastKeyword, joinBcp47Subtags(subtagsForKeyword));
+ } else {
+ keywords.put(lastKeyword, "");
}
}
@@ -1991,7 +2011,10 @@ public final class Locale implements Cloneable, Serializable {
return sb.toString();
}
- private static String adjustLanguageCode(String languageCode) {
+ /**
+ * @hide for internal use only.
+ */
+ public static String adjustLanguageCode(String languageCode) {
String adjusted = languageCode.toLowerCase(Locale.US);
// Map new language codes to the obsolete language
// codes so the correct resource bundles will be used.
@@ -2005,4 +2028,230 @@ public final class Locale implements Cloneable, Serializable {
return adjusted;
}
+
+ /**
+ * Map of grandfathered language tags to their modern replacements.
+ */
+ private static final TreeMap<String, String> GRANDFATHERED_LOCALES;
+
+ static {
+ GRANDFATHERED_LOCALES = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
+
+ // From http://tools.ietf.org/html/bcp47
+ //
+ // grandfathered = irregular ; non-redundant tags registered
+ // / regular ; during the RFC 3066 era
+ // irregular =
+ GRANDFATHERED_LOCALES.put("en-GB-oed", "en-GB-x-oed");
+ GRANDFATHERED_LOCALES.put("i-ami", "ami");
+ GRANDFATHERED_LOCALES.put("i-bnn", "bnn");
+ GRANDFATHERED_LOCALES.put("i-default", "en-x-i-default");
+ GRANDFATHERED_LOCALES.put("i-enochian", "und-x-i-enochian");
+ GRANDFATHERED_LOCALES.put("i-hak", "hak");
+ GRANDFATHERED_LOCALES.put("i-klingon", "tlh");
+ GRANDFATHERED_LOCALES.put("i-lux", "lb");
+ GRANDFATHERED_LOCALES.put("i-mingo", "see-x-i-mingo");
+ GRANDFATHERED_LOCALES.put("i-navajo", "nv");
+ GRANDFATHERED_LOCALES.put("i-pwn", "pwn");
+ GRANDFATHERED_LOCALES.put("i-tao", "tao");
+ GRANDFATHERED_LOCALES.put("i-tay", "tay");
+ GRANDFATHERED_LOCALES.put("i-tsu", "tsu");
+ GRANDFATHERED_LOCALES.put("sgn-BE-FR", "sfb");
+ GRANDFATHERED_LOCALES.put("sgn-BE-NL", "vgt");
+ GRANDFATHERED_LOCALES.put("sgn-CH-DE", "sgg");
+
+ // regular =
+ GRANDFATHERED_LOCALES.put("art-lojban", "jbo");
+ GRANDFATHERED_LOCALES.put("cel-gaulish", "xtg-x-cel-gaulish");
+ GRANDFATHERED_LOCALES.put("no-bok", "nb");
+ GRANDFATHERED_LOCALES.put("no-nyn", "nn");
+ GRANDFATHERED_LOCALES.put("zh-guoyu", "cmn");
+ GRANDFATHERED_LOCALES.put("zh-hakka", "hak");
+ GRANDFATHERED_LOCALES.put("zh-min", "nan-x-zh-min");
+ GRANDFATHERED_LOCALES.put("zh-min-nan", "nan");
+ GRANDFATHERED_LOCALES.put("zh-xiang", "hsn");
+ }
+
+ private static String convertGrandfatheredTag(String original) {
+ final String converted = GRANDFATHERED_LOCALES.get(original);
+ return converted != null ? converted : original;
+ }
+
+ /**
+ * Scans elements of {@code subtags} in the range {@code [startIndex, endIndex)}
+ * and appends valid variant subtags upto the first invalid subtag (if any) to
+ * {@code normalizedVariants}. All appended variant subtags are converted to uppercase.
+ */
+ private static void extractVariantSubtags(String[] subtags, int startIndex, int endIndex,
+ List<String> normalizedVariants) {
+ for (int i = startIndex; i < endIndex; i++) {
+ final String subtag = subtags[i];
+
+ if (Builder.isValidVariantSubtag(subtag)) {
+ normalizedVariants.add(subtag.toUpperCase(Locale.ROOT));
+ } else {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Scans elements of {@code subtags} in the range {@code [startIndex, endIndex)}
+ * and inserts valid extensions into {@code extensions}. The scan is aborted
+ * when an invalid extension is encountered. Returns the index of the first
+ * unparsable element of {@code subtags}.
+ */
+ private static int extractExtensions(String[] subtags, int startIndex, int endIndex,
+ Map<Character, String> extensions) {
+ int privateUseExtensionIndex = -1;
+ int extensionKeyIndex = -1;
+
+ int i = startIndex;
+ for (; i < endIndex; i++) {
+ final String subtag = subtags[i];
+
+ final boolean parsingPrivateUse = (privateUseExtensionIndex != -1) &&
+ (extensionKeyIndex == privateUseExtensionIndex);
+
+ // Note that private use extensions allow subtags of length 1.
+ // Private use extensions *must* come last, so there's no ambiguity
+ // in that case.
+ if (subtag.length() == 1 && !parsingPrivateUse) {
+ // Emit the last extension we encountered if any. First check
+ // whether we encountered two keys in a row (which is an error).
+ // Also checks if we already have an extension with the same key,
+ // which is again an error.
+ if (extensionKeyIndex != -1) {
+ if ((i - 1) == extensionKeyIndex) {
+ return extensionKeyIndex;
+ }
+
+ final String key = subtags[extensionKeyIndex];
+ if (extensions.containsKey(key.charAt(0))) {
+ return extensionKeyIndex;
+ }
+
+ final String value = concatenateRange(subtags, extensionKeyIndex + 1, i);
+ extensions.put(key.charAt(0), value.toLowerCase(Locale.ROOT));
+ }
+
+ // Mark the start of the next extension. Also keep track of whether this
+ // is a private use extension, and throw an error if it doesn't come last.
+ extensionKeyIndex = i;
+ if ("x".equals(subtag)) {
+ privateUseExtensionIndex = i;
+ } else if (privateUseExtensionIndex != -1) {
+ // The private use extension must come last.
+ return privateUseExtensionIndex;
+ }
+ } else if (extensionKeyIndex != -1) {
+ // We must have encountered a valid key in order to start parsing
+ // its subtags.
+ if (!isValidBcp47Alphanum(subtag, parsingPrivateUse ? 1 : 2, 8)) {
+ return i;
+ }
+ } else {
+ // Encountered a value without a preceding key.
+ return i;
+ }
+ }
+
+ if (extensionKeyIndex != -1) {
+ if ((i - 1) == extensionKeyIndex) {
+ return extensionKeyIndex;
+ }
+
+ final String key = subtags[extensionKeyIndex];
+ if (extensions.containsKey(key.charAt(0))) {
+ return extensionKeyIndex;
+ }
+
+ final String value = concatenateRange(subtags, extensionKeyIndex + 1, i);
+ extensions.put(key.charAt(0), value.toLowerCase(Locale.ROOT));
+ }
+
+ return i;
+ }
+
+ private static Locale forLanguageTag(/* @Nonnull */ String tag, boolean strict) {
+ final String converted = convertGrandfatheredTag(tag);
+ final String[] subtags = converted.split("-");
+
+ int lastSubtag = subtags.length;
+ for (int i = 0; i < subtags.length; ++i) {
+ final String subtag = subtags[i];
+ if (subtag.isEmpty() || subtag.length() > 8) {
+ if (strict) {
+ throw new IllformedLocaleException("Invalid subtag at index: " + i
+ + " in tag: " + tag);
+ } else {
+ lastSubtag = (i - 1);
+ }
+
+ break;
+ }
+ }
+
+ final String languageCode = Builder.normalizeAndValidateLanguage(subtags[0], strict);
+ String scriptCode = "";
+ int nextSubtag = 1;
+ if (lastSubtag > nextSubtag) {
+ scriptCode = Builder.normalizeAndValidateScript(subtags[nextSubtag], false /* strict */);
+ if (!scriptCode.isEmpty()) {
+ nextSubtag++;
+ }
+ }
+
+ String regionCode = "";
+ if (lastSubtag > nextSubtag) {
+ regionCode = Builder.normalizeAndValidateRegion(subtags[nextSubtag], false /* strict */);
+ if (!regionCode.isEmpty()) {
+ nextSubtag++;
+ }
+ }
+
+ List<String> variants = null;
+ if (lastSubtag > nextSubtag) {
+ variants = new ArrayList<String>();
+ extractVariantSubtags(subtags, nextSubtag, lastSubtag, variants);
+ nextSubtag += variants.size();
+ }
+
+ Map<Character, String> extensions = Collections.EMPTY_MAP;
+ if (lastSubtag > nextSubtag) {
+ extensions = new TreeMap<Character, String>();
+ nextSubtag = extractExtensions(subtags, nextSubtag, lastSubtag, extensions);
+ }
+
+ if (nextSubtag != lastSubtag) {
+ if (strict) {
+ throw new IllformedLocaleException("Unparseable subtag: " + subtags[nextSubtag]
+ + " from language tag: " + tag);
+ }
+ }
+
+ Set<String> unicodeKeywords = Collections.EMPTY_SET;
+ Map<String, String> unicodeAttributes = Collections.EMPTY_MAP;
+ if (extensions.containsKey(UNICODE_LOCALE_EXTENSION)) {
+ unicodeKeywords = new TreeSet<String>();
+ unicodeAttributes = new TreeMap<String, String>();
+ parseUnicodeExtension(extensions.get(UNICODE_LOCALE_EXTENSION).split("-"),
+ unicodeAttributes, unicodeKeywords);
+ }
+
+ String variantCode = "";
+ if (variants != null && !variants.isEmpty()) {
+ StringBuilder variantsBuilder = new StringBuilder(variants.size() * 8);
+ for (int i = 0; i < variants.size(); ++i) {
+ if (i != 0) {
+ variantsBuilder.append('_');
+ }
+ variantsBuilder.append(variants.get(i));
+ }
+ variantCode = variantsBuilder.toString();
+ }
+
+ return new Locale(languageCode, regionCode, variantCode, scriptCode,
+ unicodeKeywords, unicodeAttributes, extensions, true /* has validated fields */);
+ }
}
diff --git a/luni/src/main/java/libcore/icu/ICU.java b/luni/src/main/java/libcore/icu/ICU.java
index 33c899e..bb57f49 100644
--- a/luni/src/main/java/libcore/icu/ICU.java
+++ b/luni/src/main/java/libcore/icu/ICU.java
@@ -58,18 +58,6 @@ public final class ICU {
return isoCountries.clone();
}
- public static Locale forLanguageTag(String languageTag, boolean strict) {
- final String icuLocaleId = localeForLanguageTag(languageTag, strict);
- if (icuLocaleId == null) {
- // TODO: We should probably return "und" here. From what I can tell,
- // this happens only when the language in the languageTag is bad.
- // Investigate this a bit more.
- return null;
- }
-
- return localeFromIcuLocaleId(icuLocaleId);
- }
-
private static final int IDX_LANGUAGE = 0;
private static final int IDX_SCRIPT = 1;
private static final int IDX_REGION = 2;
@@ -444,8 +432,6 @@ public final class ICU {
private static native String[] getISOLanguagesNative();
private static native String[] getISOCountriesNative();
- private static native String localeForLanguageTag(String languageTag, boolean strict);
-
static native boolean initLocaleDataNative(String locale, LocaleData result);
/**
diff --git a/luni/src/main/java/libcore/util/ZoneInfo.java b/luni/src/main/java/libcore/util/ZoneInfo.java
index ed7ab64..fbd120b 100644
--- a/luni/src/main/java/libcore/util/ZoneInfo.java
+++ b/luni/src/main/java/libcore/util/ZoneInfo.java
@@ -13,11 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+/*
+ * Elements of the WallTime class are a port of Bionic's localtime.c to Java. That code had the
+ * following header:
+ *
+ * This file is in the public domain, so clarified as of
+ * 1996-06-05 by Arthur David Olson.
+ */
package libcore.util;
import java.util.Arrays;
+import java.util.Calendar;
import java.util.Date;
+import java.util.GregorianCalendar;
import java.util.TimeZone;
import libcore.io.BufferIterator;
@@ -310,4 +318,655 @@ public final class ZoneInfo extends TimeZone {
// respectively.
return super.clone();
}
+
+ /**
+ * A class that represents a "wall time". This class is modeled on the C tm struct and
+ * is used to support android.text.format.Time behavior. Unlike the tm struct the year is
+ * represented as the full year, not the years since 1900.
+ *
+ * <p>This class contains a rewrite of various native functions that android.text.format.Time
+ * once relied on such as mktime_tz and localtime_tz. This replacement does not support leap
+ * seconds but does try to preserve behavior around ambiguous date/times found in the BSD
+ * version of mktime that was previously used.
+ *
+ * <p>The original native code used a 32-bit value for time_t on 32-bit Android, which
+ * was the only variant of Android available at the time. To preserve old behavior this code
+ * deliberately uses {@code int} rather than {@code long} for most things and performs
+ * calculations in seconds. This creates deliberate truncation issues for date / times before
+ * 1901 and after 2038. This is intentional but might be fixed in future if all the knock-ons
+ * can be resolved: Application code may have come to rely on the range so previously values
+ * like zero for year could indicate an invalid date but if we move to long the year zero would
+ * be valid.
+ *
+ * <p>All offsets are considered to be safe for addition / subtraction / multiplication without
+ * worrying about overflow. All absolute time arithmetic is checked for overflow / underflow.
+ */
+ public static class WallTime {
+
+ // We use a GregorianCalendar (set to UTC) to handle all the date/time normalization logic
+ // and to convert from a broken-down date/time to a millis value.
+ // Unfortunately, it cannot represent an initial state with a zero day and would
+ // automatically normalize it, so we must copy values into and out of it as needed.
+ private final GregorianCalendar calendar;
+
+ private int year;
+ private int month;
+ private int monthDay;
+ private int hour;
+ private int minute;
+ private int second;
+ private int weekDay;
+ private int yearDay;
+ private int isDst;
+ private int gmtOffsetSeconds;
+
+ public WallTime() {
+ this.calendar = new GregorianCalendar(false);
+ calendar.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ /**
+ * Sets the wall time to a point in time using the time zone information provided. This
+ * is a replacement for the old native localtime_tz() function.
+ *
+ * <p>When going from an instant to a wall time it is always unambiguous because there
+ * is only one offset rule acting at any given instant. We do not consider leap seconds.
+ */
+ public void localtime(int timeSeconds, ZoneInfo zoneInfo) {
+ try {
+ int offsetSeconds = zoneInfo.mRawOffset / 1000;
+
+ // Find out the timezone DST state and adjustment.
+ byte isDst;
+ if (zoneInfo.mTransitions.length == 0) {
+ isDst = 0;
+ } else {
+ // transitionIndex can be in the range -1..zoneInfo.mTransitions.length - 1
+ int transitionIndex = findTransitionIndex(zoneInfo, timeSeconds);
+ if (transitionIndex < 0) {
+ // -1 means timeSeconds is "before the first recorded transition". The first
+ // recorded transition is treated as a transition from non-DST and the raw
+ // offset.
+ isDst = 0;
+ } else {
+ byte transitionType = zoneInfo.mTypes[transitionIndex];
+ offsetSeconds += zoneInfo.mOffsets[transitionType];
+ isDst = zoneInfo.mIsDsts[transitionType];
+ }
+ }
+
+ // Perform arithmetic that might underflow before setting fields.
+ int wallTimeSeconds = checkedAdd(timeSeconds, offsetSeconds);
+
+ // Set fields.
+ calendar.setTimeInMillis(wallTimeSeconds * 1000L);
+ copyFieldsFromCalendar();
+ this.isDst = isDst;
+ this.gmtOffsetSeconds = offsetSeconds;
+ } catch (CheckedArithmeticException e) {
+ // Just stop, leaving fields untouched.
+ }
+ }
+
+ /**
+ * Returns the time in seconds since beginning of the Unix epoch for the wall time using the
+ * time zone information provided. This is a replacement for an old native mktime_tz() C
+ * function.
+ *
+ * <p>When going from a wall time to an instant the answer can be ambiguous. A wall
+ * time can map to zero, one or two instants given sane date/time transitions. Sane
+ * in this case means that transitions occur less frequently than the offset
+ * differences between them (which could cause all sorts of craziness like the
+ * skipping out of transitions).
+ *
+ * <p>For example, this is not fully supported:
+ * <ul>
+ * <li>t1 { time = 1, offset = 0 }
+ * <li>t2 { time = 2, offset = -1 }
+ * <li>t3 { time = 3, offset = -2 }
+ * </ul>
+ * A wall time in this case might map to t1, t2 or t3.
+ *
+ * <p>We do not handle leap seconds.
+ * <p>We assume that no timezone offset transition has an absolute offset > 24 hours.
+ * <p>We do not assume that adjacent transitions modify the DST state; adjustments can
+ * occur for other reasons such as when a zone changes its raw offset.
+ */
+ public int mktime(ZoneInfo zoneInfo) {
+ // Normalize isDst to -1, 0 or 1 to simplify isDst equality checks below.
+ this.isDst = this.isDst > 0 ? this.isDst = 1 : this.isDst < 0 ? this.isDst = -1 : 0;
+
+ copyFieldsToCalendar();
+ final long longWallTimeSeconds = calendar.getTimeInMillis() / 1000;
+ if (Integer.MIN_VALUE > longWallTimeSeconds
+ || longWallTimeSeconds > Integer.MAX_VALUE) {
+ // For compatibility with the old native 32-bit implementation we must treat
+ // this as an error. Note: -1 could be confused with a real time.
+ return -1;
+ }
+
+ try {
+ final int wallTimeSeconds = (int) longWallTimeSeconds;
+ final int rawOffsetSeconds = zoneInfo.mRawOffset / 1000;
+ final int rawTimeSeconds = checkedSubtract(wallTimeSeconds, rawOffsetSeconds);
+
+ if (zoneInfo.mTransitions.length == 0) {
+ // There is no transition information. There is just a raw offset for all time.
+ if (this.isDst > 0) {
+ // Caller has asserted DST, but there is no DST information available.
+ return -1;
+ }
+ copyFieldsFromCalendar();
+ this.isDst = 0;
+ this.gmtOffsetSeconds = rawOffsetSeconds;
+ return rawTimeSeconds;
+ }
+
+ // We cannot know for sure what instant the wall time will map to. Unfortunately, in
+ // order to know for sure we need the timezone information, but to get the timezone
+ // information we need an instant. To resolve this we use the raw offset to find an
+ // OffsetInterval; this will get us the OffsetInterval we need or very close.
+
+ // The initialTransition can be between -1 and (zoneInfo.mTransitions - 1). -1
+ // indicates the rawTime is before the first transition and is handled gracefully by
+ // createOffsetInterval().
+ final int initialTransitionIndex = findTransitionIndex(zoneInfo, rawTimeSeconds);
+
+ if (isDst < 0) {
+ // This is treated as a special case to get it out of the way:
+ // When a caller has set isDst == -1 it means we can return the first match for
+ // the wall time we find. If the caller has specified a wall time that cannot
+ // exist this always returns -1.
+
+ Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex,
+ wallTimeSeconds, true /* mustMatchDst */);
+ return result == null ? -1 : result;
+ }
+
+ // If the wall time asserts a DST (isDst == 0 or 1) the search is performed twice:
+ // 1) The first attempts to find a DST offset that matches isDst exactly.
+ // 2) If it fails, isDst is assumed to be incorrect and adjustments are made to see
+ // if a valid wall time can be created. The result can be somewhat arbitrary.
+
+ Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds,
+ true /* mustMatchDst */);
+ if (result == null) {
+ result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds,
+ false /* mustMatchDst */);
+ }
+ if (result == null) {
+ result = -1;
+ }
+ return result;
+ } catch (CheckedArithmeticException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * Attempt to apply DST adjustments to {@code oldWallTimeSeconds} to create a wall time in
+ * {@code targetInterval}.
+ *
+ * <p>This is used when a caller has made an assertion about standard time / DST that cannot
+ * be matched to any offset interval that exists. We must therefore assume that the isDst
+ * assertion is incorrect and the invalid wall time is the result of some modification the
+ * caller made to a valid wall time that pushed them outside of the offset interval they
+ * were in. We must correct for any DST change that should have been applied when they did
+ * so.
+ *
+ * <p>Unfortunately, we have no information about what adjustment they made and so cannot
+ * know which offset interval they were previously in. For example, they may have added a
+ * second or a year to a valid time to arrive at what they have.
+ *
+ * <p>We try all offset types that are not the same as the isDst the caller asserted. For
+ * each possible offset we work out the offset difference between that and
+ * {@code targetInterval}, apply it, and see if we are still in {@code targetInterval}. If
+ * we are, then we have found an adjustment.
+ */
+ private Integer tryOffsetAdjustments(ZoneInfo zoneInfo, int oldWallTimeSeconds,
+ OffsetInterval targetInterval, int transitionIndex, int isDstToFind)
+ throws CheckedArithmeticException {
+
+ int[] offsetsToTry = getOffsetsOfType(zoneInfo, transitionIndex, isDstToFind);
+ for (int j = 0; j < offsetsToTry.length; j++) {
+ int rawOffsetSeconds = zoneInfo.mRawOffset / 1000;
+ int jOffsetSeconds = rawOffsetSeconds + offsetsToTry[j];
+ int targetIntervalOffsetSeconds = targetInterval.getTotalOffsetSeconds();
+ int adjustmentSeconds = targetIntervalOffsetSeconds - jOffsetSeconds;
+ int adjustedWallTimeSeconds = checkedAdd(oldWallTimeSeconds, adjustmentSeconds);
+ if (targetInterval.containsWallTime(adjustedWallTimeSeconds)) {
+ // Perform any arithmetic that might overflow.
+ int returnValue = checkedSubtract(adjustedWallTimeSeconds,
+ targetIntervalOffsetSeconds);
+
+ // Modify field state and return the result.
+ calendar.setTimeInMillis(adjustedWallTimeSeconds * 1000L);
+ copyFieldsFromCalendar();
+ this.isDst = targetInterval.getIsDst();
+ this.gmtOffsetSeconds = targetIntervalOffsetSeconds;
+ return returnValue;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return an array of offsets that have the requested {@code isDst} value.
+ * The {@code startIndex} is used as a starting point so transitions nearest
+ * to that index are returned first.
+ */
+ private static int[] getOffsetsOfType(ZoneInfo zoneInfo, int startIndex, int isDst) {
+ // +1 to account for the synthetic transition we invent before the first recorded one.
+ int[] offsets = new int[zoneInfo.mOffsets.length + 1];
+ boolean[] seen = new boolean[zoneInfo.mOffsets.length];
+ int numFound = 0;
+
+ int delta = 0;
+ boolean clampTop = false;
+ boolean clampBottom = false;
+ do {
+ // delta = { 1, -1, 2, -2, 3, -3...}
+ delta *= -1;
+ if (delta >= 0) {
+ delta++;
+ }
+
+ int transitionIndex = startIndex + delta;
+ if (delta < 0 && transitionIndex < -1) {
+ clampBottom = true;
+ continue;
+ } else if (delta > 0 && transitionIndex >= zoneInfo.mTypes.length) {
+ clampTop = true;
+ continue;
+ }
+
+ if (transitionIndex == -1) {
+ if (isDst == 0) {
+ // Synthesize a non-DST transition before the first transition we have
+ // data for.
+ offsets[numFound++] = 0; // offset of 0 from raw offset
+ }
+ continue;
+ }
+ byte type = zoneInfo.mTypes[transitionIndex];
+ if (!seen[type]) {
+ if (zoneInfo.mIsDsts[type] == isDst) {
+ offsets[numFound++] = zoneInfo.mOffsets[type];
+ }
+ seen[type] = true;
+ }
+ } while (!(clampTop && clampBottom));
+
+ int[] toReturn = new int[numFound];
+ System.arraycopy(offsets, 0, toReturn, 0, numFound);
+ return toReturn;
+ }
+
+ /**
+ * Find a time <em>in seconds</em> the same or close to {@code wallTimeSeconds} that
+ * satisfies {@code mustMatchDst}. The search begins around the timezone offset transition
+ * with {@code initialTransitionIndex}.
+ *
+ * <p>If {@code mustMatchDst} is {@code true} the method can only return times that
+ * use timezone offsets that satisfy the {@code this.isDst} requirements.
+ * If {@code this.isDst == -1} it means that any offset can be used.
+ *
+ * <p>If {@code mustMatchDst} is {@code false} any offset that covers the
+ * currently set time is acceptable. That is: if {@code this.isDst} == -1, any offset
+ * transition can be used, if it is 0 or 1 the offset used must match {@code this.isDst}.
+ *
+ * <p>Note: This method both uses and can modify field state. It returns the matching time
+ * in seconds if a match has been found and modifies fields, or it returns {@code null} and
+ * leaves the field state unmodified.
+ */
+ private Integer doWallTimeSearch(ZoneInfo zoneInfo, int initialTransitionIndex,
+ int wallTimeSeconds, boolean mustMatchDst) throws CheckedArithmeticException {
+
+ // The loop below starts at the initialTransitionIndex and radiates out from that point
+ // up to 24 hours in either direction by applying transitionIndexDelta to inspect
+ // adjacent transitions (0, -1, +1, -2, +2). 24 hours is used because we assume that no
+ // total offset from UTC is ever > 24 hours. clampTop and clampBottom are used to
+ // indicate whether the search has either searched > 24 hours or exhausted the
+ // transition data in that direction. The search stops when a match is found or if
+ // clampTop and clampBottom are both true.
+ // The match logic employed is determined by the mustMatchDst parameter.
+ final int MAX_SEARCH_SECONDS = 24 * 60 * 60;
+ boolean clampTop = false, clampBottom = false;
+ int loop = 0;
+ do {
+ // transitionIndexDelta = { 0, -1, 1, -2, 2,..}
+ int transitionIndexDelta = (loop + 1) / 2;
+ if (loop % 2 == 1) {
+ transitionIndexDelta *= -1;
+ }
+ loop++;
+
+ // Only do any work in this iteration if we need to.
+ if (transitionIndexDelta > 0 && clampTop
+ || transitionIndexDelta < 0 && clampBottom) {
+ continue;
+ }
+
+ // Obtain the OffsetInterval to use.
+ int currentTransitionIndex = initialTransitionIndex + transitionIndexDelta;
+ OffsetInterval offsetInterval =
+ OffsetInterval.create(zoneInfo, currentTransitionIndex);
+ if (offsetInterval == null) {
+ // No transition exists with the index we tried: Stop searching in the
+ // current direction.
+ clampTop |= (transitionIndexDelta > 0);
+ clampBottom |= (transitionIndexDelta < 0);
+ continue;
+ }
+
+ // Match the wallTimeSeconds against the OffsetInterval.
+ if (mustMatchDst) {
+ // Work out if the interval contains the wall time the caller specified and
+ // matches their isDst value.
+ if (offsetInterval.containsWallTime(wallTimeSeconds)) {
+ if (this.isDst == -1 || offsetInterval.getIsDst() == this.isDst) {
+ // This always returns the first OffsetInterval it finds that matches
+ // the wall time and isDst requirements. If this.isDst == -1 this means
+ // the result might be a DST or a non-DST answer for wall times that can
+ // exist in two OffsetIntervals.
+ int totalOffsetSeconds = offsetInterval.getTotalOffsetSeconds();
+ int returnValue = checkedSubtract(wallTimeSeconds,
+ totalOffsetSeconds);
+
+ copyFieldsFromCalendar();
+ this.isDst = offsetInterval.getIsDst();
+ this.gmtOffsetSeconds = totalOffsetSeconds;
+ return returnValue;
+ }
+ }
+ } else {
+ // To retain similar behavior to the old native implementation: if the caller is
+ // asserting the same isDst value as the OffsetInterval we are looking at we do
+ // not try to find an adjustment from another OffsetInterval of the same isDst
+ // type. If you remove this you get different results in situations like a
+ // DST -> DST transition or STD -> STD transition that results in an interval of
+ // "skipped" wall time. For example: if 01:30 (DST) is invalid and between two
+ // DST intervals, and the caller has passed isDst == 1, this results in a -1
+ // being returned.
+ if (isDst != offsetInterval.getIsDst()) {
+ final int isDstToFind = isDst;
+ Integer returnValue = tryOffsetAdjustments(zoneInfo, wallTimeSeconds,
+ offsetInterval, currentTransitionIndex, isDstToFind);
+ if (returnValue != null) {
+ return returnValue;
+ }
+ }
+ }
+
+ // See if we can avoid another loop in the current direction.
+ if (transitionIndexDelta > 0) {
+ // If we are searching forward and the OffsetInterval we have ends
+ // > MAX_SEARCH_SECONDS after the wall time, we don't need to look any further
+ // forward.
+ boolean endSearch = offsetInterval.getEndWallTimeSeconds() - wallTimeSeconds
+ > MAX_SEARCH_SECONDS;
+ if (endSearch) {
+ clampTop = true;
+ }
+ } else if (transitionIndexDelta < 0) {
+ boolean endSearch = wallTimeSeconds - offsetInterval.getStartWallTimeSeconds()
+ >= MAX_SEARCH_SECONDS;
+ if (endSearch) {
+ // If we are searching backward and the OffsetInterval starts
+ // > MAX_SEARCH_SECONDS before the wall time, we don't need to look any
+ // further backwards.
+ clampBottom = true;
+ }
+ }
+ } while (!(clampTop && clampBottom));
+ return null;
+ }
+
+ public void setYear(int year) {
+ this.year = year;
+ }
+
+ public void setMonth(int month) {
+ this.month = month;
+ }
+
+ public void setMonthDay(int monthDay) {
+ this.monthDay = monthDay;
+ }
+
+ public void setHour(int hour) {
+ this.hour = hour;
+ }
+
+ public void setMinute(int minute) {
+ this.minute = minute;
+ }
+
+ public void setSecond(int second) {
+ this.second = second;
+ }
+
+ public void setWeekDay(int weekDay) {
+ this.weekDay = weekDay;
+ }
+
+ public void setYearDay(int yearDay) {
+ this.yearDay = yearDay;
+ }
+
+ public void setIsDst(int isDst) {
+ this.isDst = isDst;
+ }
+
+ public void setGmtOffset(int gmtoff) {
+ this.gmtOffsetSeconds = gmtoff;
+ }
+
+ public int getYear() {
+ return year;
+ }
+
+ public int getMonth() {
+ return month;
+ }
+
+ public int getMonthDay() {
+ return monthDay;
+ }
+
+ public int getHour() {
+ return hour;
+ }
+
+ public int getMinute() {
+ return minute;
+ }
+
+ public int getSecond() {
+ return second;
+ }
+
+ public int getWeekDay() {
+ return weekDay;
+ }
+
+ public int getYearDay() {
+ return yearDay;
+ }
+
+ public int getGmtOffset() {
+ return gmtOffsetSeconds;
+ }
+
+ public int getIsDst() {
+ return isDst;
+ }
+
+ private void copyFieldsToCalendar() {
+ calendar.set(Calendar.YEAR, year);
+ calendar.set(Calendar.MONTH, month);
+ calendar.set(Calendar.DAY_OF_MONTH, monthDay);
+ calendar.set(Calendar.HOUR_OF_DAY, hour);
+ calendar.set(Calendar.MINUTE, minute);
+ calendar.set(Calendar.SECOND, second);
+ }
+
+ private void copyFieldsFromCalendar() {
+ year = calendar.get(Calendar.YEAR);
+ month = calendar.get(Calendar.MONTH);
+ monthDay = calendar.get(Calendar.DAY_OF_MONTH);
+ hour = calendar.get(Calendar.HOUR_OF_DAY);
+ minute = calendar.get(Calendar.MINUTE);
+ second = calendar.get(Calendar.SECOND);
+
+ // Calendar uses Sunday == 1. Android Time uses Sunday = 0.
+ weekDay = calendar.get(Calendar.DAY_OF_WEEK) - 1;
+ // Calendar enumerates from 1, Android Time enumerates from 0.
+ yearDay = calendar.get(Calendar.DAY_OF_YEAR) - 1;
+ }
+
+ /**
+ * Find the transition in the {@code timezone} in effect at {@code timeSeconds}.
+ *
+ * <p>Returns an index in the range -1..timeZone.mTransitions.length - 1. -1 is used to
+ * indicate the time is before the first transition. Other values are an index into
+ * timeZone.mTransitions.
+ */
+ private static int findTransitionIndex(ZoneInfo timeZone, int timeSeconds) {
+ int matchingRawTransition = Arrays.binarySearch(timeZone.mTransitions, timeSeconds);
+ if (matchingRawTransition < 0) {
+ matchingRawTransition = ~matchingRawTransition - 1;
+ }
+ return matchingRawTransition;
+ }
+ }
+
+ /**
+ * A wall-time representation of a timezone offset interval.
+ *
+ * <p>Wall-time means "as it would appear locally in the timezone in which it applies".
+ * For example in 2007:
+ * PST was a -8:00 offset that ran until Mar 11, 2:00 AM.
+ * PDT was a -7:00 offset and ran from Mar 11, 3:00 AM to Nov 4, 2:00 AM.
+ * PST was a -8:00 offset and ran from Nov 4, 1:00 AM.
+ * Crucially this means that there was a "gap" after PST when PDT started, and an overlap when
+ * PDT ended and PST began.
+ *
+ * <p>For convenience all wall-time values are represented as the number of seconds since the
+ * beginning of the Unix epoch <em>in UTC</em>. To convert from a wall-time to the actual time
+ * in the offset it is necessary to <em>subtract</em> the {@code totalOffsetSeconds}.
+ * For example: If the offset in PST is -07:00 hours, then:
+ * timeInPstSeconds = wallTimeUtcSeconds - offsetSeconds
+ * i.e. 13:00 UTC - (-07:00) = 20:00 UTC = 13:00 PST
+ */
+ static class OffsetInterval {
+
+ private final int startWallTimeSeconds;
+ private final int endWallTimeSeconds;
+ private final int isDst;
+ private final int totalOffsetSeconds;
+
+ /**
+ * Creates an {@link OffsetInterval}.
+ *
+ * <p>If {@code transitionIndex} is -1, the transition is synthesized to be a non-DST offset
+ * that runs from the beginning of time until the first transition in {@code timeZone} and
+ * has an offset of {@code timezone.mRawOffset}. If {@code transitionIndex} is the last
+ * transition that transition is considered to run until the end of representable time.
+ * Otherwise, the information is extracted from {@code timeZone.mTransitions},
+ * {@code timeZone.mOffsets} an {@code timeZone.mIsDsts}.
+ */
+ public static OffsetInterval create(ZoneInfo timeZone, int transitionIndex)
+ throws CheckedArithmeticException {
+
+ if (transitionIndex < -1 || transitionIndex >= timeZone.mTransitions.length) {
+ return null;
+ }
+
+ int rawOffsetSeconds = timeZone.mRawOffset / 1000;
+ if (transitionIndex == -1) {
+ int endWallTimeSeconds = checkedAdd(timeZone.mTransitions[0], rawOffsetSeconds);
+ return new OffsetInterval(Integer.MIN_VALUE, endWallTimeSeconds, 0 /* isDst */,
+ rawOffsetSeconds);
+ }
+
+ byte type = timeZone.mTypes[transitionIndex];
+ int totalOffsetSeconds = timeZone.mOffsets[type] + rawOffsetSeconds;
+ int endWallTimeSeconds;
+ if (transitionIndex == timeZone.mTransitions.length - 1) {
+ // If this is the last transition, make up the end time.
+ endWallTimeSeconds = Integer.MAX_VALUE;
+ } else {
+ endWallTimeSeconds = checkedAdd(timeZone.mTransitions[transitionIndex + 1],
+ totalOffsetSeconds);
+ }
+ int isDst = timeZone.mIsDsts[type];
+ int startWallTimeSeconds =
+ checkedAdd(timeZone.mTransitions[transitionIndex], totalOffsetSeconds);
+ return new OffsetInterval(
+ startWallTimeSeconds, endWallTimeSeconds, isDst, totalOffsetSeconds);
+ }
+
+ private OffsetInterval(int startWallTimeSeconds, int endWallTimeSeconds, int isDst,
+ int totalOffsetSeconds) {
+ this.startWallTimeSeconds = startWallTimeSeconds;
+ this.endWallTimeSeconds = endWallTimeSeconds;
+ this.isDst = isDst;
+ this.totalOffsetSeconds = totalOffsetSeconds;
+ }
+
+ public boolean containsWallTime(long wallTimeSeconds) {
+ return wallTimeSeconds >= startWallTimeSeconds && wallTimeSeconds < endWallTimeSeconds;
+ }
+
+ public int getIsDst() {
+ return isDst;
+ }
+
+ public int getTotalOffsetSeconds() {
+ return totalOffsetSeconds;
+ }
+
+ public long getEndWallTimeSeconds() {
+ return endWallTimeSeconds;
+ }
+
+ public long getStartWallTimeSeconds() {
+ return startWallTimeSeconds;
+ }
+ }
+
+ /**
+ * An exception used to indicate an arithmetic overflow or underflow.
+ */
+ private static class CheckedArithmeticException extends Exception {
+ }
+
+ /**
+ * Calculate (a + b).
+ *
+ * @throws CheckedArithmeticException if overflow or underflow occurs
+ */
+ private static int checkedAdd(int a, int b) throws CheckedArithmeticException {
+ // Adapted from Guava IntMath.checkedAdd();
+ long result = (long) a + b;
+ if (result != (int) result) {
+ throw new CheckedArithmeticException();
+ }
+ return (int) result;
+ }
+
+ /**
+ * Calculate (a - b).
+ *
+ * @throws CheckedArithmeticException if overflow or underflow occurs
+ */
+ private static int checkedSubtract(int a, int b) throws CheckedArithmeticException {
+ // Adapted from Guava IntMath.checkedSubtract();
+ long result = (long) a - b;
+ if (result != (int) result) {
+ throw new CheckedArithmeticException();
+ }
+ return (int) result;
+ }
}
diff --git a/luni/src/main/java/libcore/util/ZoneInfoDB.java b/luni/src/main/java/libcore/util/ZoneInfoDB.java
index 74947a6..07aaf04 100644
--- a/luni/src/main/java/libcore/util/ZoneInfoDB.java
+++ b/luni/src/main/java/libcore/util/ZoneInfoDB.java
@@ -229,9 +229,9 @@ public final class ZoneInfoDB {
}
public ZoneInfo makeTimeZone(String id) throws IOException {
- ZoneInfo zoneInfo = cache.get(id);
- // The object from the cache is cloned because TimeZone / ZoneInfo are mutable.
- return zoneInfo == null ? null : (ZoneInfo) zoneInfo.clone();
+ ZoneInfo zoneInfo = cache.get(id);
+ // The object from the cache is cloned because TimeZone / ZoneInfo are mutable.
+ return zoneInfo == null ? null : (ZoneInfo) zoneInfo.clone();
}
}
diff --git a/luni/src/main/native/libcore_icu_ICU.cpp b/luni/src/main/native/libcore_icu_ICU.cpp
index 163d19c..733bf38 100644
--- a/luni/src/main/native/libcore_icu_ICU.cpp
+++ b/luni/src/main/native/libcore_icu_ICU.cpp
@@ -116,56 +116,6 @@ static jstring ICU_getScript(JNIEnv* env, jclass, jstring javaLocaleName) {
return env->NewStringUTF(icuLocale.locale().getScript());
}
-static jstring ICU_localeForLanguageTag(JNIEnv* env, jclass, jstring languageTag, jboolean strict) {
- ScopedUtfChars languageTagChars(env, languageTag);
- if (languageTagChars.c_str() == NULL) {
- return NULL;
- }
-
- // Naively assume that in the average case, the size of
- // the normalized language tag will be very nearly the same as the
- // size of the input. This is generally true for language
- // tags that are "simple" language-region-variant combinations
- // that don't contain any grandfathered tags.
- const size_t initialBufferSize = languageTagChars.size() + 32;
- std::vector<char> buffer(initialBufferSize);
- int32_t parsedLength = 0;
-
- UErrorCode status = U_ZERO_ERROR;
- size_t outputLength = uloc_forLanguageTag(languageTagChars.c_str(), &buffer[0],
- buffer.size(), &parsedLength, &status);
- // Note that we always allocate 1 char more than ICU asks us for,
- // so that we can cleanly assert that it didn't overflow after the
- // second call to uloc_forLanguageTag.
- if (status == U_STRING_NOT_TERMINATED_WARNING) {
- const size_t unterminated_size = buffer.size();
- buffer.resize(unterminated_size + 1);
- buffer[unterminated_size] = '\0';
- } else if (status == U_BUFFER_OVERFLOW_ERROR) {
- buffer.resize(outputLength + 1);
- status = U_ZERO_ERROR;
- outputLength = uloc_forLanguageTag(languageTagChars.c_str(), &buffer[0], buffer.size(),
- &parsedLength, &status);
- }
-
- if (U_FAILURE(status) || outputLength >= buffer.size()) {
- return NULL;
- }
-
- // By default, ICU will ignore all subtags starting at the first unparseable
- // or invalid subtag. Our "strict" mode is specified to throw an error if
- // that happens.
- //
- // NOTE: The cast is safe because parsedLength can never be negative thanks
- // to the check above. ICU does not document any negative return values for
- // that field, but check for it anyway.
- if ((strict == JNI_TRUE) && (static_cast<uint32_t>(parsedLength) != languageTagChars.size())) {
- return NULL;
- }
-
- return env->NewStringUTF(&buffer[0]);
-}
-
static jint ICU_getCurrencyFractionDigits(JNIEnv* env, jclass, jstring javaCurrencyCode) {
ScopedJavaUnicodeString currencyCode(env, javaCurrencyCode);
if (!currencyCode.valid()) {
@@ -820,7 +770,6 @@ static JNINativeMethod gMethods[] = {
NATIVE_METHOD(ICU, getIcuVersion, "()Ljava/lang/String;"),
NATIVE_METHOD(ICU, getScript, "(Ljava/lang/String;)Ljava/lang/String;"),
NATIVE_METHOD(ICU, getUnicodeVersion, "()Ljava/lang/String;"),
- NATIVE_METHOD(ICU, localeForLanguageTag, "(Ljava/lang/String;Z)Ljava/lang/String;"),
NATIVE_METHOD(ICU, initLocaleDataNative, "(Ljava/lang/String;Llibcore/icu/LocaleData;)Z"),
NATIVE_METHOD(ICU, setDefaultLocale, "(Ljava/lang/String;)V"),
NATIVE_METHOD(ICU, toLowerCase, "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"),
diff --git a/luni/src/test/java/libcore/icu/ICUTest.java b/luni/src/test/java/libcore/icu/ICUTest.java
index 3fa1f46..525d372 100644
--- a/luni/src/test/java/libcore/icu/ICUTest.java
+++ b/luni/src/test/java/libcore/icu/ICUTest.java
@@ -198,12 +198,6 @@ public class ICUTest extends junit.framework.TestCase {
Collator.getInstance(sr_Latn_BA);
Collator.getInstance(sr_Latn_ME);
- // TODO: This needs to be fixed. We shouldn't output attribute key
- // expansions in the language tag or the toString output. The tests
- // will fail with something like:
- //
- // expected:<de-u-co[-phonebk-kf-upper-kn]> but was:
- // <de-u-co[lcasefirst-upper-collation-phonebook-colnumeric-yes]>
Locale l = Locale.forLanguageTag("de-u-co-phonebk-kf-upper-kn");
assertEquals("de__#u-co-phonebk-kf-upper-kn", l.toString());
assertEquals("de-u-co-phonebk-kf-upper-kn", l.toLanguageTag());
diff --git a/luni/src/test/java/libcore/java/nio/BufferTest.java b/luni/src/test/java/libcore/java/nio/BufferTest.java
index 613c6fa..c936cdf 100644
--- a/luni/src/test/java/libcore/java/nio/BufferTest.java
+++ b/luni/src/test/java/libcore/java/nio/BufferTest.java
@@ -20,6 +20,8 @@ import junit.framework.TestCase;
import java.io.File;
import java.io.RandomAccessFile;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.nio.Buffer;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
@@ -600,17 +602,31 @@ public class BufferTest extends TestCase {
assertTrue(b.isDirect());
// Check the buffer has an array of the right size.
assertTrue(b.hasArray());
- assertEquals(0, b.arrayOffset());
byte[] array = b.array();
- assertEquals(10, array.length);
+ assertTrue(array.length >= b.capacity());
+ assertEquals(10, b.capacity());
// Check that writes to the array show up in the buffer.
assertEquals(0, b.get(0));
- array[0] = 1;
+ array[b.arrayOffset()] = 1;
assertEquals(1, b.get(0));
// Check that writes to the buffer show up in the array.
- assertEquals(1, array[0]);
+ assertEquals(1, array[b.arrayOffset()]);
b.put(0, (byte) 0);
- assertEquals(0, array[0]);
+ assertEquals(0, array[b.arrayOffset()]);
+ }
+
+ // Test that direct byte buffers are 8 byte aligned.
+ // http://b/16449607
+ public void testDirectByteBufferAlignment() throws Exception {
+ ByteBuffer b = ByteBuffer.allocateDirect(10);
+ Field addressField = Buffer.class.getDeclaredField("effectiveDirectAddress");
+ assertTrue(addressField != null);
+ addressField.setAccessible(true);
+ long address = addressField.getLong(b);
+ // Check that the address field is aligned by 8.
+ // Normally reading this field happens in native code by calling
+ // GetDirectBufferAddress.
+ assertEquals(0, address % 8);
}
public void testSliceOffset() throws Exception {
@@ -618,14 +634,12 @@ public class BufferTest extends TestCase {
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.get();
ByteBuffer slice = buffer.slice();
- assertEquals(0, buffer.arrayOffset());
- assertEquals(1, slice.arrayOffset());
+ assertEquals(buffer.arrayOffset() + 1, slice.arrayOffset());
ByteBuffer directBuffer = ByteBuffer.allocateDirect(10);
directBuffer.get();
ByteBuffer directSlice = directBuffer.slice();
- assertEquals(0, directBuffer.arrayOffset());
- assertEquals(1, directSlice.arrayOffset());
+ assertEquals(directBuffer.arrayOffset() + 1, directSlice.arrayOffset());
}
// http://code.google.com/p/android/issues/detail?id=16184
diff --git a/luni/src/test/java/libcore/java/util/LocaleTest.java b/luni/src/test/java/libcore/java/util/LocaleTest.java
index 94bf363..23a4f28 100644
--- a/luni/src/test/java/libcore/java/util/LocaleTest.java
+++ b/luni/src/test/java/libcore/java/util/LocaleTest.java
@@ -541,13 +541,6 @@ public class LocaleTest extends junit.framework.TestCase {
assertEquals("eng", l.getLanguage());
assertEquals("419", l.getCountry());
- // IND is an invalid region code so ICU helpfully tries to parse it as
- // a 3 letter language code, even if it isn't a valid ISO-639-3 code
- // either.
- l = fromLanguageTag("en-USB", useBuilder);
- assertEquals("usb", l.getLanguage());
- assertEquals("", l.getCountry());
-
// Script tags shouldn't be mis-recognized as regions.
l = fromLanguageTag("en-Latn", useBuilder);
assertEquals("en", l.getLanguage());
@@ -612,16 +605,24 @@ public class LocaleTest extends junit.framework.TestCase {
} catch (IllformedLocaleException expected) {
}
- // Ill-formed extension with long subtag.
+ // Two extension keys in a row (i.e, another case of an ill-formed
+ // empty exception).
try {
- fromLanguageTag("en-f-fooobaaaz", true);
+ fromLanguageTag("en-f-g-fo-baar", true);
fail();
} catch (IllformedLocaleException expected) {
}
- // Ill-formed extension key.
+ // Dangling empty key after a well formed extension.
try {
- fromLanguageTag("en-9-baa", true);
+ fromLanguageTag("en-f-fo-baar-g", true);
+ fail();
+ } catch (IllformedLocaleException expected) {
+ }
+
+ // Ill-formed extension with long subtag.
+ try {
+ fromLanguageTag("en-f-fooobaaaz", true);
fail();
} catch (IllformedLocaleException expected) {
}
@@ -700,7 +701,7 @@ public class LocaleTest extends junit.framework.TestCase {
assertEquals("en", l.getLanguage());
assertEquals("Latn", l.getScript());
assertEquals("GB", l.getCountry());
- assertEquals("FOOOO_POSIX", l.getVariant());
+ assertEquals("FOOOO", l.getVariant());
assertEquals("fo-bar-baaz", l.getExtension('g'));
// Multiple extensions
@@ -708,7 +709,7 @@ public class LocaleTest extends junit.framework.TestCase {
assertEquals("en", l.getLanguage());
assertEquals("Latn", l.getScript());
assertEquals("US", l.getCountry());
- assertEquals("FOOOO_POSIX", l.getVariant());
+ assertEquals("FOOOO", l.getVariant());
assertEquals("fo-bar", l.getExtension('g'));
assertEquals("go-gaz", l.getExtension('h'));
@@ -738,6 +739,13 @@ public class LocaleTest extends junit.framework.TestCase {
assertEquals("", l.getScript());
assertEquals("", l.getCountry());
assertEquals("fo", l.getExtension('f'));
+
+ l = fromLanguageTag("en-f-fo-x-a-b-c-d-e-fo", useBuilder);
+ assertEquals("en", l.getLanguage());
+ assertEquals("", l.getScript());
+ assertEquals("", l.getCountry());
+ assertEquals("fo", l.getExtension('f'));
+ assertEquals("a-b-c-d-e-fo", l.getExtension('x'));
}
public void test_forLanguageTag() {
@@ -782,7 +790,7 @@ public class LocaleTest extends junit.framework.TestCase {
public void test_setLanguageTag_malformedTags() {
Locale l = fromLanguageTag("a", false);
- assertEquals("", l.getLanguage());
+ assertEquals("und", l.getLanguage());
assertEquals("", l.getCountry());
assertEquals("", l.getVariant());
assertEquals("", l.getScript());
@@ -1113,4 +1121,22 @@ public class LocaleTest extends junit.framework.TestCase {
.build();
assertEquals("en-US-POSIX", posix.toLanguageTag());
}
+
+ public void test_forLanguageTag_grandFatheredLocale() {
+ // Regular grandfathered locale.
+ Locale gaulish = Locale.forLanguageTag("cel-gaulish");
+ assertEquals("xtg", gaulish.getLanguage());
+ assertEquals("cel-gaulish", gaulish.getExtension(Locale.PRIVATE_USE_EXTENSION));
+ assertEquals("", gaulish.getCountry());
+ assertEquals("", gaulish.getScript());
+ assertEquals("", gaulish.getVariant());
+
+ // Irregular grandfathered locale.
+ Locale enochian = Locale.forLanguageTag("i-enochian");
+ assertEquals("und", enochian.getLanguage());
+ assertEquals("i-enochian", enochian.getExtension(Locale.PRIVATE_USE_EXTENSION));
+ assertEquals("", enochian.getCountry());
+ assertEquals("", enochian.getScript());
+ assertEquals("", enochian.getVariant());
+ }
}