diff options
author | Victor Chang <vichang@google.com> | 2021-01-05 16:24:59 +0000 |
---|---|---|
committer | Victor Chang <vichang@google.com> | 2021-01-12 18:24:25 +0000 |
commit | 2c09218f13106837aa39cd597fa4f5b6d1c3597c (patch) | |
tree | f9c6cf03a5dd2c17e9815e2797f83253ad2e7c2d /android_icu4j | |
parent | 0e3f9a9e8214629bf74a6815efc0a02505493137 (diff) | |
download | icu-2c09218f13106837aa39cd597fa4f5b6d1c3597c.tar.gz |
Integrate ICU4J 67.1 with Android patches into android_icu4j/
android_icu4j/ files updated using:
tools/srcgen/generate_android_icu4j.sh
Change-Id: Ia1038271820ee3c5da364d6c5ff688261ba5a378
Diffstat (limited to 'android_icu4j')
226 files changed, 4973 insertions, 1974 deletions
diff --git a/android_icu4j/src/main/java/android/icu/impl/CurrencyData.java b/android_icu4j/src/main/java/android/icu/impl/CurrencyData.java index 608e178aa..ffb0a8921 100644 --- a/android_icu4j/src/main/java/android/icu/impl/CurrencyData.java +++ b/android_icu4j/src/main/java/android/icu/impl/CurrencyData.java @@ -181,6 +181,16 @@ public class CurrencyData { } @Override + public String getFormalSymbol(String isoCode) { + return fallback ? isoCode : null; + } + + @Override + public String getVariantSymbol(String isoCode) { + return fallback ? isoCode : null; + } + + @Override public Map<String, String> symbolMap() { return Collections.emptyMap(); } diff --git a/android_icu4j/src/main/java/android/icu/impl/FormattedStringBuilder.java b/android_icu4j/src/main/java/android/icu/impl/FormattedStringBuilder.java index a6b48eb4b..0b793f3a9 100644 --- a/android_icu4j/src/main/java/android/icu/impl/FormattedStringBuilder.java +++ b/android_icu4j/src/main/java/android/icu/impl/FormattedStringBuilder.java @@ -3,7 +3,6 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package android.icu.impl; -import java.text.Format.Field; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -26,23 +25,29 @@ import android.icu.text.NumberFormat; * @author sffc (Shane Carr) * @hide Only a subset of ICU is exposed in Android */ -public class FormattedStringBuilder implements CharSequence { +public class FormattedStringBuilder implements CharSequence, Appendable { /** A constant, empty FormattedStringBuilder. Do NOT call mutative operations on this. */ public static final FormattedStringBuilder EMPTY = new FormattedStringBuilder(); char[] chars; - Field[] fields; + Object[] fields; int zero; int length; + /** Number of characters from the end where .append() operations insert. */ + int appendOffset = 0; + + /** Field applied when Appendable methods are used. */ + Object appendableField = null; + public FormattedStringBuilder() { this(40); } public FormattedStringBuilder(int capacity) { chars = new char[capacity]; - fields = new Field[capacity]; + fields = new Object[capacity]; zero = capacity / 2; length = 0; } @@ -74,7 +79,7 @@ public class FormattedStringBuilder implements CharSequence { return chars[zero + index]; } - public Field fieldAt(int index) { + public Object fieldAt(int index) { assert index >= 0; assert index < length; return fields[zero + index]; @@ -108,11 +113,20 @@ public class FormattedStringBuilder implements CharSequence { return this; } - public int appendChar16(char codeUnit, Field field) { - return insertChar16(length, codeUnit, field); + /** + * Sets the index at which append operations insert. Defaults to the end. + * + * @param index The index at which append operations should insert. + */ + public void setAppendIndex(int index) { + appendOffset = length - index; + } + + public int appendChar16(char codeUnit, Object field) { + return insertChar16(length - appendOffset, codeUnit, field); } - public int insertChar16(int index, char codeUnit, Field field) { + public int insertChar16(int index, char codeUnit, Object field) { int count = 1; int position = prepareForInsert(index, count); chars[position] = codeUnit; @@ -125,8 +139,8 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise. */ - public int appendCodePoint(int codePoint, Field field) { - return insertCodePoint(length, codePoint, field); + public int appendCodePoint(int codePoint, Object field) { + return insertCodePoint(length - appendOffset, codePoint, field); } /** @@ -134,7 +148,7 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise. */ - public int insertCodePoint(int index, int codePoint, Field field) { + public int insertCodePoint(int index, int codePoint, Object field) { int count = Character.charCount(codePoint); int position = prepareForInsert(index, count); Character.toChars(codePoint, chars, position); @@ -149,8 +163,8 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added, which is the length of CharSequence. */ - public int append(CharSequence sequence, Field field) { - return insert(length, sequence, field); + public int append(CharSequence sequence, Object field) { + return insert(length - appendOffset, sequence, field); } /** @@ -158,7 +172,7 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added, which is the length of CharSequence. */ - public int insert(int index, CharSequence sequence, Field field) { + public int insert(int index, CharSequence sequence, Object field) { if (sequence.length() == 0) { // Nothing to insert. return 0; @@ -177,7 +191,7 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added, which is the length of CharSequence. */ - public int insert(int index, CharSequence sequence, int start, int end, Field field) { + public int insert(int index, CharSequence sequence, int start, int end, Object field) { int count = end - start; int position = prepareForInsert(index, count); for (int i = 0; i < count; i++) { @@ -201,7 +215,7 @@ public class FormattedStringBuilder implements CharSequence { CharSequence sequence, int startOther, int endOther, - Field field) { + Object field) { int thisLength = endThis - startThis; int otherLength = endOther - startOther; int count = otherLength - thisLength; @@ -226,8 +240,8 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added, which is the length of the char array. */ - public int append(char[] chars, Field[] fields) { - return insert(length, chars, fields); + public int append(char[] chars, Object[] fields) { + return insert(length - appendOffset, chars, fields); } /** @@ -236,7 +250,7 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added, which is the length of the char array. */ - public int insert(int index, char[] chars, Field[] fields) { + public int insert(int index, char[] chars, Object[] fields) { assert fields == null || chars.length == fields.length; int count = chars.length; if (count == 0) @@ -255,7 +269,7 @@ public class FormattedStringBuilder implements CharSequence { * @return The number of chars added, which is the length of the other {@link FormattedStringBuilder}. */ public int append(FormattedStringBuilder other) { - return insert(length, other); + return insert(length - appendOffset, other); } /** @@ -290,6 +304,9 @@ public class FormattedStringBuilder implements CharSequence { * @return The position in the char array to insert the chars. */ private int prepareForInsert(int index, int count) { + if (index == -1) { + index = length; + } if (index == 0 && zero - count >= 0) { // Append to start zero -= count; @@ -311,13 +328,13 @@ public class FormattedStringBuilder implements CharSequence { int oldCapacity = getCapacity(); int oldZero = zero; char[] oldChars = chars; - Field[] oldFields = fields; + Object[] oldFields = fields; if (length + count > oldCapacity) { int newCapacity = (length + count) * 2; int newZero = newCapacity / 2 - (length + count) / 2; char[] newChars = new char[newCapacity]; - Field[] newFields = new Field[newCapacity]; + Object[] newFields = new Object[newCapacity]; // First copy the prefix and then the suffix, leaving room for the new chars that the // caller wants to insert. @@ -410,7 +427,7 @@ public class FormattedStringBuilder implements CharSequence { return new String(chars, zero, length); } - private static final Map<Field, Character> fieldToDebugChar = new HashMap<>(); + private static final Map<Object, Character> fieldToDebugChar = new HashMap<>(); static { fieldToDebugChar.put(NumberFormat.Field.SIGN, '-'); @@ -461,17 +478,59 @@ public class FormattedStringBuilder implements CharSequence { } /** @return A new array containing the field values of this string builder. */ - public Field[] toFieldArray() { + public Object[] toFieldArray() { return Arrays.copyOfRange(fields, zero, zero + length); } /** + * Call this method before using any of the Appendable overrides. + * + * @param field The field used when inserting strings. + */ + public void setAppendableField(Object field) { + appendableField = field; + } + + /** + * This method is provided for Java Appendable compatibility. In most cases, please use the append methods that take + * a Field parameter. If you do use this method, you must call {@link #setAppendableField} first. + */ + @Override + public Appendable append(CharSequence csq) { + assert appendableField != null; + insert(length - appendOffset, csq, appendableField); + return this; + } + + /** + * This method is provided for Java Appendable compatibility. In most cases, please use the append methods that take + * a Field parameter. If you do use this method, you must call {@link #setAppendableField} first. + */ + @Override + public Appendable append(CharSequence csq, int start, int end) { + assert appendableField != null; + insert(length - appendOffset, csq, start, end, appendableField); + return this; + } + + /** + * This method is provided for Java Appendable compatibility. In most cases, please use the append methods that take + * a Field parameter. If you do use this method, you must call {@link #setAppendableField} first. + */ + @Override + public Appendable append(char c) { + assert appendableField != null; + insertChar16(length - appendOffset, c, appendableField); + return this; + } + + /** * @return Whether the contents and field values of this string builder are equal to the given chars * and fields. * @see #toCharArray * @see #toFieldArray */ - public boolean contentEquals(char[] chars, Field[] fields) { + public boolean contentEquals(char[] chars, Object[] fields) { if (chars.length != length) return false; if (fields.length != length) diff --git a/android_icu4j/src/main/java/android/icu/impl/FormattedValueStringBuilderImpl.java b/android_icu4j/src/main/java/android/icu/impl/FormattedValueStringBuilderImpl.java index 5b316cda8..4cce71c60 100644 --- a/android_icu4j/src/main/java/android/icu/impl/FormattedValueStringBuilderImpl.java +++ b/android_icu4j/src/main/java/android/icu/impl/FormattedValueStringBuilderImpl.java @@ -9,7 +9,9 @@ import java.text.FieldPosition; import java.text.Format.Field; import android.icu.text.ConstrainedFieldPosition; +import android.icu.text.ListFormatter; import android.icu.text.NumberFormat; +import android.icu.text.UFormat; import android.icu.text.UnicodeSet; /** @@ -26,6 +28,34 @@ import android.icu.text.UnicodeSet; */ public class FormattedValueStringBuilderImpl { + /** + * Placeholder field used for calculating spans. + * Does not currently support nested fields beyond one level. + * @hide Only a subset of ICU is exposed in Android + */ + public static class SpanFieldPlaceholder { + public UFormat.SpanField spanField; + public Field normalField; + public Object value; + } + + /** + * Finds the index at which a span field begins. + * + * @param value The value of the span field to search for. + * @return The index, or -1 if not found. + */ + public static int findSpan(FormattedStringBuilder self, Object value) { + for (int i = self.zero; i < self.zero + self.length; i++) { + if (!(self.fields[i] instanceof SpanFieldPlaceholder)) { + continue; + } + if (((SpanFieldPlaceholder) self.fields[i]).value.equals(value)) { + return i - self.zero; + } + } + return -1; + } public static boolean nextFieldPosition(FormattedStringBuilder self, FieldPosition fp) { java.text.Format.Field rawField = fp.getFieldAttribute(); @@ -80,7 +110,11 @@ public class FormattedValueStringBuilderImpl { AttributedString as = new AttributedString(self.toString()); while (nextPosition(self, cfpos, numericField)) { // Backwards compatibility: field value = field - as.addAttribute(cfpos.getField(), cfpos.getField(), cfpos.getStart(), cfpos.getLimit()); + Object value = cfpos.getFieldValue(); + if (value == null) { + value = cfpos.getField(); + } + as.addAttribute(cfpos.getField(), value, cfpos.getStart(), cfpos.getLimit()); } return as.getIterator(); } @@ -104,15 +138,21 @@ public class FormattedValueStringBuilderImpl { */ public static boolean nextPosition(FormattedStringBuilder self, ConstrainedFieldPosition cfpos, Field numericField) { int fieldStart = -1; - Field currField = null; + Object currField = null; for (int i = self.zero + cfpos.getLimit(); i <= self.zero + self.length; i++) { - Field _field = (i < self.zero + self.length) ? self.fields[i] : NullField.END; + Object _field = (i < self.zero + self.length) ? self.fields[i] : NullField.END; // Case 1: currently scanning a field. if (currField != null) { if (currField != _field) { int end = i - self.zero; + // Handle span fields; don't trim them + if (currField instanceof SpanFieldPlaceholder) { + boolean handleResult = handleSpan(currField, cfpos, fieldStart, end); + assert handleResult; + return true; + } // Grouping separators can be whitespace; don't throw them out! - if (currField != NumberFormat.Field.GROUPING_SEPARATOR) { + if (isTrimmable(currField)) { end = trimBack(self, end); } if (end <= fieldStart) { @@ -123,10 +163,10 @@ public class FormattedValueStringBuilderImpl { continue; } int start = fieldStart; - if (currField != NumberFormat.Field.GROUPING_SEPARATOR) { + if (isTrimmable(currField)) { start = trimFront(self, start); } - cfpos.setState(currField, null, start, end); + cfpos.setState((Field) currField, null, start, end); return true; } continue; @@ -156,6 +196,15 @@ public class FormattedValueStringBuilderImpl { cfpos.setState(numericField, null, j - self.zero + 1, i - self.zero); return true; } + // Special case: emit normalField if we are pointing at the end of spanField. + if (i > self.zero + && self.fields[i-1] instanceof SpanFieldPlaceholder) { + int j = i - 1; + for (; j >= self.zero && self.fields[j] == self.fields[i-1]; j--) {} + if (handleSpan(self.fields[i-1], cfpos, j - self.zero + 1, i - self.zero)) { + return true; + } + } // Special case: skip over INTEGER; will be coalesced later. if (_field == NumberFormat.Field.INTEGER) { _field = null; @@ -165,7 +214,16 @@ public class FormattedValueStringBuilderImpl { continue; } // Case 3: check for field starting at this position - if (cfpos.matchesField(_field, null)) { + // Case 3a: SpanField placeholder + if (_field instanceof SpanFieldPlaceholder) { + SpanFieldPlaceholder ph = (SpanFieldPlaceholder) _field; + if (cfpos.matchesField(ph.normalField, null) || cfpos.matchesField(ph.spanField, ph.value)) { + fieldStart = i - self.zero; + currField = _field; + } + } + // Case 3b: No SpanField + else if (cfpos.matchesField((Field) _field, null)) { fieldStart = i - self.zero; currField = _field; } @@ -175,14 +233,19 @@ public class FormattedValueStringBuilderImpl { return false; } - private static boolean isIntOrGroup(Field field) { + private static boolean isIntOrGroup(Object field) { return field == NumberFormat.Field.INTEGER || field == NumberFormat.Field.GROUPING_SEPARATOR; } - private static boolean isNumericField(Field field) { + private static boolean isNumericField(Object field) { return field == null || NumberFormat.Field.class.isAssignableFrom(field.getClass()); } + private static boolean isTrimmable(Object field) { + return field != NumberFormat.Field.GROUPING_SEPARATOR + && !(field instanceof ListFormatter.Field); + } + private static int trimBack(FormattedStringBuilder self, int limit) { return StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES) .spanBack(self, limit, UnicodeSet.SpanCondition.CONTAINED); @@ -192,4 +255,19 @@ public class FormattedValueStringBuilderImpl { return StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES) .span(self, start, UnicodeSet.SpanCondition.CONTAINED); } + + private static boolean handleSpan(Object field, ConstrainedFieldPosition cfpos, int start, int limit) { + SpanFieldPlaceholder ph = (SpanFieldPlaceholder) field; + if (cfpos.matchesField(ph.spanField, ph.value) + && cfpos.getLimit() < limit) { + cfpos.setState(ph.spanField, ph.value, start, limit); + return true; + } + if (cfpos.matchesField(ph.normalField, null) + && (cfpos.getLimit() < limit || cfpos.getField() != ph.normalField)) { + cfpos.setState(ph.normalField, null, start, limit); + return true; + } + return false; + } } diff --git a/android_icu4j/src/main/java/android/icu/impl/ICUCurrencyDisplayInfoProvider.java b/android_icu4j/src/main/java/android/icu/impl/ICUCurrencyDisplayInfoProvider.java index b0f8dfc86..78fddf879 100644 --- a/android_icu4j/src/main/java/android/icu/impl/ICUCurrencyDisplayInfoProvider.java +++ b/android_icu4j/src/main/java/android/icu/impl/ICUCurrencyDisplayInfoProvider.java @@ -79,10 +79,10 @@ public class ICUCurrencyDisplayInfoProvider implements CurrencyDisplayInfoProvid private volatile FormattingData formattingDataCache = null; /** - * Single-item cache for getNarrowSymbol(). + * Single-item cache for variant symbols. * Holds data for only one currency. If another currency is requested, the old cache item is overwritten. */ - private volatile NarrowSymbol narrowSymbolCache = null; + private volatile VariantSymbol variantSymbolCache = null; /** * Single-item cache for getPluralName(). @@ -120,11 +120,15 @@ public class ICUCurrencyDisplayInfoProvider implements CurrencyDisplayInfoProvid FormattingData(String isoCode) { this.isoCode = isoCode; } } - static class NarrowSymbol { + static class VariantSymbol { final String isoCode; - String narrowSymbol = null; + final String variant; + String symbol = null; - NarrowSymbol(String isoCode) { this.isoCode = isoCode; } + VariantSymbol(String isoCode, String variant) { + this.isoCode = isoCode; + this.variant = variant; + } } static class ParsingData { @@ -171,13 +175,35 @@ public class ICUCurrencyDisplayInfoProvider implements CurrencyDisplayInfoProvid @Override public String getNarrowSymbol(String isoCode) { - NarrowSymbol narrowSymbol = fetchNarrowSymbol(isoCode); + VariantSymbol variantSymbol = fetchVariantSymbol(isoCode, "narrow"); - // Fall back to ISO Code - if (narrowSymbol.narrowSymbol == null && fallback) { + // Fall back to regular symbol + if (variantSymbol.symbol == null && fallback) { + return getSymbol(isoCode); + } + return variantSymbol.symbol; + } + + @Override + public String getFormalSymbol(String isoCode) { + VariantSymbol variantSymbol = fetchVariantSymbol(isoCode, "formal"); + + // Fall back to regular symbol + if (variantSymbol.symbol == null && fallback) { + return getSymbol(isoCode); + } + return variantSymbol.symbol; + } + + @Override + public String getVariantSymbol(String isoCode) { + VariantSymbol variantSymbol = fetchVariantSymbol(isoCode, "variant"); + + // Fall back to regular symbol + if (variantSymbol.symbol == null && fallback) { return getSymbol(isoCode); } - return narrowSymbol.narrowSymbol; + return variantSymbol.symbol; } @Override @@ -260,14 +286,14 @@ public class ICUCurrencyDisplayInfoProvider implements CurrencyDisplayInfoProvid return result; } - NarrowSymbol fetchNarrowSymbol(String isoCode) { - NarrowSymbol result = narrowSymbolCache; - if (result == null || !result.isoCode.equals(isoCode)) { - result = new NarrowSymbol(isoCode); - CurrencySink sink = new CurrencySink(!fallback, CurrencySink.EntrypointTable.CURRENCY_NARROW); - sink.narrowSymbol = result; - rb.getAllItemsWithFallbackNoFail("Currencies%narrow/" + isoCode, sink); - narrowSymbolCache = result; + VariantSymbol fetchVariantSymbol(String isoCode, String variant) { + VariantSymbol result = variantSymbolCache; + if (result == null || !result.isoCode.equals(isoCode) || !result.variant.equals(variant)) { + result = new VariantSymbol(isoCode, variant); + CurrencySink sink = new CurrencySink(!fallback, CurrencySink.EntrypointTable.CURRENCY_VARIANT); + sink.variantSymbol = result; + rb.getAllItemsWithFallbackNoFail("Currencies%" + variant + "/" + isoCode, sink); + variantSymbolCache = result; } return result; } @@ -335,7 +361,7 @@ public class ICUCurrencyDisplayInfoProvider implements CurrencyDisplayInfoProvid ParsingData parsingData = null; Map<String, String> unitPatterns = null; CurrencySpacingInfo spacingInfo = null; - NarrowSymbol narrowSymbol = null; + VariantSymbol variantSymbol = null; enum EntrypointTable { // For Parsing: @@ -344,7 +370,7 @@ public class ICUCurrencyDisplayInfoProvider implements CurrencyDisplayInfoProvid // For Formatting: CURRENCIES, CURRENCY_PLURALS, - CURRENCY_NARROW, + CURRENCY_VARIANT, CURRENCY_SPACING, CURRENCY_UNIT_PATTERNS } @@ -375,8 +401,8 @@ public class ICUCurrencyDisplayInfoProvider implements CurrencyDisplayInfoProvid case CURRENCY_PLURALS: consumeCurrencyPluralsEntry(key, value); break; - case CURRENCY_NARROW: - consumeCurrenciesNarrowEntry(key, value); + case CURRENCY_VARIANT: + consumeCurrenciesVariantEntry(key, value); break; case CURRENCY_SPACING: consumeCurrencySpacingTable(key, value); @@ -479,11 +505,11 @@ public class ICUCurrencyDisplayInfoProvider implements CurrencyDisplayInfoProvid * ... * } */ - void consumeCurrenciesNarrowEntry(UResource.Key key, UResource.Value value) { - assert narrowSymbol != null; + void consumeCurrenciesVariantEntry(UResource.Key key, UResource.Value value) { + assert variantSymbol != null; // No extra structure to traverse. - if (narrowSymbol.narrowSymbol == null) { - narrowSymbol.narrowSymbol = value.getString(); + if (variantSymbol.symbol == null) { + variantSymbol.symbol = value.getString(); } } diff --git a/android_icu4j/src/main/java/android/icu/impl/ICUService.java b/android_icu4j/src/main/java/android/icu/impl/ICUService.java index aa79df43e..850da1afd 100644 --- a/android_icu4j/src/main/java/android/icu/impl/ICUService.java +++ b/android_icu4j/src/main/java/android/icu/impl/ICUService.java @@ -597,15 +597,13 @@ public class ICUService extends ICUNotifier { Factory f = lIter.previous(); f.updateVisibleIDs(mutableMap); } - Map<String, Factory> result = Collections.unmodifiableMap(mutableMap); - this.idcache = result; - return result; + this.idcache = Collections.unmodifiableMap(mutableMap); } finally { factoryLock.releaseRead(); } } - return idcache; } + return idcache; } private Map<String, Factory> idcache; diff --git a/android_icu4j/src/main/java/android/icu/impl/PluralRulesLoader.java b/android_icu4j/src/main/java/android/icu/impl/PluralRulesLoader.java index 8e270fdf6..41401d4cb 100644 --- a/android_icu4j/src/main/java/android/icu/impl/PluralRulesLoader.java +++ b/android_icu4j/src/main/java/android/icu/impl/PluralRulesLoader.java @@ -13,6 +13,7 @@ import java.text.ParseException; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.Map; import java.util.MissingResourceException; import java.util.Set; @@ -49,12 +50,11 @@ public class PluralRulesLoader extends PluralRules.Factory { */ public ULocale[] getAvailableULocales() { Set<String> keys = getLocaleIdToRulesIdMap(PluralType.CARDINAL).keySet(); - ULocale[] locales = new ULocale[keys.size()]; - int n = 0; + Set<ULocale> locales = new LinkedHashSet<ULocale>(keys.size()); for (Iterator<String> iter = keys.iterator(); iter.hasNext();) { - locales[n++] = ULocale.createCanonical(iter.next()); + locales.add(ULocale.createCanonical(iter.next())); } - return locales; + return locales.toArray(new ULocale[0]); } /** @@ -501,4 +501,4 @@ public class PluralRulesLoader extends PluralRules.Factory { // now make whole thing immutable localeIdToPluralRanges = Collections.unmodifiableMap(tempLocaleIdToPluralRanges); } -}
\ No newline at end of file +} diff --git a/android_icu4j/src/main/java/android/icu/impl/SimpleFormatterImpl.java b/android_icu4j/src/main/java/android/icu/impl/SimpleFormatterImpl.java index ce8fe0fea..f25a0d980 100644 --- a/android_icu4j/src/main/java/android/icu/impl/SimpleFormatterImpl.java +++ b/android_icu4j/src/main/java/android/icu/impl/SimpleFormatterImpl.java @@ -9,6 +9,11 @@ */ package android.icu.impl; +import java.io.IOException; +import java.text.Format; + +import android.icu.util.ICUUncheckedIOException; + /** * Formats simple patterns like "{1} was born in {0}". * Internal version of {@link android.icu.text.SimpleFormatter} @@ -306,18 +311,125 @@ public final class SimpleFormatterImpl { return sb.toString(); } - /** Poor-man's iterator interface. See ICU-20406. - * @hide Only a subset of ICU is exposed in Android*/ - public static class Int64Iterator { + /** + * Returns the length of the pattern text with none of the arguments. + * @param compiledPattern Compiled form of a pattern string. + * @param codePoints true to count code points; false to count code units. + * @return The number of code points or code units. + */ + public static int getLength(String compiledPattern, boolean codePoints) { + int result = 0; + for (int i = 1; i < compiledPattern.length();) { + int segmentLength = compiledPattern.charAt(i++) - ARG_NUM_LIMIT; + if (segmentLength > 0) { + int limit = i + segmentLength; + if (codePoints) { + result += Character.codePointCount(compiledPattern, i, limit); + } else { + result += (limit - i); + } + i = limit; + } + } + return result; + } + + /** + * Returns the length in code units of the pattern text up until the first argument. + * @param compiledPattern Compiled form of a pattern string. + * @return The number of code units. + */ + public static int getPrefixLength(String compiledPattern) { + if (compiledPattern.length() == 1) { + return 0; + } else if (compiledPattern.charAt(0) == 0) { + return compiledPattern.length() - 2; + } else if (compiledPattern.charAt(1) <= ARG_NUM_LIMIT) { + return 0; + } else { + return compiledPattern.charAt(1) - ARG_NUM_LIMIT; + } + } + + /** + * Special case for using FormattedStringBuilder with patterns with 0 or 1 argument. + * + * With 1 argument, treat the current contents of the FormattedStringBuilder between + * start and end as the argument {0}. Insert the extra strings from compiledPattern + * to surround the argument in the output. + * + * With 0 arguments, overwrite the entire contents of the FormattedStringBuilder + * between start and end. + * + * @param compiledPattern Compiled form of a pattern string. + * @param field Field to use when adding chars to the output. + * @param start The start index of the argument already in the output string. + * @param end The end index of the argument already in the output string. + * @param output Destination for formatted output. + * @return Net number of characters added to the formatted string. + */ + public static int formatPrefixSuffix( + String compiledPattern, + Format.Field field, + int start, + int end, + FormattedStringBuilder output) { + int argLimit = getArgumentLimit(compiledPattern); + if (argLimit == 0) { + // No arguments in compiled pattern; overwrite the entire segment with our string. + return output.splice(start, end, compiledPattern, 2, compiledPattern.length(), field); + } else { + assert argLimit == 1; + int suffixOffset; + int length = 0; + if (compiledPattern.charAt(1) != '\u0000') { + int prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT; + length = output.insert(start, compiledPattern, 2, 2 + prefixLength, field); + suffixOffset = 3 + prefixLength; + } else { + suffixOffset = 2; + } + if (suffixOffset < compiledPattern.length()) { + int suffixLength = compiledPattern.charAt(suffixOffset) - ARG_NUM_LIMIT; + length += output.insert(end + length, compiledPattern, 1 + suffixOffset, + 1 + suffixOffset + suffixLength, field); + } + return length; + } + } + + /** Internal iterator interface for maximum efficiency. + * + * Usage boilerplate: + * + * <pre> + * long state = 0; + * while (true) { + * state = IterInternal.step(state, compiledPattern, output); + * if (state == IterInternal.DONE) { + * break; + * } + * int argIndex = IterInternal.getArgIndex(state); + * // Append the string corresponding to argIndex to output + * } + * </pre> + * @hide Only a subset of ICU is exposed in Android + * + */ + public static class IterInternal { public static final long DONE = -1; - public static long step(CharSequence compiledPattern, long state, StringBuffer output) { + public static long step(long state, CharSequence compiledPattern, Appendable output) { int i = (int) (state >>> 32); assert i < compiledPattern.length(); i++; while (i < compiledPattern.length() && compiledPattern.charAt(i) > ARG_NUM_LIMIT) { int limit = i + compiledPattern.charAt(i) + 1 - ARG_NUM_LIMIT; - output.append(compiledPattern, i + 1, limit); + try { + output.append(compiledPattern, i + 1, limit); + } catch (IOException e) { + throw new ICUUncheckedIOException(e); + } i = limit; } if (i == compiledPattern.length()) { diff --git a/android_icu4j/src/main/java/android/icu/impl/StringSegment.java b/android_icu4j/src/main/java/android/icu/impl/StringSegment.java index 41aa1349e..28cf0556a 100644 --- a/android_icu4j/src/main/java/android/icu/impl/StringSegment.java +++ b/android_icu4j/src/main/java/android/icu/impl/StringSegment.java @@ -223,8 +223,14 @@ public class StringSegment implements CharSequence { return Utility.charSequenceHashCode(this); } + /** Returns a string representation useful for debugging. */ @Override public String toString() { return str.substring(0, start) + "[" + str.substring(start, end) + "]" + str.substring(end); } + + /** Returns a String that is equivalent to the CharSequence representation. */ + public String asString() { + return str.substring(start, end); + } } diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/InternalLocaleBuilder.java b/android_icu4j/src/main/java/android/icu/impl/locale/InternalLocaleBuilder.java index d6f1b1a49..9eb165751 100644 --- a/android_icu4j/src/main/java/android/icu/impl/locale/InternalLocaleBuilder.java +++ b/android_icu4j/src/main/java/android/icu/impl/locale/InternalLocaleBuilder.java @@ -10,6 +10,7 @@ package android.icu.impl.locale; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -330,7 +331,8 @@ public final class InternalLocaleBuilder { _script = langtag.getScript(); _region = langtag.getRegion(); - List<String> bcpVariants = langtag.getVariants(); + ArrayList<String> bcpVariants = new ArrayList<String>(langtag.getVariants()); + Collections.sort(bcpVariants); if (bcpVariants.size() > 0) { StringBuilder var = new StringBuilder(bcpVariants.get(0)); for (int i = 1; i < bcpVariants.size(); i++) { diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/LSR.java b/android_icu4j/src/main/java/android/icu/impl/locale/LSR.java index d7db9a0f6..d49440934 100644 --- a/android_icu4j/src/main/java/android/icu/impl/locale/LSR.java +++ b/android_icu4j/src/main/java/android/icu/impl/locale/LSR.java @@ -11,6 +11,13 @@ import java.util.Objects; public final class LSR { public static final int REGION_INDEX_LIMIT = 1001 + 26 * 26; + public static final int EXPLICIT_LSR = 7; + public static final int EXPLICIT_LANGUAGE = 4; + public static final int EXPLICIT_SCRIPT = 2; + public static final int EXPLICIT_REGION = 1; + public static final int IMPLICIT_LSR = 0; + public static final int DONT_CARE_FLAGS = 0; + public static final boolean DEBUG_OUTPUT = false; public final String language; @@ -18,12 +25,14 @@ public final class LSR { public final String region; /** Index for region, negative if ill-formed. @see indexForRegion */ final int regionIndex; + public final int flags; - public LSR(String language, String script, String region) { + public LSR(String language, String script, String region, int flags) { this.language = language; this.script = script; this.region = region; regionIndex = indexForRegion(region); + this.flags = flags; } /** @@ -61,6 +70,13 @@ public final class LSR { } return result.toString(); } + + public boolean isEquivalentTo(LSR other) { + return language.equals(other.language) + && script.equals(other.script) + && region.equals(other.region); + } + @Override public boolean equals(Object obj) { LSR other; @@ -69,10 +85,12 @@ public final class LSR { && obj.getClass() == this.getClass() && language.equals((other = (LSR) obj).language) && script.equals(other.script) - && region.equals(other.region)); + && region.equals(other.region) + && flags == other.flags); } + @Override public int hashCode() { - return Objects.hash(language, script, region); + return Objects.hash(language, script, region, flags); } } diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/LanguageTag.java b/android_icu4j/src/main/java/android/icu/impl/locale/LanguageTag.java index 469eceee8..f1b1a9cf1 100644 --- a/android_icu4j/src/main/java/android/icu/impl/locale/LanguageTag.java +++ b/android_icu4j/src/main/java/android/icu/impl/locale/LanguageTag.java @@ -701,7 +701,21 @@ public class LanguageTag { } public static String canonicalizeExtension(String s) { - return AsciiUtil.toLowerString(s); + s = AsciiUtil.toLowerString(s); + int found; + while (s.endsWith("-true")) { + s = s.substring(0, s.length() - 5); // length of "-true" is 5 + } + while ((found = s.indexOf("-true-")) > 0) { + s = s.substring(0, found) + s.substring(found + 5); // length of "-true" is 5 + } + while (s.endsWith("-yes")) { + s = s.substring(0, s.length() - 4); // length of "-yes" is 4 + } + while ((found = s.indexOf("-yes-")) > 0) { + s = s.substring(0, found) + s.substring(found + 4); // length of "-yes" is 5 + } + return s; } public static String canonicalizeExtensionSingleton(String s) { diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/LocaleDistance.java b/android_icu4j/src/main/java/android/icu/impl/locale/LocaleDistance.java index 2cd8eb9b6..25da1d8c3 100644 --- a/android_icu4j/src/main/java/android/icu/impl/locale/LocaleDistance.java +++ b/android_icu4j/src/main/java/android/icu/impl/locale/LocaleDistance.java @@ -6,7 +6,7 @@ package android.icu.impl.locale; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Map; import java.util.MissingResourceException; import java.util.Set; @@ -16,6 +16,7 @@ import android.icu.impl.ICUData; import android.icu.impl.ICUResourceBundle; import android.icu.impl.UResource; import android.icu.util.BytesTrie; +import android.icu.util.LocaleMatcher; import android.icu.util.LocaleMatcher.FavorSubtag; import android.icu.util.ULocale; @@ -36,6 +37,17 @@ public class LocaleDistance { private static final int DISTANCE_IS_FINAL = 0x100; private static final int DISTANCE_IS_FINAL_OR_SKIP_SCRIPT = DISTANCE_IS_FINAL | DISTANCE_SKIP_SCRIPT; + + // The distance is shifted left to gain some fraction bits. + private static final int DISTANCE_SHIFT = 3; + private static final int DISTANCE_FRACTION_MASK = 7; + // 7 bits for 0..100 + private static final int DISTANCE_INT_SHIFT = 7; + private static final int INDEX_SHIFT = DISTANCE_INT_SHIFT + DISTANCE_SHIFT; + private static final int DISTANCE_MASK = 0x3ff; + // vate static final int MAX_INDEX = 0x1fffff; // avoids sign bit + private static final int INDEX_NEG_1 = 0xfffffc00; + // Indexes into array of distances. public static final int IX_DEF_LANG_DISTANCE = 0; public static final int IX_DEF_SCRIPT_DISTANCE = 1; @@ -69,6 +81,28 @@ public class LocaleDistance { private final int minRegionDistance; private final int defaultDemotionPerDesiredLocale; + public static final int shiftDistance(int distance) { + return distance << DISTANCE_SHIFT; + } + + public static final int getShiftedDistance(int indexAndDistance) { + return indexAndDistance & DISTANCE_MASK; + } + + public static final double getDistanceDouble(int indexAndDistance) { + double shiftedDistance = getShiftedDistance(indexAndDistance); + return shiftedDistance / (1 << DISTANCE_SHIFT); + } + + private static final int getDistanceFloor(int indexAndDistance) { + return (indexAndDistance & DISTANCE_MASK) >> DISTANCE_SHIFT; + } + + public static final int getIndex(int indexAndDistance) { + assert indexAndDistance >= 0; + return indexAndDistance >> INDEX_SHIFT; + } + // VisibleForTesting /** * @hide Only a subset of ICU is exposed in Android @@ -124,9 +158,11 @@ public class LocaleDistance { Set<LSR> paradigmLSRs; if (matchTable.findValue("paradigms", value)) { String[] paradigms = value.getStringArray(); - paradigmLSRs = new HashSet<>(paradigms.length / 3); + // LinkedHashSet for stable order; otherwise a unit test is flaky. + paradigmLSRs = new LinkedHashSet<>(paradigms.length / 3); for (int i = 0; i < paradigms.length; i += 3) { - paradigmLSRs.add(new LSR(paradigms[i], paradigms[i + 1], paradigms[i + 2])); + paradigmLSRs.add(new LSR(paradigms[i], paradigms[i + 1], paradigms[i + 2], + LSR.DONT_CARE_FLAGS)); } } else { paradigmLSRs = Collections.emptySet(); @@ -144,7 +180,7 @@ public class LocaleDistance { @Override public boolean equals(Object other) { if (this == other) { return true; } - if (!getClass().equals(other.getClass())) { return false; } + if (other == null || !getClass().equals(other.getClass())) { return false; } Data od = (Data)other; return Arrays.equals(trie, od.trie) && Arrays.equals(regionToPartitionsIndex, od.regionToPartitionsIndex) && @@ -152,6 +188,11 @@ public class LocaleDistance { paradigmLSRs.equals(od.paradigmLSRs) && Arrays.equals(distances, od.distances); } + + @Override + public int hashCode() { // unused; silence ErrorProne + return 1; + } } // VisibleForTesting @@ -173,10 +214,11 @@ public class LocaleDistance { // a mere region difference for one desired locale // is as good as a perfect match for the next following desired locale. // As of CLDR 36, we have <languageMatch desired="en_*_*" supported="en_*_*" distance="5"/>. - LSR en = new LSR("en", "Latn", "US"); - LSR enGB = new LSR("en", "Latn", "GB"); - defaultDemotionPerDesiredLocale = getBestIndexAndDistance(en, new LSR[] { enGB }, - 50, FavorSubtag.LANGUAGE) & 0xff; + LSR en = new LSR("en", "Latn", "US", LSR.EXPLICIT_LSR); + LSR enGB = new LSR("en", "Latn", "GB", LSR.EXPLICIT_LSR); + int indexAndDistance = getBestIndexAndDistance(en, new LSR[] { enGB }, 1, + shiftDistance(50), FavorSubtag.LANGUAGE, LocaleMatcher.Direction.WITH_ONE_WAY); + defaultDemotionPerDesiredLocale = getDistanceFloor(indexAndDistance); if (DEBUG_OUTPUT) { System.out.println("*** locale distance"); @@ -192,29 +234,32 @@ public class LocaleDistance { int threshold, FavorSubtag favorSubtag) { LSR supportedLSR = XLikelySubtags.INSTANCE.makeMaximizedLsrFrom(supported); LSR desiredLSR = XLikelySubtags.INSTANCE.makeMaximizedLsrFrom(desired); - return getBestIndexAndDistance(desiredLSR, new LSR[] { supportedLSR }, - threshold, favorSubtag) & 0xff; + int indexAndDistance = getBestIndexAndDistance(desiredLSR, new LSR[] { supportedLSR }, 1, + shiftDistance(threshold), favorSubtag, LocaleMatcher.Direction.WITH_ONE_WAY); + return getDistanceFloor(indexAndDistance); } /** * Finds the supported LSR with the smallest distance from the desired one. * Equivalent LSR subtags must be normalized into a canonical form. * - * <p>Returns the index of the lowest-distance supported LSR in bits 31..8 + * <p>Returns the index of the lowest-distance supported LSR in the high bits * (negative if none has a distance below the threshold), - * and its distance (0..ABOVE_THRESHOLD) in bits 7..0. + * and its distance (0..ABOVE_THRESHOLD) in the low bits. */ - public int getBestIndexAndDistance(LSR desired, LSR[] supportedLSRs, - int threshold, FavorSubtag favorSubtag) { + public int getBestIndexAndDistance(LSR desired, LSR[] supportedLSRs, int supportedLSRsLength, + int shiftedThreshold, FavorSubtag favorSubtag, LocaleMatcher.Direction direction) { BytesTrie iter = new BytesTrie(trie); // Look up the desired language only once for all supported LSRs. // Its "distance" is either a match point value of 0, or a non-match negative value. // Note: The data builder verifies that there are no <*, supported> or <desired, *> rules. int desLangDistance = trieNext(iter, desired.language, false); - long desLangState = desLangDistance >= 0 && supportedLSRs.length > 1 ? iter.getState64() : 0; + long desLangState = desLangDistance >= 0 && supportedLSRsLength > 1 ? iter.getState64() : 0; // Index of the supported LSR with the lowest distance. int bestIndex = -1; - for (int slIndex = 0; slIndex < supportedLSRs.length; ++slIndex) { + // Cached lookup info from XLikelySubtags.compareLikely(). + int bestLikelyInfo = -1; + for (int slIndex = 0; slIndex < supportedLSRsLength; ++slIndex) { LSR supported = supportedLSRs[slIndex]; boolean star = false; int distance = desLangDistance; @@ -243,6 +288,11 @@ public class LocaleDistance { star = true; } assert 0 <= distance && distance <= 100; + // Round up the shifted threshold (if fraction bits are not 0) + // for comparison with un-shifted distances until we need fraction bits. + // (If we simply shifted non-zero fraction bits away, then we might ignore a language + // when it's really still a micro distance below the threshold.) + int roundedThreshold = (shiftedThreshold + DISTANCE_FRACTION_MASK) >> DISTANCE_SHIFT; // We implement "favor subtag" by reducing the language subtag distance // (unscientifically reducing it to a quarter of the normal value), // so that the script distance is relatively more important. @@ -251,7 +301,9 @@ public class LocaleDistance { if (favorSubtag == FavorSubtag.SCRIPT) { distance >>= 2; } - if (distance >= threshold) { + // Let distance == roundedThreshold pass until the tie-breaker logic + // at the end of the loop. + if (distance > roundedThreshold) { continue; } @@ -269,7 +321,7 @@ public class LocaleDistance { scriptDistance &= ~DISTANCE_IS_FINAL; } distance += scriptDistance; - if (distance >= threshold) { + if (distance > roundedThreshold) { continue; } @@ -278,8 +330,8 @@ public class LocaleDistance { } else if (star || (flags & DISTANCE_IS_FINAL) != 0) { distance += defaultRegionDistance; } else { - int remainingThreshold = threshold - distance; - if (minRegionDistance >= remainingThreshold) { + int remainingThreshold = roundedThreshold - distance; + if (minRegionDistance > remainingThreshold) { continue; } @@ -294,15 +346,58 @@ public class LocaleDistance { partitionsForRegion(supported), remainingThreshold); } - if (distance < threshold) { - if (distance == 0) { - return slIndex << 8; + int shiftedDistance = shiftDistance(distance); + if (shiftedDistance == 0) { + // Distinguish between equivalent but originally unequal locales via an + // additional micro distance. + shiftedDistance |= (desired.flags ^ supported.flags); + if (shiftedDistance < shiftedThreshold) { + if (direction != LocaleMatcher.Direction.ONLY_TWO_WAY || + // Is there also a match when we swap desired/supported? + isMatch(supported, desired, shiftedThreshold, favorSubtag)) { + if (shiftedDistance == 0) { + return slIndex << INDEX_SHIFT; + } + bestIndex = slIndex; + shiftedThreshold = shiftedDistance; + bestLikelyInfo = -1; + } + } + } else { + if (shiftedDistance < shiftedThreshold) { + if (direction != LocaleMatcher.Direction.ONLY_TWO_WAY || + // Is there also a match when we swap desired/supported? + isMatch(supported, desired, shiftedThreshold, favorSubtag)) { + bestIndex = slIndex; + shiftedThreshold = shiftedDistance; + bestLikelyInfo = -1; + } + } else if (shiftedDistance == shiftedThreshold && bestIndex >= 0) { + if (direction != LocaleMatcher.Direction.ONLY_TWO_WAY || + // Is there also a match when we swap desired/supported? + isMatch(supported, desired, shiftedThreshold, favorSubtag)) { + bestLikelyInfo = XLikelySubtags.INSTANCE.compareLikely( + supported, supportedLSRs[bestIndex], bestLikelyInfo); + if ((bestLikelyInfo & 1) != 0) { + // This supported locale matches as well as the previous best match, + // and neither matches perfectly, + // but this one is "more likely" (has more-default subtags). + bestIndex = slIndex; + } + } } - bestIndex = slIndex; - threshold = distance; } } - return bestIndex >= 0 ? (bestIndex << 8) | threshold : 0xffffff00 | ABOVE_THRESHOLD; + return bestIndex >= 0 ? + (bestIndex << INDEX_SHIFT) | shiftedThreshold : + INDEX_NEG_1 | shiftDistance(ABOVE_THRESHOLD); + } + + private boolean isMatch(LSR desired, LSR supported, + int shiftedThreshold, FavorSubtag favorSubtag) { + return getBestIndexAndDistance( + desired, new LSR[] { supported }, 1, + shiftedThreshold, favorSubtag, null) >= 0; } private static final int getDesSuppScriptDistance(BytesTrie iter, long startState, @@ -364,7 +459,7 @@ public class LocaleDistance { d = getFallbackRegionDistance(iter, startState); star = true; } - if (d >= threshold) { + if (d > threshold) { return d; } else if (regionDistance < d) { regionDistance = d; @@ -377,7 +472,7 @@ public class LocaleDistance { } } else if (!star) { int d = getFallbackRegionDistance(iter, startState); - if (d >= threshold) { + if (d > threshold) { return d; } else if (regionDistance < d) { regionDistance = d; @@ -444,7 +539,17 @@ public class LocaleDistance { } public boolean isParadigmLSR(LSR lsr) { - return paradigmLSRs.contains(lsr); + // Linear search for a very short list (length 6 as of 2019), + // because we look for equivalence not equality, and + // HashSet does not support customizing equality. + // If there are many paradigm LSRs we should revisit this. + assert paradigmLSRs.size() <= 15; + for (LSR plsr : paradigmLSRs) { + if (lsr.isEquivalentTo(plsr)) { + return true; + } + } + return false; } // VisibleForTesting @@ -460,9 +565,6 @@ public class LocaleDistance { return defaultDemotionPerDesiredLocale; } - // TODO: When we build data offline, - // write test code to compare the loaded table with the builder output. - // Fail if different, with instructions for how to update the data file. // VisibleForTesting public Map<String, Integer> testOnlyGetDistanceTable() { Map<String, Integer> map = new TreeMap<>(); diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/XLikelySubtags.java b/android_icu4j/src/main/java/android/icu/impl/locale/XLikelySubtags.java index 201958d0a..0294a36ca 100644 --- a/android_icu4j/src/main/java/android/icu/impl/locale/XLikelySubtags.java +++ b/android_icu4j/src/main/java/android/icu/impl/locale/XLikelySubtags.java @@ -94,7 +94,8 @@ public final class XLikelySubtags { String[] lsrSubtags = getValue(likelyTable, "lsrs", value).getStringArray(); LSR[] lsrs = new LSR[lsrSubtags.length / 3]; for (int i = 0, j = 0; i < lsrSubtags.length; i += 3, ++j) { - lsrs[j] = new LSR(lsrSubtags[i], lsrSubtags[i + 1], lsrSubtags[i + 2]); + lsrs[j] = new LSR(lsrSubtags[i], lsrSubtags[i + 1], lsrSubtags[i + 2], + LSR.IMPLICIT_LSR); } return new Data(languageAliases, regionAliases, trie, lsrs); @@ -103,7 +104,7 @@ public final class XLikelySubtags { @Override public boolean equals(Object other) { if (this == other) { return true; } - if (!getClass().equals(other.getClass())) { return false; } + if (other == null || !getClass().equals(other.getClass())) { return false; } Data od = (Data)other; return languageAliases.equals(od.languageAliases) && @@ -111,6 +112,11 @@ public final class XLikelySubtags { Arrays.equals(trie, od.trie) && Arrays.equals(lsrs, od.lsrs); } + + @Override + public int hashCode() { // unused; silence ErrorProne + return 1; + } } // VisibleForTesting @@ -192,7 +198,7 @@ public final class XLikelySubtags { String tag = locale.toLanguageTag(); assert tag.startsWith("x-"); // Private use language tag x-subtag-subtag... - return new LSR(tag, "", ""); + return new LSR(tag, "", "", LSR.EXPLICIT_LSR); } return makeMaximizedLsr(locale.getLanguage(), locale.getScript(), locale.getCountry(), locale.getVariant()); @@ -202,7 +208,7 @@ public final class XLikelySubtags { String tag = locale.toLanguageTag(); if (tag.startsWith("x-")) { // Private use language tag x-subtag-subtag... - return new LSR(tag, "", ""); + return new LSR(tag, "", "", LSR.EXPLICIT_LSR); } return makeMaximizedLsr(locale.getLanguage(), locale.getScript(), locale.getCountry(), locale.getVariant()); @@ -216,29 +222,34 @@ public final class XLikelySubtags { switch (region.charAt(1)) { case 'A': return new LSR(PSEUDO_ACCENTS_PREFIX + language, - PSEUDO_ACCENTS_PREFIX + script, region); + PSEUDO_ACCENTS_PREFIX + script, region, LSR.EXPLICIT_LSR); case 'B': return new LSR(PSEUDO_BIDI_PREFIX + language, - PSEUDO_BIDI_PREFIX + script, region); + PSEUDO_BIDI_PREFIX + script, region, LSR.EXPLICIT_LSR); case 'C': return new LSR(PSEUDO_CRACKED_PREFIX + language, - PSEUDO_CRACKED_PREFIX + script, region); + PSEUDO_CRACKED_PREFIX + script, region, LSR.EXPLICIT_LSR); default: // normal locale break; } } if (variant.startsWith("PS")) { + int lsrFlags = region.isEmpty() ? + LSR.EXPLICIT_LANGUAGE | LSR.EXPLICIT_SCRIPT : LSR.EXPLICIT_LSR; switch (variant) { case "PSACCENT": return new LSR(PSEUDO_ACCENTS_PREFIX + language, - PSEUDO_ACCENTS_PREFIX + script, region.isEmpty() ? "XA" : region); + PSEUDO_ACCENTS_PREFIX + script, + region.isEmpty() ? "XA" : region, lsrFlags); case "PSBIDI": return new LSR(PSEUDO_BIDI_PREFIX + language, - PSEUDO_BIDI_PREFIX + script, region.isEmpty() ? "XB" : region); + PSEUDO_BIDI_PREFIX + script, + region.isEmpty() ? "XB" : region, lsrFlags); case "PSCRACK": return new LSR(PSEUDO_CRACKED_PREFIX + language, - PSEUDO_CRACKED_PREFIX + script, region.isEmpty() ? "XC" : region); + PSEUDO_CRACKED_PREFIX + script, + region.isEmpty() ? "XC" : region, lsrFlags); default: // normal locale break; } @@ -264,7 +275,7 @@ public final class XLikelySubtags { region = ""; } if (!script.isEmpty() && !region.isEmpty() && !language.isEmpty()) { - return new LSR(language, script, region); // already maximized + return new LSR(language, script, region, LSR.EXPLICIT_LSR); // already maximized } int retainOldMask = 0; @@ -347,6 +358,7 @@ public final class XLikelySubtags { } if (retainOldMask == 0) { + assert result.flags == LSR.IMPLICIT_LSR; return result; } if ((retainOldMask & 4) == 0) { @@ -358,7 +370,116 @@ public final class XLikelySubtags { if ((retainOldMask & 1) == 0) { region = result.region; } - return new LSR(language, script, region); + // retainOldMask flags = LSR explicit-subtag flags + return new LSR(language, script, region, retainOldMask); + } + + /** + * Tests whether lsr is "more likely" than other. + * For example, fr-Latn-FR is more likely than fr-Latn-CH because + * FR is the default region for fr-Latn. + * + * <p>The likelyInfo caches lookup information between calls. + * The return value is an updated likelyInfo value, + * with bit 0 set if lsr is "more likely". + * The initial value of likelyInfo must be negative. + */ + int compareLikely(LSR lsr, LSR other, int likelyInfo) { + // If likelyInfo >= 0: + // likelyInfo bit 1 is set if the previous comparison with lsr + // was for equal language and script. + // Otherwise the scripts differed. + if (!lsr.language.equals(other.language)) { + return 0xfffffffc; // negative, lsr not better than other + } + if (!lsr.script.equals(other.script)) { + int index; + if (likelyInfo >= 0 && (likelyInfo & 2) == 0) { + index = likelyInfo >> 2; + } else { + index = getLikelyIndex(lsr.language, ""); + likelyInfo = index << 2; + } + LSR likely = lsrs[index]; + if (lsr.script.equals(likely.script)) { + return likelyInfo | 1; + } else { + return likelyInfo & ~1; + } + } + if (!lsr.region.equals(other.region)) { + int index; + if (likelyInfo >= 0 && (likelyInfo & 2) != 0) { + index = likelyInfo >> 2; + } else { + index = getLikelyIndex(lsr.language, lsr.region); + likelyInfo = (index << 2) | 2; + } + LSR likely = lsrs[index]; + if (lsr.region.equals(likely.region)) { + return likelyInfo | 1; + } else { + return likelyInfo & ~1; + } + } + return likelyInfo & ~1; // lsr not better than other + } + + // Subset of maximize(). + private int getLikelyIndex(String language, String script) { + if (language.equals("und")) { + language = ""; + } + if (script.equals("Zzzz")) { + script = ""; + } + + BytesTrie iter = new BytesTrie(trie); + long state; + int value; + // Small optimization: Array lookup for first language letter. + int c0; + if (language.length() >= 2 && 0 <= (c0 = language.charAt(0) - 'a') && c0 <= 25 && + (state = trieFirstLetterStates[c0]) != 0) { + value = trieNext(iter.resetToState64(state), language, 1); + } else { + value = trieNext(iter, language, 0); + } + if (value >= 0) { + state = iter.getState64(); + } else { + iter.resetToState64(trieUndState); // "und" ("*") + state = 0; + } + + if (value > 0) { + // Intermediate or final value from just language. + if (value == SKIP_SCRIPT) { + value = 0; + } + } else { + value = trieNext(iter, script, 0); + if (value >= 0) { + state = iter.getState64(); + } else { + if (state == 0) { + iter.resetToState64(trieUndZzzzState); // "und-Zzzz" ("**") + } else { + iter.resetToState64(state); + value = trieNext(iter, "", 0); + assert value >= 0; + state = iter.getState64(); + } + } + } + + if (value > 0) { + // Final value from just language or language+script. + } else { + value = trieNext(iter, "", 0); + assert value > 0; + } + return value; } private static final int trieNext(BytesTrie iter, String s, int i) { @@ -418,9 +539,9 @@ public final class XLikelySubtags { boolean favorRegionOk = false; if (result.script.equals(value00.script)) { //script is default if (result.region.equals(value00.region)) { - return new LSR(result.language, "", ""); + return new LSR(result.language, "", "", LSR.DONT_CARE_FLAGS); } else if (fieldToFavor == ULocale.Minimize.FAVOR_REGION) { - return new LSR(result.language, "", result.region); + return new LSR(result.language, "", result.region, LSR.DONT_CARE_FLAGS); } else { favorRegionOk = true; } @@ -430,9 +551,9 @@ public final class XLikelySubtags { // Maybe do later, but for now use the straightforward code. LSR result2 = maximize(languageIn, scriptIn, ""); if (result2.equals(result)) { - return new LSR(result.language, result.script, ""); + return new LSR(result.language, result.script, "", LSR.DONT_CARE_FLAGS); } else if (favorRegionOk) { - return new LSR(result.language, "", result.region); + return new LSR(result.language, "", result.region, LSR.DONT_CARE_FLAGS); } return result; } diff --git a/android_icu4j/src/main/java/android/icu/impl/number/AdoptingModifierStore.java b/android_icu4j/src/main/java/android/icu/impl/number/AdoptingModifierStore.java index 91941aad1..76c3d6423 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/AdoptingModifierStore.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/AdoptingModifierStore.java @@ -4,6 +4,7 @@ package android.icu.impl.number; import android.icu.impl.StandardPlural; +import android.icu.impl.number.Modifier.Signum; /** * This implementation of ModifierStore adopts references to Modifiers. @@ -13,7 +14,8 @@ import android.icu.impl.StandardPlural; */ public class AdoptingModifierStore implements ModifierStore { private final Modifier positive; - private final Modifier zero; + private final Modifier posZero; + private final Modifier negZero; private final Modifier negative; final Modifier[] mods; boolean frozen; @@ -24,9 +26,10 @@ public class AdoptingModifierStore implements ModifierStore { * <p> * If this constructor is used, a plural form CANNOT be passed to {@link #getModifier}. */ - public AdoptingModifierStore(Modifier positive, Modifier zero, Modifier negative) { + public AdoptingModifierStore(Modifier positive, Modifier posZero, Modifier negZero, Modifier negative) { this.positive = positive; - this.zero = zero; + this.posZero = posZero; + this.negZero = negZero; this.negative = negative; this.mods = null; this.frozen = true; @@ -41,13 +44,14 @@ public class AdoptingModifierStore implements ModifierStore { */ public AdoptingModifierStore() { this.positive = null; - this.zero = null; + this.posZero = null; + this.negZero = null; this.negative = null; - this.mods = new Modifier[3 * StandardPlural.COUNT]; + this.mods = new Modifier[4 * StandardPlural.COUNT]; this.frozen = false; } - public void setModifier(int signum, StandardPlural plural, Modifier mod) { + public void setModifier(Signum signum, StandardPlural plural, Modifier mod) { assert !frozen; mods[getModIndex(signum, plural)] = mod; } @@ -56,21 +60,34 @@ public class AdoptingModifierStore implements ModifierStore { frozen = true; } - public Modifier getModifierWithoutPlural(int signum) { + public Modifier getModifierWithoutPlural(Signum signum) { assert frozen; assert mods == null; - return signum == 0 ? zero : signum < 0 ? negative : positive; + assert signum != null; + switch (signum) { + case POS: + return positive; + case POS_ZERO: + return posZero; + case NEG_ZERO: + return negZero; + case NEG: + return negative; + default: + throw new AssertionError("Unreachable"); + } } - public Modifier getModifier(int signum, StandardPlural plural) { + @Override + public Modifier getModifier(Signum signum, StandardPlural plural) { assert frozen; assert positive == null; return mods[getModIndex(signum, plural)]; } - private static int getModIndex(int signum, StandardPlural plural) { - assert signum >= -1 && signum <= 1; + private static int getModIndex(Signum signum, StandardPlural plural) { + assert signum != null; assert plural != null; - return plural.ordinal() * 3 + (signum + 1); + return plural.ordinal() * Signum.COUNT + signum.ordinal(); } } diff --git a/android_icu4j/src/main/java/android/icu/impl/number/ConstantMultiFieldModifier.java b/android_icu4j/src/main/java/android/icu/impl/number/ConstantMultiFieldModifier.java index 5dab11193..e8e787e26 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/ConstantMultiFieldModifier.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/ConstantMultiFieldModifier.java @@ -20,8 +20,8 @@ public class ConstantMultiFieldModifier implements Modifier { // value and is treated internally as immutable. protected final char[] prefixChars; protected final char[] suffixChars; - protected final Field[] prefixFields; - protected final Field[] suffixFields; + protected final Object[] prefixFields; + protected final Object[] suffixFields; private final boolean overwrite; private final boolean strong; diff --git a/android_icu4j/src/main/java/android/icu/impl/number/CurrencySpacingEnabledModifier.java b/android_icu4j/src/main/java/android/icu/impl/number/CurrencySpacingEnabledModifier.java index 41e4fd174..5c93ba968 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/CurrencySpacingEnabledModifier.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/CurrencySpacingEnabledModifier.java @@ -3,8 +3,6 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package android.icu.impl.number; -import java.text.Format.Field; - import android.icu.impl.FormattedStringBuilder; import android.icu.text.DecimalFormatSymbols; import android.icu.text.NumberFormat; @@ -58,7 +56,7 @@ public class CurrencySpacingEnabledModifier extends ConstantMultiFieldModifier { afterPrefixInsert = null; } if (suffix.length() > 0 && suffix.fieldAt(0) == NumberFormat.Field.CURRENCY) { - int suffixCp = suffix.getLastCodePoint(); + int suffixCp = suffix.getFirstCodePoint(); UnicodeSet suffixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, SUFFIX); if (suffixUnicodeSet.contains(suffixCp)) { beforeSuffixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, SUFFIX); @@ -127,7 +125,7 @@ public class CurrencySpacingEnabledModifier extends ConstantMultiFieldModifier { // NOTE: For prefix, output.fieldAt(index-1) gets the last field type in the prefix. // This works even if the last code point in the prefix is 2 code units because the // field value gets populated to both indices in the field array. - Field affixField = (affix == PREFIX) ? output.fieldAt(index - 1) + Object affixField = (affix == PREFIX) ? output.fieldAt(index - 1) : output.fieldAt(index); if (affixField != NumberFormat.Field.CURRENCY) { return 0; diff --git a/android_icu4j/src/main/java/android/icu/impl/number/DecimalFormatProperties.java b/android_icu4j/src/main/java/android/icu/impl/number/DecimalFormatProperties.java index 7367a5395..4a73640fd 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/DecimalFormatProperties.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/DecimalFormatProperties.java @@ -70,7 +70,7 @@ public class DecimalFormatProperties implements Cloneable, Serializable { /** * Internal parse mode for increased compatibility with java.text.DecimalFormat. * Used by Android libcore. To enable this feature, java.text.DecimalFormat holds an instance of - * ICU4J's DecimalFormat and enable it by calling setParseStrictMode(ParseMode.COMPATIBILITY). + * ICU4J's DecimalFormat and enable it by calling setParseStrictMode(ParseMode.JAVA_COMPATIBILITY). */ JAVA_COMPATIBILITY, } diff --git a/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity.java b/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity.java index bb3cc71e1..b6f548dac 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity.java @@ -8,6 +8,7 @@ import java.math.MathContext; import java.text.FieldPosition; import android.icu.impl.StandardPlural; +import android.icu.impl.number.Modifier.Signum; import android.icu.text.PluralRules; import android.icu.text.UFieldPosition; @@ -124,6 +125,26 @@ public interface DecimalQuantity extends PluralRules.IFixedDecimal { public int getMagnitude() throws ArithmeticException; /** + * @return The value of the (suppressed) exponent after the number has been + * put into a notation with exponents (ex: compact, scientific). Ex: given + * the number 1000 as "1K" / "1E3", the return value will be 3 (positive). + */ + public int getExponent(); + + /** + * Adjusts the value for the (suppressed) exponent stored when using + * notation with exponents (ex: compact, scientific). + * + * <p>Adjusting the exponent is decoupled from {@link #adjustMagnitude} in + * order to allow flexibility for {@link StandardPlural} to be selected in + * formatting (ex: for compact notation) either with or without the exponent + * applied in the value of the number. + * @param delta + * The value to adjust the exponent by. + */ + public void adjustExponent(int delta); + + /** * @return Whether the value represented by this {@link DecimalQuantity} is * zero, infinity, or NaN. */ @@ -132,8 +153,8 @@ public interface DecimalQuantity extends PluralRules.IFixedDecimal { /** @return Whether the value represented by this {@link DecimalQuantity} is less than zero. */ public boolean isNegative(); - /** @return -1 if the value is negative; 1 if positive; or 0 if zero. */ - public int signum(); + /** @return The appropriate value from the Signum enum. */ + public Signum signum(); /** @return Whether the value represented by this {@link DecimalQuantity} is infinite. */ @Override diff --git a/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_AbstractBCD.java b/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_AbstractBCD.java index 47d5bfdc3..f00ae97cb 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_AbstractBCD.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_AbstractBCD.java @@ -10,6 +10,7 @@ import java.text.FieldPosition; import android.icu.impl.StandardPlural; import android.icu.impl.Utility; +import android.icu.impl.number.Modifier.Signum; import android.icu.text.PluralRules; import android.icu.text.PluralRules.Operand; import android.icu.text.UFieldPosition; @@ -87,6 +88,12 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { protected int lReqPos = 0; protected int rReqPos = 0; + /** + * The value of the (suppressed) exponent after the number has been put into + * a notation with exponents (ex: compact, scientific). + */ + protected int exponent = 0; + @Override public void copyFrom(DecimalQuantity _other) { copyBcdFrom(_other); @@ -99,13 +106,14 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { origDouble = other.origDouble; origDelta = other.origDelta; isApproximate = other.isApproximate; + exponent = other.exponent; } public DecimalQuantity_AbstractBCD clear() { lReqPos = 0; rReqPos = 0; flags = 0; - setBcdToZero(); // sets scale, precision, hasDouble, origDouble, origDelta, and BCD data + setBcdToZero(); // sets scale, precision, hasDouble, origDouble, origDelta, exponent, and BCD data return this; } @@ -218,6 +226,16 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { } @Override + public int getExponent() { + return exponent; + } + + @Override + public void adjustExponent(int delta) { + exponent = exponent + delta; + } + + @Override public StandardPlural getStandardPlural(PluralRules rules) { if (rules == null) { // Fail gracefully if the user didn't provide a PluralRules @@ -247,6 +265,8 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { return fractionCount(); case w: return fractionCountWithoutTrailingZeros(); + case e: + return getExponent(); default: return Math.abs(toDouble()); } @@ -292,11 +312,11 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { } private int fractionCount() { - return -getLowerDisplayMagnitude(); + return Math.max(0, -getLowerDisplayMagnitude() - exponent); } private int fractionCountWithoutTrailingZeros() { - return Math.max(-scale, 0); + return Math.max(-scale - exponent, 0); } @Override @@ -305,8 +325,18 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { } @Override - public int signum() { - return isNegative() ? -1 : (isZeroish() && !isInfinite()) ? 0 : 1; + public Signum signum() { + boolean isZero = (isZeroish() && !isInfinite()); + boolean isNeg = isNegative(); + if (isZero && isNeg) { + return Signum.NEG_ZERO; + } else if (isZero) { + return Signum.POS_ZERO; + } else if (isNeg) { + return Signum.NEG; + } else { + return Signum.POS; + } } @Override @@ -460,8 +490,14 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { return; } + if (exponent == -1023 || exponent == 1024) { + // The extreme values of exponent are special; use slow path. + convertToAccurateDouble(); + return; + } + // 3.3219... is log2(10) - int fracLength = (int) ((52 - exponent) / 3.32192809489); + int fracLength = (int) ((52 - exponent) / 3.32192809488736234787031942948939017586); if (fracLength >= 0) { int i = fracLength; // 1e22 is the largest exact double. @@ -568,7 +604,9 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { /** * Returns a long approximating the internal BCD. A long can only represent the integral part of the - * number. + * number. Note: this method incorporates the value of {@code exponent} + * (for cases such as compact notation) to return the proper long value + * represented by the result. * * @param truncateIfOverflow if false and the number does NOT fit, fails with an assertion error. * @return A 64-bit integer representation of the internal BCD. @@ -579,12 +617,12 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { // Fallback behavior upon truncateIfOverflow is to truncate at 17 digits. assert(truncateIfOverflow || fitsInLong()); long result = 0L; - int upperMagnitude = scale + precision - 1; + int upperMagnitude = exponent + scale + precision - 1; if (truncateIfOverflow) { upperMagnitude = Math.min(upperMagnitude, 17); } for (int magnitude = upperMagnitude; magnitude >= 0; magnitude--) { - result = result * 10 + getDigitPos(magnitude - scale); + result = result * 10 + getDigitPos(magnitude - scale - exponent); } if (isNegative()) { result = -result; @@ -596,10 +634,13 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { * This returns a long representing the fraction digits of the number, as required by PluralRules. * For example, if we represent the number "1.20" (including optional and required digits), then this * function returns "20" if includeTrailingZeros is true or "2" if false. + * Note: this method incorporates the value of {@code exponent} + * (for cases such as compact notation) to return the proper long value + * represented by the result. */ public long toFractionLong(boolean includeTrailingZeros) { long result = 0L; - int magnitude = -1; + int magnitude = -1 - exponent; int lowerMagnitude = scale; if (includeTrailingZeros) { lowerMagnitude = Math.min(lowerMagnitude, rReqPos); @@ -629,7 +670,7 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { if (isZeroish()) { return true; } - if (scale < 0) { + if (exponent + scale < 0) { return false; } int magnitude = getMagnitude(); @@ -982,20 +1023,43 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { @Override public String toPlainString() { - // NOTE: This logic is duplicated between here and DecimalQuantity_SimpleStorage. StringBuilder sb = new StringBuilder(); + toPlainString(sb); + return sb.toString(); + } + + public void toPlainString(StringBuilder result) { + assert(!isApproximate); if (isNegative()) { - sb.append('-'); + result.append('-'); } - if (precision == 0 || getMagnitude() < 0) { - sb.append('0'); + if (precision == 0) { + result.append('0'); + return; } - for (int m = getUpperDisplayMagnitude(); m >= getLowerDisplayMagnitude(); m--) { - sb.append((char) ('0' + getDigit(m))); - if (m == 0) - sb.append('.'); + + int upper = scale + precision + exponent - 1; + int lower = scale + exponent; + if (upper < lReqPos - 1) { + upper = lReqPos - 1; + } + if (lower > rReqPos) { + lower = rReqPos; + } + + int p = upper; + if (p < 0) { + result.append('0'); + } + for (; p >= 0; p--) { + result.append((char) ('0' + getDigitPos(p - scale - exponent))); + } + if (lower < 0) { + result.append('.'); + } + for(; p >= lower; p--) { + result.append((char) ('0' + getDigitPos(p - scale - exponent))); } - return sb.toString(); } public String toScientificString() { @@ -1026,7 +1090,7 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { } } result.append('E'); - int _scale = upperPos + scale; + int _scale = upperPos + scale + exponent; if (_scale == Integer.MIN_VALUE) { result.append("-2147483648"); return; @@ -1137,7 +1201,7 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { /** * Sets the internal representation to zero. Clears any values stored in scale, precision, hasDouble, - * origDouble, origDelta, and BCD data. + * origDouble, origDelta, exponent, and BCD data. */ protected abstract void setBcdToZero(); diff --git a/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_DualStorageBCD.java b/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_DualStorageBCD.java index a83fcb0e3..bfca13c09 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_DualStorageBCD.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_DualStorageBCD.java @@ -182,6 +182,7 @@ public final class DecimalQuantity_DualStorageBCD extends DecimalQuantity_Abstra isApproximate = false; origDouble = 0; origDelta = 0; + exponent = 0; } @Override @@ -256,11 +257,11 @@ public final class DecimalQuantity_DualStorageBCD extends DecimalQuantity_Abstra } BigDecimal result = BigDecimal.valueOf(tempLong); // Test that the new scale fits inside the BigDecimal - long newScale = result.scale() + scale; + long newScale = result.scale() + scale + exponent; if (newScale <= Integer.MIN_VALUE) { result = BigDecimal.ZERO; } else { - result = result.scaleByPowerOfTen(scale); + result = result.scaleByPowerOfTen(scale + exponent); } if (isNegative()) { result = result.negate(); diff --git a/android_icu4j/src/main/java/android/icu/impl/number/LocalizedNumberFormatterAsFormat.java b/android_icu4j/src/main/java/android/icu/impl/number/LocalizedNumberFormatterAsFormat.java index a23cefa0e..79d62ed11 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/LocalizedNumberFormatterAsFormat.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/LocalizedNumberFormatterAsFormat.java @@ -13,7 +13,9 @@ import java.text.FieldPosition; import java.text.Format; import java.text.ParsePosition; -import android.icu.number.FormattedNumber; +import android.icu.impl.FormattedStringBuilder; +import android.icu.impl.FormattedValueStringBuilderImpl; +import android.icu.impl.Utility; import android.icu.number.LocalizedNumberFormatter; import android.icu.number.NumberFormatter; import android.icu.util.ULocale; @@ -48,16 +50,18 @@ public class LocalizedNumberFormatterAsFormat extends Format { if (!(obj instanceof Number)) { throw new IllegalArgumentException(); } - FormattedNumber result = formatter.format((Number) obj); + DecimalQuantity dq = new DecimalQuantity_DualStorageBCD((Number) obj); + FormattedStringBuilder string = new FormattedStringBuilder(); + formatter.formatImpl(dq, string); // always return first occurrence: pos.setBeginIndex(0); pos.setEndIndex(0); - boolean found = result.nextFieldPosition(pos); + boolean found = FormattedValueStringBuilderImpl.nextFieldPosition(string, pos); if (found && toAppendTo.length() != 0) { pos.setBeginIndex(pos.getBeginIndex() + toAppendTo.length()); pos.setEndIndex(pos.getEndIndex() + toAppendTo.length()); } - result.appendTo(toAppendTo); + Utility.appendTo(string, toAppendTo); return toAppendTo; } diff --git a/android_icu4j/src/main/java/android/icu/impl/number/LongNameHandler.java b/android_icu4j/src/main/java/android/icu/impl/number/LongNameHandler.java index 6b4d10aea..d67f0212f 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/LongNameHandler.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/LongNameHandler.java @@ -13,6 +13,7 @@ import android.icu.impl.ICUResourceBundle; import android.icu.impl.SimpleFormatterImpl; import android.icu.impl.StandardPlural; import android.icu.impl.UResource; +import android.icu.impl.number.Modifier.Signum; import android.icu.number.NumberFormatter.UnitWidth; import android.icu.text.NumberFormat; import android.icu.text.PluralRules; @@ -244,8 +245,10 @@ public class LongNameHandler implements MicroPropsGenerator, ModifierStore { String compiled = SimpleFormatterImpl .compileToStringMinMaxArguments(rawPerUnitFormat, sb, 2, 2); String secondaryFormat = getWithPlural(secondaryData, StandardPlural.ONE); + + // Some "one" pattern may not contain "{0}". For example in "ar" or "ne" locale. String secondaryCompiled = SimpleFormatterImpl - .compileToStringMinMaxArguments(secondaryFormat, sb, 1, 1); + .compileToStringMinMaxArguments(secondaryFormat, sb, 0, 1); String secondaryString = SimpleFormatterImpl.getTextWithNoArguments(secondaryCompiled) .trim(); perUnitFormat = SimpleFormatterImpl.formatCompiledPattern(compiled, "{0}", secondaryString); @@ -266,7 +269,7 @@ public class LongNameHandler implements MicroPropsGenerator, ModifierStore { String compiled = SimpleFormatterImpl.compileToStringMinMaxArguments(simpleFormat, sb, 0, 1); Modifier.Parameters parameters = new Modifier.Parameters(); parameters.obj = this; - parameters.signum = 0; + parameters.signum = null;// Signum ignored parameters.plural = plural; modifiers.put(plural, new SimpleModifier(compiled, field, false, parameters)); } @@ -285,7 +288,7 @@ public class LongNameHandler implements MicroPropsGenerator, ModifierStore { .compileToStringMinMaxArguments(compoundFormat, sb, 0, 1); Modifier.Parameters parameters = new Modifier.Parameters(); parameters.obj = this; - parameters.signum = 0; + parameters.signum = null; // Signum ignored parameters.plural = plural; modifiers.put(plural, new SimpleModifier(compoundCompiled, field, false, parameters)); } @@ -300,7 +303,8 @@ public class LongNameHandler implements MicroPropsGenerator, ModifierStore { } @Override - public Modifier getModifier(int signum, StandardPlural plural) { + public Modifier getModifier(Signum signum, StandardPlural plural) { + // Signum ignored return modifiers.get(plural); } } diff --git a/android_icu4j/src/main/java/android/icu/impl/number/Modifier.java b/android_icu4j/src/main/java/android/icu/impl/number/Modifier.java index 3507a025c..5fa5fd166 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/Modifier.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/Modifier.java @@ -19,6 +19,15 @@ import android.icu.impl.StandardPlural; */ public interface Modifier { + static enum Signum { + NEG, + NEG_ZERO, + POS_ZERO, + POS; + + static final int COUNT = Signum.values().length; + }; + /** * Apply this Modifier to the string builder. * @@ -68,7 +77,7 @@ public interface Modifier { */ public static class Parameters { public ModifierStore obj; - public int signum; + public Signum signum; public StandardPlural plural; } diff --git a/android_icu4j/src/main/java/android/icu/impl/number/ModifierStore.java b/android_icu4j/src/main/java/android/icu/impl/number/ModifierStore.java index 92479c7ad..134d94d90 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/ModifierStore.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/ModifierStore.java @@ -4,6 +4,7 @@ package android.icu.impl.number; import android.icu.impl.StandardPlural; +import android.icu.impl.number.Modifier.Signum; /** * This is *not* a modifier; rather, it is an object that can return modifiers @@ -16,5 +17,5 @@ public interface ModifierStore { /** * Returns a Modifier with the given parameters (best-effort). */ - Modifier getModifier(int signum, StandardPlural plural); + Modifier getModifier(Signum signum, StandardPlural plural); } diff --git a/android_icu4j/src/main/java/android/icu/impl/number/MutablePatternModifier.java b/android_icu4j/src/main/java/android/icu/impl/number/MutablePatternModifier.java index 7f69ce71c..89f9274ba 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/MutablePatternModifier.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/MutablePatternModifier.java @@ -52,7 +52,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr PluralRules rules; // Number details - int signum; + Signum signum; StandardPlural plural; // QuantityChain details @@ -131,7 +131,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr * The plural form of the number, required only if the pattern contains the triple * currency sign, "¤¤¤" (and as indicated by {@link #needsPlurals()}). */ - public void setNumberProperties(int signum, StandardPlural plural) { + public void setNumberProperties(Signum signum, StandardPlural plural) { assert (plural != null) == needsPlurals(); this.signum = signum; this.plural = plural; @@ -157,44 +157,35 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr * @return An immutable that supports both positive and negative numbers. */ public ImmutablePatternModifier createImmutable() { - return createImmutableAndChain(null); - } - - /** - * Creates a new quantity-dependent Modifier that behaves the same as the current instance, but which - * is immutable and can be saved for future use. The number properties in the current instance are - * mutated; all other properties are left untouched. - * - * @param parent - * The QuantityChain to which to chain this immutable. - * @return An immutable that supports both positive and negative numbers. - */ - public ImmutablePatternModifier createImmutableAndChain(MicroPropsGenerator parent) { FormattedStringBuilder a = new FormattedStringBuilder(); FormattedStringBuilder b = new FormattedStringBuilder(); if (needsPlurals()) { // Slower path when we require the plural keyword. AdoptingModifierStore pm = new AdoptingModifierStore(); for (StandardPlural plural : StandardPlural.VALUES) { - setNumberProperties(1, plural); - pm.setModifier(1, plural, createConstantModifier(a, b)); - setNumberProperties(0, plural); - pm.setModifier(0, plural, createConstantModifier(a, b)); - setNumberProperties(-1, plural); - pm.setModifier(-1, plural, createConstantModifier(a, b)); + setNumberProperties(Signum.POS, plural); + pm.setModifier(Signum.POS, plural, createConstantModifier(a, b)); + setNumberProperties(Signum.POS_ZERO, plural); + pm.setModifier(Signum.POS_ZERO, plural, createConstantModifier(a, b)); + setNumberProperties(Signum.NEG_ZERO, plural); + pm.setModifier(Signum.NEG_ZERO, plural, createConstantModifier(a, b)); + setNumberProperties(Signum.NEG, plural); + pm.setModifier(Signum.NEG, plural, createConstantModifier(a, b)); } pm.freeze(); - return new ImmutablePatternModifier(pm, rules, parent); + return new ImmutablePatternModifier(pm, rules); } else { // Faster path when plural keyword is not needed. - setNumberProperties(1, null); + setNumberProperties(Signum.POS, null); Modifier positive = createConstantModifier(a, b); - setNumberProperties(0, null); - Modifier zero = createConstantModifier(a, b); - setNumberProperties(-1, null); + setNumberProperties(Signum.POS_ZERO, null); + Modifier posZero = createConstantModifier(a, b); + setNumberProperties(Signum.NEG_ZERO, null); + Modifier negZero = createConstantModifier(a, b); + setNumberProperties(Signum.NEG, null); Modifier negative = createConstantModifier(a, b); - AdoptingModifierStore pm = new AdoptingModifierStore(positive, zero, negative); - return new ImmutablePatternModifier(pm, null, parent); + AdoptingModifierStore pm = new AdoptingModifierStore(positive, posZero, negZero, negative); + return new ImmutablePatternModifier(pm, null); } } @@ -227,20 +218,30 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr public static class ImmutablePatternModifier implements MicroPropsGenerator { final AdoptingModifierStore pm; final PluralRules rules; - final MicroPropsGenerator parent; + /* final */ MicroPropsGenerator parent; ImmutablePatternModifier( AdoptingModifierStore pm, - PluralRules rules, - MicroPropsGenerator parent) { + PluralRules rules) { this.pm = pm; this.rules = rules; + this.parent = null; + } + + public ImmutablePatternModifier addToChain(MicroPropsGenerator parent) { this.parent = parent; + return this; } @Override public MicroProps processQuantity(DecimalQuantity quantity) { MicroProps micros = parent.processQuantity(quantity); + if (micros.rounder != null) { + micros.rounder.apply(quantity); + } + if (micros.modMiddle != null) { + return micros; + } applyToMicros(micros, quantity); return micros; } @@ -275,6 +276,12 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr @Override public MicroProps processQuantity(DecimalQuantity fq) { MicroProps micros = parent.processQuantity(fq); + if (micros.rounder != null) { + micros.rounder.apply(fq); + } + if (micros.modMiddle != null) { + return micros; + } if (needsPlurals()) { StandardPlural pluralForm = RoundingUtils.getPluralSafe(micros.rounder, rules, fq); setNumberProperties(fq.signum(), pluralForm); @@ -372,8 +379,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr } PatternStringUtils.patternInfoToStringBuilder(patternInfo, isPrefix, - signum, - signDisplay, + PatternStringUtils.resolveSignDisplay(signDisplay, signum), plural, perMilleReplacesPercent, currentAffix); @@ -400,8 +406,23 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr } else if (unitWidth == UnitWidth.HIDDEN) { return ""; } else { - int selector = unitWidth == UnitWidth.NARROW ? Currency.NARROW_SYMBOL_NAME - : Currency.SYMBOL_NAME; + int selector; + switch (unitWidth) { + case SHORT: + selector = Currency.SYMBOL_NAME; + break; + case NARROW: + selector = Currency.NARROW_SYMBOL_NAME; + break; + case FORMAL: + selector = Currency.FORMAL_SYMBOL_NAME; + break; + case VARIANT: + selector = Currency.VARIANT_SYMBOL_NAME; + break; + default: + throw new AssertionError(); + } return currency.getName(symbols.getULocale(), selector, null); } case AffixUtils.TYPE_CURRENCY_DOUBLE: diff --git a/android_icu4j/src/main/java/android/icu/impl/number/PatternStringUtils.java b/android_icu4j/src/main/java/android/icu/impl/number/PatternStringUtils.java index ac7bf0a13..2aeb45acd 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/PatternStringUtils.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/PatternStringUtils.java @@ -6,6 +6,7 @@ package android.icu.impl.number; import java.math.BigDecimal; import android.icu.impl.StandardPlural; +import android.icu.impl.number.Modifier.Signum; import android.icu.impl.number.Padder.PadPosition; import android.icu.number.NumberFormatter.SignDisplay; import android.icu.text.DecimalFormatSymbols; @@ -16,6 +17,21 @@ import android.icu.text.DecimalFormatSymbols; */ public class PatternStringUtils { + // Note: the order of fields in this enum matters for parsing. + /** + * @hide Only a subset of ICU is exposed in Android + */ + public static enum PatternSignType { + // Render using normal positive subpattern rules + POS, + // Render using rules to force the display of a plus sign + POS_SIGN, + // Render using negative subpattern rules + NEG; + + public static final PatternSignType[] VALUES = PatternSignType.values(); + }; + /** * Determine whether a given roundingIncrement should be ignored for formatting * based on the current maxFrac value (maximum fraction digits). For example a @@ -25,7 +41,7 @@ public class PatternStringUtils { * it should not be ignored if maxFrac is 2 or more (but a roundingIncrement of * 0.005 is treated like 0.001 for significance). * - * This test is needed for both NumberPropertyMapper.oldToNew and + * This test is needed for both NumberPropertyMapper.oldToNew and * PatternStringUtils.propertiesToPatternString, but NumberPropertyMapper * is package-private so we have it here. * @@ -82,7 +98,7 @@ public class PatternStringUtils { boolean alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown(); int exponentDigits = Math.min(properties.getMinimumExponentDigits(), dosMax); boolean exponentShowPlusSign = properties.getExponentSignAlwaysShown(); - PropertiesAffixPatternProvider affixes = new PropertiesAffixPatternProvider(properties); + AffixPatternProvider affixes = PropertiesAffixPatternProvider.forProperties(properties); // Prefixes sb.append(affixes.getString(AffixPatternProvider.FLAG_POS_PREFIX)); @@ -418,25 +434,19 @@ public class PatternStringUtils { public static void patternInfoToStringBuilder( AffixPatternProvider patternInfo, boolean isPrefix, - int signum, - SignDisplay signDisplay, + PatternSignType patternSignType, StandardPlural plural, boolean perMilleReplacesPercent, StringBuilder output) { - // Should the output render '+' where '-' would normally appear in the pattern? - boolean plusReplacesMinusSign = signum != -1 - && (signDisplay == SignDisplay.ALWAYS - || signDisplay == SignDisplay.ACCOUNTING_ALWAYS - || (signum == 1 - && (signDisplay == SignDisplay.EXCEPT_ZERO - || signDisplay == SignDisplay.ACCOUNTING_EXCEPT_ZERO))) - && patternInfo.positiveHasPlusSign() == false; - - // Should we use the affix from the negative subpattern? (If not, we will use the positive - // subpattern.) + boolean plusReplacesMinusSign = (patternSignType == PatternSignType.POS_SIGN) + && !patternInfo.positiveHasPlusSign(); + + // Should we use the affix from the negative subpattern? + // (If not, we will use the positive subpattern.) boolean useNegativeAffixPattern = patternInfo.hasNegativeSubpattern() - && (signum == -1 || (patternInfo.negativeHasMinusSign() && plusReplacesMinusSign)); + && (patternSignType == PatternSignType.NEG + || (patternInfo.negativeHasMinusSign() && plusReplacesMinusSign)); // Resolve the flags for the affix pattern. int flags = 0; @@ -455,8 +465,8 @@ public class PatternStringUtils { boolean prependSign; if (!isPrefix || useNegativeAffixPattern) { prependSign = false; - } else if (signum == -1) { - prependSign = signDisplay != SignDisplay.NEVER; + } else if (patternSignType == PatternSignType.NEG) { + prependSign = true; } else { prependSign = plusReplacesMinusSign; } @@ -485,4 +495,53 @@ public class PatternStringUtils { } } + public static PatternSignType resolveSignDisplay(SignDisplay signDisplay, Signum signum) { + switch (signDisplay) { + case AUTO: + case ACCOUNTING: + switch (signum) { + case NEG: + case NEG_ZERO: + return PatternSignType.NEG; + case POS_ZERO: + case POS: + return PatternSignType.POS; + } + break; + + case ALWAYS: + case ACCOUNTING_ALWAYS: + switch (signum) { + case NEG: + case NEG_ZERO: + return PatternSignType.NEG; + case POS_ZERO: + case POS: + return PatternSignType.POS_SIGN; + } + break; + + case EXCEPT_ZERO: + case ACCOUNTING_EXCEPT_ZERO: + switch (signum) { + case NEG: + return PatternSignType.NEG; + case NEG_ZERO: + case POS_ZERO: + return PatternSignType.POS; + case POS: + return PatternSignType.POS_SIGN; + } + break; + + case NEVER: + return PatternSignType.POS; + + default: + break; + } + + throw new AssertionError("Unreachable"); + } + } diff --git a/android_icu4j/src/main/java/android/icu/impl/number/PropertiesAffixPatternProvider.java b/android_icu4j/src/main/java/android/icu/impl/number/PropertiesAffixPatternProvider.java index 837f35ef6..f9deb3774 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/PropertiesAffixPatternProvider.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/PropertiesAffixPatternProvider.java @@ -13,7 +13,15 @@ public class PropertiesAffixPatternProvider implements AffixPatternProvider { private final String negSuffix; private final boolean isCurrencyPattern; - public PropertiesAffixPatternProvider(DecimalFormatProperties properties) { + public static AffixPatternProvider forProperties(DecimalFormatProperties properties) { + if (properties.getCurrencyPluralInfo() == null) { + return new PropertiesAffixPatternProvider(properties); + } else { + return new CurrencyPluralInfoAffixProvider(properties.getCurrencyPluralInfo(), properties); + } + } + + PropertiesAffixPatternProvider(DecimalFormatProperties properties) { // There are two ways to set affixes in DecimalFormat: via the pattern string (applyPattern), and via the // explicit setters (setPositivePrefix and friends). The way to resolve the settings is as follows: // diff --git a/android_icu4j/src/main/java/android/icu/impl/number/RoundingUtils.java b/android_icu4j/src/main/java/android/icu/impl/number/RoundingUtils.java index b233475dd..8dfeea255 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/RoundingUtils.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/RoundingUtils.java @@ -230,6 +230,9 @@ public class RoundingUtils { */ public static StandardPlural getPluralSafe( Precision rounder, PluralRules rules, DecimalQuantity dq) { + if (rounder == null) { + return dq.getStandardPlural(rules); + } // TODO(ICU-20500): Avoid the copy? DecimalQuantity copy = dq.createCopy(); rounder.apply(copy); diff --git a/android_icu4j/src/main/java/android/icu/impl/number/SimpleModifier.java b/android_icu4j/src/main/java/android/icu/impl/number/SimpleModifier.java index 8f84a7d9f..923e90577 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/SimpleModifier.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/SimpleModifier.java @@ -19,9 +19,6 @@ public class SimpleModifier implements Modifier { private final String compiledPattern; private final Field field; private final boolean strong; - private final int prefixLength; - private final int suffixOffset; - private final int suffixLength; // Parameters: used for number range formatting private final Parameters parameters; @@ -41,53 +38,21 @@ public class SimpleModifier implements Modifier { this.field = field; this.strong = strong; this.parameters = parameters; - - int argLimit = SimpleFormatterImpl.getArgumentLimit(compiledPattern); - if (argLimit == 0) { - // No arguments in compiled pattern - prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT; - assert 2 + prefixLength == compiledPattern.length(); - // Set suffixOffset = -1 to indicate no arguments in compiled pattern. - suffixOffset = -1; - suffixLength = 0; - } else { - assert argLimit == 1; - if (compiledPattern.charAt(1) != '\u0000') { - prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT; - suffixOffset = 3 + prefixLength; - } else { - prefixLength = 0; - suffixOffset = 2; - } - if (3 + prefixLength < compiledPattern.length()) { - suffixLength = compiledPattern.charAt(suffixOffset) - ARG_NUM_LIMIT; - } else { - suffixLength = 0; - } - } } @Override public int apply(FormattedStringBuilder output, int leftIndex, int rightIndex) { - return formatAsPrefixSuffix(output, leftIndex, rightIndex); + return SimpleFormatterImpl.formatPrefixSuffix(compiledPattern, field, leftIndex, rightIndex, output); } @Override public int getPrefixLength() { - return prefixLength; + return SimpleFormatterImpl.getPrefixLength(compiledPattern); } @Override public int getCodePointCount() { - int count = 0; - if (prefixLength > 0) { - count += Character.codePointCount(compiledPattern, 2, 2 + prefixLength); - } - if (suffixLength > 0) { - count += Character - .codePointCount(compiledPattern, 1 + suffixOffset, 1 + suffixOffset + suffixLength); - } - return count; + return SimpleFormatterImpl.getLength(compiledPattern, true); } @Override @@ -120,49 +85,6 @@ public class SimpleModifier implements Modifier { } /** - * TODO: This belongs in SimpleFormatterImpl. The only reason I haven't moved it there yet is because - * DoubleSidedStringBuilder is an internal class and SimpleFormatterImpl feels like it should not - * depend on it. - * - * <p> - * Formats a value that is already stored inside the StringBuilder <code>result</code> between the - * indices <code>startIndex</code> and <code>endIndex</code> by inserting characters before the start - * index and after the end index. - * - * <p> - * This is well-defined only for patterns with exactly one argument. - * - * @param result - * The StringBuilder containing the value argument. - * @param startIndex - * The left index of the value within the string builder. - * @param endIndex - * The right index of the value within the string builder. - * @return The number of characters (UTF-16 code points) that were added to the StringBuilder. - */ - public int formatAsPrefixSuffix( - FormattedStringBuilder result, - int startIndex, - int endIndex) { - if (suffixOffset == -1) { - // There is no argument for the inner number; overwrite the entire segment with our string. - return result.splice(startIndex, endIndex, compiledPattern, 2, 2 + prefixLength, field); - } else { - if (prefixLength > 0) { - result.insert(startIndex, compiledPattern, 2, 2 + prefixLength, field); - } - if (suffixLength > 0) { - result.insert(endIndex + prefixLength, - compiledPattern, - 1 + suffixOffset, - 1 + suffixOffset + suffixLength, - field); - } - return prefixLength + suffixLength; - } - } - - /** * TODO: Like above, this belongs with the rest of the SimpleFormatterImpl code. * I put it here so that the SimpleFormatter uses in FormattedStringBuilder are near each other. * diff --git a/android_icu4j/src/main/java/android/icu/impl/number/parse/AffixMatcher.java b/android_icu4j/src/main/java/android/icu/impl/number/parse/AffixMatcher.java index a961741b8..b9e70750f 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/parse/AffixMatcher.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/parse/AffixMatcher.java @@ -13,7 +13,7 @@ import android.icu.impl.StringSegment; import android.icu.impl.number.AffixPatternProvider; import android.icu.impl.number.AffixUtils; import android.icu.impl.number.PatternStringUtils; -import android.icu.number.NumberFormatter.SignDisplay; +import android.icu.impl.number.PatternStringUtils.PatternSignType; /** * @author sffc @@ -92,20 +92,27 @@ public class AffixMatcher implements NumberParseMatcher { StringBuilder sb = new StringBuilder(); ArrayList<AffixMatcher> matchers = new ArrayList<>(6); boolean includeUnpaired = 0 != (parseFlags & ParsingUtils.PARSE_FLAG_INCLUDE_UNPAIRED_AFFIXES); - SignDisplay signDisplay = (0 != (parseFlags & ParsingUtils.PARSE_FLAG_PLUS_SIGN_ALLOWED)) - ? SignDisplay.ALWAYS - : SignDisplay.AUTO; AffixPatternMatcher posPrefix = null; AffixPatternMatcher posSuffix = null; // Pre-process the affix strings to resolve LDML rules like sign display. - for (int signum = 1; signum >= -1; signum--) { + for (PatternSignType type : PatternSignType.VALUES) { + + // Skip affixes in some cases + if (type == PatternSignType.POS + && 0 != (parseFlags & ParsingUtils.PARSE_FLAG_PLUS_SIGN_ALLOWED)) { + continue; + } + if (type == PatternSignType.POS_SIGN + && 0 == (parseFlags & ParsingUtils.PARSE_FLAG_PLUS_SIGN_ALLOWED)) { + continue; + } + // Generate Prefix PatternStringUtils.patternInfoToStringBuilder(patternInfo, true, - signum, - signDisplay, + type, StandardPlural.OTHER, false, sb); @@ -115,15 +122,14 @@ public class AffixMatcher implements NumberParseMatcher { // Generate Suffix PatternStringUtils.patternInfoToStringBuilder(patternInfo, false, - signum, - signDisplay, + type, StandardPlural.OTHER, false, sb); AffixPatternMatcher suffix = AffixPatternMatcher .fromAffixPattern(sb.toString(), factory, parseFlags); - if (signum == 1) { + if (type == PatternSignType.POS) { posPrefix = prefix; posSuffix = suffix; } else if (Objects.equals(prefix, posPrefix) && Objects.equals(suffix, posSuffix)) { @@ -132,17 +138,17 @@ public class AffixMatcher implements NumberParseMatcher { } // Flags for setting in the ParsedNumber; the token matchers may add more. - int flags = (signum == -1) ? ParsedNumber.FLAG_NEGATIVE : 0; + int flags = (type == PatternSignType.NEG) ? ParsedNumber.FLAG_NEGATIVE : 0; // Note: it is indeed possible for posPrefix and posSuffix to both be null. // We still need to add that matcher for strict mode to work. matchers.add(getInstance(prefix, suffix, flags)); if (includeUnpaired && prefix != null && suffix != null) { // The following if statements are designed to prevent adding two identical matchers. - if (signum == 1 || !Objects.equals(prefix, posPrefix)) { + if (type == PatternSignType.POS || !Objects.equals(prefix, posPrefix)) { matchers.add(getInstance(prefix, null, flags)); } - if (signum == 1 || !Objects.equals(suffix, posSuffix)) { + if (type == PatternSignType.POS || !Objects.equals(suffix, posSuffix)) { matchers.add(getInstance(null, suffix, flags)); } } diff --git a/android_icu4j/src/main/java/android/icu/impl/number/parse/NumberParserImpl.java b/android_icu4j/src/main/java/android/icu/impl/number/parse/NumberParserImpl.java index 48a71acce..5b847dd65 100644 --- a/android_icu4j/src/main/java/android/icu/impl/number/parse/NumberParserImpl.java +++ b/android_icu4j/src/main/java/android/icu/impl/number/parse/NumberParserImpl.java @@ -11,7 +11,6 @@ import java.util.List; import android.icu.impl.StringSegment; import android.icu.impl.number.AffixPatternProvider; import android.icu.impl.number.AffixUtils; -import android.icu.impl.number.CurrencyPluralInfoAffixProvider; import android.icu.impl.number.CustomSymbolCurrency; import android.icu.impl.number.DecimalFormatProperties; import android.icu.impl.number.DecimalFormatProperties.ParseMode; @@ -140,12 +139,7 @@ public class NumberParserImpl { boolean parseCurrency) { ULocale locale = symbols.getULocale(); - AffixPatternProvider affixProvider; - if (properties.getCurrencyPluralInfo() == null) { - affixProvider = new PropertiesAffixPatternProvider(properties); - } else { - affixProvider = new CurrencyPluralInfoAffixProvider(properties.getCurrencyPluralInfo(), properties); - } + AffixPatternProvider affixProvider = PropertiesAffixPatternProvider.forProperties(properties); Currency currency = CustomSymbolCurrency.resolve(properties.getCurrency(), locale, symbols); ParseMode parseMode = properties.getParseMode(); if (parseMode == null) { diff --git a/android_icu4j/src/main/java/android/icu/number/CompactNotation.java b/android_icu4j/src/main/java/android/icu/number/CompactNotation.java index d0c701291..02aa21e47 100644 --- a/android_icu4j/src/main/java/android/icu/number/CompactNotation.java +++ b/android_icu4j/src/main/java/android/icu/number/CompactNotation.java @@ -66,9 +66,10 @@ public class CompactNotation extends Notation { CompactType compactType, PluralRules rules, MutablePatternModifier buildReference, + boolean safe, MicroPropsGenerator parent) { // TODO: Add a data cache? It would be keyed by locale, nsName, compact type, and compact style. - return new CompactHandler(this, locale, nsName, compactType, rules, buildReference, parent); + return new CompactHandler(this, locale, nsName, compactType, rules, buildReference, safe, parent); } private static class CompactHandler implements MicroPropsGenerator { @@ -76,6 +77,7 @@ public class CompactNotation extends Notation { final PluralRules rules; final MicroPropsGenerator parent; final Map<String, ImmutablePatternModifier> precomputedMods; + final MutablePatternModifier unsafePatternModifier; final CompactData data; private CompactHandler( @@ -85,6 +87,7 @@ public class CompactNotation extends Notation { CompactType compactType, PluralRules rules, MutablePatternModifier buildReference, + boolean safe, MicroPropsGenerator parent) { this.rules = rules; this.parent = parent; @@ -94,13 +97,15 @@ public class CompactNotation extends Notation { } else { data.populate(notation.compactCustomData); } - if (buildReference != null) { + if (safe) { // Safe code path precomputedMods = new HashMap<>(); precomputeAllModifiers(buildReference); + unsafePatternModifier = null; } else { // Unsafe code path precomputedMods = null; + unsafePatternModifier = buildReference; } } @@ -123,11 +128,12 @@ public class CompactNotation extends Notation { // Treat zero, NaN, and infinity as if they had magnitude 0 int magnitude; + int multiplier = 0; if (quantity.isZeroish()) { magnitude = 0; micros.rounder.apply(quantity); } else { - int multiplier = micros.rounder.chooseMultiplierAndApply(quantity, data); + multiplier = micros.rounder.chooseMultiplierAndApply(quantity, data); magnitude = quantity.isZeroish() ? 0 : quantity.getMagnitude(); magnitude -= multiplier; } @@ -145,13 +151,19 @@ public class CompactNotation extends Notation { } else { // Unsafe code path. // Overwrite the PatternInfo in the existing modMiddle. - assert micros.modMiddle instanceof MutablePatternModifier; ParsedPatternInfo patternInfo = PatternStringParser.parseToPatternInfo(patternString); - ((MutablePatternModifier) micros.modMiddle).setPatternInfo(patternInfo, NumberFormat.Field.COMPACT); + unsafePatternModifier.setPatternInfo(patternInfo, NumberFormat.Field.COMPACT); + unsafePatternModifier.setNumberProperties(quantity.signum(), null); + micros.modMiddle = unsafePatternModifier; } + // Change the exponent only after we select appropriate plural form + // for formatting purposes so that we preserve expected formatted + // string behavior. + quantity.adjustExponent(-1 * multiplier); + // We already performed rounding. Do not perform it again. - micros.rounder = Precision.constructPassThrough(); + micros.rounder = null; return micros; } diff --git a/android_icu4j/src/main/java/android/icu/number/FormattedNumber.java b/android_icu4j/src/main/java/android/icu/number/FormattedNumber.java index 04790ff06..3ea745bef 100644 --- a/android_icu4j/src/main/java/android/icu/number/FormattedNumber.java +++ b/android_icu4j/src/main/java/android/icu/number/FormattedNumber.java @@ -5,8 +5,6 @@ package android.icu.number; import java.math.BigDecimal; import java.text.AttributedCharacterIterator; -import java.text.FieldPosition; -import java.util.Arrays; import android.icu.impl.FormattedStringBuilder; import android.icu.impl.FormattedValueStringBuilderImpl; @@ -44,7 +42,7 @@ public class FormattedNumber implements FormattedValue { /** * {@inheritDoc} * - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ @Override public int length() { @@ -54,7 +52,7 @@ public class FormattedNumber implements FormattedValue { /** * {@inheritDoc} * - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ @Override public char charAt(int index) { @@ -64,7 +62,7 @@ public class FormattedNumber implements FormattedValue { /** * {@inheritDoc} * - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ @Override public CharSequence subSequence(int start, int end) { @@ -84,7 +82,7 @@ public class FormattedNumber implements FormattedValue { /** * {@inheritDoc} * - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ @Override public boolean nextPosition(ConstrainedFieldPosition cfpos) { @@ -100,43 +98,6 @@ public class FormattedNumber implements FormattedValue { } /** - * Determines the start (inclusive) and end (exclusive) indices of the next occurrence of the - * given <em>field</em> in the output string. This allows you to determine the locations of, - * for example, the integer part, fraction part, or symbols. - * <p> - * This is a simpler but less powerful alternative to {@link #nextPosition}. - * <p> - * If a field occurs just once, calling this method will find that occurrence and return it. If a - * field occurs multiple times, this method may be called repeatedly with the following pattern: - * - * <pre> - * FieldPosition fpos = new FieldPosition(NumberFormat.Field.GROUPING_SEPARATOR); - * while (formattedNumber.nextFieldPosition(fpos, status)) { - * // do something with fpos. - * } - * </pre> - * <p> - * This method is useful if you know which field to query. If you want all available field position - * information, use {@link #nextPosition} or {@link #toCharacterIterator()}. - * - * @param fieldPosition - * Input+output variable. On input, the "field" property determines which field to look - * up, and the "beginIndex" and "endIndex" properties determine where to begin the search. - * On output, the "beginIndex" is set to the beginning of the first occurrence of the - * field with either begin or end indices after the input indices, "endIndex" is set to - * the end of that occurrence of the field (exclusive index). If a field position is not - * found, the method returns FALSE and the FieldPosition may or may not be changed. - * @return true if a new occurrence of the field was found; false otherwise. - * @see android.icu.text.NumberFormat.Field - * @see NumberFormatter - * @hide draft / provisional / internal are hidden on Android - */ - public boolean nextFieldPosition(FieldPosition fieldPosition) { - fq.populateUFieldPosition(fieldPosition); - return FormattedValueStringBuilderImpl.nextFieldPosition(string, fieldPosition); - } - - /** * Export the formatted number as a BigDecimal. This endpoint is useful for obtaining the exact * number being printed after scaling and rounding have been applied by the number formatting * pipeline. @@ -156,39 +117,4 @@ public class FormattedNumber implements FormattedValue { public IFixedDecimal getFixedDecimal() { return fq; } - - /** - * {@inheritDoc} - * - * @hide draft / provisional / internal are hidden on Android - */ - @Override - public int hashCode() { - // FormattedStringBuilder and BigDecimal are mutable, so we can't call - // #equals() or #hashCode() on them directly. - return Arrays.hashCode(string.toCharArray()) - ^ Arrays.hashCode(string.toFieldArray()) - ^ fq.toBigDecimal().hashCode(); - } - - /** - * {@inheritDoc} - * - * @hide draft / provisional / internal are hidden on Android - */ - @Override - public boolean equals(Object other) { - if (this == other) - return true; - if (other == null) - return false; - if (!(other instanceof FormattedNumber)) - return false; - // FormattedStringBuilder and BigDecimal are mutable, so we can't call - // #equals() or #hashCode() on them directly. - FormattedNumber _other = (FormattedNumber) other; - return Arrays.equals(string.toCharArray(), _other.string.toCharArray()) - && Arrays.equals(string.toFieldArray(), _other.string.toFieldArray()) - && fq.toBigDecimal().equals(_other.fq.toBigDecimal()); - } }
\ No newline at end of file diff --git a/android_icu4j/src/main/java/android/icu/number/FormattedNumberRange.java b/android_icu4j/src/main/java/android/icu/number/FormattedNumberRange.java index c9a6183ad..a17e56af6 100644 --- a/android_icu4j/src/main/java/android/icu/number/FormattedNumberRange.java +++ b/android_icu4j/src/main/java/android/icu/number/FormattedNumberRange.java @@ -6,7 +6,6 @@ package android.icu.number; import java.io.IOException; import java.math.BigDecimal; import java.text.AttributedCharacterIterator; -import java.text.FieldPosition; import java.util.Arrays; import android.icu.impl.FormattedStringBuilder; @@ -67,7 +66,7 @@ public class FormattedNumberRange implements FormattedValue { /** * {@inheritDoc} * - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ @Override public char charAt(int index) { @@ -77,7 +76,7 @@ public class FormattedNumberRange implements FormattedValue { /** * {@inheritDoc} * - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ @Override public int length() { @@ -87,7 +86,7 @@ public class FormattedNumberRange implements FormattedValue { /** * {@inheritDoc} * - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ @Override public CharSequence subSequence(int start, int end) { @@ -97,7 +96,7 @@ public class FormattedNumberRange implements FormattedValue { /** * {@inheritDoc} * - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ @Override public boolean nextPosition(ConstrainedFieldPosition cfpos) { @@ -105,38 +104,6 @@ public class FormattedNumberRange implements FormattedValue { } /** - * Determines the start (inclusive) and end (exclusive) indices of the next occurrence of the given - * <em>field</em> in the output string. This allows you to determine the locations of, for example, - * the integer part, fraction part, or symbols. - * <p> - * If both sides of the range have the same field, the field will occur twice, once before the range separator and - * once after the range separator, if applicable. - * <p> - * If a field occurs just once, calling this method will find that occurrence and return it. If a field occurs - * multiple times, this method may be called repeatedly with the following pattern: - * - * <pre> - * FieldPosition fpos = new FieldPosition(NumberFormat.Field.INTEGER); - * while (formattedNumberRange.nextFieldPosition(fpos, status)) { - * // do something with fpos. - * } - * </pre> - * <p> - * This method is useful if you know which field to query. If you want all available field position information, use - * {@link #toCharacterIterator()}. - * - * @param fieldPosition - * Input+output variable. See {@link FormattedNumber#nextFieldPosition(FieldPosition)}. - * @return true if a new occurrence of the field was found; false otherwise. - * @see android.icu.text.NumberFormat.Field - * @see NumberRangeFormatter - * @hide draft / provisional / internal are hidden on Android - */ - public boolean nextFieldPosition(FieldPosition fieldPosition) { - return FormattedValueStringBuilderImpl.nextFieldPosition(string, fieldPosition); - } - - /** * {@inheritDoc} */ @Override @@ -184,7 +151,7 @@ public class FormattedNumberRange implements FormattedValue { /** * {@inheritDoc} * - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ @Override public int hashCode() { @@ -197,7 +164,7 @@ public class FormattedNumberRange implements FormattedValue { /** * {@inheritDoc} * - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ @Override public boolean equals(Object other) { diff --git a/android_icu4j/src/main/java/android/icu/number/LocalizedNumberFormatter.java b/android_icu4j/src/main/java/android/icu/number/LocalizedNumberFormatter.java index 99acd6a32..181831fbb 100644 --- a/android_icu4j/src/main/java/android/icu/number/LocalizedNumberFormatter.java +++ b/android_icu4j/src/main/java/android/icu/number/LocalizedNumberFormatter.java @@ -93,20 +93,11 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN * @see NumberFormatter */ public FormattedNumber format(Measure input) { + DecimalQuantity fq = new DecimalQuantity_DualStorageBCD(input.getNumber()); MeasureUnit unit = input.getUnit(); - Number number = input.getNumber(); - // Use this formatter if possible - if (Objects.equals(resolve().unit, unit)) { - return format(number); - } - // This mechanism saves the previously used unit, so if the user calls this method with the - // same unit multiple times in a row, they get a more efficient code path. - LocalizedNumberFormatter withUnit = savedWithUnit; - if (withUnit == null || !Objects.equals(withUnit.resolve().unit, unit)) { - withUnit = new LocalizedNumberFormatter(this, KEY_UNIT, unit); - savedWithUnit = withUnit; - } - return withUnit.format(number); + FormattedStringBuilder string = new FormattedStringBuilder(); + formatImpl(fq, unit, string); + return new FormattedNumber(string, fq); } /** @@ -157,6 +148,29 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN } /** + * Version of above for unit override. + * + * @deprecated ICU 67 This API is ICU internal only. + * @hide draft / provisional / internal are hidden on Android + */ + @Deprecated + public void formatImpl(DecimalQuantity fq, MeasureUnit unit, FormattedStringBuilder string) { + // Use this formatter if possible + if (Objects.equals(resolve().unit, unit)) { + formatImpl(fq, string); + return; + } + // This mechanism saves the previously used unit, so if the user calls this method with the + // same unit multiple times in a row, they get a more efficient code path. + LocalizedNumberFormatter withUnit = savedWithUnit; + if (withUnit == null || !Objects.equals(withUnit.resolve().unit, unit)) { + withUnit = new LocalizedNumberFormatter(this, KEY_UNIT, unit); + savedWithUnit = withUnit; + } + withUnit.formatImpl(fq, string); + } + + /** * @deprecated This API is ICU internal only. Use {@link FormattedNumber#nextPosition} * for related functionality. * @hide draft / provisional / internal are hidden on Android diff --git a/android_icu4j/src/main/java/android/icu/number/NumberFormatter.java b/android_icu4j/src/main/java/android/icu/number/NumberFormatter.java index 11782924e..1b20071a9 100644 --- a/android_icu4j/src/main/java/android/icu/number/NumberFormatter.java +++ b/android_icu4j/src/main/java/android/icu/number/NumberFormatter.java @@ -127,8 +127,10 @@ public final class NumberFormatter { FULL_NAME, /** - * Use the three-digit ISO XXX code in place of the symbol for displaying currencies. The - * behavior of this option is currently undefined for use with measure units. + * Use the three-digit ISO XXX code in place of the symbol for displaying currencies. + * + * <p> + * Behavior of this option with non-currency units is not defined at this time. * * <p> * In CLDR, this option corresponds to the "¤¤" placeholder for currencies. @@ -138,6 +140,30 @@ public final class NumberFormatter { ISO_CODE, /** + * Use the formal variant of the currency symbol; for example, "NT$" for the New Taiwan + * dollar in zh-TW. + * + * <p> + * Behavior of this option with non-currency units is not defined at this time. + * + * @see NumberFormatter + * @hide draft / provisional / internal are hidden on Android + */ + FORMAL, + + /** + * Use the alternate variant of the currency symbol; for example, "TL" for the Turkish + * lira (TRY). + * + * <p> + * Behavior of this option with non-currency units is not defined at this time. + * + * @see NumberFormatter + * @hide draft / provisional / internal are hidden on Android + */ + VARIANT, + + /** * Format the number according to the specified unit, but do not display the unit. For * currencies, apply monetary symbols and formats as with SHORT, but omit the currency symbol. * For measure units, the behavior is equivalent to not specifying the unit at all. @@ -309,7 +335,7 @@ public final class NumberFormatter { /** * Show the minus sign on negative numbers and the plus sign on positive numbers. Do not show a - * sign on zero or NaN, unless the sign bit is set (-0.0 gets a sign). + * sign on zero, numbers that round to zero, or NaN. * * @see NumberFormatter */ @@ -317,9 +343,8 @@ public final class NumberFormatter { /** * Use the locale-dependent accounting format on negative numbers, and show the plus sign on - * positive numbers. Do not show a sign on zero or NaN, unless the sign bit is set (-0.0 gets a - * sign). For more information on the accounting format, see the ACCOUNTING sign display - * strategy. + * positive numbers. Do not show a sign on zero, numbers that round to zero, or NaN. For more + * information on the accounting format, see the ACCOUNTING sign display strategy. * * @see NumberFormatter */ diff --git a/android_icu4j/src/main/java/android/icu/number/NumberFormatterImpl.java b/android_icu4j/src/main/java/android/icu/number/NumberFormatterImpl.java index 5971d831a..0ec562e28 100644 --- a/android_icu4j/src/main/java/android/icu/number/NumberFormatterImpl.java +++ b/android_icu4j/src/main/java/android/icu/number/NumberFormatterImpl.java @@ -16,6 +16,7 @@ import android.icu.impl.number.MicroProps; import android.icu.impl.number.MicroPropsGenerator; import android.icu.impl.number.MultiplierFormatHandler; import android.icu.impl.number.MutablePatternModifier; +import android.icu.impl.number.MutablePatternModifier.ImmutablePatternModifier; import android.icu.impl.number.Padder; import android.icu.impl.number.PatternStringParser; import android.icu.impl.number.PatternStringParser.ParsedPatternInfo; @@ -96,7 +97,6 @@ class NumberFormatterImpl { */ public MicroProps preProcess(DecimalQuantity inValue) { MicroProps micros = microPropsGenerator.processQuantity(inValue); - micros.rounder.apply(inValue); if (micros.integerWidth.maxInt == -1) { inValue.setMinInteger(micros.integerWidth.minInt); } else { @@ -110,7 +110,6 @@ class NumberFormatterImpl { MicroProps micros = new MicroProps(false); MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, micros, false); micros = microPropsGenerator.processQuantity(inValue); - micros.rounder.apply(inValue); if (micros.integerWidth.maxInt == -1) { inValue.setMinInteger(micros.integerWidth.minInt); } else { @@ -335,10 +334,9 @@ class NumberFormatterImpl { } else { patternMod.setSymbols(micros.symbols, currency, unitWidth, null); } + ImmutablePatternModifier immPatternMod = null; if (safe) { - chain = patternMod.createImmutableAndChain(chain); - } else { - chain = patternMod.addToChain(chain); + immPatternMod = patternMod.createImmutable(); } // Outer modifier (CLDR units and currency long names) @@ -361,8 +359,6 @@ class NumberFormatterImpl { } // Compact notation - // NOTE: Compact notation can (but might not) override the middle modifier and rounding. - // It therefore needs to go at the end of the chain. if (macros.notation instanceof CompactNotation) { if (rules == null) { // Lazily create PluralRules @@ -375,10 +371,18 @@ class NumberFormatterImpl { micros.nsName, compactType, rules, - safe ? patternMod : null, + patternMod, + safe, chain); } + // Always add the pattern modifier as the last element of the chain. + if (safe) { + chain = immPatternMod.addToChain(chain); + } else { + chain = patternMod.addToChain(chain); + } + return chain; } @@ -434,6 +438,19 @@ class NumberFormatterImpl { // Add the fraction digits length += writeFractionDigits(micros, quantity, string, length + index); + + if (length == 0) { + // Force output of the digit for value 0 + if (micros.symbols.getCodePointZero() != -1) { + length += string.insertCodePoint(index, + micros.symbols.getCodePointZero(), + NumberFormat.Field.INTEGER); + } else { + length += string.insert(index, + micros.symbols.getDigitStringsLocal()[0], + NumberFormat.Field.INTEGER); + } + } } return length; diff --git a/android_icu4j/src/main/java/android/icu/number/NumberPropertyMapper.java b/android_icu4j/src/main/java/android/icu/number/NumberPropertyMapper.java index d86921c00..f76e71d99 100644 --- a/android_icu4j/src/main/java/android/icu/number/NumberPropertyMapper.java +++ b/android_icu4j/src/main/java/android/icu/number/NumberPropertyMapper.java @@ -7,7 +7,6 @@ import java.math.BigDecimal; import java.math.MathContext; import android.icu.impl.number.AffixPatternProvider; -import android.icu.impl.number.CurrencyPluralInfoAffixProvider; import android.icu.impl.number.CustomSymbolCurrency; import android.icu.impl.number.DecimalFormatProperties; import android.icu.impl.number.Grouper; @@ -105,12 +104,7 @@ final class NumberPropertyMapper { // AFFIXES // ///////////// - AffixPatternProvider affixProvider; - if (properties.getCurrencyPluralInfo() == null) { - affixProvider = new PropertiesAffixPatternProvider(properties); - } else { - affixProvider = new CurrencyPluralInfoAffixProvider(properties.getCurrencyPluralInfo(), properties); - } + AffixPatternProvider affixProvider = PropertiesAffixPatternProvider.forProperties(properties); macros.affixProvider = affixProvider; /////////// @@ -162,10 +156,8 @@ final class NumberPropertyMapper { } // Validate min/max int/frac. // For backwards compatibility, minimum overrides maximum if the two conflict. - // The following logic ensures that there is always a minimum of at least one digit. if (minInt == 0 && maxFrac != 0) { - // Force a digit after the decimal point. - minFrac = minFrac <= 0 ? 1 : minFrac; + minFrac = (minFrac < 0 || (minFrac == 0 && maxInt == 0)) ? 1 : minFrac; maxFrac = maxFrac < 0 ? -1 : maxFrac < minFrac ? minFrac : maxFrac; minInt = 0; maxInt = maxInt < 0 ? -1 : maxInt > RoundingUtils.MAX_INT_FRAC_SIG ? -1 : maxInt; diff --git a/android_icu4j/src/main/java/android/icu/number/NumberSkeletonImpl.java b/android_icu4j/src/main/java/android/icu/number/NumberSkeletonImpl.java index 114e541ee..8dfb9ecf7 100644 --- a/android_icu4j/src/main/java/android/icu/number/NumberSkeletonImpl.java +++ b/android_icu4j/src/main/java/android/icu/number/NumberSkeletonImpl.java @@ -54,6 +54,7 @@ class NumberSkeletonImpl { STATE_INCREMENT_PRECISION, STATE_MEASURE_UNIT, STATE_PER_MEASURE_UNIT, + STATE_IDENTIFIER_UNIT, STATE_CURRENCY_UNIT, STATE_INTEGER_WIDTH, STATE_NUMBERING_SYSTEM, @@ -77,6 +78,7 @@ class NumberSkeletonImpl { STEM_BASE_UNIT, STEM_PERCENT, STEM_PERMILLE, + STEM_PERCENT_100, // concise-only STEM_PRECISION_INTEGER, STEM_PRECISION_UNLIMITED, STEM_PRECISION_CURRENCY_STANDARD, @@ -99,6 +101,8 @@ class NumberSkeletonImpl { STEM_UNIT_WIDTH_SHORT, STEM_UNIT_WIDTH_FULL_NAME, STEM_UNIT_WIDTH_ISO_CODE, + STEM_UNIT_WIDTH_FORMAL, + STEM_UNIT_WIDTH_VARIANT, STEM_UNIT_WIDTH_HIDDEN, STEM_SIGN_AUTO, STEM_SIGN_ALWAYS, @@ -114,12 +118,24 @@ class NumberSkeletonImpl { STEM_PRECISION_INCREMENT, STEM_MEASURE_UNIT, STEM_PER_MEASURE_UNIT, + STEM_UNIT, STEM_CURRENCY, STEM_INTEGER_WIDTH, STEM_NUMBERING_SYSTEM, STEM_SCALE, }; + /** Default wildcard char, accepted on input and printed in output */ + static final char WILDCARD_CHAR = '*'; + + /** Alternative wildcard char, accept on input but not printed in output */ + static final char ALT_WILDCARD_CHAR = '+'; + + /** Checks whether the char is a wildcard on input */ + static boolean isWildcardChar(char c) { + return c == WILDCARD_CHAR || c == ALT_WILDCARD_CHAR; + } + /** For mapping from ordinal back to StemEnum in Java. */ static final StemEnum[] STEM_ENUM_VALUES = StemEnum.values(); @@ -160,6 +176,8 @@ class NumberSkeletonImpl { b.add("unit-width-short", StemEnum.STEM_UNIT_WIDTH_SHORT.ordinal()); b.add("unit-width-full-name", StemEnum.STEM_UNIT_WIDTH_FULL_NAME.ordinal()); b.add("unit-width-iso-code", StemEnum.STEM_UNIT_WIDTH_ISO_CODE.ordinal()); + b.add("unit-width-formal", StemEnum.STEM_UNIT_WIDTH_FORMAL.ordinal()); + b.add("unit-width-variant", StemEnum.STEM_UNIT_WIDTH_VARIANT.ordinal()); b.add("unit-width-hidden", StemEnum.STEM_UNIT_WIDTH_HIDDEN.ordinal()); b.add("sign-auto", StemEnum.STEM_SIGN_AUTO.ordinal()); b.add("sign-always", StemEnum.STEM_SIGN_ALWAYS.ordinal()); @@ -175,11 +193,27 @@ class NumberSkeletonImpl { b.add("precision-increment", StemEnum.STEM_PRECISION_INCREMENT.ordinal()); b.add("measure-unit", StemEnum.STEM_MEASURE_UNIT.ordinal()); b.add("per-measure-unit", StemEnum.STEM_PER_MEASURE_UNIT.ordinal()); + b.add("unit", StemEnum.STEM_UNIT.ordinal()); b.add("currency", StemEnum.STEM_CURRENCY.ordinal()); b.add("integer-width", StemEnum.STEM_INTEGER_WIDTH.ordinal()); b.add("numbering-system", StemEnum.STEM_NUMBERING_SYSTEM.ordinal()); b.add("scale", StemEnum.STEM_SCALE.ordinal()); + // Section 3 (concise tokens): + b.add("K", StemEnum.STEM_COMPACT_SHORT.ordinal()); + b.add("KK", StemEnum.STEM_COMPACT_LONG.ordinal()); + b.add("%", StemEnum.STEM_PERCENT.ordinal()); + b.add("%x100", StemEnum.STEM_PERCENT_100.ordinal()); + b.add(",_", StemEnum.STEM_GROUP_OFF.ordinal()); + b.add(",?", StemEnum.STEM_GROUP_MIN2.ordinal()); + b.add(",!", StemEnum.STEM_GROUP_ON_ALIGNED.ordinal()); + b.add("+!", StemEnum.STEM_SIGN_ALWAYS.ordinal()); + b.add("+_", StemEnum.STEM_SIGN_NEVER.ordinal()); + b.add("()", StemEnum.STEM_SIGN_ACCOUNTING.ordinal()); + b.add("()!", StemEnum.STEM_SIGN_ACCOUNTING_ALWAYS.ordinal()); + b.add("+?", StemEnum.STEM_SIGN_EXCEPT_ZERO.ordinal()); + b.add("()?", StemEnum.STEM_SIGN_ACCOUNTING_EXCEPT_ZERO.ordinal()); + // Build the CharsTrie // TODO: Use SLOW or FAST here? return b.buildCharSequence(StringTrieBuilder.Option.FAST).toString(); @@ -286,6 +320,10 @@ class NumberSkeletonImpl { return UnitWidth.FULL_NAME; case STEM_UNIT_WIDTH_ISO_CODE: return UnitWidth.ISO_CODE; + case STEM_UNIT_WIDTH_FORMAL: + return UnitWidth.FORMAL; + case STEM_UNIT_WIDTH_VARIANT: + return UnitWidth.VARIANT; case STEM_UNIT_WIDTH_HIDDEN: return UnitWidth.HIDDEN; default: @@ -399,6 +437,12 @@ class NumberSkeletonImpl { case ISO_CODE: sb.append("unit-width-iso-code"); break; + case FORMAL: + sb.append("unit-width-formal"); + break; + case VARIANT: + sb.append("unit-width-variant"); + break; case HIDDEN: sb.append("unit-width-hidden"); break; @@ -605,6 +649,14 @@ class NumberSkeletonImpl { checkNull(macros.precision, segment); BlueprintHelpers.parseDigitsStem(segment, macros); return ParseState.STATE_NULL; + case 'E': + checkNull(macros.notation, segment); + BlueprintHelpers.parseScientificStem(segment, macros); + return ParseState.STATE_NULL; + case '0': + checkNull(macros.notation, segment); + BlueprintHelpers.parseIntegerStem(segment, macros); + return ParseState.STATE_NULL; } // Now look at the stemsTrie, which is already be pointing at our stem. @@ -642,6 +694,13 @@ class NumberSkeletonImpl { macros.unit = StemToObject.unit(stem); return ParseState.STATE_NULL; + case STEM_PERCENT_100: + checkNull(macros.scale, segment); + checkNull(macros.unit, segment); + macros.scale = Scale.powerOfTen(2); + macros.unit = NoUnit.PERCENT; + return ParseState.STATE_NULL; + case STEM_PRECISION_INTEGER: case STEM_PRECISION_UNLIMITED: case STEM_PRECISION_CURRENCY_STANDARD: @@ -685,6 +744,8 @@ class NumberSkeletonImpl { case STEM_UNIT_WIDTH_SHORT: case STEM_UNIT_WIDTH_FULL_NAME: case STEM_UNIT_WIDTH_ISO_CODE: + case STEM_UNIT_WIDTH_FORMAL: + case STEM_UNIT_WIDTH_VARIANT: case STEM_UNIT_WIDTH_HIDDEN: checkNull(macros.unitWidth, segment); macros.unitWidth = StemToObject.unitWidth(stem); @@ -721,6 +782,11 @@ class NumberSkeletonImpl { checkNull(macros.perUnit, segment); return ParseState.STATE_PER_MEASURE_UNIT; + case STEM_UNIT: + checkNull(macros.unit, segment); + checkNull(macros.perUnit, segment); + return ParseState.STATE_IDENTIFIER_UNIT; + case STEM_CURRENCY: checkNull(macros.unit, segment); return ParseState.STATE_CURRENCY_UNIT; @@ -762,6 +828,9 @@ class NumberSkeletonImpl { case STATE_PER_MEASURE_UNIT: BlueprintHelpers.parseMeasurePerUnitOption(segment, macros); return ParseState.STATE_NULL; + case STATE_IDENTIFIER_UNIT: + BlueprintHelpers.parseIdentifierUnitOption(segment, macros); + return ParseState.STATE_NULL; case STATE_INCREMENT_PRECISION: BlueprintHelpers.parseIncrementOption(segment, macros); return ParseState.STATE_NULL; @@ -883,7 +952,7 @@ class NumberSkeletonImpl { /** @return Whether we successfully found and parsed an exponent width option. */ private static boolean parseExponentWidthOption(StringSegment segment, MacroProps macros) { - if (segment.charAt(0) != '+') { + if (!isWildcardChar(segment.charAt(0))) { return false; } int offset = 1; @@ -904,7 +973,7 @@ class NumberSkeletonImpl { } private static void generateExponentWidthOption(int minExponentDigits, StringBuilder sb) { - sb.append('+'); + sb.append(WILDCARD_CHAR); appendMultiple(sb, 'e', minExponentDigits); } @@ -971,7 +1040,7 @@ class NumberSkeletonImpl { } private static void parseMeasurePerUnitOption(StringSegment segment, MacroProps macros) { - // A little bit of a hack: safe the current unit (numerator), call the main measure unit + // A little bit of a hack: save the current unit (numerator), call the main measure unit // parsing code, put back the numerator unit, and put the new unit into per-unit. MeasureUnit numerator = macros.unit; parseMeasureUnitOption(segment, macros); @@ -979,6 +1048,17 @@ class NumberSkeletonImpl { macros.unit = numerator; } + private static void parseIdentifierUnitOption(StringSegment segment, MacroProps macros) { + MeasureUnit[] units = MeasureUnit.parseCoreUnitIdentifier(segment.asString()); + if (units == null) { + throw new SkeletonSyntaxException("Invalid core unit identifier", segment); + } + macros.unit = units[0]; + if (units.length == 2) { + macros.perUnit = units[1]; + } + } + private static void parseFractionStem(StringSegment segment, MacroProps macros) { assert segment.charAt(0) == '.'; int offset = 1; @@ -992,7 +1072,7 @@ class NumberSkeletonImpl { } } if (offset < segment.length()) { - if (segment.charAt(offset) == '+') { + if (isWildcardChar(segment.charAt(offset))) { maxFrac = -1; offset++; } else { @@ -1013,7 +1093,11 @@ class NumberSkeletonImpl { } // Use the public APIs to enforce bounds checking if (maxFrac == -1) { - macros.precision = Precision.minFraction(minFrac); + if (minFrac == 0) { + macros.precision = Precision.unlimited(); + } else { + macros.precision = Precision.minFraction(minFrac); + } } else { macros.precision = Precision.minMaxFraction(minFrac, maxFrac); } @@ -1027,7 +1111,7 @@ class NumberSkeletonImpl { sb.append('.'); appendMultiple(sb, '0', minFrac); if (maxFrac == -1) { - sb.append('+'); + sb.append(WILDCARD_CHAR); } else { appendMultiple(sb, '#', maxFrac - minFrac); } @@ -1046,7 +1130,7 @@ class NumberSkeletonImpl { } } if (offset < segment.length()) { - if (segment.charAt(offset) == '+') { + if (isWildcardChar(segment.charAt(offset))) { maxSig = -1; offset++; } else { @@ -1076,12 +1160,77 @@ class NumberSkeletonImpl { private static void generateDigitsStem(int minSig, int maxSig, StringBuilder sb) { appendMultiple(sb, '@', minSig); if (maxSig == -1) { - sb.append('+'); + sb.append(WILDCARD_CHAR); } else { appendMultiple(sb, '#', maxSig - minSig); } } + private static void parseScientificStem(StringSegment segment, MacroProps macros) { + assert(segment.charAt(0) == 'E'); + block: + { + int offset = 1; + if (segment.length() == offset) { + break block; + } + boolean isEngineering = false; + if (segment.charAt(offset) == 'E') { + isEngineering = true; + offset++; + if (segment.length() == offset) { + break block; + } + } + SignDisplay signDisplay = SignDisplay.AUTO; + if (segment.charAt(offset) == '+') { + offset++; + if (segment.length() == offset) { + break block; + } + if (segment.charAt(offset) == '!') { + signDisplay = SignDisplay.ALWAYS; + } else if (segment.charAt(offset) == '?') { + signDisplay = SignDisplay.EXCEPT_ZERO; + } else { + break block; + } + offset++; + if (segment.length() == offset) { + break block; + } + } + int minDigits = 0; + for (; offset < segment.length(); offset++) { + if (segment.charAt(offset) != '0') { + break block; + } + minDigits++; + } + macros.notation = (isEngineering ? Notation.engineering() : Notation.scientific()) + .withExponentSignDisplay(signDisplay) + .withMinExponentDigits(minDigits); + return; + } + throw new SkeletonSyntaxException("Invalid scientific stem", segment); + } + + private static void parseIntegerStem(StringSegment segment, MacroProps macros) { + assert(segment.charAt(0) == '0'); + int offset = 1; + for (; offset < segment.length(); offset++) { + if (segment.charAt(offset) != '0') { + offset--; + break; + } + } + if (offset < segment.length()) { + throw new SkeletonSyntaxException("Invalid integer stem", segment); + } + macros.integerWidth = IntegerWidth.zeroFillTo(offset); + return; + } + /** @return Whether we successfully found and parsed a frac-sig option. */ private static boolean parseFracSigOption(StringSegment segment, MacroProps macros) { if (segment.charAt(0) != '@') { @@ -1103,7 +1252,7 @@ class NumberSkeletonImpl { // Invalid: @, @@, @@@ // Invalid: @@#, @@##, @@@# if (offset < segment.length()) { - if (segment.charAt(offset) == '+') { + if (isWildcardChar(segment.charAt(offset))) { maxSig = -1; offset++; } else if (minSig > 1) { @@ -1157,7 +1306,7 @@ class NumberSkeletonImpl { int offset = 0; int minInt = 0; int maxInt; - if (segment.charAt(0) == '+') { + if (isWildcardChar(segment.charAt(0))) { maxInt = -1; offset++; } else { @@ -1195,7 +1344,7 @@ class NumberSkeletonImpl { private static void generateIntegerWidthOption(int minInt, int maxInt, StringBuilder sb) { if (maxInt == -1) { - sb.append('+'); + sb.append(WILDCARD_CHAR); } else { appendMultiple(sb, '#', maxInt - minInt); } diff --git a/android_icu4j/src/main/java/android/icu/number/Precision.java b/android_icu4j/src/main/java/android/icu/number/Precision.java index f60520cd2..e0dd17433 100644 --- a/android_icu4j/src/main/java/android/icu/number/Precision.java +++ b/android_icu4j/src/main/java/android/icu/number/Precision.java @@ -370,8 +370,6 @@ public abstract class Precision { static final CurrencyRounderImpl MONETARY_STANDARD = new CurrencyRounderImpl(CurrencyUsage.STANDARD); static final CurrencyRounderImpl MONETARY_CASH = new CurrencyRounderImpl(CurrencyUsage.CASH); - static final PassThroughRounderImpl PASS_THROUGH = new PassThroughRounderImpl(); - static Precision constructInfinite() { return NONE; } @@ -460,10 +458,6 @@ public abstract class Precision { return returnValue.withMode(base.mathContext); } - static Precision constructPassThrough() { - return PASS_THROUGH; - } - /** * Returns a valid working Rounder. If the Rounder is a CurrencyRounder, applies the given currency. * Otherwise, simply passes through the argument. @@ -755,24 +749,6 @@ public abstract class Precision { } } - static class PassThroughRounderImpl extends Precision { - - public PassThroughRounderImpl() { - } - - @Override - Precision createCopy() { - PassThroughRounderImpl copy = new PassThroughRounderImpl(); - copy.mathContext = mathContext; - return copy; - } - - @Override - public void apply(DecimalQuantity value) { - // TODO: Assert that value has already been rounded - } - } - private static int getRoundingMagnitudeFraction(int maxFrac) { if (maxFrac == -1) { return Integer.MIN_VALUE; diff --git a/android_icu4j/src/main/java/android/icu/number/ScientificNotation.java b/android_icu4j/src/main/java/android/icu/number/ScientificNotation.java index 145ecb1cb..591703c28 100644 --- a/android_icu4j/src/main/java/android/icu/number/ScientificNotation.java +++ b/android_icu4j/src/main/java/android/icu/number/ScientificNotation.java @@ -190,8 +190,13 @@ public class ScientificNotation extends Notation { micros.modInner = this; } + // Change the exponent only after we select appropriate plural form + // for formatting purposes so that we preserve expected formatted + // string behavior. + quantity.adjustExponent(exponent); + // We already performed rounding. Do not perform it again. - micros.rounder = Precision.constructPassThrough(); + micros.rounder = null; return micros; } diff --git a/android_icu4j/src/main/java/android/icu/text/ChineseDateFormat.java b/android_icu4j/src/main/java/android/icu/text/ChineseDateFormat.java index 7b6495c63..5e8c05a83 100644 --- a/android_icu4j/src/main/java/android/icu/text/ChineseDateFormat.java +++ b/android_icu4j/src/main/java/android/icu/text/ChineseDateFormat.java @@ -128,12 +128,13 @@ public class ChineseDateFormat extends SimpleDateFormat { char ch, int count, int beginOffset, int fieldNum, DisplayContext capitalizationContext, FieldPosition pos, + char patternCharToOutput, Calendar cal) { // Logic to handle 'G' for chinese calendar is moved into SimpleDateFormat, // and obsolete pattern char 'l' is now ignored in SimpleDateFormat, so we // just use its implementation - super.subFormat(buf, ch, count, beginOffset, fieldNum, capitalizationContext, pos, cal); + super.subFormat(buf, ch, count, beginOffset, fieldNum, capitalizationContext, pos, patternCharToOutput, cal); // The following is no longer an issue for this subclass... // TODO: add code to set FieldPosition for 'G' and 'l' fields. This diff --git a/android_icu4j/src/main/java/android/icu/text/ConstrainedFieldPosition.java b/android_icu4j/src/main/java/android/icu/text/ConstrainedFieldPosition.java index 3e7cb9ddf..139162fd0 100644 --- a/android_icu4j/src/main/java/android/icu/text/ConstrainedFieldPosition.java +++ b/android_icu4j/src/main/java/android/icu/text/ConstrainedFieldPosition.java @@ -17,7 +17,6 @@ import java.util.Objects; * * @author sffc * @hide Only a subset of ICU is exposed in Android - * @hide draft / provisional / internal are hidden on Android */ public class ConstrainedFieldPosition { @@ -78,8 +77,6 @@ public class ConstrainedFieldPosition { * Initializes a CategoryFieldPosition. * * By default, the CategoryFieldPosition has no iteration constraints. - * - * @hide draft / provisional / internal are hidden on Android */ public ConstrainedFieldPosition() { reset(); @@ -90,8 +87,6 @@ public class ConstrainedFieldPosition { * * - Removes any constraints that may have been set on the instance. * - Resets the iteration position. - * - * @hide draft / provisional / internal are hidden on Android */ public void reset() { fConstraint = ConstraintType.NONE; @@ -126,7 +121,6 @@ public class ConstrainedFieldPosition { * * @param field * The field to fix when iterating. - * @hide draft / provisional / internal are hidden on Android */ public void constrainField(Field field) { if (field == null) { @@ -158,7 +152,6 @@ public class ConstrainedFieldPosition { * * @param classConstraint * The field class to fix when iterating. - * @hide draft / provisional / internal are hidden on Android */ public void constrainClass(Class<?> classConstraint) { if (classConstraint == null) { @@ -208,7 +201,6 @@ public class ConstrainedFieldPosition { * FormattedValue#nextPosition returns TRUE. * * @return The field saved in the instance. See above for null conditions. - * @hide draft / provisional / internal are hidden on Android */ public Field getField() { return fField; @@ -220,7 +212,6 @@ public class ConstrainedFieldPosition { * The return value is well-defined only after FormattedValue#nextPosition returns TRUE. * * @return The start index saved in the instance. - * @hide draft / provisional / internal are hidden on Android */ public int getStart() { return fStart; @@ -232,7 +223,6 @@ public class ConstrainedFieldPosition { * The return value is well-defined only after FormattedValue#nextPosition returns TRUE. * * @return The end index saved in the instance. - * @hide draft / provisional / internal are hidden on Android */ public int getLimit() { return fLimit; @@ -244,7 +234,6 @@ public class ConstrainedFieldPosition { * The return value is well-defined only after FormattedValue#nextPosition returns TRUE. * * @return The value for the current position. Might be null. - * @hide draft / provisional / internal are hidden on Android */ public Object getFieldValue() { return fValue; @@ -258,7 +247,6 @@ public class ConstrainedFieldPosition { * Users of FormattedValue should not need to call this method. * * @return The current iteration context from {@link #setInt64IterationContext}. - * @hide draft / provisional / internal are hidden on Android */ public long getInt64IterationContext() { return fContext; @@ -271,7 +259,6 @@ public class ConstrainedFieldPosition { * * @param context * The new iteration context. - * @hide draft / provisional / internal are hidden on Android */ public void setInt64IterationContext(long context) { fContext = context; @@ -293,7 +280,6 @@ public class ConstrainedFieldPosition { * The new inclusive start index. * @param limit * The new exclusive end index. - * @hide draft / provisional / internal are hidden on Android */ public void setState(Field field, Object value, int start, int limit) { // Check matchesField only as an assertion (debug build) @@ -314,7 +300,6 @@ public class ConstrainedFieldPosition { * @param field The field to test. * @param fieldValue The field value to test. Should be null if there is no value. * @return Whether the field should be included given the constraints. - * @hide draft / provisional / internal are hidden on Android */ public boolean matchesField(Field field, Object fieldValue) { if (field == null) { @@ -337,7 +322,6 @@ public class ConstrainedFieldPosition { /** * {@inheritDoc} - * @hide draft / provisional / internal are hidden on Android */ @Override public String toString() { diff --git a/android_icu4j/src/main/java/android/icu/text/CurrencyDisplayNames.java b/android_icu4j/src/main/java/android/icu/text/CurrencyDisplayNames.java index 18c38957b..dcdcb7307 100644 --- a/android_icu4j/src/main/java/android/icu/text/CurrencyDisplayNames.java +++ b/android_icu4j/src/main/java/android/icu/text/CurrencyDisplayNames.java @@ -106,9 +106,10 @@ public abstract class CurrencyDisplayNames { public abstract ULocale getULocale(); /** - * Returns the symbol for the currency with the provided ISO code. If - * there is no data for the ISO code, substitutes isoCode, or returns null - * if noSubstitute was set in the factory method. + * Returns the symbol for the currency with the provided ISO code. + * <p> + * If there is no data for this symbol, substitutes isoCode, + * or returns null if noSubstitute was set in the factory method. * * @param isoCode the three-letter ISO code. * @return the symbol. @@ -117,7 +118,12 @@ public abstract class CurrencyDisplayNames { /** * Returns the narrow symbol for the currency with the provided ISO code. - * If there is no data for narrow symbol, substitutes the default symbol, + * <p> + * The narrow currency symbol is similar to the regular currency symbol, + * but it always takes the shortest form; + * for example, "$" instead of "US$" for USD in en-CA. + * <p> + * If there is no data for this symbol, substitutes the default symbol, * or returns null if noSubstitute was set in the factory method. * * @param isoCode the three-letter ISO code. @@ -126,6 +132,37 @@ public abstract class CurrencyDisplayNames { public abstract String getNarrowSymbol(String isoCode); /** + * Returns the formal symbol for the currency with the provided ISO code. + * <p> + * The formal currency symbol is similar to the regular currency symbol, + * but it always takes the form used in formal settings such as banking; + * for example, "NT$" instead of "$" for TWD in zh-TW. + * <p> + * If there is no data for this symbol, substitutes the default symbol, + * or returns null if noSubstitute was set in the factory method. + * + * @param isoCode the three-letter ISO code. + * @return the formal symbol. + * @hide draft / provisional / internal are hidden on Android + */ + public abstract String getFormalSymbol(String isoCode); + + /** + * Returns the variant symbol for the currency with the provided ISO code. + * <p> + * The variant symbol for a currency is an alternative symbol that is not + * necessarily as widely used as the regular symbol. + * <p> + * If there is no data for variant symbol, substitutes the default symbol, + * or returns null if noSubstitute was set in the factory method. + * + * @param isoCode the three-letter ISO code. + * @return the variant symbol. + * @hide draft / provisional / internal are hidden on Android + */ + public abstract String getVariantSymbol(String isoCode); + + /** * Returns the 'long name' for the currency with the provided ISO code. * If there is no data for the ISO code, substitutes isoCode, or returns null * if noSubstitute was set in the factory method. diff --git a/android_icu4j/src/main/java/android/icu/text/DateFormat.java b/android_icu4j/src/main/java/android/icu/text/DateFormat.java index ed226d329..62dd15515 100644 --- a/android_icu4j/src/main/java/android/icu/text/DateFormat.java +++ b/android_icu4j/src/main/java/android/icu/text/DateFormat.java @@ -491,6 +491,37 @@ public abstract class DateFormat extends UFormat { */ private EnumSet<BooleanAttribute> booleanAttributes = EnumSet.allOf(BooleanAttribute.class); + /** + * Hour Cycle + * @hide Only a subset of ICU is exposed in Android + * @hide draft / provisional / internal are hidden on Android + */ + public enum HourCycle { + /** + * hour in am/pm (0~11) + * @hide draft / provisional / internal are hidden on Android + */ + HOUR_CYCLE_11, + + /** + * hour in am/pm (1~12) + * @hide draft / provisional / internal are hidden on Android + */ + HOUR_CYCLE_12, + + /** + * hour in day (0~23) + * @hide draft / provisional / internal are hidden on Android + */ + HOUR_CYCLE_23, + + /** + * hour in day (1~24) + * @hide draft / provisional / internal are hidden on Android + */ + HOUR_CYCLE_24; + }; + /* * Capitalization setting, hoisted to DateFormat ICU 53 * Note that SimpleDateFormat serialization may call getContext/setContext to read/write diff --git a/android_icu4j/src/main/java/android/icu/text/DateFormatSymbols.java b/android_icu4j/src/main/java/android/icu/text/DateFormatSymbols.java index f465782ee..ce5a0f050 100644 --- a/android_icu4j/src/main/java/android/icu/text/DateFormatSymbols.java +++ b/android_icu4j/src/main/java/android/icu/text/DateFormatSymbols.java @@ -683,7 +683,7 @@ public class DateFormatSymbols implements Serializable, Cloneable { */ private static final Map<String, CapitalizationContextUsage> contextUsageTypeMap; static { - contextUsageTypeMap=new HashMap<String, CapitalizationContextUsage>(); + contextUsageTypeMap=new HashMap<>(); contextUsageTypeMap.put("month-format-except-narrow", CapitalizationContextUsage.MONTH_FORMAT); contextUsageTypeMap.put("month-standalone-except-narrow", CapitalizationContextUsage.MONTH_STANDALONE); contextUsageTypeMap.put("month-narrow", CapitalizationContextUsage.MONTH_NARROW); @@ -742,7 +742,7 @@ public class DateFormatSymbols implements Serializable, Cloneable { /** * <strong>[icu]</strong> Returns narrow era name strings. For example: "A" and "B". * @return the narrow era strings. - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ @libcore.api.IntraCoreApi public String[] getNarrowEras() { @@ -752,7 +752,7 @@ public class DateFormatSymbols implements Serializable, Cloneable { /** * <strong>[icu]</strong> Sets narrow era name strings. For example: "A" and "B". * @param newNarrowEras the new narrow era strings. - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public void setNarrowEras(String[] newNarrowEras) { narrowEras = duplicate(newNarrowEras); @@ -1669,9 +1669,9 @@ public class DateFormatSymbols implements Serializable, Cloneable { private static final class CalendarDataSink extends UResource.Sink { // Data structures to store resources from the resource bundle - Map<String, String[]> arrays = new TreeMap<String, String[]>(); - Map<String, Map<String, String>> maps = new TreeMap<String, Map<String, String>>(); - List<String> aliasPathPairs = new ArrayList<String>(); + Map<String, String[]> arrays = new TreeMap<>(); + Map<String, Map<String, String>> maps = new TreeMap<>(); + List<String> aliasPathPairs = new ArrayList<>(); // Current and next calendar resource table which should be loaded String currentCalendarType = null; @@ -1726,7 +1726,7 @@ public class DateFormatSymbols implements Serializable, Cloneable { // Whenever an alias to the next calendar (except gregorian) is encountered, register the // calendar type it's pointing to if (resourcesToVisitNext == null) { - resourcesToVisitNext = new HashSet<String>(); + resourcesToVisitNext = new HashSet<>(); } resourcesToVisitNext.add(aliasRelativePath); continue; @@ -1814,7 +1814,7 @@ public class DateFormatSymbols implements Serializable, Cloneable { if (value.getType() == ICUResourceBundle.STRING) { // We are on a leaf, store the map elements into the stringMap if (i == 0) { - stringMap = new HashMap<String, String>(); + stringMap = new HashMap<>(); maps.put(path, stringMap); } assert stringMap != null; @@ -2106,7 +2106,7 @@ public class DateFormatSymbols implements Serializable, Cloneable { ULocale uloc = rb.getULocale(); setLocale(uloc, uloc); - capitalization = new HashMap<CapitalizationContextUsage,boolean[]>(); + capitalization = new HashMap<>(); boolean[] noTransforms = new boolean[2]; noTransforms[0] = false; noTransforms[1] = false; @@ -2301,6 +2301,20 @@ public class DateFormatSymbols implements Serializable, Cloneable { initializeData(locale, calType); } + // Android patch (http://b/30464240) start: Add constructor taking a calendar type. + /** + * Variant of DateFormatSymbols(Calendar, ULocale) that takes the calendar type + * instead of a Calendar instance. + * @see #DateFormatSymbols(Calendar, Locale) + * @deprecated This API is ICU internal only. + * @hide draft / provisional / internal are hidden on Android + */ + @Deprecated + public DateFormatSymbols(ULocale locale, String calType) { + initializeData(locale, calType); + } + // Android patch end. + /** * Fetches a custom calendar's DateFormatSymbols out of the given resource * bundle. Symbols that are not overridden are inherited from the diff --git a/android_icu4j/src/main/java/android/icu/text/DateIntervalFormat.java b/android_icu4j/src/main/java/android/icu/text/DateIntervalFormat.java index 5be5b2134..8d9365665 100644 --- a/android_icu4j/src/main/java/android/icu/text/DateIntervalFormat.java +++ b/android_icu4j/src/main/java/android/icu/text/DateIntervalFormat.java @@ -231,7 +231,7 @@ import android.icu.util.UResourceBundle; * * // a series of set interval patterns. * // Only ERA, YEAR, MONTH, DATE, DAY_OF_MONTH, DAY_OF_WEEK, AM_PM, HOUR, HOUR_OF_DAY, - * MINUTE and SECOND are supported. + * MINUTE, SECOND and MILLISECOND are supported. * dtitvinf.setIntervalPattern("yMMMd", Calendar.YEAR, "'y ~ y'"); * dtitvinf.setIntervalPattern("yMMMd", Calendar.MONTH, "yyyy 'diff' MMM d - MMM d"); * dtitvinf.setIntervalPattern("yMMMd", Calendar.DATE, "yyyy MMM d ~ d"); @@ -276,7 +276,6 @@ public class DateIntervalFormat extends UFormat { * Not intended for public subclassing. * * @hide Only a subset of ICU is exposed in Android - * @hide draft / provisional / internal are hidden on Android */ public static final class FormattedDateInterval implements FormattedValue { private final String string; @@ -289,7 +288,6 @@ public class DateIntervalFormat extends UFormat { /** * {@inheritDoc} - * @hide draft / provisional / internal are hidden on Android */ @Override public String toString() { @@ -298,7 +296,6 @@ public class DateIntervalFormat extends UFormat { /** * {@inheritDoc} - * @hide draft / provisional / internal are hidden on Android */ @Override public int length() { @@ -307,7 +304,6 @@ public class DateIntervalFormat extends UFormat { /** * {@inheritDoc} - * @hide draft / provisional / internal are hidden on Android */ @Override public char charAt(int index) { @@ -316,7 +312,6 @@ public class DateIntervalFormat extends UFormat { /** * {@inheritDoc} - * @hide draft / provisional / internal are hidden on Android */ @Override public CharSequence subSequence(int start, int end) { @@ -325,7 +320,6 @@ public class DateIntervalFormat extends UFormat { /** * {@inheritDoc} - * @hide draft / provisional / internal are hidden on Android */ @Override public <A extends Appendable> A appendTo(A appendable) { @@ -334,7 +328,6 @@ public class DateIntervalFormat extends UFormat { /** * {@inheritDoc} - * @hide draft / provisional / internal are hidden on Android */ @Override public boolean nextPosition(ConstrainedFieldPosition cfpos) { @@ -343,7 +336,6 @@ public class DateIntervalFormat extends UFormat { /** * {@inheritDoc} - * @hide draft / provisional / internal are hidden on Android */ @Override public AttributedCharacterIterator toCharacterIterator() { @@ -355,7 +347,6 @@ public class DateIntervalFormat extends UFormat { * Class for span fields in FormattedDateInterval. * * @hide Only a subset of ICU is exposed in Android - * @hide draft / provisional / internal are hidden on Android */ public static final class SpanField extends UFormat.SpanField { private static final long serialVersionUID = -6330879259553618133L; @@ -366,8 +357,6 @@ public class DateIntervalFormat extends UFormat { * Instances of DATE_INTERVAL_SPAN should have an associated value. If * 0, the date fields within the span are for the "from" date; if 1, * the date fields within the span are for the "to" date. - * - * @hide draft / provisional / internal are hidden on Android */ public static final SpanField DATE_INTERVAL_SPAN = new SpanField("date-interval-span"); @@ -376,7 +365,7 @@ public class DateIntervalFormat extends UFormat { } /** - * serizalization method resolve instances to the constant + * serialization method resolve instances to the constant * DateIntervalFormat.SpanField values * @hide draft / provisional / internal are hidden on Android */ @@ -778,7 +767,7 @@ public class DateIntervalFormat extends UFormat { * * @param dtInterval DateInterval to be formatted. * @return A FormattedDateInterval containing the format result. - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public FormattedDateInterval formatToValue(DateInterval dtInterval) { StringBuffer sb = new StringBuffer(); @@ -839,6 +828,9 @@ public class DateIntervalFormat extends UFormat { } else if ( fromCalendar.get(Calendar.SECOND) != toCalendar.get(Calendar.SECOND) ) { field = Calendar.SECOND; + } else if ( fromCalendar.get(Calendar.MILLISECOND) != + toCalendar.get(Calendar.MILLISECOND) ) { + field = Calendar.MILLISECOND; } else { return null; } @@ -882,7 +874,7 @@ public class DateIntervalFormat extends UFormat { * @param toCalendar calendar set to the to date in date interval * to be formatted into date interval string * @return A FormattedDateInterval containing the format result. - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public FormattedDateInterval formatToValue(Calendar fromCalendar, Calendar toCalendar) { StringBuffer sb = new StringBuffer(); @@ -933,16 +925,19 @@ public class DateIntervalFormat extends UFormat { } else if ( fromCalendar.get(Calendar.MINUTE) != toCalendar.get(Calendar.MINUTE) ) { field = Calendar.MINUTE; - } else if ( fromCalendar.get(Calendar.SECOND) != + } else if ( fromCalendar.get(Calendar.SECOND) != toCalendar.get(Calendar.SECOND) ) { field = Calendar.SECOND; - } else { + } else if ( fromCalendar.get(Calendar.MILLISECOND) != + toCalendar.get(Calendar.MILLISECOND) ) { + field = Calendar.MILLISECOND; + } else { /* ignore the millisecond etc. small fields' difference. * use single date when all the above are the same. */ return fDateFormat.format(fromCalendar, appendTo, pos, attributes); } - boolean fromToOnSameDay = (field==Calendar.AM_PM || field==Calendar.HOUR || field==Calendar.MINUTE || field==Calendar.SECOND); + boolean fromToOnSameDay = (field==Calendar.AM_PM || field==Calendar.HOUR || field==Calendar.MINUTE || field==Calendar.SECOND || field==Calendar.MILLISECOND); // get interval pattern PatternInfo intervalPattern = fIntervalPatterns.get( @@ -1013,11 +1008,11 @@ public class DateIntervalFormat extends UFormat { fInfo.getFallbackIntervalPattern(), patternSB, 2, 2); long state = 0; while (true) { - state = SimpleFormatterImpl.Int64Iterator.step(compiledPattern, state, appendTo); - if (state == SimpleFormatterImpl.Int64Iterator.DONE) { + state = SimpleFormatterImpl.IterInternal.step(state, compiledPattern, appendTo); + if (state == SimpleFormatterImpl.IterInternal.DONE) { break; } - if (SimpleFormatterImpl.Int64Iterator.getArgIndex(state) == 0) { + if (SimpleFormatterImpl.IterInternal.getArgIndex(state) == 0) { if (output != null) { output.register(0); } @@ -1071,11 +1066,11 @@ public class DateIntervalFormat extends UFormat { // {1} is single date portion long state = 0; while (true) { - state = SimpleFormatterImpl.Int64Iterator.step(compiledPattern, state, appendTo); - if (state == SimpleFormatterImpl.Int64Iterator.DONE) { + state = SimpleFormatterImpl.IterInternal.step(state, compiledPattern, appendTo); + if (state == SimpleFormatterImpl.IterInternal.DONE) { break; } - if (SimpleFormatterImpl.Int64Iterator.getArgIndex(state) == 0) { + if (SimpleFormatterImpl.IterInternal.getArgIndex(state) == 0) { fDateFormat.applyPattern(fTimePattern); fallbackFormatRange(fromCalendar, toCalendar, appendTo, patternSB, pos, output, attributes); } else { diff --git a/android_icu4j/src/main/java/android/icu/text/DateIntervalInfo.java b/android_icu4j/src/main/java/android/icu/text/DateIntervalInfo.java index d96276100..761481804 100644 --- a/android_icu4j/src/main/java/android/icu/text/DateIntervalInfo.java +++ b/android_icu4j/src/main/java/android/icu/text/DateIntervalInfo.java @@ -144,7 +144,7 @@ import android.icu.util.UResourceBundle; * the interval patterns using setIntervalPattern function as so desired. * Currently, users can only set interval patterns when the following * calendar fields are different: ERA, YEAR, MONTH, DATE, DAY_OF_MONTH, - * DAY_OF_WEEK, AM_PM, HOUR, HOUR_OF_DAY, MINUTE and SECOND. + * DAY_OF_WEEK, AM_PM, HOUR, HOUR_OF_DAY, MINUTE, SECOND, and MILLISECOND. * Interval patterns when other calendar fields are different is not supported. * <P> * DateIntervalInfo objects are cloneable. @@ -289,7 +289,7 @@ public class DateIntervalInfo implements Cloneable, Freezable<DateIntervalInfo>, private static final long serialVersionUID = 1; private static final int MINIMUM_SUPPORTED_CALENDAR_FIELD = - Calendar.SECOND; + Calendar.MILLISECOND; //private static boolean DEBUG = true; private static String CALENDAR_KEY = "calendar"; @@ -415,8 +415,9 @@ public class DateIntervalInfo implements Cloneable, Freezable<DateIntervalInfo>, * Calendar.HOUR_OF_DAY * Calendar.MINUTE * Calendar.SECOND + * Calendar.MILLISECOND */ - private static final String ACCEPTED_PATTERN_LETTERS = "GyMdahHms"; + private static final String ACCEPTED_PATTERN_LETTERS = "GyMdahHmsS"; // Output data DateIntervalInfo dateIntervalInfo; @@ -695,7 +696,7 @@ public class DateIntervalInfo implements Cloneable, Freezable<DateIntervalInfo>, * Restriction: * Currently, users can only set interval patterns when the following * calendar fields are different: ERA, YEAR, MONTH, DATE, DAY_OF_MONTH, - * DAY_OF_WEEK, AM_PM, HOUR, HOUR_OF_DAY, MINUTE, and SECOND. + * DAY_OF_WEEK, AM_PM, HOUR, HOUR_OF_DAY, MINUTE, SECOND, and MILLISECOND. * Interval patterns when other calendar fields are different are * not supported. * @@ -839,7 +840,7 @@ public class DateIntervalInfo implements Cloneable, Freezable<DateIntervalInfo>, public PatternInfo getIntervalPattern(String skeleton, int field) { if ( field > MINIMUM_SUPPORTED_CALENDAR_FIELD ) { - throw new IllegalArgumentException("no support for field less than SECOND"); + throw new IllegalArgumentException("no support for field less than MILLISECOND"); } Map<String, PatternInfo> patternsOfOneSkeleton = fIntervalPatterns.get(skeleton); if ( patternsOfOneSkeleton != null ) { diff --git a/android_icu4j/src/main/java/android/icu/text/DateTimePatternGenerator.java b/android_icu4j/src/main/java/android/icu/text/DateTimePatternGenerator.java index 6ad49c215..497cfa2bb 100644 --- a/android_icu4j/src/main/java/android/icu/text/DateTimePatternGenerator.java +++ b/android_icu4j/src/main/java/android/icu/text/DateTimePatternGenerator.java @@ -364,6 +364,26 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera String[] list = getAllowedHourFormatsLangCountry(language, country); + // We need to check if there is an hour cycle on locale + Character defaultCharFromLocale = null; + String hourCycle = uLocale.getKeywordValue("hours"); + if (hourCycle != null) { + switch(hourCycle) { + case "h24": + defaultCharFromLocale = 'k'; + break; + case "h23": + defaultCharFromLocale = 'H'; + break; + case "h12": + defaultCharFromLocale = 'h'; + break; + case "h11": + defaultCharFromLocale = 'K'; + break; + } + } + // Check if the region has an alias if (list == null) { try { @@ -376,11 +396,11 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera } if (list != null) { - defaultHourFormatChar = list[0].charAt(0); + defaultHourFormatChar = defaultCharFromLocale != null ? defaultCharFromLocale : list[0].charAt(0); allowedHourFormats = Arrays.copyOfRange(list, 1, list.length - 1); } else { allowedHourFormats = LAST_RESORT_ALLOWED_HOUR_FORMAT; - defaultHourFormatChar = allowedHourFormats[0].charAt(0); + defaultHourFormatChar = (defaultCharFromLocale != null) ? defaultCharFromLocale : allowedHourFormats[0].charAt(0); } } @@ -1187,7 +1207,6 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera private static final int APPENDITEM_WIDTH_INT = APPENDITEM_WIDTH.ordinal(); private static final DisplayWidth[] CLDR_FIELD_WIDTH = DisplayWidth.values(); - // Option masks for getBestPattern, replaceFieldTypes (individual masks may be ORed together) /** @@ -1290,6 +1309,20 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera } /** + * Return the default hour cycle. + * @hide draft / provisional / internal are hidden on Android + */ + public DateFormat.HourCycle getDefaultHourCycle() { + switch(getDefaultHourFormatChar()) { + case 'h': return DateFormat.HourCycle.HOUR_CYCLE_12; + case 'H': return DateFormat.HourCycle.HOUR_CYCLE_23; + case 'k': return DateFormat.HourCycle.HOUR_CYCLE_24; + case 'K': return DateFormat.HourCycle.HOUR_CYCLE_11; + default: throw new AssertionError("should be unreachable"); + } + } + + /** * The private interface to set a display name for a particular date/time field, * in one of several possible display widths. * @@ -2024,6 +2057,7 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera // if (SHOW_DISTANCE) System.out.println("Searching for: " + source.pattern // + ", mask: " + showMask(includeMask)); int bestDistance = Integer.MAX_VALUE; + int bestMissingFieldMask = Integer.MIN_VALUE; PatternWithMatcher bestPatternWithMatcher = new PatternWithMatcher("", null); DistanceInfo tempInfo = new DistanceInfo(); for (DateTimeMatcher trial : skeleton2pattern.keySet()) { @@ -2033,8 +2067,16 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera int distance = source.getDistance(trial, includeMask, tempInfo); // if (SHOW_DISTANCE) System.out.println("\tDistance: " + trial.pattern + ":\t" // + distance + ",\tmissing fields: " + tempInfo); - if (distance < bestDistance) { + + // Because we iterate over a map the order is undefined. Can change between implementations, + // versions, and will very likely be different between Java and C/C++. + // So if we have patterns with the same distance we also look at the missingFieldMask, + // and we favour the smallest one. Because the field is a bitmask this technically means we + // favour differences in the "least significant fields". For example we prefer the one with differences + // in seconds field vs one with difference in the hours field. + if (distance < bestDistance || (distance == bestDistance && bestMissingFieldMask < tempInfo.missingFieldMask)) { bestDistance = distance; + bestMissingFieldMask = tempInfo.missingFieldMask; PatternWithSkeletonFlag patternWithSkelFlag = skeleton2pattern.get(trial); bestPatternWithMatcher.pattern = patternWithSkelFlag.pattern; // If the best raw match had a specified skeleton then return it too. @@ -2093,8 +2135,10 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera // - "field" is the field from the found pattern. // // The adjusted field should consist of characters from the originally requested - // skeleton, except in the case of HOUR or MONTH or WEEKDAY or YEAR, in which case it - // should consist of characters from the found pattern. + // skeleton, except in the case of MONTH or WEEKDAY or YEAR, in which case it + // should consist of characters from the found pattern. There is some adjustment + // in some cases of HOUR to "defaultHourFormatChar". There is explanation + // how it is done below. // // The length of the adjusted field (adjFieldLen) should match that in the originally // requested skeleton, except that in the following cases the length of the adjusted field @@ -2139,8 +2183,25 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera && (type != YEAR || reqFieldChar=='Y')) ? reqFieldChar : fieldBuilder.charAt(0); - if (type == HOUR && flags.contains(DTPGflags.SKELETON_USES_CAP_J)) { - c = defaultHourFormatChar; + if (type == HOUR) { + // The adjustment here is required to match spec (https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-hour). + // It is necessary to match the hour-cycle preferred by the Locale. + // Given that, we need to do the following adjustments: + // 1. When hour-cycle is h11 it should replace 'h' by 'K'. + // 2. When hour-cycle is h23 it should replace 'H' by 'k'. + // 3. When hour-cycle is h24 it should replace 'k' by 'H'. + // 4. When hour-cycle is h12 it should replace 'K' by 'h'. + if (flags.contains(DTPGflags.SKELETON_USES_CAP_J) || reqFieldChar == defaultHourFormatChar) { + c = defaultHourFormatChar; + } else if (reqFieldChar == 'h' && defaultHourFormatChar == 'K') { + c = 'K'; + } else if (reqFieldChar == 'H' && defaultHourFormatChar == 'k') { + c = 'k'; + } else if (reqFieldChar == 'k' && defaultHourFormatChar == 'H') { + c = 'H'; + } else if (reqFieldChar == 'K' && defaultHourFormatChar == 'h') { + c = 'h'; + } } fieldBuilder = new StringBuilder(); for (int i = adjFieldLen; i > 0; --i) fieldBuilder.append(c); @@ -2639,6 +2700,32 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera if (subField > 0) subField += value.length(); type[field] = subField; } + + // #20739, we have a skeleton with minutes and milliseconds, but no seconds + // + // Theoretically we would need to check and fix all fields with "gaps": + // for example year-day (no month), month-hour (no day), and so on, All the possible field combinations. + // Plus some smartness: year + hour => should we add month, or add day-of-year? + // What about month + day-of-week, or month + am/pm indicator. + // I think beyond a certain point we should not try to fix bad developer input and try guessing what they mean. + // Garbage in, garbage out. + if (!original.isFieldEmpty(MINUTE) && !original.isFieldEmpty(FRACTIONAL_SECOND) && original.isFieldEmpty(SECOND)) { + // Force the use of seconds + for (int i = 0; i < types.length; ++i) { + int[] row = types[i]; + if (row[1] == SECOND) { + // first entry for SECOND + original.populate(SECOND, (char)row[0], row[3]); + baseOriginal.populate(SECOND, (char)row[0], row[3]); + // We add value.length, same as above, when type is first initialized. + // The value we want to "fake" here is "s", and 1 means "s".length() + int subField = row[2]; + type[SECOND] = (subField > 0) ? subField + 1 : subField; + break; + } + } + } + // #13183, handle special behavior for day period characters (a, b, B) if (!original.isFieldEmpty(HOUR)) { if (original.getFieldChar(HOUR)=='h' || original.getFieldChar(HOUR)=='K') { diff --git a/android_icu4j/src/main/java/android/icu/text/DecimalFormat.java b/android_icu4j/src/main/java/android/icu/text/DecimalFormat.java index 42d46c831..65a2c8e81 100644 --- a/android_icu4j/src/main/java/android/icu/text/DecimalFormat.java +++ b/android_icu4j/src/main/java/android/icu/text/DecimalFormat.java @@ -13,9 +13,14 @@ import java.text.AttributedCharacterIterator; import java.text.FieldPosition; import java.text.ParsePosition; +import android.icu.impl.FormattedStringBuilder; +import android.icu.impl.FormattedValueStringBuilderImpl; +import android.icu.impl.Utility; import android.icu.impl.number.AffixUtils; import android.icu.impl.number.DecimalFormatProperties; import android.icu.impl.number.DecimalFormatProperties.ParseMode; +import android.icu.impl.number.DecimalQuantity; +import android.icu.impl.number.DecimalQuantity_DualStorageBCD; import android.icu.impl.number.Padder; import android.icu.impl.number.Padder.PadPosition; import android.icu.impl.number.PatternStringParser; @@ -693,9 +698,11 @@ public class DecimalFormat extends NumberFormat { */ @Override public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) { - FormattedNumber output = formatter.format(number); - fieldPositionHelper(output, fieldPosition, result.length()); - output.appendTo(result); + DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(number); + FormattedStringBuilder string = new FormattedStringBuilder(); + formatter.formatImpl(dq, string); + fieldPositionHelper(dq, string, fieldPosition, result.length()); + Utility.appendTo(string, result); return result; } @@ -704,9 +711,11 @@ public class DecimalFormat extends NumberFormat { */ @Override public StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition) { - FormattedNumber output = formatter.format(number); - fieldPositionHelper(output, fieldPosition, result.length()); - output.appendTo(result); + DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(number); + FormattedStringBuilder string = new FormattedStringBuilder(); + formatter.formatImpl(dq, string); + fieldPositionHelper(dq, string, fieldPosition, result.length()); + Utility.appendTo(string, result); return result; } @@ -715,9 +724,11 @@ public class DecimalFormat extends NumberFormat { */ @Override public StringBuffer format(BigInteger number, StringBuffer result, FieldPosition fieldPosition) { - FormattedNumber output = formatter.format(number); - fieldPositionHelper(output, fieldPosition, result.length()); - output.appendTo(result); + DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(number); + FormattedStringBuilder string = new FormattedStringBuilder(); + formatter.formatImpl(dq, string); + fieldPositionHelper(dq, string, fieldPosition, result.length()); + Utility.appendTo(string, result); return result; } @@ -727,9 +738,11 @@ public class DecimalFormat extends NumberFormat { @Override public StringBuffer format( java.math.BigDecimal number, StringBuffer result, FieldPosition fieldPosition) { - FormattedNumber output = formatter.format(number); - fieldPositionHelper(output, fieldPosition, result.length()); - output.appendTo(result); + DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(number); + FormattedStringBuilder string = new FormattedStringBuilder(); + formatter.formatImpl(dq, string); + fieldPositionHelper(dq, string, fieldPosition, result.length()); + Utility.appendTo(string, result); return result; } @@ -738,9 +751,11 @@ public class DecimalFormat extends NumberFormat { */ @Override public StringBuffer format(BigDecimal number, StringBuffer result, FieldPosition fieldPosition) { - FormattedNumber output = formatter.format(number); - fieldPositionHelper(output, fieldPosition, result.length()); - output.appendTo(result); + DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(number); + FormattedStringBuilder string = new FormattedStringBuilder(); + formatter.formatImpl(dq, string); + fieldPositionHelper(dq, string, fieldPosition, result.length()); + Utility.appendTo(string, result); return result; } @@ -765,12 +780,14 @@ public class DecimalFormat extends NumberFormat { // because its caching mechanism will not provide any benefit here. DecimalFormatSymbols localSymbols = (DecimalFormatSymbols) symbols.clone(); localSymbols.setCurrency(currAmt.getCurrency()); - FormattedNumber output = formatter - .symbols(localSymbols) + + DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(currAmt.getNumber()); + FormattedStringBuilder string = new FormattedStringBuilder(); + formatter.symbols(localSymbols) .unit(currAmt.getCurrency()) - .format(currAmt.getNumber()); - fieldPositionHelper(output, fieldPosition, result.length()); - output.appendTo(result); + .formatImpl(dq, string); + fieldPositionHelper(dq, string, fieldPosition, result.length()); + Utility.appendTo(string, result); return result; } @@ -1016,7 +1033,7 @@ public class DecimalFormat extends NumberFormat { * * @return Whether the sign is shown on positive numbers and zero. * @see #setSignAlwaysShown - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public synchronized boolean isSignAlwaysShown() { // This is not in the exported properties @@ -1044,7 +1061,7 @@ public class DecimalFormat extends NumberFormat { * signs in the pattern is undefined. * * @param value true to always show a sign; false to hide the sign on positive numbers and zero. - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public synchronized void setSignAlwaysShown(boolean value) { properties.setSignAlwaysShown(value); @@ -1824,7 +1841,7 @@ public class DecimalFormat extends NumberFormat { * <strong>[icu]</strong> Returns the minimum number of digits before grouping is triggered. * * @see #setMinimumGroupingDigits - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public synchronized int getMinimumGroupingDigits() { if (properties.getMinimumGroupingDigits() > 0) { @@ -1839,7 +1856,7 @@ public class DecimalFormat extends NumberFormat { * to 2, in <em>en-US</em>, 1234 will be printed as "1234" and 12345 will be printed as "12,345". * * @param number The minimum number of digits before grouping is triggered. - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public synchronized void setMinimumGroupingDigits(int number) { properties.setMinimumGroupingDigits(number); @@ -2123,7 +2140,7 @@ public synchronized void setParseStrictMode(ParseMode parseMode) { * <strong>[icu]</strong> Returns whether to ignore exponents when parsing. * * @see #setParseNoExponent - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public synchronized boolean isParseNoExponent() { return properties.getParseNoExponent(); @@ -2135,7 +2152,7 @@ public synchronized void setParseStrictMode(ParseMode parseMode) { * 5). * * @param value true to prevent exponents from being parsed; false to allow them to be parsed. - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public synchronized void setParseNoExponent(boolean value) { properties.setParseNoExponent(value); @@ -2146,7 +2163,7 @@ public synchronized void setParseStrictMode(ParseMode parseMode) { * <strong>[icu]</strong> Returns whether to force case (uppercase/lowercase) to match when parsing. * * @see #setParseNoExponent - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public synchronized boolean isParseCaseSensitive() { return properties.getParseCaseSensitive(); @@ -2159,7 +2176,7 @@ public synchronized void setParseStrictMode(ParseMode parseMode) { * * @param value true to force case (uppercase/lowercase) to match when parsing; false to ignore * case and perform case folding. - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public synchronized void setParseCaseSensitive(boolean value) { properties.setParseCaseSensitive(value); @@ -2398,11 +2415,13 @@ public synchronized void setParseStrictMode(ParseMode parseMode) { PatternStringParser.parseToExistingProperties(pattern, properties, ignoreRounding); } - static void fieldPositionHelper(FormattedNumber formatted, FieldPosition fieldPosition, int offset) { + static void fieldPositionHelper( + DecimalQuantity dq, FormattedStringBuilder string, FieldPosition fieldPosition, int offset) { // always return first occurrence: fieldPosition.setBeginIndex(0); fieldPosition.setEndIndex(0); - boolean found = formatted.nextFieldPosition(fieldPosition); + dq.populateUFieldPosition(fieldPosition); + boolean found = FormattedValueStringBuilderImpl.nextFieldPosition(string, fieldPosition);; if (found && offset != 0) { fieldPosition.setBeginIndex(fieldPosition.getBeginIndex() + offset); fieldPosition.setEndIndex(fieldPosition.getEndIndex() + offset); diff --git a/android_icu4j/src/main/java/android/icu/text/FormattedValue.java b/android_icu4j/src/main/java/android/icu/text/FormattedValue.java index 02daa93c7..ff67f1247 100644 --- a/android_icu4j/src/main/java/android/icu/text/FormattedValue.java +++ b/android_icu4j/src/main/java/android/icu/text/FormattedValue.java @@ -13,7 +13,6 @@ import android.icu.util.ICUUncheckedIOException; * * @author sffc * @hide Only a subset of ICU is exposed in Android - * @hide draft / provisional / internal are hidden on Android */ public interface FormattedValue extends CharSequence { /** @@ -22,7 +21,6 @@ public interface FormattedValue extends CharSequence { * Consider using {@link #appendTo} for greater efficiency. * * @return The formatted string. - * @hide draft / provisional / internal are hidden on Android */ @Override public String toString(); @@ -36,7 +34,6 @@ public interface FormattedValue extends CharSequence { * @param appendable The Appendable to which to append the string output. * @return The same Appendable, for chaining. * @throws ICUUncheckedIOException if the Appendable throws IOException - * @hide draft / provisional / internal are hidden on Android */ public <A extends Appendable> A appendTo(A appendable); @@ -58,7 +55,6 @@ public interface FormattedValue extends CharSequence { * only one specific field; see {@link ConstrainedFieldPosition#constrainField}. * @return true if a new occurrence of the field was found; * false otherwise. - * @hide draft / provisional / internal are hidden on Android */ public boolean nextPosition(ConstrainedFieldPosition cfpos); @@ -68,7 +64,6 @@ public interface FormattedValue extends CharSequence { * Consider using {@link #nextPosition} if you are trying to get field information. * * @return An AttributedCharacterIterator containing full field information. - * @hide draft / provisional / internal are hidden on Android */ public AttributedCharacterIterator toCharacterIterator(); } diff --git a/android_icu4j/src/main/java/android/icu/text/ListFormatter.java b/android_icu4j/src/main/java/android/icu/text/ListFormatter.java index c54f68361..795af87b4 100644 --- a/android_icu4j/src/main/java/android/icu/text/ListFormatter.java +++ b/android_icu4j/src/main/java/android/icu/text/ListFormatter.java @@ -9,19 +9,26 @@ */ package android.icu.text; -import java.io.IOException; +import java.io.InvalidObjectException; +import java.text.AttributedCharacterIterator; +import java.text.Format; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.Locale; +import java.util.regex.Pattern; +import android.icu.impl.FormattedStringBuilder; +import android.icu.impl.FormattedValueStringBuilderImpl; +import android.icu.impl.FormattedValueStringBuilderImpl.SpanFieldPlaceholder; import android.icu.impl.ICUCache; import android.icu.impl.ICUData; import android.icu.impl.ICUResourceBundle; import android.icu.impl.SimpleCache; import android.icu.impl.SimpleFormatterImpl; -import android.icu.util.ICUUncheckedIOException; +import android.icu.impl.SimpleFormatterImpl.IterInternal; +import android.icu.impl.Utility; import android.icu.util.ULocale; import android.icu.util.UResourceBundle; @@ -33,14 +40,19 @@ import android.icu.util.UResourceBundle; */ final public class ListFormatter { // Compiled SimpleFormatter patterns. - private final String two; private final String start; private final String middle; - private final String end; private final ULocale locale; + private interface PatternHandler { + public String getTwoPattern(String text); + public String getEndPattern(String text); + } + private final PatternHandler patternHandler; + /** * Indicates the style of Listformatter + * TODO(ICU-20888): Remove this in ICU 68. * @deprecated This API is ICU internal only. * @hide Only a subset of ICU is exposed in Android * @hide draft / provisional / internal are hidden on Android @@ -100,6 +112,225 @@ final public class ListFormatter { } /** + * Type of meaning expressed by the list. + * + * @hide Only a subset of ICU is exposed in Android + * @hide draft / provisional / internal are hidden on Android + */ + public enum Type { + /** + * Conjunction formatting, e.g. "Alice, Bob, Charlie, and Delta". + * + * @hide draft / provisional / internal are hidden on Android + */ + AND, + + /** + * Disjunction (or alternative, or simply one of) formatting, e.g. + * "Alice, Bob, Charlie, or Delta". + * + * @hide draft / provisional / internal are hidden on Android + */ + OR, + + /** + * Formatting of a list of values with units, e.g. "5 pounds, 12 ounces". + * + * @hide draft / provisional / internal are hidden on Android + */ + UNITS + }; + + /** + * Verbosity level of the list patterns. + * + * @hide Only a subset of ICU is exposed in Android + * @hide draft / provisional / internal are hidden on Android + */ + public enum Width { + /** + * Use list formatting with full words (no abbreviations) when possible. + * + * @hide draft / provisional / internal are hidden on Android + */ + WIDE, + + /** + * Use list formatting of typical length. + * + * @hide draft / provisional / internal are hidden on Android + */ + SHORT, + + /** + * Use list formatting of the shortest possible length. + * + * @hide draft / provisional / internal are hidden on Android + */ + NARROW, + }; + + /** + * Class for span fields in FormattedList. + * + * @hide Only a subset of ICU is exposed in Android + * @hide draft / provisional / internal are hidden on Android + */ + public static final class SpanField extends UFormat.SpanField { + private static final long serialVersionUID = 3563544214705634403L; + + /** + * The concrete field used for spans in FormattedList. + * + * Instances of LIST_SPAN should have an associated value, the index + * within the input list that is represented by the span. + * + * @hide draft / provisional / internal are hidden on Android + */ + public static final SpanField LIST_SPAN = new SpanField("list-span"); + + private SpanField(String name) { + super(name); + } + + /** + * serialization method resolve instances to the constant + * ListFormatter.SpanField values + * @deprecated This API is ICU internal only. + * @hide draft / provisional / internal are hidden on Android + */ + @Deprecated + @Override + protected Object readResolve() throws InvalidObjectException { + if (this.getName().equals(LIST_SPAN.getName())) + return LIST_SPAN; + + throw new InvalidObjectException("An invalid object."); + } + } + + /** + * Field selectors for format fields defined by ListFormatter. + * @hide Only a subset of ICU is exposed in Android + * @hide draft / provisional / internal are hidden on Android + */ + public static final class Field extends Format.Field { + private static final long serialVersionUID = -8071145668708265437L; + + /** + * The literal text in the result which came from the resources. + * @hide draft / provisional / internal are hidden on Android + */ + public static Field LITERAL = new Field("literal"); + + /** + * The element text in the result which came from the input strings. + * @hide draft / provisional / internal are hidden on Android + */ + public static Field ELEMENT = new Field("element"); + + private Field(String name) { + super(name); + } + + /** + * Serizalization method resolve instances to the constant Field values + * + * @hide draft / provisional / internal are hidden on Android + */ + @Override + protected Object readResolve() throws InvalidObjectException { + if (this.getName().equals(LITERAL.getName())) + return LITERAL; + if (this.getName().equals(ELEMENT.getName())) + return ELEMENT; + + throw new InvalidObjectException("An invalid object."); + } + } + + /** + * An immutable class containing the result of a list formatting operation. + * + * Instances of this class are immutable and thread-safe. + * + * Not intended for public subclassing. + * + * @hide Only a subset of ICU is exposed in Android + * @hide draft / provisional / internal are hidden on Android + */ + public static final class FormattedList implements FormattedValue { + private final FormattedStringBuilder string; + + FormattedList(FormattedStringBuilder string) { + this.string = string; + } + + /** + * {@inheritDoc} + * @hide draft / provisional / internal are hidden on Android + */ + @Override + public String toString() { + return string.toString(); + } + + /** + * {@inheritDoc} + * @hide draft / provisional / internal are hidden on Android + */ + @Override + public int length() { + return string.length(); + } + + /** + * {@inheritDoc} + * @hide draft / provisional / internal are hidden on Android + */ + @Override + public char charAt(int index) { + return string.charAt(index); + } + + /** + * {@inheritDoc} + * @hide draft / provisional / internal are hidden on Android + */ + @Override + public CharSequence subSequence(int start, int end) { + return string.subString(start, end); + } + + /** + * {@inheritDoc} + * @hide draft / provisional / internal are hidden on Android + */ + @Override + public <A extends Appendable> A appendTo(A appendable) { + return Utility.appendTo(string, appendable); + } + + /** + * {@inheritDoc} + * @hide draft / provisional / internal are hidden on Android + */ + @Override + public boolean nextPosition(ConstrainedFieldPosition cfpos) { + return FormattedValueStringBuilderImpl.nextPosition(string, cfpos, null); + } + + /** + * {@inheritDoc} + * @hide draft / provisional / internal are hidden on Android + */ + @Override + public AttributedCharacterIterator toCharacterIterator() { + return FormattedValueStringBuilderImpl.toCharacterIterator(string, null); + } + } + + /** * <b>Internal:</b> Create a ListFormatter from component strings, * with definitions as in LDML. * @@ -129,11 +360,10 @@ final public class ListFormatter { } private ListFormatter(String two, String start, String middle, String end, ULocale locale) { - this.two = two; this.start = start; this.middle = middle; - this.end = end; this.locale = locale; + this.patternHandler = createPatternHandler(two, end); } private static String compilePattern(String pattern, StringBuilder sb) { @@ -146,9 +376,14 @@ final public class ListFormatter { * @param locale * the locale in question. * @return ListFormatter + * @hide draft / provisional / internal are hidden on Android */ - public static ListFormatter getInstance(ULocale locale) { - return getInstance(locale, Style.STANDARD); + public static ListFormatter getInstance(ULocale locale, Type type, Width width) { + String styleName = typeWidthToStyleString(type, width); + if (styleName == null) { + throw new IllegalArgumentException("Invalid list format type/width"); + } + return cache.get(locale, styleName); } /** @@ -157,9 +392,10 @@ final public class ListFormatter { * @param locale * the locale in question. * @return ListFormatter + * @hide draft / provisional / internal are hidden on Android */ - public static ListFormatter getInstance(Locale locale) { - return getInstance(ULocale.forLocale(locale), Style.STANDARD); + public static ListFormatter getInstance(Locale locale, Type type, Width width) { + return getInstance(ULocale.forLocale(locale), type, width); } /** @@ -177,6 +413,28 @@ final public class ListFormatter { } /** + * Create a list formatter that is appropriate for a locale. + * + * @param locale + * the locale in question. + * @return ListFormatter + */ + public static ListFormatter getInstance(ULocale locale) { + return getInstance(locale, Style.STANDARD); + } + + /** + * Create a list formatter that is appropriate for a locale. + * + * @param locale + * the locale in question. + * @return ListFormatter + */ + public static ListFormatter getInstance(Locale locale) { + return getInstance(ULocale.forLocale(locale), Style.STANDARD); + } + + /** * Create a list formatter that is appropriate for the default FORMAT locale. * * @return ListFormatter @@ -204,30 +462,174 @@ final public class ListFormatter { * @return items formatted into a string */ public String format(Collection<?> items) { - return format(items, -1).toString(); + return formatImpl(items, false).toString(); + } + + /** + * Format a list of objects to a FormattedList. You can access the offsets + * of each element from the FormattedList. + * + * @param items + * items to format. The toString() method is called on each. + * @return items formatted into a FormattedList + * @hide draft / provisional / internal are hidden on Android + */ + public FormattedList formatToValue(Object... items) { + return formatToValue(Arrays.asList(items)); + } + + + /** + * Format a collection of objects to a FormattedList. You can access the offsets + * of each element from the FormattedList. + * + * @param items + * items to format. The toString() method is called on each. + * @return items formatted into a FormattedList + * @hide draft / provisional / internal are hidden on Android + */ + public FormattedList formatToValue(Collection<?> items) { + return formatImpl(items, true).toValue(); } // Formats a collection of objects and returns the formatted string plus the offset // in the string where the index th element appears. index is zero based. If index is // negative or greater than or equal to the size of items then this function returns -1 for // the offset. - FormattedListBuilder format(Collection<?> items, int index) { + FormattedListBuilder formatImpl(Collection<?> items, boolean needsFields) { Iterator<?> it = items.iterator(); int count = items.size(); switch (count) { case 0: - return new FormattedListBuilder("", false); + return new FormattedListBuilder("", needsFields); case 1: - return new FormattedListBuilder(it.next(), index == 0); + return new FormattedListBuilder(it.next(), needsFields); case 2: - return new FormattedListBuilder(it.next(), index == 0).append(two, it.next(), index == 1); + Object first = it.next(); + Object second = it.next(); + return new FormattedListBuilder(first, needsFields) + .append(patternHandler.getTwoPattern(String.valueOf(second)), second, 1); } - FormattedListBuilder builder = new FormattedListBuilder(it.next(), index == 0); - builder.append(start, it.next(), index == 1); + FormattedListBuilder builder = new FormattedListBuilder(it.next(), needsFields); + builder.append(start, it.next(), 1); for (int idx = 2; idx < count - 1; ++idx) { - builder.append(middle, it.next(), index == idx); + builder.append(middle, it.next(), idx); + } + Object last = it.next(); + return builder.append(patternHandler.getEndPattern(String.valueOf(last)), last, count - 1); + } + + // A static handler just returns the pattern without considering the input text. + private class StaticHandler implements PatternHandler { + StaticHandler(String two, String end) { + twoPattern = two; + endPattern = end; + } + + @Override + public String getTwoPattern(String text) { return twoPattern; } + + @Override + public String getEndPattern(String text) { return endPattern; } + + private final String twoPattern; + private final String endPattern; + } + + // A contextual handler returns one of the two patterns depending on whether the text matched the regexp. + private class ContextualHandler implements PatternHandler { + ContextualHandler(Pattern regexp, String thenTwo, String elseTwo, String thenEnd, String elseEnd) { + this.regexp = regexp; + thenTwoPattern = thenTwo; + elseTwoPattern = elseTwo; + thenEndPattern = thenEnd; + elseEndPattern = elseEnd; + } + + @Override + public String getTwoPattern(String text) { + if(regexp.matcher(text).matches()) { + return thenTwoPattern; + } else { + return elseTwoPattern; + } + } + + @Override + public String getEndPattern(String text) { + if(regexp.matcher(text).matches()) { + return thenEndPattern; + } else { + return elseEndPattern; + } + } + + private final Pattern regexp; + private final String thenTwoPattern; + private final String elseTwoPattern; + private final String thenEndPattern; + private final String elseEndPattern; + + } + + // Pattern in the ICU Data which might be replaced y by e. + private static final String compiledY = compilePattern("{0} y {1}", new StringBuilder()); + + // The new pattern to replace y to e + private static final String compiledE = compilePattern("{0} e {1}", new StringBuilder()); + + // Pattern in the ICU Data which might be replaced o by u. + private static final String compiledO = compilePattern("{0} o {1}", new StringBuilder()); + + // The new pattern to replace u to o + private static final String compiledU = compilePattern("{0} u {1}", new StringBuilder()); + + // Condition to change to e. + // Starts with "hi" or "i" but not with "hie" nor "hia"a + private static final Pattern changeToE = Pattern.compile("(i.*|hi|hi[^ae].*)", Pattern.CASE_INSENSITIVE); + + // Condition to change to u. + // Starts with "o", "ho", and "8". Also "11" by itself. + private static final Pattern changeToU = Pattern.compile("((o|ho|8).*|11)", Pattern.CASE_INSENSITIVE); + + // Pattern in the ICU Data which might need to add a DASH after VAV. + private static final String compiledVav = compilePattern("{0} \u05D5{1}", new StringBuilder()); + + // Pattern to add a DASH after VAV. + private static final String compiledVavDash = compilePattern("{0} \u05D5-{1}", new StringBuilder()); + + // Condition to change to VAV follow by a dash. + // Starts with non Hebrew letter. + private static final Pattern changeToVavDash = Pattern.compile("^[\\P{InHebrew}].*$"); + + // A factory function to create function based on locale + // Handle specal case of Spanish and Hebrew + private PatternHandler createPatternHandler(String two, String end) { + if (this.locale != null) { + String language = this.locale.getLanguage(); + if (language.equals("es")) { + boolean twoIsY = two.equals(compiledY); + boolean endIsY = end.equals(compiledY); + if (twoIsY || endIsY) { + return new ContextualHandler( + changeToE, twoIsY ? compiledE : two, two, endIsY ? compiledE : end, end); + } + boolean twoIsO = two.equals(compiledO); + boolean endIsO = end.equals(compiledO); + if (twoIsO || endIsO) { + return new ContextualHandler( + changeToU, twoIsO ? compiledU : two, two, endIsO ? compiledU : end, end); + } + } else if (language.equals("he") || language.equals("iw")) { + boolean twoIsVav = two.equals(compiledVav); + boolean endIsVav = end.equals(compiledVav); + if (twoIsVav || endIsVav) { + return new ContextualHandler(changeToVavDash, + twoIsVav ? compiledVavDash : two, two, endIsVav ? compiledVavDash : end, end); + } + } } - return builder.append(end, it.next(), index == count - 1); + return new StaticHandler(two, end); } /** @@ -241,7 +643,7 @@ final public class ListFormatter { if (count <= 0) { throw new IllegalArgumentException("count must be > 0"); } - ArrayList<String> list = new ArrayList<String>(); + ArrayList<String> list = new ArrayList<>(); for (int i = 0; i < count; i++) { list.add(String.format("{%d}", i)); } @@ -260,64 +662,74 @@ final public class ListFormatter { // Builds a formatted list static class FormattedListBuilder { - private StringBuilder current; - private int offset; + private FormattedStringBuilder string; + boolean needsFields; - // Start is the first object in the list; If recordOffset is true, records the offset of - // this first object. - public FormattedListBuilder(Object start, boolean recordOffset) { - this.current = new StringBuilder(start.toString()); - this.offset = recordOffset ? 0 : -1; + // Start is the first object in the list; If needsFields is true, enable the slightly + // more expensive code path that records offsets of each element. + public FormattedListBuilder(Object start, boolean needsFields) { + string = new FormattedStringBuilder(); + this.needsFields = needsFields; + string.setAppendableField(Field.LITERAL); + appendElement(start, 0); } // Appends additional object. pattern is a template indicating where the new object gets // added in relation to the rest of the list. {0} represents the rest of the list; {1} - // represents the new object in pattern. next is the object to be added. If recordOffset - // is true, records the offset of next in the formatted string. - public FormattedListBuilder append(String pattern, Object next, boolean recordOffset) { - int[] offsets = (recordOffset || offsetRecorded()) ? new int[2] : null; - SimpleFormatterImpl.formatAndReplace( - pattern, current, offsets, current, next.toString()); - if (offsets != null) { - if (offsets[0] == -1 || offsets[1] == -1) { - throw new IllegalArgumentException( - "{0} or {1} missing from pattern " + pattern); + // represents the new object in pattern. next is the object to be added. position is the + // index of the next object in the list of inputs. + public FormattedListBuilder append(String compiledPattern, Object next, int position) { + assert SimpleFormatterImpl.getArgumentLimit(compiledPattern) == 2; + string.setAppendIndex(0); + long state = 0; + while (true) { + state = IterInternal.step(state, compiledPattern, string); + if (state == IterInternal.DONE) { + break; } - if (recordOffset) { - offset = offsets[1]; + int argIndex = IterInternal.getArgIndex(state); + if (argIndex == 0) { + string.setAppendIndex(string.length()); } else { - offset += offsets[0]; + appendElement(next, position); } } return this; } - public void appendTo(Appendable appendable) { - try { - appendable.append(current); - } catch(IOException e) { - throw new ICUUncheckedIOException(e); + private void appendElement(Object element, int position) { + if (needsFields) { + SpanFieldPlaceholder field = new SpanFieldPlaceholder(); + field.spanField = SpanField.LIST_SPAN; + field.normalField = Field.ELEMENT; + field.value = position; + string.append(element.toString(), field); + } else { + string.append(element.toString(), null); } } - @Override - public String toString() { - return current.toString(); + public void appendTo(Appendable appendable) { + Utility.appendTo(string, appendable); } - // Gets the last recorded offset or -1 if no offset recorded. - public int getOffset() { - return offset; + public int getOffset(int fieldPositionFoundIndex) { + return FormattedValueStringBuilderImpl.findSpan(string, fieldPositionFoundIndex); } - private boolean offsetRecorded() { - return offset >= 0; + @Override + public String toString() { + return string.toString(); + } + + public FormattedList toValue() { + return new FormattedList(string); } } private static class Cache { private final ICUCache<String, ListFormatter> cache = - new SimpleCache<String, ListFormatter>(); + new SimpleCache<>(); public ListFormatter get(ULocale locale, String style) { String key = String.format("%s:%s", locale.toString(), style); @@ -343,4 +755,42 @@ final public class ListFormatter { } static Cache cache = new Cache(); + + static String typeWidthToStyleString(Type type, Width width) { + switch (type) { + case AND: + switch (width) { + case WIDE: + return "standard"; + case SHORT: + return "standard-short"; + case NARROW: + return "standard-narrow"; + } + break; + + case OR: + switch (width) { + case WIDE: + return "or"; + case SHORT: + return "or-short"; + case NARROW: + return "or-narrow"; + } + break; + + case UNITS: + switch (width) { + case WIDE: + return "unit"; + case SHORT: + return "unit-short"; + case NARROW: + return "unit-narrow"; + } + } + + return null; + } } diff --git a/android_icu4j/src/main/java/android/icu/text/MeasureFormat.java b/android_icu4j/src/main/java/android/icu/text/MeasureFormat.java index f9d079145..4122d189e 100644 --- a/android_icu4j/src/main/java/android/icu/text/MeasureFormat.java +++ b/android_icu4j/src/main/java/android/icu/text/MeasureFormat.java @@ -32,13 +32,16 @@ import java.util.concurrent.ConcurrentHashMap; import android.icu.impl.DontCareFieldPosition; import android.icu.impl.FormattedStringBuilder; +import android.icu.impl.FormattedValueStringBuilderImpl; import android.icu.impl.ICUData; import android.icu.impl.ICUResourceBundle; import android.icu.impl.SimpleCache; import android.icu.impl.SimpleFormatterImpl; +import android.icu.impl.Utility; +import android.icu.impl.number.DecimalQuantity; +import android.icu.impl.number.DecimalQuantity_DualStorageBCD; import android.icu.impl.number.LongNameHandler; import android.icu.impl.number.RoundingUtils; -import android.icu.number.FormattedNumber; import android.icu.number.IntegerWidth; import android.icu.number.LocalizedNumberFormatter; import android.icu.number.NumberFormatter; @@ -298,9 +301,10 @@ public class MeasureFormat extends UFormat { } else if (obj instanceof Measure[]) { formatMeasuresInternal(toAppendTo, fpos, (Measure[]) obj); } else if (obj instanceof Measure) { - FormattedNumber result = formatMeasure((Measure) obj); - result.nextFieldPosition(fpos); // No offset: toAppendTo.length() is considered below - result.appendTo(toAppendTo); + FormattedStringBuilder result = formatMeasure((Measure) obj); + // No offset: toAppendTo.length() is considered below + FormattedValueStringBuilderImpl.nextFieldPosition(result, fpos); + Utility.appendTo(result, toAppendTo); } else { throw new IllegalArgumentException(obj.toString()); } @@ -361,11 +365,13 @@ public class MeasureFormat extends UFormat { MeasureUnit perUnit, StringBuilder appendTo, FieldPosition pos) { - FormattedNumber result = getUnitFormatterFromCache(NUMBER_FORMATTER_STANDARD, - measure.getUnit(), - perUnit).format(measure.getNumber()); - DecimalFormat.fieldPositionHelper(result, pos, appendTo.length()); - result.appendTo(appendTo); + DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(measure.getNumber()); + FormattedStringBuilder string = new FormattedStringBuilder(); + getUnitFormatterFromCache( + NUMBER_FORMATTER_STANDARD, measure.getUnit(), perUnit + ).formatImpl(dq, string); + DecimalFormat.fieldPositionHelper(dq, string, pos, appendTo.length()); + Utility.appendTo(string, appendTo); return appendTo; } @@ -407,9 +413,9 @@ public class MeasureFormat extends UFormat { return; } if (measures.length == 1) { - FormattedNumber result = formatMeasure(measures[0]); - result.nextFieldPosition(fieldPosition); - result.appendTo(appendTo); + FormattedStringBuilder result = formatMeasure(measures[0]); + FormattedValueStringBuilderImpl.nextFieldPosition(result, fieldPosition); + Utility.appendTo(result, appendTo); return; } @@ -438,7 +444,7 @@ public class MeasureFormat extends UFormat { results[i] = formatMeasureInteger(measures[i]).toString(); } } - FormattedListBuilder builder = listFormatter.format(Arrays.asList(results), -1); + FormattedListBuilder builder = listFormatter.formatImpl(Arrays.asList(results), false); builder.appendTo(appendTo); } @@ -727,20 +733,26 @@ public class MeasureFormat extends UFormat { /// END NUMBER FORMATTER CACHING MACHINERY /// - private FormattedNumber formatMeasure(Measure measure) { + private FormattedStringBuilder formatMeasure(Measure measure) { MeasureUnit unit = measure.getUnit(); + DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(measure.getNumber()); + FormattedStringBuilder string = new FormattedStringBuilder(); if (unit instanceof Currency) { - return getUnitFormatterFromCache(NUMBER_FORMATTER_CURRENCY, unit, null) - .format(measure.getNumber()); + getUnitFormatterFromCache(NUMBER_FORMATTER_CURRENCY, unit, null) + .formatImpl(dq, string); } else { - return getUnitFormatterFromCache(NUMBER_FORMATTER_STANDARD, unit, null) - .format(measure.getNumber()); + getUnitFormatterFromCache(NUMBER_FORMATTER_STANDARD, unit, null) + .formatImpl(dq, string); } + return string; } - private FormattedNumber formatMeasureInteger(Measure measure) { - return getUnitFormatterFromCache(NUMBER_FORMATTER_INTEGER, measure.getUnit(), null) - .format(measure.getNumber()); + private FormattedStringBuilder formatMeasureInteger(Measure measure) { + DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(measure.getNumber()); + FormattedStringBuilder string = new FormattedStringBuilder(); + getUnitFormatterFromCache(NUMBER_FORMATTER_INTEGER, measure.getUnit(), null) + .formatImpl(dq, string); + return string; } private void formatMeasuresSlowTrack( @@ -756,27 +768,27 @@ public class MeasureFormat extends UFormat { int fieldPositionFoundIndex = -1; for (int i = 0; i < measures.length; ++i) { - FormattedNumber result; + FormattedStringBuilder result; if (i == measures.length - 1) { result = formatMeasure(measures[i]); } else { result = formatMeasureInteger(measures[i]); } if (fieldPositionFoundIndex == -1) { - result.nextFieldPosition(fpos); + FormattedValueStringBuilderImpl.nextFieldPosition(result, fpos); if (fpos.getEndIndex() != 0) { fieldPositionFoundIndex = i; } } results[i] = result.toString(); } - ListFormatter.FormattedListBuilder builder = listFormatter.format(Arrays.asList(results), - fieldPositionFoundIndex); + ListFormatter.FormattedListBuilder builder = listFormatter.formatImpl(Arrays.asList(results), true); // Fix up FieldPosition indexes if our field is found. - if (builder.getOffset() != -1) { - fieldPosition.setBeginIndex(fpos.getBeginIndex() + builder.getOffset()); - fieldPosition.setEndIndex(fpos.getEndIndex() + builder.getOffset()); + int offset = builder.getOffset(fieldPositionFoundIndex); + if (offset != -1) { + fieldPosition.setBeginIndex(fpos.getBeginIndex() + offset); + fieldPosition.setEndIndex(fpos.getEndIndex() + offset); } builder.appendTo(appendTo); } diff --git a/android_icu4j/src/main/java/android/icu/text/NumberFormat.java b/android_icu4j/src/main/java/android/icu/text/NumberFormat.java index aad1a3cc9..eeecd6e6f 100644 --- a/android_icu4j/src/main/java/android/icu/text/NumberFormat.java +++ b/android_icu4j/src/main/java/android/icu/text/NumberFormat.java @@ -1795,12 +1795,12 @@ public abstract class NumberFormat extends UFormat { public static final Field CURRENCY = new Field("currency"); /** - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final Field MEASURE_UNIT = new Field("measure unit"); /** - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final Field COMPACT = new Field("compact"); diff --git a/android_icu4j/src/main/java/android/icu/text/PluralRules.java b/android_icu4j/src/main/java/android/icu/text/PluralRules.java index 737b6b923..7dbba6951 100644 --- a/android_icu4j/src/main/java/android/icu/text/PluralRules.java +++ b/android_icu4j/src/main/java/android/icu/text/PluralRules.java @@ -173,20 +173,11 @@ public class PluralRules implements Serializable { static final UnicodeSet ALLOWED_ID = new UnicodeSet("[a-z]").freeze(); // TODO Remove RulesList by moving its API and fields into PluralRules. + /** - * @deprecated This API is ICU internal only. - * @hide original deprecated declaration - * @hide draft / provisional / internal are hidden on Android - */ - @Deprecated - public static final String CATEGORY_SEPARATOR = "; "; - /** - * @deprecated This API is ICU internal only. * @hide original deprecated declaration - * @hide draft / provisional / internal are hidden on Android */ - @Deprecated - public static final String KEYWORD_RULE_SEPARATOR = ": "; + private static final String CATEGORY_SEPARATOR = "; "; private static final long serialVersionUID = 1; @@ -473,6 +464,16 @@ public class PluralRules implements Serializable { w, /** + * Suppressed exponent for compact notation (exponent needed in + * scientific notation with compact notation to approximate i). + * + * @deprecated This API is ICU internal only. + * @hide draft / provisional / internal are hidden on Android + */ + @Deprecated + e, + + /** * THIS OPERAND IS DEPRECATED AND HAS BEEN REMOVED FROM THE SPEC. * * <p>Returns the integer value, but will fail if the number has fraction digits. @@ -847,6 +848,7 @@ public class PluralRules implements Serializable { case t: return decimalDigitsWithoutTrailingZeros; case v: return visibleDecimalDigitCount; case w: return visibleDecimalDigitCountWithoutTrailingZeros; + case e: return 0; default: return source; } } @@ -2113,7 +2115,7 @@ public class PluralRules implements Serializable { * * @param number The number for which the rule has to be determined. * @return The keyword of the selected rule. - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public String select(FormattedNumber number) { return rules.select(number.getFixedDecimal()); @@ -2287,12 +2289,9 @@ public class PluralRules implements Serializable { } /** - * @deprecated This API is ICU internal only. * @hide original deprecated declaration - * @hide draft / provisional / internal are hidden on Android */ - @Deprecated - public boolean addSample(String keyword, Number sample, int maxCount, Set<Double> result) { + private boolean addSample(String keyword, Number sample, int maxCount, Set<Double> result) { String selectedKeyword = sample instanceof FixedDecimal ? select((FixedDecimal)sample) : select(sample.doubleValue()); if (selectedKeyword.equals(keyword)) { result.add(sample.doubleValue()); @@ -2542,12 +2541,9 @@ public class PluralRules implements Serializable { } /** - * @deprecated internal * @hide original deprecated declaration - * @hide draft / provisional / internal are hidden on Android */ - @Deprecated - public Boolean isLimited(String keyword) { + Boolean isLimited(String keyword) { return rules.isLimited(keyword, SampleType.INTEGER); } diff --git a/android_icu4j/src/main/java/android/icu/text/RBBIRuleScanner.java b/android_icu4j/src/main/java/android/icu/text/RBBIRuleScanner.java index 71a84f12a..77a76f0d1 100644 --- a/android_icu4j/src/main/java/android/icu/text/RBBIRuleScanner.java +++ b/android_icu4j/src/main/java/android/icu/text/RBBIRuleScanner.java @@ -74,7 +74,7 @@ class RBBIRuleScanner { RBBISymbolTable fSymbolTable; // symbol table, holds definitions of // $variable symbols. - HashMap<String, RBBISetTableEl> fSetTable = new HashMap<String, RBBISetTableEl>(); // UnicocodeSet hash table, holds indexes to + HashMap<String, RBBISetTableEl> fSetTable = new HashMap<>(); // UnicocodeSet hash table, holds indexes to // the sets created while parsing rules. // The key is the string used for creating // the set. @@ -934,7 +934,7 @@ class RBBIRuleScanner { // Perform any action specified by this row in the state table. if (doParseActions(tableEl.fAction) == false) { // Break out of the state machine loop if the - // the action signalled some kind of error, or + // the action signaled some kind of error, or // the action was to exit, occurs on normal end-of-rules-input. break; } @@ -1071,7 +1071,7 @@ class RBBIRuleScanner { error(RBBIRuleBuilder.U_BRK_RULE_EMPTY_SET); } - // Advance the RBBI parse postion over the UnicodeSet pattern. + // Advance the RBBI parse position over the UnicodeSet pattern. // Don't just set fScanIndex because the line/char positions maintained // for error reporting would be thrown off. i = pos.getIndex(); @@ -1090,12 +1090,17 @@ class RBBIRuleScanner { n.fText = fRB.fRules.substring(n.fFirstPos, n.fLastPos); // findSetFor() serves several purposes here: // - Adopts storage for the UnicodeSet, will be responsible for deleting. - // - Mantains collection of all sets in use, needed later for establishing + // - Maintains collection of all sets in use, needed later for establishing // character categories for run time engine. - // - Eliminates mulitiple instances of the same set. + // - Eliminates multiple instances of the same set. // - Creates a new uset node if necessary (if this isn't a duplicate.) findSetFor(n.fText, n, uset); } + /** + * @return the number of rules that have been seen. + */ + int numRules() { + return fRuleNum; + } } - diff --git a/android_icu4j/src/main/java/android/icu/text/RBBITableBuilder.java b/android_icu4j/src/main/java/android/icu/text/RBBITableBuilder.java index 99fd43f97..73561f28f 100644 --- a/android_icu4j/src/main/java/android/icu/text/RBBITableBuilder.java +++ b/android_icu4j/src/main/java/android/icu/text/RBBITableBuilder.java @@ -54,8 +54,8 @@ class RBBITableBuilder { // in RBBITableBuilder.fDStates RBBIStateDescriptor(int maxInputSymbol) { - fTagVals = new TreeSet<Integer>(); - fPositions = new HashSet<RBBINode>(); + fTagVals = new TreeSet<>(); + fPositions = new HashSet<>(); fDtran = new int[maxInputSymbol+1]; // fDtran needs to be pre-sized. // It is indexed by input symbols, and will // hold the next state number for each @@ -75,6 +75,9 @@ class RBBITableBuilder { /** Synthesized safe table, a List of row arrays. */ private List<short[]> fSafeTable; + /** Map from rule number (fVal in look ahead nodes) to sequential lookahead index. */ + int[] fLookAheadRuleMap; + //----------------------------------------------------------------------------- // // Constructor for RBBITableBuilder. @@ -85,7 +88,7 @@ class RBBITableBuilder { RBBITableBuilder(RBBIRuleBuilder rb, int rootNodeIx) { fRootIx = rootNodeIx; fRB = rb; - fDStates = new ArrayList<RBBIStateDescriptor>(); + fDStates = new ArrayList<>(); } @@ -138,7 +141,7 @@ class RBBITableBuilder { RBBINode cn = new RBBINode(RBBINode.opCat); cn.fLeftChild = fRB.fTreeRoots[fRootIx]; fRB.fTreeRoots[fRootIx].fParent = cn; - cn.fRightChild = new RBBINode(RBBINode.endMark); + RBBINode endMarkerNode = cn.fRightChild = new RBBINode(RBBINode.endMark); cn.fRightChild.fParent = cn; fRB.fTreeRoots[fRootIx] = cn; @@ -173,7 +176,7 @@ class RBBITableBuilder { // For "chained" rules, modify the followPos sets // if (fRB.fChainRules) { - calcChainedFollowPos(fRB.fTreeRoots[fRootIx]); + calcChainedFollowPos(fRB.fTreeRoots[fRootIx], endMarkerNode); } // @@ -187,6 +190,7 @@ class RBBITableBuilder { // Build the DFA state transition tables. // buildStateTable(); + mapLookAheadRules(); flagAcceptingStates(); flagLookAheadStates(); flagTaggedStates(); @@ -391,13 +395,9 @@ class RBBITableBuilder { // to implement rule chaining. NOT described by Aho // //----------------------------------------------------------------------------- - void calcChainedFollowPos(RBBINode tree) { - - List<RBBINode> endMarkerNodes = new ArrayList<RBBINode>(); - List<RBBINode> leafNodes = new ArrayList<RBBINode>(); + void calcChainedFollowPos(RBBINode tree, RBBINode endMarkNode) { - // get a list of all endmarker nodes. - tree.findNodes(endMarkerNodes, RBBINode.endMark); + List<RBBINode> leafNodes = new ArrayList<>(); // get a list all leaf nodes tree.findNodes(leafNodes, RBBINode.leafChar); @@ -406,10 +406,10 @@ class RBBITableBuilder { // with inbound chaining enabled, which is the union of the // firstPosition sets from each of the rule root nodes. - List<RBBINode> ruleRootNodes = new ArrayList<RBBINode>(); + List<RBBINode> ruleRootNodes = new ArrayList<>(); addRuleRootNodes(ruleRootNodes, tree); - Set<RBBINode> matchStartNodes = new HashSet<RBBINode>(); + Set<RBBINode> matchStartNodes = new HashSet<>(); for (RBBINode node: ruleRootNodes) { if (node.fChainIn) { matchStartNodes.addAll(node.fFirstPosSet); @@ -418,28 +418,26 @@ class RBBITableBuilder { // Iterate over all leaf nodes, // - for (RBBINode tNode : leafNodes) { - RBBINode endNode = null; + for (RBBINode endNode : leafNodes) { // Identify leaf nodes that correspond to overall rule match positions. - // These include an endMarkerNode in their followPos sets. - for (RBBINode endMarkerNode : endMarkerNodes) { - if (tNode.fFollowPos.contains(endMarkerNode)) { - endNode = tNode; - break; - } - } - if (endNode == null) { - // node wasn't an end node. Try again with the next. + // These include the endMarkNode in their followPos sets. + // + // Note: do not consider other end marker nodes, those that are added to + // look-ahead rules. These can't chain; a match immediately stops + // further matching. This leaves exactly one end marker node, the one + // at the end of the complete tree. + + if (!endNode.fFollowPos.contains(endMarkNode)) { continue; } // We've got a node that can end a match. - // Line Break Specific hack: If this node's val correspond to the $CM char class, - // don't chain from it. - // TODO: Add rule syntax for this behavior, get specifics out of here and - // into the rule file. + // !!LBCMNoChain implementation: If this node's val correspond to + // the Line Break $CM char class, don't chain from it. + // TODO: Remove this. !!LBCMNoChain is deprecated, and is not used + // by any of the standard ICU rules. if (fRB.fLBCMNoChain) { int c = this.fRB.fSetBuilder.getFirstChar(endNode.fVal); if (c != -1) { @@ -572,7 +570,7 @@ class RBBITableBuilder { for (RBBINode p : T.fPositions) { if ((p.fType == RBBINode.leafChar) && (p.fVal == a)) { if (U == null) { - U = new HashSet<RBBINode>(); + U = new HashSet<>(); } U.addAll(p.fFollowPos); } @@ -611,7 +609,66 @@ class RBBITableBuilder { } } - + /** + * mapLookAheadRules + * + */ + void mapLookAheadRules() { + fLookAheadRuleMap = new int[fRB.fScanner.numRules() + 1]; + int laSlotsInUse = 0; + + for (RBBIStateDescriptor sd: fDStates) { + int laSlotForState = 0; + + // Establish the look-ahead slot for this state, if the state covers + // any look-ahead nodes - corresponding to the '/' in look-ahead rules. + + // If any of the look-ahead nodes already have a slot assigned, use it, + // otherwise assign a new one. + + boolean sawLookAheadNode = false; + for (RBBINode node: sd.fPositions) { + if (node.fType != RBBINode.lookAhead) { + continue; + } + sawLookAheadNode = true; + int ruleNum = node.fVal; // Set when rule was originally parsed. + assert(ruleNum < fLookAheadRuleMap.length); + assert(ruleNum > 0); + int laSlot = fLookAheadRuleMap[ruleNum]; + if (laSlot != 0) { + if (laSlotForState == 0) { + laSlotForState = laSlot; + } else { + // TODO: figure out if this can fail, change to setting an error code if so. + assert(laSlot == laSlotForState); + } + } + } + if (!sawLookAheadNode) { + continue; + } + + if (laSlotForState == 0) { + laSlotForState = ++laSlotsInUse; + } + + // For each look ahead node covered by this state, + // set the mapping from the node's rule number to the look ahead slot. + // There can be multiple nodes/rule numbers going to the same la slot. + + for (RBBINode node: sd.fPositions) { + if (node.fType != RBBINode.lookAhead) { + continue; + } + int ruleNum = node.fVal; // Set when rule was originally parsed. + int existingVal = fLookAheadRuleMap[ruleNum]; + assert(existingVal == 0 || existingVal == laSlotForState); + fLookAheadRuleMap[ruleNum] = laSlotForState; + } + } + + } //----------------------------------------------------------------------------- // @@ -623,7 +680,7 @@ class RBBITableBuilder { // //----------------------------------------------------------------------------- void flagAcceptingStates() { - List<RBBINode> endMarkerNodes = new ArrayList<RBBINode>(); + List<RBBINode> endMarkerNodes = new ArrayList<>(); RBBINode endMarker; int i; int n; @@ -641,29 +698,20 @@ class RBBITableBuilder { // If no other value was specified, force it to -1. if (sd.fAccepting==0) { - // State hasn't been marked as accepting yet. Do it now. - sd.fAccepting = endMarker.fVal; + // State hasn't been marked as accepting yet. Do it now. + sd.fAccepting = fLookAheadRuleMap[endMarker.fVal]; if (sd.fAccepting == 0) { sd.fAccepting = -1; - } + } } if (sd.fAccepting==-1 && endMarker.fVal != 0) { - // Both lookahead and non-lookahead accepting for this state. - // Favor the look-ahead. Expedient for line break. - // TODO: need a more elegant resolution for conflicting rules. - sd.fAccepting = endMarker.fVal; - } - // implicit else: - // if sd.fAccepting already had a value other than 0 or -1, leave it be. - - // If the end marker node is from a look-ahead rule, set - // the fLookAhead field for this state also. - if (endMarker.fLookAheadEnd) { - // TODO: don't change value if already set? - // TODO: allow for more than one active look-ahead rule in engine. - // Make value here an index to a side array in engine? - sd.fLookAhead = sd.fAccepting; + // Both lookahead and non-lookahead accepting for this state. + // Favor the look-ahead, because a look-ahead match needs to + // immediately stop the run-time engine. First match, not longest. + sd.fAccepting = fLookAheadRuleMap[endMarker.fVal]; } + // implicit else: + // if sd.fAccepting already had a value other than 0 or -1, leave it be. } } } @@ -676,7 +724,7 @@ class RBBITableBuilder { // //----------------------------------------------------------------------------- void flagLookAheadStates() { - List<RBBINode> lookAheadNodes = new ArrayList<RBBINode>(); + List<RBBINode> lookAheadNodes = new ArrayList<>(); RBBINode lookAheadNode; int i; int n; @@ -684,11 +732,12 @@ class RBBITableBuilder { fRB.fTreeRoots[fRootIx].findNodes(lookAheadNodes, RBBINode.lookAhead); for (i=0; i<lookAheadNodes.size(); i++) { lookAheadNode = lookAheadNodes.get(i); - for (n=0; n<fDStates.size(); n++) { RBBIStateDescriptor sd = fDStates.get(n); if (sd.fPositions.contains(lookAheadNode)) { - sd.fLookAhead = lookAheadNode.fVal; + int lookaheadSlot = fLookAheadRuleMap[lookAheadNode.fVal]; + assert(sd.fLookAhead == 0 || sd.fLookAhead == lookaheadSlot); + sd.fLookAhead = lookaheadSlot; } } } @@ -703,7 +752,7 @@ class RBBITableBuilder { // //----------------------------------------------------------------------------- void flagTaggedStates() { - List<RBBINode> tagNodes = new ArrayList<RBBINode>(); + List<RBBINode> tagNodes = new ArrayList<>(); RBBINode tagNode; int i; int n; @@ -766,12 +815,12 @@ class RBBITableBuilder { fRB.fRuleStatusVals.add(Integer.valueOf(1)); // Num of statuses in group fRB.fRuleStatusVals.add(Integer.valueOf(0)); // and our single status of zero - SortedSet<Integer> s0 = new TreeSet<Integer>(); - Integer izero = Integer.valueOf(0); - fRB.fStatusSets.put(s0, izero); - SortedSet<Integer> s1 = new TreeSet<Integer>(); - s1.add(izero); - fRB.fStatusSets.put(s0, izero); + SortedSet<Integer> s0 = new TreeSet<>(); // mapping for rules with no explicit tagging + fRB.fStatusSets.put(s0, Integer.valueOf(0)); // (key is an empty set). + + SortedSet<Integer> s1 = new TreeSet<>(); // mapping for rules with explicit tagging of {0} + s1.add(Integer.valueOf(0)); + fRB.fStatusSets.put(s1, Integer.valueOf(0)); } // For each state, check whether the state's status tag values are @@ -987,16 +1036,6 @@ class RBBITableBuilder { } sd.fDtran[col] = newVal; } - if (sd.fAccepting == duplState) { - sd.fAccepting = keepState; - } else if (sd.fAccepting > duplState) { - sd.fAccepting--; - } - if (sd.fLookAhead == duplState) { - sd.fLookAhead = keepState; - } else if (sd.fLookAhead > duplState) { - sd.fLookAhead--; - } } } @@ -1168,7 +1207,7 @@ class RBBITableBuilder { // fLookAhead, etc. are not needed for the safe table, and are omitted at this stage of building. assert(fSafeTable == null); - fSafeTable = new ArrayList<short[]>(); + fSafeTable = new ArrayList<>(); for (int row=0; row<numCharClasses + 2; ++row) { fSafeTable.add(new short[numCharClasses]); } diff --git a/android_icu4j/src/main/java/android/icu/text/RelativeDateTimeFormatter.java b/android_icu4j/src/main/java/android/icu/text/RelativeDateTimeFormatter.java index d6a538fdf..dd2e4c6ec 100644 --- a/android_icu4j/src/main/java/android/icu/text/RelativeDateTimeFormatter.java +++ b/android_icu4j/src/main/java/android/icu/text/RelativeDateTimeFormatter.java @@ -9,7 +9,6 @@ */ package android.icu.text; -import java.io.IOException; import java.io.InvalidObjectException; import java.text.AttributedCharacterIterator; import java.text.Format; @@ -26,13 +25,12 @@ import android.icu.impl.SimpleFormatterImpl; import android.icu.impl.SoftCache; import android.icu.impl.StandardPlural; import android.icu.impl.UResource; +import android.icu.impl.Utility; import android.icu.impl.number.DecimalQuantity; import android.icu.impl.number.DecimalQuantity_DualStorageBCD; -import android.icu.impl.number.SimpleModifier; import android.icu.lang.UCharacter; import android.icu.util.Calendar; import android.icu.util.ICUException; -import android.icu.util.ICUUncheckedIOException; import android.icu.util.ULocale; import android.icu.util.UResourceBundle; @@ -223,7 +221,7 @@ public final class RelativeDateTimeFormatter { /** * Quarter - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ QUARTER, @@ -366,22 +364,17 @@ public final class RelativeDateTimeFormatter { * constants defined here. * <p> * @hide Only a subset of ICU is exposed in Android - * @hide draft / provisional / internal are hidden on Android */ public static class Field extends Format.Field { private static final long serialVersionUID = -5327685528663492325L; /** * Represents a literal text string, like "tomorrow" or "days ago". - * - * @hide draft / provisional / internal are hidden on Android */ public static final Field LITERAL = new Field("literal"); /** * Represents a number quantity, like "3" in "3 days ago". - * - * @hide draft / provisional / internal are hidden on Android */ public static final Field NUMERIC = new Field("numeric"); @@ -415,7 +408,6 @@ public final class RelativeDateTimeFormatter { * * @author sffc * @hide Only a subset of ICU is exposed in Android - * @hide draft / provisional / internal are hidden on Android */ public static class FormattedRelativeDateTime implements FormattedValue { @@ -427,8 +419,6 @@ public final class RelativeDateTimeFormatter { /** * {@inheritDoc} - * - * @hide draft / provisional / internal are hidden on Android */ @Override public String toString() { @@ -437,8 +427,6 @@ public final class RelativeDateTimeFormatter { /** * {@inheritDoc} - * - * @hide draft / provisional / internal are hidden on Android */ @Override public int length() { @@ -447,8 +435,6 @@ public final class RelativeDateTimeFormatter { /** * {@inheritDoc} - * - * @hide draft / provisional / internal are hidden on Android */ @Override public char charAt(int index) { @@ -457,8 +443,6 @@ public final class RelativeDateTimeFormatter { /** * {@inheritDoc} - * - * @hide draft / provisional / internal are hidden on Android */ @Override public CharSequence subSequence(int start, int end) { @@ -467,24 +451,14 @@ public final class RelativeDateTimeFormatter { /** * {@inheritDoc} - * - * @hide draft / provisional / internal are hidden on Android */ @Override public <A extends Appendable> A appendTo(A appendable) { - try { - appendable.append(string); - } catch (IOException e) { - // Throw as an unchecked exception to avoid users needing try/catch - throw new ICUUncheckedIOException(e); - } - return appendable; + return Utility.appendTo(string, appendable); } /** * {@inheritDoc} - * - * @hide draft / provisional / internal are hidden on Android */ @Override public boolean nextPosition(ConstrainedFieldPosition cfpos) { @@ -493,8 +467,6 @@ public final class RelativeDateTimeFormatter { /** * {@inheritDoc} - * - * @hide draft / provisional / internal are hidden on Android */ @Override public AttributedCharacterIterator toCharacterIterator() { @@ -626,7 +598,7 @@ public final class RelativeDateTimeFormatter { * @return the formatted relative datetime * @throws IllegalArgumentException if direction is something other than * NEXT or LAST. - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public FormattedRelativeDateTime formatToValue(double quantity, Direction direction, RelativeUnit unit) { checkNoAdjustForContext(); @@ -654,8 +626,7 @@ public final class RelativeDateTimeFormatter { StandardPlural pluralForm = StandardPlural.orOtherFromString(pluralKeyword); String compiledPattern = getRelativeUnitPluralPattern(style, unit, pastFutureIndex, pluralForm); - SimpleModifier modifier = new SimpleModifier(compiledPattern, Field.LITERAL, false); - modifier.formatAsPrefixSuffix(output, 0, output.length()); + SimpleFormatterImpl.formatPrefixSuffix(compiledPattern, Field.LITERAL, 0, output.length(), output); return output; } @@ -695,7 +666,7 @@ public final class RelativeDateTimeFormatter { * date, e.g. RelativeDateTimeUnit.WEEK, * RelativeDateTimeUnit.FRIDAY. * @return The formatted string (may be empty in case of error) - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public FormattedRelativeDateTime formatNumericToValue(double offset, RelativeDateTimeUnit unit) { checkNoAdjustForContext(); @@ -768,7 +739,7 @@ public final class RelativeDateTimeFormatter { * return null to signal that no formatted string is available. * @throws IllegalArgumentException if the direction is incompatible with * unit this can occur with NOW which can only take PLAIN. - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public FormattedRelativeDateTime formatToValue(Direction direction, AbsoluteUnit unit) { checkNoAdjustForContext(); @@ -838,7 +809,7 @@ public final class RelativeDateTimeFormatter { * date, e.g. RelativeDateTimeUnit.WEEK, * RelativeDateTimeUnit.FRIDAY. * @return The formatted string (may be empty in case of error) - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public FormattedRelativeDateTime formatToValue(double offset, RelativeDateTimeUnit unit) { checkNoAdjustForContext(); diff --git a/android_icu4j/src/main/java/android/icu/text/RuleBasedBreakIterator.java b/android_icu4j/src/main/java/android/icu/text/RuleBasedBreakIterator.java index 7425ab9f4..df416d1d2 100644 --- a/android_icu4j/src/main/java/android/icu/text/RuleBasedBreakIterator.java +++ b/android_icu4j/src/main/java/android/icu/text/RuleBasedBreakIterator.java @@ -916,9 +916,14 @@ public class RuleBasedBreakIterator extends BreakIterator { } } + // If we are at the position of the '/' in a look-ahead (hard break) rule; + // record the current position, to be returned later, if the full rule matches. + // TODO: Move this check before the previous check of fAccepting. + // This would enable hard-break rules with no following context. + // But there are line break test failures when trying this. Investigate. + // Issue ICU-20837 int rule = stateTable[row + RBBIDataWrapper.LOOKAHEAD]; if (rule != 0) { - // At the position of a '/' in a look-ahead match. Record it. int pos = text.getIndex(); if (c >= UTF16.SUPPLEMENTARY_MIN_VALUE && c <= UTF16.CODEPOINT_MAX_VALUE) { // The iterator has been left in the middle of a surrogate pair. diff --git a/android_icu4j/src/main/java/android/icu/text/SimpleDateFormat.java b/android_icu4j/src/main/java/android/icu/text/SimpleDateFormat.java index ac698bf60..67a645507 100644 --- a/android_icu4j/src/main/java/android/icu/text/SimpleDateFormat.java +++ b/android_icu4j/src/main/java/android/icu/text/SimpleDateFormat.java @@ -1386,10 +1386,10 @@ public class SimpleDateFormat extends DateFormat { } if (useFastFormat) { subFormat(toAppendTo, item.type, item.length, toAppendTo.length(), - i, capitalizationContext, pos, cal); + i, capitalizationContext, pos, item.type, cal); } else { toAppendTo.append(subFormat(item.type, item.length, toAppendTo.length(), - i, capitalizationContext, pos, cal)); + i, capitalizationContext, pos, item.type, cal)); } if (attributes != null) { // Check the sub format length @@ -1535,7 +1535,7 @@ public class SimpleDateFormat extends DateFormat { throws IllegalArgumentException { // Note: formatData is ignored - return subFormat(ch, count, beginOffset, 0, DisplayContext.CAPITALIZATION_NONE, pos, cal); + return subFormat(ch, count, beginOffset, 0, DisplayContext.CAPITALIZATION_NONE, pos, ch, cal); } /** @@ -1543,17 +1543,17 @@ public class SimpleDateFormat extends DateFormat { * adds fieldNum and capitalizationContext parameters. * * @deprecated This API is ICU internal only. - * @hide original deprecated declaration * @hide draft / provisional / internal are hidden on Android */ @Deprecated protected String subFormat(char ch, int count, int beginOffset, int fieldNum, DisplayContext capitalizationContext, FieldPosition pos, + char patternCharToOutput, Calendar cal) { StringBuffer buf = new StringBuffer(); - subFormat(buf, ch, count, beginOffset, fieldNum, capitalizationContext, pos, cal); + subFormat(buf, ch, count, beginOffset, fieldNum, capitalizationContext, pos, patternCharToOutput, cal); return buf.toString(); } @@ -1567,7 +1567,6 @@ public class SimpleDateFormat extends DateFormat { * has to pass it in to us. * * @deprecated This API is ICU internal only. - * @hide original deprecated declaration * @hide draft / provisional / internal are hidden on Android */ @Deprecated @@ -1576,6 +1575,7 @@ public class SimpleDateFormat extends DateFormat { char ch, int count, int beginOffset, int fieldNum, DisplayContext capitalizationContext, FieldPosition pos, + char patternCharToOutput, Calendar cal) { final int maxIntCount = Integer.MAX_VALUE; @@ -1934,7 +1934,10 @@ public class SimpleDateFormat extends DateFormat { if (toAppend == null) { // Time isn't exactly midnight or noon (as displayed) or localized string doesn't // exist for requested period. Fall back to am/pm instead. - subFormat(buf, 'a', count, beginOffset, fieldNum, capitalizationContext, pos, cal); + // We are passing a different patternCharToOutput because we want to add + // 'b' to field position. This makes this fallback stable when + // there is a data change on locales. + subFormat(buf, 'a', count, beginOffset, fieldNum, capitalizationContext, pos, 'b', cal); } else { buf.append(toAppend); } @@ -1949,8 +1952,11 @@ public class SimpleDateFormat extends DateFormat { if (ruleSet == null) { // Data doesn't exist for the locale we're looking for. // Fall back to am/pm. - subFormat(buf, 'a', count, beginOffset, fieldNum, capitalizationContext, pos, cal); - break; + // We are passing a different patternCharToOutput because we want to add + // 'B' to field position. This makes this fallback stable when + // there is a data change on locales. + subFormat(buf, 'a', count, beginOffset, fieldNum, capitalizationContext, pos, 'B', cal); + return; } // Get current display time. @@ -2015,7 +2021,11 @@ public class SimpleDateFormat extends DateFormat { if (periodType == DayPeriodRules.DayPeriod.AM || periodType == DayPeriodRules.DayPeriod.PM || toAppend == null) { - subFormat(buf, 'a', count, beginOffset, fieldNum, capitalizationContext, pos, cal); + // We are passing a different patternCharToOutput because we want to add + // 'B' to field position. This makes this fallback stable when + // there is a data change on locales. + subFormat(buf, 'a', count, beginOffset, fieldNum, capitalizationContext, pos, 'B', cal); + return; } else { buf.append(toAppend); @@ -2079,12 +2089,13 @@ public class SimpleDateFormat extends DateFormat { } // Set the FieldPosition (for the first occurrence only) + int outputCharIndex = getIndexFromChar(patternCharToOutput); if (pos.getBeginIndex() == pos.getEndIndex()) { - if (pos.getField() == PATTERN_INDEX_TO_DATE_FORMAT_FIELD[patternCharIndex]) { + if (pos.getField() == PATTERN_INDEX_TO_DATE_FORMAT_FIELD[outputCharIndex]) { pos.setBeginIndex(beginOffset); pos.setEndIndex(beginOffset + buf.length() - bufstart); } else if (pos.getFieldAttribute() == - PATTERN_INDEX_TO_DATE_FORMAT_ATTRIBUTE[patternCharIndex]) { + PATTERN_INDEX_TO_DATE_FORMAT_ATTRIBUTE[outputCharIndex]) { pos.setBeginIndex(beginOffset); pos.setEndIndex(beginOffset + buf.length() - bufstart); } @@ -4341,10 +4352,10 @@ public class SimpleDateFormat extends DateFormat { PatternItem item = (PatternItem)items[i]; if (useFastFormat) { subFormat(appendTo, item.type, item.length, appendTo.length(), - i, capSetting, pos, fromCalendar); + i, capSetting, pos, item.type, fromCalendar); } else { appendTo.append(subFormat(item.type, item.length, appendTo.length(), - i, capSetting, pos, fromCalendar)); + i, capSetting, pos, item.type, fromCalendar)); } } } @@ -4359,10 +4370,10 @@ public class SimpleDateFormat extends DateFormat { PatternItem item = (PatternItem)items[i]; if (useFastFormat) { subFormat(appendTo, item.type, item.length, appendTo.length(), - i, capSetting, pos, toCalendar); + i, capSetting, pos, item.type, toCalendar); } else { appendTo.append(subFormat(item.type, item.length, appendTo.length(), - i, capSetting, pos, toCalendar)); + i, capSetting, pos, item.type, toCalendar)); } } } diff --git a/android_icu4j/src/main/java/android/icu/text/UFormat.java b/android_icu4j/src/main/java/android/icu/text/UFormat.java index d4057966d..30a81141b 100644 --- a/android_icu4j/src/main/java/android/icu/text/UFormat.java +++ b/android_icu4j/src/main/java/android/icu/text/UFormat.java @@ -31,15 +31,12 @@ public abstract class UFormat extends Format { * SpanField classes usually have an associated value. * * @hide Only a subset of ICU is exposed in Android - * @hide draft / provisional / internal are hidden on Android */ public static abstract class SpanField extends Format.Field { private static final long serialVersionUID = -4732719509273350606L; /** * Construct a new instance. - * - * @hide draft / provisional / internal are hidden on Android */ protected SpanField(String name) { super(name); diff --git a/android_icu4j/src/main/java/android/icu/util/BytesTrie.java b/android_icu4j/src/main/java/android/icu/util/BytesTrie.java index 2a0ff789b..583fc3e14 100644 --- a/android_icu4j/src/main/java/android/icu/util/BytesTrie.java +++ b/android_icu4j/src/main/java/android/icu/util/BytesTrie.java @@ -53,8 +53,6 @@ public final class BytesTrie implements Cloneable, Iterable<BytesTrie.Entry> { * Makes a shallow copy of the other trie reader object and its state. * Does not copy the byte array which will be shared. * Same as clone() but without the throws clause. - * - * @hide draft / provisional / internal are hidden on Android */ public BytesTrie(BytesTrie other) { bytes_ = other.bytes_; @@ -89,7 +87,6 @@ public final class BytesTrie implements Cloneable, Iterable<BytesTrie.Entry> { * * @return opaque state value * @see #resetToState64 - * @hide draft / provisional / internal are hidden on Android */ public long getState64() { return ((long)remainingMatchLength_ << 32) | pos_; @@ -107,7 +104,6 @@ public final class BytesTrie implements Cloneable, Iterable<BytesTrie.Entry> { * @see #getState64 * @see #resetToState * @see #reset - * @hide draft / provisional / internal are hidden on Android */ public BytesTrie resetToState64(long state) { remainingMatchLength_ = (int)(state >> 32); diff --git a/android_icu4j/src/main/java/android/icu/util/Calendar.java b/android_icu4j/src/main/java/android/icu/util/Calendar.java index 12e750e74..4c4a7e40f 100644 --- a/android_icu4j/src/main/java/android/icu/util/Calendar.java +++ b/android_icu4j/src/main/java/android/icu/util/Calendar.java @@ -4580,7 +4580,7 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca } /** - * Simple, immutable struct-like class for access to the CLDR weekend data. + * Simple, immutable struct-like class for access to the CLDR week data. */ public static final class WeekData { /** @@ -4675,7 +4675,7 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca } /** - * <strong>[icu]</strong> Return simple, immutable struct-like class for access to the CLDR weekend data. + * <strong>[icu]</strong> Return simple, immutable struct-like class for access to the CLDR week data. * @param region The input region. The results are undefined if the region code is not valid. * @return the WeekData for the input region. It is never null. */ @@ -4684,7 +4684,7 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca } /** - * <strong>[icu]</strong> Return simple, immutable struct-like class for access to the weekend data in this calendar. + * <strong>[icu]</strong> Return simple, immutable struct-like class for access to the week data in this calendar. * @return the WeekData for this calendar. */ public WeekData getWeekData() { @@ -4751,7 +4751,7 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca private static final WeekDataCache WEEK_DATA_CACHE = new WeekDataCache(); /* - * Set this calendar to contain week and weekend data for the given region. + * Set this calendar to contain week and week data for the given region. */ private void setWeekData(String region) { if (region == null) { diff --git a/android_icu4j/src/main/java/android/icu/util/CharsTrie.java b/android_icu4j/src/main/java/android/icu/util/CharsTrie.java index a01236b44..ebfd7e3c0 100644 --- a/android_icu4j/src/main/java/android/icu/util/CharsTrie.java +++ b/android_icu4j/src/main/java/android/icu/util/CharsTrie.java @@ -56,8 +56,6 @@ public final class CharsTrie implements Cloneable, Iterable<CharsTrie.Entry> { * Makes a shallow copy of the other trie reader object and its state. * Does not copy the char array which will be shared. * Same as clone() but without the throws clause. - * - * @hide draft / provisional / internal are hidden on Android */ public CharsTrie(CharsTrie other) { chars_ = other.chars_; @@ -92,7 +90,6 @@ public final class CharsTrie implements Cloneable, Iterable<CharsTrie.Entry> { * * @return opaque state value * @see #resetToState64 - * @hide draft / provisional / internal are hidden on Android */ public long getState64() { return ((long)remainingMatchLength_ << 32) | pos_; @@ -110,7 +107,6 @@ public final class CharsTrie implements Cloneable, Iterable<CharsTrie.Entry> { * @see #getState64 * @see #resetToState * @see #reset - * @hide draft / provisional / internal are hidden on Android */ public CharsTrie resetToState64(long state) { remainingMatchLength_ = (int)(state >> 32); diff --git a/android_icu4j/src/main/java/android/icu/util/Currency.java b/android_icu4j/src/main/java/android/icu/util/Currency.java index 93865538b..4e141b39c 100644 --- a/android_icu4j/src/main/java/android/icu/util/Currency.java +++ b/android_icu4j/src/main/java/android/icu/util/Currency.java @@ -87,13 +87,35 @@ public class Currency extends MeasureUnit { /** * Selector for getName() indicating the narrow currency symbol. - * The narrow currency symbol is similar to the regular currency - * symbol, but it always takes the shortest form: for example, - * "$" instead of "US$" for USD in en-CA. + * <p> + * The narrow currency symbol is similar to the regular currency symbol, + * but it always takes the shortest form; + * for example, "$" instead of "US$" for USD in en-CA. */ public static final int NARROW_SYMBOL_NAME = 3; /** + * Selector for getName() indicating the formal currency symbol. + * <p> + * The formal currency symbol is similar to the regular currency symbol, + * but it always takes the form used in formal settings such as banking; + * for example, "NT$" instead of "$" for TWD in zh-TW. + * + * @hide draft / provisional / internal are hidden on Android + */ + public static final int FORMAL_SYMBOL_NAME = 4; + + /** + * Selector for getName() indicating the variant currency symbol. + * <p> + * The variant symbol for a currency is an alternative symbol that is not + * necessarily as widely used as the regular symbol. + * + * @hide draft / provisional / internal are hidden on Android + */ + public static final int VARIANT_SYMBOL_NAME = 5; + + /** * Currency Usage used for Decimal Format */ public enum CurrencyUsage{ @@ -545,6 +567,10 @@ public class Currency extends MeasureUnit { return names.getSymbol(subType); case NARROW_SYMBOL_NAME: return names.getNarrowSymbol(subType); + case FORMAL_SYMBOL_NAME: + return names.getFormalSymbol(subType); + case VARIANT_SYMBOL_NAME: + return names.getVariantSymbol(subType); case LONG_NAME: return names.getName(subType); default: diff --git a/android_icu4j/src/main/java/android/icu/util/GlobalizationPreferences.java b/android_icu4j/src/main/java/android/icu/util/GlobalizationPreferences.java index f51ea61c8..52e797fd7 100644 --- a/android_icu4j/src/main/java/android/icu/util/GlobalizationPreferences.java +++ b/android_icu4j/src/main/java/android/icu/util/GlobalizationPreferences.java @@ -9,7 +9,6 @@ */ package android.icu.util; -import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; @@ -19,6 +18,7 @@ import java.util.List; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; +import java.util.Set; import android.icu.impl.Utility; import android.icu.text.BreakIterator; @@ -249,14 +249,10 @@ public class GlobalizationPreferences implements Freezable<GlobalizationPreferen if (isFrozen()) { throw new UnsupportedOperationException("Attempt to modify immutable object"); } - ULocale[] acceptLocales = null; - try { - acceptLocales = ULocale.parseAcceptLanguage(acceptLanguageString, true); - } catch (ParseException pe) { - //TODO: revisit after 3.8 - throw new IllegalArgumentException("Invalid Accept-Language string"); - } - return setLocales(acceptLocales); + Set<ULocale> acceptSet = LocalePriorityList.add(acceptLanguageString).build().getULocales(); + // processLocales() wants a List even though it only iterates front-to-back. + locales = processLocales(new ArrayList<>(acceptSet)); + return this; } /** @@ -784,6 +780,9 @@ public class GlobalizationPreferences implements Freezable<GlobalizationPreferen * @hide draft / provisional / internal are hidden on Android */ protected List<ULocale> processLocales(List<ULocale> inputLocales) { + // Note: Some of the callers, and non-ICU call sites, could be simpler/more efficient + // if this method took a Collection or even an Iterable. + // Maybe we can change it since this is still @draft and probably not widely overridden. List<ULocale> result = new ArrayList<>(); /* * Step 1: Relocate later occurrence of more specific locale @@ -793,9 +792,7 @@ public class GlobalizationPreferences implements Freezable<GlobalizationPreferen * Before - en_US, fr_FR, zh, en_US_Boston, zh_TW, zh_Hant, fr_CA * After - en_US_Boston, en_US, fr_FR, zh_TW, zh_Hant, zh, fr_CA */ - for (int i = 0; i < inputLocales.size(); i++) { - ULocale uloc = inputLocales.get(i); - + for (ULocale uloc : inputLocales) { String language = uloc.getLanguage(); String script = uloc.getScript(); String country = uloc.getCountry(); diff --git a/android_icu4j/src/main/java/android/icu/util/LocaleMatcher.java b/android_icu4j/src/main/java/android/icu/util/LocaleMatcher.java index e5868561d..39a54ac90 100644 --- a/android_icu4j/src/main/java/android/icu/util/LocaleMatcher.java +++ b/android_icu4j/src/main/java/android/icu/util/LocaleMatcher.java @@ -11,12 +11,11 @@ package android.icu.util; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; import android.icu.impl.locale.LSR; import android.icu.impl.locale.LocaleDistance; @@ -65,7 +64,7 @@ import android.icu.impl.locale.XLikelySubtags; * @hide Only a subset of ICU is exposed in Android */ public final class LocaleMatcher { - private static final LSR UND_LSR = new LSR("und","",""); + private static final LSR UND_LSR = new LSR("und","","", LSR.EXPLICIT_LSR); // In ULocale, "und" and "" make the same object. private static final ULocale UND_ULOCALE = new ULocale("und"); // In Locale, "und" and "" make different objects. @@ -154,6 +153,40 @@ public final class LocaleMatcher { } /** + * Builder option for whether to include or ignore one-way (fallback) match data. + * The LocaleMatcher uses CLDR languageMatch data which includes fallback (oneway=true) entries. + * Sometimes it is desirable to ignore those. + * + * <p>For example, consider a web application with the UI in a given language, + * with a link to another, related web app. + * The link should include the UI language, and the target server may also use + * the client’s Accept-Language header data. + * The target server has its own list of supported languages. + * One may want to favor UI language consistency, that is, + * if there is a decent match for the original UI language, we want to use it, + * but not if it is merely a fallback. + * + * @see LocaleMatcher.Builder#setDirection(LocaleMatcher.Direction) + * @hide Only a subset of ICU is exposed in Android + * @hide draft / provisional / internal are hidden on Android + */ + public enum Direction { + /** + * Locale matching includes one-way matches such as Breton→French. (default) + * + * @hide draft / provisional / internal are hidden on Android + */ + WITH_ONE_WAY, + /** + * Locale matching limited to two-way matches including e.g. Danish↔Norwegian + * but ignoring one-way matches. + * + * @hide draft / provisional / internal are hidden on Android + */ + ONLY_TWO_WAY + } + + /** * Data for the best-matching pair of a desired and a supported locale. * * @hide Only a subset of ICU is exposed in Android @@ -309,6 +342,7 @@ public final class LocaleMatcher { private final int thresholdDistance; private final int demotionPerDesiredLocale; private final FavorSubtag favorSubtag; + private final Direction direction; // These are in input order. private final ULocale[] supportedULocales; @@ -319,9 +353,9 @@ public final class LocaleMatcher { // The distance lookup loops over the supportedLSRs and returns the index of the best match. private final LSR[] supportedLSRs; private final int[] supportedIndexes; + private final int supportedLSRsLength; private final ULocale defaultULocale; private final Locale defaultLocale; - private final int defaultLocaleIndex; /** * LocaleMatcher Builder. @@ -336,6 +370,7 @@ public final class LocaleMatcher { private Demotion demotion; private ULocale defaultLocale; private FavorSubtag favor; + private Direction direction; private Builder() {} @@ -465,6 +500,19 @@ public final class LocaleMatcher { } /** + * Option for whether to include or ignore one-way (fallback) match data. + * By default, they are included. + * + * @param direction the match direction to set. + * @return this Builder object + * @hide draft / provisional / internal are hidden on Android + */ + public Builder setDirection(Direction direction) { + this.direction = direction; + return this; + } + + /** * <i>Internal only!</i> * * @param thresholdDistance the thresholdDistance to set, with -1 = default @@ -500,19 +548,19 @@ public final class LocaleMatcher { public String toString() { StringBuilder s = new StringBuilder().append("{LocaleMatcher.Builder"); if (supportedLocales != null && !supportedLocales.isEmpty()) { - s.append(" supported={").append(supportedLocales.toString()).append('}'); + s.append(" supported={").append(supportedLocales).append('}'); } if (defaultLocale != null) { - s.append(" default=").append(defaultLocale.toString()); + s.append(" default=").append(defaultLocale); } if (favor != null) { - s.append(" distance=").append(favor.toString()); + s.append(" distance=").append(favor); } if (thresholdDistance >= 0) { s.append(String.format(" threshold=%d", thresholdDistance)); } if (demotion != null) { - s.append(" demotion=").append(demotion.toString()); + s.append(" demotion=").append(demotion); } return s.append('}').toString(); } @@ -554,114 +602,102 @@ public final class LocaleMatcher { private LocaleMatcher(Builder builder) { thresholdDistance = builder.thresholdDistance < 0 ? LocaleDistance.INSTANCE.getDefaultScriptDistance() : builder.thresholdDistance; - int supportedLocalesLength = builder.supportedLocales != null ? - builder.supportedLocales.size() : 0; ULocale udef = builder.defaultLocale; Locale def = null; - int idef = -1; + LSR defLSR = null; + if (udef != null) { + def = udef.toLocale(); + defLSR = getMaximalLsrOrUnd(udef); + } // Store the supported locales in input order, // so that when different types are used (e.g., java.util.Locale) // we can return those by parallel index. + int supportedLocalesLength = builder.supportedLocales != null ? + builder.supportedLocales.size() : 0; supportedULocales = new ULocale[supportedLocalesLength]; supportedLocales = new Locale[supportedLocalesLength]; // Supported LRSs in input order. LSR lsrs[] = new LSR[supportedLocalesLength]; - // Also find the first supported locale whose LSR is - // the same as that for the default locale. - LSR defLSR = null; - if (udef != null) { - def = udef.toLocale(); - defLSR = getMaximalLsrOrUnd(udef); - } int i = 0; if (supportedLocalesLength > 0) { for (ULocale locale : builder.supportedLocales) { supportedULocales[i] = locale; supportedLocales[i] = locale.toLocale(); - LSR lsr = lsrs[i] = getMaximalLsrOrUnd(locale); - if (idef < 0 && defLSR != null && lsr.equals(defLSR)) { - idef = i; - } + lsrs[i] = getMaximalLsrOrUnd(locale); ++i; } } // We need an unordered map from LSR to first supported locale with that LSR, - // and an ordered list of (LSR, supported index). - // We use a LinkedHashMap for both, - // and insert the supported locales in the following order: + // and an ordered list of (LSR, supported index) for + // the supported locales in the following order: // 1. Default locale, if it is supported. // 2. Priority locales (aka "paradigm locales") in builder order. // 3. Remaining locales in builder order. - supportedLsrToIndex = new LinkedHashMap<>(supportedLocalesLength); - // Note: We could work with a single LinkedHashMap by storing ~i (the binary-not index) - // for the default and paradigm locales, counting the number of those locales, - // and keeping two indexes to fill the LSR and index arrays with - // priority vs. normal locales. In that loop we would need to entry.setValue(~i) - // to restore non-negative indexes in the map. - // Probably saves little but less readable. - Map<LSR, Integer> otherLsrToIndex = null; - if (idef >= 0) { - supportedLsrToIndex.put(defLSR, idef); - } + supportedLsrToIndex = new HashMap<>(supportedLocalesLength); + supportedLSRs = new LSR[supportedLocalesLength]; + supportedIndexes = new int[supportedLocalesLength]; + int suppLength = 0; + // Determine insertion order. + // Add locales immediately that are equivalent to the default. + byte[] order = new byte[supportedLocalesLength]; + int numParadigms = 0; i = 0; for (ULocale locale : supportedULocales) { - if (i == idef) { - ++i; - continue; - } LSR lsr = lsrs[i]; if (defLSR == null) { assert i == 0; udef = locale; def = supportedLocales[0]; defLSR = lsr; - idef = 0; - supportedLsrToIndex.put(lsr, 0); - } else if (idef >= 0 && lsr.equals(defLSR)) { - // lsr.equals(defLSR) means that this supported locale is - // a duplicate of the default locale. - // Either an explicit default locale is supported, and we added it before the loop, - // or there is no explicit default locale, and this is - // a duplicate of the first supported locale. - // In both cases, idef >= 0 now, so otherwise we can skip the comparison. - // For a duplicate, putIfAbsent() is a no-op, so nothing to do. + suppLength = putIfAbsent(lsr, 0, suppLength); + } else if (lsr.isEquivalentTo(defLSR)) { + suppLength = putIfAbsent(lsr, i, suppLength); } else if (LocaleDistance.INSTANCE.isParadigmLSR(lsr)) { - putIfAbsent(supportedLsrToIndex, lsr, i); + order[i] = 2; + ++numParadigms; } else { - if (otherLsrToIndex == null) { - otherLsrToIndex = new LinkedHashMap<>(supportedLocalesLength); - } - putIfAbsent(otherLsrToIndex, lsr, i); + order[i] = 3; } ++i; } - if (otherLsrToIndex != null) { - supportedLsrToIndex.putAll(otherLsrToIndex); + // Add supported paradigm locales. + int paradigmLimit = suppLength + numParadigms; + for (i = 0; i < supportedLocalesLength && suppLength < paradigmLimit; ++i) { + if (order[i] == 2) { + suppLength = putIfAbsent(lsrs[i], i, suppLength); + } } - int supportedLSRsLength = supportedLsrToIndex.size(); - supportedLSRs = new LSR[supportedLSRsLength]; - supportedIndexes = new int[supportedLSRsLength]; - i = 0; - for (Map.Entry<LSR, Integer> entry : supportedLsrToIndex.entrySet()) { - supportedLSRs[i] = entry.getKey(); // = lsrs[entry.getValue()] - supportedIndexes[i++] = entry.getValue(); + // Add remaining supported locales. + for (i = 0; i < supportedLocalesLength; ++i) { + if (order[i] == 3) { + suppLength = putIfAbsent(lsrs[i], i, suppLength); + } } + supportedLSRsLength = suppLength; + // If supportedLSRsLength < supportedLocalesLength then + // we waste as many array slots as there are duplicate supported LSRs, + // but the amount of wasted space is small as long as there are few duplicates. defaultULocale = udef; defaultLocale = def; - defaultLocaleIndex = idef; demotionPerDesiredLocale = builder.demotion == Demotion.NONE ? 0 : LocaleDistance.INSTANCE.getDefaultDemotionPerDesiredLocale(); // null or REGION favorSubtag = builder.favor; + direction = builder.direction; + if (TRACE_MATCHER) { + System.err.printf("new LocaleMatcher: %s\n", toString()); + } } - private static final void putIfAbsent(Map<LSR, Integer> lsrToIndex, LSR lsr, int i) { - Integer index = lsrToIndex.get(lsr); - if (index == null) { - lsrToIndex.put(lsr, i); + private final int putIfAbsent(LSR lsr, int i, int suppLength) { + if (!supportedLsrToIndex.containsKey(lsr)) { + supportedLsrToIndex.put(lsr, i); + supportedLSRs[suppLength] = lsr; + supportedIndexes[suppLength++] = i; } + return suppLength; } private static final LSR getMaximalLsrOrUnd(ULocale locale) { @@ -806,7 +842,7 @@ public final class LocaleMatcher { } private Result defaultResult() { - return new Result(null, defaultULocale, null, defaultLocale, -1, defaultLocaleIndex); + return new Result(null, defaultULocale, null, defaultLocale, -1, -1); } private Result makeResult(ULocale desiredLocale, ULocaleLsrIterator lsrIter, int suppIndex) { @@ -904,26 +940,35 @@ public final class LocaleMatcher { private int getBestSuppIndex(LSR desiredLSR, LsrIterator remainingIter) { int desiredIndex = 0; int bestSupportedLsrIndex = -1; - for (int bestDistance = thresholdDistance;;) { + StringBuilder sb = null; + if (TRACE_MATCHER) { + sb = new StringBuilder("LocaleMatcher desired:"); + } + for (int bestShiftedDistance = LocaleDistance.shiftDistance(thresholdDistance);;) { + if (TRACE_MATCHER) { + sb.append(' ').append(desiredLSR); + } // Quick check for exact maximized LSR. Integer index = supportedLsrToIndex.get(desiredLSR); if (index != null) { int suppIndex = index; if (TRACE_MATCHER) { - System.err.printf("Returning %s: desiredLSR=supportedLSR\n", - supportedULocales[suppIndex]); + System.err.printf("%s --> best=%s: desiredLSR=supportedLSR\n", + sb, supportedULocales[suppIndex]); } if (remainingIter != null) { remainingIter.rememberCurrent(desiredIndex); } return suppIndex; } int bestIndexAndDistance = LocaleDistance.INSTANCE.getBestIndexAndDistance( - desiredLSR, supportedLSRs, bestDistance, favorSubtag); + desiredLSR, supportedLSRs, supportedLSRsLength, + bestShiftedDistance, favorSubtag, direction); if (bestIndexAndDistance >= 0) { - bestDistance = bestIndexAndDistance & 0xff; + bestShiftedDistance = LocaleDistance.getShiftedDistance(bestIndexAndDistance); if (remainingIter != null) { remainingIter.rememberCurrent(desiredIndex); } - bestSupportedLsrIndex = bestIndexAndDistance >> 8; + bestSupportedLsrIndex = LocaleDistance.getIndex(bestIndexAndDistance); } - if ((bestDistance -= demotionPerDesiredLocale) <= 0) { + if ((bestShiftedDistance -= LocaleDistance.shiftDistance(demotionPerDesiredLocale)) + <= 0) { break; } if (remainingIter == null || !remainingIter.hasNext()) { @@ -934,14 +979,14 @@ public final class LocaleMatcher { } if (bestSupportedLsrIndex < 0) { if (TRACE_MATCHER) { - System.err.printf("Returning default %s: no good match\n", defaultULocale); + System.err.printf("%s --> best=default %s: no good match\n", sb, defaultULocale); } return -1; } int suppIndex = supportedIndexes[bestSupportedLsrIndex]; if (TRACE_MATCHER) { - System.err.printf("Returning %s: best matching supported locale\n", - supportedULocales[suppIndex]); + System.err.printf("%s --> best=%s: best matching supported locale\n", + sb, supportedULocales[suppIndex]); } return suppIndex; } @@ -966,11 +1011,16 @@ public final class LocaleMatcher { @Deprecated public double match(ULocale desired, ULocale desiredMax, ULocale supported, ULocale supportedMax) { // Returns the inverse of the distance: That is, 1-distance(desired, supported). - int distance = LocaleDistance.INSTANCE.getBestIndexAndDistance( + int indexAndDistance = LocaleDistance.INSTANCE.getBestIndexAndDistance( getMaximalLsrOrUnd(desired), - new LSR[] { getMaximalLsrOrUnd(supported) }, - thresholdDistance, favorSubtag) & 0xff; - return (100 - distance) / 100.0; + new LSR[] { getMaximalLsrOrUnd(supported) }, 1, + LocaleDistance.shiftDistance(thresholdDistance), favorSubtag, direction); + double distance = LocaleDistance.getDistanceDouble(indexAndDistance); + if (TRACE_MATCHER) { + System.err.printf("LocaleMatcher distance(desired=%s, supported=%s)=%g\n", + String.valueOf(desired), String.valueOf(supported), distance); + } + return (100.0 - distance) / 100.0; } /** @@ -996,16 +1046,20 @@ public final class LocaleMatcher { @Override public String toString() { StringBuilder s = new StringBuilder().append("{LocaleMatcher"); - if (supportedULocales.length > 0) { - s.append(" supported={").append(supportedULocales[0].toString()); - for (int i = 1; i < supportedULocales.length; ++i) { - s.append(", ").append(supportedULocales[i].toString()); + // Supported languages in the order that we try to match them. + if (supportedLSRsLength > 0) { + s.append(" supportedLSRs={").append(supportedLSRs[0]); + for (int i = 1; i < supportedLSRsLength; ++i) { + s.append(", ").append(supportedLSRs[i]); } s.append('}'); } - s.append(" default=").append(Objects.toString(defaultULocale)); + s.append(" default=").append(defaultULocale); if (favorSubtag != null) { - s.append(" distance=").append(favorSubtag.toString()); + s.append(" favor=").append(favorSubtag); + } + if (direction != null) { + s.append(" direction=").append(direction); } if (thresholdDistance >= 0) { s.append(String.format(" threshold=%d", thresholdDistance)); diff --git a/android_icu4j/src/main/java/android/icu/util/MeasureUnit.java b/android_icu4j/src/main/java/android/icu/util/MeasureUnit.java index b6bfe87b1..b2815df1f 100644 --- a/android_icu4j/src/main/java/android/icu/util/MeasureUnit.java +++ b/android_icu4j/src/main/java/android/icu/util/MeasureUnit.java @@ -194,6 +194,47 @@ public class MeasureUnit implements Serializable { return MeasureUnit.addUnit(type, subType, factory); } + private static MeasureUnit findBySubType(String subType) { + populateCache(); + for (Map<String, MeasureUnit> unitsForType : cache.values()) { + if (unitsForType.containsKey(subType)) { + return unitsForType.get(subType); + } + } + return null; + } + + /** + * For ICU use only. + * @deprecated This API is ICU internal only. + * @hide draft / provisional / internal are hidden on Android + */ + @Deprecated + public static MeasureUnit[] parseCoreUnitIdentifier(String coreUnitIdentifier) { + // First search for the whole code unit identifier as a subType + MeasureUnit whole = findBySubType(coreUnitIdentifier); + if (whole != null) { + return new MeasureUnit[] { whole }; // found a numerator but not denominator + } + + // If not found, try breaking apart numerator and denominator + int perIdx = coreUnitIdentifier.indexOf("-per-"); + if (perIdx == -1) { + // String does not contain "-per-" + return null; + } + String numeratorStr = coreUnitIdentifier.substring(0, perIdx); + String denominatorStr = coreUnitIdentifier.substring(perIdx + 5); + MeasureUnit numerator = findBySubType(numeratorStr); + MeasureUnit denominator = findBySubType(denominatorStr); + if (numerator != null && denominator != null) { + return new MeasureUnit[] { numerator, denominator }; // found both a numerator and denominator + } + + // The numerator or denominator were invalid + return null; + } + /** * For ICU use only. * @deprecated This API is ICU internal only. @@ -372,9 +413,9 @@ public class MeasureUnit implements Serializable { public static final MeasureUnit G_FORCE = MeasureUnit.internalGetInstance("acceleration", "g-force"); /** - * Constant for unit of acceleration: meter-per-second-squared + * Constant for unit of acceleration: meter-per-square-second */ - public static final MeasureUnit METER_PER_SECOND_SQUARED = MeasureUnit.internalGetInstance("acceleration", "meter-per-second-squared"); + public static final MeasureUnit METER_PER_SECOND_SQUARED = MeasureUnit.internalGetInstance("acceleration", "meter-per-square-second"); /** * Constant for unit of angle: arc-minute @@ -408,7 +449,7 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of area: dunam - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit DUNAM = MeasureUnit.internalGetInstance("area", "dunam"); @@ -469,14 +510,14 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of concentr: mole - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit MOLE = MeasureUnit.internalGetInstance("concentr", "mole"); /** - * Constant for unit of concentr: part-per-million + * Constant for unit of concentr: permillion */ - public static final MeasureUnit PART_PER_MILLION = MeasureUnit.internalGetInstance("concentr", "part-per-million"); + public static final MeasureUnit PART_PER_MILLION = MeasureUnit.internalGetInstance("concentr", "permillion"); /** * Constant for unit of concentr: percent @@ -490,14 +531,14 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of concentr: permyriad - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit PERMYRIAD = MeasureUnit.internalGetInstance("concentr", "permyriad"); /** - * Constant for unit of consumption: liter-per-100kilometers + * Constant for unit of consumption: liter-per-100-kilometer */ - public static final MeasureUnit LITER_PER_100KILOMETERS = MeasureUnit.internalGetInstance("consumption", "liter-per-100kilometers"); + public static final MeasureUnit LITER_PER_100KILOMETERS = MeasureUnit.internalGetInstance("consumption", "liter-per-100-kilometer"); /** * Constant for unit of consumption: liter-per-kilometer @@ -581,7 +622,7 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of duration: day-person - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit DAY_PERSON = MeasureUnit.internalGetInstance("duration", "day-person"); @@ -618,7 +659,7 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of duration: month-person - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit MONTH_PERSON = MeasureUnit.internalGetInstance("duration", "month-person"); @@ -639,7 +680,7 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of duration: week-person - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit WEEK_PERSON = MeasureUnit.internalGetInstance("duration", "week-person"); @@ -650,7 +691,7 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of duration: year-person - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit YEAR_PERSON = MeasureUnit.internalGetInstance("duration", "year-person"); @@ -676,7 +717,7 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of energy: british-thermal-unit - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit BRITISH_THERMAL_UNIT = MeasureUnit.internalGetInstance("energy", "british-thermal-unit"); @@ -687,7 +728,7 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of energy: electronvolt - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit ELECTRONVOLT = MeasureUnit.internalGetInstance("energy", "electronvolt"); @@ -724,13 +765,13 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of force: newton - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit NEWTON = MeasureUnit.internalGetInstance("force", "newton"); /** * Constant for unit of force: pound-force - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit POUND_FORCE = MeasureUnit.internalGetInstance("force", "pound-force"); @@ -893,7 +934,7 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of length: solar-radius - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit SOLAR_RADIUS = MeasureUnit.internalGetInstance("length", "solar-radius"); @@ -909,7 +950,7 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of light: solar-luminosity - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit SOLAR_LUMINOSITY = MeasureUnit.internalGetInstance("light", "solar-luminosity"); @@ -920,13 +961,13 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of mass: dalton - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit DALTON = MeasureUnit.internalGetInstance("mass", "dalton"); /** * Constant for unit of mass: earth-mass - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit EARTH_MASS = MeasureUnit.internalGetInstance("mass", "earth-mass"); @@ -972,7 +1013,7 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of mass: solar-mass - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit SOLAR_MASS = MeasureUnit.internalGetInstance("mass", "solar-mass"); @@ -1033,19 +1074,19 @@ public class MeasureUnit implements Serializable { public static final MeasureUnit HECTOPASCAL = MeasureUnit.internalGetInstance("pressure", "hectopascal"); /** - * Constant for unit of pressure: inch-hg + * Constant for unit of pressure: inch-ofhg */ - public static final MeasureUnit INCH_HG = MeasureUnit.internalGetInstance("pressure", "inch-hg"); + public static final MeasureUnit INCH_HG = MeasureUnit.internalGetInstance("pressure", "inch-ofhg"); /** * Constant for unit of pressure: kilopascal - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit KILOPASCAL = MeasureUnit.internalGetInstance("pressure", "kilopascal"); /** * Constant for unit of pressure: megapascal - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit MEGAPASCAL = MeasureUnit.internalGetInstance("pressure", "megapascal"); @@ -1055,9 +1096,9 @@ public class MeasureUnit implements Serializable { public static final MeasureUnit MILLIBAR = MeasureUnit.internalGetInstance("pressure", "millibar"); /** - * Constant for unit of pressure: millimeter-of-mercury + * Constant for unit of pressure: millimeter-ofhg */ - public static final MeasureUnit MILLIMETER_OF_MERCURY = MeasureUnit.internalGetInstance("pressure", "millimeter-of-mercury"); + public static final MeasureUnit MILLIMETER_OF_MERCURY = MeasureUnit.internalGetInstance("pressure", "millimeter-ofhg"); /** * Constant for unit of pressure: pascal @@ -1066,9 +1107,9 @@ public class MeasureUnit implements Serializable { public static final MeasureUnit PASCAL = MeasureUnit.internalGetInstance("pressure", "pascal"); /** - * Constant for unit of pressure: pound-per-square-inch + * Constant for unit of pressure: pound-force-per-square-inch */ - public static final MeasureUnit POUND_PER_SQUARE_INCH = MeasureUnit.internalGetInstance("pressure", "pound-per-square-inch"); + public static final MeasureUnit POUND_PER_SQUARE_INCH = MeasureUnit.internalGetInstance("pressure", "pound-force-per-square-inch"); /** * Constant for unit of speed: kilometer-per-hour @@ -1112,15 +1153,15 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of torque: newton-meter - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit NEWTON_METER = MeasureUnit.internalGetInstance("torque", "newton-meter"); /** - * Constant for unit of torque: pound-foot - * @hide draft / provisional / internal are hidden on Android + * Constant for unit of torque: pound-force-foot + * @hide Hide new API in Android temporarily */ - public static final MeasureUnit POUND_FOOT = MeasureUnit.internalGetInstance("torque", "pound-foot"); + public static final MeasureUnit POUND_FOOT = MeasureUnit.internalGetInstance("torque", "pound-force-foot"); /** * Constant for unit of volume: acre-foot @@ -1129,7 +1170,7 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of volume: barrel - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit BARREL = MeasureUnit.internalGetInstance("volume", "barrel"); @@ -1200,7 +1241,7 @@ public class MeasureUnit implements Serializable { /** * Constant for unit of volume: fluid-ounce-imperial - * @hide draft / provisional / internal are hidden on Android + * @hide Hide new API in Android temporarily */ public static final MeasureUnit FLUID_OUNCE_IMPERIAL = MeasureUnit.internalGetInstance("volume", "fluid-ounce-imperial"); diff --git a/android_icu4j/src/main/java/android/icu/util/ULocale.java b/android_icu4j/src/main/java/android/icu/util/ULocale.java index aef4496b3..d81f4c8f2 100644 --- a/android_icu4j/src/main/java/android/icu/util/ULocale.java +++ b/android_icu4j/src/main/java/android/icu/util/ULocale.java @@ -13,7 +13,6 @@ package android.icu.util; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -35,7 +34,6 @@ import android.icu.impl.ICUResourceBundle; import android.icu.impl.ICUResourceTableAccess; import android.icu.impl.LocaleIDParser; import android.icu.impl.LocaleIDs; -import android.icu.impl.LocaleUtility; import android.icu.impl.SoftCache; import android.icu.impl.locale.AsciiUtil; import android.icu.impl.locale.BaseLocale; @@ -348,23 +346,24 @@ public final class ULocale implements Serializable, Comparable<ULocale> { * canonicalized id. */ private static String[][] CANONICALIZE_MAP = { - { "art_LOJBAN", "jbo" }, /* registered name */ - { "cel_GAULISH", "cel__GAULISH" }, /* registered name */ - { "de_1901", "de__1901" }, /* registered name */ - { "de_1906", "de__1906" }, /* registered name */ - { "en_BOONT", "en__BOONT" }, /* registered name */ - { "en_SCOUSE", "en__SCOUSE" }, /* registered name */ + { "art__LOJBAN", "jbo" }, /* registered name */ + { "cel__GAULISH", "cel__GAULISH" }, /* registered name */ + { "de__1901", "de__1901" }, /* registered name */ + { "de__1906", "de__1906" }, /* registered name */ + { "en__BOONT", "en__BOONT" }, /* registered name */ + { "en__SCOUSE", "en__SCOUSE" }, /* registered name */ { "hy__AREVELA", "hy", null, null }, /* Registered IANA variant */ { "hy__AREVMDA", "hyw", null, null }, /* Registered IANA variant */ - { "sl_ROZAJ", "sl__ROZAJ" }, /* registered name */ - { "zh_GAN", "zh__GAN" }, /* registered name */ - { "zh_GUOYU", "zh" }, /* registered name */ - { "zh_HAKKA", "zh__HAKKA" }, /* registered name */ + { "sl__ROZAJ", "sl__ROZAJ" }, /* registered name */ + { "zh__GUOYU", "zh" }, /* registered name */ + { "zh__HAKKA", "hak" }, /* registered name */ + { "zh__XIANG", "hsn" }, /* registered name */ + // Three letter subtags won't be treated as variants. + { "zh_GAN", "gan" }, /* registered name */ { "zh_MIN", "zh__MIN" }, /* registered name */ - { "zh_MIN_NAN", "zh__MINNAN" }, /* registered name */ - { "zh_WUU", "zh__WUU" }, /* registered name */ - { "zh_XIANG", "zh__XIANG" }, /* registered name */ - { "zh_YUE", "zh__YUE" } /* registered name */ + { "zh_MIN_NAN", "nan" }, /* registered name */ + { "zh_WUU", "wuu" }, /* registered name */ + { "zh_YUE", "yue" } /* registered name */ }; /** @@ -376,15 +375,6 @@ public final class ULocale implements Serializable, Comparable<ULocale> { } /** - * Construct a ULocale object from a {@link java.util.Locale}. - * @param loc a {@link java.util.Locale} - */ - private ULocale(Locale loc) { - this.localeID = getName(forLocale(loc).toString()); - this.locale = loc; - } - - /** * <strong>[icu]</strong> Returns a ULocale object for a {@link java.util.Locale}. * The ULocale is canonicalized. * @param loc a {@link java.util.Locale} @@ -452,7 +442,7 @@ public final class ULocale implements Serializable, Comparable<ULocale> { } /** - * <strong>[icu]</strong> Creates a ULocale from the id by first canonicalizing the id. + * <strong>[icu]</strong> Creates a ULocale from the id by first canonicalizing the id according to CLDR. * @param nonCanonicalID the locale id to canonicalize * @return the locale created from the canonical version of the ID. */ @@ -460,6 +450,16 @@ public final class ULocale implements Serializable, Comparable<ULocale> { return new ULocale(canonicalize(nonCanonicalID), (Locale)null); } + /** + * Creates a ULocale from the locale by first canonicalizing the locale according to CLDR. + * @param locale the ULocale to canonicalize + * @return the ULocale created from the canonical version of the ULocale. + * @hide draft / provisional / internal are hidden on Android + */ + public static ULocale createCanonical(ULocale locale) { + return createCanonical(locale.getName()); + } + private static String lscvToID(String lang, String script, String country, String variant) { StringBuilder buf = new StringBuilder(); @@ -1124,8 +1124,8 @@ public final class ULocale implements Serializable, Comparable<ULocale> { } /** - * <strong>[icu]</strong> Returns the canonical name for the specified locale ID. This is used to - * convert POSIX and other grandfathered IDs to standard ICU form. + * <strong>[icu]</strong> Returns the canonical name according to CLDR for the specified locale ID. + * This is used to convert POSIX and other grandfathered IDs to standard ICU form. * @param localeID the locale id * @return the canonicalized id */ @@ -1158,6 +1158,144 @@ public final class ULocale implements Serializable, Comparable<ULocale> { } } + // If the BCP 47 primary language subtag matches the type attribute of a languageAlias + // element in Supplemental Data, replace the language subtag with the replacement value. + // If there are additional subtags in the replacement value, add them to the result, but + // only if there is no corresponding subtag already in the tag. + // Five special deprecated grandfathered codes (such as i-default) are in type attributes, and are also replaced. + try { + UResourceBundle languageAlias = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, + "metadata", ICUResourceBundle.ICU_DATA_CLASS_LOADER) + .get("alias") + .get("language"); + // language _ variant + if (!parser.getVariant().isEmpty()) { + String [] variants = parser.getVariant().split("_"); + for (String variant : variants) { + try { + // Note the key in the metadata.txt is formatted as language_variant + // instead of language__variant but lscvToID will generate + // language__variant so we have to build the string ourselves. + ULocale replaceLocale = new ULocale(languageAlias.get( + (new StringBuilder(parser.getLanguage().length() + 1 + parser.getVariant().length())) + .append(parser.getLanguage()) + .append("_") + .append(variant) + .toString()) + .get("replacement") + .getString()); + StringBuilder replacedVariant = new StringBuilder(parser.getVariant().length()); + for (String current : variants) { + if (current.equals(variant)) continue; + if (replacedVariant.length() > 0) replacedVariant.append("_"); + replacedVariant.append(current); + } + parser = new LocaleIDParser( + (new StringBuilder(localeID.length())) + .append(lscvToID(replaceLocale.getLanguage(), + !parser.getScript().isEmpty() ? parser.getScript() : replaceLocale.getScript(), + !parser.getCountry().isEmpty() ? parser.getCountry() : replaceLocale.getCountry(), + replacedVariant.toString())) + .append(parser.getName().substring(parser.getBaseName().length())) + .toString()); + } catch (MissingResourceException e) { + } + } + } + + // language _ script _ country + // ug_Arab_CN -> ug_CN + if (!parser.getScript().isEmpty() && !parser.getCountry().isEmpty()) { + try { + ULocale replaceLocale = new ULocale(languageAlias.get( + lscvToID(parser.getLanguage(), parser.getScript(), parser.getCountry(), null)) + .get("replacement") + .getString()); + parser = new LocaleIDParser((new StringBuilder(localeID.length())) + .append(lscvToID(replaceLocale.getLanguage(), + replaceLocale.getScript(), + replaceLocale.getCountry(), + parser.getVariant())) + .append(parser.getName().substring(parser.getBaseName().length())) + .toString()); + } catch (MissingResourceException e) { + } + } + // language _ country + // eg. az_AZ -> az_Latn_AZ + if (!parser.getCountry().isEmpty()) { + try { + ULocale replaceLocale = new ULocale(languageAlias.get( + lscvToID(parser.getLanguage(), null, parser.getCountry(), null)) + .get("replacement") + .getString()); + parser = new LocaleIDParser((new StringBuilder(localeID.length())) + .append(lscvToID(replaceLocale.getLanguage(), + parser.getScript().isEmpty() ? replaceLocale.getScript() : parser.getScript(), + replaceLocale.getCountry(), + parser.getVariant())) + .append(parser.getName().substring(parser.getBaseName().length())) + .toString()); + } catch (MissingResourceException e) { + } + } + // only language + // e.g. twi -> ak + try { + ULocale replaceLocale = new ULocale(languageAlias.get(parser.getLanguage()) + .get("replacement") + .getString()); + parser = new LocaleIDParser((new StringBuilder(localeID.length())) + .append(lscvToID(replaceLocale.getLanguage(), + parser.getScript().isEmpty() ? replaceLocale.getScript() : parser.getScript() , + parser.getCountry().isEmpty() ? replaceLocale.getCountry() : parser.getCountry() , + parser.getVariant())) + .append(parser.getName().substring(parser.getBaseName().length())) + .toString()); + } catch (MissingResourceException e) { + } + } catch (MissingResourceException e) { + } + + // If the BCP 47 region subtag matches the type attribute of a + // territoryAlias element in Supplemental Data, replace the language + // subtag with the replacement value, as follows: + if (!parser.getCountry().isEmpty()) { + try { + String replacements[] = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, + "metadata", ICUResourceBundle.ICU_DATA_CLASS_LOADER) + .get("alias") + .get("territory") + .get(parser.getCountry()) + .get("replacement") + .getString() + .split(" "); + String replacement = replacements[0]; + // If there is a single territory in the replacement, use it. + // If there are multiple territories: + // Look up the most likely territory for the base language code (and script, if there is one). + // If that likely territory is in the list, use it. + // Otherwise, use the first territory in the list. + if (replacements.length > 1) { + String likelyCountry = ULocale.addLikelySubtags( + new ULocale(lscvToID(parser.getLanguage(), parser.getScript(), null, parser.getVariant()))) + .getCountry(); + for (String country : replacements) { + if (country.equals(likelyCountry)) { + replacement = likelyCountry; + break; + } + } + } + parser = new LocaleIDParser( + (new StringBuilder(localeID.length())) + .append(lscvToID(parser.getLanguage(), parser.getScript(), replacement, parser.getVariant())) + .append(parser.getName().substring(parser.getBaseName().length())) + .toString()); + } catch (MissingResourceException e) { + } + } + return parser.getName(); } @@ -1859,27 +1997,41 @@ public final class ULocale implements Serializable, Comparable<ULocale> { * ROOT ULocale if if a ROOT locale was used as a fallback (because nothing else in * availableLocales matched). No ULocale array element should be null; behavior is * undefined if this is the case. + * + * <p>This is a thin wrapper over {@link LocalePriorityList} + {@link LocaleMatcher}. + * * @param acceptLanguageList list in HTTP "Accept-Language:" format of acceptable locales * @param availableLocales list of available locales. One of these will be returned. * @param fallback if non-null, a 1-element array containing a boolean to be set with * the fallback status * @return one of the locales from the availableLocales list, or null if none match + * @see LocaleMatcher + * @see LocalePriorityList */ public static ULocale acceptLanguage(String acceptLanguageList, ULocale[] availableLocales, boolean[] fallback) { - if (acceptLanguageList == null) { - throw new NullPointerException(); + if (fallback != null) { + fallback[0] = true; } - ULocale acceptList[] = null; + LocalePriorityList desired; try { - acceptList = parseAcceptLanguage(acceptLanguageList, true); - } catch (ParseException pe) { - acceptList = null; - } - if (acceptList == null) { + desired = LocalePriorityList.add(acceptLanguageList).build(); + } catch (IllegalArgumentException e) { return null; } - return acceptLanguage(acceptList, availableLocales, fallback); + LocaleMatcher.Builder builder = LocaleMatcher.builder(); + for (ULocale locale : availableLocales) { + builder.addSupportedULocale(locale); + } + LocaleMatcher matcher = builder.build(); + LocaleMatcher.Result result = matcher.getBestMatchResult(desired); + if (result.getDesiredIndex() >= 0) { + if (fallback != null && result.getDesiredULocale().equals(result.getSupportedULocale())) { + fallback[0] = false; + } + return result.getSupportedULocale(); + } + return null; } /** @@ -1890,56 +2042,38 @@ public final class ULocale implements Serializable, Comparable<ULocale> { * will be one of the locales in availableLocales, or the ROOT ULocale if if a ROOT * locale was used as a fallback (because nothing else in availableLocales matched). * No ULocale array element should be null; behavior is undefined if this is the case. + * + * <p>This is a thin wrapper over {@link LocaleMatcher}. + * * @param acceptLanguageList list of acceptable locales * @param availableLocales list of available locales. One of these will be returned. * @param fallback if non-null, a 1-element array containing a boolean to be set with * the fallback status * @return one of the locales from the availableLocales list, or null if none match + * @see LocaleMatcher */ - public static ULocale acceptLanguage(ULocale[] acceptLanguageList, ULocale[] - availableLocales, boolean[] fallback) { - // fallbacklist - int i,j; - if(fallback != null) { - fallback[0]=true; + public static ULocale acceptLanguage(ULocale[] acceptLanguageList, ULocale[] availableLocales, + boolean[] fallback) { + if (fallback != null) { + fallback[0] = true; } - for(i=0;i<acceptLanguageList.length;i++) { - ULocale aLocale = acceptLanguageList[i]; - boolean[] setFallback = fallback; - do { - for(j=0;j<availableLocales.length;j++) { - if(availableLocales[j].equals(aLocale)) { - if(setFallback != null) { - setFallback[0]=false; // first time with this locale - not a fallback. - } - return availableLocales[j]; - } - // compare to scriptless alias, so locales such as - // zh_TW, zh_CN are considered as available locales - see #7190 - if (aLocale.getScript().length() == 0 - && availableLocales[j].getScript().length() > 0 - && availableLocales[j].getLanguage().equals(aLocale.getLanguage()) - && availableLocales[j].getCountry().equals(aLocale.getCountry()) - && availableLocales[j].getVariant().equals(aLocale.getVariant())) { - ULocale minAvail = ULocale.minimizeSubtags(availableLocales[j]); - if (minAvail.getScript().length() == 0) { - if(setFallback != null) { - setFallback[0] = false; // not a fallback. - } - return aLocale; - } - } - } - Locale loc = aLocale.toLocale(); - Locale parent = LocaleUtility.fallback(loc); - if(parent != null) { - aLocale = new ULocale(parent); - } else { - aLocale = null; - } - setFallback = null; // Do not set fallback in later iterations - } while (aLocale != null); + LocaleMatcher.Builder builder = LocaleMatcher.builder(); + for (ULocale locale : availableLocales) { + builder.addSupportedULocale(locale); + } + LocaleMatcher matcher = builder.build(); + LocaleMatcher.Result result; + if (acceptLanguageList.length == 1) { + result = matcher.getBestMatchResult(acceptLanguageList[0]); + } else { + result = matcher.getBestMatchResult(Arrays.asList(acceptLanguageList)); + } + if (result.getDesiredIndex() >= 0) { + if (fallback != null && result.getDesiredULocale().equals(result.getSupportedULocale())) { + fallback[0] = false; + } + return result.getSupportedULocale(); } return null; } @@ -1954,11 +2088,16 @@ public final class ULocale implements Serializable, Comparable<ULocale> { * availableLocales matched). No ULocale array element should be null; behavior is * undefined if this is the case. This function will choose a locale from the * ULocale.getAvailableLocales() list as available. + * + * <p>This is a thin wrapper over {@link LocalePriorityList} + {@link LocaleMatcher}. + * * @param acceptLanguageList list in HTTP "Accept-Language:" format of acceptable locales * @param fallback if non-null, a 1-element array containing a boolean to be set with * the fallback status * @return one of the locales from the ULocale.getAvailableLocales() list, or null if * none match + * @see LocaleMatcher + * @see LocalePriorityList */ public static ULocale acceptLanguage(String acceptLanguageList, boolean[] fallback) { return acceptLanguage(acceptLanguageList, ULocale.getAvailableLocales(), @@ -1975,274 +2114,20 @@ public final class ULocale implements Serializable, Comparable<ULocale> { * availableLocales matched). No ULocale array element should be null; behavior is * undefined if this is the case. This function will choose a locale from the * ULocale.getAvailableLocales() list as available. + * + * <p>This is a thin wrapper over {@link LocaleMatcher}. + * * @param acceptLanguageList ordered array of acceptable locales (preferred are listed first) * @param fallback if non-null, a 1-element array containing a boolean to be set with * the fallback status * @return one of the locales from the ULocale.getAvailableLocales() list, or null if none match + * @see LocaleMatcher */ public static ULocale acceptLanguage(ULocale[] acceptLanguageList, boolean[] fallback) { return acceptLanguage(acceptLanguageList, ULocale.getAvailableLocales(), fallback); } - /** - * Package local method used for parsing Accept-Language string - */ - static ULocale[] parseAcceptLanguage(String acceptLanguage, boolean isLenient) - throws ParseException { - class ULocaleAcceptLanguageQ implements Comparable<ULocaleAcceptLanguageQ> { - private double q; - private double serial; - public ULocaleAcceptLanguageQ(double theq, int theserial) { - q = theq; - serial = theserial; - } - @Override - public int compareTo(ULocaleAcceptLanguageQ other) { - if (q > other.q) { // reverse - to sort in descending order - return -1; - } else if (q < other.q) { - return 1; - } - if (serial < other.serial) { - return -1; - } else if (serial > other.serial) { - return 1; - } else { - return 0; // same object - } - } - } - - // parse out the acceptLanguage into an array - TreeMap<ULocaleAcceptLanguageQ, ULocale> map = - new TreeMap<>(); - StringBuilder languageRangeBuf = new StringBuilder(); - StringBuilder qvalBuf = new StringBuilder(); - int state = 0; - acceptLanguage += ","; // append comma to simplify the parsing code - int n; - boolean subTag = false; - boolean q1 = false; - for (n = 0; n < acceptLanguage.length(); n++) { - boolean gotLanguageQ = false; - char c = acceptLanguage.charAt(n); - switch (state) { - case 0: // before language-range start - if (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) { - // in language-range - languageRangeBuf.append(c); - state = 1; - subTag = false; - } else if (c == '*') { - languageRangeBuf.append(c); - state = 2; - } else if (c != ' ' && c != '\t') { - // invalid character - state = -1; - } - break; - case 1: // in language-range - if (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) { - languageRangeBuf.append(c); - } else if (c == '-') { - subTag = true; - languageRangeBuf.append(c); - } else if (c == '_') { - if (isLenient) { - subTag = true; - languageRangeBuf.append(c); - } else { - state = -1; - } - } else if ('0' <= c && c <= '9') { - if (subTag) { - languageRangeBuf.append(c); - } else { - // DIGIT is allowed only in language sub tag - state = -1; - } - } else if (c == ',') { - // language-q end - gotLanguageQ = true; - } else if (c == ' ' || c == '\t') { - // language-range end - state = 3; - } else if (c == ';') { - // before q - state = 4; - } else { - // invalid character for language-range - state = -1; - } - break; - case 2: // saw wild card range - if (c == ',') { - // language-q end - gotLanguageQ = true; - } else if (c == ' ' || c == '\t') { - // language-range end - state = 3; - } else if (c == ';') { - // before q - state = 4; - } else { - // invalid - state = -1; - } - break; - case 3: // language-range end - if (c == ',') { - // language-q end - gotLanguageQ = true; - } else if (c == ';') { - // before q - state =4; - } else if (c != ' ' && c != '\t') { - // invalid - state = -1; - } - break; - case 4: // before q - if (c == 'q') { - // before equal - state = 5; - } else if (c != ' ' && c != '\t') { - // invalid - state = -1; - } - break; - case 5: // before equal - if (c == '=') { - // before q value - state = 6; - } else if (c != ' ' && c != '\t') { - // invalid - state = -1; - } - break; - case 6: // before q value - if (c == '0') { - // q value start with 0 - q1 = false; - qvalBuf.append(c); - state = 7; - } else if (c == '1') { - // q value start with 1 - qvalBuf.append(c); - state = 7; - } else if (c == '.') { - if (isLenient) { - qvalBuf.append(c); - state = 8; - } else { - state = -1; - } - } else if (c != ' ' && c != '\t') { - // invalid - state = -1; - } - break; - case 7: // q value start - if (c == '.') { - // before q value fraction part - qvalBuf.append(c); - state = 8; - } else if (c == ',') { - // language-q end - gotLanguageQ = true; - } else if (c == ' ' || c == '\t') { - // after q value - state = 10; - } else { - // invalid - state = -1; - } - break; - case 8: // before q value fraction part - if ('0' <= c && c <= '9') { - if (q1 && c != '0' && !isLenient) { - // if q value starts with 1, the fraction part must be 0 - state = -1; - } else { - // in q value fraction part - qvalBuf.append(c); - state = 9; - } - } else { - // invalid - state = -1; - } - break; - case 9: // in q value fraction part - if ('0' <= c && c <= '9') { - if (q1 && c != '0') { - // if q value starts with 1, the fraction part must be 0 - state = -1; - } else { - qvalBuf.append(c); - } - } else if (c == ',') { - // language-q end - gotLanguageQ = true; - } else if (c == ' ' || c == '\t') { - // after q value - state = 10; - } else { - // invalid - state = -1; - } - break; - case 10: // after q value - if (c == ',') { - // language-q end - gotLanguageQ = true; - } else if (c != ' ' && c != '\t') { - // invalid - state = -1; - } - break; - } - if (state == -1) { - // error state - throw new ParseException("Invalid Accept-Language", n); - } - if (gotLanguageQ) { - double q = 1.0; - if (qvalBuf.length() != 0) { - try { - q = Double.parseDouble(qvalBuf.toString()); - } catch (NumberFormatException nfe) { - // Already validated, so it should never happen - q = 1.0; - } - if (q > 1.0) { - q = 1.0; - } - } - if (languageRangeBuf.charAt(0) != '*') { - int serial = map.size(); - ULocaleAcceptLanguageQ entry = new ULocaleAcceptLanguageQ(q, serial); - // sort in reverse order.. 1.0, 0.9, 0.8 .. etc - map.put(entry, new ULocale(canonicalize(languageRangeBuf.toString()))); - } - - // reset buffer and parse state - languageRangeBuf.setLength(0); - qvalBuf.setLength(0); - state = 0; - } - } - if (state != 0) { - // Well, the parser should handle all cases. So just in case. - throw new ParseException("Invalid AcceptlLanguage", n); - } - - // pull out the map - ULocale acceptList[] = map.values().toArray(new ULocale[map.size()]); - return acceptList; - } - private static final String UNDEFINED_LANGUAGE = "und"; private static final String UNDEFINED_SCRIPT = "Zzzz"; private static final String UNDEFINED_REGION = "ZZ"; @@ -3107,7 +2992,10 @@ public final class ULocale implements Serializable, Comparable<ULocale> { } List<String>subtags = tag.getVariants(); - for (String s : subtags) { + // ICU-20478: Sort variants per UTS35. + ArrayList<String> variants = new ArrayList<>(subtags); + Collections.sort(variants); + for (String s : variants) { buf.append(LanguageTag.SEP); buf.append(LanguageTag.canonicalizeVariant(s)); } diff --git a/android_icu4j/src/main/java/android/icu/util/VersionInfo.java b/android_icu4j/src/main/java/android/icu/util/VersionInfo.java index 02fb446f0..730187048 100644 --- a/android_icu4j/src/main/java/android/icu/util/VersionInfo.java +++ b/android_icu4j/src/main/java/android/icu/util/VersionInfo.java @@ -180,7 +180,7 @@ public final class VersionInfo implements Comparable<VersionInfo> * @hide draft / provisional / internal are hidden on Android */ @Deprecated - public static final String ICU_DATA_VERSION_PATH = "66b"; + public static final String ICU_DATA_VERSION_PATH = "67b"; /** * Data version in ICU4J. @@ -551,7 +551,7 @@ public final class VersionInfo implements Comparable<VersionInfo> UNICODE_12_1 = getInstance(12, 1, 0, 0); UNICODE_13_0 = getInstance(13, 0, 0, 0); - ICU_VERSION = getInstance(66, 1, 0, 0); + ICU_VERSION = getInstance(67, 1, 0, 0); ICU_DATA_VERSION = ICU_VERSION; UNICODE_VERSION = UNICODE_13_0; diff --git a/android_icu4j/src/main/tests/android/icu/dev/data/collationtest.txt b/android_icu4j/src/main/tests/android/icu/dev/data/collationtest.txt index 366362db2..abda337e5 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/data/collationtest.txt +++ b/android_icu4j/src/main/tests/android/icu/dev/data/collationtest.txt @@ -1,7 +1,5 @@ # Copyright (C) 2016 and later: Unicode, Inc. and others. -# License & terms of use: http://www.unicode.org/copyright.html#License -# -# Corporation and others. All Rights Reserved. +# License & terms of use: http://www.unicode.org/copyright.html # Copyright (c) 2012-2015 International Business Machines # Corporation and others. All Rights Reserved. # @@ -2542,3 +2540,46 @@ # Before ICU 55, the following reordered together with Gothic. <1 𐌈 # Old Italic <1 𐑐 # Shavian + +# Check for presence of certain chars 乛冂刂卜又小彑艹日月爫牛辶 in +# zh pinyin and stroke, ICU-13790 +# (bracket pinyin test with 卬..作, stroke test with 一..乾) + +** test: DataDrivenCollationTest/VerifyCertainCharsInPinyin +@ locale zh-u-co-pinyin +* compare +< 卬 +< 卜 +< 艹 +< 辶 +< 刂 +< 彑 +< 冂 +< 牛 +< 日 +< 小 +< 乛 +< 又 +< 月 +< 爫 +< 作 + +** test: DataDrivenCollationTest/VerifyCertainCharsInStroke +@ locale zh-u-co-stroke +* compare +< 一 +< 乛 +< 冂 +< 刂 +< 卜 +< 又 +< 小 +< 彑 +< 艹 +< 日 +< 月 +< 爫 +< 牛 +< 辶 +< 乾 + diff --git a/android_icu4j/src/main/tests/android/icu/dev/data/numberpermutationtest.txt b/android_icu4j/src/main/tests/android/icu/dev/data/numberpermutationtest.txt index 25f7da8b7..24136ad10 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/data/numberpermutationtest.txt +++ b/android_icu4j/src/main/tests/android/icu/dev/data/numberpermutationtest.txt @@ -1,4 +1,4 @@ -# © 2017 and later: Unicode, Inc. and others. +# © 2019 and later: Unicode, Inc. and others. # License & terms of use: http://www.unicode.org/copyright.html compact-short percent unit-width-narrow @@ -951,7 +951,7 @@ compact-short currency/EUR sign-accounting-except-zero bn-BD ০€ +৯২ হা€ - (০.২২ €) + (০.২২€) compact-short measure-unit/length-furlong sign-accounting-except-zero es-MX @@ -993,7 +993,7 @@ scientific/+ee/sign-always currency/EUR sign-accounting-except-zero bn-BD ০.০০E+০০€ +৯.১৮E+০৪€ - (২.২২E-০১ €) + (২.২২E-০১€) scientific/+ee/sign-always measure-unit/length-furlong sign-accounting-except-zero es-MX @@ -2273,15 +2273,15 @@ compact-short precision-integer sign-accounting-except-zero es-MX 0 +92 k - -0 + 0 zh-TW 0 +9萬 - -0 + 0 bn-BD ০ +৯২ হা - -০ + ০ compact-short .000 sign-accounting-except-zero es-MX @@ -3877,7 +3877,7 @@ currency/EUR unit-width-narrow sign-accounting-except-zero bn-BD ০.০০€ +৯১,৮২৭.৩৬€ - (০.২২ €) + (০.২২€) currency/EUR unit-width-full-name sign-accounting-except-zero es-MX @@ -4849,15 +4849,15 @@ percent precision-integer sign-accounting-except-zero es-MX 0 % +91,827 % - -0 % + 0 % zh-TW 0% +91,827% - -0% + 0% bn-BD ০% +৯১,৮২৭% - -০% + ০% percent .000 sign-accounting-except-zero es-MX @@ -4905,15 +4905,15 @@ currency/EUR precision-integer sign-accounting-except-zero es-MX EUR 0 +EUR 91,827 - -EUR 0 + EUR 0 zh-TW €0 +€91,827 - (€0) + €0 bn-BD ০€ +৯১,৮২৭€ - (০ €) + ০€ currency/EUR .000 sign-accounting-except-zero es-MX @@ -4927,7 +4927,7 @@ currency/EUR .000 sign-accounting-except-zero bn-BD ০.০০০€ +৯১,৮২৭.৩৬৪€ - (০.২২২ €) + (০.২২২€) currency/EUR .##/@@@+ sign-accounting-except-zero es-MX @@ -4941,7 +4941,7 @@ currency/EUR .##/@@@+ sign-accounting-except-zero bn-BD ০€ +৯১,৮২৭.৩৬€ - (০.২২২ €) + (০.২২২€) currency/EUR @@ sign-accounting-except-zero es-MX @@ -4955,21 +4955,21 @@ currency/EUR @@ sign-accounting-except-zero bn-BD ০.০€ +৯২,০০০€ - (০.২২ €) + (০.২২€) measure-unit/length-furlong precision-integer sign-accounting-except-zero es-MX 0 fur +91,827 fur - -0 fur + 0 fur zh-TW 0 化朗 +91,827 化朗 - -0 化朗 + 0 化朗 bn-BD ০ ফার্লং +৯১,৮২৭ ফার্লং - -০ ফার্লং + ০ ফার্লং measure-unit/length-furlong .000 sign-accounting-except-zero es-MX @@ -5375,7 +5375,7 @@ currency/EUR rounding-mode-floor sign-accounting-except-zero bn-BD ০.০০€ +৯১,৮২৭.৩৬€ - (০.২৩ €) + (০.২৩€) measure-unit/length-furlong rounding-mode-floor sign-accounting-except-zero es-MX @@ -5585,7 +5585,7 @@ currency/EUR integer-width/##00 sign-accounting-except-zero bn-BD ০০.০০€ +১,৮২৭.৩৬€ - (০০.২২ €) + (০০.২২€) measure-unit/length-furlong integer-width/##00 sign-accounting-except-zero es-MX @@ -5753,7 +5753,7 @@ currency/EUR scale/0.5 sign-accounting-except-zero bn-BD ০.০০€ +৪৫,৯১৩.৬৮€ - (০.১১ €) + (০.১১€) measure-unit/length-furlong scale/0.5 sign-accounting-except-zero es-MX @@ -5879,7 +5879,7 @@ currency/EUR group-on-aligned sign-accounting-except-zero bn-BD ০.০০€ +৯১,৮২৭.৩৬€ - (০.২২ €) + (০.২২€) measure-unit/length-furlong group-on-aligned sign-accounting-except-zero es-MX @@ -5963,7 +5963,7 @@ currency/EUR latin sign-accounting-except-zero bn-BD 0.00€ +91,827.36€ - (0.22 €) + (0.22€) measure-unit/length-furlong latin sign-accounting-except-zero es-MX @@ -6047,7 +6047,7 @@ currency/EUR sign-accounting-except-zero decimal-always bn-BD ০.০০€ +৯১,৮২৭.৩৬€ - (০.২২ €) + (০.২২€) measure-unit/length-furlong sign-accounting-except-zero decimal-always es-MX @@ -6627,15 +6627,15 @@ unit-width-narrow precision-integer sign-accounting-except-zero es-MX 0 +91,827 - -0 + 0 zh-TW 0 +91,827 - -0 + 0 bn-BD ০ +৯১,৮২৭ - -০ + ০ unit-width-narrow .000 sign-accounting-except-zero es-MX @@ -6683,15 +6683,15 @@ unit-width-full-name precision-integer sign-accounting-except-zero es-MX 0 +91,827 - -0 + 0 zh-TW 0 +91,827 - -0 + 0 bn-BD ০ +৯১,৮২৭ - -০ + ০ unit-width-full-name .000 sign-accounting-except-zero es-MX @@ -7943,15 +7943,15 @@ precision-integer integer-width/##00 sign-accounting-except-zero es-MX 00 +1827 - -00 + 00 zh-TW 00 +1,827 - -00 + 00 bn-BD ০০ +১,৮২৭ - -০০ + ০০ .000 integer-width/##00 sign-accounting-except-zero es-MX @@ -8167,15 +8167,15 @@ precision-integer scale/0.5 sign-accounting-except-zero es-MX 0 +45,914 - -0 + 0 zh-TW 0 +45,914 - -0 + 0 bn-BD ০ +৪৫,৯১৪ - -০ + ০ .000 scale/0.5 sign-accounting-except-zero es-MX @@ -8335,15 +8335,15 @@ precision-integer group-on-aligned sign-accounting-except-zero es-MX 0 +91,827 - -0 + 0 zh-TW 0 +91,827 - -0 + 0 bn-BD ০ +৯১,৮২৭ - -০ + ০ .000 group-on-aligned sign-accounting-except-zero es-MX @@ -8447,15 +8447,15 @@ precision-integer latin sign-accounting-except-zero es-MX 0 +91,827 - -0 + 0 zh-TW 0 +91,827 - -0 + 0 bn-BD 0 +91,827 - -0 + 0 .000 latin sign-accounting-except-zero es-MX @@ -8559,15 +8559,15 @@ precision-integer sign-accounting-except-zero decimal-always es-MX 0. +91,827. - -0. + 0. zh-TW 0. +91,827. - -0. + 0. bn-BD ০. +৯১,৮২৭. - -০. + ০. .000 sign-accounting-except-zero decimal-always es-MX diff --git a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/ibm9027.cnv b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/ibm9027.cnv Binary files differindex 7c0659e85..8a53bfeb1 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/ibm9027.cnv +++ b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/ibm9027.cnv diff --git a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/root.res b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/root.res Binary files differindex 4497c2d11..3d30e163c 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/root.res +++ b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/root.res diff --git a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/structLocale.res b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/structLocale.res Binary files differindex b41279f89..086ded758 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/structLocale.res +++ b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/structLocale.res diff --git a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test1.cnv b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test1.cnv Binary files differindex 3c6a7d0d6..499ee2ea4 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test1.cnv +++ b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test1.cnv diff --git a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test1bmp.cnv b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test1bmp.cnv Binary files differindex b4be8904b..b81ee1068 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test1bmp.cnv +++ b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test1bmp.cnv diff --git a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test2.cnv b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test2.cnv Binary files differindex 91c0eca00..f853304af 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test2.cnv +++ b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test2.cnv diff --git a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test3.cnv b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test3.cnv Binary files differindex 1372e9d76..327529aca 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test3.cnv +++ b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test3.cnv diff --git a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test4.cnv b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test4.cnv Binary files differindex 1f430c428..557221cdc 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test4.cnv +++ b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test4.cnv diff --git a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test4x.cnv b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test4x.cnv Binary files differindex b14218f8f..cab1387bc 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test4x.cnv +++ b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test4x.cnv diff --git a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test5.cnv b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test5.cnv Binary files differindex c67620995..758e3bd2b 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test5.cnv +++ b/android_icu4j/src/main/tests/android/icu/dev/data/testdata/test5.cnv diff --git a/android_icu4j/src/main/tests/android/icu/dev/impl/number/DecimalQuantity_64BitBCD.java b/android_icu4j/src/main/tests/android/icu/dev/impl/number/DecimalQuantity_64BitBCD.java index c4cd54775..4d35af871 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/impl/number/DecimalQuantity_64BitBCD.java +++ b/android_icu4j/src/main/tests/android/icu/dev/impl/number/DecimalQuantity_64BitBCD.java @@ -98,6 +98,7 @@ public final class DecimalQuantity_64BitBCD extends DecimalQuantity_AbstractBCD isApproximate = false; origDouble = 0; origDelta = 0; + exponent = 0; } @Override diff --git a/android_icu4j/src/main/tests/android/icu/dev/impl/number/DecimalQuantity_ByteArrayBCD.java b/android_icu4j/src/main/tests/android/icu/dev/impl/number/DecimalQuantity_ByteArrayBCD.java index 516e9af13..7e281cb56 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/impl/number/DecimalQuantity_ByteArrayBCD.java +++ b/android_icu4j/src/main/tests/android/icu/dev/impl/number/DecimalQuantity_ByteArrayBCD.java @@ -115,6 +115,7 @@ public final class DecimalQuantity_ByteArrayBCD extends DecimalQuantity_Abstract isApproximate = false; origDouble = 0; origDelta = 0; + exponent = 0; } @Override diff --git a/android_icu4j/src/main/tests/android/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java b/android_icu4j/src/main/tests/android/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java index 41118b7ac..394a2b22b 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java +++ b/android_icu4j/src/main/tests/android/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java @@ -10,6 +10,7 @@ import java.text.FieldPosition; import android.icu.impl.StandardPlural; import android.icu.impl.number.DecimalQuantity; +import android.icu.impl.number.Modifier.Signum; import android.icu.text.PluralRules; import android.icu.text.PluralRules.Operand; import android.icu.text.UFieldPosition; @@ -97,6 +98,8 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity { 1000000000000000000L }; + private int origPrimaryScale; + @Override public int maxRepresentableDigits() { return Integer.MAX_VALUE; @@ -112,6 +115,7 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity { primaryScale = 0; primaryPrecision = computePrecision(primary); fallback = null; + origPrimaryScale = primaryScale; } /** @@ -191,6 +195,8 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity { primary = -1; fallback = new BigDecimal(temp); } + + origPrimaryScale = primaryScale; } static final double LOG_2_OF_TEN = 3.32192809489; @@ -281,6 +287,7 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity { primaryPrecision = _other.primaryPrecision; fallback = _other.fallback; flags = _other.flags; + origPrimaryScale = _other.origPrimaryScale; } @Override @@ -520,8 +527,18 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity { } @Override - public int signum() { - return isNegative() ? -1 : isZeroish() ? 0 : 1; + public Signum signum() { + boolean isZero = (isZeroish() && !isInfinite()); + boolean isNeg = isNegative(); + if (isZero && isNeg) { + return Signum.NEG_ZERO; + } else if (isZero) { + return Signum.POS_ZERO; + } else if (isNeg) { + return Signum.NEG; + } else { + return Signum.POS; + } } private void setNegative(boolean isNegative) { @@ -884,9 +901,17 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity { if (isNegative()) { sb.append('-'); } - for (int m = getUpperDisplayMagnitude(); m >= getLowerDisplayMagnitude(); m--) { - sb.append(getDigit(m)); - if (m == 0) sb.append('.'); + int upper = getUpperDisplayMagnitude(); + int lower = getLowerDisplayMagnitude(); + int p = upper; + for (; p >= 0; p--) { + sb.append((char) ('0' + getDigit(p))); + } + if (lower < 0) { + sb.append('.'); + } + for(; p >= lower; p--) { + sb.append((char) ('0' + getDigit(p))); } return sb.toString(); } @@ -908,4 +933,14 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity { .setFractionDigits((int) getPluralOperand(Operand.v), (long) getPluralOperand(Operand.f)); } } + + @Override + public int getExponent() { + return origPrimaryScale; + } + + @Override + public void adjustExponent(int delta) { + origPrimaryScale = origPrimaryScale + delta; + } } diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/calendar/CalendarRegressionTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/calendar/CalendarRegressionTest.java index dce734594..7e4cdd6dd 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/calendar/CalendarRegressionTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/calendar/CalendarRegressionTest.java @@ -2294,7 +2294,7 @@ public class CalendarRegressionTest extends android.icu.dev.test.TestFmwk { gc.setFirstDayOfWeek(Calendar.MONDAY); gc.setMinimalDaysInFirstWeek(4); - // Force the calender to resolve the fields once. + // Force the calendar to resolve the fields once. // The maximum week number in 2011 is 52. gc.set(Calendar.YEAR, 2011); gc.get(Calendar.YEAR); diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/format/DateFormatTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/format/DateFormatTest.java index 8c5c6206f..1704bcb3d 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/format/DateFormatTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/format/DateFormatTest.java @@ -50,6 +50,7 @@ import android.icu.text.ChineseDateFormatSymbols; import android.icu.text.DateFormat; import android.icu.text.DateFormat.BooleanAttribute; import android.icu.text.DateFormatSymbols; +import android.icu.text.DateTimePatternGenerator; import android.icu.text.DisplayContext; import android.icu.text.NumberFormat; import android.icu.text.SimpleDateFormat; @@ -5434,4 +5435,82 @@ public class DateFormatTest extends TestFmwk { dfmt.parse(inDate, pos); assertEquals("Error index", inDate.length(), pos.getErrorIndex()); } + + @Test + public void test20739_MillisecondsWithoutSeconds() { + String[][] cases = new String[][]{ + // Ticket #20739 + // minutes+milliseconds, seconds missing, should be repaired + {"SSSSm", "mm:ss.SSSS"}, + {"mSSSS", "mm:ss.SSSS"}, + {"SSSm", "mm:ss.SSS"}, + {"mSSS", "mm:ss.SSS"}, + {"SSm", "mm:ss.SS"}, + {"mSS", "mm:ss.SS"}, + {"Sm", "mm:ss.S"}, + {"mS", "mm:ss.S"}, + // only milliseconds, untouched, no repairs + {"S", "S"}, + {"SS", "SS"}, + {"SSS", "SSS"}, + {"SSSS", "SSSS"}, + // hour:minute+seconds+milliseconds, correct, no repairs, proper pattern + {"jmsSSS", "h:mm:ss.SSS a"}, + {"jmSSS", "h:mm:ss.SSS a"}, + // Ticket #20738 + // seconds+milliseconds, correct, no repairs, proper pattern + {"sS", "s.S"}, + {"sSS", "s.SS"}, + {"sSSS", "s.SSS"}, + {"sSSSS", "s.SSSS"}, + // minutes+seconds+milliseconds, correct, no repairs, proper pattern + {"msS", "mm:ss.S"}, + {"msSS", "mm:ss.SS"}, + {"msSSS", "mm:ss.SSS"}, + {"msSSSS", "mm:ss.SSSS"} + }; + + ULocale locale = ULocale.ENGLISH; + for (String[] cas : cases) { + DateFormat fmt = DateFormat.getInstanceForSkeleton( cas[0], locale); + String pattern = ((SimpleDateFormat) fmt).toPattern(); + assertEquals("Format pattern", cas[1], pattern); + } + } + + @Test + public void test20741_ABFields() { + String [] skeletons = {"EEEEEBBBBB", "EEEEEbbbbb"}; + ULocale[] locales = ULocale.getAvailableLocales(); + for (String skeleton : skeletons) { + for (int i = 0; i < locales.length; i++) { + ULocale locale = locales[i]; + if (isQuick() && (i % 17 != 0)) continue; + + DateTimePatternGenerator gen = DateTimePatternGenerator.getInstance(locale); + String pattern = gen.getBestPattern(skeleton); + SimpleDateFormat dateFormat = new SimpleDateFormat(pattern, locale); + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("PST8PDT")); + calendar.setTime(new Date(0)); + + FieldPosition pos_c = new FieldPosition(DateFormat.Field.DAY_OF_WEEK); + dateFormat.format(calendar, new StringBuffer(""), pos_c); + assertFalse("'Day of week' field was not found", pos_c.getBeginIndex() == 0 && pos_c.getEndIndex() == 0); + + if (skeleton.equals("EEEEEBBBBB")) { + FieldPosition pos_B = new FieldPosition(DateFormat.Field.FLEXIBLE_DAY_PERIOD); + dateFormat.format(calendar, new StringBuffer(""), pos_B); + assertFalse("'Flexible day period' field was not found", pos_B.getBeginIndex() == 0 && pos_B.getEndIndex() == 0); + } else { + FieldPosition pos_b = new FieldPosition(DateFormat.Field.AM_PM_MIDNIGHT_NOON); + dateFormat.format(calendar, new StringBuffer(""), pos_b); + assertFalse("'AM/PM/Midnight/Noon' field was not found", pos_b.getBeginIndex() == 0 && pos_b.getEndIndex() == 0); + } + + FieldPosition pos_a = new FieldPosition(DateFormat.Field.AM_PM); + dateFormat.format(calendar, new StringBuffer(""), pos_a); + assertTrue("'AM/PM' field was found", pos_a.getBeginIndex() == 0 && pos_a.getEndIndex() == 0); + } + } + } } diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/format/DateIntervalFormatTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/format/DateIntervalFormatTest.java index ff05e18ac..0e3737e52 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/format/DateIntervalFormatTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/format/DateIntervalFormatTest.java @@ -1263,10 +1263,10 @@ public class DateIntervalFormatTest extends TestFmwk { @Test public void TestGetIntervalPattern(){ // Tests when "if ( field > MINIMUM_SUPPORTED_CALENDAR_FIELD )" is true - // MINIMUM_SUPPORTED_CALENDAR_FIELD = Calendar.SECOND; + // MINIMUM_SUPPORTED_CALENDAR_FIELD = Calendar.MILLISECOND; DateIntervalInfo dii = new DateIntervalInfo(); try{ - dii.getIntervalPattern("", Calendar.SECOND+1); + dii.getIntervalPattern("", Calendar.MILLISECOND+1); errln("DateIntervalInfo.getIntervalPattern(String,int) was suppose " + "to return an exception for the 'int field' parameter " + "when it exceeds MINIMUM_SUPPORTED_CALENDAR_FIELD."); @@ -1289,10 +1289,10 @@ public class DateIntervalFormatTest extends TestFmwk { } catch(Exception e){} // Tests when "if ( lrgDiffCalUnit > MINIMUM_SUPPORTED_CALENDAR_FIELD )" is true - // MINIMUM_SUPPORTED_CALENDAR_FIELD = Calendar.SECOND; + // MINIMUM_SUPPORTED_CALENDAR_FIELD = Calendar.MILLISECOND; try{ dii = dii.cloneAsThawed(); - dii.setIntervalPattern("", Calendar.SECOND+1, ""); + dii.setIntervalPattern("", Calendar.MILLISECOND+1, ""); errln("DateIntervalInfo.setIntervalPattern(String,int,String) " + "was suppose to return an exception when the " + "variable 'lrgDiffCalUnit' is greater than " + @@ -2060,4 +2060,100 @@ public class DateIntervalFormatTest extends TestFmwk { } } } + + @Test + public void testTicket20707() { + TimeZone tz = TimeZone.getTimeZone("UTC"); + Locale locales[] = { + new Locale("en-u-hc-h24"), + new Locale("en-u-hc-h23"), + new Locale("en-u-hc-h12"), + new Locale("en-u-hc-h11"), + new Locale("en"), + new Locale("en-u-hc-h25"), + new Locale("hi-IN-u-hc-h11") + }; + + // Clomuns: hh, HH, kk, KK, jj, JJs, CC + String expected[][] = { + // Hour-cycle: k + {"12 AM", "24", "24", "12 AM", "24", "0 (hour: 24)", "12 AM"}, + // Hour-cycle: H + {"12 AM", "00", "00", "12 AM", "00", "0 (hour: 00)", "12 AM"}, + // Hour-cycle: h + {"12 AM", "00", "00", "12 AM", "12 AM", "0 (hour: 12)", "12 AM"}, + // Hour-cycle: K + {"0 AM", "00", "00", "0 AM", "0 AM", "0 (hour: 00)", "0 AM"}, + {"12 AM", "00", "00", "12 AM", "12 AM", "0 (hour: 12)", "12 AM"}, + {"12 AM", "00", "00", "12 AM", "12 AM", "0 (hour: 12)", "12 AM"}, + {"0 am", "00", "00", "0 am", "0 am", "0 (\u0918\u0902\u091F\u093E: 00)", "\u0930\u093E\u0924 0"} + }; + + int i = 0; + for (Locale locale : locales) { + int j = 0; + String skeletons[] = {"hh", "HH", "kk", "KK", "jj", "JJs", "CC"}; + for (String skeleton : skeletons) { + DateIntervalFormat dateFormat = DateIntervalFormat.getInstance(skeleton, locale); + Calendar calendar = Calendar.getInstance(tz); + calendar.setTime(new Date(1563235200000L)); + StringBuffer resultBuffer = dateFormat.format(calendar, calendar, new StringBuffer(""), new FieldPosition(0)); + + assertEquals("Formatted result for " + skeleton + " locale: " + locale.getDisplayName(), expected[i][j++], resultBuffer.toString()); + } + i++; + } + } + + @Test + public void testFormatMillisecond() { + Object[][] kTestCases = { + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 321, "ms", "23:45"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 321, "msS", "23:45.3"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 321, "msSS", "23:45.32"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 321, "msSSS", "23:45.321"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 987, "ms", "23:45"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 987, "msS", "23:45.3 – 23:45.9"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 987, "msSS", "23:45.32 – 23:45.98"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 987, "msSSS", "23:45.321 – 23:45.987"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 46), 987, "ms", "23:45 – 23:46"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 46), 987, "msS", "23:45.3 – 23:46.9"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 46), 987, "msSS", "23:45.32 – 23:46.98"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 46), 987, "msSSS", "23:45.321 – 23:46.987"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 2, 24, 45), 987, "ms", "23:45 – 24:45"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 2, 24, 45), 987, "msS", "23:45.3 – 24:45.9"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 2, 24, 45), 987, "msSS", "23:45.32 – 24:45.98"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 2, 24, 45), 987, "msSSS", "23:45.321 – 24:45.987"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 321, "s", "45"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 321, "sS", "45.3"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 321, "sSS", "45.32"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 321, "sSSS", "45.321"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 987, "s", "45"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 987, "sS", "45.3 – 45.9"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 987, "sSS", "45.32 – 45.98"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 45), 987, "sSSS", "45.321 – 45.987"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 46), 987, "s", "45 – 46"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 46), 987, "sS", "45.3 – 46.9"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 46), 987, "sSS", "45.32 – 46.98"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 1, 23, 46), 987, "sSSS", "45.321 – 46.987"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 2, 24, 45), 987, "s", "45 – 45"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 2, 24, 45), 987, "sS", "45.3 – 45.9"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 2, 24, 45), 987, "sSS", "45.32 – 45.98"}, + { new Date(2019, 2, 10, 1, 23, 45), 321, new Date(2019, 2, 10, 2, 24, 45), 987, "sSSS", "45.321 – 45.987"}, + }; + + Locale enLocale = Locale.ENGLISH; + + for (Object[] testCase : kTestCases) { + DateIntervalFormat fmt = DateIntervalFormat.getInstance((String)testCase[4], enLocale); + + Date fromDate = (Date)testCase[0]; + long from = fromDate.getTime() + (Integer)testCase[1]; + Date toDate = (Date)testCase[2]; + long to = toDate.getTime() + (Integer)testCase[3]; + + FormattedDateInterval res = fmt.formatToValue(new DateInterval(from, to)); + assertEquals("Formate for " + testCase[4], testCase[5], res.toString()); + } + } } diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/format/DateTimeGeneratorTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/format/DateTimeGeneratorTest.java index 87c4bc35a..b3a93ad2b 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/format/DateTimeGeneratorTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/format/DateTimeGeneratorTest.java @@ -1742,14 +1742,14 @@ public class DateTimeGeneratorTest extends TestFmwk { String[][] cases = new String[][]{ // ars is interesting because it does not have a region, but it aliases // to ar_SA, which has a region. - {"ars", "h a", "h:mm a"}, + {"ars", "h a", "h:mm a", "HOUR_CYCLE_12"}, // en_NH is interesting because NH is a depregated region code. - {"en_NH", "h a", "h:mm a"}, + {"en_NH", "h a", "h:mm a", "HOUR_CYCLE_12"}, // ch_ZH is a typo (should be zh_CN), but we should fail gracefully. // {"cn_ZH", "HH", "H:mm"}, // TODO(ICU-20653): Desired behavior - {"cn_ZH", "HH", "h:mm a"}, // Actual behavior + {"cn_ZH", "HH", "h:mm a", "HOUR_CYCLE_23"}, // Actual behavior // a non-BCP47 locale without a country code should not fail - {"ja_TRADITIONAL", "H時", "H:mm"}, + {"ja_TRADITIONAL", "H時", "H:mm", "HOUR_CYCLE_23"}, }; for (String[] cas : cases) { @@ -1764,6 +1764,8 @@ public class DateTimeGeneratorTest extends TestFmwk { cas[1], dtpgPattern); assertEquals("timePattern " + cas[1], cas[2], timePattern); + assertEquals("default hour cycle " + cas[3], + cas[3], dtpg.getDefaultHourCycle().toString()); } } } diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/format/FormattedStringBuilderTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/format/FormattedStringBuilderTest.java index 07e1a6c3e..d2651df95 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/format/FormattedStringBuilderTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/format/FormattedStringBuilderTest.java @@ -9,7 +9,6 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import java.text.FieldPosition; -import java.text.Format.Field; import org.junit.Test; @@ -173,7 +172,7 @@ public class FormattedStringBuilderTest { FormattedStringBuilder sb = new FormattedStringBuilder(); sb.append(str, null); sb.append(str, NumberFormat.Field.CURRENCY); - Field[] fields = sb.toFieldArray(); + Object[] fields = sb.toFieldArray(); assertEquals(str.length() * 2, fields.length); for (int i = 0; i < str.length(); i++) { assertEquals(null, fields[i]); @@ -201,7 +200,7 @@ public class FormattedStringBuilderTest { int numNull = 0; int numCurr = 0; int numInt = 0; - Field[] oldFields = fields; + Object[] oldFields = fields; fields = sb.toFieldArray(); for (int i = 0; i < sb.length(); i++) { assertEquals(oldFields[i % oldFields.length], fields[i]); diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/format/GlobalizationPreferencesTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/format/GlobalizationPreferencesTest.java index f1b49efd2..ee0fa33d0 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/format/GlobalizationPreferencesTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/format/GlobalizationPreferencesTest.java @@ -350,10 +350,19 @@ public class GlobalizationPreferencesTest extends TestFmwk { gp.reset(); gp.setLocales(acceptLanguage); - List resultLocales = gp.getLocales(); + List<ULocale> resultLocales = gp.getLocales(); + List<ULocale> expectedLocales = new ArrayList<>(RESULTS_LOCALEIDS[i].length); + for (String exp : RESULTS_LOCALEIDS[i]) { + expectedLocales.add(new ULocale(exp)); + } + assertEquals("#" + i, expectedLocales.toString(), resultLocales.toString()); if (resultLocales.size() != RESULTS_LOCALEIDS[i].length) { + StringBuilder res = new StringBuilder(); + for (ULocale l : resultLocales) { + res.append(l.toString()).append(","); + } errln("FAIL: Number of locales mismatch - GP:" + resultLocales.size() - + " Expected:" + RESULTS_LOCALEIDS[i].length); + + " Expected:" + RESULTS_LOCALEIDS[i].length + " index: " + i + " " + res.toString()); } else { for (int j = 0; j < RESULTS_LOCALEIDS[i].length; j++) { @@ -376,22 +385,23 @@ public class GlobalizationPreferencesTest extends TestFmwk { } // Invalid accept-language - logln("Set locale - ko_KR"); - gp.setLocale(new ULocale("ko_KR")); - boolean bException = false; - try { - logln("Set invlaid accept-language - ko=100"); - gp.setLocales("ko=100"); - } catch (IllegalArgumentException iae) { - logln("IllegalArgumentException was thrown"); - bException = true; - } - if (!bException) { - errln("FAIL: IllegalArgumentException was not thrown for illegal accept-language - ko=100"); - } - if (!gp.getLocale(0).toString().equals("ko_KR")) { - errln("FAIL: Previous valid locale list had gone"); - } + // ICU-20700 changed the parser to using LocalePriorityList which is more lenient. +// logln("Set locale - ko_KR"); +// gp.setLocale(new ULocale("ko_KR")); +// boolean bException = false; +// try { +// logln("Set invlaid accept-language - ko=100"); +// gp.setLocales("ko=100"); +// } catch (IllegalArgumentException iae) { +// logln("IllegalArgumentException was thrown"); +// bException = true; +// } +// if (!bException) { +// errln("FAIL: IllegalArgumentException was not thrown for illegal accept-language - ko=100"); +// } +// if (!gp.getLocale(0).toString().equals("ko_KR")) { +// errln("FAIL: Previous valid locale list had gone"); +// } } @Test diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/format/IntlTestNumberFormat.java b/android_icu4j/src/main/tests/android/icu/dev/test/format/IntlTestNumberFormat.java index 5f7a2869d..f958435ab 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/format/IntlTestNumberFormat.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/format/IntlTestNumberFormat.java @@ -14,6 +14,7 @@ **/ package android.icu.dev.test.format; + import java.util.Locale; import java.util.Random; @@ -24,6 +25,7 @@ import org.junit.runners.JUnit4; import android.icu.dev.test.TestFmwk; import android.icu.text.DecimalFormat; import android.icu.text.NumberFormat; +import android.icu.util.ULocale; import android.icu.testsharding.MainTestShard; /** @@ -170,6 +172,8 @@ public class IntlTestNumberFormat extends TestFmwk { boolean dump = false; int i; + String message = "Locale: " + fNumberFormat.getLocale(ULocale.VALID_LOCALE); + for (i = 0; i < DEPTH; i++) { if (i == 0) { number[i] = aNumber; @@ -177,7 +181,7 @@ public class IntlTestNumberFormat extends TestFmwk { try { number[i - 1] = fNumberFormat.parse(string[i - 1]).doubleValue(); } catch(java.text.ParseException pe) { - errln("**** FAIL: Parse of " + string[i-1] + " failed."); + errln("**** FAIL: Parse of " + string[i-1] + " failed: " + message); dump = true; break; } @@ -190,7 +194,7 @@ public class IntlTestNumberFormat extends TestFmwk { numberMatch = i; else if (numberMatch > 0 && number[i] != number[i-1]) { - errln("**** FAIL: Numeric mismatch after match."); + errln("**** FAIL: Numeric mismatch after match: " + message); dump = true; break; } @@ -198,7 +202,7 @@ public class IntlTestNumberFormat extends TestFmwk { stringMatch = i; else if (stringMatch > 0 && string[i] != string[i-1]) { - errln("**** FAIL: String mismatch after match."); + errln("**** FAIL: String mismatch after match: " + message); dump = true; break; } @@ -211,7 +215,7 @@ public class IntlTestNumberFormat extends TestFmwk { if (stringMatch > 2 || numberMatch > 2) { - errln("**** FAIL: No string and/or number match within 2 iterations."); + errln("**** FAIL: No string and/or number match within 2 iterations: " + message); dump = true; } @@ -232,16 +236,19 @@ public class IntlTestNumberFormat extends TestFmwk { public void tryIt(int aNumber) { long number; + String message = "Locale: " + fNumberFormat.getLocale(ULocale.VALID_LOCALE); + String stringNum = fNumberFormat.format(aNumber); try { number = fNumberFormat.parse(stringNum).longValue(); } catch (java.text.ParseException pe) { - errln("**** FAIL: Parse of " + stringNum + " failed."); + errln("**** FAIL: Parse of " + stringNum + " failed: " + message); return; } if (number != aNumber) { - errln("**** FAIL: Parse of " + stringNum + " failed. Got:" + number + errln("**** FAIL: Parse of " + stringNum + " failed: " + message + + " Got:" + number + " Expected:" + aNumber); } @@ -282,9 +289,9 @@ public class IntlTestNumberFormat extends TestFmwk { count = locales.length; if (count != 0) { - if (TestFmwk.getExhaustiveness() < 10 && count > 6) { - count = 6; - locales = new Locale[6]; + if (TestFmwk.getExhaustiveness() < 10 && count > 7) { + count = 7; + locales = new Locale[count]; locales[0] = allLocales[0]; locales[1] = allLocales[1]; locales[2] = allLocales[2]; @@ -294,6 +301,7 @@ public class IntlTestNumberFormat extends TestFmwk { locales[3] = new Locale("ar", "AE", ""); locales[4] = new Locale("cs", "CZ", ""); locales[5] = new Locale("en", "IN", ""); + locales[6] = new Locale("su", "", ""); } for (int i=0; i<count; ++i) { diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/format/ListFormatterTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/format/ListFormatterTest.java index 958447ede..7e1a7b5c1 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/format/ListFormatterTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/format/ListFormatterTest.java @@ -10,6 +10,8 @@ package android.icu.dev.test.format; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Locale; import org.junit.Test; @@ -18,6 +20,9 @@ import org.junit.runners.JUnit4; import android.icu.dev.test.TestFmwk; import android.icu.text.ListFormatter; +import android.icu.text.ListFormatter.FormattedList; +import android.icu.text.ListFormatter.Type; +import android.icu.text.ListFormatter.Width; import android.icu.util.ULocale; import android.icu.testsharding.MainTestShard; @@ -212,4 +217,144 @@ public class ListFormatterTest extends TestFmwk { ULocale defaultLocale = ULocale.getDefault(ULocale.Category.FORMAT); return defaultLocale.equals(ULocale.ENGLISH) || defaultLocale.equals(ULocale.US); } + + @Test + public void TestFormattedValue() { + ListFormatter fmt = ListFormatter.getInstance(ULocale.ENGLISH); + + { + String message = "Field position test 1"; + String expectedString = "hello, wonderful, and world"; + String[] inputs = { + "hello", + "wonderful", + "world" + }; + FormattedList result = fmt.formatToValue(Arrays.asList(inputs)); + Object[][] expectedFieldPositions = new Object[][] { + // field, begin index, end index + {ListFormatter.SpanField.LIST_SPAN, 0, 5, 0}, + {ListFormatter.Field.ELEMENT, 0, 5}, + {ListFormatter.Field.LITERAL, 5, 7}, + {ListFormatter.SpanField.LIST_SPAN, 7, 16, 1}, + {ListFormatter.Field.ELEMENT, 7, 16}, + {ListFormatter.Field.LITERAL, 16, 22}, + {ListFormatter.SpanField.LIST_SPAN, 22, 27, 2}, + {ListFormatter.Field.ELEMENT, 22, 27}}; + FormattedValueTest.checkFormattedValue( + message, + result, + expectedString, + expectedFieldPositions); + } + } + + @Test + public void TestCreateStyled() { + // Locale en has interesting data + Object[][] cases = { + { "pt", Type.AND, Width.WIDE, "A, B e C" }, + { "pt", Type.AND, Width.SHORT, "A, B e C" }, + { "pt", Type.AND, Width.NARROW, "A, B, C" }, + { "pt", Type.OR, Width.WIDE, "A, B ou C" }, + { "pt", Type.OR, Width.SHORT, "A, B ou C" }, + { "pt", Type.OR, Width.NARROW, "A, B ou C" }, + { "pt", Type.UNITS, Width.WIDE, "A, B e C" }, + { "pt", Type.UNITS, Width.SHORT, "A, B e C" }, + { "pt", Type.UNITS, Width.NARROW, "A B C" }, + { "en", Type.AND, Width.WIDE, "A, B, and C" }, + { "en", Type.AND, Width.SHORT, "A, B, & C" }, + { "en", Type.AND, Width.NARROW, "A, B, C" }, + { "en", Type.OR, Width.WIDE, "A, B, or C" }, + { "en", Type.OR, Width.SHORT, "A, B, or C" }, + { "en", Type.OR, Width.NARROW, "A, B, or C" }, + { "en", Type.UNITS, Width.WIDE, "A, B, C" }, + { "en", Type.UNITS, Width.SHORT, "A, B, C" }, + { "en", Type.UNITS, Width.NARROW, "A B C" }, + }; + for (Object[] cas : cases) { + Locale loc = new Locale((String) cas[0]); + ULocale uloc = new ULocale((String) cas[0]); + Type type = (Type) cas[1]; + Width width = (Width) cas[2]; + String expected = (String) cas[3]; + ListFormatter fmt1 = ListFormatter.getInstance(loc, type, width); + ListFormatter fmt2 = ListFormatter.getInstance(uloc, type, width); + String message = "TestCreateStyled loc=" + + loc + " type=" + + type + " width=" + + width; + String[] inputs = { + "A", + "B", + "C" + }; + String result = fmt1.format(Arrays.asList(inputs)); + assertEquals(message, expected, result); + // Coverage for the other factory method overload: + result = fmt2.format(Arrays.asList(inputs)); + assertEquals(message, expected, result); + } + } + + @Test + public void TestContextual() { + String [] es = { "es", "es_419", "es_PY", "es_DO" }; + String [] he = { "he", "he_IL", "iw", "iw_IL" }; + Width[] widths = {Width.WIDE, Width.SHORT, Width.NARROW}; + Object[][] cases = { + { es, Type.AND, "fascinante e incre\u00EDblemente", "fascinante", "incre\u00EDblemente"}, + { es, Type.AND, "Comunicaciones Industriales e IIoT", "Comunicaciones Industriales", "IIoT"}, + { es, Type.AND, "Espa\u00F1a e Italia", "Espa\u00F1a", "Italia"}, + { es, Type.AND, "hijas intr\u00E9pidas e hijos solidarios", "hijas intr\u00E9pidas", "hijos solidarios"}, + { es, Type.AND, "a un hombre e hirieron a otro", "a un hombre", "hirieron a otro"}, + { es, Type.AND, "hija e hijo", "hija", "hijo"}, + { es, Type.AND, "esposa, hija e hijo", "esposa", "hija", "hijo"}, + // For 'y' exception + { es, Type.AND, "oro y hierro", "oro", "hierro"}, + { es, Type.AND, "agua y hielo", "agua", "hielo"}, + { es, Type.AND, "col\u00E1geno y hialur\u00F3nico", "col\u00E1geno", "hialur\u00F3nico"}, + + { es, Type.OR, "desierto u oasis", "desierto", "oasis"}, + { es, Type.OR, "oasis, desierto u océano", "oasis", "desierto", "océano"}, + { es, Type.OR, "7 u 8", "7", "8"}, + { es, Type.OR, "7 u 80", "7", "80"}, + { es, Type.OR, "7 u 800", "7", "800"}, + { es, Type.OR, "6, 7 u 8", "6", "7", "8"}, + { es, Type.OR, "10 u 11", "10", "11"}, + { es, Type.OR, "10 o 111", "10", "111"}, + { es, Type.OR, "10 o 11.2", "10", "11.2"}, + { es, Type.OR, "9, 10 u 11", "9", "10", "11"}, + + { he, Type.AND, "a, b \u05D5-c", "a", "b", "c" }, + { he, Type.AND, "a \u05D5-b", "a", "b" }, + { he, Type.AND, "1, 2 \u05D5-3", "1", "2", "3" }, + { he, Type.AND, "1 \u05D5-2", "1", "2" }, + { he, Type.AND, "\u05D0\u05D4\u05D1\u05D4 \u05D5\u05DE\u05E7\u05D5\u05D5\u05D4", + "\u05D0\u05D4\u05D1\u05D4", "\u05DE\u05E7\u05D5\u05D5\u05D4" }, + { he, Type.AND, "\u05D0\u05D4\u05D1\u05D4, \u05DE\u05E7\u05D5\u05D5\u05D4 \u05D5\u05D0\u05DE\u05D5\u05E0\u05D4", + "\u05D0\u05D4\u05D1\u05D4", "\u05DE\u05E7\u05D5\u05D5\u05D4", "\u05D0\u05DE\u05D5\u05E0\u05D4" }, + }; + for (Width width : widths) { + for (Object[] cas : cases) { + String [] locales = (String[]) cas[0]; + Type type = (Type) cas[1]; + String expected = (String) cas[2]; + for (String locale : locales) { + ULocale uloc = new ULocale(locale); + List inputs = Arrays.asList(cas).subList(3, cas.length); + ListFormatter fmt = ListFormatter.getInstance(uloc, type, width); + String message = "TestContextual uloc=" + + uloc + " type=" + + type + " width=" + + width + "data="; + for (Object i : inputs) { + message += i + ","; + } + String result = fmt.format(inputs); + assertEquals(message, expected, result); + } + } + } + } } diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/format/MeasureUnitTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/format/MeasureUnitTest.java index 1094e2a49..a475414e2 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/format/MeasureUnitTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/format/MeasureUnitTest.java @@ -87,7 +87,7 @@ public class MeasureUnitTest extends TestFmwk { } } - private static final String[] DRAFT_VERSIONS = {"64", "65"}; + private static final String[] DRAFT_VERSIONS = {"64", "65", "66", "67"}; private static final HashSet<String> DRAFT_VERSION_SET = new HashSet<>(); @@ -270,6 +270,10 @@ public class MeasureUnitTest extends TestFmwk { private static final HashMap<String, String> JAVA_VERSION_MAP = new HashMap<>(); + // modify certain CLDR unit names before generating functions + // that create/get the corresponding MeasureUnit objects + private static final Map<String,String> CLDR_NAME_REMAP = new HashMap(); + static { TIME_CODES.add("year"); TIME_CODES.add("month"); @@ -284,6 +288,20 @@ public class MeasureUnitTest extends TestFmwk { for (String[] funcNameAndVersion : JAVA_VERSIONS) { JAVA_VERSION_MAP.put(funcNameAndVersion[0], funcNameAndVersion[1]); } + + // CLDR_NAME_REMAP entries + // The first two fix overly-generic CLDR unit names + CLDR_NAME_REMAP.put("revolution", "revolution-angle"); + CLDR_NAME_REMAP.put("generic", "generic-temperature"); + // The next seven map updated CLDR 37 names back to their + // old form in order to preserve the old function names + CLDR_NAME_REMAP.put("meter-per-square-second", "meter-per-second-squared"); + CLDR_NAME_REMAP.put("permillion", "part-per-million"); + CLDR_NAME_REMAP.put("liter-per-100-kilometer", "liter-per-100kilometers"); + CLDR_NAME_REMAP.put("inch-ofhg", "inch-hg"); + CLDR_NAME_REMAP.put("millimeter-ofhg", "millimeter-of-mercury"); + CLDR_NAME_REMAP.put("pound-force-per-square-inch", "pound-per-square-inch"); + CLDR_NAME_REMAP.put("pound-force-foot", "pound-foot"); } @Test @@ -291,12 +309,12 @@ public class MeasureUnitTest extends TestFmwk { // various generateXXX calls go here, see // http://site.icu-project.org/design/formatting/measureformat/updating-measure-unit // use this test to run each of the ollowing in succession - //generateConstants("65"); // for MeasureUnit.java, update generated MeasureUnit constants - //generateBackwardCompatibilityTest("65"); // for MeasureUnitTest.java, create TestCompatible65 - //generateCXXHConstants("65"); // for measunit.h, update generated createXXX methods + //generateConstants("67"); // for MeasureUnit.java, update generated MeasureUnit constants + //generateBackwardCompatibilityTest("67"); // for MeasureUnitTest.java, create TestCompatible65 + //generateCXXHConstants("67"); // for measunit.h, update generated createXXX methods //generateCXXConstants(); // for measunit.cpp, update generated code - //generateCXXBackwardCompatibilityTest("65"); // for measfmttest.cpp, create TestCompatible65 - //updateJAVAVersions("65"); // for MeasureUnitTest.java, JAVA_VERSIONS + //generateCXXBackwardCompatibilityTest("67"); // for measfmttest.cpp, create TestCompatible65 + //updateJAVAVersions("67"); // for MeasureUnitTest.java, JAVA_VERSIONS } @Test @@ -2926,12 +2944,12 @@ public class MeasureUnitTest extends TestFmwk { StringBuilder result = new StringBuilder(); boolean caps = true; String code = unit.getSubtype(); - if (code.equals("revolution")) { - code = code + "-angle"; - } - if (code.equals("generic")) { - code = code + "-temperature"; + + String replacement = CLDR_NAME_REMAP.get(code); + if (replacement != null) { + code = replacement; } + int len = code.length(); for (int i = 0; i < len; i++) { char ch = code.charAt(i); @@ -3007,19 +3025,17 @@ public class MeasureUnitTest extends TestFmwk { static String toJAVAName(MeasureUnit unit) { String code = unit.getSubtype(); String type = unit.getType(); + + String replacement = CLDR_NAME_REMAP.get(code); + if (replacement != null) { + code = replacement; + } + String name = code.toUpperCase(Locale.ENGLISH).replace("-", "_"); if (type.equals("angle")) { if (code.equals("minute") || code.equals("second")) { name = "ARC_" + name; } - if (code.equals("revolution")) { - name = name + "_ANGLE"; - } - } - if (type.equals("temperature")) { - if (code.equals("generic")) { - name = name + "_TEMPERATURE"; - } } return name; } diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/format/NumberFormatTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/format/NumberFormatTest.java index 1b0f6bb2a..76f483c66 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/format/NumberFormatTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/format/NumberFormatTest.java @@ -4171,6 +4171,62 @@ public class NumberFormatTest extends TestFmwk { } @Test + public void TestMinIntMinFracZero() { + class TestMinIntMinFracItem { + double value;; + String expDecFmt; + String expCurFmt; + // Simple constructor + public TestMinIntMinFracItem(double valueIn, String expDecFmtIn, String expCurFmtIn) { + value = valueIn; + expDecFmt = expDecFmtIn; + expCurFmt = expCurFmtIn; + } + }; + + final TestMinIntMinFracItem[] items = { + // decFmt curFmt + new TestMinIntMinFracItem( 10.0, "10", "$10" ), + new TestMinIntMinFracItem( 0.9, ".9", "$.9" ), + new TestMinIntMinFracItem( 0.0, "0", "$0" ), + }; + int minInt, minFrac; + + NumberFormat decFormat = NumberFormat.getInstance(ULocale.US, NumberFormat.NUMBERSTYLE); + decFormat.setMinimumIntegerDigits(0); + decFormat.setMinimumFractionDigits(0); + minInt = decFormat.getMinimumIntegerDigits(); + minFrac = decFormat.getMinimumFractionDigits(); + if (minInt != 0 || minFrac != 0) { + errln("after setting DECIMAL minInt=minFrac=0, get minInt " + minInt + ", minFrac " + minFrac); + } + String decPattern = ((DecimalFormat)decFormat).toPattern(); + if (decPattern.length() < 3 || decPattern.indexOf("#.#")< 0) { + errln("after setting DECIMAL minInt=minFrac=0, expect pattern to contain \"#.#\", but get " + decPattern); + } + + NumberFormat curFormat = NumberFormat.getInstance(ULocale.US, NumberFormat.CURRENCYSTYLE); + curFormat.setMinimumIntegerDigits(0); + curFormat.setMinimumFractionDigits(0); + minInt = curFormat.getMinimumIntegerDigits(); + minFrac = curFormat.getMinimumFractionDigits(); + if (minInt != 0 || minFrac != 0) { + errln("after setting CURRENCY minInt=minFrac=0, get minInt " + minInt + ", minFrac " + minFrac); + } + + for (TestMinIntMinFracItem item: items) { + String decString = decFormat.format(item.value); + if (!decString.equals(item.expDecFmt)) { + errln("format DECIMAL value " + item.value + ", expected \"" + item.expDecFmt + "\", got \"" + decString + "\""); + } + String curString = curFormat.format(item.value); + if (!curString.equals(item.expCurFmt)) { + errln("format CURRENCY value " + item.value + ", expected \"" + item.expCurFmt + "\", got \"" + curString + "\""); + } + } + } + + @Test public void TestBug9936() { DecimalFormat numberFormat = (DecimalFormat) NumberFormat.getInstance(ULocale.US); @@ -6735,4 +6791,10 @@ public class NumberFormatTest extends TestFmwk { assertEquals("result: ", null, result); } } + + @Test + public void test20961_CurrencyPluralPattern() { + DecimalFormat decimalFormat = (DecimalFormat) NumberFormat.getInstance(ULocale.US, NumberFormat.PLURALCURRENCYSTYLE); + assertEquals("Currency pattern", "#,##0.00 ¤¤¤", decimalFormat.toPattern()); + } } diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/format/PluralRulesTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/format/PluralRulesTest.java index 0da015d3b..cf23155fb 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/format/PluralRulesTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/format/PluralRulesTest.java @@ -43,6 +43,7 @@ import android.icu.dev.util.CollectionUtilities; import android.icu.impl.Relation; import android.icu.impl.Utility; import android.icu.number.FormattedNumber; +import android.icu.number.LocalizedNumberFormatter; import android.icu.number.NumberFormatter; import android.icu.number.Precision; import android.icu.number.UnlocalizedNumberFormatter; @@ -932,6 +933,66 @@ public class PluralRulesTest extends TestFmwk { } } + + + @Test + public void testCompactDecimalPluralKeyword() { + PluralRules rules = PluralRules.createRules("one: i = 0,1 @integer 0, 1 @decimal 0.0~1.5; many: e = 0 and i % 1000000 = 0 and v = 0 or " + + "e != 0 .. 5; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"); + ULocale locale = new ULocale("fr-FR"); + + Object[][] casesData = { + // unlocalized formatter skeleton, input, string output, plural rule keyword + {"", 0, "0", "one"}, + {"compact-long", 0, "0", "one"}, + + {"", 1, "1", "one"}, + {"compact-long", 1, "1", "one"}, + + {"", 2, "2", "other"}, + {"compact-long", 2, "2", "other"}, + + {"", 1000000, "1 000 000", "many"}, + {"compact-long", 1000000, "1 million", "many"}, + + {"", 1000001, "1 000 001", "other"}, + {"compact-long", 1000001, "1 million", "many"}, + + {"", 120000, "1 200 000", "other"}, + {"compact-long", 1200000, "1,2 millions", "many"}, + + {"", 1200001, "1 200 001", "other"}, + {"compact-long", 1200001, "1,2 millions", "many"}, + + {"", 2000000, "2 000 000", "many"}, + {"compact-long", 2000000, "2 millions", "many"}, + }; + + for (Object[] caseDatum : casesData) { + String skeleton = (String) caseDatum[0]; + int input = (int) caseDatum[1]; + String expectedString = (String) caseDatum[2]; + String expectPluralRuleKeyword = (String) caseDatum[3]; + + String actualPluralRuleKeyword = + getPluralKeyword(rules, locale, input, skeleton); + + assertEquals( + String.format("PluralRules select %s: %d", skeleton, input), + expectPluralRuleKeyword, + actualPluralRuleKeyword); + } + } + + private String getPluralKeyword(PluralRules rules, ULocale locale, double number, String skeleton) { + LocalizedNumberFormatter formatter = + NumberFormatter.forSkeleton(skeleton) + .locale(locale); + FormattedNumber fn = formatter.format(number); + String pluralKeyword = rules.select(fn); + return pluralKeyword; + } + enum StandardPluralCategories { zero, one, two, few, many, other; /** diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/number/DecimalQuantityTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/number/DecimalQuantityTest.java index 4038b0b0c..2453d3e4d 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/number/DecimalQuantityTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/number/DecimalQuantityTest.java @@ -25,10 +25,15 @@ import android.icu.impl.number.DecimalFormatProperties; import android.icu.impl.number.DecimalQuantity; import android.icu.impl.number.DecimalQuantity_DualStorageBCD; import android.icu.impl.number.RoundingUtils; +import android.icu.number.FormattedNumber; import android.icu.number.LocalizedNumberFormatter; +import android.icu.number.Notation; import android.icu.number.NumberFormatter; +import android.icu.number.Precision; +import android.icu.number.Scale; import android.icu.text.CompactDecimalFormat.CompactStyle; import android.icu.text.DecimalFormatSymbols; +import android.icu.text.PluralRules.Operand; import android.icu.util.ULocale; import android.icu.testsharding.MainTestShard; @@ -533,13 +538,13 @@ public class DecimalQuantityTest extends TestFmwk { @Test public void testNickelRounding() { Object[][] cases = new Object[][] { - {1.000, -2, RoundingMode.HALF_EVEN, "1."}, - {1.001, -2, RoundingMode.HALF_EVEN, "1."}, - {1.010, -2, RoundingMode.HALF_EVEN, "1."}, - {1.020, -2, RoundingMode.HALF_EVEN, "1."}, - {1.024, -2, RoundingMode.HALF_EVEN, "1."}, - {1.025, -2, RoundingMode.HALF_EVEN, "1."}, - {1.025, -2, RoundingMode.HALF_DOWN, "1."}, + {1.000, -2, RoundingMode.HALF_EVEN, "1"}, + {1.001, -2, RoundingMode.HALF_EVEN, "1"}, + {1.010, -2, RoundingMode.HALF_EVEN, "1"}, + {1.020, -2, RoundingMode.HALF_EVEN, "1"}, + {1.024, -2, RoundingMode.HALF_EVEN, "1"}, + {1.025, -2, RoundingMode.HALF_EVEN, "1"}, + {1.025, -2, RoundingMode.HALF_DOWN, "1"}, {1.025, -2, RoundingMode.HALF_UP, "1.05"}, {1.026, -2, RoundingMode.HALF_EVEN, "1.05"}, {1.030, -2, RoundingMode.HALF_EVEN, "1.05"}, @@ -555,28 +560,28 @@ public class DecimalQuantityTest extends TestFmwk { {1.080, -2, RoundingMode.HALF_EVEN, "1.1"}, {1.090, -2, RoundingMode.HALF_EVEN, "1.1"}, {1.099, -2, RoundingMode.HALF_EVEN, "1.1"}, - {1.999, -2, RoundingMode.HALF_EVEN, "2."}, - {2.25, -1, RoundingMode.HALF_EVEN, "2."}, + {1.999, -2, RoundingMode.HALF_EVEN, "2"}, + {2.25, -1, RoundingMode.HALF_EVEN, "2"}, {2.25, -1, RoundingMode.HALF_UP, "2.5"}, {2.75, -1, RoundingMode.HALF_DOWN, "2.5"}, - {2.75, -1, RoundingMode.HALF_EVEN, "3."}, - {3.00, -1, RoundingMode.CEILING, "3."}, + {2.75, -1, RoundingMode.HALF_EVEN, "3"}, + {3.00, -1, RoundingMode.CEILING, "3"}, {3.25, -1, RoundingMode.CEILING, "3.5"}, {3.50, -1, RoundingMode.CEILING, "3.5"}, - {3.75, -1, RoundingMode.CEILING, "4."}, - {4.00, -1, RoundingMode.FLOOR, "4."}, - {4.25, -1, RoundingMode.FLOOR, "4."}, + {3.75, -1, RoundingMode.CEILING, "4"}, + {4.00, -1, RoundingMode.FLOOR, "4"}, + {4.25, -1, RoundingMode.FLOOR, "4"}, {4.50, -1, RoundingMode.FLOOR, "4.5"}, {4.75, -1, RoundingMode.FLOOR, "4.5"}, - {5.00, -1, RoundingMode.UP, "5."}, + {5.00, -1, RoundingMode.UP, "5"}, {5.25, -1, RoundingMode.UP, "5.5"}, {5.50, -1, RoundingMode.UP, "5.5"}, - {5.75, -1, RoundingMode.UP, "6."}, - {6.00, -1, RoundingMode.DOWN, "6."}, - {6.25, -1, RoundingMode.DOWN, "6."}, + {5.75, -1, RoundingMode.UP, "6"}, + {6.00, -1, RoundingMode.DOWN, "6"}, + {6.25, -1, RoundingMode.DOWN, "6"}, {6.50, -1, RoundingMode.DOWN, "6.5"}, {6.75, -1, RoundingMode.DOWN, "6.5"}, - {7.00, -1, RoundingMode.UNNECESSARY, "7."}, + {7.00, -1, RoundingMode.UNNECESSARY, "7"}, {7.50, -1, RoundingMode.UNNECESSARY, "7.5"}, }; for (Object[] cas : cases) { @@ -606,8 +611,272 @@ public class DecimalQuantityTest extends TestFmwk { } } + @Test + public void testCompactDecimalSuppressedExponent() { + ULocale locale = new ULocale("fr-FR"); + + Object[][] casesData = { + // unlocalized formatter skeleton, input, string output, long output, double output, BigDecimal output, plain string, suppressed exponent + {"", 123456789, "123 456 789", 123456789L, 123456789.0, new BigDecimal("123456789"), "123456789", 0}, + {"compact-long", 123456789, "123 millions", 123000000L, 123000000.0, new BigDecimal("123000000"), "123000000", 6}, + {"compact-short", 123456789, "123 M", 123000000L, 123000000.0, new BigDecimal("123000000"), "123000000", 6}, + {"scientific", 123456789, "1,234568E8", 123456800L, 123456800.0, new BigDecimal("123456800"), "123456800", 8}, + + {"", 1234567, "1 234 567", 1234567L, 1234567.0, new BigDecimal("1234567"), "1234567", 0}, + {"compact-long", 1234567, "1,2 million", 1200000L, 1200000.0, new BigDecimal("1200000"), "1200000", 6}, + {"compact-short", 1234567, "1,2 M", 1200000L, 1200000.0, new BigDecimal("1200000"), "1200000", 6}, + {"scientific", 1234567, "1,234567E6", 1234567L, 1234567.0, new BigDecimal("1234567"), "1234567", 6}, + + {"", 123456, "123 456", 123456L, 123456.0, new BigDecimal("123456"), "123456", 0}, + {"compact-long", 123456, "123 mille", 123000L, 123000.0, new BigDecimal("123000"), "123000", 3}, + {"compact-short", 123456, "123 k", 123000L, 123000.0, new BigDecimal("123000"), "123000", 3}, + {"scientific", 123456, "1,23456E5", 123456L, 123456.0, new BigDecimal("123456"), "123456", 5}, + + {"", 123, "123", 123L, 123.0, new BigDecimal("123"), "123", 0}, + {"compact-long", 123, "123", 123L, 123.0, new BigDecimal("123"), "123", 0}, + {"compact-short", 123, "123", 123L, 123.0, new BigDecimal("123"), "123", 0}, + {"scientific", 123, "1,23E2", 123L, 123.0, new BigDecimal("123"), "123", 2}, + + {"", 1.2, "1,2", 1L, 1.2, new BigDecimal("1.2"), "1.2", 0}, + {"compact-long", 1.2, "1,2", 1L, 1.2, new BigDecimal("1.2"), "1.2", 0}, + {"compact-short", 1.2, "1,2", 1L, 1.2, new BigDecimal("1.2"), "1.2", 0}, + {"scientific", 1.2, "1,2E0", 1L, 1.2, new BigDecimal("1.2"), "1.2", 0}, + + {"", 0.12, "0,12", 0L, 0.12, new BigDecimal("0.12"), "0.12", 0}, + {"compact-long", 0.12, "0,12", 0L, 0.12, new BigDecimal("0.12"), "0.12", 0}, + {"compact-short", 0.12, "0,12", 0L, 0.12, new BigDecimal("0.12"), "0.12", 0}, + {"scientific", 0.12, "1,2E-1", 0L, 0.12, new BigDecimal("0.12"), "0.12", -1}, + + {"", 0.012, "0,012", 0L, 0.012, new BigDecimal("0.012"), "0.012", 0}, + {"compact-long", 0.012, "0,012", 0L, 0.012, new BigDecimal("0.012"), "0.012", 0}, + {"compact-short", 0.012, "0,012", 0L, 0.012, new BigDecimal("0.012"), "0.012", 0}, + {"scientific", 0.012, "1,2E-2", 0L, 0.012, new BigDecimal("0.012"), "0.012", -2}, + + {"", 999.9, "999,9", 999L, 999.9, new BigDecimal("999.9"), "999.9", 0}, + {"compact-long", 999.9, "1 millier", 1000L, 1000.0, new BigDecimal("1000"), "1000", 3}, + {"compact-short", 999.9, "1 k", 1000L, 1000.0, new BigDecimal("1000"), "1000", 3}, + {"scientific", 999.9, "9,999E2", 999L, 999.9, new BigDecimal("999.9"), "999.9", 2}, + + {"", 1000.0, "1 000", 1000L, 1000.0, new BigDecimal("1000"), "1000", 0}, + {"compact-long", 1000.0, "1 millier", 1000L, 1000.0, new BigDecimal("1000"), "1000", 3}, + {"compact-short", 1000.0, "1 k", 1000L, 1000.0, new BigDecimal("1000"), "1000", 3}, + {"scientific", 1000.0, "1E3", 1000L, 1000.0, new BigDecimal("1000"), "1000", 3}, + }; + + for (Object[] caseDatum : casesData) { + // test the helper methods used to compute plural operand values + + String skeleton = (String) caseDatum[0]; + LocalizedNumberFormatter formatter = + NumberFormatter.forSkeleton(skeleton) + .locale(locale); + double input = ((Number) caseDatum[1]).doubleValue(); + String expectedString = (String) caseDatum[2]; + long expectedLong = (long) caseDatum[3]; + double expectedDouble = (double) caseDatum[4]; + BigDecimal expectedBigDecimal = (BigDecimal) caseDatum[5]; + String expectedPlainString = (String) caseDatum[6]; + int expectedSuppressedExponent = (int) caseDatum[7]; + + FormattedNumber fn = formatter.format(input); + DecimalQuantity_DualStorageBCD dq = (DecimalQuantity_DualStorageBCD) + fn.getFixedDecimal(); + String actualString = fn.toString(); + long actualLong = dq.toLong(true); + double actualDouble = dq.toDouble(); + BigDecimal actualBigDecimal = dq.toBigDecimal(); + String actualPlainString = dq.toPlainString(); + int actualSuppressedExponent = dq.getExponent(); + + assertEquals( + String.format("formatted number %s toString: %f", skeleton, input), + expectedString, + actualString); + assertEquals( + String.format("compact decimal %s toLong: %f", skeleton, input), + expectedLong, + actualLong); + assertDoubleEquals( + String.format("compact decimal %s toDouble: %f", skeleton, input), + expectedDouble, + actualDouble); + assertBigDecimalEquals( + String.format("compact decimal %s toBigDecimal: %f", skeleton, input), + expectedBigDecimal, + actualBigDecimal); + assertEquals( + String.format("formatted number %s toPlainString: %f", skeleton, input), + expectedPlainString, + actualPlainString); + assertEquals( + String.format("compact decimal %s suppressed exponent: %f", skeleton, input), + expectedSuppressedExponent, + actualSuppressedExponent); + + // test the actual computed values of the plural operands + + double expectedNOperand = expectedDouble; + double expectedIOperand = expectedLong; + double expectedEOperand = expectedSuppressedExponent; + double actualNOperand = dq.getPluralOperand(Operand.n); + double actualIOperand = dq.getPluralOperand(Operand.i); + double actualEOperand = dq.getPluralOperand(Operand.e); + + assertEquals( + String.format("formatted number %s toString: %s", skeleton, input), + expectedString, + actualString); + assertDoubleEquals( + String.format("compact decimal %s n operand: %f", skeleton, input), + expectedNOperand, + actualNOperand); + assertDoubleEquals( + String.format("compact decimal %s i operand: %f", skeleton, input), + expectedIOperand, + actualIOperand); + assertDoubleEquals( + String.format("compact decimal %s e operand: %f", skeleton, input), + expectedEOperand, + actualEOperand); + } + } + + + @Test + public void testCompactNotationFractionPluralOperands() { + ULocale locale = new ULocale("fr-FR"); + LocalizedNumberFormatter formatter = + NumberFormatter.withLocale(locale) + .notation(Notation.compactLong()) + .precision(Precision.fixedFraction(5)) + .scale(Scale.powerOfTen(-1)); + double formatterInput = 12345; + double inputVal = 1234.5; + FormattedNumber fn = formatter.format(formatterInput); + DecimalQuantity_DualStorageBCD dq = (DecimalQuantity_DualStorageBCD) + fn.getFixedDecimal(); + + double expectedNOperand = 1234.5; + double expectedIOperand = 1234; + double expectedFOperand = 50; + double expectedTOperand = 5; + double expectedVOperand = 2; + double expectedWOperand = 1; + double expectedEOperand = 3; + String expectedString = "1,23450 millier"; + double actualNOperand = dq.getPluralOperand(Operand.n); + double actualIOperand = dq.getPluralOperand(Operand.i); + double actualFOperand = dq.getPluralOperand(Operand.f); + double actualTOperand = dq.getPluralOperand(Operand.t); + double actualVOperand = dq.getPluralOperand(Operand.v); + double actualWOperand = dq.getPluralOperand(Operand.w); + double actualEOperand = dq.getPluralOperand(Operand.e); + String actualString = fn.toString(); + + assertDoubleEquals( + String.format("compact decimal fraction n operand: %f", inputVal), + expectedNOperand, + actualNOperand); + assertDoubleEquals( + String.format("compact decimal fraction i operand: %f", inputVal), + expectedIOperand, + actualIOperand); + assertDoubleEquals( + String.format("compact decimal fraction f operand: %f", inputVal), + expectedFOperand, + actualFOperand); + assertDoubleEquals( + String.format("compact decimal fraction t operand: %f", inputVal), + expectedTOperand, + actualTOperand); + assertDoubleEquals( + String.format("compact decimal fraction v operand: %f", inputVal), + expectedVOperand, + actualVOperand); + assertDoubleEquals( + String.format("compact decimal fraction w operand: %f", inputVal), + expectedWOperand, + actualWOperand); + assertDoubleEquals( + String.format("compact decimal fraction e operand: %f", inputVal), + expectedEOperand, + actualEOperand); + assertEquals( + String.format("compact decimal fraction toString: %f", inputVal), + expectedString, + actualString); + } + + @Test + public void testSuppressedExponentUnchangedByInitialScaling() { + ULocale locale = new ULocale("fr-FR"); + LocalizedNumberFormatter withLocale = NumberFormatter.withLocale(locale); + LocalizedNumberFormatter compactLong = + withLocale.notation(Notation.compactLong()); + LocalizedNumberFormatter compactScaled = + compactLong.scale(Scale.powerOfTen(3)); + + Object[][] casesData = { + // input, compact long string output, + // compact n operand, compact i operand, compact e operand + {123456789, "123 millions", 123000000.0, 123000000.0, 6.0}, + {1234567, "1,2 million", 1200000.0, 1200000.0, 6.0}, + {123456, "123 mille", 123000.0, 123000.0, 3.0}, + {123, "123", 123.0, 123.0, 0.0}, + }; + + for (Object[] caseDatum : casesData) { + int input = (int) caseDatum[0]; + String expectedString = (String) caseDatum[1]; + double expectedNOperand = (double) caseDatum[2]; + double expectedIOperand = (double) caseDatum[3]; + double expectedEOperand = (double) caseDatum[4]; + + FormattedNumber fnCompactScaled = compactScaled.format(input); + DecimalQuantity_DualStorageBCD dqCompactScaled = + (DecimalQuantity_DualStorageBCD) fnCompactScaled.getFixedDecimal(); + double compactScaledEOperand = dqCompactScaled.getPluralOperand(Operand.e); + + FormattedNumber fnCompact = compactLong.format(input); + DecimalQuantity_DualStorageBCD dqCompact = + (DecimalQuantity_DualStorageBCD) fnCompact.getFixedDecimal(); + String actualString = fnCompact.toString(); + double compactNOperand = dqCompact.getPluralOperand(Operand.n); + double compactIOperand = dqCompact.getPluralOperand(Operand.i); + double compactEOperand = dqCompact.getPluralOperand(Operand.e); + assertEquals( + String.format("formatted number compactLong toString: %s", input), + expectedString, + actualString); + assertDoubleEquals( + String.format("compact decimal %d, n operand vs. expected", input), + expectedNOperand, + compactNOperand); + assertDoubleEquals( + String.format("compact decimal %d, i operand vs. expected", input), + expectedIOperand, + compactIOperand); + assertDoubleEquals( + String.format("compact decimal %d, e operand vs. expected", input), + expectedEOperand, + compactEOperand); + + // By scaling by 10^3 in a locale that has words / compact notation + // based on powers of 10^3, we guarantee that the suppressed + // exponent will differ by 3. + assertDoubleEquals( + String.format("decimal %d, e operand for compact vs. compact scaled", input), + compactEOperand + 3, + compactScaledEOperand); + } + } + + static boolean doubleEquals(double d1, double d2) { + return (Math.abs(d1 - d2) < 1e-6) || (Math.abs((d1 - d2) / d1) < 1e-6); + } + static void assertDoubleEquals(String message, double d1, double d2) { - boolean equal = (Math.abs(d1 - d2) < 1e-6) || (Math.abs((d1 - d2) / d1) < 1e-6); + boolean equal = doubleEquals(d1, d2); handleAssert(equal, message, d1, d2, null, false); } diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/number/ExhaustiveNumberTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/number/ExhaustiveNumberTest.java index 5edd9579e..a15c038ac 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/number/ExhaustiveNumberTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/number/ExhaustiveNumberTest.java @@ -126,8 +126,6 @@ public class ExhaustiveNumberTest extends TestFmwk { // based on https://github.com/google/double-conversion/issues/28 double[] hardDoubles = { 1651087494906221570.0, - -5074790912492772E-327, - 83602530019752571E-327, 2.207817077636718750000000000000, 1.818351745605468750000000000000, 3.941719055175781250000000000000, @@ -152,9 +150,11 @@ public class ExhaustiveNumberTest extends TestFmwk { 1.305290222167968750000000000000, 3.834922790527343750000000000000, }; - double[] integerDoubles = { + double[] exactDoubles = { 51423, 51423e10, + -5074790912492772E-327, + 83602530019752571E-327, 4.503599627370496E15, 6.789512076111555E15, 9.007199254740991E15, @@ -164,7 +164,7 @@ public class ExhaustiveNumberTest extends TestFmwk { checkDoubleBehavior(d, true, ""); } - for (double d : integerDoubles) { + for (double d : exactDoubles) { checkDoubleBehavior(d, false, ""); } diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/number/MutablePatternModifierTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/number/MutablePatternModifierTest.java index 32a50cc31..27d41de58 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/number/MutablePatternModifierTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/number/MutablePatternModifierTest.java @@ -13,6 +13,7 @@ import android.icu.impl.FormattedStringBuilder; import android.icu.impl.number.DecimalQuantity; import android.icu.impl.number.DecimalQuantity_DualStorageBCD; import android.icu.impl.number.MicroProps; +import android.icu.impl.number.Modifier.Signum; import android.icu.impl.number.MutablePatternModifier; import android.icu.impl.number.PatternStringParser; import android.icu.number.NumberFormatter.SignDisplay; @@ -35,19 +36,22 @@ public class MutablePatternModifierTest { UnitWidth.SHORT, null); - mod.setNumberProperties(1, null); + mod.setNumberProperties(Signum.POS, null); assertEquals("a", getPrefix(mod)); assertEquals("b", getSuffix(mod)); mod.setPatternAttributes(SignDisplay.ALWAYS, false); assertEquals("+a", getPrefix(mod)); assertEquals("b", getSuffix(mod)); - mod.setNumberProperties(0, null); + mod.setNumberProperties(Signum.POS_ZERO, null); assertEquals("+a", getPrefix(mod)); assertEquals("b", getSuffix(mod)); + mod.setNumberProperties(Signum.NEG_ZERO, null); + assertEquals("-a", getPrefix(mod)); + assertEquals("b", getSuffix(mod)); mod.setPatternAttributes(SignDisplay.EXCEPT_ZERO, false); assertEquals("a", getPrefix(mod)); assertEquals("b", getSuffix(mod)); - mod.setNumberProperties(-1, null); + mod.setNumberProperties(Signum.NEG, null); assertEquals("-a", getPrefix(mod)); assertEquals("b", getSuffix(mod)); mod.setPatternAttributes(SignDisplay.NEVER, false); @@ -56,24 +60,27 @@ public class MutablePatternModifierTest { mod.setPatternInfo(PatternStringParser.parseToPatternInfo("a0b;c-0d"), null); mod.setPatternAttributes(SignDisplay.AUTO, false); - mod.setNumberProperties(1, null); + mod.setNumberProperties(Signum.POS, null); assertEquals("a", getPrefix(mod)); assertEquals("b", getSuffix(mod)); mod.setPatternAttributes(SignDisplay.ALWAYS, false); assertEquals("c+", getPrefix(mod)); assertEquals("d", getSuffix(mod)); - mod.setNumberProperties(0, null); + mod.setNumberProperties(Signum.POS_ZERO, null); assertEquals("c+", getPrefix(mod)); assertEquals("d", getSuffix(mod)); + mod.setNumberProperties(Signum.NEG_ZERO, null); + assertEquals("c-", getPrefix(mod)); + assertEquals("d", getSuffix(mod)); mod.setPatternAttributes(SignDisplay.EXCEPT_ZERO, false); assertEquals("a", getPrefix(mod)); assertEquals("b", getSuffix(mod)); - mod.setNumberProperties(-1, null); + mod.setNumberProperties(Signum.NEG, null); assertEquals("c-", getPrefix(mod)); assertEquals("d", getSuffix(mod)); mod.setPatternAttributes(SignDisplay.NEVER, false); - assertEquals("c-", getPrefix(mod)); // TODO: What should this behavior be? - assertEquals("d", getSuffix(mod)); + assertEquals("a", getPrefix(mod)); + assertEquals("b", getSuffix(mod)); } @Test @@ -115,7 +122,7 @@ public class MutablePatternModifierTest { Currency.getInstance("USD"), UnitWidth.SHORT, null); - mod.setNumberProperties(1, null); + mod.setNumberProperties(Signum.POS_ZERO, null); // Unsafe Code Path FormattedStringBuilder nsb = new FormattedStringBuilder(); diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/number/NumberFormatterApiTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/number/NumberFormatterApiTest.java index d89d10571..cb72b5ae8 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/number/NumberFormatterApiTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/number/NumberFormatterApiTest.java @@ -47,6 +47,7 @@ import android.icu.number.Precision; import android.icu.number.Scale; import android.icu.number.ScientificNotation; import android.icu.number.UnlocalizedNumberFormatter; +import android.icu.text.ConstrainedFieldPosition; import android.icu.text.DecimalFormatSymbols; import android.icu.text.NumberFormat; import android.icu.text.NumberingSystem; @@ -69,12 +70,16 @@ public class NumberFormatterApiTest { private static final Currency ESP = Currency.getInstance("ESP"); private static final Currency PTE = Currency.getInstance("PTE"); private static final Currency RON = Currency.getInstance("RON"); + private static final Currency TWD = Currency.getInstance("TWD"); + private static final Currency TRY = Currency.getInstance("TRY"); + private static final Currency CNY = Currency.getInstance("CNY"); @Test public void notationSimple() { assertFormatDescending( "Basic", "", + "", NumberFormatter.with(), ULocale.ENGLISH, "87,650", @@ -90,6 +95,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Big Simple", "notation-simple", + "", NumberFormatter.with().notation(Notation.simple()), ULocale.ENGLISH, "87,650,000", @@ -105,6 +111,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Basic with Negative Sign", "", + "", NumberFormatter.with(), ULocale.ENGLISH, -9876543.21, @@ -116,6 +123,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Scientific", "scientific", + "E0", NumberFormatter.with().notation(Notation.scientific()), ULocale.ENGLISH, "8.765E4", @@ -131,6 +139,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Engineering", "engineering", + "EE0", NumberFormatter.with().notation(Notation.engineering()), ULocale.ENGLISH, "87.65E3", @@ -146,6 +155,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Scientific sign always shown", "scientific/sign-always", + "E+!0", NumberFormatter.with().notation(Notation.scientific().withExponentSignDisplay(SignDisplay.ALWAYS)), ULocale.ENGLISH, "8.765E+4", @@ -160,7 +170,8 @@ public class NumberFormatterApiTest { assertFormatDescending( "Scientific min exponent digits", - "scientific/+ee", + "scientific/*ee", + "E00", NumberFormatter.with().notation(Notation.scientific().withMinExponentDigits(2)), ULocale.ENGLISH, "8.765E04", @@ -176,6 +187,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Scientific Negative", "scientific", + "E0", NumberFormatter.with().notation(Notation.scientific()), ULocale.ENGLISH, -1000000, @@ -184,6 +196,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Scientific Infinity", "scientific", + "E0", NumberFormatter.with().notation(Notation.scientific()), ULocale.ENGLISH, Double.NEGATIVE_INFINITY, @@ -192,6 +205,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Scientific NaN", "scientific", + "E0", NumberFormatter.with().notation(Notation.scientific()), ULocale.ENGLISH, Double.NaN, @@ -203,6 +217,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Compact Short", "compact-short", + "K", NumberFormatter.with().notation(Notation.compactShort()), ULocale.ENGLISH, "88M", @@ -218,6 +233,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Compact Long", "compact-long", + "KK", NumberFormatter.with().notation(Notation.compactLong()), ULocale.ENGLISH, "88 million", @@ -233,6 +249,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Compact Short Currency", "compact-short currency/USD", + "K currency/USD", NumberFormatter.with().notation(Notation.compactShort()).unit(USD), ULocale.ENGLISH, "$88K", @@ -248,6 +265,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Compact Short with ISO Currency", "compact-short currency/USD unit-width-iso-code", + "K currency/USD unit-width-iso-code", NumberFormatter.with().notation(Notation.compactShort()).unit(USD).unitWidth(UnitWidth.ISO_CODE), ULocale.ENGLISH, "USD 88K", @@ -263,6 +281,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Compact Short with Long Name Currency", "compact-short currency/USD unit-width-full-name", + "K currency/USD unit-width-full-name", NumberFormatter.with().notation(Notation.compactShort()).unit(USD).unitWidth(UnitWidth.FULL_NAME), ULocale.ENGLISH, "88K US dollars", @@ -280,6 +299,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Compact Long Currency", "compact-long currency/USD", + "KK currency/USD", NumberFormatter.with().notation(Notation.compactLong()).unit(USD), ULocale.ENGLISH, "$88K", // should be something like "$88 thousand" @@ -297,6 +317,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Compact Long with ISO Currency", "compact-long currency/USD unit-width-iso-code", + "KK currency/USD unit-width-iso-code", NumberFormatter.with().notation(Notation.compactLong()).unit(USD).unitWidth(UnitWidth.ISO_CODE), ULocale.ENGLISH, "USD 88K", // should be something like "USD 88 thousand" @@ -313,6 +334,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Compact Long with Long Name Currency", "compact-long currency/USD unit-width-full-name", + "KK currency/USD unit-width-full-name", NumberFormatter.with().notation(Notation.compactLong()).unit(USD).unitWidth(UnitWidth.FULL_NAME), ULocale.ENGLISH, "88 thousand US dollars", @@ -328,14 +350,25 @@ public class NumberFormatterApiTest { assertFormatSingle( "Compact Plural One", "compact-long", + "KK", NumberFormatter.with().notation(Notation.compactLong()), ULocale.forLanguageTag("es"), 1000000, "1 millón"); assertFormatSingle( + "Compact Plural One with rounding", + "compact-long precision-integer", + "KK precision-integer", + NumberFormatter.with().notation(Notation.compactLong()).precision(Precision.integer()), + ULocale.forLanguageTag("es"), + 1222222, + "1 millón"); + + assertFormatSingle( "Compact Plural Other", "compact-long", + "KK", NumberFormatter.with().notation(Notation.compactLong()), ULocale.forLanguageTag("es"), 2000000, @@ -344,6 +377,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Compact with Negative Sign", "compact-short", + "K", NumberFormatter.with().notation(Notation.compactShort()), ULocale.ENGLISH, -9876543.21, @@ -352,6 +386,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Compact Rounding", "compact-short", + "K", NumberFormatter.with().notation(Notation.compactShort()), ULocale.ENGLISH, 990000, @@ -360,6 +395,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Compact Rounding", "compact-short", + "K", NumberFormatter.with().notation(Notation.compactShort()), ULocale.ENGLISH, 999000, @@ -368,6 +404,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Compact Rounding", "compact-short", + "K", NumberFormatter.with().notation(Notation.compactShort()), ULocale.ENGLISH, 999900, @@ -376,6 +413,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Compact Rounding", "compact-short", + "K", NumberFormatter.with().notation(Notation.compactShort()), ULocale.ENGLISH, 9900000, @@ -384,6 +422,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Compact Rounding", "compact-short", + "K", NumberFormatter.with().notation(Notation.compactShort()), ULocale.ENGLISH, 9990000, @@ -392,6 +431,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Compact in zh-Hant-HK", "compact-short", + "K", NumberFormatter.with().notation(Notation.compactShort()), new ULocale("zh-Hant-HK"), 1e7, @@ -400,6 +440,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Compact in zh-Hant", "compact-short", + "K", NumberFormatter.with().notation(Notation.compactShort()), new ULocale("zh-Hant"), 1e7, @@ -408,6 +449,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Compact Infinity", "compact-short", + "K", NumberFormatter.with().notation(Notation.compactShort()), ULocale.ENGLISH, Double.NEGATIVE_INFINITY, @@ -416,6 +458,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Compact NaN", "compact-short", + "K", NumberFormatter.with().notation(Notation.compactShort()), ULocale.ENGLISH, Double.NaN, @@ -429,6 +472,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Compact Somali No Figure", null, // feature not supported in skeleton + null, NumberFormatter.with().notation(CompactNotation.forCustomData(compactCustomData)), ULocale.ENGLISH, 1000, @@ -440,6 +484,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Meters Short", "measure-unit/length-meter", + "unit/meter", NumberFormatter.with().unit(MeasureUnit.METER), ULocale.ENGLISH, "87,650 m", @@ -455,6 +500,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Meters Long", "measure-unit/length-meter unit-width-full-name", + "unit/meter unit-width-full-name", NumberFormatter.with().unit(MeasureUnit.METER).unitWidth(UnitWidth.FULL_NAME), ULocale.ENGLISH, "87,650 meters", @@ -470,6 +516,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Compact Meters Long", "compact-long measure-unit/length-meter unit-width-full-name", + "KK unit/meter unit-width-full-name", NumberFormatter.with().notation(Notation.compactLong()).unit(MeasureUnit.METER) .unitWidth(UnitWidth.FULL_NAME), ULocale.ENGLISH, @@ -486,6 +533,7 @@ public class NumberFormatterApiTest { assertFormatSingleMeasure( "Meters with Measure Input", "unit-width-full-name", + "unit-width-full-name", NumberFormatter.with().unitWidth(UnitWidth.FULL_NAME), ULocale.ENGLISH, new Measure(5.43, MeasureUnit.METER), @@ -494,6 +542,7 @@ public class NumberFormatterApiTest { assertFormatSingleMeasure( "Measure format method takes precedence over fluent chain", "measure-unit/length-meter", + "unit/meter", NumberFormatter.with().unit(MeasureUnit.METER), ULocale.ENGLISH, new Measure(5.43, USD), @@ -502,6 +551,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Meters with Negative Sign", "measure-unit/length-meter", + "unit/meter", NumberFormatter.with().unit(MeasureUnit.METER), ULocale.ENGLISH, -9876543.21, @@ -511,6 +561,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Interesting Data Fallback 1", "measure-unit/duration-day unit-width-full-name", + "unit/day unit-width-full-name", NumberFormatter.with().unit(MeasureUnit.DAY).unitWidth(UnitWidth.FULL_NAME), ULocale.forLanguageTag("brx"), 5.43, @@ -520,6 +571,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Interesting Data Fallback 2", "measure-unit/duration-day unit-width-narrow", + "unit/day unit-width-narrow", NumberFormatter.with().unit(MeasureUnit.DAY).unitWidth(UnitWidth.NARROW), ULocale.forLanguageTag("brx"), 5.43, @@ -530,15 +582,17 @@ public class NumberFormatterApiTest { assertFormatSingle( "Interesting Data Fallback 3", "measure-unit/area-square-meter unit-width-narrow", + "unit/square-meter unit-width-narrow", NumberFormatter.with().unit(MeasureUnit.SQUARE_METER).unitWidth(UnitWidth.NARROW), ULocale.forLanguageTag("en-GB"), 5.43, - "5.43 m²"); + "5.43m²"); // Try accessing a narrow unit directly from root. assertFormatSingle( "Interesting Data Fallback 4", "measure-unit/area-square-meter unit-width-narrow", + "unit/square-meter unit-width-narrow", NumberFormatter.with().unit(MeasureUnit.SQUARE_METER).unitWidth(UnitWidth.NARROW), ULocale.forLanguageTag("root"), 5.43, @@ -549,6 +603,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "MeasureUnit Difference between Narrow and Short (Narrow Version)", "measure-unit/temperature-fahrenheit unit-width-narrow", + "unit/fahrenheit unit-width-narrow", NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT).unitWidth(UnitWidth.NARROW), ULocale.forLanguageTag("es-US"), 5.43, @@ -557,6 +612,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "MeasureUnit Difference between Narrow and Short (Short Version)", "measure-unit/temperature-fahrenheit unit-width-short", + "unit/fahrenheit unit-width-short", NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT).unitWidth(UnitWidth.SHORT), ULocale.forLanguageTag("es-US"), 5.43, @@ -565,6 +621,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "MeasureUnit form without {0} in CLDR pattern", "measure-unit/temperature-kelvin unit-width-full-name", + "unit/kelvin unit-width-full-name", NumberFormatter.with().unit(MeasureUnit.KELVIN).unitWidth(UnitWidth.FULL_NAME), ULocale.forLanguageTag("es-MX"), 1, @@ -573,6 +630,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "MeasureUnit form without {0} in CLDR pattern and wide base form", "measure-unit/temperature-kelvin .00000000000000000000 unit-width-full-name", + "unit/kelvin .00000000000000000000 unit-width-full-name", NumberFormatter.with() .precision(Precision.fixedFraction(20)) .unit(MeasureUnit.KELVIN) @@ -587,6 +645,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Meters Per Second Short (unit that simplifies) and perUnit method", "measure-unit/length-meter per-measure-unit/duration-second", + "~unit/meter-per-second", // does not round-trip to the full skeleton above NumberFormatter.with().unit(MeasureUnit.METER).perUnit(MeasureUnit.SECOND), ULocale.ENGLISH, "87,650 m/s", @@ -602,6 +661,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Pounds Per Square Mile Short (secondary unit has per-format)", "measure-unit/mass-pound per-measure-unit/area-square-mile", + "unit/pound-per-square-mile", NumberFormatter.with().unit(MeasureUnit.POUND).perUnit(MeasureUnit.SQUARE_MILE), ULocale.ENGLISH, "87,650 lb/mi²", @@ -617,6 +677,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Joules Per Furlong Short (unit with no simplifications or special patterns)", "measure-unit/energy-joule per-measure-unit/length-furlong", + "unit/joule-per-furlong", NumberFormatter.with().unit(MeasureUnit.JOULE).perUnit(MeasureUnit.FURLONG), ULocale.ENGLISH, "87,650 J/fur", @@ -635,6 +696,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Currency", "currency/GBP", + "currency/GBP", NumberFormatter.with().unit(GBP), ULocale.ENGLISH, "£87,650.00", @@ -650,6 +712,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Currency ISO", "currency/GBP unit-width-iso-code", + "currency/GBP unit-width-iso-code", NumberFormatter.with().unit(GBP).unitWidth(UnitWidth.ISO_CODE), ULocale.ENGLISH, "GBP 87,650.00", @@ -665,6 +728,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Currency Long Name", "currency/GBP unit-width-full-name", + "currency/GBP unit-width-full-name", NumberFormatter.with().unit(GBP).unitWidth(UnitWidth.FULL_NAME), ULocale.ENGLISH, "87,650.00 British pounds", @@ -680,6 +744,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Currency Hidden", "currency/GBP unit-width-hidden", + "currency/GBP unit-width-hidden", NumberFormatter.with().unit(GBP).unitWidth(UnitWidth.HIDDEN), ULocale.ENGLISH, "87,650.00", @@ -695,6 +760,7 @@ public class NumberFormatterApiTest { assertFormatSingleMeasure( "Currency with CurrencyAmount Input", "", + "", NumberFormatter.with(), ULocale.ENGLISH, new CurrencyAmount(5.43, GBP), @@ -703,6 +769,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency Long Name from Pattern Syntax", null, + null, NumberFormatter.fromDecimalFormat( PatternStringParser.parseToProperties("0 ¤¤¤"), DecimalFormatSymbols.getInstance(ULocale.ENGLISH), @@ -714,6 +781,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency with Negative Sign", "currency/GBP", + "currency/GBP", NumberFormatter.with().unit(GBP), ULocale.ENGLISH, -9876543.21, @@ -724,6 +792,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency Difference between Narrow and Short (Narrow Version)", "currency/USD unit-width-narrow", + "currency/USD unit-width-narrow", NumberFormatter.with().unit(USD).unitWidth(UnitWidth.NARROW), ULocale.forLanguageTag("en-CA"), 5.43, @@ -732,14 +801,52 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency Difference between Narrow and Short (Short Version)", "currency/USD unit-width-short", + "currency/USD unit-width-short", NumberFormatter.with().unit(USD).unitWidth(UnitWidth.SHORT), ULocale.forLanguageTag("en-CA"), 5.43, "US$5.43"); assertFormatSingle( + "Currency Difference between Formal and Short (Formal Version)", + "currency/TWD unit-width-formal", + "currency/TWD unit-width-formal", + NumberFormatter.with().unit(TWD).unitWidth(UnitWidth.FORMAL), + ULocale.forLanguageTag("zh-TW"), + 5.43, + "NT$5.43"); + + assertFormatSingle( + "Currency Difference between Formal and Short (Short Version)", + "currency/TWD unit-width-short", + "currency/TWD unit-width-short", + NumberFormatter.with().unit(TWD).unitWidth(UnitWidth.SHORT), + ULocale.forLanguageTag("zh-TW"), + 5.43, + "$5.43"); + + assertFormatSingle( + "Currency Difference between Variant and Short (Formal Version)", + "currency/TRY unit-width-variant", + "currency/TRY unit-width-variant", + NumberFormatter.with().unit(TRY).unitWidth(UnitWidth.VARIANT), + ULocale.forLanguageTag("tr-TR"), + 5.43, + "TL\u00A05,43"); + + assertFormatSingle( + "Currency Difference between Variant and Short (Short Version)", + "currency/TRY unit-width-short", + "currency/TRY unit-width-short", + NumberFormatter.with().unit(TRY).unitWidth(UnitWidth.SHORT), + ULocale.forLanguageTag("tr-TR"), + 5.43, + "₺5,43"); + + assertFormatSingle( "Currency-dependent format (Control)", "currency/USD unit-width-short", + "currency/USD unit-width-short", NumberFormatter.with().unit(USD).unitWidth(UnitWidth.SHORT), ULocale.forLanguageTag("ca"), 444444.55, @@ -748,6 +855,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency-dependent format (Test)", "currency/ESP unit-width-short", + "currency/ESP unit-width-short", NumberFormatter.with().unit(ESP).unitWidth(UnitWidth.SHORT), ULocale.forLanguageTag("ca"), 444444.55, @@ -756,6 +864,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency-dependent symbols (Control)", "currency/USD unit-width-short", + "currency/USD unit-width-short", NumberFormatter.with().unit(USD).unitWidth(UnitWidth.SHORT), ULocale.forLanguageTag("pt-PT"), 444444.55, @@ -766,6 +875,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency-dependent symbols (Test Short)", "currency/PTE unit-width-short", + "currency/PTE unit-width-short", NumberFormatter.with().unit(PTE).unitWidth(UnitWidth.SHORT), ULocale.forLanguageTag("pt-PT"), 444444.55, @@ -774,6 +884,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency-dependent symbols (Test Narrow)", "currency/PTE unit-width-narrow", + "currency/PTE unit-width-narrow", NumberFormatter.with().unit(PTE).unitWidth(UnitWidth.NARROW), ULocale.forLanguageTag("pt-PT"), 444444.55, @@ -782,6 +893,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency-dependent symbols (Test ISO Code)", "currency/PTE unit-width-iso-code", + "currency/PTE unit-width-iso-code", NumberFormatter.with().unit(PTE).unitWidth(UnitWidth.ISO_CODE), ULocale.forLanguageTag("pt-PT"), 444444.55, @@ -790,10 +902,20 @@ public class NumberFormatterApiTest { assertFormatSingle( "Plural form depending on visible digits (ICU-20499)", "currency/RON unit-width-full-name", + "currency/RON unit-width-full-name", NumberFormatter.with().unit(RON).unitWidth(UnitWidth.FULL_NAME), ULocale.forLanguageTag("ro-RO"), 24, "24,00 lei românești"); + + assertFormatSingle( + "Currency spacing in suffix (ICU-20954)", + "currency/CNY", + "currency/CNY", + NumberFormatter.with().unit(CNY), + ULocale.forLanguageTag("lu"), + 123.12, + "123,12 CN¥"); } @Test @@ -801,6 +923,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Percent", "percent", + "%", NumberFormatter.with().unit(NoUnit.PERCENT), ULocale.ENGLISH, "87,650%", @@ -816,6 +939,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Permille", "permille", + "permille", NumberFormatter.with().unit(NoUnit.PERMILLE), ULocale.ENGLISH, "87,650‰", @@ -831,6 +955,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "NoUnit Base", "base-unit", + "", NumberFormatter.with().unit(NoUnit.BASE), ULocale.ENGLISH, 51423, @@ -839,6 +964,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Percent with Negative Sign", "percent", + "%", NumberFormatter.with().unit(NoUnit.PERCENT), ULocale.ENGLISH, -98.7654321, @@ -850,6 +976,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Integer", "precision-integer", + ".", NumberFormatter.with().precision(Precision.integer()), ULocale.ENGLISH, "87,650", @@ -865,6 +992,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Fixed Fraction", ".000", + ".000", NumberFormatter.with().precision(Precision.fixedFraction(3)), ULocale.ENGLISH, "87,650.000", @@ -879,6 +1007,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Min Fraction", + ".0*", ".0+", NumberFormatter.with().precision(Precision.minFraction(1)), ULocale.ENGLISH, @@ -895,6 +1024,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Max Fraction", ".#", + ".#", NumberFormatter.with().precision(Precision.maxFraction(1)), ULocale.ENGLISH, "87,650", @@ -910,6 +1040,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Min/Max Fraction", ".0##", + ".0##", NumberFormatter.with().precision(Precision.minMaxFraction(1, 3)), ULocale.ENGLISH, "87,650.0", @@ -928,6 +1059,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Fixed Significant", "@@@", + "@@@", NumberFormatter.with().precision(Precision.fixedSignificantDigits(3)), ULocale.ENGLISH, -98, @@ -936,6 +1068,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Fixed Significant Rounding", "@@@", + "@@@", NumberFormatter.with().precision(Precision.fixedSignificantDigits(3)), ULocale.ENGLISH, -98.7654321, @@ -944,6 +1077,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Fixed Significant Zero", "@@@", + "@@@", NumberFormatter.with().precision(Precision.fixedSignificantDigits(3)), ULocale.ENGLISH, 0, @@ -951,6 +1085,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Min Significant", + "@@*", "@@+", NumberFormatter.with().precision(Precision.minSignificantDigits(2)), ULocale.ENGLISH, @@ -960,6 +1095,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Max Significant", "@###", + "@###", NumberFormatter.with().precision(Precision.maxSignificantDigits(4)), ULocale.ENGLISH, 98.7654321, @@ -968,6 +1104,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Min/Max Significant", "@@@#", + "@@@#", NumberFormatter.with().precision(Precision.minMaxSignificantDigits(3, 4)), ULocale.ENGLISH, 9.99999, @@ -975,6 +1112,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Fixed Significant on zero with zero integer width", + "@ integer-width/*", "@ integer-width/+", NumberFormatter.with().precision(Precision.fixedSignificantDigits(1)).integerWidth(IntegerWidth.zeroFillTo(0)), ULocale.ENGLISH, @@ -984,6 +1122,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Fixed Significant on zero with lots of integer width", "@ integer-width/+000", + "@ 000", NumberFormatter.with().precision(Precision.fixedSignificantDigits(1)).integerWidth(IntegerWidth.zeroFillTo(3)), ULocale.ENGLISH, 0, @@ -995,6 +1134,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Basic Significant", // for comparison "@#", + "@#", NumberFormatter.with().precision(Precision.maxSignificantDigits(2)), ULocale.ENGLISH, "88,000", @@ -1009,6 +1149,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "FracSig minMaxFrac minSig", + ".0#/@@@*", ".0#/@@@+", NumberFormatter.with().precision(Precision.minMaxFraction(1, 2).withMinDigits(3)), ULocale.ENGLISH, @@ -1025,6 +1166,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "FracSig minMaxFrac maxSig A", ".0##/@#", + ".0##/@#", NumberFormatter.with().precision(Precision.minMaxFraction(1, 3).withMaxDigits(2)), ULocale.ENGLISH, "88,000.0", // maxSig beats maxFrac @@ -1040,6 +1182,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "FracSig minMaxFrac maxSig B", ".00/@#", + ".00/@#", NumberFormatter.with().precision(Precision.fixedFraction(2).withMaxDigits(2)), ULocale.ENGLISH, "88,000.00", // maxSig beats maxFrac @@ -1055,6 +1198,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "FracSig minFrac maxSig", ".0+/@#", + ".0+/@#", NumberFormatter.with().precision(Precision.minFraction(1).withMaxDigits(2)), ULocale.ENGLISH, "88,000.0", @@ -1069,6 +1213,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "FracSig with trailing zeros A", + ".00/@@@*", ".00/@@@+", NumberFormatter.with().precision(Precision.fixedFraction(2).withMinDigits(3)), ULocale.ENGLISH, @@ -1077,6 +1222,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "FracSig with trailing zeros B", + ".00/@@@*", ".00/@@@+", NumberFormatter.with().precision(Precision.fixedFraction(2).withMinDigits(3)), ULocale.ENGLISH, @@ -1089,6 +1235,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Rounding None", "precision-unlimited", + ".+", NumberFormatter.with().precision(Precision.unlimited()), ULocale.ENGLISH, "87,650", @@ -1104,6 +1251,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Increment", "precision-increment/0.5", + "precision-increment/0.5", NumberFormatter.with().precision(Precision.increment(BigDecimal.valueOf(0.5))), ULocale.ENGLISH, "87,650.0", @@ -1119,6 +1267,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Increment with Min Fraction", "precision-increment/0.50", + "precision-increment/0.50", NumberFormatter.with().precision(Precision.increment(new BigDecimal("0.50"))), ULocale.ENGLISH, "87,650.00", @@ -1134,6 +1283,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Strange Increment", "precision-increment/3.140", + "precision-increment/3.140", NumberFormatter.with().precision(Precision.increment(new BigDecimal("3.140"))), ULocale.ENGLISH, "87,649.960", @@ -1149,6 +1299,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Increment Resolving to Power of 10", "precision-increment/0.010", + "precision-increment/0.010", NumberFormatter.with().precision(Precision.increment(new BigDecimal("0.010"))), ULocale.ENGLISH, "87,650.000", @@ -1164,6 +1315,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Currency Standard", "currency/CZK precision-currency-standard", + "currency/CZK precision-currency-standard", NumberFormatter.with().precision(Precision.currency(CurrencyUsage.STANDARD)).unit(CZK), ULocale.ENGLISH, "CZK 87,650.00", @@ -1179,6 +1331,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Currency Cash", "currency/CZK precision-currency-cash", + "currency/CZK precision-currency-cash", NumberFormatter.with().precision(Precision.currency(CurrencyUsage.CASH)).unit(CZK), ULocale.ENGLISH, "CZK 87,650", @@ -1194,6 +1347,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Currency Cash with Nickel Rounding", "currency/CAD precision-currency-cash", + "currency/CAD precision-currency-cash", NumberFormatter.with().precision(Precision.currency(CurrencyUsage.CASH)).unit(CAD), ULocale.ENGLISH, "CA$87,650.00", @@ -1209,6 +1363,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Currency not in top-level fluent chain", "precision-integer", // calling .withCurrency() applies currency rounding rules immediately + ".", NumberFormatter.with().precision(Precision.currency(CurrencyUsage.CASH).withCurrency(CZK)), ULocale.ENGLISH, "87,650", @@ -1225,6 +1380,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Rounding Mode CEILING", "precision-integer rounding-mode-ceiling", + ". rounding-mode-ceiling", NumberFormatter.with().precision(Precision.integer()).roundingMode(RoundingMode.CEILING), ULocale.ENGLISH, "87,650", @@ -1236,6 +1392,24 @@ public class NumberFormatterApiTest { "1", "1", "0"); + + assertFormatSingle( + "ICU-20974 Double.MIN_NORMAL", + "scientific", + "E0", + NumberFormatter.with().notation(Notation.scientific()), + ULocale.ENGLISH, + Double.MIN_NORMAL, + "2.225074E-308"); + + assertFormatSingle( + "ICU-20974 Double.MIN_VALUE", + "scientific", + "E0", + NumberFormatter.with().notation(Notation.scientific()), + ULocale.ENGLISH, + Double.MIN_VALUE, + "4.9E-324"); } @Test @@ -1243,6 +1417,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Western Grouping", "group-auto", + "", NumberFormatter.with().grouping(GroupingStrategy.AUTO), ULocale.ENGLISH, "87,650,000", @@ -1258,6 +1433,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Indic Grouping", "group-auto", + "", NumberFormatter.with().grouping(GroupingStrategy.AUTO), new ULocale("en-IN"), "8,76,50,000", @@ -1273,6 +1449,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Western Grouping, Min 2", "group-min2", + ",?", NumberFormatter.with().grouping(GroupingStrategy.MIN2), ULocale.ENGLISH, "87,650,000", @@ -1288,6 +1465,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Indic Grouping, Min 2", "group-min2", + ",?", NumberFormatter.with().grouping(GroupingStrategy.MIN2), new ULocale("en-IN"), "8,76,50,000", @@ -1303,6 +1481,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "No Grouping", "group-off", + ",_", NumberFormatter.with().grouping(GroupingStrategy.OFF), new ULocale("en-IN"), "87650000", @@ -1318,6 +1497,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Indic locale with THOUSANDS grouping", "group-thousands", + "group-thousands", NumberFormatter.with().grouping(GroupingStrategy.THOUSANDS), new ULocale("en-IN"), "87,650,000", @@ -1336,6 +1516,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Polish Grouping", "group-auto", + "", NumberFormatter.with().grouping(GroupingStrategy.AUTO), new ULocale("pl"), "87 650 000", @@ -1351,6 +1532,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Polish Grouping, Min 2", "group-min2", + ",?", NumberFormatter.with().grouping(GroupingStrategy.MIN2), new ULocale("pl"), "87 650 000", @@ -1366,6 +1548,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Polish Grouping, Always", "group-on-aligned", + ",!", NumberFormatter.with().grouping(GroupingStrategy.ON_ALIGNED), new ULocale("pl"), "87 650 000", @@ -1383,6 +1566,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Bulgarian Currency Grouping", "currency/USD group-auto", + "currency/USD", NumberFormatter.with().grouping(GroupingStrategy.AUTO).unit(USD), new ULocale("bg"), "87650000,00 щ.д.", @@ -1398,6 +1582,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Bulgarian Currency Grouping, Always", "currency/USD group-on-aligned", + "currency/USD ,!", NumberFormatter.with().grouping(GroupingStrategy.ON_ALIGNED).unit(USD), new ULocale("bg"), "87 650 000,00 щ.д.", @@ -1415,6 +1600,7 @@ public class NumberFormatterApiTest { assertFormatDescendingBig( "Custom Grouping via Internal API", null, + null, NumberFormatter.with().macros(macros), ULocale.ENGLISH, "8,7,6,5,0000", @@ -1433,6 +1619,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Padding", null, + null, NumberFormatter.with().padding(Padder.none()), ULocale.ENGLISH, "87,650", @@ -1448,6 +1635,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Padding", null, + null, NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)), ULocale.ENGLISH, "**87,650", @@ -1463,6 +1651,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Padding with code points", null, + null, NumberFormatter.with().padding(Padder.codePoints(0x101E4, 8, PadPosition.AFTER_PREFIX)), ULocale.ENGLISH, "𐇤𐇤87,650", @@ -1478,6 +1667,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Padding with wide digits", null, + null, NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)) .symbols(NumberingSystem.getInstanceByName("mathsanb")), ULocale.ENGLISH, @@ -1494,6 +1684,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Padding with currency spacing", null, + null, NumberFormatter.with().padding(Padder.codePoints('*', 10, PadPosition.AFTER_PREFIX)).unit(GBP) .unitWidth(UnitWidth.ISO_CODE), ULocale.ENGLISH, @@ -1510,6 +1701,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Pad Before Prefix", null, + null, NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.BEFORE_PREFIX)), ULocale.ENGLISH, -88.88, @@ -1518,6 +1710,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Pad After Prefix", null, + null, NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)), ULocale.ENGLISH, -88.88, @@ -1526,6 +1719,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Pad Before Suffix", null, + null, NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.BEFORE_SUFFIX)) .unit(NoUnit.PERCENT), ULocale.ENGLISH, @@ -1535,6 +1729,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Pad After Suffix", null, + null, NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_SUFFIX)) .unit(NoUnit.PERCENT), ULocale.ENGLISH, @@ -1544,6 +1739,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency Spacing with Zero Digit Padding Broken", null, + null, NumberFormatter.with().padding(Padder.codePoints('0', 12, PadPosition.AFTER_PREFIX)).unit(GBP) .unitWidth(UnitWidth.ISO_CODE), ULocale.ENGLISH, @@ -1556,6 +1752,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Integer Width Default", "integer-width/+0", + "0", NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1)), ULocale.ENGLISH, "87,650", @@ -1570,6 +1767,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Integer Width Zero Fill 0", + "integer-width/*", "integer-width/+", NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(0)), ULocale.ENGLISH, @@ -1581,11 +1779,12 @@ public class NumberFormatterApiTest { ".8765", ".08765", ".008765", - ""); // TODO: Avoid the empty string here? + "0"); // see ICU-20844 assertFormatDescending( "Integer Width Zero Fill 3", "integer-width/+000", + "000", NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(3)), ULocale.ENGLISH, "87,650", @@ -1601,6 +1800,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Integer Width Max 3", "integer-width/##0", + "integer-width/##0", NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1).truncateAt(3)), ULocale.ENGLISH, "650", @@ -1616,6 +1816,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Integer Width Fixed 2", "integer-width/00", + "integer-width/00", NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2).truncateAt(2)), ULocale.ENGLISH, "50", @@ -1628,9 +1829,64 @@ public class NumberFormatterApiTest { "00.008765", "00"); + assertFormatDescending( + "Integer Width Compact", + "compact-short integer-width/000", + "K integer-width/000", + NumberFormatter.with() + .notation(Notation.compactShort()) + .integerWidth(IntegerWidth.zeroFillTo(3).truncateAt(3)), + ULocale.ENGLISH, + "088K", + "008.8K", + "876", + "088", + "008.8", + "000.88", + "000.088", + "000.0088", + "000"); + + assertFormatDescending( + "Integer Width Scientific", + "scientific integer-width/000", + "E0 integer-width/000", + NumberFormatter.with() + .notation(Notation.scientific()) + .integerWidth(IntegerWidth.zeroFillTo(3).truncateAt(3)), + ULocale.ENGLISH, + "008.765E4", + "008.765E3", + "008.765E2", + "008.765E1", + "008.765E0", + "008.765E-1", + "008.765E-2", + "008.765E-3", + "000E0"); + + assertFormatDescending( + "Integer Width Engineering", + "engineering integer-width/000", + "EE0 integer-width/000", + NumberFormatter.with() + .notation(Notation.engineering()) + .integerWidth(IntegerWidth.zeroFillTo(3).truncateAt(3)), + ULocale.ENGLISH, + "087.65E3", + "008.765E3", + "876.5E0", + "087.65E0", + "008.765E0", + "876.5E-3", + "087.65E-3", + "008.765E-3", + "000E0"); + assertFormatSingle( "Integer Width Remove All A", "integer-width/00", + "integer-width/00", NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2).truncateAt(2)), ULocale.ENGLISH, 2500, @@ -1639,6 +1895,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Integer Width Remove All B", "integer-width/00", + "integer-width/00", NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2).truncateAt(2)), ULocale.ENGLISH, 25000, @@ -1647,6 +1904,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Integer Width Remove All B, Bytes Mode", "integer-width/00", + "integer-width/00", NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2).truncateAt(2)), ULocale.ENGLISH, // Note: this double produces all 17 significant digits @@ -1659,6 +1917,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "French Symbols with Japanese Data 1", null, + null, NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)), ULocale.JAPAN, "87\u202F650", @@ -1674,6 +1933,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "French Symbols with Japanese Data 2", null, + null, NumberFormatter.with().notation(Notation.compactShort()) .symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)), ULocale.JAPAN, @@ -1683,6 +1943,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Latin Numbering System with Arabic Data", "currency/USD latin", + "currency/USD latin", NumberFormatter.with().symbols(NumberingSystem.LATIN).unit(USD), new ULocale("ar"), "US$ 87,650.00", @@ -1698,6 +1959,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Math Numbering System with French Data", "numbering-system/mathsanb", + "numbering-system/mathsanb", NumberFormatter.with().symbols(NumberingSystem.getInstanceByName("mathsanb")), ULocale.FRENCH, "𝟴𝟳\u202f𝟲𝟱𝟬", @@ -1713,6 +1975,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Swiss Symbols (used in documentation)", null, + null, NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(new ULocale("de-CH"))), ULocale.ENGLISH, 12345.67, @@ -1721,6 +1984,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Myanmar Symbols (used in documentation)", null, + null, NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(new ULocale("my_MY"))), ULocale.ENGLISH, 12345.67, @@ -1731,6 +1995,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency symbol should precede number in ar with NS latn", "currency/USD latin", + "currency/USD latin", NumberFormatter.with().symbols(NumberingSystem.LATIN).unit(USD), new ULocale("ar"), 12345.67, @@ -1739,6 +2004,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency symbol should precede number in ar@numbers=latn", "currency/USD", + "currency/USD", NumberFormatter.with().unit(USD), new ULocale("ar@numbers=latn"), 12345.67, @@ -1747,6 +2013,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency symbol should follow number in ar-EG with NS arab", "currency/USD", + "currency/USD", NumberFormatter.with().unit(USD), new ULocale("ar-EG"), 12345.67, @@ -1755,6 +2022,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Currency symbol should follow number in ar@numbers=arab", "currency/USD", + "currency/USD", NumberFormatter.with().unit(USD), new ULocale("ar@numbers=arab"), 12345.67, @@ -1763,6 +2031,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "NumberingSystem in API should win over @numbers keyword", "currency/USD latin", + "currency/USD latin", NumberFormatter.with().symbols(NumberingSystem.LATIN).unit(USD), new ULocale("ar@numbers=arab"), 12345.67, @@ -1782,6 +2051,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Symbols object should be copied", null, + null, f, ULocale.ENGLISH, 12345.67, @@ -1790,6 +2060,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "The last symbols setter wins", "latin", + "latin", NumberFormatter.with().symbols(symbols).symbols(NumberingSystem.LATIN), ULocale.ENGLISH, 12345.67, @@ -1798,6 +2069,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "The last symbols setter wins", null, + null, NumberFormatter.with().symbols(NumberingSystem.LATIN).symbols(symbols), ULocale.ENGLISH, 12345.67, @@ -1813,6 +2085,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Custom Short Currency Symbol", "$XXX", + "$XXX", NumberFormatter.with().unit(Currency.getInstance("XXX")).symbols(dfs), ULocale.ENGLISH, 12.3, @@ -1824,6 +2097,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Auto Positive", "sign-auto", + "", NumberFormatter.with().sign(SignDisplay.AUTO), ULocale.ENGLISH, 444444, @@ -1832,6 +2106,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Auto Negative", "sign-auto", + "", NumberFormatter.with().sign(SignDisplay.AUTO), ULocale.ENGLISH, -444444, @@ -1840,6 +2115,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Auto Zero", "sign-auto", + "", NumberFormatter.with().sign(SignDisplay.AUTO), ULocale.ENGLISH, 0, @@ -1848,6 +2124,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Always Positive", "sign-always", + "+!", NumberFormatter.with().sign(SignDisplay.ALWAYS), ULocale.ENGLISH, 444444, @@ -1856,6 +2133,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Always Negative", "sign-always", + "+!", NumberFormatter.with().sign(SignDisplay.ALWAYS), ULocale.ENGLISH, -444444, @@ -1864,6 +2142,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Always Zero", "sign-always", + "+!", NumberFormatter.with().sign(SignDisplay.ALWAYS), ULocale.ENGLISH, 0, @@ -1872,6 +2151,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Never Positive", "sign-never", + "+_", NumberFormatter.with().sign(SignDisplay.NEVER), ULocale.ENGLISH, 444444, @@ -1880,6 +2160,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Never Negative", "sign-never", + "+_", NumberFormatter.with().sign(SignDisplay.NEVER), ULocale.ENGLISH, -444444, @@ -1888,6 +2169,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Never Zero", "sign-never", + "+_", NumberFormatter.with().sign(SignDisplay.NEVER), ULocale.ENGLISH, 0, @@ -1896,6 +2178,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting Positive", "currency/USD sign-accounting", + "currency/USD ()", NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD), ULocale.ENGLISH, 444444, @@ -1904,6 +2187,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting Negative", "currency/USD sign-accounting", + "currency/USD ()", NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD), ULocale.ENGLISH, -444444, @@ -1912,6 +2196,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting Zero", "currency/USD sign-accounting", + "currency/USD ()", NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD), ULocale.ENGLISH, 0, @@ -1920,6 +2205,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting-Always Positive", "currency/USD sign-accounting-always", + "currency/USD ()!", NumberFormatter.with().sign(SignDisplay.ACCOUNTING_ALWAYS).unit(USD), ULocale.ENGLISH, 444444, @@ -1928,6 +2214,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting-Always Negative", "currency/USD sign-accounting-always", + "currency/USD ()!", NumberFormatter.with().sign(SignDisplay.ACCOUNTING_ALWAYS).unit(USD), ULocale.ENGLISH, -444444, @@ -1936,6 +2223,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting-Always Zero", "currency/USD sign-accounting-always", + "currency/USD ()!", NumberFormatter.with().sign(SignDisplay.ACCOUNTING_ALWAYS).unit(USD), ULocale.ENGLISH, 0, @@ -1944,6 +2232,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Except-Zero Positive", "sign-except-zero", + "+?", NumberFormatter.with().sign(SignDisplay.EXCEPT_ZERO), ULocale.ENGLISH, 444444, @@ -1952,6 +2241,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Except-Zero Negative", "sign-except-zero", + "+?", NumberFormatter.with().sign(SignDisplay.EXCEPT_ZERO), ULocale.ENGLISH, -444444, @@ -1960,6 +2250,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Except-Zero Zero", "sign-except-zero", + "+?", NumberFormatter.with().sign(SignDisplay.EXCEPT_ZERO), ULocale.ENGLISH, 0, @@ -1968,6 +2259,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting-Except-Zero Positive", "currency/USD sign-accounting-except-zero", + "currency/USD ()?", NumberFormatter.with().sign(SignDisplay.ACCOUNTING_EXCEPT_ZERO).unit(USD), ULocale.ENGLISH, 444444, @@ -1976,6 +2268,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting-Except-Zero Negative", "currency/USD sign-accounting-except-zero", + "currency/USD ()?", NumberFormatter.with().sign(SignDisplay.ACCOUNTING_EXCEPT_ZERO).unit(USD), ULocale.ENGLISH, -444444, @@ -1984,6 +2277,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting-Except-Zero Zero", "currency/USD sign-accounting-except-zero", + "currency/USD ()?", NumberFormatter.with().sign(SignDisplay.ACCOUNTING_EXCEPT_ZERO).unit(USD), ULocale.ENGLISH, 0, @@ -1992,6 +2286,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting Negative Hidden", "currency/USD unit-width-hidden sign-accounting", + "currency/USD unit-width-hidden ()", NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD).unitWidth(UnitWidth.HIDDEN), ULocale.ENGLISH, -444444, @@ -2000,6 +2295,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting Negative Narrow", "currency/USD unit-width-narrow sign-accounting", + "currency/USD unit-width-narrow ()", NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD).unitWidth(UnitWidth.NARROW), ULocale.CANADA, -444444, @@ -2008,6 +2304,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting Negative Short", "currency/USD sign-accounting", + "currency/USD ()", NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD).unitWidth(UnitWidth.SHORT), ULocale.CANADA, -444444, @@ -2016,6 +2313,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting Negative Iso Code", "currency/USD unit-width-iso-code sign-accounting", + "currency/USD unit-width-iso-code ()", NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD).unitWidth(UnitWidth.ISO_CODE), ULocale.CANADA, -444444, @@ -2026,6 +2324,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Sign Accounting Negative Full Name", "currency/USD unit-width-full-name sign-accounting", + "currency/USD unit-width-full-name ()", NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD).unitWidth(UnitWidth.FULL_NAME), ULocale.CANADA, -444444, @@ -2033,13 +2332,52 @@ public class NumberFormatterApiTest { } @Test + public void signNearZero() { + // https://unicode-org.atlassian.net/browse/ICU-20709 + Object[][] cases = { + { SignDisplay.AUTO, 1.1, "1" }, + { SignDisplay.AUTO, 0.9, "1" }, + { SignDisplay.AUTO, 0.1, "0" }, + { SignDisplay.AUTO, -0.1, "-0" }, // interesting case + { SignDisplay.AUTO, -0.9, "-1" }, + { SignDisplay.AUTO, -1.1, "-1" }, + { SignDisplay.ALWAYS, 1.1, "+1" }, + { SignDisplay.ALWAYS, 0.9, "+1" }, + { SignDisplay.ALWAYS, 0.1, "+0" }, + { SignDisplay.ALWAYS, -0.1, "-0" }, + { SignDisplay.ALWAYS, -0.9, "-1" }, + { SignDisplay.ALWAYS, -1.1, "-1" }, + { SignDisplay.EXCEPT_ZERO, 1.1, "+1" }, + { SignDisplay.EXCEPT_ZERO, 0.9, "+1" }, + { SignDisplay.EXCEPT_ZERO, 0.1, "0" }, // interesting case + { SignDisplay.EXCEPT_ZERO, -0.1, "0" }, // interesting case + { SignDisplay.EXCEPT_ZERO, -0.9, "-1" }, + { SignDisplay.EXCEPT_ZERO, -1.1, "-1" }, + }; + for (Object[] cas : cases) { + SignDisplay sign = (SignDisplay) cas[0]; + double input = (Double) cas[1]; + String expected = (String) cas[2]; + String actual = NumberFormatter.with() + .sign(sign) + .precision(Precision.integer()) + .locale(Locale.US) + .format(input) + .toString(); + assertEquals( + input + " @ SignDisplay " + sign, + expected, actual); + } + } + + @Test public void signCoverage() { // https://unicode-org.atlassian.net/browse/ICU-20708 Object[][][] cases = new Object[][][] { { {SignDisplay.AUTO}, { "-∞", "-1", "-0", "0", "1", "∞", "NaN", "-NaN" } }, { {SignDisplay.ALWAYS}, { "-∞", "-1", "-0", "+0", "+1", "+∞", "+NaN", "-NaN" } }, { {SignDisplay.NEVER}, { "∞", "1", "0", "0", "1", "∞", "NaN", "NaN" } }, - { {SignDisplay.EXCEPT_ZERO}, { "-∞", "-1", "-0", "0", "+1", "+∞", "NaN", "-NaN" } }, + { {SignDisplay.EXCEPT_ZERO}, { "-∞", "-1", "0", "0", "+1", "+∞", "NaN", "NaN" } }, }; double negNaN = Math.copySign(Double.NaN, -0.0); double inputs[] = new double[] { @@ -2067,6 +2405,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Decimal Default", "decimal-auto", + "", NumberFormatter.with().decimal(DecimalSeparatorDisplay.AUTO), ULocale.ENGLISH, "87,650", @@ -2082,6 +2421,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Decimal Always Shown", "decimal-always", + "decimal-always", NumberFormatter.with().decimal(DecimalSeparatorDisplay.ALWAYS), ULocale.ENGLISH, "87,650.", @@ -2100,6 +2440,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Multiplier None", "scale/1", + "", NumberFormatter.with().scale(Scale.none()), ULocale.ENGLISH, "87,650", @@ -2115,6 +2456,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Multiplier Power of Ten", "scale/1000000", + "scale/1000000", NumberFormatter.with().scale(Scale.powerOfTen(6)), ULocale.ENGLISH, "87,650,000,000", @@ -2130,6 +2472,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Multiplier Arbitrary Double", "scale/5.2", + "scale/5.2", NumberFormatter.with().scale(Scale.byDouble(5.2)), ULocale.ENGLISH, "455,780", @@ -2145,6 +2488,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Multiplier Arbitrary BigDecimal", "scale/5.2", + "scale/5.2", NumberFormatter.with().scale(Scale.byBigDecimal(new BigDecimal("5.2"))), ULocale.ENGLISH, "455,780", @@ -2160,6 +2504,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Multiplier Arbitrary Double And Power Of Ten", "scale/5200", + "scale/5200", NumberFormatter.with().scale(Scale.byDoubleAndPowerOfTen(5.2, 3)), ULocale.ENGLISH, "455,780,000", @@ -2175,6 +2520,7 @@ public class NumberFormatterApiTest { assertFormatDescending( "Multiplier Zero", "scale/0", + "scale/0", NumberFormatter.with().scale(Scale.byDouble(0)), ULocale.ENGLISH, "0", @@ -2190,6 +2536,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Multiplier Skeleton Scientific Notation and Percent", "percent scale/1E2", + "%x100", NumberFormatter.with().unit(NoUnit.PERCENT).scale(Scale.powerOfTen(2)), ULocale.ENGLISH, 0.5, @@ -2198,6 +2545,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Negative Multiplier", "scale/-5.2", + "scale/-5.2", NumberFormatter.with().scale(Scale.byDouble(-5.2)), ULocale.ENGLISH, 2, @@ -2206,6 +2554,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Negative One Multiplier", "scale/-1", + "scale/-1", NumberFormatter.with().scale(Scale.byDouble(-1)), ULocale.ENGLISH, 444444, @@ -2214,6 +2563,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Two-Type Multiplier with Overlap", "scale/10000", + "scale/10000", NumberFormatter.with().scale(Scale.byDoubleAndPowerOfTen(100, 2)), ULocale.ENGLISH, 2, @@ -2259,6 +2609,7 @@ public class NumberFormatterApiTest { FormattedNumber fmtd = assertFormatSingle( message, "", + "", NumberFormatter.with(), ULocale.ENGLISH, -9876543210.12, @@ -2276,9 +2627,10 @@ public class NumberFormatterApiTest { assertNumberFieldPositions(message, fmtd, expectedFieldPositions); // Test the iteration functionality of nextFieldPosition - FieldPosition actual = new FieldPosition(NumberFormat.Field.GROUPING_SEPARATOR); + ConstrainedFieldPosition actual = new ConstrainedFieldPosition(); + actual.constrainField(NumberFormat.Field.GROUPING_SEPARATOR); int i = 1; - while (fmtd.nextFieldPosition(actual)) { + while (fmtd.nextPosition(actual)) { Object[] cas = expectedFieldPositions[i++]; NumberFormat.Field expectedField = (NumberFormat.Field) cas[0]; int expectedBeginIndex = (Integer) cas[1]; @@ -2287,22 +2639,23 @@ public class NumberFormatterApiTest { assertEquals( "Next for grouping, field, case #" + i, expectedField, - actual.getFieldAttribute()); + actual.getField()); assertEquals( "Next for grouping, begin index, case #" + i, expectedBeginIndex, - actual.getBeginIndex()); + actual.getStart()); assertEquals( "Next for grouping, end index, case #" + i, expectedEndIndex, - actual.getEndIndex()); + actual.getLimit()); } assertEquals("Should have seen all grouping separators", 4, i); // Make sure strings without fraction do not contain fraction field - actual = new FieldPosition(NumberFormat.Field.FRACTION); + actual.reset(); + actual.constrainField(NumberFormat.Field.FRACTION); fmtd = NumberFormatter.withLocale(ULocale.ENGLISH).format(5); - assertFalse("No fraction part in an integer", fmtd.nextFieldPosition(actual)); + assertFalse("No fraction part in an integer", fmtd.nextPosition(actual)); } @Test @@ -2312,6 +2665,7 @@ public class NumberFormatterApiTest { FormattedNumber result = assertFormatSingle( message, "measure-unit/temperature-fahrenheit", + "unit/fahrenheit", NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT), ULocale.ENGLISH, 68, @@ -2331,6 +2685,7 @@ public class NumberFormatterApiTest { FormattedNumber result = assertFormatSingle( message, "measure-unit/temperature-fahrenheit per-measure-unit/duration-day", + "unit/fahrenheit-per-day", NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT).perUnit(MeasureUnit.DAY), ULocale.ENGLISH, 68, @@ -2350,6 +2705,7 @@ public class NumberFormatterApiTest { FormattedNumber result = assertFormatSingle( message, "measure-unit/length-meter unit-width-full-name", + "unit/meter unit-width-full-name", NumberFormatter.with().unit(MeasureUnit.METER).unitWidth(UnitWidth.FULL_NAME), ULocale.ENGLISH, 68, @@ -2370,6 +2726,7 @@ public class NumberFormatterApiTest { FormattedNumber result = assertFormatSingle( message, "measure-unit/length-meter per-measure-unit/duration-second unit-width-full-name", + "~unit/meter-per-second unit-width-full-name", // does not round-trip to the full skeleton above NumberFormatter.with().unit(MeasureUnit.METER).perUnit(MeasureUnit.SECOND).unitWidth(UnitWidth.FULL_NAME), new ULocale("ky"), // locale with the interesting data 68, @@ -2390,6 +2747,7 @@ public class NumberFormatterApiTest { FormattedNumber result = assertFormatSingle( message, "measure-unit/temperature-fahrenheit unit-width-full-name", + "unit/fahrenheit unit-width-full-name", NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT).unitWidth(UnitWidth.FULL_NAME), new ULocale("vi"), // locale with the interesting data 68, @@ -2413,6 +2771,7 @@ public class NumberFormatterApiTest { FormattedNumber result = assertFormatSingle( message, "measure-unit/temperature-kelvin", + "unit/kelvin", NumberFormatter.with().unit(MeasureUnit.KELVIN), new ULocale("fa"), // locale with the interesting data 68, @@ -2432,6 +2791,7 @@ public class NumberFormatterApiTest { FormattedNumber result = assertFormatSingle( message, "compact-short", + "K", NumberFormatter.with().notation(Notation.compactShort()), ULocale.US, 65000, @@ -2451,6 +2811,7 @@ public class NumberFormatterApiTest { FormattedNumber result = assertFormatSingle( message, "compact-long", + "KK", NumberFormatter.with().notation(Notation.compactLong()), ULocale.US, 65000, @@ -2470,6 +2831,7 @@ public class NumberFormatterApiTest { FormattedNumber result = assertFormatSingle( message, "compact-long", + "KK", NumberFormatter.with().notation(Notation.compactLong()), new ULocale("fil"), // locale with interesting data 6000, @@ -2489,6 +2851,7 @@ public class NumberFormatterApiTest { FormattedNumber result = assertFormatSingle( message, "compact-long", + "KK", NumberFormatter.with().notation(Notation.compactLong()), new ULocale("he"), // locale with interesting data 6000, @@ -2508,6 +2871,7 @@ public class NumberFormatterApiTest { FormattedNumber result = assertFormatSingle( message, "compact-short currency/USD", + "K currency/USD", NumberFormatter.with().notation(Notation.compactShort()).unit(USD), new ULocale("sr_Latn"), // locale with interesting data 65000, @@ -2528,6 +2892,7 @@ public class NumberFormatterApiTest { FormattedNumber result = assertFormatSingle( message, "currency/USD unit-width-full-name", + "currency/USD unit-width-full-name", NumberFormatter.with().unit(USD) .unitWidth(UnitWidth.FULL_NAME), ULocale.ENGLISH, @@ -2551,6 +2916,7 @@ public class NumberFormatterApiTest { FormattedNumber result = assertFormatSingle( message, "compact-long measure-unit/length-meter unit-width-full-name", + "KK unit/meter unit-width-full-name", NumberFormatter.with().notation(Notation.compactLong()) .unit(MeasureUnit.METER) .unitWidth(UnitWidth.FULL_NAME), @@ -2617,6 +2983,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Plural 1", "currency/USD precision-integer unit-width-full-name", + "currency/USD . unit-width-full-name", NumberFormatter.with().unit(USD).unitWidth(UnitWidth.FULL_NAME).precision(Precision.fixedFraction(0)), ULocale.ENGLISH, 1, @@ -2625,6 +2992,7 @@ public class NumberFormatterApiTest { assertFormatSingle( "Plural 1.00", "currency/USD .00 unit-width-full-name", + "currency/USD .00 unit-width-full-name", NumberFormatter.with().unit(USD).unitWidth(UnitWidth.FULL_NAME).precision(Precision.fixedFraction(2)), ULocale.ENGLISH, 1, @@ -2748,26 +3116,29 @@ public class NumberFormatterApiTest { static void assertFormatDescending( String message, String skeleton, + String conciseSkeleton, UnlocalizedNumberFormatter f, ULocale locale, String... expected) { final double[] inputs = new double[] { 87650, 8765, 876.5, 87.65, 8.765, 0.8765, 0.08765, 0.008765, 0 }; - assertFormatDescending(message, skeleton, f, locale, inputs, expected); + assertFormatDescending(message, skeleton, conciseSkeleton, f, locale, inputs, expected); } static void assertFormatDescendingBig( String message, String skeleton, + String conciseSkeleton, UnlocalizedNumberFormatter f, ULocale locale, String... expected) { final double[] inputs = new double[] { 87650000, 8765000, 876500, 87650, 8765, 876.5, 87.65, 8.765, 0 }; - assertFormatDescending(message, skeleton, f, locale, inputs, expected); + assertFormatDescending(message, skeleton, conciseSkeleton, f, locale, inputs, expected); } static void assertFormatDescending( String message, String skeleton, + String conciseSkeleton, UnlocalizedNumberFormatter f, ULocale locale, double[] inputs, @@ -2793,6 +3164,22 @@ public class NumberFormatterApiTest { String actual3 = l3.format(d).toString(); assertEquals(message + ": Skeleton Path: " + d, expected[i], actual3); } + // Concise skeletons should have same output, and usually round-trip to the normalized skeleton. + // If the concise skeleton starts with '~', disable the round-trip check. + boolean shouldRoundTrip = true; + if (conciseSkeleton.length() > 0 && conciseSkeleton.charAt(0) == '~') { + conciseSkeleton = conciseSkeleton.substring(1); + shouldRoundTrip = false; + } + LocalizedNumberFormatter l4 = NumberFormatter.forSkeleton(conciseSkeleton).locale(locale); + if (shouldRoundTrip) { + assertEquals(message + ": Concise Skeleton:", normalized, l4.toSkeleton()); + } + for (int i = 0; i < 9; i++) { + double d = inputs[i]; + String actual4 = l4.format(d).toString(); + assertEquals(message + ": Concise Skeleton Path: '" + normalized + "': " + d, expected[i], actual4); + } } else { assertUndefinedSkeleton(f); } @@ -2801,6 +3188,7 @@ public class NumberFormatterApiTest { static FormattedNumber assertFormatSingle( String message, String skeleton, + String conciseSkeleton, UnlocalizedNumberFormatter f, ULocale locale, Number input, @@ -2820,6 +3208,19 @@ public class NumberFormatterApiTest { LocalizedNumberFormatter l3 = NumberFormatter.forSkeleton(normalized).locale(locale); String actual3 = l3.format(input).toString(); assertEquals(message + ": Skeleton Path: " + input, expected, actual3); + // Concise skeletons should have same output, and usually round-trip to the normalized skeleton. + // If the concise skeleton starts with '~', disable the round-trip check. + boolean shouldRoundTrip = true; + if (conciseSkeleton.length() > 0 && conciseSkeleton.charAt(0) == '~') { + conciseSkeleton = conciseSkeleton.substring(1); + shouldRoundTrip = false; + } + LocalizedNumberFormatter l4 = NumberFormatter.forSkeleton(conciseSkeleton).locale(locale); + if (shouldRoundTrip) { + assertEquals(message + ": Concise Skeleton:", normalized, l4.toSkeleton()); + } + String actual4 = l4.format(input).toString(); + assertEquals(message + ": Concise Skeleton Path: '" + normalized + "': " + input, expected, actual4); } else { assertUndefinedSkeleton(f); } @@ -2829,6 +3230,7 @@ public class NumberFormatterApiTest { static void assertFormatSingleMeasure( String message, String skeleton, + String conciseSkeleton, UnlocalizedNumberFormatter f, ULocale locale, Measure input, @@ -2847,6 +3249,19 @@ public class NumberFormatterApiTest { LocalizedNumberFormatter l3 = NumberFormatter.forSkeleton(normalized).locale(locale); String actual3 = l3.format(input).toString(); assertEquals(message + ": Skeleton Path: " + input, expected, actual3); + // Concise skeletons should have same output, and usually round-trip to the normalized skeleton. + // If the concise skeleton starts with '~', disable the round-trip check. + boolean shouldRoundTrip = true; + if (conciseSkeleton.length() > 0 && conciseSkeleton.charAt(0) == '~') { + conciseSkeleton = conciseSkeleton.substring(1); + shouldRoundTrip = false; + } + LocalizedNumberFormatter l4 = NumberFormatter.forSkeleton(conciseSkeleton).locale(locale); + if (shouldRoundTrip) { + assertEquals(message + ": Concise Skeleton:", normalized, l4.toSkeleton()); + } + String actual4 = l4.format(input).toString(); + assertEquals(message + ": Concise Skeleton Path: '" + normalized + "': " + input, expected, actual4); } else { assertUndefinedSkeleton(f); } diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/number/NumberPermutationTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/number/NumberPermutationTest.java index c932149ec..31999aafd 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/number/NumberPermutationTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/number/NumberPermutationTest.java @@ -99,7 +99,7 @@ public class NumberPermutationTest extends TestFmwk { // Build up the golden data string as we evaluate all permutations ArrayList<String> resultLines = new ArrayList<>(); - resultLines.add("# © 2017 and later: Unicode, Inc. and others."); + resultLines.add("# © 2019 and later: Unicode, Inc. and others."); resultLines.add("# License & terms of use: http://www.unicode.org/copyright.html"); resultLines.add(""); diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/number/NumberSkeletonTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/number/NumberSkeletonTest.java index 1a2a913c4..71825e85c 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/number/NumberSkeletonTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/number/NumberSkeletonTest.java @@ -31,26 +31,35 @@ public class NumberSkeletonTest { "precision-integer", "precision-unlimited", "@@@##", + "@@*", "@@+", ".000##", + ".00*", ".00+", ".", + ".*", ".+", ".######", + ".00/@@*", ".00/@@+", ".00/@##", "precision-increment/3.14", "precision-currency-standard", "precision-integer rounding-mode-half-up", ".00# rounding-mode-ceiling", + ".00/@@* rounding-mode-floor", ".00/@@+ rounding-mode-floor", "scientific", + "scientific/*ee", "scientific/+ee", "scientific/sign-always", + "scientific/*ee/sign-always", "scientific/+ee/sign-always", + "scientific/sign-always/*ee", "scientific/sign-always/+ee", "scientific/sign-except-zero", "engineering", + "engineering/*eee", "engineering/+eee", "compact-short", "compact-long", @@ -60,6 +69,7 @@ public class NumberSkeletonTest { "measure-unit/length-meter", "measure-unit/area-square-meter", "measure-unit/energy-joule per-measure-unit/length-meter", + "unit/square-meter-per-square-meter", "currency/XXX", "currency/ZZZ", "currency/usd", @@ -70,6 +80,7 @@ public class NumberSkeletonTest { "group-thousands", "integer-width/00", "integer-width/#0", + "integer-width/*00", "integer-width/+00", "sign-always", "sign-auto", @@ -95,7 +106,20 @@ public class NumberSkeletonTest { "numbering-system/latn", "precision-integer/@##", "precision-integer rounding-mode-ceiling", - "precision-currency-cash rounding-mode-ceiling" }; + "precision-currency-cash rounding-mode-ceiling", + "0", + "00", + "000", + "E0", + "E00", + "E000", + "EE0", + "EE00", + "EE+?0", + "EE+?00", + "EE+!0", + "EE+!00", + }; for (String cas : cases) { try { @@ -111,16 +135,22 @@ public class NumberSkeletonTest { String[] cases = { ".00x", ".00##0", + ".##*", + ".00##*", + ".0#*", + "@#*", ".##+", ".00##+", ".0#+", + "@#+", "@@x", "@@##0", - "@#+", ".00/@", ".00/@@", ".00/@@x", ".00/@@#", + ".00/@@#*", + ".00/floor/@@*", // wrong order ".00/@@#+", ".00/floor/@@+", // wrong order "precision-increment/français", // non-invariant characters for C++ @@ -136,11 +166,29 @@ public class NumberSkeletonTest { "currency/ççç", // three characters but not ASCII "measure-unit/foo", "integer-width/xxx", + "integer-width/0*", + "integer-width/*0#", + "integer-width/*#", + "integer-width/*#0", "integer-width/0+", "integer-width/+0#", "integer-width/+#", "integer-width/+#0", - "scientific/foo" }; + "scientific/foo", + "E", + "E1", + "E+", + "E+?", + "E+!", + "E+0", + "EE", + "EE+", + "EEE", + "EEE0", + "001", + "00*", + "00+", + }; for (String cas : cases) { try { @@ -273,6 +321,26 @@ public class NumberSkeletonTest { } @Test + public void wildcardCharacters() { + String[][] cases = { + { ".00*", ".00+" }, + { "@@*", "@@+" }, + { ".00/@@*", ".00/@@+" }, + { "scientific/*ee", "scientific/+ee" }, + { "integer-width/*00", "integer-width/+00" }, + }; + + for (String[] cas : cases) { + String star = cas[0]; + String plus = cas[1]; + + String normalized = NumberFormatter.forSkeleton(plus) + .toSkeleton(); + assertEquals("Plus should normalize to star", star, normalized); + } + } + + @Test public void roundingModeNames() { for (RoundingMode mode : RoundingMode.values()) { if (mode == RoundingMode.HALF_EVEN) { @@ -284,4 +352,65 @@ public class NumberSkeletonTest { assertEquals(mode.toString(), modeString, skeleton.substring(14)); } } + + @Test + public void perUnitInArabic() { + String[][] cases = { + {"area", "acre"}, + {"digital", "bit"}, + {"digital", "byte"}, + {"temperature", "celsius"}, + {"length", "centimeter"}, + {"duration", "day"}, + {"angle", "degree"}, + {"temperature", "fahrenheit"}, + {"volume", "fluid-ounce"}, + {"length", "foot"}, + {"volume", "gallon"}, + {"digital", "gigabit"}, + {"digital", "gigabyte"}, + {"mass", "gram"}, + {"area", "hectare"}, + {"duration", "hour"}, + {"length", "inch"}, + {"digital", "kilobit"}, + {"digital", "kilobyte"}, + {"mass", "kilogram"}, + {"length", "kilometer"}, + {"volume", "liter"}, + {"digital", "megabit"}, + {"digital", "megabyte"}, + {"length", "meter"}, + {"length", "mile"}, + {"length", "mile-scandinavian"}, + {"volume", "milliliter"}, + {"length", "millimeter"}, + {"duration", "millisecond"}, + {"duration", "minute"}, + {"duration", "month"}, + {"mass", "ounce"}, + {"concentr", "percent"}, + {"digital", "petabyte"}, + {"mass", "pound"}, + {"duration", "second"}, + {"mass", "stone"}, + {"digital", "terabit"}, + {"digital", "terabyte"}, + {"duration", "week"}, + {"length", "yard"}, + {"duration", "year"}, + }; + + ULocale arabic = new ULocale("ar"); + for (String[] cas1 : cases) { + for (String[] cas2 : cases) { + String skeleton = "measure-unit/"; + skeleton += cas1[0] + "-" + cas1[1] + " per-measure-unit/" + cas2[0] + "-" + cas2[1]; + + String actual = NumberFormatter.forSkeleton(skeleton).locale(arabic).format(5142.3) + .toString(); + // Just make sure it won't throw exception + } + } + } } diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/rbbi/RBBITestMonkey.java b/android_icu4j/src/main/tests/android/icu/dev/test/rbbi/RBBITestMonkey.java index a9323ff57..5246ac862 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/rbbi/RBBITestMonkey.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/rbbi/RBBITestMonkey.java @@ -57,6 +57,11 @@ public class RBBITestMonkey extends TestFmwk { // testing, but works purely in terms of the interface defined here. // abstract static class RBBIMonkeyKind { + RBBIMonkeyKind() { + fSets = new ArrayList(); + fClassNames = new ArrayList(); + fAppliedRules = new ArrayList(); + } // Return a List of UnicodeSets, representing the character classes used // for this type of iterator. @@ -69,10 +74,59 @@ public class RBBITestMonkey extends TestFmwk { // Return -1 after reaching end of string. abstract int next(int i); + // Name of each character class, parallel with charClasses. Used for debugging output + // of characters. + List<String> characterClassNames() { + return fClassNames; + } + + void setAppliedRule(int position, String value) { + fAppliedRules.set(position, value); + } + + String getAppliedRule(int position) { + return fAppliedRules.get(position); + } + + String classNameFromCodepoint(int c) { + // Simply iterate through fSets to find character's class + for (int aClassNum = 0; aClassNum < charClasses().size(); aClassNum++) { + UnicodeSet classSet = (UnicodeSet)charClasses().get(aClassNum); + if (classSet.contains(c)) { + return fClassNames.get(aClassNum); + } + } + return "bad class name"; + } + + int maxClassNameSize() { + int maxSize = 0; + for (int aClassNum = 0; aClassNum < charClasses().size(); aClassNum++) { + if (fClassNames.get(aClassNum).length() > maxSize) { + maxSize = fClassNames.get(aClassNum).length(); + } + } + return maxSize; + } + + // Clear `appliedRules` and fill it with empty strings in the size of test text. + void prepareAppliedRules(int size) { + // Remove all the information in the `appliedRules`. + fAppliedRules.clear(); + fAppliedRules.ensureCapacity(size + 1); + while (fAppliedRules.size() < size + 1) { + fAppliedRules.add(""); + } + } + // A Character Property, one of the constants defined in class UProperty. // The value of this property will be displayed for the characters // near any test failure. int fCharProperty; + + List fSets; + ArrayList<String> fClassNames; + ArrayList<String> fAppliedRules; } /** @@ -80,8 +134,6 @@ public class RBBITestMonkey extends TestFmwk { * Note: As of Unicode 6.1, fPrependSet is empty, so don't add it to fSets */ static class RBBICharMonkey extends RBBIMonkeyKind { - List fSets; - UnicodeSet fCRLFSet; UnicodeSet fControlSet; UnicodeSet fExtendSet; @@ -104,7 +156,6 @@ public class RBBITestMonkey extends TestFmwk { StringBuffer fText; - RBBICharMonkey() { fText = null; fCharProperty = UProperty.GRAPHEME_CLUSTER_BREAK; @@ -136,28 +187,28 @@ public class RBBITestMonkey extends TestFmwk { fAnySet = new UnicodeSet("[\\u0000-\\U0010ffff]"); - fSets = new ArrayList(); - fSets.add(fCRLFSet); - fSets.add(fControlSet); - fSets.add(fExtendSet); - fSets.add(fRegionalIndicatorSet); + fSets.add(fCRLFSet); fClassNames.add("CRLF"); + fSets.add(fControlSet); fClassNames.add("Control"); + fSets.add(fExtendSet); fClassNames.add("Extended"); + fSets.add(fRegionalIndicatorSet); fClassNames.add("RegionalIndicator"); if (!fPrependSet.isEmpty()) { - fSets.add(fPrependSet); + fSets.add(fPrependSet); fClassNames.add("Prepend"); } - fSets.add(fSpacingSet); - fSets.add(fHangulSet); - fSets.add(fAnySet); - fSets.add(fZWJSet); - fSets.add(fExtendedPictSet); - fSets.add(fViramaSet); - fSets.add(fLinkingConsonantSet); - fSets.add(fExtCccZwjSet); + fSets.add(fSpacingSet); fClassNames.add("Spacing"); + fSets.add(fHangulSet); fClassNames.add("Hangul"); + fSets.add(fAnySet); fClassNames.add("Any"); + fSets.add(fZWJSet); fClassNames.add("ZWJ"); + fSets.add(fExtendedPictSet); fClassNames.add("ExtendedPict"); + fSets.add(fViramaSet); fClassNames.add("Virama"); + fSets.add(fLinkingConsonantSet); fClassNames.add("LinkingConsonant"); + fSets.add(fExtCccZwjSet); fClassNames.add("ExtCccZwj"); } @Override void setText(StringBuffer s) { fText = s; + prepareAppliedRules(s.length()); } @Override @@ -200,74 +251,72 @@ public class RBBITestMonkey extends TestFmwk { continue; } if (p2 == fText.length()) { - // Reached end of string. Always a break position. + setAppliedRule(p2, "End of String"); break; } - // Rule GB3 CR x LF // No Extend or Format characters may appear between the CR and LF, // which requires the additional check for p2 immediately following p1. // if (c1==0x0D && c2==0x0A && p1==(p2-1)) { + setAppliedRule(p2, "GB 3 CR x LF"); continue; } - // Rule (GB4). ( Control | CR | LF ) <break> if (fControlSet.contains(c1) || c1 == 0x0D || c1 == 0x0A) { + setAppliedRule(p2, "GB 4 ( Control | CR | LF ) <break>"); break; } - // Rule (GB5) <break> ( Control | CR | LF ) - // if (fControlSet.contains(c2) || c2 == 0x0D || c2 == 0x0A) { + setAppliedRule(p2, "GB 5 <break> ( Control | CR | LF )"); break; } - // Rule (GB6) L x ( L | V | LV | LVT ) if (fLSet.contains(c1) && (fLSet.contains(c2) || fVSet.contains(c2) || fLVSet.contains(c2) || fLVTSet.contains(c2))) { + setAppliedRule(p2, "GB 6 L x ( L | V | LV | LVT )"); continue; } - // Rule (GB7) ( LV | V ) x ( V | T ) if ((fLVSet.contains(c1) || fVSet.contains(c1)) && (fVSet.contains(c2) || fTSet.contains(c2))) { + setAppliedRule(p2, "GB 7 ( LV | V ) x ( V | T )"); continue; } - // Rule (GB8) ( LVT | T) x T if ((fLVTSet.contains(c1) || fTSet.contains(c1)) && fTSet.contains(c2)) { + setAppliedRule(p2, "GB 8 ( LVT | T) x T"); continue; } - // Rule (GB9) x (Extend | ZWJ) if (fExtendSet.contains(c2) || fZWJSet.contains(c2)) { if (!fExtendSet.contains(c1)) { cBase = c1; } + setAppliedRule(p2, "GB 9 x (Extend | ZWJ)"); continue; } - // Rule (GB9a) x SpacingMark if (fSpacingSet.contains(c2)) { + setAppliedRule(p2, "GB 9a x SpacingMark"); continue; } - // Rule (GB9b) Prepend x if (fPrependSet.contains(c1)) { + setAppliedRule(p2, "GB 9b Prepend x"); continue; } - // Rule (GB9.3) LinkingConsonant ExtCccZwj* Virama ExtCccZwj* × LinkingConsonant // Note: Viramas are also included in the ExtCccZwj class. if (fLinkingConsonantSet.contains(c2)) { int pi = p1; @@ -279,37 +328,39 @@ public class RBBITestMonkey extends TestFmwk { pi = fText.offsetByCodePoints(pi, -1); } if (sawVirama && fLinkingConsonantSet.contains(fText.codePointAt(pi))) { + setAppliedRule(p2, "GB 9.3 LinkingConsonant ExtCccZwj* Virama ExtCccZwj* × LinkingConsonant"); continue; } } - // Rule (GB11) Extended_Pictographic ZWJ x Extended_Pictographic if (fExtendedPictSet.contains(cBase) && fZWJSet.contains(c1) && fExtendedPictSet.contains(c2) ) { + setAppliedRule(p2, "GB 11 Extended_Pictographic Extend * ZWJ x Extended_Pictographic"); continue; } - // Rule (GB12-13) Regional_Indicator x Regional_Indicator // Note: The first if condition is a little tricky. We only need to force // a break if there are three or more contiguous RIs. If there are // only two, a break following will occur via other rules, and will include // any trailing extend characters, which is needed behavior. if (fRegionalIndicatorSet.contains(c0) && fRegionalIndicatorSet.contains(c1) && fRegionalIndicatorSet.contains(c2)) { + setAppliedRule(p2, "GB 12-13 Regional_Indicator x Regional_Indicator"); break; } if (fRegionalIndicatorSet.contains(c1) && fRegionalIndicatorSet.contains(c2)) { + setAppliedRule(p2, "GB 12-13 Regional_Indicator x Regional_Indicator"); continue; } - // Rule (GB999) Any <break> Any + setAppliedRule(p2, "GB 999 Any <break> Any"); break; } breakPos = p2; return breakPos; } - } + } /** * @@ -319,7 +370,6 @@ public class RBBITestMonkey extends TestFmwk { * */ static class RBBIWordMonkey extends RBBIMonkeyKind { - List fSets; StringBuffer fText; UnicodeSet fCRSet; @@ -344,7 +394,6 @@ public class RBBITestMonkey extends TestFmwk { UnicodeSet fZWJSet; UnicodeSet fExtendedPictSet; - RBBIWordMonkey() { fCharProperty = UProperty.WORD_BREAK; @@ -405,30 +454,28 @@ public class RBBITestMonkey extends TestFmwk { fOtherSet.removeAll(new UnicodeSet("[[\\p{LineBreak = Complex_Context}][:Line_Break=Surrogate:]]")); fOtherSet.removeAll(fDictionarySet); - fSets = new ArrayList(); - fSets.add(fCRSet); - fSets.add(fLFSet); - fSets.add(fNewlineSet); - fSets.add(fRegionalIndicatorSet); - fSets.add(fHebrew_LetterSet); - fSets.add(fALetterSet); + fSets.add(fCRSet); fClassNames.add("CR"); + fSets.add(fLFSet); fClassNames.add("LF"); + fSets.add(fNewlineSet); fClassNames.add("Newline"); + fSets.add(fRegionalIndicatorSet); fClassNames.add("RegionalIndicator"); + fSets.add(fHebrew_LetterSet); fClassNames.add("Hebrew"); + fSets.add(fALetterSet); fClassNames.add("ALetter"); //fSets.add(fKatakanaSet); // Omit Katakana from fSets, which omits Katakana characters // from the test data. They are all in the dictionary set, // which this (old, to be retired) monkey test cannot handle. - fSets.add(fSingle_QuoteSet); - fSets.add(fDouble_QuoteSet); - fSets.add(fMidLetterSet); - fSets.add(fMidNumLetSet); - fSets.add(fMidNumSet); - fSets.add(fNumericSet); - fSets.add(fFormatSet); - fSets.add(fExtendSet); - fSets.add(fExtendNumLetSet); - fSets.add(fWSegSpaceSet); - fSets.add(fRegionalIndicatorSet); - fSets.add(fZWJSet); - fSets.add(fExtendedPictSet); - fSets.add(fOtherSet); + fSets.add(fSingle_QuoteSet); fClassNames.add("Single Quote"); + fSets.add(fDouble_QuoteSet); fClassNames.add("Double Quote"); + fSets.add(fMidLetterSet); fClassNames.add("MidLetter"); + fSets.add(fMidNumLetSet); fClassNames.add("MidNumLet"); + fSets.add(fMidNumSet); fClassNames.add("MidNum"); + fSets.add(fNumericSet); fClassNames.add("Numeric"); + fSets.add(fFormatSet); fClassNames.add("Format"); + fSets.add(fExtendSet); fClassNames.add("Extend"); + fSets.add(fExtendNumLetSet); fClassNames.add("ExtendNumLet"); + fSets.add(fWSegSpaceSet); fClassNames.add("WSegSpace"); + fSets.add(fZWJSet); fClassNames.add("ZWJ"); + fSets.add(fExtendedPictSet); fClassNames.add("ExtendedPict"); + fSets.add(fOtherSet); fClassNames.add("Other"); } @@ -440,6 +487,7 @@ public class RBBITestMonkey extends TestFmwk { @Override void setText(StringBuffer s) { fText = s; + prepareAppliedRules(s.length()); } @Override @@ -492,149 +540,146 @@ public class RBBITestMonkey extends TestFmwk { break; } - // Rule (3) CR x LF // No Extend or Format characters may appear between the CR and LF, // which requires the additional check for p2 immediately following p1. // if (c1==0x0D && c2==0x0A) { + setAppliedRule(p2, "WB 3 CR x LF"); continue; } - // Rule (3a) Break before and after newlines (including CR and LF) // if (fCRSet.contains(c1) || fLFSet.contains(c1) || fNewlineSet.contains(c1)) { + setAppliedRule(p2, "WB 3a Break before and after newlines (including CR and LF)"); break; } if (fCRSet.contains(c2) || fLFSet.contains(c2) || fNewlineSet.contains(c2)) { + setAppliedRule(p2, "WB 3a Break before and after newlines (including CR and LF)"); break; } - // Rule (3c) ZWJ x Extended_Pictographic. // Not ignoring extend chars, so peek into input text to // get the potential ZWJ, the character immediately preceding c2. if (fZWJSet.contains(fText.codePointBefore(p2)) && fExtendedPictSet.contains(c2)) { + setAppliedRule(p2, "WB 3c ZWJ x Extended_Pictographic"); continue; } - // Rule (3d) Keep horizontal whitespace together. if (fWSegSpaceSet.contains(fText.codePointBefore(p2)) && fWSegSpaceSet.contains(c2)) { + setAppliedRule(p2, "WB 3d Keep horizontal whitespace together"); continue; } - // Rule (5). (ALetter | Hebrew_Letter) x (ALetter | Hebrew_Letter) if ((fALetterSet.contains(c1) || fHebrew_LetterSet.contains(c1)) && (fALetterSet.contains(c2) || fHebrew_LetterSet.contains(c2))) { + setAppliedRule(p2, "WB 4 (ALetter | Hebrew_Letter) x (ALetter | Hebrew_Letter)"); continue; } - // Rule (6) (ALetter | Hebrew_Letter) x (MidLetter | MidNumLet | Single_Quote) (ALetter | Hebrew_Letter) - // if ( (fALetterSet.contains(c1) || fHebrew_LetterSet.contains(c1)) && (fMidLetterSet.contains(c2) || fMidNumLetSet.contains(c2) || fSingle_QuoteSet.contains(c2)) && (setContains(fALetterSet, c3) || setContains(fHebrew_LetterSet, c3))) { + setAppliedRule(p2, "WB 6 (ALetter | Hebrew_Letter) x (MidLetter | MidNumLet | Single_Quote) (ALetter | Hebrew_Letter)"); continue; } - // Rule (7) (ALetter | Hebrew_Letter) (MidLetter | MidNumLet | Single_Quote) x (ALetter | Hebrew_Letter) if ((fALetterSet.contains(c0) || fHebrew_LetterSet.contains(c0)) && (fMidLetterSet.contains(c1) || fMidNumLetSet.contains(c1) || fSingle_QuoteSet.contains(c1)) && (fALetterSet.contains(c2) || fHebrew_LetterSet.contains(c2))) { + setAppliedRule(p2, "WB 7 (ALetter | Hebrew_Letter) (MidLetter | MidNumLet | Single_Quote) x (ALetter | Hebrew_Letter)"); continue; } - // Rule (7a) Hebrew_Letter x Single_Quote if (fHebrew_LetterSet.contains(c1) && fSingle_QuoteSet.contains(c2)) { + setAppliedRule(p2, "WB 7a Hebrew_Letter x Single_Quote"); continue; } - // Rule (7b) Hebrew_Letter x Double_Quote Hebrew_Letter if (fHebrew_LetterSet.contains(c1) && fDouble_QuoteSet.contains(c2) && setContains(fHebrew_LetterSet,c3)) { + setAppliedRule(p2, "WB 7b Hebrew_Letter x Single_Quote"); continue; } - // Rule (7c) Hebrew_Letter Double_Quote x Hebrew_Letter if (fHebrew_LetterSet.contains(c0) && fDouble_QuoteSet.contains(c1) && fHebrew_LetterSet.contains(c2)) { + setAppliedRule(p2, "WB 7c Hebrew_Letter Double_Quote x Hebrew_Letter"); continue; } - // Rule (8) Numeric x Numeric if (fNumericSet.contains(c1) && fNumericSet.contains(c2)) { + setAppliedRule(p2, "WB 8 Numeric x Numeric"); continue; } - // Rule (9) (ALetter | Hebrew_Letter) x Numeric if ((fALetterSet.contains(c1) || fHebrew_LetterSet.contains(c1)) && fNumericSet.contains(c2)) { + setAppliedRule(p2, "WB 9 (ALetter | Hebrew_Letter) x Numeric"); continue; } - // Rule (10) Numeric x (ALetter | Hebrew_Letter) if (fNumericSet.contains(c1) && (fALetterSet.contains(c2) || fHebrew_LetterSet.contains(c2))) { + setAppliedRule(p2, "WB 10 Numeric x (ALetter | Hebrew_Letter)"); continue; } - // Rule (11) Numeric (MidNum | MidNumLet | Single_Quote) x Numeric if (fNumericSet.contains(c0) && (fMidNumSet.contains(c1) || fMidNumLetSet.contains(c1) || fSingle_QuoteSet.contains(c1)) && fNumericSet.contains(c2)) { + setAppliedRule(p2, "WB 11 Numeric (MidNum | MidNumLet | Single_Quote) x Numeric"); continue; } - // Rule (12) Numeric x (MidNum | MidNumLet | SingleQuote) Numeric if (fNumericSet.contains(c1) && (fMidNumSet.contains(c2) || fMidNumLetSet.contains(c2) || fSingle_QuoteSet.contains(c2)) && setContains(fNumericSet, c3)) { + setAppliedRule(p2, "WB 12 Numeric x (MidNum | MidNumLet | SingleQuote) Numeric"); continue; } - // Rule (13) Katakana x Katakana // Note: matches UAX 29 rules, but doesn't come into play for ICU because // all Katakana are handled by the dictionary breaker. if (fKatakanaSet.contains(c1) && fKatakanaSet.contains(c2)) { + setAppliedRule(p2, "WB 13 Katakana x Katakana"); continue; } - // Rule 13a (ALetter | Hebrew_Letter | Numeric | KataKana | ExtendNumLet) x ExtendNumLet if ((fALetterSet.contains(c1) || fHebrew_LetterSet.contains(c1) ||fNumericSet.contains(c1) || fKatakanaSet.contains(c1) || fExtendNumLetSet.contains(c1)) && fExtendNumLetSet.contains(c2)) { + setAppliedRule(p2, "WB 13a (ALetter | Hebrew_Letter | Numeric | KataKana | ExtendNumLet) x ExtendNumLet"); continue; } - // Rule 13b ExtendNumLet x (ALetter | Hebrew_Letter | Numeric | Katakana) if (fExtendNumLetSet.contains(c1) && (fALetterSet.contains(c2) || fHebrew_LetterSet.contains(c2) || fNumericSet.contains(c2) || fKatakanaSet.contains(c2))) { + setAppliedRule(p2, "WB 13b ExtendNumLet x (ALetter | Hebrew_Letter | Numeric | Katakana)"); continue; } - // Rule 15 - 17 Group piars of Regional Indicators if (fRegionalIndicatorSet.contains(c0) && fRegionalIndicatorSet.contains(c1)) { + setAppliedRule(p2, "WB 15-17 Group pairs of Regional Indicators."); break; } if (fRegionalIndicatorSet.contains(c1) && fRegionalIndicatorSet.contains(c2)) { + setAppliedRule(p2, "WB 15-17 Group pairs of Regional Indicators."); continue; } - // Rule 999. Break found here. + setAppliedRule(p2, "WB 999"); break; } breakPos = p2; return breakPos; } - } static class RBBILineMonkey extends RBBIMonkeyKind { - - List fSets; - // UnicodeSets for each of the Line Breaking character classes. // Order matches that of Unicode UAX 14, Table 1, which makes it a little easier // to verify that they are all accounted for. @@ -688,7 +733,6 @@ public class RBBITestMonkey extends TestFmwk { StringBuffer fText; int fOrigPositions; - // XUnicodeSet is like UnicodeSet, except that the method contains(int codePoint) does not // throw exceptions on out-of-range codePoints. This matches ICU4C behavior. // The LineMonkey test (ported from ICU4C) relies on this behavior, it uses a value of -1 @@ -708,7 +752,6 @@ public class RBBITestMonkey extends TestFmwk { RBBILineMonkey() { fCharProperty = UProperty.LINE_BREAK; - fSets = new ArrayList(); fBK = new XUnicodeSet("[\\p{Line_Break=BK}]"); fCR = new XUnicodeSet("[\\p{Line_break=CR}]"); @@ -772,55 +815,56 @@ public class RBBITestMonkey extends TestFmwk { fHH.add('\u2010'); // Hyphen, '‐' - fSets.add(fBK); - fSets.add(fCR); - fSets.add(fLF); - fSets.add(fCM); - fSets.add(fNL); - fSets.add(fWJ); - fSets.add(fZW); - fSets.add(fGL); - fSets.add(fSP); - fSets.add(fB2); - fSets.add(fBA); - fSets.add(fBB); - fSets.add(fHY); - fSets.add(fCB); - fSets.add(fCL); - fSets.add(fCP); - fSets.add(fEX); - fSets.add(fIN); - fSets.add(fJL); - fSets.add(fJT); - fSets.add(fJV); - fSets.add(fNS); - fSets.add(fOP); - fSets.add(fQU); - fSets.add(fIS); - fSets.add(fNU); - fSets.add(fPO); - fSets.add(fPR); - fSets.add(fSY); - fSets.add(fAI); - fSets.add(fAL); - fSets.add(fH2); - fSets.add(fH3); - fSets.add(fHL); - fSets.add(fID); - fSets.add(fWJ); - fSets.add(fRI); - fSets.add(fSG); - fSets.add(fEB); - fSets.add(fEM); - fSets.add(fZWJ); + fSets.add(fBK); fClassNames.add("BK"); + fSets.add(fCR); fClassNames.add("CR"); + fSets.add(fLF); fClassNames.add("LF"); + fSets.add(fCM); fClassNames.add("CM"); + fSets.add(fNL); fClassNames.add("NL"); + fSets.add(fWJ); fClassNames.add("WJ"); + fSets.add(fZW); fClassNames.add("ZW"); + fSets.add(fGL); fClassNames.add("GL"); + fSets.add(fSP); fClassNames.add("SP"); + fSets.add(fB2); fClassNames.add("B2"); + fSets.add(fBA); fClassNames.add("BA"); + fSets.add(fBB); fClassNames.add("BB"); + fSets.add(fHY); fClassNames.add("HY"); + fSets.add(fCB); fClassNames.add("CB"); + fSets.add(fCL); fClassNames.add("CL"); + fSets.add(fCP); fClassNames.add("CP"); + fSets.add(fEX); fClassNames.add("EX"); + fSets.add(fIN); fClassNames.add("IN"); + fSets.add(fJL); fClassNames.add("JL"); + fSets.add(fJT); fClassNames.add("JT"); + fSets.add(fJV); fClassNames.add("JV"); + fSets.add(fNS); fClassNames.add("NV"); + fSets.add(fOP); fClassNames.add("OP"); + fSets.add(fQU); fClassNames.add("QU"); + fSets.add(fIS); fClassNames.add("IS"); + fSets.add(fNU); fClassNames.add("NU"); + fSets.add(fPO); fClassNames.add("PO"); + fSets.add(fPR); fClassNames.add("PR"); + fSets.add(fSY); fClassNames.add("SY"); + fSets.add(fAI); fClassNames.add("AI"); + fSets.add(fAL); fClassNames.add("AL"); + fSets.add(fH2); fClassNames.add("H2"); + fSets.add(fH3); fClassNames.add("H3"); + fSets.add(fHL); fClassNames.add("HL"); + fSets.add(fID); fClassNames.add("ID"); + fSets.add(fWJ); fClassNames.add("WJ"); + fSets.add(fRI); fClassNames.add("RI"); + fSets.add(fSG); fClassNames.add("SG"); + fSets.add(fEB); fClassNames.add("EB"); + fSets.add(fEM); fClassNames.add("EM"); + fSets.add(fZWJ); fClassNames.add("ZWJ"); // TODO: fOP30 & fCP30 overlap with plain fOP. Probably OK, but fOP/CP chars will be over-represented. - fSets.add(fOP30); - fSets.add(fCP30); + fSets.add(fOP30); fClassNames.add("OP30"); + fSets.add(fCP30); fClassNames.add("CP30"); } @Override void setText(StringBuffer s) { fText = s; + prepareAppliedRules(s.length()); } @@ -872,12 +916,11 @@ public class RBBITestMonkey extends TestFmwk { pos = nextPos; nextPos = moveIndex32(fText, pos, 1); - // Rule LB2 - Break at end of text. if (pos >= fText.length()) { + setAppliedRule(pos, "LB 2 Break at end of text"); break; } - // Rule LB 9 - adjust for combining sequences. // We do this rule out-of-order because the adjustment does // not effect the way that rules LB 3 through LB 6 match, // and doing it here rather than after LB 6 is substantially @@ -915,41 +958,43 @@ public class RBBITestMonkey extends TestFmwk { // -1 positions out of prevPos yet - loop back to advance the // position in the input without any further looking for breaks. if (prevPos == -1) { + setAppliedRule(pos, "LB 9 adjust for combining sequences."); continue; } - // LB 4 Always break after hard line breaks, if (fBK.contains(prevChar)) { + setAppliedRule(pos, "LB 4 Always break after hard line breaks"); break; } - // LB 5 Break after CR, LF, NL, but not inside CR LF if (fCR.contains(prevChar) && fLF.contains(thisChar)) { + setAppliedRule(pos, "LB 5 Break after CR, LF, NL, but not inside CR LF"); continue; } if (fCR.contains(prevChar) || fLF.contains(prevChar) || fNL.contains(prevChar)) { + setAppliedRule(pos, "LB 5 Break after CR, LF, NL, but not inside CR LF"); break; } - // LB 6 Don't break before hard line breaks if (fBK.contains(thisChar) || fCR.contains(thisChar) || fLF.contains(thisChar) || fNL.contains(thisChar) ) { + setAppliedRule(pos, "LB 6 Don't break before hard line breaks"); continue; } - // LB 7 Don't break before spaces or zero-width space. if (fSP.contains(thisChar)) { + setAppliedRule(pos, "LB 7 Don't break before spaces or zero-width space"); continue; } if (fZW.contains(thisChar)) { + setAppliedRule(pos, "LB 7 Don't break before spaces or zero-width space"); continue; } - // LB 8 Break after zero width space // ZW SP* ÷ // Scan backwards from prevChar for SP* ZW tPos = prevPos; @@ -957,10 +1002,10 @@ public class RBBITestMonkey extends TestFmwk { tPos = moveIndex32(fText, tPos, -1); } if (fZW.contains(UTF16.charAt(fText, tPos))) { + setAppliedRule(pos, "LB 8 Break after zero width space"); break; } - // LB 25 Numbers // Move this test up, before LB8a, because numbers can match a longer sequence that would // also match 8a. e.g. NU ZWJ IS PO (ZWJ acts like CM) matchVals = LBNumberCheck(fText, prevPos, matchVals); @@ -982,57 +1027,53 @@ public class RBBITestMonkey extends TestFmwk { } while (fCM.contains(thisChar)); } + setAppliedRule(pos, "LB 25 Numbers"); continue; } } - // LB 8a: ZWJ x (ID | Extended_Pictographic | Emoji) // The monkey test's way of ignoring combining characters doesn't work // for this rule. ZWJ is also a CM. Need to get the actual character // preceding "thisChar", not ignoring combining marks, possibly ZWJ. { int prevC = fText.codePointBefore(pos); if (fZWJ.contains(prevC)) { + setAppliedRule(pos, "LB 8a ZWJ x"); continue; } } - // LB 9, 10 Already done, at top of loop. - // + // appliedRule: "LB 9, 10"; // Already done, at top of loop."; - // LB 11 // x WJ // WJ x if (fWJ.contains(thisChar) || fWJ.contains(prevChar)) { + setAppliedRule(pos, "LB 11 Do not break before or after WORD JOINER and related characters."); continue; } - // LB 12 - // GL x if (fGL.contains(prevChar)) { + setAppliedRule(pos, "LB 12 GL x"); continue; } - // LB 12a - // [^SP BA HY] x GL if (!(fSP.contains(prevChar) || fBA.contains(prevChar) || fHY.contains(prevChar) ) && fGL.contains(thisChar)) { + setAppliedRule(pos, "LB 12a [^SP BA HY] x GL"); continue; } - // LB 13 Don't break before closings. - // if (fCL.contains(thisChar) || fCP.contains(thisChar) || fEX.contains(thisChar) || fSY.contains(thisChar)) { + setAppliedRule(pos, "LB 13 Don't break before closings"); continue; } - // LB 14 Don't break after OP SP* // Scan backwards, checking for this sequence. // The OP char could include combining marks, so we actually check for // OP CM* SP* x @@ -1046,24 +1087,23 @@ public class RBBITestMonkey extends TestFmwk { tPos=moveIndex32(fText, tPos, -1); } if (fOP.contains(UTF16.charAt(fText, tPos))) { + setAppliedRule(pos, "LB 14 Don't break after OP SP*"); continue; } - // LB 14a Break before an IS that begins a number and follows a space if (nextPos < fText.length()) { int nextChar = fText.codePointAt(nextPos); if (fSP.contains(prevChar) && fIS.contains(thisChar) && fNU.contains(nextChar)) { + setAppliedRule(pos, "LB 14a Break before an IS that begins a number and follows a space"); break; } } - // LB14b Do not break before numeric separators, even after spaces. if (fIS.contains(thisChar)) { + setAppliedRule(pos, "LB 14b Do not break before numeric separators, even after spaces"); continue; } - // LB 15 Do not break within "[ - // QU CM* SP* x OP if (fOP.contains(thisChar)) { // Scan backwards from prevChar to see if it is preceded by QU CM* SP* tPos = prevPos; @@ -1074,11 +1114,11 @@ public class RBBITestMonkey extends TestFmwk { tPos = moveIndex32(fText, tPos, -1); } if (fQU.contains(UTF16.charAt(fText, tPos))) { + setAppliedRule(pos, "LB 15 QU SP* x OP"); continue; } } - // LB 16 (CL | CP) SP* x NS if (fNS.contains(thisChar)) { tPos = prevPos; while (tPos > 0 && fSP.contains(UTF16.charAt(fText, tPos))) { @@ -1088,12 +1128,12 @@ public class RBBITestMonkey extends TestFmwk { tPos = moveIndex32(fText, tPos, -1); } if (fCL.contains(UTF16.charAt(fText, tPos)) || fCP.contains(UTF16.charAt(fText, tPos))) { + setAppliedRule(pos, "LB 16 (CL | CP) SP* x NS"); continue; } } - // LB 17 B2 SP* x B2 if (fB2.contains(thisChar)) { tPos = prevPos; while (tPos > 0 && fSP.contains(UTF16.charAt(fText, tPos))) { @@ -1103,156 +1143,169 @@ public class RBBITestMonkey extends TestFmwk { tPos = moveIndex32(fText, tPos, -1); } if (fB2.contains(UTF16.charAt(fText, tPos))) { + setAppliedRule(pos, "LB 17 B2 SP* x B2"); continue; } } - // LB 18 break after space if (fSP.contains(prevChar)) { + setAppliedRule(pos, "LB 18 break after space"); break; } - // LB 19 // x QU // QU x if (fQU.contains(thisChar) || fQU.contains(prevChar)) { + setAppliedRule(pos, "LB 19"); continue; } - // LB 20 Break around a CB if (fCB.contains(thisChar) || fCB.contains(prevChar)) { + setAppliedRule(pos, "LB 20 Break around a CB"); break; } - // LB 20.09 Don't break between Hyphens and letters if a break precedes the hyphen. + // Don't break between Hyphens and letters if a break precedes the hyphen. // Formerly this was a Finnish tailoring. // Moved to root in ICU 63. This is an ICU customization, not in UAX-14. // ^($HY | $HH) $AL; if (fAL.contains(thisChar) && (fHY.contains(prevChar) || fHH.contains(prevChar)) && prevPosX2 == -1) { + setAppliedRule(pos, "LB 20.09"); continue; } - // LB 21 if (fBA.contains(thisChar) || fHY.contains(thisChar) || fNS.contains(thisChar) || fBB.contains(prevChar) ) { + setAppliedRule(pos, "LB 21"); continue; } - // LB 21a, HL (HY | BA) x if (fHL.contains(prevCharX2) && (fHY.contains(prevChar) || fBA.contains(prevChar))) { + setAppliedRule(pos, "LB 21a HL (HY | BA) x"); continue; } - // LB 21b, SY x HL if (fSY.contains(prevChar) && fHL.contains(thisChar)) { + setAppliedRule(pos, "LB 21b SY x HL"); continue; } - // LB 22 if (fIN.contains(thisChar)) { + setAppliedRule(pos, "LB 22"); continue; } - // LB 23 (AL | HL) x NU + // (AL | HL) x NU // NU x (AL | HL) if ((fAL.contains(prevChar) || fHL.contains(prevChar)) && fNU.contains(thisChar)) { + setAppliedRule(pos, "LB 23"); continue; } if (fNU.contains(prevChar) && (fAL.contains(thisChar) || fHL.contains(thisChar))) { + setAppliedRule(pos, "LB 23"); continue; } - // LB 23a Do not break between numeric prefixes and ideographs, or between ideographs and numeric postfixes. + // Do not break between numeric prefixes and ideographs, or between ideographs and numeric postfixes. // PR x (ID | EB | EM) // (ID | EB | EM) x PO if (fPR.contains(prevChar) && (fID.contains(thisChar) || fEB.contains(thisChar) || fEM.contains(thisChar))) { + setAppliedRule(pos, "LB 23a"); continue; } if ((fID.contains(prevChar) || fEB.contains(prevChar) || fEM.contains(prevChar)) && fPO.contains(thisChar)) { + setAppliedRule(pos, "LB 23a"); continue; } - // LB 24 Do not break between prefix and letters or ideographs. + // Do not break between prefix and letters or ideographs. // (PR | PO) x (AL | HL) // (AL | HL) x (PR | PO) if ((fPR.contains(prevChar) || fPO.contains(prevChar)) && (fAL.contains(thisChar) || fHL.contains(thisChar))) { + setAppliedRule(pos, "LB 24 no break between prefix and letters or ideographs"); continue; } if ((fAL.contains(prevChar) || fHL.contains(prevChar)) && (fPR.contains(thisChar) || fPO.contains(thisChar))) { + setAppliedRule(pos, "LB 24 no break between prefix and letters or ideographs"); continue; } - // LB 25 Numbers match, moved up, before LB 8a. + // appliedRule: "LB 25 numbers match"; // moved up, before LB 8a, - // LB 26 Do not break Korean Syllables if (fJL.contains(prevChar) && (fJL.contains(thisChar) || fJV.contains(thisChar) || fH2.contains(thisChar) || fH3.contains(thisChar))) { + setAppliedRule(pos, "LB 26 Do not break a Korean syllable."); continue; } if ((fJV.contains(prevChar) || fH2.contains(prevChar)) && (fJV.contains(thisChar) || fJT.contains(thisChar))) { + setAppliedRule(pos, "LB 26 Do not break a Korean syllable."); continue; } if ((fJT.contains(prevChar) || fH3.contains(prevChar)) && fJT.contains(thisChar)) { + setAppliedRule(pos, "LB 26 Do not break a Korean syllable."); continue; } - // LB 27 Treat a Korean Syllable Block the same as ID if ((fJL.contains(prevChar) || fJV.contains(prevChar) || fJT.contains(prevChar) || fH2.contains(prevChar) || fH3.contains(prevChar)) && fIN.contains(thisChar)) { + setAppliedRule(pos, "LB 27 Treat a Korean Syllable Block the same as ID."); continue; } if ((fJL.contains(prevChar) || fJV.contains(prevChar) || fJT.contains(prevChar) || fH2.contains(prevChar) || fH3.contains(prevChar)) && fPO.contains(thisChar)) { + setAppliedRule(pos, "LB 27 Treat a Korean Syllable Block the same as ID."); continue; } if (fPR.contains(prevChar) && (fJL.contains(thisChar) || fJV.contains(thisChar) || fJT.contains(thisChar) || fH2.contains(thisChar) || fH3.contains(thisChar))) { + setAppliedRule(pos, "LB 27 Treat a Korean Syllable Block the same as ID."); continue; } - // LB 28 Do not break between alphabetics if ((fAL.contains(prevChar) || fHL.contains(prevChar)) && (fAL.contains(thisChar) || fHL.contains(thisChar))) { + setAppliedRule(pos, "LB 28 Do not break between alphabetics"); continue; } - // LB 29 Do not break between numeric punctuation and alphabetics if (fIS.contains(prevChar) && (fAL.contains(thisChar) || fHL.contains(thisChar))) { + setAppliedRule(pos, "LB 29 Do not break between numeric punctuation and alphabetics"); continue; } - // LB 30 Do not break between letters, numbers, or ordinary symbols and opening or closing punctuation. // (AL | NU) x OP // CP x (AL | NU) if ((fAL.contains(prevChar) || fHL.contains(prevChar) || fNU.contains(prevChar)) && fOP30.contains(thisChar)) { + setAppliedRule(pos, "LB 30 Do not break between letters, numbers, or ordinary symbols and opening or closing punctuation."); continue; } if (fCP30.contains(prevChar) && (fAL.contains(thisChar) || fHL.contains(thisChar) || fNU.contains(thisChar))) { + setAppliedRule(pos, "LB 30 Do not break between letters, numbers, or ordinary symbols and opening or closing punctuation."); continue; } - // LB 30a Break between pairs of Regional Indicators. // RI RI ÷ RI // RI x RI if (fRI.contains(prevCharX2) && fRI.contains(prevChar) && fRI.contains(thisChar)) { + setAppliedRule(pos, "LB 30a Break between pairs of Regional Indicators."); break; } if (fRI.contains(prevChar) && fRI.contains(thisChar)) { @@ -1260,14 +1313,16 @@ public class RBBITestMonkey extends TestFmwk { // Over-write the trailing one (thisChar) to prevent it from forming another pair with a // following RI. This is a hack. thisChar = -1; + setAppliedRule(pos, "LB 30a Break between pairs of Regional Indicators."); continue; } - // LB30b Emoji Base x Emoji Modifier if (fEB.contains(prevChar) && fEM.contains(thisChar)) { + setAppliedRule(pos, "LB 30b Emoji Base x Emoji Modifier"); continue; } // LB 31 Break everywhere else + setAppliedRule(pos, "LB 31 Break everywhere else"); break; } @@ -1450,7 +1505,6 @@ public class RBBITestMonkey extends TestFmwk { * */ static class RBBISentenceMonkey extends RBBIMonkeyKind { - List fSets; StringBuffer fText; UnicodeSet fSepSet; @@ -1467,13 +1521,9 @@ public class RBBITestMonkey extends TestFmwk { UnicodeSet fOtherSet; UnicodeSet fExtendSet; - - RBBISentenceMonkey() { fCharProperty = UProperty.SENTENCE_BREAK; - fSets = new ArrayList(); - // Separator Set Note: Beginning with Unicode 5.1, CR and LF were removed from the separator // set and made into character classes of their own. For the monkey impl, // they remain in SEP, since Sep always appears with CR and LF in the rules. @@ -1506,20 +1556,20 @@ public class RBBITestMonkey extends TestFmwk { fOtherSet.removeAll(fCloseSet); fOtherSet.removeAll(fExtendSet); - fSets.add(fSepSet); - fSets.add(fFormatSet); - - fSets.add(fSpSet); - fSets.add(fLowerSet); - fSets.add(fUpperSet); - fSets.add(fOLetterSet); - fSets.add(fNumericSet); - fSets.add(fATermSet); - fSets.add(fSContinueSet); - fSets.add(fSTermSet); - fSets.add(fCloseSet); - fSets.add(fOtherSet); - fSets.add(fExtendSet); + fSets.add(fSepSet); fClassNames.add("Sep"); + fSets.add(fFormatSet); fClassNames.add("Format"); + + fSets.add(fSpSet); fClassNames.add("Sp"); + fSets.add(fLowerSet); fClassNames.add("Lower"); + fSets.add(fUpperSet); fClassNames.add("Upper"); + fSets.add(fOLetterSet); fClassNames.add("OLetter"); + fSets.add(fNumericSet); fClassNames.add("Numeric"); + fSets.add(fATermSet); fClassNames.add("ATerm"); + fSets.add(fSContinueSet); fClassNames.add("SContinue"); + fSets.add(fSTermSet); fClassNames.add("STerm"); + fSets.add(fCloseSet); fClassNames.add("Close"); + fSets.add(fOtherSet); fClassNames.add("Other"); + fSets.add(fExtendSet); fClassNames.add("Extend"); } @@ -1531,6 +1581,7 @@ public class RBBITestMonkey extends TestFmwk { @Override void setText(StringBuffer s) { fText = s; + prepareAppliedRules(s.length()); } @@ -1601,43 +1652,44 @@ public class RBBITestMonkey extends TestFmwk { p1 = p2; c1 = c2; p2 = p3; c2 = c3; - // Advancd p3 by X(Extend | Format)* Rule 4 + // Advance p3 by X(Extend | Format)* Rule 4 p3 = moveForward(p3); c3 = cAt(p3); - // Rule (3) CR x LF if (c1==0x0d && c2==0x0a && p2==(p1+1)) { + setAppliedRule(p2, "SB3 CR x LF"); continue; } - // Rule (4) Sep <break> if (fSepSet.contains(c1)) { p2 = p1+1; // Separators don't combine with Extend or Format + setAppliedRule(p2, "SB4 Sep <break>"); break; } if (p2 >= fText.length()) { // Reached end of string. Always a break position. + setAppliedRule(p2, "SB4 Sep <break>"); break; } if (p2 == prevPos) { // Still warming up the loop. (won't work with zero length strings, but we don't care) + setAppliedRule(p2, "SB4 Sep <break>"); continue; } - // Rule (6). ATerm x Numeric if (fATermSet.contains(c1) && fNumericSet.contains(c2)) { + setAppliedRule(p2, "SB6 ATerm x Numeric"); continue; } - // Rule (7). (Upper | Lower) ATerm x Uppper if ((fUpperSet.contains(c0) || fLowerSet.contains(c0)) && fATermSet.contains(c1) && fUpperSet.contains(c2)) { + setAppliedRule(p2, "SB7 (Upper | Lower) ATerm x Uppper"); continue; } - // Rule (8) ATerm Close* Sp* x (not (OLettter | Upper | Lower | Sep))* Lower // Note: Sterm | ATerm are added to the negated part of the expression by a // note to the Unicode 5.0 documents. int p8 = p1; @@ -1655,16 +1707,17 @@ public class RBBITestMonkey extends TestFmwk { fLowerSet.contains(c) || fSepSet.contains(c) || fATermSet.contains(c) || fSTermSet.contains(c)) { + setAppliedRule(p2, "SB8 ATerm Close* Sp* x (not (OLettter | Upper | Lower | Sep))* Lower"); break; } p8 = moveForward(p8); } if (p8<fText.length() && fLowerSet.contains(cAt(p8))) { + setAppliedRule(p2, "SB8 ATerm Close* Sp* x (not (OLettter | Upper | Lower | Sep))* Lower"); continue; } } - // Rule 8a (STerm | ATerm) Close* Sp* x (SContinue | Sterm | ATerm) if (fSContinueSet.contains(c2) || fSTermSet.contains(c2) || fATermSet.contains(c2)) { p8 = p1; while (setContains(fSpSet, cAt(p8))) { @@ -1675,12 +1728,12 @@ public class RBBITestMonkey extends TestFmwk { } c = cAt(p8); if (setContains(fSTermSet, c) || setContains(fATermSet, c)) { + setAppliedRule(p2, "SB8a (STerm | ATerm) Close* Sp* x (SContinue | Sterm | ATerm)"); continue; } } - // Rule (9) (STerm | ATerm) Close* x (Close | Sp | Sep | CR | LF) int p9 = p1; while (p9>0 && fCloseSet.contains(cAt(p9))) { p9 = moveBack(p9); @@ -1688,11 +1741,11 @@ public class RBBITestMonkey extends TestFmwk { c = cAt(p9); if ((fSTermSet.contains(c) || fATermSet.contains(c))) { if (fCloseSet.contains(c2) || fSpSet.contains(c2) || fSepSet.contains(c2)) { + setAppliedRule(p2, "SB9 (STerm | ATerm) Close* x (Close | Sp | Sep | CR | LF)"); continue; } } - // Rule (10) (Sterm | ATerm) Close* Sp* x (Sp | Sep | CR | LF) int p10 = p1; while (p10>0 && fSpSet.contains(cAt(p10))) { p10 = moveBack(p10); @@ -1702,11 +1755,11 @@ public class RBBITestMonkey extends TestFmwk { } if (fSTermSet.contains(cAt(p10)) || fATermSet.contains(cAt(p10))) { if (fSpSet.contains(c2) || fSepSet.contains(c2)) { + setAppliedRule(p2, "SB10 (Sterm | ATerm) Close* Sp* x (Sp | Sep | CR | LF)"); continue; } } - // Rule (11) (STerm | ATerm) Close* Sp* <break> int p11 = p1; if (p11>0 && fSepSet.contains(cAt(p11))) { p11 = moveBack(p11); @@ -1718,18 +1771,16 @@ public class RBBITestMonkey extends TestFmwk { p11 = moveBack(p11); } if (fSTermSet.contains(cAt(p11)) || fATermSet.contains(cAt(p11))) { + setAppliedRule(p2, "SB11 (STerm | ATerm) Close* Sp* <break>"); break; } - // Rule (12) Any x Any + setAppliedRule(p2, "SB12 Any x Any"); continue; } breakPos = p2; return breakPos; } - - - } @@ -1879,7 +1930,6 @@ public class RBBITestMonkey extends TestFmwk { StringBuffer testText = new StringBuffer(); int numCharClasses; List chClasses; - int[] expected = new int[TESTSTRINGLEN*2 + 1]; int expectedCount = 0; boolean[] expectedBreaks = new boolean[TESTSTRINGLEN*2 + 1]; boolean[] forwardBreaks = new boolean[TESTSTRINGLEN*2 + 1]; @@ -1926,6 +1976,9 @@ public class RBBITestMonkey extends TestFmwk { // //-------------------------------------------------------------------------------------------- + // For minimizing width of class name output. + int classNameSize = mk.maxClassNameSize(); + int dotsOnLine = 0; while (loopCount < numIterations || numIterations == -1) { if (numIterations == -1 && loopCount % 10 == 0) { @@ -1969,7 +2022,6 @@ public class RBBITestMonkey extends TestFmwk { System.out.println(); } - Arrays.fill(expected, 0); Arrays.fill(expectedBreaks, false); Arrays.fill(forwardBreaks, false); Arrays.fill(reverseBreaks, false); @@ -1977,11 +2029,10 @@ public class RBBITestMonkey extends TestFmwk { Arrays.fill(followingBreaks, false); Arrays.fill(precedingBreaks, false); - // Calculate the expected results for this test string. + // Calculate the expected results for this test string and reset applied rules. mk.setText(testText); expectedCount = 0; expectedBreaks[0] = true; - expected[expectedCount ++] = 0; int breakPos = 0; int lastBreakPos = -1; for (;;) { @@ -1998,7 +2049,6 @@ public class RBBITestMonkey extends TestFmwk { // break; } expectedBreaks[breakPos] = true; - expected[expectedCount ++] = breakPos; } // Find the break positions using forward iteration @@ -2079,16 +2129,22 @@ public class RBBITestMonkey extends TestFmwk { // Compare the expected and actual results. for (i=0; i<=testText.length(); i++) { String errorType = null; + boolean[] currentBreakData = null; if (forwardBreaks[i] != expectedBreaks[i]) { errorType = "next()"; + currentBreakData = forwardBreaks; } else if (reverseBreaks[i] != forwardBreaks[i]) { errorType = "previous()"; + currentBreakData = reverseBreaks; } else if (isBoundaryBreaks[i] != expectedBreaks[i]) { errorType = "isBoundary()"; + currentBreakData = isBoundaryBreaks; } else if (followingBreaks[i] != expectedBreaks[i]) { errorType = "following()"; + currentBreakData = followingBreaks; } else if (precedingBreaks[i] != expectedBreaks[i]) { errorType = "preceding()"; + currentBreakData = precedingBreaks; } if (errorType != null) { @@ -2122,43 +2178,47 @@ public class RBBITestMonkey extends TestFmwk { } } - // Format looks like "<data><>\uabcd\uabcd<>\U0001abcd...</data>" - StringBuffer errorText = new StringBuffer(); - - int c; // Char from test data + // Formatting of each line includes: + // character code + // reference break: '|' -> a break, '.' -> no break + // actual break: '|' -> a break, '.' -> no break + // (name of character clase) + // Unicode name of character + // '--→' indicates location of the difference. + + StringBuilder buffer = new StringBuilder(); + buffer.append("\n") + .append((expectedBreaks[i] ? "Break expected but not found." : "Break found but not expected.")) + .append( + String.format(" at index %d. Parameters to reproduce: @\"type=%s seed=%d loop=1\"\n", + i, name, seed)); + + int c; // Char from test data for (ci = startContext; ci <= endContext && ci != -1; ci = nextCP(testText, ci)) { - if (ci == i) { - // This is the location of the error. - errorText.append("<?>---------------------------------\n"); - } else if (expectedBreaks[ci]) { - // This a non-error expected break position. - errorText.append("------------------------------------\n"); - } - if (ci < testText.length()) { - c = UTF16.charAt(testText, ci); - appendCharToBuf(errorText, c, 11); - String gc = UCharacter.getPropertyValueName(UProperty.GENERAL_CATEGORY, UCharacter.getType(c), UProperty.NameChoice.SHORT); - appendToBuf(errorText, gc, 8); - int extraProp = UCharacter.getIntPropertyValue(c, mk.fCharProperty); - String extraPropValue = - UCharacter.getPropertyValueName(mk.fCharProperty, extraProp, UProperty.NameChoice.LONG); - appendToBuf(errorText, extraPropValue, 20); - - String charName = UCharacter.getExtendedName(c); - appendToBuf(errorText, charName, 40); - errorText.append('\n'); + + c = testText.codePointAt(ci); + buffer.append((ci == i) ? " --→" : " ") + .append(String.format(" %3d : ", ci)) + .append(!expectedBreaks[ci] ? " . " : " | ") // Reference break + .append(!currentBreakData[ci] ? " . " : " | "); // Actual break + + // BMP or SMP character in hex + if (c >= 0x10000) { + buffer.append("\\U").append(String.format("%08x", (int) c)); + } else { + buffer.append(" \\u").append(String.format("%04x", (int) c)); } + + buffer.append( + String.format(String.format(" %%-%ds", (int) classNameSize), + mk.classNameFromCodepoint(c))) + .append(String.format(" %-40s", mk.getAppliedRule(ci))) + .append(String.format(" %-40s\n", UCharacter.getExtendedName(c))); + + if (ci >= endContext) { break; } } - if (ci == testText.length() && ci != -1) { - errorText.append("<>"); - } - errorText.append("</data>\n"); + errln(buffer.toString()); - // Output the error - errln(name + " break monkey test error. " + - (expectedBreaks[i]? "Break expected but not found." : "Break found but not expected.") + - "\nOperation = " + errorType + "; random seed = " + seed + "; buf Idx = " + i + "\n" + - errorText); break; } } diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/rbbi/rbbitst.txt b/android_icu4j/src/main/tests/android/icu/dev/test/rbbi/rbbitst.txt index 240dbddf2..9962f94e4 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/rbbi/rbbitst.txt +++ b/android_icu4j/src/main/tests/android/icu/dev/test/rbbi/rbbitst.txt @@ -1991,3 +1991,23 @@ Bangkok)•</data> • •より<400>詳しい<400>こと<400>を<400>お<400>知<400>り<400>に<400>なり<400>たい<400>方<400>は<400>、•Glossary<200>,• •Technical<200> •Introduction<200> •および<400> •Useful<200> •Resources<200>を<400>ご<400>参照<400>くだ<400>さい<400>。• •</data> + + +# +# Bug 20303 Multiple Look-ahead rules with similar contexts. +# Check that samples of such rules are being handled correctly. +# + +<rules> +!!forward; +!!quoted_literals_only; +!!chain; +[a] [b] / [c] [d]; +[a] [b] / [c] [d] {100}; +[a] [b] / [e] [f] {200}; +[a] [b] / [e] [g] {300}; +[a] [b] [c] [h] {400}; +[x] [a] [b] / [c] [d] {500}; +[y] [a] [b] [c] [d] {600}; +</rules> +<data>•ab<100>c•d•ab<200>e•f•ab<300>e•g•abch<400>xab<500>c•d•yabcd<600></data> diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/FormatHandler.java b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/FormatHandler.java index 2d8d3e9b9..9ed7d9cdd 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/FormatHandler.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/FormatHandler.java @@ -31,6 +31,7 @@ import android.icu.text.DateIntervalInfo; import android.icu.text.DecimalFormat; import android.icu.text.DecimalFormatSymbols; import android.icu.text.DurationFormat; +import android.icu.text.ListFormatter; import android.icu.text.MessageFormat; import android.icu.text.NumberFormat; import android.icu.text.PluralFormat; @@ -1834,6 +1835,36 @@ public class FormatHandler } } + public static class ListFormatterFieldHandler implements SerializableTestUtility.Handler + { + @Override + public Object[] getTestObjects() + { + return new Object[] {ListFormatter.Field.ELEMENT, ListFormatter.Field.LITERAL}; + } + + @Override + public boolean hasSameBehavior(Object a, Object b) + { + return (a == b); + } + } + + public static class ListFormatterSpanFieldHandler implements SerializableTestUtility.Handler + { + @Override + public Object[] getTestObjects() + { + return new Object[] {ListFormatter.SpanField.LIST_SPAN}; + } + + @Override + public boolean hasSameBehavior(Object a, Object b) + { + return (a == b); + } + } + public static class DateFormatHandler implements SerializableTestUtility.Handler { static HashMap cannedPatterns = new HashMap(); diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/SerializableTestUtility.java b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/SerializableTestUtility.java index aca092de4..acc94028f 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/SerializableTestUtility.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/SerializableTestUtility.java @@ -823,6 +823,8 @@ public class SerializableTestUtility { map.put("android.icu.text.MessageFormat$Field", new FormatHandler.MessageFormatFieldHandler()); map.put("android.icu.text.RelativeDateTimeFormatter$Field", new FormatHandler.RelativeDateTimeFormatterFieldHandler()); map.put("android.icu.text.DateIntervalFormat$SpanField", new FormatHandler.DateIntervalSpanFieldHandler()); + map.put("android.icu.text.ListFormatter$Field", new FormatHandler.ListFormatterFieldHandler()); + map.put("android.icu.text.ListFormatter$SpanField", new FormatHandler.ListFormatterSpanFieldHandler()); map.put("android.icu.impl.duration.BasicDurationFormat", new FormatHandler.BasicDurationFormatHandler()); map.put("android.icu.impl.RelativeDateFormat", new FormatHandler.RelativeDateFormatHandler()); diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.IllegalIcuArgumentException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.IllegalIcuArgumentException.dat Binary files differdeleted file mode 100644 index 9b78984e9..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.IllegalIcuArgumentException.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.InvalidFormatException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.InvalidFormatException.dat Binary files differdeleted file mode 100644 index da8f973bd..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.InvalidFormatException.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.locale.LocaleSyntaxException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.locale.LocaleSyntaxException.dat Binary files differdeleted file mode 100644 index 4798049bb..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.locale.LocaleSyntaxException.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.number.SkeletonSyntaxException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.number.SkeletonSyntaxException.dat Binary files differdeleted file mode 100644 index 2a00128af..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.number.SkeletonSyntaxException.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.ArabicShapingException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.ArabicShapingException.dat Binary files differdeleted file mode 100644 index 80d91a70d..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.ArabicShapingException.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.ChineseDateFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.ChineseDateFormat.dat Binary files differdeleted file mode 100644 index 8eb1f6c70..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.ChineseDateFormat.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.ChineseDateFormatSymbols.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.ChineseDateFormatSymbols.dat Binary files differdeleted file mode 100644 index 5da5423f2..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.ChineseDateFormatSymbols.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateFormat.dat Binary files differdeleted file mode 100644 index 63f5bf19f..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateFormat.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateFormatSymbols.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateFormatSymbols.dat Binary files differdeleted file mode 100644 index 976171fcc..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateFormatSymbols.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DecimalFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DecimalFormat.dat Binary files differdeleted file mode 100644 index 29efabacb..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DecimalFormat.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.SimpleDateFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.SimpleDateFormat.dat Binary files differdeleted file mode 100644 index 1d53bbdf4..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.SimpleDateFormat.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.StringPrepParseException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.StringPrepParseException.dat Binary files differdeleted file mode 100644 index c713de0f0..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.StringPrepParseException.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.BuddhistCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.BuddhistCalendar.dat Binary files differdeleted file mode 100644 index fee1a26ce..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.BuddhistCalendar.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ChineseCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ChineseCalendar.dat Binary files differdeleted file mode 100644 index d0bc59af7..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ChineseCalendar.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.DangiCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.DangiCalendar.dat Binary files differdeleted file mode 100644 index a6a2e6cd2..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.DangiCalendar.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.EthiopicCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.EthiopicCalendar.dat Binary files differdeleted file mode 100644 index fc8f092cf..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.EthiopicCalendar.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.HebrewCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.HebrewCalendar.dat Binary files differdeleted file mode 100644 index f5056c8e9..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.HebrewCalendar.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ICUCloneNotSupportedException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ICUCloneNotSupportedException.dat Binary files differdeleted file mode 100644 index 77b327fdc..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ICUCloneNotSupportedException.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ICUException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ICUException.dat Binary files differdeleted file mode 100644 index 123aaf008..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ICUException.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ICUUncheckedIOException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ICUUncheckedIOException.dat Binary files differdeleted file mode 100644 index 9bdef522f..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ICUUncheckedIOException.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.IllformedLocaleException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.IllformedLocaleException.dat Binary files differdeleted file mode 100644 index 2a66dbd8f..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.IllformedLocaleException.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.IndianCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.IndianCalendar.dat Binary files differdeleted file mode 100644 index 2518d9c7c..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.IndianCalendar.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.JapaneseCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.JapaneseCalendar.dat Binary files differdeleted file mode 100644 index b7e866ede..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.JapaneseCalendar.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.PersianCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.PersianCalendar.dat Binary files differdeleted file mode 100644 index 3f842b076..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.PersianCalendar.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.TaiwanCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.TaiwanCalendar.dat Binary files differdeleted file mode 100644 index 339b00147..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.TaiwanCalendar.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.UResourceTypeMismatchException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.UResourceTypeMismatchException.dat Binary files differdeleted file mode 100644 index 87d044695..000000000 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.UResourceTypeMismatchException.dat +++ /dev/null diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.DateNumberFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.DateNumberFormat.dat Binary files differindex ab393f69c..ab393f69c 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.DateNumberFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.DateNumberFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.IllegalIcuArgumentException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.IllegalIcuArgumentException.dat Binary files differnew file mode 100644 index 000000000..eecd041d2 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.IllegalIcuArgumentException.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.InvalidFormatException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.InvalidFormatException.dat Binary files differnew file mode 100644 index 000000000..e5e894448 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.InvalidFormatException.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.OlsonTimeZone.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.OlsonTimeZone.dat Binary files differindex 22947440d..13b444859 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.OlsonTimeZone.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.OlsonTimeZone.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.RelativeDateFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.RelativeDateFormat.dat Binary files differindex 727baf3af..b9c034bae 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.RelativeDateFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.RelativeDateFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.TZDBTimeZoneNames.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.TZDBTimeZoneNames.dat Binary files differindex be0463283..be0463283 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.TZDBTimeZoneNames.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.TZDBTimeZoneNames.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.TimeZoneAdapter.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.TimeZoneAdapter.dat Binary files differindex f284bee10..6ebd7fe7a 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.TimeZoneAdapter.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.TimeZoneAdapter.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.TimeZoneGenericNames.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.TimeZoneGenericNames.dat Binary files differindex de19e0718..de19e0718 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.TimeZoneGenericNames.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.TimeZoneGenericNames.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.TimeZoneNamesImpl.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.TimeZoneNamesImpl.dat Binary files differindex c04af70cf..c04af70cf 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.TimeZoneNamesImpl.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.TimeZoneNamesImpl.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.duration.BasicDurationFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.duration.BasicDurationFormat.dat Binary files differindex 3bfb44296..3bfb44296 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.duration.BasicDurationFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.duration.BasicDurationFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.locale.LocaleSyntaxException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.locale.LocaleSyntaxException.dat Binary files differnew file mode 100644 index 000000000..431275f82 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.locale.LocaleSyntaxException.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.number.CustomSymbolCurrency.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.number.CustomSymbolCurrency.dat Binary files differindex 7566c1dd6..7566c1dd6 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.number.CustomSymbolCurrency.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.number.CustomSymbolCurrency.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.number.DecimalFormatProperties.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.number.DecimalFormatProperties.dat Binary files differindex 1c6023b29..1c6023b29 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.number.DecimalFormatProperties.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.number.DecimalFormatProperties.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.number.LocalizedNumberFormatterAsFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.number.LocalizedNumberFormatterAsFormat.dat Binary files differindex edb3d5dd1..edb3d5dd1 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.number.LocalizedNumberFormatterAsFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.number.LocalizedNumberFormatterAsFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.number.Properties.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.number.Properties.dat Binary files differindex 3e0c8db45..3e0c8db45 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.impl.number.Properties.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.impl.number.Properties.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.math.BigDecimal.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.math.BigDecimal.dat Binary files differindex dd4ce5221..dd4ce5221 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.math.BigDecimal.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.math.BigDecimal.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.math.MathContext.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.math.MathContext.dat Binary files differindex da7116766..da7116766 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.math.MathContext.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.math.MathContext.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.number.SkeletonSyntaxException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.number.SkeletonSyntaxException.dat Binary files differnew file mode 100644 index 000000000..77f643059 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.number.SkeletonSyntaxException.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ArabicShapingException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ArabicShapingException.dat Binary files differnew file mode 100644 index 000000000..825d642e8 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ArabicShapingException.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.ChineseDateFormat$Field.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ChineseDateFormat$Field.dat Binary files differindex 1480893fe..1480893fe 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.ChineseDateFormat$Field.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ChineseDateFormat$Field.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ChineseDateFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ChineseDateFormat.dat Binary files differnew file mode 100644 index 000000000..f85bb92ed --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ChineseDateFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ChineseDateFormatSymbols.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ChineseDateFormatSymbols.dat Binary files differnew file mode 100644 index 000000000..f6681c6f9 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ChineseDateFormatSymbols.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.CompactDecimalFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.CompactDecimalFormat.dat Binary files differindex 4795b267d..4795b267d 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.CompactDecimalFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.CompactDecimalFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.CurrencyPluralInfo.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.CurrencyPluralInfo.dat Binary files differindex ea54e21b2..ea54e21b2 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.CurrencyPluralInfo.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.CurrencyPluralInfo.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateFormat$Field.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateFormat$Field.dat Binary files differindex 06b87bb1b..06b87bb1b 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateFormat$Field.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateFormat$Field.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateFormat.dat Binary files differnew file mode 100644 index 000000000..27396f433 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateFormatSymbols.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateFormatSymbols.dat Binary files differnew file mode 100644 index 000000000..0224350fd --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateFormatSymbols.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateIntervalFormat$SpanField.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateIntervalFormat$SpanField.dat Binary files differnew file mode 100644 index 000000000..d1fb9f8a5 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateIntervalFormat$SpanField.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateIntervalFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateIntervalFormat.dat Binary files differindex 8560a4198..193369dba 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateIntervalFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateIntervalFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateIntervalInfo$PatternInfo.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateIntervalInfo$PatternInfo.dat Binary files differindex 25ebef5b2..25ebef5b2 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateIntervalInfo$PatternInfo.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateIntervalInfo$PatternInfo.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateIntervalInfo.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateIntervalInfo.dat Binary files differindex 59c719a70..546ebbb23 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DateIntervalInfo.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DateIntervalInfo.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DecimalFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DecimalFormat.dat Binary files differnew file mode 100644 index 000000000..b9761f9ec --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DecimalFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DecimalFormatSymbols.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DecimalFormatSymbols.dat Binary files differindex a9e8b06a1..1a9fe1e2b 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.DecimalFormatSymbols.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.DecimalFormatSymbols.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ListFormatter$Field.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ListFormatter$Field.dat Binary files differnew file mode 100644 index 000000000..d2e613616 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ListFormatter$Field.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ListFormatter$SpanField.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ListFormatter$SpanField.dat Binary files differnew file mode 100644 index 000000000..50e3c2b04 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.ListFormatter$SpanField.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.MeasureFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.MeasureFormat.dat Binary files differindex b61ad990f..3c8ca94b4 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.MeasureFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.MeasureFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.MessageFormat$Field.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.MessageFormat$Field.dat Binary files differindex a1aca43fd..a1aca43fd 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.MessageFormat$Field.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.MessageFormat$Field.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.MessageFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.MessageFormat.dat Binary files differindex 3e8d13fe9..3e8d13fe9 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.MessageFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.MessageFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.NumberFormat$Field.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.NumberFormat$Field.dat Binary files differindex 78dbc5193..78dbc5193 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.NumberFormat$Field.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.NumberFormat$Field.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.NumberFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.NumberFormat.dat Binary files differindex b8d94928e..6c6664bf2 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.NumberFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.NumberFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.PluralFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.PluralFormat.dat Binary files differindex 6f74ae924..b60447ed2 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.PluralFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.PluralFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.PluralRules.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.PluralRules.dat Binary files differindex 8e3375ba4..8e3375ba4 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.PluralRules.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.PluralRules.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.RelativeDateTimeFormatter$Field.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.RelativeDateTimeFormatter$Field.dat Binary files differnew file mode 100644 index 000000000..9ff848c76 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.RelativeDateTimeFormatter$Field.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.RuleBasedNumberFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.RuleBasedNumberFormat.dat Binary files differindex d0b7ea93e..d0b7ea93e 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.RuleBasedNumberFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.RuleBasedNumberFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.SelectFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.SelectFormat.dat Binary files differindex bbd38651c..bbd38651c 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.SelectFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.SelectFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.SimpleDateFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.SimpleDateFormat.dat Binary files differnew file mode 100644 index 000000000..aa2351eed --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.SimpleDateFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.StringPrepParseException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.StringPrepParseException.dat Binary files differnew file mode 100644 index 000000000..1bfd404df --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.StringPrepParseException.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.TimeUnitFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.TimeUnitFormat.dat Binary files differindex 4a160eccf..7cd75ffb7 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.TimeUnitFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.TimeUnitFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.TimeZoneFormat.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.TimeZoneFormat.dat Binary files differindex 247ad4fe4..247ad4fe4 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.text.TimeZoneFormat.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.text.TimeZoneFormat.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.AnnualTimeZoneRule.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.AnnualTimeZoneRule.dat Binary files differindex da8dfef5a..da8dfef5a 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.AnnualTimeZoneRule.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.AnnualTimeZoneRule.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.BuddhistCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.BuddhistCalendar.dat Binary files differnew file mode 100644 index 000000000..ba0e8603a --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.BuddhistCalendar.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.Calendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.Calendar.dat Binary files differindex b4fd7f21a..47c3ced38 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.Calendar.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.Calendar.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ChineseCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ChineseCalendar.dat Binary files differnew file mode 100644 index 000000000..4c3a74449 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ChineseCalendar.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.CopticCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.CopticCalendar.dat Binary files differindex e3a828df6..7280bfa03 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.CopticCalendar.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.CopticCalendar.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.Currency.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.Currency.dat Binary files differindex 7566c1dd6..7566c1dd6 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.Currency.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.Currency.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.DangiCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.DangiCalendar.dat Binary files differnew file mode 100644 index 000000000..252aefbd5 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.DangiCalendar.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.DateInterval.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.DateInterval.dat Binary files differindex c30bb84f4..c30bb84f4 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.DateInterval.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.DateInterval.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.DateTimeRule.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.DateTimeRule.dat Binary files differindex 7057de988..7057de988 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.DateTimeRule.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.DateTimeRule.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.EthiopicCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.EthiopicCalendar.dat Binary files differnew file mode 100644 index 000000000..d772ef323 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.EthiopicCalendar.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.GregorianCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.GregorianCalendar.dat Binary files differindex 3c886337f..d1672cc87 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.GregorianCalendar.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.GregorianCalendar.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.HebrewCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.HebrewCalendar.dat Binary files differnew file mode 100644 index 000000000..dcb0cedf9 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.HebrewCalendar.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ICUCloneNotSupportedException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ICUCloneNotSupportedException.dat Binary files differnew file mode 100644 index 000000000..577df9362 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ICUCloneNotSupportedException.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ICUException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ICUException.dat Binary files differnew file mode 100644 index 000000000..5d8784e72 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ICUException.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ICUUncheckedIOException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ICUUncheckedIOException.dat Binary files differnew file mode 100644 index 000000000..020284881 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ICUUncheckedIOException.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.IllformedLocaleException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.IllformedLocaleException.dat Binary files differnew file mode 100644 index 000000000..bde3c7ae2 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.IllformedLocaleException.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.IndianCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.IndianCalendar.dat Binary files differnew file mode 100644 index 000000000..6ac000349 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.IndianCalendar.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.InitialTimeZoneRule.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.InitialTimeZoneRule.dat Binary files differindex cc19377dd..cc19377dd 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.InitialTimeZoneRule.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.InitialTimeZoneRule.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.IslamicCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.IslamicCalendar.dat Binary files differindex 2c80c04e1..70ac880dc 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.IslamicCalendar.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.IslamicCalendar.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.JapaneseCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.JapaneseCalendar.dat Binary files differnew file mode 100644 index 000000000..00b1462a3 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.JapaneseCalendar.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.MeasureUnit.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.MeasureUnit.dat Binary files differindex e97cd298a..e97cd298a 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.MeasureUnit.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.MeasureUnit.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.NoUnit.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.NoUnit.dat Binary files differindex e97cd298a..e97cd298a 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.NoUnit.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.NoUnit.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.PersianCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.PersianCalendar.dat Binary files differnew file mode 100644 index 000000000..ec332d41f --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.PersianCalendar.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.RuleBasedTimeZone.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.RuleBasedTimeZone.dat Binary files differindex 9271a99ef..d69978e1a 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.RuleBasedTimeZone.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.RuleBasedTimeZone.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.SimpleTimeZone.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.SimpleTimeZone.dat Binary files differindex 71d8d1a4c..71d8d1a4c 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.SimpleTimeZone.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.SimpleTimeZone.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.TaiwanCalendar.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.TaiwanCalendar.dat Binary files differnew file mode 100644 index 000000000..750ee7f43 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.TaiwanCalendar.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.TimeArrayTimeZoneRule.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.TimeArrayTimeZoneRule.dat Binary files differindex 189e4dae3..189e4dae3 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.TimeArrayTimeZoneRule.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.TimeArrayTimeZoneRule.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.TimeUnit.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.TimeUnit.dat Binary files differindex e97cd298a..e97cd298a 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.TimeUnit.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.TimeUnit.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.TimeZone.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.TimeZone.dat Binary files differindex 56e8cd29a..56e8cd29a 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.TimeZone.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.TimeZone.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ULocale.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ULocale.dat Binary files differindex b222926d5..b222926d5 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.ULocale.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.ULocale.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.UResourceTypeMismatchException.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.UResourceTypeMismatchException.dat Binary files differnew file mode 100644 index 000000000..c273bec33 --- /dev/null +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.UResourceTypeMismatchException.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.VTimeZone.dat b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.VTimeZone.dat Binary files differindex 1f69b2990..1f69b2990 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_62.1/android.icu.util.VTimeZone.dat +++ b/android_icu4j/src/main/tests/android/icu/dev/test/serializable/data/ICU_67.1/android.icu.util.VTimeZone.dat diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/timezone/TimeZoneTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/timezone/TimeZoneTest.java index 6900799be..25bd35cb6 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/timezone/TimeZoneTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/timezone/TimeZoneTest.java @@ -1790,7 +1790,6 @@ public class TimeZoneTest extends TestFmwk {"America/Sao_Paulo", "en", Boolean.FALSE, TZSHORT, "GMT-3"/*"BRT"*/}, {"America/Sao_Paulo", "en", Boolean.FALSE, TZLONG, "Brasilia Standard Time"}, - // Per https://mm.icann.org/pipermail/tz-announce/2019-July/000056.html // Brazil has canceled DST and will stay on standard time indefinitely. // {"America/Sao_Paulo", "en", Boolean.TRUE, TZSHORT, "GMT-2"/*"BRST"*/}, diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/util/CurrencyTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/util/CurrencyTest.java index bfb813a67..f29100cda 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/util/CurrencyTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/util/CurrencyTest.java @@ -55,6 +55,7 @@ public class CurrencyTest extends TestFmwk { Currency usd = Currency.getInstance("USD"); /*int hash = */usd.hashCode(); Currency jpy = Currency.getInstance("JPY"); + Currency jpy2 = Currency.getInstance("jpy"); if (usd.equals(jpy)) { errln("FAIL: USD == JPY"); } @@ -67,6 +68,12 @@ public class CurrencyTest extends TestFmwk { if (!usd.equals(usd)) { errln("FAIL: USD != USD"); } + if (!jpy.equals(jpy2)) { + errln("FAIL: JPY != jpy"); + } + if (!jpy2.equals(jpy)) { + errln("FAIL: jpy != JPY"); + } try { Currency nullCurrency = Currency.getInstance((String)null); @@ -90,7 +97,7 @@ public class CurrencyTest extends TestFmwk { } try { - usd.getName(ULocale.US, 5, new boolean[1]); + usd.getName(ULocale.US, 6, new boolean[1]); errln("expected getName with invalid type parameter to throw exception"); } catch (Exception e) { @@ -173,7 +180,7 @@ public class CurrencyTest extends TestFmwk { Locale[] locs = Currency.getAvailableLocales(); found = false; for (int i = 0; i < locs.length; ++i) { - if (locs[i].equals(fu_FU)) { + if (locs[i].equals(fu_FU.toLocale())) { found = true; break; } @@ -242,26 +249,44 @@ public class CurrencyTest extends TestFmwk { } @Test - public void test20484_NarrowSymbolFallback() { + public void testCurrencyVariants() { Object[][] cases = new Object[][] { - {"en-US", "CAD", "CA$", "$"}, - {"en-US", "CDF", "CDF", "CDF"}, - {"sw-CD", "CDF", "FC", "FC"}, - {"en-US", "GEL", "GEL", "₾"}, - {"ka-GE", "GEL", "₾", "₾"}, - {"ka", "GEL", "₾", "₾"}, + {"en-US", "CAD", "CA$", "$", "CA$", "CA$"}, + {"en-US", "CDF", "CDF", "CDF", "CDF", "CDF"}, + {"sw-CD", "CDF", "FC", "FC", "FC", "FC"}, + {"en-US", "GEL", "GEL", "₾", "GEL", "GEL"}, + {"ka-GE", "GEL", "₾", "₾", "₾", "₾"}, + {"ka", "GEL", "₾", "₾", "₾", "₾"}, + {"zh-TW", "TWD", "$", "$", "NT$", "$"}, + {"ccp", "TRY", "TRY", "₺", "TRY", "TL"} }; for (Object[] cas : cases) { ULocale locale = new ULocale((String) cas[0]); String isoCode = (String) cas[1]; String expectedShort = (String) cas[2]; String expectedNarrow = (String) cas[3]; + String expectedFormal = (String) cas[4]; + String expectedVariant = (String) cas[5]; CurrencyDisplayNames cdn = CurrencyDisplayNames.getInstance(locale); assertEquals("Short symbol: " + locale + ": " + isoCode, expectedShort, cdn.getSymbol(isoCode)); assertEquals("Narrow symbol: " + locale + ": " + isoCode, expectedNarrow, cdn.getNarrowSymbol(isoCode)); + assertEquals("Formal symbol: " + locale + ": " + isoCode, + expectedFormal, cdn.getFormalSymbol(isoCode)); + assertEquals("Variant symbol: " + locale + ": " + isoCode, + expectedVariant, cdn.getVariantSymbol(isoCode)); + + Currency currency = Currency.getInstance(isoCode); + assertEquals("Old API, Short symbol: " + locale + ": " + isoCode, + expectedShort, currency.getName(locale, Currency.SYMBOL_NAME, null)); + assertEquals("Old API, Narrow symbol: " + locale + ": " + isoCode, + expectedNarrow, currency.getName(locale, Currency.NARROW_SYMBOL_NAME, null)); + assertEquals("Old API, Formal symbol: " + locale + ": " + isoCode, + expectedFormal, currency.getName(locale, Currency.FORMAL_SYMBOL_NAME, null)); + assertEquals("Old API, Variant symbol: " + locale + ": " + isoCode, + expectedVariant, currency.getName(locale, Currency.VARIANT_SYMBOL_NAME, null)); } } @@ -544,7 +569,7 @@ public class CurrencyTest extends TestFmwk { assertTrue("More than one currency for switzerland", currencies.size() > 1); assertEquals( "With tender", - Arrays.asList(new String[] {"CHF", "CHE", "CHW"}), + Arrays.asList(new String[] {"CHF"}), // no longer include currencies with tender=false metainfo.currencies(filter.withTender())); } @@ -652,8 +677,8 @@ public class CurrencyTest extends TestFmwk { { "eo_AO", "1969-12-31" }, { "eo_DE@currency=DEM", "2000-12-23", "EUR", "DEM" }, { "eo-DE-u-cu-dem", "2000-12-23", "EUR", "DEM" }, - { "en_US", null, "USD", "USN" }, - { "en_US_Q", null, "USD", "USN" }, + { "en_US", null, "USD" }, // no longer include currencies with tender=false + { "en_US_Q", null, "USD" }, // no longer include currencies with tender=false }; DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd", Locale.US); @@ -741,20 +766,20 @@ public class CurrencyTest extends TestFmwk { final String[][] PREFERRED = { {"root", }, {"und", }, - {"und_ZZ", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XDR", "XPD", "XPT", "XSU", "XTS", "XUA", "XXX"}, - {"en_US", "USD", "USN"}, + {"und_ZZ", }, // no longer include currencies with tender=false + {"en_US", "USD"}, // no longer include currencies with tender=false {"en_029", }, {"en_TH", "THB"}, {"de", "EUR"}, {"de_DE", "EUR"}, - {"de_ZZ", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XDR", "XPD", "XPT", "XSU", "XTS", "XUA", "XXX"}, + {"de_ZZ", }, // no longer include currencies with tender=false {"ar", "EGP"}, {"ar_PS", "ILS", "JOD"}, - {"en@currency=CAD", "USD", "USN"}, + {"en@currency=CAD", "USD"}, // no longer include currencies with tender=false {"fr@currency=ZZZ", "EUR"}, {"de_DE@currency=DEM", "EUR"}, {"en_US@rg=THZZZZ", "THB"}, - {"de@rg=USZZZZ", "USD", "USN"}, + {"de@rg=USZZZZ", "USD"}, // no longer include currencies with tender=false {"en_US@currency=CAD;rg=THZZZZ", "THB"}, }; diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/util/DebugUtilitiesData.java b/android_icu4j/src/main/tests/android/icu/dev/test/util/DebugUtilitiesData.java index 29e4f17b2..0f609cc57 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/util/DebugUtilitiesData.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/util/DebugUtilitiesData.java @@ -14,7 +14,7 @@ import android.icu.testsharding.MainTestShard; @MainTestShard public class DebugUtilitiesData extends Object { - public static final String ICU4C_VERSION="66.1"; + public static final String ICU4C_VERSION="67.1"; public static final int UDebugEnumType = 0; public static final int UCalendarDateFields = 1; public static final int UCalendarMonths = 2; diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/util/ICUResourceBundleTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/util/ICUResourceBundleTest.java index 939323380..b172b5c7a 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/util/ICUResourceBundleTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/util/ICUResourceBundleTest.java @@ -704,14 +704,13 @@ public final class ICUResourceBundleTest extends TestFmwk { Set<String> localCountryExceptions = new HashSet<String>(); if (logKnownIssue("cldrbug:8903", - "No localized region name for lrc_IQ, lrc_IR, nus_SS, nds_DE, ti_ER, ti_ET")) { + "No localized region name for lrc_IQ, lrc_IR, nus_SS, nds_DE, su_Latn_ID")) { localCountryExceptions.add("lrc_IQ"); localCountryExceptions.add("lrc_IR"); localCountryExceptions.add("nus_SS"); localCountryExceptions.add("nds_DE"); localCountryExceptions.add("nds_NL"); - localCountryExceptions.add("ti_ER"); - localCountryExceptions.add("ti_ET"); + localCountryExceptions.add("su_Latn_ID"); } Set<String> localLangExceptions = new HashSet<String>(); diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/util/LocaleBuilderTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/util/LocaleBuilderTest.java index 6cba79c7a..90b7a6f21 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/util/LocaleBuilderTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/util/LocaleBuilderTest.java @@ -76,7 +76,7 @@ public class LocaleBuilderTest extends TestFmwk { {"U", "ja_JP@calendar=japanese;currency=JPY", "L", "ko", "T", "ko-JP-u-ca-japanese-cu-jpy", "ko_JP@calendar=japanese;currency=jpy"}, {"U", "ja_JP@calendar=japanese;currency=JPY", "K", "ca", null, "T", "ja-JP-u-cu-jpy", "ja_JP@currency=jpy"}, {"U", "ja_JP@calendar=japanese;currency=JPY", "E", "u", "attr1-ca-gregory", "T", "ja-JP-u-attr1-ca-gregory", "ja_JP@attribute=attr1;calendar=gregorian"}, - {"U", "en@colnumeric=yes", "K", "kn", "", "T", "en-u-kn-true", "en@colnumeric=yes"}, + {"U", "en@colnumeric=yes", "K", "kn", "", "T", "en-u-kn", "en@colnumeric=yes"}, {"L", "th", "R", "th", "K", "nu", "thai", "T", "th-TH-u-nu-thai", "th_TH@numbers=thai"}, {"U", "zh_Hans", "R", "sg", "K", "ca", "badcalendar", "X"}, {"U", "zh_Hans", "R", "sg", "K", "cal", "gregory", "X"}, @@ -90,18 +90,18 @@ public class LocaleBuilderTest extends TestFmwk { // However, once the legacy keyword is translated back to BCP 47 u extension, key "0a" is unknown, // so "yes" is preserved - not mapped to "true". We could change the code to automatically transform // "yes" to "true", but it will break roundtrip conversion if BCP 47 u extension has "0a-yes". - {"L", "en", "E", "u", "bbb-aaa-0a", "T", "en-u-aaa-bbb-0a-yes", "en@0a=yes;attribute=aaa-bbb"}, + {"L", "en", "E", "u", "bbb-aaa-0a", "T", "en-u-aaa-bbb-0a", "en@0a=yes;attribute=aaa-bbb"}, {"L", "fr", "R", "FR", "P", "Yoshito-ICU", "T", "fr-FR-x-yoshito-icu", "fr_FR@x=yoshito-icu"}, {"L", "ja", "R", "jp", "K", "ca", "japanese", "T", "ja-JP-u-ca-japanese", "ja_JP@calendar=japanese"}, {"K", "co", "PHONEBK", "K", "ca", "gregory", "L", "De", "T", "de-u-ca-gregory-co-phonebk", "de@calendar=gregorian;collation=phonebook"}, {"E", "o", "OPQR", "E", "a", "aBcD", "T", "und-a-abcd-o-opqr", "@a=abcd;o=opqr"}, {"E", "u", "nu-thai-ca-gregory", "L", "TH", "T", "th-u-ca-gregory-nu-thai", "th@calendar=gregorian;numbers=thai"}, {"L", "en", "K", "tz", "usnyc", "R", "US", "T", "en-US-u-tz-usnyc", "en_US@timezone=America/New_York"}, - {"L", "de", "K", "co", "phonebk", "K", "ks", "level1", "K", "kk", "true", "T", "de-u-co-phonebk-kk-true-ks-level1", "de@collation=phonebook;colnormalization=yes;colstrength=primary"}, + {"L", "de", "K", "co", "phonebk", "K", "ks", "level1", "K", "kk", "true", "T", "de-u-co-phonebk-kk-ks-level1", "de@collation=phonebook;colnormalization=yes;colstrength=primary"}, {"L", "en", "R", "US", "K", "ca", "gregory", "T", "en-US-u-ca-gregory", "en_US@calendar=gregorian"}, {"L", "en", "R", "US", "K", "cal", "gregory", "X"}, {"L", "en", "R", "US", "K", "ca", "gregorian", "X"}, - {"L", "en", "R", "US", "K", "kn", "", "T", "en-US-u-kn-true", "en_US@colnumeric=yes"}, + {"L", "en", "R", "US", "K", "kn", "", "T", "en-US-u-kn", "en_US@colnumeric=yes"}, {"B", "de-DE-u-co-phonebk", "C", "L", "pt", "T", "pt", "pt"}, {"B", "ja-jp-u-ca-japanese", "N", "T", "ja-JP", "ja_JP"}, {"B", "es-u-def-abc-co-trad", "A", "hij", "D", "def", "T", "es-u-abc-hij-co-trad", "es@attribute=abc-hij;collation=traditional"}, diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/util/LocaleMatcherTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/util/LocaleMatcherTest.java index a7dde6a94..5afb9806b 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/util/LocaleMatcherTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/util/LocaleMatcherTest.java @@ -197,7 +197,7 @@ public class LocaleMatcherTest extends TestFmwk { assertEquals("getBestMatchResult(ja_JP).supp", "en_GB", locString(result.getSupportedULocale())); assertEquals("getBestMatchResult(ja_JP).suppIndex", - 1, result.getSupportedIndex()); + -1, result.getSupportedIndex()); } @Test @@ -642,6 +642,21 @@ public class LocaleMatcherTest extends TestFmwk { } @Test + public void testDirection() { + List<ULocale> desired = Arrays.asList(new ULocale("arz-EG"), new ULocale("nb-DK")); + LocaleMatcher.Builder builder = + LocaleMatcher.builder().setSupportedLocales("ar, nn"); + // arz is a close one-way match to ar, and the region matches. + // (Egyptian Arabic vs. Arabic) + LocaleMatcher withOneWay = builder.build(); + assertEquals("with one-way", "ar", withOneWay.getBestMatch(desired).toString()); + // nb is a less close two-way match to nn, and the regions differ. + // (Norwegian Bokmal vs. Nynorsk) + LocaleMatcher onlyTwoWay = builder.setDirection(LocaleMatcher.Direction.ONLY_TWO_WAY).build(); + assertEquals("only two-way", "nn", onlyTwoWay.getBestMatch(desired).toString()); + } + + @Test public void testCanonicalize() { LocaleMatcher matcher = LocaleMatcher.builder().build(); assertEquals("bh --> bho", new ULocale("bho"), matcher.canonicalize(new ULocale("bh"))); diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/util/ULocaleTest.java b/android_icu4j/src/main/tests/android/icu/dev/test/util/ULocaleTest.java index dc9abdd6c..7c1f639e5 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/util/ULocaleTest.java +++ b/android_icu4j/src/main/tests/android/icu/dev/test/util/ULocaleTest.java @@ -23,7 +23,6 @@ import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.TreeMap; import java.util.TreeSet; import java.util.regex.Pattern; @@ -35,7 +34,6 @@ import org.junit.runners.JUnit4; import android.icu.dev.test.TestFmwk; import android.icu.dev.test.TestUtil; import android.icu.dev.test.TestUtil.JavaVendor; -import android.icu.lang.UCharacter; import android.icu.text.DateFormat; import android.icu.text.DecimalFormat; import android.icu.text.DisplayContext; @@ -47,6 +45,7 @@ import android.icu.text.SimpleDateFormat; import android.icu.util.Calendar; import android.icu.util.IllformedLocaleException; import android.icu.util.LocaleData; +import android.icu.util.LocalePriorityList; import android.icu.util.ULocale; import android.icu.util.ULocale.Builder; import android.icu.util.ULocale.Category; @@ -676,10 +675,10 @@ public class ULocaleTest extends TestFmwk { {"x-piglatin", "", "ML", "", "x-piglatin_ML.MBE", "x-piglatin_ML.MBE", "x-piglatin_ML"}, /* Multibyte English */ {"i-cherokee", "","US", "", "i-Cherokee_US.utf7", "i-cherokee_US.utf7", "i-cherokee_US"}, {"x-filfli", "", "MT", "FILFLA", "x-filfli_MT_FILFLA.gb-18030", "x-filfli_MT_FILFLA.gb-18030", "x-filfli_MT_FILFLA"}, - {"no", "", "NO", "NY_B", "no-no-ny.utf32@B", "no_NO_NY.utf32@B", "no_NO_NY_B"}, - {"no", "", "NO", "B", "no-no.utf32@B", "no_NO.utf32@B", "no_NO_B"}, - {"no", "", "", "NY", "no__ny", "no__NY", null}, - {"no", "", "", "NY", "no@ny", "no@ny", "no__NY"}, + {"no", "", "NO", "NY_B", "no-no-ny.utf32@B", "no_NO_NY.utf32@B", "nb_NO_NY_B"}, + {"no", "", "NO", "B", "no-no.utf32@B", "no_NO.utf32@B", "nb_NO_B"}, + {"no", "", "", "NY", "no__ny", "no__NY", "nb__NY"}, + {"no", "", "", "NY", "no@ny", "no@ny", "nb__NY"}, {"el", "Latn", "", "", "el-latn", "el_Latn", null}, {"en", "Cyrl", "RU", "", "en-cyrl-ru", "en_Cyrl_RU", null}, {"qq", "Qqqq", "QQ", "QQ", "qq_Qqqq_QQ_QQ", "qq_Qqqq_QQ_QQ", null}, @@ -896,13 +895,13 @@ public class ULocaleTest extends TestFmwk { public void TestCanonicalization(){ final String[][]testCases = new String[][]{ { "zh@collation=pinyin", "zh@collation=pinyin", "zh@collation=pinyin" }, - { "zh_CN@collation=pinyin", "zh_CN@collation=pinyin", "zh_CN@collation=pinyin" }, - { "zh_CN_CA@collation=pinyin", "zh_CN_CA@collation=pinyin", "zh_CN_CA@collation=pinyin" }, + { "zh_CN@collation=pinyin", "zh_CN@collation=pinyin", "zh_Hans_CN@collation=pinyin" }, + { "zh_CN_CA@collation=pinyin", "zh_CN_CA@collation=pinyin", "zh_Hans_CN_CA@collation=pinyin" }, { "en_US_POSIX", "en_US_POSIX", "en_US_POSIX" }, { "hy_AM_REVISED", "hy_AM_REVISED", "hy_AM_REVISED" }, - { "no_NO_NY", "no_NO_NY", "no_NO_NY" /* not: "nn_NO" [alan ICU3.0] */ }, - { "no@ny", null, "no__NY" /* not: "nn" [alan ICU3.0] */ }, /* POSIX ID */ - { "no-no.utf32@B", null, "no_NO_B" /* not: "nb_NO_B" [alan ICU3.0] */ }, /* POSIX ID */ + { "no_NO_NY", "no_NO_NY", "nb_NO_NY" /* not: "nn_NO" [alan ICU3.0] */ }, + { "no@ny", null, "nb__NY" /* not: "nn" [alan ICU3.0] */ }, /* POSIX ID */ + { "no-no.utf32@B", null, "nb_NO_B" /* not: "nb_NO_B" [alan ICU3.0] */ }, /* POSIX ID */ { "en-BOONT", "en__BOONT", "en__BOONT" }, /* registered name */ { "de-1901", "de__1901", "de__1901" }, /* registered name */ { "de-1906", "de__1906", "de__1906" }, /* registered name */ @@ -913,7 +912,7 @@ public class ULocaleTest extends TestFmwk { { "x-piglatin_ML.MBE", null, "x-piglatin_ML" }, { "i-cherokee_US.utf7", null, "i-cherokee_US" }, { "x-filfli_MT_FILFLA.gb-18030", null, "x-filfli_MT_FILFLA" }, - { "no-no-ny.utf8@B", null, "no_NO_NY_B" /* not: "nn_NO" [alan ICU3.0] */ }, /* @ ignored unless variant is empty */ + { "no-no-ny.utf8@B", null, "nb_NO_NY_B" /* not: "nn_NO" [alan ICU3.0] */ }, /* @ ignored unless variant is empty */ /* fleshing out canonicalization */ /* sort keywords, ';' is separator so not present at end in canonical form */ @@ -922,7 +921,7 @@ public class ULocaleTest extends TestFmwk { { "en_Hant_IL_VALLEY_GIRL@calendar=Japanese;currency=EUR", "en_Hant_IL_VALLEY_GIRL@calendar=Japanese;currency=EUR", "en_Hant_IL_VALLEY_GIRL@calendar=Japanese;currency=EUR" }, /* norwegian is just too weird, if we handle things in their full generality */ /* this is a negative test to show that we DO NOT handle 'lang=no,var=NY' specially. */ - { "no-Hant-GB_NY@currency=$$$", "no_Hant_GB_NY@currency=$$$", "no_Hant_GB_NY@currency=$$$" /* not: "nn_Hant_GB@currency=$$$" [alan ICU3.0] */ }, + { "no-Hant-GB_NY@currency=$$$", "no_Hant_GB_NY@currency=$$$", "nb_Hant_GB_NY@currency=$$$" /* not: "nn_Hant_GB@currency=$$$" [alan ICU3.0] */ }, /* test cases reflecting internal resource bundle usage */ /* root is just a language */ @@ -960,14 +959,14 @@ public class ULocaleTest extends TestFmwk { { "hi__DIRECT", "hi__DIRECT", "hi__DIRECT" }, { "ja_JP_TRADITIONAL", "ja_JP_TRADITIONAL", "ja_JP_TRADITIONAL" }, { "th_TH_TRADITIONAL", "th_TH_TRADITIONAL", "th_TH_TRADITIONAL" }, - { "zh_TW_STROKE", "zh_TW_STROKE", "zh_TW_STROKE" }, + { "zh_TW_STROKE", "zh_TW_STROKE", "zh_Hant_TW_STROKE" }, { "zh__PINYIN", "zh__PINYIN", "zh__PINYIN" }, { "qz-qz@Euro", null, "qz_QZ_EURO" }, /* qz-qz uses private use iso codes */ { "sr-SP-Cyrl", "sr_SP_CYRL", "sr_SP_CYRL" }, /* .NET name */ { "sr-SP-Latn", "sr_SP_LATN", "sr_SP_LATN" }, /* .NET name */ - { "sr_YU_CYRILLIC", "sr_YU_CYRILLIC", "sr_YU_CYRILLIC" }, /* Linux name */ - { "uz-UZ-Cyrl", "uz_UZ_CYRL", "uz_UZ_CYRL" }, /* .NET name */ - { "uz-UZ-Latn", "uz_UZ_LATN", "uz_UZ_LATN" }, /* .NET name */ + { "sr_YU_CYRILLIC", "sr_YU_CYRILLIC", "sr_RS_CYRILLIC" }, /* Linux name */ + { "uz-UZ-Cyrl", "uz_UZ_CYRL", "uz_Latn_UZ_CYRL" }, /* .NET name */ + { "uz-UZ-Latn", "uz_UZ_LATN", "uz_Latn_UZ_LATN" }, /* .NET name */ { "zh-CHS", "zh_CHS", "zh_CHS" }, /* .NET name */ { "zh-CHT", "zh_CHT", "zh_CHT" }, /* .NET name This may change back to zh_Hant */ /* PRE_EURO and EURO conversions don't affect other keywords */ @@ -1593,15 +1592,15 @@ public class ULocaleTest extends TestFmwk { /*3*/ { null, "true" }, /*4*/ { "es", "false" }, /*5*/ { "de", "false" }, - /*6*/ { "zh_TW", "false" }, - /*7*/ { "zh", "true" }, + /*6*/ { "zh_Hant_TW", "true" }, + /*7*/ { "zh_Hant", "true" }, }; private static final String ACCEPT_LANGUAGE_HTTP[] = { /*0*/ "mt-mt, ja;q=0.76, en-us;q=0.95, en;q=0.92, en-gb;q=0.89, fr;q=0.87, iu-ca;q=0.84, iu;q=0.82, ja-jp;q=0.79, mt;q=0.97, de-de;q=0.74, de;q=0.71, es;q=0.68, it-it;q=0.66, it;q=0.63, vi-vn;q=0.61, vi;q=0.58, nl-nl;q=0.55, nl;q=0.53, th-th-traditional;q=.01", /*1*/ "ja;q=0.5, en;q=0.8, tlh", /*2*/ "en-zzz, de-lx;q=0.8", - /*3*/ "mga-ie;q=0.9, tlh", + /*3*/ "mga-ie;q=0.9, sux", /*4*/ "xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, "+ "xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, "+ "xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, "+ @@ -1613,16 +1612,16 @@ public class ULocaleTest extends TestFmwk { "xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, "+ "xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, xxx-yyy;q=.01, "+ "es", - /*5*/ "de;q=.9, fr;q=.9, xxx-yyy, sr;q=.8", - /*6*/ "zh-tw", - /*7*/ "zh-hant-cn", + /*5*/ "de;q=.9, fr;q=.9, xxx-yyy, sr;q=.8", + /*6*/ "zh-tw", + /*7*/ "zh-hant-cn", }; @Test public void TestAcceptLanguage() { for(int i = 0 ; i < (ACCEPT_LANGUAGE_HTTP.length); i++) { - Boolean expectBoolean = new Boolean(ACCEPT_LANGUAGE_TESTS[i][1]); + Boolean expectBoolean = Boolean.valueOf(ACCEPT_LANGUAGE_TESTS[i][1]); String expectLocale=ACCEPT_LANGUAGE_TESTS[i][0]; logln("#" + i + ": expecting: " + expectLocale + " (" + expectBoolean + ")"); @@ -1630,128 +1629,50 @@ public class ULocaleTest extends TestFmwk { boolean r[] = { false }; ULocale n = ULocale.acceptLanguage(ACCEPT_LANGUAGE_HTTP[i], r); if((n==null)&&(expectLocale!=null)) { - errln("result was null! line #" + i); + errln("#" + i + ": result was null!"); continue; } if(((n==null)&&(expectLocale==null)) || (n.toString().equals(expectLocale))) { - logln(" locale: OK." ); + logln("#" + i + ": locale: OK." ); } else { - errln("expected " + expectLocale + " but got " + n.toString()); + errln("#" + i + ": locale: expected " + expectLocale + " but got " + n); } - if(expectBoolean.equals(new Boolean(r[0]))) { - logln(" bool: OK."); + Boolean actualBoolean = Boolean.valueOf(r[0]); + if(expectBoolean.equals(actualBoolean)) { + logln("#" + i + ": fallback: OK."); } else { - errln("bool: not OK, was " + new Boolean(r[0]).toString() + " expected " + expectBoolean.toString()); + errln("#" + i + ": fallback: was " + actualBoolean + " expected " + expectBoolean); } } } - private ULocale[] StringToULocaleArray(String acceptLanguageList){ - //following code is copied from - //ULocale.acceptLanguage(String acceptLanguageList, ULocale[] availableLocales, boolean[] fallback) - class ULocaleAcceptLanguageQ implements Comparable { - private double q; - private double serial; - public ULocaleAcceptLanguageQ(double theq, int theserial) { - q = theq; - serial = theserial; - } - @Override - public int compareTo(Object o) { - ULocaleAcceptLanguageQ other = (ULocaleAcceptLanguageQ) o; - if(q > other.q) { // reverse - to sort in descending order - return -1; - } else if(q < other.q) { - return 1; - } - if(serial < other.serial) { - return -1; - } else if(serial > other.serial) { - return 1; - } else { - return 0; // same object - } - } - } - - // 1st: parse out the acceptLanguageList into an array - - TreeMap map = new TreeMap(); - - final int l = acceptLanguageList.length(); - int n; - for(n=0;n<l;n++) { - int itemEnd = acceptLanguageList.indexOf(',',n); - if(itemEnd == -1) { - itemEnd = l; - } - int paramEnd = acceptLanguageList.indexOf(';',n); - double q = 1.0; - - if((paramEnd != -1) && (paramEnd < itemEnd)) { - /* semicolon (;) is closer than end (,) */ - int t = paramEnd + 1; - while(UCharacter.isWhitespace(acceptLanguageList.charAt(t))) { - t++; - } - if(acceptLanguageList.charAt(t)=='q') { - t++; - } - while(UCharacter.isWhitespace(acceptLanguageList.charAt(t))) { - t++; - } - if(acceptLanguageList.charAt(t)=='=') { - t++; - } - while(UCharacter.isWhitespace(acceptLanguageList.charAt(t))) { - t++; - } - try { - String val = acceptLanguageList.substring(t,itemEnd).trim(); - q = Double.parseDouble(val); - } catch (NumberFormatException nfe) { - q = 1.0; - } - } else { - q = 1.0; //default - paramEnd = itemEnd; - } - - String loc = acceptLanguageList.substring(n,paramEnd).trim(); - int serial = map.size(); - ULocaleAcceptLanguageQ entry = new ULocaleAcceptLanguageQ(q,serial); - map.put(entry, new ULocale(ULocale.canonicalize(loc))); // sort in reverse order.. 1.0, 0.9, 0.8 .. etc - n = itemEnd; // get next item. (n++ will skip over delimiter) - } - - // 2. pull out the map - ULocale acceptList[] = (ULocale[])map.values().toArray(new ULocale[map.size()]); - return acceptList; - } - @Test public void TestAcceptLanguage2() { for(int i = 0 ; i < (ACCEPT_LANGUAGE_HTTP.length); i++) { - Boolean expectBoolean = new Boolean(ACCEPT_LANGUAGE_TESTS[i][1]); + Boolean expectBoolean = Boolean.valueOf(ACCEPT_LANGUAGE_TESTS[i][1]); String expectLocale=ACCEPT_LANGUAGE_TESTS[i][0]; logln("#" + i + ": expecting: " + expectLocale + " (" + expectBoolean + ")"); boolean r[] = { false }; - ULocale n = ULocale.acceptLanguage(StringToULocaleArray(ACCEPT_LANGUAGE_HTTP[i]), r); + Set<ULocale> desiredSet = + LocalePriorityList.add(ACCEPT_LANGUAGE_HTTP[i]).build().getULocales(); + ULocale[] desiredArray = desiredSet.toArray(new ULocale[desiredSet.size()]); + ULocale n = ULocale.acceptLanguage(desiredArray, r); if((n==null)&&(expectLocale!=null)) { - errln("result was null! line #" + i); + errln("#" + i + ": result was null!"); continue; } if(((n==null)&&(expectLocale==null)) || (n.toString().equals(expectLocale))) { - logln(" locale: OK." ); + logln("#" + i + ": locale: OK."); } else { - errln("expected " + expectLocale + " but got " + n.toString()); + errln("#" + i + ": expected " + expectLocale + " but got " + n.toString()); } - if(expectBoolean.equals(new Boolean(r[0]))) { - logln(" bool: OK."); + Boolean actualBoolean = Boolean.valueOf(r[0]); + if(expectBoolean.equals(actualBoolean)) { + logln("#" + i + ": fallback: OK."); } else { - errln("bool: not OK, was " + new Boolean(r[0]).toString() + " expected " + expectBoolean.toString()); + errln("#" + i + ": fallback: was " + actualBoolean + " expected " + expectBoolean); } } } @@ -4109,8 +4030,8 @@ public class ULocaleTest extends TestFmwk { {"aa_BB_CYRL", "aa-BB-x-lvariant-cyrl"}, {"en_US_1234", "en-US-1234"}, {"en_US_VARIANTA_VARIANTB", "en-US-varianta-variantb"}, - {"en_US_VARIANTB_VARIANTA", "en-US-variantb-varianta"}, - {"ja__9876_5432", "ja-9876-5432"}, + {"en_US_VARIANTB_VARIANTA", "en-US-varianta-variantb"}, /* ICU-20478 */ + {"ja__9876_5432", "ja-5432-9876"}, /* ICU-20478 */ {"zh_Hant__VAR", "zh-Hant-x-lvariant-var"}, {"es__BADVARIANT_GOODVAR", "es"}, {"es__GOODVAR_BAD_BADVARIANT", "es-goodvar-x-lvariant-bad"}, @@ -4134,6 +4055,16 @@ public class ULocaleTest extends TestFmwk { {"en@a=bar;attribute=baz;calendar=islamic-civil;x=u-foo", "en-a-bar-u-baz-ca-islamic-civil-x-u-foo"}, /* ICU-20320*/ {"en@9=efg;a=baz", "en-9-efg-a-baz"}, + /* ICU-20478 */ + {"sl__ROZAJ_BISKE_1994", "sl-1994-biske-rozaj"}, + {"en__SCOUSE_FONIPA", "en-fonipa-scouse"}, + /* ICU-20310 */ + {"en-u-kn-true", "en-u-kn"}, + {"en-u-kn", "en-u-kn"}, + {"de-u-co-yes", "de-u-co"}, + {"de-u-co", "de-u-co"}, + {"de@collation=yes", "de-u-co"}, + {"cmn-hans-cn-u-ca-t-ca-x-t-u", "cmn-Hans-CN-t-ca-u-ca-x-t-u"}, }; for (int i = 0; i < locale_to_langtag.length; i++) { @@ -4231,7 +4162,7 @@ public class ULocaleTest extends TestFmwk { {"bogus", "bogus", NOERROR}, {"boguslang", "", Integer.valueOf(0)}, {"EN-lATN-us", "en_Latn_US", NOERROR}, - {"und-variant-1234", "__VARIANT_1234", NOERROR}, + {"und-variant-1234", "__1234_VARIANT", NOERROR}, /* ICU-20478 */ {"und-varzero-var1-vartwo", "__VARZERO", Integer.valueOf(12)}, {"en-u-ca-gregory", "en@calendar=gregorian", NOERROR}, {"en-U-cu-USD", "en@currency=usd", NOERROR}, @@ -4277,6 +4208,16 @@ public class ULocaleTest extends TestFmwk { /* #20410 */ {"art-lojban-x-0", "jbo@x=0", NOERROR}, {"zh-xiang-u-nu-thai-x-0", "hsn@numbers=thai;x=0", NOERROR}, + /* ICU-20478 */ + {"ja-9876-5432", "ja__5432_9876", NOERROR}, + {"en-US-variantb-varianta", "en_US_VARIANTA_VARIANTB", NOERROR}, + {"en-US-varianta-variantb", "en_US_VARIANTA_VARIANTB", NOERROR}, + {"sl-rozaj-biske-1994", "sl__1994_BISKE_ROZAJ", NOERROR}, + {"sl-biske-rozaj-1994", "sl__1994_BISKE_ROZAJ", NOERROR}, + {"sl-biske-1994-rozaj", "sl__1994_BISKE_ROZAJ", NOERROR}, + {"sl-1994-biske-rozaj", "sl__1994_BISKE_ROZAJ", NOERROR}, + {"en-fonipa-scouse", "en__FONIPA_SCOUSE", NOERROR}, + {"en-scouse-fonipa", "en__FONIPA_SCOUSE", NOERROR}, }; for (int i = 0; i < langtag_to_locale.length; i++) { @@ -5120,4 +5061,101 @@ public class ULocaleTest extends TestFmwk { Assert.assertEquals(displayName, locale_tag.getDisplayName(displayLocale)); Assert.assertEquals(displayName, locale_build.getDisplayName(displayLocale)); } + + @Test + public void Test20900() { + final String [][] testData = new String[][]{ + {"art-lojban", "jbo"}, + {"zh-guoyu", "zh"}, + {"zh-hakka", "hak"}, + {"zh-xiang", "hsn"}, + {"zh-min-nan", "nan"}, + {"zh-gan", "gan"}, + {"zh-yue", "yue"}, + }; + for (int row=0;row<testData.length;row++) { + ULocale loc = ULocale.createCanonical(testData[row][0]); + Assert.assertEquals(testData[row][1], loc.toLanguageTag()); + } + } + + // Helper function + private String canonicalTag(String languageTag) { + return ULocale.createCanonical(ULocale.forLanguageTag(languageTag)).toLanguageTag(); + } + + @Test + public void TestCanonical() { + // Test replacement of languageAlias + + // language _ variant -> language + Assert.assertEquals("nb", canonicalTag("no-BOKMAL")); + // also test with script, country and extensions + Assert.assertEquals("nb-Cyrl-ID-u-ca-japanese", canonicalTag("no-Cyrl-ID-BOKMAL-u-ca-japanese")); + // also test with other variants, script, country and extensions + Assert.assertEquals("nb-Cyrl-ID-1901-xsistemo-u-ca-japanese", + canonicalTag("no-Cyrl-ID-1901-BOKMAL-xsistemo-u-ca-japanese")); + Assert.assertEquals("nb-Cyrl-ID-1901-u-ca-japanese", + canonicalTag("no-Cyrl-ID-1901-BOKMAL-u-ca-japanese")); + Assert.assertEquals("nb-Cyrl-ID-xsistemo-u-ca-japanese", + canonicalTag("no-Cyrl-ID-BOKMAL-xsistemo-u-ca-japanese")); + + Assert.assertEquals("nn", canonicalTag("no-NYNORSK")); + // also test with script, country and extensions + Assert.assertEquals("nn-Cyrl-ID-u-ca-japanese", canonicalTag("no-Cyrl-ID-NYNORSK-u-ca-japanese")); + + Assert.assertEquals("ssy", canonicalTag("aa-SAAHO")); + // also test with script, country and extensions + Assert.assertEquals("ssy-Devn-IN-u-ca-japanese", canonicalTag("aa-Devn-IN-SAAHO-u-ca-japanese")); + + // language -> language + Assert.assertEquals("aas", canonicalTag("aam")); + // also test with script, country, variants and extensions + Assert.assertEquals("aas-Cyrl-ID-3456-u-ca-japanese", canonicalTag("aam-Cyrl-ID-3456-u-ca-japanese")); + + // language -> language _ Script + Assert.assertEquals("sr-Latn", canonicalTag("sh")); + // also test with script + Assert.assertEquals("sr-Cyrl", canonicalTag("sh-Cyrl")); + // also test with country, variants and extensions + Assert.assertEquals("sr-Latn-ID-3456-u-ca-roc", canonicalTag("sh-ID-3456-u-ca-roc")); + + // language -> language _ country + Assert.assertEquals("fa-AF", canonicalTag("prs")); + // also test with country + Assert.assertEquals("fa-RU", canonicalTag("prs-RU")); + // also test with script, variants and extensions + Assert.assertEquals("fa-Cyrl-AF-1009-u-ca-roc", canonicalTag("prs-Cyrl-1009-u-ca-roc")); + + // language _ country -> language _ script _ country + Assert.assertEquals("pa-Guru-IN", canonicalTag("pa-IN")); + // also test with script + Assert.assertEquals("pa-Latn-IN", canonicalTag("pa-Latn-IN")); + // also test with variants and extensions + Assert.assertEquals("pa-Guru-IN-5678-u-ca-hindi", canonicalTag("pa-IN-5678-u-ca-hindi")); + + // language _ script _ country -> language _ country + Assert.assertEquals("ky-KG", canonicalTag("ky-Cyrl-KG")); + // also test with variants and extensions + Assert.assertEquals("ky-KG-3456-u-ca-roc", canonicalTag("ky-Cyrl-KG-3456-u-ca-roc")); + + // Test replacement of territoryAlias + // 554 has one replacement + Assert.assertEquals("en-NZ", canonicalTag("en-554")); + Assert.assertEquals("en-NZ-u-nu-arab", canonicalTag("en-554-u-nu-arab")); + + // 172 has multiple replacements + // also test with variants + Assert.assertEquals("ru-RU-1234", canonicalTag("ru-172-1234")); + // also test with variants + Assert.assertEquals("ru-RU-1234-u-nu-latn", canonicalTag("ru-172-1234-u-nu-latn")); + Assert.assertEquals("uz-UZ", canonicalTag("uz-172")); + // also test with scripts + Assert.assertEquals("uz-Cyrl-UZ", canonicalTag("uz-Cyrl-172")); + Assert.assertEquals("uz-Bopo-UZ", canonicalTag("uz-Bopo-172")); + // also test with variants and scripts + Assert.assertEquals("uz-Cyrl-UZ-5678-u-nu-latn", canonicalTag("uz-Cyrl-172-5678-u-nu-latn")); + // a language not used in this region + Assert.assertEquals("fr-RU", canonicalTag("fr-172")); + } } diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/util/data/localeDistanceTest.txt b/android_icu4j/src/main/tests/android/icu/dev/test/util/data/localeDistanceTest.txt index ba783b569..a6926da6e 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/util/data/localeDistanceTest.txt +++ b/android_icu4j/src/main/tests/android/icu/dev/test/util/data/localeDistanceTest.txt @@ -18,10 +18,10 @@ zh ; cmn ; 0 # fallback languages get closer distances, between script (40) and region (4) @debug -to ; en ; 14 ; 100 +to ; en ; 34 ; 100 no ; no-DE ; 4 -nn ; no ; 10 -no-DE ; nn ; 14 +nn ; no ; 20 +no-DE ; nn ; 24 no ; no ; 0 no ; da ; 12 da ; zh-Hant ; 100 diff --git a/android_icu4j/src/main/tests/android/icu/dev/test/util/data/localeMatcherTest.txt b/android_icu4j/src/main/tests/android/icu/dev/test/util/data/localeMatcherTest.txt index 21c9b6014..7a1098673 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/test/util/data/localeMatcherTest.txt +++ b/android_icu4j/src/main/tests/android/icu/dev/test/util/data/localeMatcherTest.txt @@ -733,7 +733,7 @@ ja >> fr @favor=script en-GB >> en-GB en-US >> en -fr >> en-GB +fr >> en ja >> fr ** test: testEmptyWithDefault @@ -761,8 +761,8 @@ en-GB >> en-GB en-US >> en fr-FR >> fr ja-JP >> fr +zu >> en # For a language that doesn't match anything, return the default. -zu >> en-GB zxx >> fr @favor=script @@ -770,7 +770,7 @@ en-GB >> en-GB en-US >> en fr-FR >> fr ja-JP >> fr -zu >> en-GB +zu >> en zxx >> en ** test: TestExactMatch @@ -1052,9 +1052,9 @@ en >> en-DE ar-EG >> ar-SY pt-BR >> pt ar-XB >> ar-XB -ar-PSBIDI >> ar-XB # These are equivalent. +ar-PSBIDI >> ar-PSBIDI en-XA >> en-XA -en-PSACCENT >> en-XA # These are equivalent. +en-PSACCENT >> en-PSACCENT ar-PSCRACK >> ar-PSCRACK @favor=script @@ -1063,9 +1063,9 @@ en >> en-DE ar-EG >> ar-SY pt-BR >> pt ar-XB >> ar-XB -ar-PSBIDI >> ar-XB # These are equivalent. +ar-PSBIDI >> ar-PSBIDI en-XA >> en-XA -en-PSACCENT >> en-XA # These are equivalent. +en-PSACCENT >> en-PSACCENT ar-PSCRACK >> ar-PSCRACK ** test: BestMatchForTraditionalChinese @@ -1322,7 +1322,7 @@ en >> en-US @favor=script und >> und ja >> und -fr-CA >> en-GB +fr-CA >> en-US en-AU >> en-GB en-BZ >> en-GB en-CA >> en-GB @@ -1359,8 +1359,8 @@ fr >> und @supported=en-GB, en-US, en, en-AU und >> und ja >> und -fr-CA >> en-GB -fr >> en-GB +fr-CA >> en-US +fr >> en-US @supported=en-AU, ja, ca fr >> en-AU @supported=pl, ja, ca @@ -1464,10 +1464,10 @@ da >> no @supported=en, nb da >> nb -** test: prefer matching languages over language variants. +** test: prefer matching languages over language variants. Get en-GB, should get nn? @supported=nn, en-GB -no, en-US >> nn -nb, en-US >> nn +no, en-US >> en-GB +nb, en-US >> en-GB @favor=script no, en-US >> nn @@ -1544,50 +1544,44 @@ zh-TW, en >> en-US zh-Hant-CN, en >> en-US zh-Hans, en >> zh-Hans-CN -** test: return first among likely-subtags equivalent locales -# Was: more specific script should win in case regions are identical -# with some different results. +** test: return most originally similar among likely-subtags equivalent locales @supported=af, af-Latn, af-Arab af >> af af-ZA >> af -af-Latn-ZA >> af -af-Latn >> af +af-Latn-ZA >> af-Latn +af-Latn >> af-Latn @favor=script af >> af af-ZA >> af -af-Latn-ZA >> af -af-Latn >> af +af-Latn-ZA >> af-Latn +af-Latn >> af-Latn -# Was: more specific region should win -# with some different results. @supported=nl, nl-NL, nl-BE @favor= nl >> nl nl-Latn >> nl -nl-Latn-NL >> nl -nl-NL >> nl +nl-Latn-NL >> nl-NL +nl-NL >> nl-NL @favor=script nl >> nl nl-Latn >> nl -nl-Latn-NL >> nl -nl-NL >> nl +nl-Latn-NL >> nl-NL +nl-NL >> nl-NL -# Was: more specific region wins over more specific script -# with some different results. @supported=nl, nl-Latn, nl-NL, nl-BE @favor= nl >> nl -nl-Latn >> nl -nl-NL >> nl -nl-Latn-NL >> nl +nl-Latn >> nl-Latn +nl-NL >> nl-NL +nl-Latn-NL >> nl-Latn @favor=script nl >> nl -nl-Latn >> nl -nl-NL >> nl -nl-Latn-NL >> nl +nl-Latn >> nl-Latn +nl-NL >> nl-NL +nl-Latn-NL >> nl-Latn ** test: region may replace matched if matched is enclosing @supported=es-419, es @@ -1670,22 +1664,22 @@ ja-Jpan-JP, en-GB >> ja ** test: pick best maximized tag @supported=ja, ja-Jpan-US, ja-JP, en, ru ja-Jpan, ru >> ja -ja-JP, ru >> ja +ja-JP, ru >> ja-JP ja-US, ru >> ja-Jpan-US @favor=script ja-Jpan, ru >> ja -ja-JP, ru >> ja +ja-JP, ru >> ja-JP ja-US, ru >> ja-Jpan-US ** test: termination: pick best maximized match @supported=ja, ja-Jpan, ja-JP, en, ru -ja-Jpan-JP, ru >> ja -ja-Jpan, ru >> ja +ja-Jpan-JP, ru >> ja-Jpan +ja-Jpan, ru >> ja-Jpan @favor=script -ja-Jpan-JP, ru >> ja -ja-Jpan, ru >> ja +ja-Jpan-JP, ru >> ja-Jpan +ja-Jpan, ru >> ja-Jpan ** test: same language over exact, but distinguish when user is explicit @supported=fr, en-GB, ja, es-ES, es-MX @@ -1900,14 +1894,14 @@ zh-TW >> zh ** test: testGetBestMatchWithMinMatchScore @supported=fr-FR, fr, fr-CA, en @default=und -fr >> fr-FR # First likely-subtags equivalent match is chosen. +fr >> fr @supported=en, fr, fr-CA fr-FR >> fr # Parent match is chosen. @supported=en, fr-CA fr-FR >> fr-CA # Sibling match is chosen. @supported=fr-CA, fr-FR fr >> fr-FR # Inferred region match is chosen. -fr-SN >> fr-CA +fr-SN >> fr-FR @supported=en, fr-FR fr >> fr-FR # Child match is chosen. @supported=de, en, it @@ -1930,14 +1924,14 @@ ru >> und @favor=script @supported=fr-FR, fr, fr-CA, en -fr >> fr-FR +fr >> fr @supported=en, fr, fr-CA fr-FR >> fr @supported=en, fr-CA fr-FR >> fr-CA @supported=fr-CA, fr-FR fr >> fr-FR -fr-SN >> fr-CA +fr-SN >> fr-FR @supported=en, fr-FR fr >> fr-FR @supported=de, en, it @@ -1957,3 +1951,10 @@ ru >> uk zh-CN >> zh-TW @supported=ja ru >> und + +** test: favor a more-default locale among equally imperfect matches +@supported=fr-CA, fr-CH, fr-FR, fr-GB +fr-SN >> fr-FR +@supported=sr-Latn, sr-Cyrl, sr-Grek +@threshold=60 +sr-Thai >> sr-Cyrl diff --git a/android_icu4j/src/main/tests/android/icu/dev/tool/locale/LikelySubtagsBuilder.java b/android_icu4j/src/main/tests/android/icu/dev/tool/locale/LikelySubtagsBuilder.java index f9e093344..9887e89ab 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/tool/locale/LikelySubtagsBuilder.java +++ b/android_icu4j/src/main/tests/android/icu/dev/tool/locale/LikelySubtagsBuilder.java @@ -142,10 +142,11 @@ public class LikelySubtagsBuilder { Map<LSR, Integer> lsrIndexes = new LinkedHashMap<>(); // Reserve index 0 as "no value": // The runtime lookup returns 0 for an intermediate match with no value. - lsrIndexes.put(new LSR("", "", ""), 0); // arbitrary LSR + lsrIndexes.put(new LSR("", "", "", LSR.DONT_CARE_FLAGS), 0); // arbitrary LSR // Reserve index 1 for SKIP_SCRIPT: // The runtime lookup returns 1 for an intermediate match with a value. - lsrIndexes.put(new LSR("skip", "script", ""), 1); // looks good when printing the data + // This LSR looks good when printing the data. + lsrIndexes.put(new LSR("skip", "script", "", LSR.DONT_CARE_FLAGS), 1); // We could prefill the lsrList with common locales to give them small indexes, // and see if that improves performance a little. for (Map.Entry<String, Map<String, Map<String, LSR>>> ls : langTable.entrySet()) { @@ -254,7 +255,7 @@ public class LikelySubtagsBuilder { } } // hack - set(result, "und", "Latn", "", new LSR("en", "Latn", "US")); + set(result, "und", "Latn", "", new LSR("en", "Latn", "US", LSR.DONT_CARE_FLAGS)); // hack, ensure that if und-YY => und-Xxxx-YY, then we add Xxxx=>YY to the table // <likelySubtag from="und_GH" to="ak_Latn_GH"/> @@ -297,7 +298,9 @@ public class LikelySubtagsBuilder { String lang = parts[0]; String p2 = parts.length < 2 ? "" : parts[1]; String p3 = parts.length < 3 ? "" : parts[2]; - return p2.length() < 4 ? new LSR(lang, "", p2) : new LSR(lang, p2, p3); + return p2.length() < 4 ? + new LSR(lang, "", p2, LSR.DONT_CARE_FLAGS) : + new LSR(lang, p2, p3, LSR.DONT_CARE_FLAGS); } private static void set(Map<String, Map<String, Map<String, LSR>>> langTable, diff --git a/android_icu4j/src/main/tests/android/icu/dev/tool/locale/LocaleDistanceBuilder.java b/android_icu4j/src/main/tests/android/icu/dev/tool/locale/LocaleDistanceBuilder.java index 42fc03bcd..6e41d976f 100644 --- a/android_icu4j/src/main/tests/android/icu/dev/tool/locale/LocaleDistanceBuilder.java +++ b/android_icu4j/src/main/tests/android/icu/dev/tool/locale/LocaleDistanceBuilder.java @@ -16,6 +16,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -487,10 +488,14 @@ public final class LocaleDistanceBuilder { ICUResourceBundle supplementalData = getSupplementalDataBundle("supplementalData"); String[] paradigms = supplementalData.getValueWithFallback( "languageMatchingInfo/written/paradigmLocales").getStringArray(); - Set<LSR> paradigmLSRs = new HashSet<>(); // could be TreeSet if LSR were Comparable + // LinkedHashSet for stable order; otherwise a unit test is flaky. + Set<LSR> paradigmLSRs = new LinkedHashSet<>(); // could be TreeSet if LSR were Comparable for (String paradigm : paradigms) { ULocale pl = new ULocale(paradigm); - paradigmLSRs.add(XLikelySubtags.INSTANCE.makeMaximizedLsrFrom(pl)); + LSR max = XLikelySubtags.INSTANCE.makeMaximizedLsrFrom(pl); + // Clear the LSR flags to make the data equality test in + // LocaleDistanceTest happy. + paradigmLSRs.add(new LSR(max.language, max.script, max.region, LSR.DONT_CARE_FLAGS)); } TerritoryContainment tc = new TerritoryContainment(supplementalData); diff --git a/android_icu4j/src/main/tests/android/icu/extratest/android_icu_version.properties b/android_icu4j/src/main/tests/android/icu/extratest/android_icu_version.properties index 8912579eb..e6b3f0d59 100644 --- a/android_icu4j/src/main/tests/android/icu/extratest/android_icu_version.properties +++ b/android_icu4j/src/main/tests/android/icu/extratest/android_icu_version.properties @@ -1,2 +1,2 @@ # Property file for AndroidICUVersionTest. -version=66.1.0.0 +version=67.1.0.0 diff --git a/android_icu4j/src/main/tests/android/icu/extratest/expected_transliteration_id_list.txt b/android_icu4j/src/main/tests/android/icu/extratest/expected_transliteration_id_list.txt index dde80eb33..87abcf97a 100644 --- a/android_icu4j/src/main/tests/android/icu/extratest/expected_transliteration_id_list.txt +++ b/android_icu4j/src/main/tests/android/icu/extratest/expected_transliteration_id_list.txt @@ -35,6 +35,9 @@ Bengali-Telugu Bopo-Latn Bopomofo-Latin Bulgarian-Latin/BGN +Burmese-Latin +CanadianAboriginal-Latin +Cans-Latn Cyrillic-Latin Cyrl-Latn Deva-Arab @@ -59,6 +62,8 @@ Devanagari-Oriya Devanagari-Tamil Devanagari-Telugu Digit-Tone +Ethi-Latn +Ethiopic-Latin Fullwidth-Halfwidth Geor-Latn Georgian-Latin @@ -162,8 +167,10 @@ Latin-Arabic Latin-Armenian Latin-Bengali Latin-Bopomofo +Latin-CanadianAboriginal Latin-Cyrillic Latin-Devanagari +Latin-Ethiopic Latin-Georgian Latin-Greek Latin-Greek/UNGEGN @@ -188,8 +195,10 @@ Latn-Arab Latn-Armn Latn-Beng Latn-Bopo +Latn-Cans Latn-Cyrl Latn-Deva +Latn-Ethi Latn-Geor Latn-Grek Latn-Grek/UNGEGN @@ -232,6 +241,7 @@ Mlym-Taml Mlym-Telu Mlym-ur Mongolian-Latin/BGN +Myanmar-Latin NumericPinyin-Latin NumericPinyin-Pinyin Oriya-Arabic @@ -429,6 +439,7 @@ my-ar my-chr my-fa my-my_FONIPA +my-my_Latn nl-Title nv-nv_FONIPA pl-am @@ -591,6 +602,8 @@ Any-Jamo Any-Armenian Any-Thai Any-Cyrillic +Any-CanadianAboriginal +Any-Ethiopic Any-Oriya Any-Latin/BGN Any-am_FONIPA @@ -633,16 +646,18 @@ Any-Latin/Names Any-vec_FONIPA Any-uk_Latn/BGN Any-Hira +Any-Hang +Any-Cans +Any-Cyrl +Any-Ethi Any-Grek Any-Grek/UNGEGN Any-Syrc Any-Bopo -Any-Hang Any-Thaa Any-Geor Any-Kana Any-Hebr -Any-Cyrl Any-Armn Any-mk_Latn/BGN Any-mn_Latn/BGN @@ -653,6 +668,7 @@ Any-eo_FONIPA Any-tk/BGN Any-chr_FONIPA Any-my_FONIPA +Any-my_Latn Any-es_FONIPA Any-ru/BGN Any-dsb_FONIPA |