summaryrefslogtreecommitdiffstats
path: root/android_icu4j/src/main/java/android
diff options
context:
space:
mode:
authorFredrik Roubert <roubert@google.com>2017-08-09 18:29:05 +0200
committerFredrik Roubert <roubert@google.com>2017-08-09 18:29:05 +0200
commit495cb271e305cfb399d463f32210a371198f0abf (patch)
tree45ad7f39c66d45330a25d76d75d5211d484d3ee1 /android_icu4j/src/main/java/android
parenteb03f4ed3153ac1c37b9a6b8d322238139f7c8a4 (diff)
downloadandroid_external_icu-495cb271e305cfb399d463f32210a371198f0abf.tar.gz
android_external_icu-495cb271e305cfb399d463f32210a371198f0abf.tar.bz2
android_external_icu-495cb271e305cfb399d463f32210a371198f0abf.zip
Integrate ICU4J 59.1 with Android patches into android_icu4j.
Bug: 62410016 Test: CtsIcuTestCases Test: CtsLibcoreOjTestCases Test: CtsLibcoreTestCases Change-Id: I76ad54bcfa00a8ae21fa33f27ca2f03d4e6c292d
Diffstat (limited to 'android_icu4j/src/main/java/android')
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/ICUNotifier.java18
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/LocaleIDs.java8
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/TZDBTimeZoneNames.java4
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/TextTrieMap.java126
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/Utility.java41
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/locale/KeyTypeData.java1
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/locale/LocaleValidityChecker.java48
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/locale/XCldrStub.java415
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/locale/XLikelySubtags.java671
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/locale/XLocaleDistance.java1348
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/locale/XLocaleMatcher.java474
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/AffixPatternUtils.java552
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/Endpoint.java302
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/Exportable.java17
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/Format.java281
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity.java185
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity1.java858
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity2.java180
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity3.java229
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity4.java415
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/FormatQuantityBCD.java919
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/FormatQuantitySelector.java54
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/Modifier.java130
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/ModifierHolder.java110
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/NumberStringBuilder.java415
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/PNAffixGenerator.java283
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/Parse.java2498
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/PatternString.java907
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/Properties.java1059
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/Rounder.java250
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/RoundingUtils.java167
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/formatters/BigDecimalMultiplier.java61
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/formatters/CompactDecimalFormat.java565
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/formatters/CurrencyFormat.java315
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/formatters/MagnitudeMultiplier.java63
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/formatters/MeasureFormat.java77
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/formatters/PaddingFormat.java177
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/formatters/PositiveDecimalFormat.java238
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/formatters/PositiveNegativeAffixFormat.java258
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/formatters/RangeFormat.java62
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/formatters/RoundingFormat.java45
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/formatters/ScientificFormat.java242
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/formatters/StrongAffixFormat.java50
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/modifiers/ConstantAffixModifier.java107
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/modifiers/ConstantMultiFieldModifier.java95
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/modifiers/GeneralPluralModifier.java78
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/modifiers/PositiveNegativeAffixModifier.java55
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/modifiers/SimpleModifier.java132
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/rounders/IncrementRounder.java71
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/rounders/MagnitudeRounder.java34
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/rounders/NoRounder.java26
-rw-r--r--android_icu4j/src/main/java/android/icu/impl/number/rounders/SignificantDigitsRounder.java209
-rw-r--r--android_icu4j/src/main/java/android/icu/text/CompactDecimalDataCache.java525
-rw-r--r--android_icu4j/src/main/java/android/icu/text/CompactDecimalFormat.java636
-rw-r--r--android_icu4j/src/main/java/android/icu/text/CurrencyPluralInfo.java24
-rw-r--r--android_icu4j/src/main/java/android/icu/text/DateFormat.java8
-rw-r--r--android_icu4j/src/main/java/android/icu/text/DateFormatSymbols.java101
-rw-r--r--android_icu4j/src/main/java/android/icu/text/DateIntervalFormat.java9
-rw-r--r--android_icu4j/src/main/java/android/icu/text/DecimalFormat.java8421
-rw-r--r--android_icu4j/src/main/java/android/icu/text/DecimalFormatSymbols.java17
-rw-r--r--android_icu4j/src/main/java/android/icu/text/DigitList.java838
-rw-r--r--android_icu4j/src/main/java/android/icu/text/MeasureFormat.java78
-rw-r--r--android_icu4j/src/main/java/android/icu/text/MessageFormat.java8
-rw-r--r--android_icu4j/src/main/java/android/icu/text/NFSubstitution.java238
-rw-r--r--android_icu4j/src/main/java/android/icu/text/NumberFormat.java60
-rw-r--r--android_icu4j/src/main/java/android/icu/text/PluralFormat.java8
-rw-r--r--android_icu4j/src/main/java/android/icu/text/PluralRules.java166
-rw-r--r--android_icu4j/src/main/java/android/icu/text/RelativeDateTimeFormatter.java18
-rw-r--r--android_icu4j/src/main/java/android/icu/text/Replaceable.java8
-rw-r--r--android_icu4j/src/main/java/android/icu/text/ReplaceableString.java17
-rw-r--r--android_icu4j/src/main/java/android/icu/text/RuleBasedBreakIterator.java104
-rw-r--r--android_icu4j/src/main/java/android/icu/text/RuleBasedNumberFormat.java8
-rw-r--r--android_icu4j/src/main/java/android/icu/text/RuleBasedTransliterator.java2
-rw-r--r--android_icu4j/src/main/java/android/icu/text/ScientificNumberFormatter.java5
-rw-r--r--android_icu4j/src/main/java/android/icu/text/SimpleFormatter.java11
-rw-r--r--android_icu4j/src/main/java/android/icu/text/StringSearch.java113
-rw-r--r--android_icu4j/src/main/java/android/icu/text/Transliterator.java3
-rw-r--r--android_icu4j/src/main/java/android/icu/text/UnhandledBreakEngine.java77
-rw-r--r--android_icu4j/src/main/java/android/icu/text/UnicodeSet.java2
-rw-r--r--android_icu4j/src/main/java/android/icu/util/Currency.java67
-rw-r--r--android_icu4j/src/main/java/android/icu/util/LocaleMatcher.java134
-rw-r--r--android_icu4j/src/main/java/android/icu/util/Measure.java2
-rw-r--r--android_icu4j/src/main/java/android/icu/util/MeasureUnit.java39
-rw-r--r--android_icu4j/src/main/java/android/icu/util/TimeZone.java33
-rw-r--r--android_icu4j/src/main/java/android/icu/util/ULocale.java157
-rw-r--r--android_icu4j/src/main/java/android/icu/util/UniversalTimeScale.java3
-rw-r--r--android_icu4j/src/main/java/android/icu/util/VersionInfo.java6
87 files changed, 19254 insertions, 8617 deletions
diff --git a/android_icu4j/src/main/java/android/icu/impl/ICUNotifier.java b/android_icu4j/src/main/java/android/icu/impl/ICUNotifier.java
index 30d9c6455..d7ed124eb 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ICUNotifier.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ICUNotifier.java
@@ -70,7 +70,7 @@ public abstract class ICUNotifier {
/**
* Stop notifying this listener. The listener must
- * not be null. Attemps to remove a listener that is
+ * not be null. Attempts to remove a listener that is
* not registered will be silently ignored.
*/
public void removeListener(EventListener l) {
@@ -100,16 +100,14 @@ public abstract class ICUNotifier {
* is called on each listener from the notification thread.
*/
public void notifyChanged() {
- if (listeners != null) {
- synchronized (notifyLock) {
- if (listeners != null) {
- if (notifyThread == null) {
- notifyThread = new NotifyThread(this);
- notifyThread.setDaemon(true);
- notifyThread.start();
- }
- notifyThread.queue(listeners.toArray(new EventListener[listeners.size()]));
+ synchronized (notifyLock) {
+ if (listeners != null) {
+ if (notifyThread == null) {
+ notifyThread = new NotifyThread(this);
+ notifyThread.setDaemon(true);
+ notifyThread.start();
}
+ notifyThread.queue(listeners.toArray(new EventListener[listeners.size()]));
}
}
}
diff --git a/android_icu4j/src/main/java/android/icu/impl/LocaleIDs.java b/android_icu4j/src/main/java/android/icu/impl/LocaleIDs.java
index 986d85ab2..2ebb60cb2 100644
--- a/android_icu4j/src/main/java/android/icu/impl/LocaleIDs.java
+++ b/android_icu4j/src/main/java/android/icu/impl/LocaleIDs.java
@@ -148,8 +148,8 @@ public class LocaleIDs {
"aa", "ab", "ace", "ach", "ada", "ady", "ae", "af",
"afa", "afh", "agq", "ain", "ak", "akk", "ale", "alg",
"alt", "am", "an", "ang", "anp", "apa", "ar", "arc",
- "arn", "arp", "art", "arw", "as", "asa", "ast", "ath",
- "aus", "av", "awa", "ay", "az",
+ "arn", "arp", "ars", "art", "arw", "as", "asa", "ast",
+ "ath", "aus", "av", "awa", "ay", "az",
"ba", "bad", "bai", "bal", "ban", "bas", "bat", "bax",
"bbj", "be", "bej", "bem", "ber", "bez", "bfd", "bg",
"bh", "bho", "bi", "bik", "bin", "bkm", "bla", "bm",
@@ -241,8 +241,8 @@ public class LocaleIDs {
"aar", "abk", "ace", "ach", "ada", "ady", "ave", "afr",
"afa", "afh", "agq", "ain", "aka", "akk", "ale", "alg",
"alt", "amh", "arg", "ang", "anp", "apa", "ara", "arc",
- "arn", "arp", "art", "arw", "asm", "asa", "ast", "ath",
- "aus", "ava", "awa", "aym", "aze",
+ "arn", "arp", "ars", "art", "arw", "asm", "asa", "ast",
+ "ath", "aus", "ava", "awa", "aym", "aze",
"bak", "bad", "bai", "bal", "ban", "bas", "bat", "bax",
"bbj", "bel", "bej", "bem", "ber", "bez", "bfd", "bul",
"bih", "bho", "bis", "bik", "bin", "bkm", "bla", "bam",
diff --git a/android_icu4j/src/main/java/android/icu/impl/TZDBTimeZoneNames.java b/android_icu4j/src/main/java/android/icu/impl/TZDBTimeZoneNames.java
index 62f011e4e..2ea7d3ab5 100644
--- a/android_icu4j/src/main/java/android/icu/impl/TZDBTimeZoneNames.java
+++ b/android_icu4j/src/main/java/android/icu/impl/TZDBTimeZoneNames.java
@@ -205,6 +205,10 @@ public class TZDBTimeZoneNames extends TimeZoneNames {
case SHORT_DAYLIGHT:
name = _names[1];
break;
+ default:
+ // No names for all other types handled by
+ // this class.
+ break;
}
return name;
diff --git a/android_icu4j/src/main/java/android/icu/impl/TextTrieMap.java b/android_icu4j/src/main/java/android/icu/impl/TextTrieMap.java
index af8158e82..68b65b97f 100644
--- a/android_icu4j/src/main/java/android/icu/impl/TextTrieMap.java
+++ b/android_icu4j/src/main/java/android/icu/impl/TextTrieMap.java
@@ -15,6 +15,7 @@ import java.util.List;
import java.util.ListIterator;
import android.icu.lang.UCharacter;
+import android.icu.text.UTF16;
/**
* TextTrieMap is a trie implementation for supporting
@@ -107,6 +108,85 @@ public class TextTrieMap<V> {
}
}
+ /**
+ * Creates an object that consumes code points one at a time and returns intermediate prefix
+ * matches. Returns null if no match exists.
+ *
+ * @return An instance of {@link ParseState}, or null if the starting code point is not a
+ * prefix for any entry in the trie.
+ */
+ public ParseState openParseState(int startingCp) {
+ // Check to see whether this is a valid starting character. If not, return null.
+ if (_ignoreCase) {
+ startingCp = UCharacter.foldCase(startingCp, true);
+ }
+ int count = Character.charCount(startingCp);
+ char ch1 = (count == 1) ? (char) startingCp : UTF16.getLeadSurrogate(startingCp);
+ if (!_root.hasChildFor(ch1)) {
+ return null;
+ }
+
+ return new ParseState(_root);
+ }
+
+ /**
+ * ParseState is mutable, not thread-safe, and intended to be used internally by parsers for
+ * consuming values from this trie.
+ */
+ public class ParseState {
+ private Node node;
+ private int offset;
+ private Node.StepResult result;
+
+ ParseState(Node start) {
+ node = start;
+ offset = 0;
+ result = start.new StepResult();
+ }
+
+ /**
+ * Consumes a code point and walk to the next node in the trie.
+ *
+ * @param cp The code point to consume.
+ */
+ public void accept(int cp) {
+ assert node != null;
+ if (_ignoreCase) {
+ cp = UCharacter.foldCase(cp, true);
+ }
+ int count = Character.charCount(cp);
+ char ch1 = (count == 1) ? (char) cp : UTF16.getLeadSurrogate(cp);
+ node.takeStep(ch1, offset, result);
+ if (count == 2 && result.node != null) {
+ char ch2 = UTF16.getTrailSurrogate(cp);
+ result.node.takeStep(ch2, result.offset, result);
+ }
+ node = result.node;
+ offset = result.offset;
+ }
+
+ /**
+ * Gets the exact prefix matches for all code points that have been consumed so far.
+ *
+ * @return The matches.
+ */
+ public Iterator<V> getCurrentMatches() {
+ if (node != null && offset == node.charCount()) {
+ return node.values();
+ }
+ return null;
+ }
+
+ /**
+ * Checks whether any more code points can be consumed.
+ *
+ * @return true if no more code points can be consumed; false otherwise.
+ */
+ public boolean atEnd() {
+ return node == null || (node.charCount() == offset && node._children == null);
+ }
+ }
+
public static class CharIterator implements Iterator<Character> {
private boolean _ignoreCase;
private CharSequence _text;
@@ -236,6 +316,21 @@ public class TextTrieMap<V> {
_children = children;
}
+ public int charCount() {
+ return _text == null ? 0 : _text.length;
+ }
+
+ public boolean hasChildFor(char ch) {
+ for (int i=0; _children != null && i < _children.size(); i++) {
+ Node child = _children.get(i);
+ if (ch < child._text[0]) break;
+ if (ch == child._text[0]) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public Iterator<V> values() {
if (_values == null) {
return null;
@@ -274,6 +369,37 @@ public class TextTrieMap<V> {
return match;
}
+ public class StepResult {
+ public Node node;
+ public int offset;
+ }
+ public void takeStep(char ch, int offset, StepResult result) {
+ assert offset <= charCount();
+ if (offset == charCount()) {
+ // Go to a child node
+ for (int i=0; _children != null && i < _children.size(); i++) {
+ Node child = _children.get(i);
+ if (ch < child._text[0]) break;
+ if (ch == child._text[0]) {
+ // Found a matching child node
+ result.node = child;
+ result.offset = 1;
+ return;
+ }
+ }
+ // No matching children; fall through
+ } else if (_text[offset] == ch) {
+ // Return to this node; increase offset
+ result.node = this;
+ result.offset = offset + 1;
+ return;
+ }
+ // No matches
+ result.node = null;
+ result.offset = -1;
+ return;
+ }
+
private void add(char[] text, int offset, V value) {
if (text.length == offset) {
_values = addValue(_values, value);
diff --git a/android_icu4j/src/main/java/android/icu/impl/Utility.java b/android_icu4j/src/main/java/android/icu/impl/Utility.java
index 26d041b69..cdb6a788f 100644
--- a/android_icu4j/src/main/java/android/icu/impl/Utility.java
+++ b/android_icu4j/src/main/java/android/icu/impl/Utility.java
@@ -11,6 +11,7 @@ package android.icu.impl;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Locale;
import java.util.regex.Pattern;
@@ -1811,4 +1812,44 @@ public final class Utility {
}
return buffer.toString();
}
+
+ /**
+ * This implementation is equivalent to Java 7+ Objects#equals(Object a, Object b)
+ *
+ * @param a an object
+ * @param b an object to be compared with a for equality
+ * @return true if the arguments are equal to each other and false otherwise
+ */
+ public static boolean equals(Object a, Object b) {
+ return (a == b)
+ || (a != null && b != null && a.equals(b));
+ }
+
+ /**
+ * This implementation is equivalent to Java 7+ Objects#hash(Object... values)
+ * @param values the values to be hashed
+ * @return a hash value of the sequence of input values
+ */
+ public static int hash(Object... values) {
+ return Arrays.hashCode(values);
+ }
+
+ /**
+ * This implementation is equivalent to Java 7+ Objects#hashCode(Object o)
+ * @param o an object
+ * @return a hash value of a non-null argument and 0 for null argument
+ */
+ public static int hashCode(Object o) {
+ return o == null ? 0 : o.hashCode();
+ }
+
+ /**
+ * This implementation is equivalent to Java 7+ Objects#toString(Object o)
+ * @param o an object
+ * @return the result of calling toStirng for a non-null argument and "null" for a
+ * null argument
+ */
+ public static String toString(Object o) {
+ return o == null ? "null" : o.toString();
+ }
}
diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/KeyTypeData.java b/android_icu4j/src/main/java/android/icu/impl/locale/KeyTypeData.java
index e89c5cbc9..084514e98 100644
--- a/android_icu4j/src/main/java/android/icu/impl/locale/KeyTypeData.java
+++ b/android_icu4j/src/main/java/android/icu/impl/locale/KeyTypeData.java
@@ -413,6 +413,7 @@ keyInfo{
}
valueType{
ca{"incremental"}
+ h0{"single"}
kr{"multiple"}
vt{"multiple"}
x0{"any"}
diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/LocaleValidityChecker.java b/android_icu4j/src/main/java/android/icu/impl/locale/LocaleValidityChecker.java
index 93a224dd1..8f01abdb6 100644
--- a/android_icu4j/src/main/java/android/icu/impl/locale/LocaleValidityChecker.java
+++ b/android_icu4j/src/main/java/android/icu/impl/locale/LocaleValidityChecker.java
@@ -77,7 +77,7 @@ public class LocaleValidityChecker {
final Set<Character> extensionKeys = locale.getExtensionKeys();
// if (language.isEmpty()) {
// // the only case where this is valid is if there is only an 'x' extension string
- // if (!script.isEmpty() || !region.isEmpty() || variantString.isEmpty()
+ // if (!script.isEmpty() || !region.isEmpty() || variantString.isEmpty()
// || extensionKeys.size() != 1 || !extensionKeys.contains('x')) {
// return where.set(Datatype.x, "Null language only with x-...");
// }
@@ -108,6 +108,8 @@ public class LocaleValidityChecker {
case u:
if (!isValidU(locale, datatype, locale.getExtension(c), where)) return false;
break;
+ default:
+ break;
}
} catch (Exception e) {
return where.set(Datatype.illegal, c+"");
@@ -137,8 +139,8 @@ public class LocaleValidityChecker {
}
/**
- * @param locale
- * @param datatype
+ * @param locale
+ * @param datatype
* @param extension
* @param where
* @return
@@ -156,7 +158,7 @@ public class LocaleValidityChecker {
// TODO: is empty -u- valid?
for (String subtag : SEPARATOR.split(extensionString)) {
- if (subtag.length() == 2
+ if (subtag.length() == 2
&& (tBuffer == null || subtag.charAt(1) <= '9')) {
// if we have accumulated a t buffer, check that first
if (tBuffer != null) {
@@ -184,7 +186,7 @@ public class LocaleValidityChecker {
} else {
++typeCount;
switch (valueType) {
- case single:
+ case single:
if (typeCount > 1) {
return where.set(datatype, key+"-"+subtag);
}
@@ -203,11 +205,13 @@ public class LocaleValidityChecker {
seen.clear();
}
break;
+ default:
+ break;
}
switch (specialCase) {
- case anything:
+ case anything:
continue;
- case codepoints:
+ case codepoints:
try {
if (Integer.parseInt(subtag,16) > 0x10FFFF) {
return where.set(datatype, key+"-"+subtag);
@@ -235,6 +239,8 @@ public class LocaleValidityChecker {
return false;
}
continue;
+ default:
+ break;
}
// en-u-sd-usca
@@ -326,14 +332,30 @@ public class LocaleValidityChecker {
}
/**
- * @param language
- * @param language2
+ * @param datatype
+ * @param code
+ * @param where
* @return
*/
private boolean isValid(Datatype datatype, String code, Where where) {
- return code.isEmpty() ? true :
- ValidIdentifiers.isValid(datatype, datasubtypes, code) != null ? true :
- where == null ? false
- : where.set(datatype, code);
+ if (code.isEmpty()) {
+ return true;
+ }
+
+ // Note:
+ // BCP 47 -u- locale extension '-u-va-posix' is mapped to variant 'posix' automatically.
+ // For example, ULocale.forLanguageTag("en-u-va-posix").getVariant() returns "posix".
+ // This is only the exceptional case when -u- locale extension is mapped to a subtag type
+ // other than keyword.
+ //
+ // The locale validity data is based on IANA language subtag registry data and "posix"
+ // is not a valid variant. So we need to handle this specific case here. There are no
+ // othe exceptions.
+ if (datatype == Datatype.variant && "posix".equalsIgnoreCase(code)) {
+ return true;
+ }
+
+ return ValidIdentifiers.isValid(datatype, datasubtypes, code) != null ?
+ true : (where == null ? false : where.set(datatype, code));
}
}
diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/XCldrStub.java b/android_icu4j/src/main/java/android/icu/impl/locale/XCldrStub.java
new file mode 100644
index 000000000..65297e0ae
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/locale/XCldrStub.java
@@ -0,0 +1,415 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.locale;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.icu.util.ICUException;
+import android.icu.util.ICUUncheckedIOException;
+
+/**
+ * Stub class to make migration easier until we get either Guava or a higher level of Java.
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class XCldrStub {
+
+ public static class Multimap<K, V> {
+ private final Map<K,Set<V>> map;
+ private final Class<Set<V>> setClass;
+
+ @SuppressWarnings("unchecked")
+ private Multimap(Map<K,Set<V>> map, Class<?> setClass) {
+ this.map = map;
+ this.setClass = (Class<Set<V>>) (setClass != null
+ ? setClass
+ : HashSet.class);
+ }
+ public Multimap<K, V> putAll(K key, V... values) {
+ if (values.length != 0) {
+ createSetIfMissing(key).addAll(Arrays.asList(values));
+ }
+ return this;
+ }
+ public void putAll(K key, Collection<V> values) {
+ if (!values.isEmpty()) {
+ createSetIfMissing(key).addAll(values);
+ }
+ }
+ public void putAll(Collection<K> keys, V value) {
+ for (K key : keys) {
+ put(key, value);
+ }
+ }
+ public void putAll(Multimap<K, V> source) {
+ for (Entry<K, Set<V>> entry : source.map.entrySet()) {
+ putAll(entry.getKey(), entry.getValue());
+ }
+ }
+ public void put(K key, V value) {
+ createSetIfMissing(key).add(value);
+ }
+ private Set<V> createSetIfMissing(K key) {
+ Set<V> old = map.get(key);
+ if (old == null) {
+ map.put(key, old = getInstance());
+ }
+ return old;
+ }
+ private Set<V> getInstance() {
+ try {
+ return setClass.newInstance();
+ } catch (Exception e) {
+ throw new ICUException(e);
+ }
+ }
+ public Set<V> get(K key) {
+ Set<V> result = map.get(key);
+ return result; // == null ? Collections.<V>emptySet() : result;
+ }
+ public Set<K> keySet() {
+ return map.keySet();
+ }
+ public Map<K, Set<V>> asMap() {
+ return map;
+ }
+ public Set<V> values() {
+ Collection<Set<V>> values = map.values();
+ if (values.size() == 0) {
+ return Collections.<V>emptySet();
+ }
+ Set<V> result = getInstance();
+ for ( Set<V> valueSet : values) {
+ result.addAll(valueSet);
+ }
+ return result;
+ }
+ public int size() {
+ return map.size();
+ }
+ public Iterable<Entry<K, V>> entries() {
+ return new MultimapIterator<K, V>(map);
+ }
+ @Override
+ public boolean equals(Object obj) {
+ return this == obj ||
+ (obj != null
+ && obj.getClass() == this.getClass()
+ && map.equals(((Multimap<?,?>) obj).map));
+ }
+
+ @Override
+ public int hashCode() {
+ return map.hashCode();
+ }
+ }
+
+ public static class Multimaps {
+ public static <K, V, R extends Multimap<K, V>> R invertFrom(Multimap<V, K> source, R target) {
+ for (Entry<V, Set<K>> entry : source.asMap().entrySet()) {
+ target.putAll(entry.getValue(), entry.getKey());
+ }
+ return target;
+ }
+ public static <K, V, R extends Multimap<K, V>> R invertFrom(Map<V, K> source, R target) {
+ for (Entry<V, K> entry : source.entrySet()) {
+ target.put(entry.getValue(), entry.getKey());
+ }
+ return target;
+ }
+ /**
+ * Warning, not functionally the same as Guava; only for use in invertFrom.
+ */
+ public static <K, V> Map<K,V> forMap(Map<K,V> map) {
+ return map;
+ }
+ }
+
+ private static class MultimapIterator<K,V> implements Iterator<Entry<K,V>>, Iterable<Entry<K,V>> {
+ private final Iterator<Entry<K, Set<V>>> it1;
+ private Iterator<V> it2 = null;
+ private final ReusableEntry<K,V> entry = new ReusableEntry<K,V>();
+
+ private MultimapIterator(Map<K,Set<V>> map) {
+ it1 = map.entrySet().iterator();
+ }
+ @Override
+ public boolean hasNext() {
+ return it1.hasNext() || it2 != null && it2.hasNext();
+ }
+ @Override
+ public Entry<K, V> next() {
+ if (it2 != null && it2.hasNext()) {
+ entry.value = it2.next();
+ } else {
+ Entry<K, Set<V>> e = it1.next();
+ entry.key = e.getKey();
+ it2 = e.getValue().iterator();
+ }
+ return entry;
+ }
+ @Override
+ public Iterator<Entry<K, V>> iterator() {
+ return this;
+ }
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private static class ReusableEntry<K,V> implements Entry<K,V> {
+ K key;
+ V value;
+ @Override
+ public K getKey() {
+ return key;
+ }
+ @Override
+ public V getValue() {
+ return value;
+ }
+ @Override
+ public V setValue(V value) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ public static class HashMultimap<K, V> extends Multimap<K, V> {
+ private HashMultimap() {
+ super(new HashMap<K, Set<V>>(), HashSet.class);
+ }
+ public static <K, V> HashMultimap<K, V> create() {
+ return new HashMultimap<K, V>();
+ }
+ }
+
+ public static class TreeMultimap<K, V> extends Multimap<K, V> {
+ private TreeMultimap() {
+ super(new TreeMap<K, Set<V>>(), TreeSet.class);
+ }
+ public static <K, V> TreeMultimap<K, V> create() {
+ return new TreeMultimap<K, V>();
+ }
+ }
+
+ public static class LinkedHashMultimap<K, V> extends Multimap<K, V> {
+ private LinkedHashMultimap() {
+ super(new LinkedHashMap<K, Set<V>>(), LinkedHashSet.class);
+ }
+ public static <K, V> LinkedHashMultimap<K, V> create() {
+ return new LinkedHashMultimap<K, V>();
+ }
+ }
+
+
+ // public static class Counter<T> implements Iterable<T>{
+ // private Map<T,Long> data;
+ // @Override
+ // public Iterator<T> iterator() {
+ // return data.keySet().iterator();
+ // }
+ // public long get(T s) {
+ // Long result = data.get(s);
+ // return result != null ? result : 0L;
+ // }
+ // public void add(T item, int count) {
+ // Long result = data.get(item);
+ // data.put(item, result == null ? count : result + count);
+ // }
+ // }
+
+ public static <T> String join(T[] source, String separator) {
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < source.length; ++i) {
+ if (i != 0) result.append(separator);
+ result.append(source[i]);
+ }
+ return result.toString();
+ }
+
+ public static <T> String join(Iterable<T> source, String separator) {
+ StringBuilder result = new StringBuilder();
+ boolean first = true;
+ for (T item : source) {
+ if (!first) result.append(separator);
+ else first = false;
+ result.append(item.toString());
+ }
+ return result.toString();
+ }
+
+ public static class CollectionUtilities {
+ public static <T, U extends Iterable<T>> String join(U source, String separator) {
+ return XCldrStub.join(source, separator);
+ }
+ }
+
+ public static class Joiner {
+ private final String separator;
+ private Joiner(String separator) {
+ this.separator = separator;
+ }
+ public static final Joiner on(String separator) {
+ return new Joiner(separator);
+ }
+ public <T> String join(T[] source) {
+ return XCldrStub.join(source, separator);
+ }
+ public <T> String join(Iterable<T> source) {
+ return XCldrStub.join(source, separator);
+ }
+ }
+
+ public static class Splitter {
+ Pattern pattern;
+ boolean trimResults = false;
+ public Splitter(char c) {
+ this(Pattern.compile("\\Q" + c + "\\E"));
+ }
+ public Splitter(Pattern p) {
+ pattern = p;
+ }
+ public static Splitter on(char c) {
+ return new Splitter(c);
+ }
+ public static Splitter on(Pattern p) {
+ return new Splitter(p);
+ }
+ public List<String> splitToList(String input) {
+ String[] items = pattern.split(input);
+ if (trimResults) {
+ for (int i = 0; i < items.length; ++i) {
+ items[i] = items[i].trim();
+ }
+ }
+ return Arrays.asList(items);
+ }
+ public Splitter trimResults() {
+ trimResults = true;
+ return this;
+ }
+ public Iterable<String> split(String input) {
+ return splitToList(input);
+ }
+ }
+
+ public static class ImmutableSet {
+ public static <T> Set<T> copyOf(Set<T> values) {
+ return Collections.unmodifiableSet(new LinkedHashSet<T>(values)); // copy set for safety, preserve order
+ }
+ }
+ public static class ImmutableMap {
+ public static <K,V> Map<K,V> copyOf(Map<K,V> values) {
+ return Collections.unmodifiableMap(new LinkedHashMap<K,V>(values)); // copy set for safety, preserve order
+ }
+ }
+ public static class ImmutableMultimap {
+ public static <K,V> Multimap<K,V> copyOf(Multimap<K,V> values) {
+ LinkedHashMap<K, Set<V>> temp = new LinkedHashMap<K,Set<V>>(); // semi-deep copy, preserve order
+ for (Entry<K, Set<V>> entry : values.asMap().entrySet()) {
+ Set<V> value = entry.getValue();
+ temp.put(entry.getKey(), value.size() == 1
+ ? Collections.singleton(value.iterator().next())
+ : Collections.unmodifiableSet(new LinkedHashSet<V>(value)));
+ }
+ return new Multimap<K,V>(Collections.unmodifiableMap(temp), null);
+ }
+ }
+
+ public static class FileUtilities {
+ public static final Charset UTF8 = Charset.forName("utf-8");
+
+ public static BufferedReader openFile(Class<?> class1, String file) {
+ return openFile(class1, file, UTF8);
+ }
+
+ public static BufferedReader openFile(Class<?> class1, String file, Charset charset) {
+ // URL path = null;
+ // String externalForm = null;
+ try {
+ final InputStream resourceAsStream = class1.getResourceAsStream(file);
+ if (charset == null) {
+ charset = UTF8;
+ }
+ InputStreamReader reader = new InputStreamReader(resourceAsStream, charset);
+ BufferedReader bufferedReader = new BufferedReader(reader, 1024 * 64);
+ return bufferedReader;
+ } catch (Exception e) {
+ String className = class1 == null ? null : class1.getCanonicalName();
+ String canonicalName = null;
+ try {
+ String relativeFileName = getRelativeFileName(class1, "../util/");
+ canonicalName = new File(relativeFileName).getCanonicalPath();
+ } catch (Exception e1) {
+ throw new ICUUncheckedIOException("Couldn't open file: " + file + "; relative to class: "
+ + className, e);
+ }
+ throw new ICUUncheckedIOException("Couldn't open file " + file + "; in path " + canonicalName + "; relative to class: "
+ + className, e);
+ }
+ }
+ public static String getRelativeFileName(Class<?> class1, String filename) {
+ URL resource = class1 == null ?
+ FileUtilities.class.getResource(filename) : class1.getResource(filename);
+ String resourceString = resource.toString();
+ if (resourceString.startsWith("file:")) {
+ return resourceString.substring(5);
+ } else if (resourceString.startsWith("jar:file:")) {
+ return resourceString.substring(9);
+ } else {
+ throw new ICUUncheckedIOException("File not found: " + resourceString);
+ }
+ }
+ }
+
+ static public class RegexUtilities {
+ public static int findMismatch(Matcher m, CharSequence s) {
+ int i;
+ for (i = 1; i < s.length(); ++i) {
+ boolean matches = m.reset(s.subSequence(0, i)).matches();
+ if (!matches && !m.hitEnd()) {
+ break;
+ }
+ }
+ return i - 1;
+ }
+ public static String showMismatch(Matcher m, CharSequence s) {
+ int failPoint = findMismatch(m, s);
+ String show = s.subSequence(0, failPoint) + "☹" + s.subSequence(failPoint, s.length());
+ return show;
+ }
+ }
+
+ public interface Predicate<T> {
+ /**
+ * Evaluates this predicate on the given argument.
+ *
+ * @param t the input argument
+ * @return {@code true} if the input argument matches the predicate,
+ * otherwise {@code false}
+ */
+ boolean test(T t);
+ }
+} \ No newline at end of file
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
new file mode 100644
index 000000000..9c05e28d6
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/locale/XLikelySubtags.java
@@ -0,0 +1,671 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.locale;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+
+import android.icu.impl.ICUData;
+import android.icu.impl.ICUResourceBundle;
+import android.icu.impl.Utility;
+import android.icu.impl.locale.XCldrStub.HashMultimap;
+import android.icu.impl.locale.XCldrStub.Multimap;
+import android.icu.impl.locale.XCldrStub.Multimaps;
+import android.icu.util.ICUException;
+import android.icu.util.ULocale;
+import android.icu.util.ULocale.Minimize;
+import android.icu.util.UResourceBundle;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class XLikelySubtags {
+
+ private static final XLikelySubtags DEFAULT = new XLikelySubtags();
+
+ public static final XLikelySubtags getDefault() {
+ return DEFAULT;
+ }
+
+ @SuppressWarnings("unchecked")
+ static abstract class Maker {
+ abstract <V> V make();
+
+ public <K,V> V getSubtable(Map<K, V> langTable, final K language) {
+ V scriptTable = langTable.get(language);
+ if (scriptTable == null) {
+ langTable.put(language, scriptTable = (V) make());
+ }
+ return scriptTable;
+ }
+
+ static final Maker HASHMAP = new Maker() {
+ @Override
+ public Map<Object,Object> make() {
+ return new HashMap<Object,Object>();
+ }
+ };
+
+ static final Maker TREEMAP = new Maker() {
+ @Override
+ public Map<Object,Object> make() {
+ return new TreeMap<Object,Object>();
+ }
+ };
+ }
+
+ public static class Aliases {
+ final Map<String, String> toCanonical;
+ final Multimap<String, String> toAliases;
+ public String getCanonical(String alias) {
+ String canonical = toCanonical.get(alias);
+ return canonical == null ? alias : canonical;
+ }
+ public Set<String> getAliases(String canonical) {
+ Set<String> aliases = toAliases.get(canonical);
+ return aliases == null ? Collections.singleton(canonical) : aliases;
+ }
+ public Aliases(String key) {
+ UResourceBundle metadata = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,"metadata",ICUResourceBundle.ICU_DATA_CLASS_LOADER);
+ UResourceBundle metadataAlias = metadata.get("alias");
+ UResourceBundle territoryAlias = metadataAlias.get(key);
+ Map<String, String> toCanonical1 = new HashMap<String, String>();
+ for ( int i = 0 ; i < territoryAlias.getSize(); i++ ) {
+ UResourceBundle res = territoryAlias.get(i);
+ String aliasFrom = res.getKey();
+ if (aliasFrom.contains("_")) {
+ continue; // only simple aliasing
+ }
+ String aliasReason = res.get("reason").getString();
+ if (aliasReason.equals("overlong")) {
+ continue;
+ }
+ String aliasTo = res.get("replacement").getString();
+ int spacePos = aliasTo.indexOf(' ');
+ String aliasFirst = spacePos < 0 ? aliasTo : aliasTo.substring(0, spacePos);
+ if (aliasFirst.contains("_")) {
+ continue; // only simple aliasing
+ }
+ toCanonical1.put(aliasFrom, aliasFirst);
+ }
+ if (key.equals("language")) {
+ toCanonical1.put("mo", "ro"); // special case
+ }
+ toCanonical = Collections.unmodifiableMap(toCanonical1);
+ toAliases = Multimaps.invertFrom(toCanonical1, HashMultimap.<String,String>create());
+ }
+ }
+
+ public static class LSR {
+ public final String language;
+ public final String script;
+ public final String region;
+
+ public static Aliases LANGUAGE_ALIASES = new Aliases("language");
+ public static Aliases REGION_ALIASES = new Aliases("territory");
+
+ public static LSR from(String language, String script, String region) {
+ return new LSR(language, script, region);
+ }
+
+ // from http://unicode.org/reports/tr35/#Unicode_language_identifier
+ // but simplified to requiring language subtag, and nothing beyond region
+ // #1 is language
+ // #2 is script
+ // #3 is region
+ // static final String pat =
+ // "language_id = (unicode_language_subtag)"
+ // + "(?:sep(unicode_script_subtag))?"
+ // + "(?:sep(unicode_region_subtag))?;\n"
+ // + "unicode_language_subtag = alpha{2,3}|alpha{5,8};\n"
+ // + "unicode_script_subtag = alpha{4};\n"
+ // + "unicode_region_subtag = alpha{2}|digit{3};\n"
+ // + "sep = [-_];\n"
+ // + "digit = [0-9];\n"
+ // + "alpha = [A-Za-z];\n"
+ // ;
+ // static {
+ // System.out.println(pat);
+ // System.out.println(new UnicodeRegex().compileBnf(pat));
+ // }
+ // static final Pattern LANGUAGE_PATTERN = Pattern.compile(
+ // "([a-zA-Z0-9]+)" // (?:[-_]([a-zA-Z0-9]+))?(?:[-_]([a-zA-Z0-9]+))?"
+ // //new UnicodeRegex().compileBnf(pat)
+ // );
+ //
+ // TODO: fix this to check for format. Not required, since this is only called internally, but safer for the future.
+ static LSR from(String languageIdentifier) {
+ String[] parts = languageIdentifier.split("[-_]");
+ if (parts.length < 1 || parts.length > 3) {
+ throw new ICUException("too many subtags");
+ }
+ String lang = parts[0].toLowerCase();
+ 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);
+
+ // Matcher matcher = LANGUAGE_PATTERN.matcher(languageIdentifier);
+ // if (!matcher.matches()) {
+ // return new LSR(matcher.group(1), matcher.group(2), matcher.group(3));
+ // }
+ // System.out.println(RegexUtilities.showMismatch(matcher, languageIdentifier));
+ // throw new ICUException("invalid language id");
+ }
+
+ public static LSR from(ULocale locale) {
+ return new LSR(locale.getLanguage(), locale.getScript(), locale.getCountry());
+ }
+
+ public static LSR fromMaximalized(ULocale locale) {
+ return fromMaximalized(locale.getLanguage(), locale.getScript(), locale.getCountry());
+ }
+
+ public static LSR fromMaximalized(String language, String script, String region) {
+ String canonicalLanguage = LANGUAGE_ALIASES.getCanonical(language);
+ // script is ok
+ String canonicalRegion = REGION_ALIASES.getCanonical(region); // getCanonical(REGION_ALIASES.get(region));
+
+ return DEFAULT.maximize(canonicalLanguage, script, canonicalRegion);
+ }
+
+ public LSR(String language, String script, String region) {
+ this.language = language;
+ this.script = script;
+ this.region = region;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder(language);
+ if (!script.isEmpty()) {
+ result.append('-').append(script);
+ }
+ if (!region.isEmpty()) {
+ result.append('-').append(region);
+ }
+ return result.toString();
+ }
+ public LSR replace(String language2, String script2, String region2) {
+ if (language2 == null && script2 == null && region2 == null) return this;
+ return new LSR(
+ language2 == null ? language: language2,
+ script2 == null ? script : script2,
+ region2 == null ? region : region2);
+ }
+ @Override
+ public boolean equals(Object obj) {
+ LSR other;
+ return this == obj ||
+ (obj != null
+ && obj.getClass() == this.getClass()
+ && language.equals((other = (LSR) obj).language)
+ && script.equals(other.script)
+ && region.equals(other.region));
+ }
+ @Override
+ public int hashCode() {
+ return Utility.hash(language, script, region);
+ }
+ }
+
+ final Map<String, Map<String, Map<String, LSR>>> langTable;
+
+ public XLikelySubtags() {
+ this(getDefaultRawData(), true);
+ }
+
+ private static Map<String, String> getDefaultRawData() {
+ Map<String, String> rawData = new TreeMap<String, String>();
+ UResourceBundle bundle = UResourceBundle.getBundleInstance( ICUData.ICU_BASE_NAME, "likelySubtags");
+ for (Enumeration<String> enumer = bundle.getKeys(); enumer.hasMoreElements();) {
+ String key = enumer.nextElement();
+ rawData.put(key, bundle.getString(key));
+ }
+ return rawData;
+ }
+
+ public XLikelySubtags(Map<String, String> rawData, boolean skipNoncanonical) {
+ this.langTable = init(rawData, skipNoncanonical);
+ }
+
+ private Map<String, Map<String, Map<String, LSR>>> init(final Map<String, String> rawData, boolean skipNoncanonical) {
+ // prepare alias info. We want a mapping from the canonical form to all aliases
+
+ //Multimap<String,String> canonicalToAliasLanguage = HashMultimap.create();
+ // getAliasInfo(LANGUAGE_ALIASES, canonicalToAliasLanguage);
+
+ // Don't bother with script; there are none
+
+ //Multimap<String,String> canonicalToAliasRegion = HashMultimap.create();
+ // getAliasInfo(REGION_ALIASES, canonicalToAliasRegion);
+
+ Maker maker = Maker.TREEMAP;
+ Map<String, Map<String, Map<String, LSR>>> result = maker.make();
+ // Splitter bar = Splitter.on('_');
+ // int last = -1;
+ // set the base data
+ Map<LSR,LSR> internCache = new HashMap<LSR,LSR>();
+ for (Entry<String, String> sourceTarget : rawData.entrySet()) {
+ LSR ltp = LSR.from(sourceTarget.getKey());
+ final String language = ltp.language;
+ final String script = ltp.script;
+ final String region = ltp.region;
+
+ ltp = LSR.from(sourceTarget.getValue());
+ String languageTarget = ltp.language;
+ final String scriptTarget = ltp.script;
+ final String regionTarget = ltp.region;
+
+ set(result, language, script, region, languageTarget, scriptTarget, regionTarget, internCache);
+ // now add aliases
+ Collection<String> languageAliases = LSR.LANGUAGE_ALIASES.getAliases(language);
+ // if (languageAliases.isEmpty()) {
+ // languageAliases = Collections.singleton(language);
+ // }
+ Collection<String> regionAliases = LSR.REGION_ALIASES.getAliases(region);
+ // if (regionAliases.isEmpty()) {
+ // regionAliases = Collections.singleton(region);
+ // }
+ for (String languageAlias : languageAliases) {
+ for (String regionAlias : regionAliases) {
+ if (languageAlias.equals(language) && regionAlias.equals(region)) {
+ continue;
+ }
+ set(result, languageAlias, script, regionAlias, languageTarget, scriptTarget, regionTarget, internCache);
+ }
+ }
+ }
+ // hack
+ set(result, "und", "Latn", "", "en", "Latn", "US", internCache);
+
+ // 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"/>
+
+ // so und-Latn-GH => ak-Latn-GH
+ Map<String, Map<String, LSR>> undScriptMap = result.get("und");
+ Map<String, LSR> undEmptyRegionMap = undScriptMap.get("");
+ for (Entry<String, LSR> regionEntry : undEmptyRegionMap.entrySet()) {
+ final LSR value = regionEntry.getValue();
+ set(result, "und", value.script, value.region, value);
+ }
+ //
+ // check that every level has "" (or "und")
+ if (!result.containsKey("und")) {
+ throw new IllegalArgumentException("failure: base");
+ }
+ for (Entry<String, Map<String, Map<String, LSR>>> langEntry : result.entrySet()) {
+ String lang = langEntry.getKey();
+ final Map<String, Map<String, LSR>> scriptMap = langEntry.getValue();
+ if (!scriptMap.containsKey("")) {
+ throw new IllegalArgumentException("failure: " + lang);
+ }
+ for (Entry<String, Map<String, LSR>> scriptEntry : scriptMap.entrySet()) {
+ String script = scriptEntry.getKey();
+ final Map<String, LSR> regionMap = scriptEntry.getValue();
+ if (!regionMap.containsKey("")) {
+ throw new IllegalArgumentException("failure: " + lang + "-" + script);
+ }
+ // for (Entry<String, LSR> regionEntry : regionMap.entrySet()) {
+ // String region = regionEntry.getKey();
+ // LSR value = regionEntry.getValue();
+ // }
+ }
+ }
+ return result;
+ }
+
+ // private void getAliasInfo(Map<String, R2<List<String>, String>> aliasInfo, Multimap<String, String> canonicalToAlias) {
+ // for (Entry<String, R2<List<String>, String>> e : aliasInfo.entrySet()) {
+ // final String alias = e.getKey();
+ // if (alias.contains("_")) {
+ // continue; // only do simple aliasing
+ // }
+ // String canonical = getCanonical(e.getValue());
+ // canonicalToAlias.put(canonical, alias);
+ // }
+ // }
+
+ // private static String getCanonical(R2<List<String>, String> aliasAndReason) {
+ // if (aliasAndReason == null) {
+ // return null;
+ // }
+ // if (aliasAndReason.get1().equals("overlong")) {
+ // return null;
+ // }
+ // List<String> value = aliasAndReason.get0();
+ // if (value.size() != 1) {
+ // return null;
+ // }
+ // final String canonical = value.iterator().next();
+ // if (canonical.contains("_")) {
+ // return null; // only do simple aliasing
+ // }
+ // return canonical;
+ // }
+
+ private void set(Map<String, Map<String, Map<String, LSR>>> langTable, final String language, final String script, final String region,
+ final String languageTarget, final String scriptTarget, final String regionTarget, Map<LSR, LSR> internCache) {
+ LSR newValue = new LSR(languageTarget, scriptTarget, regionTarget);
+ LSR oldValue = internCache.get(newValue);
+ if (oldValue == null) {
+ internCache.put(newValue, newValue);
+ oldValue = newValue;
+ }
+ set(langTable, language, script, region, oldValue);
+ }
+
+ private void set(Map<String, Map<String, Map<String, LSR>>> langTable, final String language, final String script, final String region, LSR newValue) {
+ Map<String, Map<String, LSR>> scriptTable = Maker.TREEMAP.getSubtable(langTable, language);
+ Map<String, LSR> regionTable = Maker.TREEMAP.getSubtable(scriptTable, script);
+ // LSR oldValue = regionTable.get(region);
+ // if (oldValue != null) {
+ // int debug = 0;
+ // }
+ regionTable.put(region, newValue);
+ }
+
+ /**
+ * Convenience methods
+ */
+ public LSR maximize(String source) {
+ return maximize(ULocale.forLanguageTag(source));
+ }
+
+ public LSR maximize(ULocale source) {
+ return maximize(source.getLanguage(), source.getScript(), source.getCountry());
+ }
+
+ public LSR maximize(LSR source) {
+ return maximize(source.language, source.script, source.region);
+ }
+
+ // public static ULocale addLikelySubtags(ULocale loc) {
+ //
+ // }
+
+ /**
+ * Raw access to addLikelySubtags. Input must be in canonical format, eg "en", not "eng" or "EN".
+ */
+ public LSR maximize(String language, String script, String region) {
+ int retainOldMask = 0;
+ Map<String, Map<String, LSR>> scriptTable = langTable.get(language);
+ if (scriptTable == null) { // cannot happen if language == "und"
+ retainOldMask |= 4;
+ scriptTable = langTable.get("und");
+ } else if (!language.equals("und")) {
+ retainOldMask |= 4;
+ }
+
+ if (script.equals("Zzzz")) {
+ script = "";
+ }
+ Map<String, LSR> regionTable = scriptTable.get(script);
+ if (regionTable == null) { // cannot happen if script == ""
+ retainOldMask |= 2;
+ regionTable = scriptTable.get("");
+ } else if (!script.isEmpty()) {
+ retainOldMask |= 2;
+ }
+
+ if (region.equals("ZZ")) {
+ region = "";
+ }
+ LSR result = regionTable.get(region);
+ if (result == null) { // cannot happen if region == ""
+ retainOldMask |= 1;
+ result = regionTable.get("");
+ if (result == null) {
+ return null;
+ }
+ } else if (!region.isEmpty()) {
+ retainOldMask |= 1;
+ }
+
+ switch (retainOldMask) {
+ default:
+ case 0: return result;
+ case 1: return result.replace(null, null, region);
+ case 2: return result.replace(null, script, null);
+ case 3: return result.replace(null, script, region);
+ case 4: return result.replace(language, null, null);
+ case 5: return result.replace(language, null, region);
+ case 6: return result.replace(language, script, null);
+ case 7: return result.replace(language, script, region);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private LSR minimizeSubtags(String languageIn, String scriptIn, String regionIn, Minimize fieldToFavor) {
+ LSR result = maximize(languageIn, scriptIn, regionIn);
+
+ // We could try just a series of checks, like:
+ // LSR result2 = addLikelySubtags(languageIn, "", "");
+ // if result.equals(result2) return result2;
+ // However, we can optimize 2 of the cases:
+ // (languageIn, "", "")
+ // (languageIn, "", regionIn)
+
+ Map<String, Map<String, LSR>> scriptTable = langTable.get(result.language);
+
+ Map<String, LSR> regionTable0 = scriptTable.get("");
+ LSR value00 = regionTable0.get("");
+ boolean favorRegionOk = false;
+ if (result.script.equals(value00.script)) { //script is default
+ if (result.region.equals(value00.region)) {
+ return result.replace(null, "", "");
+ } else if (fieldToFavor == Minimize.FAVOR_REGION) {
+ return result.replace(null, "", null);
+ } else {
+ favorRegionOk = true;
+ }
+ }
+
+ // The last case is not as easy to optimize.
+ // Maybe do later, but for now use the straightforward code.
+ LSR result2 = maximize(languageIn, scriptIn, "");
+ if (result2.equals(result)) {
+ return result.replace(null, null, "");
+ } else if (favorRegionOk) {
+ return result.replace(null, "", null);
+ }
+ return result;
+ }
+
+ private static StringBuilder show(Map<?,?> map, String indent, StringBuilder output) {
+ String first = indent.isEmpty() ? "" : "\t";
+ for (Entry<?,?> e : map.entrySet()) {
+ String key = e.getKey().toString();
+ Object value = e.getValue();
+ output.append(first + (key.isEmpty() ? "∅" : key));
+ if (value instanceof Map) {
+ show((Map<?,?>)value, indent+"\t", output);
+ } else {
+ output.append("\t" + Utility.toString(value)).append("\n");
+ }
+ first = indent;
+ }
+ return output;
+ }
+
+ @Override
+ public String toString() {
+ return show(langTable, "", new StringBuilder()).toString();
+ }
+
+ // public static void main(String[] args) {
+ // System.out.println(LSR.fromMaximalized(ULocale.ENGLISH));
+ //
+ // final Map<String, String> rawData = sdi.getLikelySubtags();
+ // XLikelySubtags ls = XLikelySubtags.getDefault();
+ // System.out.println(ls);
+ // ls.maximize(new ULocale("iw"));
+ // if (true) return;
+ //
+ // LanguageTagParser ltp = new LanguageTagParser();
+ //
+ // // get all the languages, scripts, and regions
+ // Set<String> languages = new TreeSet<String>();
+ // Set<String> scripts = new TreeSet<String>();
+ // Set<String> regions = new TreeSet<String>();
+ // Counter<String> languageCounter = new Counter<String>();
+ // Counter<String> scriptCounter = new Counter<String>();
+ // Counter<String> regionCounter = new Counter<String>();
+ //
+ // for (Entry<String, String> sourceTarget : rawData.entrySet()) {
+ // final String source = sourceTarget.getKey();
+ // ltp.set(source);
+ // languages.add(ltp.getLanguage());
+ // scripts.add(ltp.getScript());
+ // regions.add(ltp.getRegion());
+ // final String target = sourceTarget.getValue();
+ // ltp.set(target);
+ // add(target, languageCounter, ltp.getLanguage(), 1);
+ // add(target, scriptCounter, ltp.getScript(), 1);
+ // add(target, regionCounter, ltp.getRegion(), 1);
+ // }
+ // ltp.set("und-Zzzz-ZZ");
+ // languageCounter.add(ltp.getLanguage(), 1);
+ // scriptCounter.add(ltp.getScript(), 1);
+ // regionCounter.add(ltp.getRegion(), 1);
+ //
+ // if (SHORT) {
+ // removeSingletons(languages, languageCounter);
+ // removeSingletons(scripts, scriptCounter);
+ // removeSingletons(regions, regionCounter);
+ // }
+ //
+ // System.out.println("languages: " + languages.size() + "\n\t" + languages + "\n\t" + languageCounter);
+ // System.out.println("scripts: " + scripts.size() + "\n\t" + scripts + "\n\t" + scriptCounter);
+ // System.out.println("regions: " + regions.size() + "\n\t" + regions + "\n\t" + regionCounter);
+ //
+ // int maxCount = Integer.MAX_VALUE;
+ //
+ // int counter = maxCount;
+ // long tempTime = System.nanoTime();
+ // newMax:
+ // for (String language : languages) {
+ // for (String script : scripts) {
+ // for (String region : regions) {
+ // if (--counter < 0) break newMax;
+ // LSR result = ls.maximize(language, script, region);
+ // }
+ // }
+ // }
+ // long newMaxTime = System.nanoTime() - tempTime;
+ // System.out.println("newMaxTime: " + newMaxTime);
+ //
+ // counter = maxCount;
+ // tempTime = System.nanoTime();
+ // newMin:
+ // for (String language : languages) {
+ // for (String script : scripts) {
+ // for (String region : regions) {
+ // if (--counter < 0) break newMin;
+ // LSR minNewS = ls.minimizeSubtags(language, script, region, Minimize.FAVOR_SCRIPT);
+ // }
+ // }
+ // }
+ // long newMinTime = System.nanoTime() - tempTime;
+ // System.out.println("newMinTime: " + newMinTime);
+ //
+ // // *****
+ //
+ // tempTime = System.nanoTime();
+ // counter = maxCount;
+ // oldMax:
+ // for (String language : languages) {
+ // for (String script : scripts) {
+ // for (String region : regions) {
+ // if (--counter < 0) break oldMax;
+ // ULocale tempLocale = new ULocale(language, script, region);
+ // ULocale max = ULocale.addLikelySubtags(tempLocale);
+ // }
+ // }
+ // }
+ // long oldMaxTime = System.nanoTime() - tempTime;
+ // System.out.println("oldMaxTime: " + oldMaxTime + "\t" + oldMaxTime/newMaxTime + "x");
+ //
+ // counter = maxCount;
+ // tempTime = System.nanoTime();
+ // oldMin:
+ // for (String language : languages) {
+ // for (String script : scripts) {
+ // for (String region : regions) {
+ // if (--counter < 0) break oldMin;
+ // ULocale tempLocale = new ULocale(language, script, region);
+ // ULocale minOldS = ULocale.minimizeSubtags(tempLocale, Minimize.FAVOR_SCRIPT);
+ // }
+ // }
+ // }
+ // long oldMinTime = System.nanoTime() - tempTime;
+ // System.out.println("oldMinTime: " + oldMinTime + "\t" + oldMinTime/newMinTime + "x");
+ //
+ // counter = maxCount;
+ // testMain:
+ // for (String language : languages) {
+ // System.out.println(language);
+ // int tests = 0;
+ // for (String script : scripts) {
+ // for (String region : regions) {
+ // ++tests;
+ // if (--counter < 0) break testMain;
+ // LSR maxNew = ls.maximize(language, script, region);
+ // LSR minNewS = ls.minimizeSubtags(language, script, region, Minimize.FAVOR_SCRIPT);
+ // LSR minNewR = ls.minimizeSubtags(language, script, region, Minimize.FAVOR_REGION);
+ //
+ // ULocale tempLocale = new ULocale(language, script, region);
+ // ULocale maxOld = ULocale.addLikelySubtags(tempLocale);
+ // ULocale minOldS = ULocale.minimizeSubtags(tempLocale, Minimize.FAVOR_SCRIPT);
+ // ULocale minOldR = ULocale.minimizeSubtags(tempLocale, Minimize.FAVOR_REGION);
+ //
+ // // check values
+ // final String maxNewS = String.valueOf(maxNew);
+ // final String maxOldS = maxOld.toLanguageTag();
+ // boolean sameMax = maxOldS.equals(maxNewS);
+ //
+ // final String minNewSS = String.valueOf(minNewS);
+ // final String minOldSS = minOldS.toLanguageTag();
+ // boolean sameMinS = minNewSS.equals(minOldSS);
+ //
+ // final String minNewRS = String.valueOf(minNewR);
+ // final String minOldRS = minOldS.toLanguageTag();
+ // boolean sameMinR = minNewRS.equals(minOldRS);
+ //
+ // if (sameMax && sameMinS && sameMinR) continue;
+ // System.out.println(new LSR(language, script, region)
+ // + "\tmax: " + maxNew
+ // + (sameMax ? "" : "≠" + maxOldS)
+ // + "\tminS: " + minNewS
+ // + (sameMinS ? "" : "≠" + minOldS)
+ // + "\tminR: " + minNewR
+ // + (sameMinR ? "" : "≠" + minOldR)
+ // );
+ // }
+ // }
+ // System.out.println(language + ": " + tests);
+ // }
+ // }
+ //
+ // private static void add(String target, Counter<String> languageCounter, String language, int count) {
+ // if (language.equals("aa")) {
+ // int debug = 0;
+ // }
+ // languageCounter.add(language, count);
+ // }
+ //
+ // private static void removeSingletons(Set<String> languages, Counter<String> languageCounter) {
+ // for (String s : languageCounter) {
+ // final long count = languageCounter.get(s);
+ // if (count <= 1) {
+ // languages.remove(s);
+ // }
+ // }
+ // }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/XLocaleDistance.java b/android_icu4j/src/main/java/android/icu/impl/locale/XLocaleDistance.java
new file mode 100644
index 000000000..488c33af7
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/locale/XLocaleDistance.java
@@ -0,0 +1,1348 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.locale;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import android.icu.impl.ICUResourceBundle;
+import android.icu.impl.Row;
+import android.icu.impl.Row.R4;
+import android.icu.impl.Utility;
+import android.icu.impl.locale.XCldrStub.CollectionUtilities;
+import android.icu.impl.locale.XCldrStub.ImmutableMap;
+import android.icu.impl.locale.XCldrStub.ImmutableMultimap;
+import android.icu.impl.locale.XCldrStub.ImmutableSet;
+import android.icu.impl.locale.XCldrStub.LinkedHashMultimap;
+import android.icu.impl.locale.XCldrStub.Multimap;
+import android.icu.impl.locale.XCldrStub.Multimaps;
+import android.icu.impl.locale.XCldrStub.Predicate;
+import android.icu.impl.locale.XCldrStub.Splitter;
+import android.icu.impl.locale.XCldrStub.TreeMultimap;
+import android.icu.impl.locale.XLikelySubtags.LSR;
+import android.icu.impl.locale.XLocaleDistance.RegionMapper.Builder;
+import android.icu.text.LocaleDisplayNames;
+import android.icu.util.LocaleMatcher;
+import android.icu.util.Output;
+import android.icu.util.ULocale;
+import android.icu.util.UResourceBundleIterator;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class XLocaleDistance {
+
+ static final boolean PRINT_OVERRIDES = false;
+
+ public static final int ABOVE_THRESHOLD = 100;
+
+ @Deprecated
+ public static final String ANY = "�"; // matches any character. Uses value above any subtag.
+
+ private static String fixAny(String string) {
+ return "*".equals(string) ? ANY : string;
+ }
+
+ static final LocaleDisplayNames english = LocaleDisplayNames.getInstance(ULocale.ENGLISH);
+
+ private static List<R4<String, String, Integer, Boolean>> xGetLanguageMatcherData() {
+ List<R4<String, String, Integer, Boolean>> distanceList = new ArrayList<R4<String, String, Integer, Boolean>>();
+
+ ICUResourceBundle suppData = LocaleMatcher.getICUSupplementalData();
+ ICUResourceBundle languageMatchingNew = suppData.findTopLevel("languageMatchingNew");
+ ICUResourceBundle written = (ICUResourceBundle) languageMatchingNew.get("written");
+
+ for(UResourceBundleIterator iter = written.getIterator(); iter.hasNext();) {
+ ICUResourceBundle item = (ICUResourceBundle) iter.next();
+ boolean oneway = item.getSize() > 3 && "1".equals(item.getString(3));
+ distanceList.add(
+ (R4<String, String, Integer, Boolean>) // note: .freeze returning wrong type, so casting.
+ Row.of(
+ item.getString(0),
+ item.getString(1),
+ Integer.parseInt(item.getString(2)),
+ oneway)
+ .freeze());
+ }
+ return Collections.unmodifiableList(distanceList);
+ }
+
+ @SuppressWarnings("unused")
+ private static Set<String> xGetParadigmLocales() {
+ ICUResourceBundle suppData = LocaleMatcher.getICUSupplementalData();
+ ICUResourceBundle languageMatchingInfo = suppData.findTopLevel("languageMatchingInfo");
+ ICUResourceBundle writtenParadigmLocales = (ICUResourceBundle) languageMatchingInfo.get("written")
+ .get("paradigmLocales");
+ // paradigmLocales{ "en", "en-GB",... }
+ HashSet<String> paradigmLocales = new HashSet<String>(Arrays.asList(writtenParadigmLocales.getStringArray()));
+ return Collections.unmodifiableSet(paradigmLocales);
+ }
+
+ @SuppressWarnings("unused")
+ private static Map<String, String> xGetMatchVariables() {
+ ICUResourceBundle suppData = LocaleMatcher.getICUSupplementalData();
+ ICUResourceBundle languageMatchingInfo = suppData.findTopLevel("languageMatchingInfo");
+ ICUResourceBundle writtenMatchVariables = (ICUResourceBundle) languageMatchingInfo.get("written")
+ .get("matchVariable");
+ // matchVariable{ americas{"019"} cnsar{"HK+MO"} ...}
+
+ HashMap<String,String> matchVariables = new HashMap<String,String>();
+ for (Enumeration<String> enumer = writtenMatchVariables.getKeys(); enumer.hasMoreElements(); ) {
+ String key = enumer.nextElement();
+ matchVariables.put(key, writtenMatchVariables.getString(key));
+ }
+ return Collections.unmodifiableMap(matchVariables);
+ }
+
+ private static Multimap<String, String> xGetContainment() {
+ TreeMultimap<String,String> containment = TreeMultimap.create();
+ containment
+ .putAll("001", "019", "002", "150", "142", "009")
+ .putAll("011", "BF", "BJ", "CI", "CV", "GH", "GM", "GN", "GW", "LR", "ML", "MR", "NE", "NG", "SH", "SL", "SN", "TG")
+ .putAll("013", "BZ", "CR", "GT", "HN", "MX", "NI", "PA", "SV")
+ .putAll("014", "BI", "DJ", "ER", "ET", "KE", "KM", "MG", "MU", "MW", "MZ", "RE", "RW", "SC", "SO", "SS", "TZ", "UG", "YT", "ZM", "ZW")
+ .putAll("142", "145", "143", "030", "034", "035")
+ .putAll("143", "TM", "TJ", "KG", "KZ", "UZ")
+ .putAll("145", "AE", "AM", "AZ", "BH", "CY", "GE", "IL", "IQ", "JO", "KW", "LB", "OM", "PS", "QA", "SA", "SY", "TR", "YE", "NT", "YD")
+ .putAll("015", "DZ", "EG", "EH", "LY", "MA", "SD", "TN", "EA", "IC")
+ .putAll("150", "154", "155", "151", "039")
+ .putAll("151", "BG", "BY", "CZ", "HU", "MD", "PL", "RO", "RU", "SK", "UA", "SU")
+ .putAll("154", "GG", "IM", "JE", "AX", "DK", "EE", "FI", "FO", "GB", "IE", "IS", "LT", "LV", "NO", "SE", "SJ")
+ .putAll("155", "AT", "BE", "CH", "DE", "FR", "LI", "LU", "MC", "NL", "DD", "FX")
+ .putAll("017", "AO", "CD", "CF", "CG", "CM", "GA", "GQ", "ST", "TD", "ZR")
+ .putAll("018", "BW", "LS", "NA", "SZ", "ZA")
+ .putAll("019", "021", "013", "029", "005", "003", "419")
+ .putAll("002", "015", "011", "017", "014", "018")
+ .putAll("021", "BM", "CA", "GL", "PM", "US")
+ .putAll("029", "AG", "AI", "AW", "BB", "BL", "BQ", "BS", "CU", "CW", "DM", "DO", "GD", "GP", "HT", "JM", "KN", "KY", "LC", "MF", "MQ", "MS", "PR", "SX", "TC", "TT", "VC", "VG", "VI", "AN")
+ .putAll("003", "021", "013", "029")
+ .putAll("030", "CN", "HK", "JP", "KP", "KR", "MN", "MO", "TW")
+ .putAll("035", "BN", "ID", "KH", "LA", "MM", "MY", "PH", "SG", "TH", "TL", "VN", "BU", "TP")
+ .putAll("039", "AD", "AL", "BA", "ES", "GI", "GR", "HR", "IT", "ME", "MK", "MT", "RS", "PT", "SI", "SM", "VA", "XK", "CS", "YU")
+ .putAll("419", "013", "029", "005")
+ .putAll("005", "AR", "BO", "BR", "CL", "CO", "EC", "FK", "GF", "GY", "PE", "PY", "SR", "UY", "VE")
+ .putAll("053", "AU", "NF", "NZ")
+ .putAll("054", "FJ", "NC", "PG", "SB", "VU")
+ .putAll("057", "FM", "GU", "KI", "MH", "MP", "NR", "PW")
+ .putAll("061", "AS", "CK", "NU", "PF", "PN", "TK", "TO", "TV", "WF", "WS")
+ .putAll("034", "AF", "BD", "BT", "IN", "IR", "LK", "MV", "NP", "PK")
+ .putAll("009", "053", "054", "057", "061", "QO")
+ .putAll("QO", "AQ", "BV", "CC", "CX", "GS", "HM", "IO", "TF", "UM", "AC", "CP", "DG", "TA")
+ ;
+ //Can't use following, because data from CLDR is discarded
+ // ICUResourceBundle suppData = LocaleMatcher.getICUSupplementalData();
+ // UResourceBundle territoryContainment = suppData.get("territoryContainment");
+ // for (int i = 0 ; i < territoryContainment.getSize(); i++) {
+ // UResourceBundle mapping = territoryContainment.get(i);
+ // String parent = mapping.getKey();
+ // for (int j = 0 ; j < mapping.getSize(); j++) {
+ // String child = mapping.getString(j);
+ // containment.put(parent,child);
+ // System.out.println(parent + " => " + child);
+ // }
+ // }
+ TreeMultimap<String,String> containmentResolved = TreeMultimap.create();
+ fill("001", containment, containmentResolved);
+ return ImmutableMultimap.copyOf(containmentResolved);
+ }
+
+ private static Set<String> fill(String region, TreeMultimap<String, String> containment, Multimap<String, String> toAddTo) {
+ Set<String> contained = containment.get(region);
+ if (contained == null) {
+ return Collections.emptySet();
+ }
+ toAddTo.putAll(region, contained); // do top level
+ // then recursively
+ for (String subregion : contained) {
+ toAddTo.putAll(region, fill(subregion, containment, toAddTo));
+ }
+ return toAddTo.get(region);
+ }
+
+
+ static final Multimap<String,String> CONTAINER_TO_CONTAINED;
+ static final Multimap<String,String> CONTAINER_TO_CONTAINED_FINAL;
+ static {
+ // Multimap<String, String> containerToContainedTemp = xGetContainment();
+ // fill(Region.getInstance("001"), containerToContainedTemp);
+
+ CONTAINER_TO_CONTAINED = xGetContainment();
+ Multimap<String, String> containerToFinalContainedBuilder = TreeMultimap.create();
+ for (Entry<String, Set<String>> entry : CONTAINER_TO_CONTAINED.asMap().entrySet()) {
+ String container = entry.getKey();
+ for (String contained : entry.getValue()) {
+ if (CONTAINER_TO_CONTAINED.get(contained) == null) {
+ containerToFinalContainedBuilder.put(container, contained);
+ }
+ }
+ }
+ CONTAINER_TO_CONTAINED_FINAL = ImmutableMultimap.copyOf(containerToFinalContainedBuilder);
+ }
+
+ final static private Set<String> ALL_FINAL_REGIONS = ImmutableSet.copyOf(CONTAINER_TO_CONTAINED_FINAL.get("001"));
+
+ // end of data from CLDR
+
+ private final DistanceTable languageDesired2Supported;
+ private final RegionMapper regionMapper;
+ private final int defaultLanguageDistance;
+ private final int defaultScriptDistance;
+ private final int defaultRegionDistance;
+
+ @Deprecated
+ public static abstract class DistanceTable {
+ abstract int getDistance(String desiredLang, String supportedlang, Output<DistanceTable> table, boolean starEquals);
+ abstract Set<String> getCloser(int threshold);
+ abstract String toString(boolean abbreviate);
+ public DistanceTable compact() {
+ return this;
+ }
+ // public Integer getInternalDistance(String a, String b) {
+ // return null;
+ // }
+ public DistanceNode getInternalNode(String any, String any2) {
+ return null;
+ }
+ public Map<String, Set<String>> getInternalMatches() {
+ return null;
+ }
+ public boolean isEmpty() {
+ return true;
+ }
+ }
+
+ @Deprecated
+ public static class DistanceNode {
+ final int distance;
+
+ public DistanceNode(int distance) {
+ this.distance = distance;
+ }
+
+ public DistanceTable getDistanceTable() {
+ return null;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return this == obj ||
+ (obj != null
+ && obj.getClass() == this.getClass()
+ && distance == ((DistanceNode) obj).distance);
+ }
+ @Override
+ public int hashCode() {
+ return distance;
+ }
+ @Override
+ public String toString() {
+ return "\ndistance: " + distance;
+ }
+ }
+
+ private interface IdMapper<K,V> {
+ public V toId(K source);
+ }
+
+ static class IdMakerFull<T> implements IdMapper<T,Integer> {
+ private final Map<T, Integer> objectToInt = new HashMap<T, Integer>();
+ private final List<T> intToObject = new ArrayList<T>();
+ final String name; // for debugging
+
+ IdMakerFull(String name) {
+ this.name = name;
+ }
+
+ IdMakerFull() {
+ this("unnamed");
+ }
+
+ IdMakerFull(String name, T zeroValue) {
+ this(name);
+ add(zeroValue);
+ }
+
+ /**
+ * Return an id, making one if there wasn't one already.
+ */
+ public Integer add(T source) {
+ Integer result = objectToInt.get(source);
+ if (result == null) {
+ Integer newResult = intToObject.size();
+ objectToInt.put(source, newResult);
+ intToObject.add(source);
+ return newResult;
+ } else {
+ return result;
+ }
+ }
+
+ /**
+ * Return an id, or null if there is none.
+ */
+ @Override
+ public Integer toId(T source) {
+ return objectToInt.get(source);
+ // return value == null ? 0 : value;
+ }
+
+ /**
+ * Return the object for the id, or null if there is none.
+ */
+ public T fromId(int id) {
+ return intToObject.get(id);
+ }
+
+ /**
+ * Return interned object
+ */
+ public T intern(T source) {
+ return fromId(add(source));
+ }
+
+ public int size() {
+ return intToObject.size();
+ }
+ /**
+ * Same as add, except if the object didn't have an id, return null;
+ */
+ public Integer getOldAndAdd(T source) {
+ Integer result = objectToInt.get(source);
+ if (result == null) {
+ Integer newResult = intToObject.size();
+ objectToInt.put(source, newResult);
+ intToObject.add(source);
+ }
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return size() + ": " + intToObject;
+ }
+ @Override
+ public boolean equals(Object obj) {
+ return this == obj ||
+ (obj != null
+ && obj.getClass() == this.getClass()
+ && intToObject.equals(((IdMakerFull<?>) obj).intToObject));
+ }
+ @Override
+ public int hashCode() {
+ return intToObject.hashCode();
+ }
+ }
+
+ static class StringDistanceNode extends DistanceNode {
+ final DistanceTable distanceTable;
+
+ public StringDistanceNode(int distance, DistanceTable distanceTable) {
+ super(distance);
+ this.distanceTable = distanceTable;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ StringDistanceNode other;
+ return this == obj ||
+ (obj != null
+ && obj.getClass() == this.getClass()
+ && distance == (other = (StringDistanceNode) obj).distance
+ && Utility.equals(distanceTable, other.distanceTable)
+ && super.equals(other));
+ }
+ @Override
+ public int hashCode() {
+ return distance ^ Utility.hashCode(distanceTable);
+ }
+
+ StringDistanceNode(int distance) {
+ this(distance, new StringDistanceTable());
+ }
+
+ public void addSubtables(String desiredSub, String supportedSub, CopyIfEmpty r) {
+ ((StringDistanceTable) distanceTable).addSubtables(desiredSub, supportedSub, r);
+ }
+ @Override
+ public String toString() {
+ return "distance: " + distance + "\n" + distanceTable;
+ }
+
+ public void copyTables(StringDistanceTable value) {
+ if (value != null) {
+ ((StringDistanceTable)distanceTable).copy(value);
+ }
+ }
+
+ @Override
+ public DistanceTable getDistanceTable() {
+ return distanceTable;
+ }
+ }
+
+ public XLocaleDistance(DistanceTable datadistancetable2, RegionMapper regionMapper) {
+ languageDesired2Supported = datadistancetable2;
+ this.regionMapper = regionMapper;
+
+ StringDistanceNode languageNode = (StringDistanceNode) ((StringDistanceTable) languageDesired2Supported).subtables.get(ANY).get(ANY);
+ defaultLanguageDistance = languageNode.distance;
+ StringDistanceNode scriptNode = (StringDistanceNode) ((StringDistanceTable)languageNode.distanceTable).subtables.get(ANY).get(ANY);
+ defaultScriptDistance = scriptNode.distance;
+ DistanceNode regionNode = ((StringDistanceTable)scriptNode.distanceTable).subtables.get(ANY).get(ANY);
+ defaultRegionDistance = regionNode.distance;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static Map newMap() { // for debugging
+ return new TreeMap();
+ }
+
+ /**
+ * Internal class
+ */
+ @Deprecated
+ public static class StringDistanceTable extends DistanceTable {
+ final Map<String, Map<String, DistanceNode>> subtables;
+
+ StringDistanceTable(Map<String, Map<String, DistanceNode>> tables) {
+ subtables = tables;
+ }
+ @SuppressWarnings("unchecked")
+ StringDistanceTable() {
+ this(newMap());
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return subtables.isEmpty();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return this == obj ||
+ (obj != null
+ && obj.getClass() == this.getClass()
+ && subtables.equals(((StringDistanceTable) obj).subtables));
+ }
+ @Override
+ public int hashCode() {
+ return subtables.hashCode();
+ }
+
+ @Override
+ public int getDistance(String desired, String supported, Output<DistanceTable> distanceTable, boolean starEquals) {
+ boolean star = false;
+ Map<String, DistanceNode> sub2 = subtables.get(desired);
+ if (sub2 == null) {
+ sub2 = subtables.get(ANY); // <*, supported>
+ star = true;
+ }
+ DistanceNode value = sub2.get(supported); // <*/desired, supported>
+ if (value == null) {
+ value = sub2.get(ANY); // <*/desired, *>
+ if (value == null && !star) {
+ sub2 = subtables.get(ANY); // <*, supported>
+ value = sub2.get(supported);
+ if (value == null) {
+ value = sub2.get(ANY); // <*, *>
+ }
+ }
+ star = true;
+ }
+ if (distanceTable != null) {
+ distanceTable.value = ((StringDistanceNode) value).distanceTable;
+ }
+ return starEquals && star && desired.equals(supported) ? 0 : value.distance;
+ }
+
+ public void copy(StringDistanceTable other) {
+ for (Entry<String, Map<String, DistanceNode>> e1 : other.subtables.entrySet()) {
+ for (Entry<String, DistanceNode> e2 : e1.getValue().entrySet()) {
+ DistanceNode value = e2.getValue();
+ @SuppressWarnings("unused")
+ DistanceNode subNode = addSubtable(e1.getKey(), e2.getKey(), value.distance);
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ DistanceNode addSubtable(String desired, String supported, int distance) {
+ Map<String, DistanceNode> sub2 = subtables.get(desired);
+ if (sub2 == null) {
+ subtables.put(desired, sub2 = newMap());
+ }
+ DistanceNode oldNode = sub2.get(supported);
+ if (oldNode != null) {
+ return oldNode;
+ }
+
+ final StringDistanceNode newNode = new StringDistanceNode(distance);
+ sub2.put(supported, newNode);
+ return newNode;
+ }
+
+ /**
+ * Return null if value doesn't exist
+ */
+ private DistanceNode getNode(String desired, String supported) {
+ Map<String, DistanceNode> sub2 = subtables.get(desired);
+ if (sub2 == null) {
+ return null;
+ }
+ return sub2.get(supported);
+ }
+
+
+ /** add table for each subitem that matches and doesn't have a table already
+ */
+ public void addSubtables(
+ String desired, String supported,
+ Predicate<DistanceNode> action) {
+ DistanceNode node = getNode(desired, supported);
+ if (node == null) {
+ // get the distance it would have
+ Output<DistanceTable> node2 = new Output<DistanceTable>();
+ int distance = getDistance(desired, supported, node2, true);
+ // now add it
+ node = addSubtable(desired, supported, distance);
+ if (node2.value != null) {
+ ((StringDistanceNode)node).copyTables((StringDistanceTable)(node2.value));
+ }
+ }
+ action.test(node);
+ }
+
+ public void addSubtables(String desiredLang, String supportedLang,
+ String desiredScript, String supportedScript,
+ int percentage) {
+
+ // add to all the values that have the matching desiredLang and supportedLang
+ @SuppressWarnings("unused")
+ boolean haveKeys = false;
+ for (Entry<String, Map<String, DistanceNode>> e1 : subtables.entrySet()) {
+ String key1 = e1.getKey();
+ final boolean desiredIsKey = desiredLang.equals(key1);
+ if (desiredIsKey || desiredLang.equals(ANY)) {
+ for (Entry<String, DistanceNode> e2 : e1.getValue().entrySet()) {
+ String key2 = e2.getKey();
+ final boolean supportedIsKey = supportedLang.equals(key2);
+ haveKeys |= (desiredIsKey && supportedIsKey);
+ if (supportedIsKey || supportedLang.equals(ANY)) {
+ DistanceNode value = e2.getValue();
+ ((StringDistanceTable)value.getDistanceTable()).addSubtable(desiredScript, supportedScript, percentage);
+ }
+ }
+ }
+ }
+ // now add the sequence explicitly
+ StringDistanceTable dt = new StringDistanceTable();
+ dt.addSubtable(desiredScript, supportedScript, percentage);
+ CopyIfEmpty r = new CopyIfEmpty(dt);
+ addSubtables(desiredLang, supportedLang, r);
+ }
+
+ public void addSubtables(String desiredLang, String supportedLang,
+ String desiredScript, String supportedScript,
+ String desiredRegion, String supportedRegion,
+ int percentage) {
+
+ // add to all the values that have the matching desiredLang and supportedLang
+ @SuppressWarnings("unused")
+ boolean haveKeys = false;
+ for (Entry<String, Map<String, DistanceNode>> e1 : subtables.entrySet()) {
+ String key1 = e1.getKey();
+ final boolean desiredIsKey = desiredLang.equals(key1);
+ if (desiredIsKey || desiredLang.equals(ANY)) {
+ for (Entry<String, DistanceNode> e2 : e1.getValue().entrySet()) {
+ String key2 = e2.getKey();
+ final boolean supportedIsKey = supportedLang.equals(key2);
+ haveKeys |= (desiredIsKey && supportedIsKey);
+ if (supportedIsKey || supportedLang.equals(ANY)) {
+ StringDistanceNode value = (StringDistanceNode) e2.getValue();
+ ((StringDistanceTable)value.distanceTable).addSubtables(desiredScript, supportedScript, desiredRegion, supportedRegion, percentage);
+ }
+ }
+ }
+ }
+ // now add the sequence explicitly
+
+ StringDistanceTable dt = new StringDistanceTable();
+ dt.addSubtable(desiredRegion, supportedRegion, percentage);
+ AddSub r = new AddSub(desiredScript, supportedScript, dt);
+ addSubtables(desiredLang, supportedLang, r);
+ }
+
+ @Override
+ public String toString() {
+ return toString(false);
+ }
+
+ @Override
+ public String toString(boolean abbreviate) {
+ return toString(abbreviate, "", new IdMakerFull<Object>("interner"), new StringBuilder()).toString();
+ }
+
+ public StringBuilder toString(boolean abbreviate, String indent, IdMakerFull<Object> intern, StringBuilder buffer) {
+ String indent2 = indent.isEmpty() ? "" : "\t";
+ Integer id = abbreviate ? intern.getOldAndAdd(subtables) : null;
+ if (id != null) {
+ buffer.append(indent2).append('#').append(id).append('\n');
+ } else for (Entry<String, Map<String, DistanceNode>> e1 : subtables.entrySet()) {
+ final Map<String, DistanceNode> subsubtable = e1.getValue();
+ buffer.append(indent2).append(e1.getKey());
+ String indent3 = "\t";
+ id = abbreviate ? intern.getOldAndAdd(subsubtable) : null;
+ if (id != null) {
+ buffer.append(indent3).append('#').append(id).append('\n');
+ } else for (Entry<String, DistanceNode> e2 : subsubtable.entrySet()) {
+ DistanceNode value = e2.getValue();
+ buffer.append(indent3).append(e2.getKey());
+ id = abbreviate ? intern.getOldAndAdd(value) : null;
+ if (id != null) {
+ buffer.append('\t').append('#').append(id).append('\n');
+ } else {
+ buffer.append('\t').append(value.distance);
+ final DistanceTable distanceTable = value.getDistanceTable();
+ if (distanceTable != null) {
+ id = abbreviate ? intern.getOldAndAdd(distanceTable) : null;
+ if (id != null) {
+ buffer.append('\t').append('#').append(id).append('\n');
+ } else {
+ ((StringDistanceTable)distanceTable).toString(abbreviate, indent+"\t\t\t", intern, buffer);
+ }
+ } else {
+ buffer.append('\n');
+ }
+ }
+ indent3 = indent+'\t';
+ }
+ indent2 = indent;
+ }
+ return buffer;
+ }
+
+ @Override
+ public StringDistanceTable compact() {
+ return new CompactAndImmutablizer().compact(this);
+ }
+
+ @Override
+ public Set<String> getCloser(int threshold) {
+ Set<String> result = new HashSet<String>();
+ for (Entry<String, Map<String, DistanceNode>> e1 : subtables.entrySet()) {
+ String desired = e1.getKey();
+ for (Entry<String, DistanceNode> e2 : e1.getValue().entrySet()) {
+ if (e2.getValue().distance < threshold) {
+ result.add(desired);
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ public Integer getInternalDistance(String a, String b) {
+ Map<String, DistanceNode> subsub = subtables.get(a);
+ if (subsub == null) {
+ return null;
+ }
+ DistanceNode dnode = subsub.get(b);
+ return dnode == null ? null : dnode.distance;
+ }
+
+ @Override
+ public DistanceNode getInternalNode(String a, String b) {
+ Map<String, DistanceNode> subsub = subtables.get(a);
+ if (subsub == null) {
+ return null;
+ }
+ return subsub.get(b);
+ }
+
+ @Override
+ public Map<String, Set<String>> getInternalMatches() {
+ Map<String, Set<String>> result = new LinkedHashMap<String, Set<String>>();
+ for (Entry<String, Map<String, DistanceNode>> entry : subtables.entrySet()) {
+ result.put(entry.getKey(), new LinkedHashSet<String>(entry.getValue().keySet()));
+ }
+ return result;
+ }
+ }
+
+ static class CopyIfEmpty implements Predicate<DistanceNode> {
+ private final StringDistanceTable toCopy;
+ CopyIfEmpty(StringDistanceTable resetIfNotNull) {
+ this.toCopy = resetIfNotNull;
+ }
+ @Override
+ public boolean test(DistanceNode node) {
+ final StringDistanceTable subtables = (StringDistanceTable) node.getDistanceTable();
+ if (subtables.subtables.isEmpty()) {
+ subtables.copy(toCopy);
+ }
+ return true;
+ }
+ }
+
+ static class AddSub implements Predicate<DistanceNode> {
+ private final String desiredSub;
+ private final String supportedSub;
+ private final CopyIfEmpty r;
+
+ AddSub(String desiredSub, String supportedSub, StringDistanceTable distanceTableToCopy) {
+ this.r = new CopyIfEmpty(distanceTableToCopy);
+ this.desiredSub = desiredSub;
+ this.supportedSub = supportedSub;
+ }
+ @Override
+ public boolean test(DistanceNode node) {
+ if (node == null) {
+ throw new IllegalArgumentException("bad structure");
+ } else {
+ ((StringDistanceNode)node).addSubtables(desiredSub, supportedSub, r);
+ }
+ return true;
+ }
+ }
+
+ public int distance(ULocale desired, ULocale supported, int threshold, DistanceOption distanceOption) {
+ LSR supportedLSR = LSR.fromMaximalized(supported);
+ LSR desiredLSR = LSR.fromMaximalized(desired);
+ return distanceRaw(desiredLSR, supportedLSR, threshold, distanceOption);
+ }
+
+ /**
+ * Returns distance, from 0 to ABOVE_THRESHOLD.
+ * ULocales must be in canonical, addLikelySubtags format. Returns distance
+ */
+ public int distanceRaw(LSR desired, LSR supported, int threshold, DistanceOption distanceOption) {
+ return distanceRaw(desired.language, supported.language,
+ desired.script, supported.script,
+ desired.region, supported.region,
+ threshold, distanceOption);
+ }
+
+ public enum DistanceOption {NORMAL, SCRIPT_FIRST}
+
+ /**
+ * Returns distance, from 0 to ABOVE_THRESHOLD.
+ * ULocales must be in canonical, addLikelySubtags format. Returns distance
+ */
+ public int distanceRaw(
+ String desiredLang, String supportedlang,
+ String desiredScript, String supportedScript,
+ String desiredRegion, String supportedRegion,
+ int threshold,
+ DistanceOption distanceOption) {
+
+ Output<DistanceTable> subtable = new Output<DistanceTable>();
+
+ int distance = languageDesired2Supported.getDistance(desiredLang, supportedlang, subtable, true);
+ boolean scriptFirst = distanceOption == DistanceOption.SCRIPT_FIRST;
+ if (scriptFirst) {
+ distance >>= 2;
+ }
+ if (distance < 0) {
+ distance = 0;
+ } else if (distance >= threshold) {
+ return ABOVE_THRESHOLD;
+ }
+
+ int scriptDistance = subtable.value.getDistance(desiredScript, supportedScript, subtable, true);
+ if (scriptFirst) {
+ scriptDistance >>= 1;
+ }
+ distance += scriptDistance;
+ if (distance >= threshold) {
+ return ABOVE_THRESHOLD;
+ }
+
+ if (desiredRegion.equals(supportedRegion)) {
+ return distance;
+ }
+
+ // From here on we know the regions are not equal
+
+ final String desiredPartition = regionMapper.toId(desiredRegion);
+ final String supportedPartition = regionMapper.toId(supportedRegion);
+ int subdistance;
+
+ // check for macros. If one is found, we take the maximum distance
+ // this could be optimized by adding some more structure, but probably not worth it.
+
+ Collection<String> desiredPartitions = desiredPartition.isEmpty() ? regionMapper.macroToPartitions.get(desiredRegion) : null;
+ Collection<String> supportedPartitions = supportedPartition.isEmpty() ? regionMapper.macroToPartitions.get(supportedRegion) : null;
+ if (desiredPartitions != null || supportedPartitions != null) {
+ subdistance = 0;
+ // make the code simple for now
+ if (desiredPartitions == null) {
+ desiredPartitions = Collections.singleton(desiredPartition);
+ }
+ if (supportedPartitions == null) {
+ supportedPartitions = Collections.singleton(supportedPartition);
+ }
+
+ for (String desiredPartition2 : desiredPartitions) {
+ for (String supportedPartition2 : supportedPartitions) {
+ int tempSubdistance = subtable.value.getDistance(desiredPartition2, supportedPartition2, null, false);
+ if (subdistance < tempSubdistance) {
+ subdistance = tempSubdistance;
+ }
+ }
+ }
+ } else {
+ subdistance = subtable.value.getDistance(desiredPartition, supportedPartition, null, false);
+ }
+ distance += subdistance;
+ return distance >= threshold ? ABOVE_THRESHOLD : distance;
+ }
+
+
+ private static final XLocaleDistance DEFAULT;
+
+ public static XLocaleDistance getDefault() {
+ return DEFAULT;
+ }
+
+ static {
+ String[][] variableOverrides = {
+ {"$enUS", "AS+GU+MH+MP+PR+UM+US+VI"},
+
+ {"$cnsar", "HK+MO"},
+
+ {"$americas", "019"},
+
+ {"$maghreb", "MA+DZ+TN+LY+MR+EH"},
+ };
+ String[] paradigmRegions = {
+ "en", "en-GB", "es", "es-419", "pt-BR", "pt-PT"
+ };
+ String[][] regionRuleOverrides = {
+ {"ar_*_$maghreb", "ar_*_$maghreb", "96"},
+ {"ar_*_$!maghreb", "ar_*_$!maghreb", "96"},
+ {"ar_*_*", "ar_*_*", "95"},
+
+ {"en_*_$enUS", "en_*_$enUS", "96"},
+ {"en_*_$!enUS", "en_*_$!enUS", "96"},
+ {"en_*_*", "en_*_*", "95"},
+
+ {"es_*_$americas", "es_*_$americas", "96"},
+ {"es_*_$!americas", "es_*_$!americas", "96"},
+ {"es_*_*", "es_*_*", "95"},
+
+ {"pt_*_$americas", "pt_*_$americas", "96"},
+ {"pt_*_$!americas", "pt_*_$!americas", "96"},
+ {"pt_*_*", "pt_*_*", "95"},
+
+ {"zh_Hant_$cnsar", "zh_Hant_$cnsar", "96"},
+ {"zh_Hant_$!cnsar", "zh_Hant_$!cnsar", "96"},
+ {"zh_Hant_*", "zh_Hant_*", "95"},
+
+ {"*_*_*", "*_*_*", "96"},
+ };
+
+ Builder rmb = new RegionMapper.Builder().addParadigms(paradigmRegions);
+ for (String[] variableRule : variableOverrides) {
+ rmb.add(variableRule[0], variableRule[1]);
+ }
+ if (PRINT_OVERRIDES) {
+ System.out.println("\t\t<languageMatches type=\"written\" alt=\"enhanced\">");
+ System.out.println("\t\t\t<paradigmLocales locales=\"" + XCldrStub.join(paradigmRegions, " ")
+ + "\"/>");
+ for (String[] variableRule : variableOverrides) {
+ System.out.println("\t\t\t<matchVariable id=\"" + variableRule[0]
+ + "\" value=\""
+ + variableRule[1]
+ + "\"/>");
+ }
+ }
+
+ final StringDistanceTable defaultDistanceTable = new StringDistanceTable();
+ final RegionMapper defaultRegionMapper = rmb.build();
+
+ Splitter bar = Splitter.on('_');
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ List<Row.R4<List<String>, List<String>, Integer, Boolean>>[] sorted = new ArrayList[3];
+ sorted[0] = new ArrayList<Row.R4<List<String>, List<String>, Integer, Boolean>>();
+ sorted[1] = new ArrayList<Row.R4<List<String>, List<String>, Integer, Boolean>>();
+ sorted[2] = new ArrayList<Row.R4<List<String>, List<String>, Integer, Boolean>>();
+
+ // sort the rules so that the language-only are first, then the language-script, and finally the language-script-region.
+ for (R4<String, String, Integer, Boolean> info : xGetLanguageMatcherData()) {
+ String desiredRaw = info.get0();
+ String supportedRaw = info.get1();
+ List<String> desired = bar.splitToList(desiredRaw);
+ List<String> supported = bar.splitToList(supportedRaw);
+ Boolean oneway = info.get3();
+ int distance = desiredRaw.equals("*_*") ? 50 : info.get2();
+ int size = desired.size();
+
+ // for now, skip size == 3
+ if (size == 3) continue;
+
+ sorted[size-1].add(Row.of(desired, supported, distance, oneway));
+ }
+
+ for (List<Row.R4<List<String>, List<String>, Integer, Boolean>> item1 : sorted) {
+ for (Row.R4<List<String>, List<String>, Integer, Boolean> item2 : item1) {
+ List<String> desired = item2.get0();
+ List<String> supported = item2.get1();
+ Integer distance = item2.get2();
+ Boolean oneway = item2.get3();
+ add(defaultDistanceTable, desired, supported, distance);
+ if (oneway != Boolean.TRUE && !desired.equals(supported)) {
+ add(defaultDistanceTable, supported, desired, distance);
+ }
+ printMatchXml(desired, supported, distance, oneway);
+ }
+ }
+
+ // add new size=3
+ for (String[] rule : regionRuleOverrides) {
+ // if (PRINT_OVERRIDES) System.out.println("\t\t\t<languageMatch desired=\""
+ // + rule[0]
+ // + "\" supported=\""
+ // + rule[1]
+ // + "\" distance=\""
+ // + rule[2]
+ // + "\"/>");
+ // if (rule[0].equals("en_*_*") || rule[1].equals("*_*_*")) {
+ // int debug = 0;
+ // }
+ List<String> desiredBase = new ArrayList<String>(bar.splitToList(rule[0]));
+ List<String> supportedBase = new ArrayList<String>(bar.splitToList(rule[1]));
+ Integer distance = 100-Integer.parseInt(rule[2]);
+ printMatchXml(desiredBase, supportedBase, distance, false);
+
+ Collection<String> desiredRegions = defaultRegionMapper.getIdsFromVariable(desiredBase.get(2));
+ if (desiredRegions.isEmpty()) {
+ throw new IllegalArgumentException("Bad region variable: " + desiredBase.get(2));
+ }
+ Collection<String> supportedRegions = defaultRegionMapper.getIdsFromVariable(supportedBase.get(2));
+ if (supportedRegions.isEmpty()) {
+ throw new IllegalArgumentException("Bad region variable: " + supportedBase.get(2));
+ }
+ for (String desiredRegion2 : desiredRegions) {
+ desiredBase.set(2, desiredRegion2.toString()); // fix later
+ for (String supportedRegion2 : supportedRegions) {
+ supportedBase.set(2, supportedRegion2.toString()); // fix later
+ add(defaultDistanceTable, desiredBase, supportedBase, distance);
+ add(defaultDistanceTable, supportedBase, desiredBase, distance);
+ }
+ }
+ }
+ if (PRINT_OVERRIDES) {
+ System.out.println("\t\t</languageMatches>");
+ }
+
+ DEFAULT = new XLocaleDistance(defaultDistanceTable.compact(), defaultRegionMapper);
+
+ if (PRINT_OVERRIDES) {
+ System.out.println(defaultRegionMapper);
+ System.out.println(defaultDistanceTable);
+ throw new IllegalArgumentException();
+ }
+ }
+
+ private static void printMatchXml(List<String> desired, List<String> supported, Integer distance, Boolean oneway) {
+ if (PRINT_OVERRIDES) {
+ String desiredStr = CollectionUtilities.join(desired, "_");
+ String supportedStr = CollectionUtilities.join(supported, "_");
+ String desiredName = fixedName(desired);
+ String supportedName = fixedName(supported);
+ System.out.println("\t\t\t<languageMatch"
+ + " desired=\"" + desiredStr
+ + "\"\tsupported=\"" + supportedStr
+ + "\"\tdistance=\"" + distance
+ + (!oneway ? "" : "\"\toneway=\"true")
+ + "\"/>\t<!-- " + desiredName + " ⇒ " + supportedName + " -->");
+ }
+ }
+
+ private static String fixedName(List<String> match) {
+ List<String> alt = new ArrayList<String>(match);
+ int size = alt.size();
+ assert size >= 1 && size <= 3;
+
+ StringBuilder result = new StringBuilder();
+
+ if (size >= 3) {
+ String region = alt.get(2);
+ if (region.equals("*") || region.startsWith("$")) {
+ result.append(region);
+ } else {
+ result.append(english.regionDisplayName(region));
+ }
+ }
+ if (size >= 2) {
+ String script = alt.get(1);
+ if (script.equals("*")) {
+ result.insert(0, script);
+ } else {
+ result.insert(0, english.scriptDisplayName(script));
+ }
+ }
+ if (size >= 1) {
+ String language = alt.get(0);
+ if (language.equals("*")) {
+ result.insert(0, language);
+ } else {
+ result.insert(0, english.languageDisplayName(language));
+ }
+ }
+ return CollectionUtilities.join(alt, "; ");
+ }
+
+ static public void add(StringDistanceTable languageDesired2Supported, List<String> desired, List<String> supported, int percentage) {
+ int size = desired.size();
+ if (size != supported.size() || size < 1 || size > 3) {
+ throw new IllegalArgumentException();
+ }
+ final String desiredLang = fixAny(desired.get(0));
+ final String supportedLang = fixAny(supported.get(0));
+ if (size == 1) {
+ languageDesired2Supported.addSubtable(desiredLang, supportedLang, percentage);
+ } else {
+ final String desiredScript = fixAny(desired.get(1));
+ final String supportedScript = fixAny(supported.get(1));
+ if (size == 2) {
+ languageDesired2Supported.addSubtables(desiredLang, supportedLang, desiredScript, supportedScript, percentage);
+ } else {
+ final String desiredRegion = fixAny(desired.get(2));
+ final String supportedRegion = fixAny(supported.get(2));
+ languageDesired2Supported.addSubtables(desiredLang, supportedLang, desiredScript, supportedScript, desiredRegion, supportedRegion, percentage);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return toString(false);
+ }
+
+ public String toString(boolean abbreviate) {
+ return regionMapper + "\n" + languageDesired2Supported.toString(abbreviate);
+ }
+
+
+ // public static XLocaleDistance createDefaultInt() {
+ // IntDistanceTable d = new IntDistanceTable(DEFAULT_DISTANCE_TABLE);
+ // return new XLocaleDistance(d, DEFAULT_REGION_MAPPER);
+ // }
+
+ static Set<String> getContainingMacrosFor(Collection<String> input, Set<String> output) {
+ output.clear();
+ for (Entry<String, Set<String>> entry : CONTAINER_TO_CONTAINED.asMap().entrySet()) {
+ if (input.containsAll(entry.getValue())) { // example; if all southern Europe are contained, then add S. Europe
+ output.add(entry.getKey());
+ }
+ }
+ return output;
+ }
+
+ static class RegionMapper implements IdMapper<String,String> {
+ /**
+ * Used for processing rules. At the start we have a variable setting like $A1=US+CA+MX. We generate a mapping from $A1 to a set of partitions {P1, P2}
+ * When we hit a rule that contains a variable, we replace that rule by multiple rules for the partitions.
+ */
+ final Multimap<String,String> variableToPartition;
+ /**
+ * Used for executing the rules. We map a region to a partition before processing.
+ */
+ final Map<String,String> regionToPartition;
+ /**
+ * Used to support es_419 compared to es_AR, etc.
+ */
+ final Multimap<String,String> macroToPartitions;
+ /**
+ * Used to get the paradigm region for a cluster, if there is one
+ */
+ final Set<ULocale> paradigms;
+
+ private RegionMapper(
+ Multimap<String, String> variableToPartitionIn,
+ Map<String, String> regionToPartitionIn,
+ Multimap<String,String> macroToPartitionsIn,
+ Set<ULocale> paradigmsIn) {
+ variableToPartition = ImmutableMultimap.copyOf(variableToPartitionIn);
+ regionToPartition = ImmutableMap.copyOf(regionToPartitionIn);
+ macroToPartitions = ImmutableMultimap.copyOf(macroToPartitionsIn);
+ paradigms = ImmutableSet.copyOf(paradigmsIn);
+ }
+
+ @Override
+ public String toId(String region) {
+ String result = regionToPartition.get(region);
+ return result == null ? "" : result;
+ }
+
+ public Collection<String> getIdsFromVariable(String variable) {
+ if (variable.equals("*")) {
+ return Collections.singleton("*");
+ }
+ Collection<String> result = variableToPartition.get(variable);
+ if (result == null || result.isEmpty()) {
+ throw new IllegalArgumentException("Variable not defined: " + variable);
+ }
+ return result;
+ }
+
+ public Set<String> regions() {
+ return regionToPartition.keySet();
+ }
+
+ public Set<String> variables() {
+ return variableToPartition.keySet();
+ }
+
+ @Override
+ public String toString() {
+ TreeMultimap<String, String> partitionToVariables = Multimaps.invertFrom(variableToPartition,
+ TreeMultimap.<String, String>create());
+ TreeMultimap<String, String> partitionToRegions = TreeMultimap.create();
+ for (Entry<String, String> e : regionToPartition.entrySet()) {
+ partitionToRegions.put(e.getValue(), e.getKey());
+ }
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("Partition ➠ Variables ➠ Regions (final)");
+ for (Entry<String, Set<String>> e : partitionToVariables.asMap().entrySet()) {
+ buffer.append('\n');
+ buffer.append(e.getKey() + "\t" + e.getValue() + "\t" + partitionToRegions.get(e.getKey()));
+ }
+ buffer.append("\nMacro ➠ Partitions");
+ for (Entry<String, Set<String>> e : macroToPartitions.asMap().entrySet()) {
+ buffer.append('\n');
+ buffer.append(e.getKey() + "\t" + e.getValue());
+ }
+
+ return buffer.toString();
+ }
+
+ static class Builder {
+ final private Multimap<String, String> regionToRawPartition = TreeMultimap.create();
+ final private RegionSet regionSet = new RegionSet();
+ final private Set<ULocale> paradigms = new LinkedHashSet<ULocale>();
+
+ void add(String variable, String barString) {
+ Set<String> tempRegions = regionSet.parseSet(barString);
+
+ for (String region : tempRegions) {
+ regionToRawPartition.put(region, variable);
+ }
+
+ // now add the inverse variable
+
+ Set<String> inverse = regionSet.inverse();
+ String inverseVariable = "$!" + variable.substring(1);
+ for (String region : inverse) {
+ regionToRawPartition.put(region, inverseVariable);
+ }
+ }
+
+ public Builder addParadigms(String... paradigmRegions) {
+ for (String paradigm : paradigmRegions) {
+ paradigms.add(new ULocale(paradigm));
+ }
+ return this;
+ }
+
+ RegionMapper build() {
+ final IdMakerFull<Collection<String>> id = new IdMakerFull<Collection<String>>("partition");
+ Multimap<String,String> variableToPartitions = TreeMultimap.create();
+ Map<String,String> regionToPartition = new TreeMap<String,String>();
+ Multimap<String,String> partitionToRegions = TreeMultimap.create();
+
+ for (Entry<String, Set<String>> e : regionToRawPartition.asMap().entrySet()) {
+ final String region = e.getKey();
+ final Collection<String> rawPartition = e.getValue();
+ String partition = String.valueOf((char)('α' + id.add(rawPartition)));
+
+ regionToPartition.put(region, partition);
+ partitionToRegions.put(partition, region);
+
+ for (String variable : rawPartition) {
+ variableToPartitions.put(variable, partition);
+ }
+ }
+
+ // we get a mapping of each macro to the partitions it intersects with
+ Multimap<String,String> macroToPartitions = TreeMultimap.create();
+ for (Entry<String, Set<String>> e : CONTAINER_TO_CONTAINED.asMap().entrySet()) {
+ String macro = e.getKey();
+ for (Entry<String, Set<String>> e2 : partitionToRegions.asMap().entrySet()) {
+ String partition = e2.getKey();
+ if (!Collections.disjoint(e.getValue(), e2.getValue())) {
+ macroToPartitions.put(macro, partition);
+ }
+ }
+ }
+
+ return new RegionMapper(
+ variableToPartitions,
+ regionToPartition,
+ macroToPartitions,
+ paradigms);
+ }
+ }
+ }
+
+ /**
+ * Parses a string of regions like "US+005-BR" and produces a set of resolved regions.
+ * All macroregions are fully resolved to sets of non-macro regions.
+ * <br>Syntax is simple for now:
+ * <pre>regionSet := region ([-+] region)*</pre>
+ * No precedence, so "x+y-y+z" is (((x+y)-y)+z) NOT (x+y)-(y+z)
+ */
+ private static class RegionSet {
+ private enum Operation {add, remove}
+ // temporaries used in processing
+ final private Set<String> tempRegions = new TreeSet<String>();
+ private Operation operation = null;
+
+ private Set<String> parseSet(String barString) {
+ operation = Operation.add;
+ int last = 0;
+ tempRegions.clear();
+ int i = 0;
+ for (; i < barString.length(); ++i) {
+ char c = barString.charAt(i); // UTF16 is ok, since syntax is only ascii
+ switch(c) {
+ case '+':
+ add(barString, last, i);
+ last = i+1;
+ operation = Operation.add;
+ break;
+ case '-':
+ add(barString, last, i);
+ last = i+1;
+ operation = Operation.remove;
+ break;
+ }
+ }
+ add(barString, last, i);
+ return tempRegions;
+ }
+
+ private Set<String> inverse() {
+ TreeSet<String> result = new TreeSet<String>(ALL_FINAL_REGIONS);
+ result.removeAll(tempRegions);
+ return result;
+ }
+
+ private void add(String barString, int last, int i) {
+ if (i > last) {
+ String region = barString.substring(last,i);
+ changeSet(operation, region);
+ }
+ }
+
+ private void changeSet(Operation operation, String region) {
+ Collection<String> contained = CONTAINER_TO_CONTAINED_FINAL.get(region);
+ if (contained != null && !contained.isEmpty()) {
+ if (Operation.add == operation) {
+ tempRegions.addAll(contained);
+ } else {
+ tempRegions.removeAll(contained);
+ }
+ } else if (Operation.add == operation) {
+ tempRegions.add(region);
+ } else {
+ tempRegions.remove(region);
+ }
+ }
+ }
+
+ public static <K,V> Multimap<K,V> invertMap(Map<V,K> map) {
+ return Multimaps.invertFrom(Multimaps.forMap(map), LinkedHashMultimap.<K,V>create());
+ }
+
+ public Set<ULocale> getParadigms() {
+ return regionMapper.paradigms;
+ }
+
+ public int getDefaultLanguageDistance() {
+ return defaultLanguageDistance;
+ }
+
+ public int getDefaultScriptDistance() {
+ return defaultScriptDistance;
+ }
+
+ public int getDefaultRegionDistance() {
+ return defaultRegionDistance;
+ }
+
+ static class CompactAndImmutablizer extends IdMakerFull<Object> {
+ StringDistanceTable compact(StringDistanceTable item) {
+ if (toId(item) != null) {
+ return (StringDistanceTable) intern(item);
+ }
+ return new StringDistanceTable(compact(item.subtables, 0));
+ }
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ <K,T> Map<K,T> compact(Map<K,T> item, int level) {
+ if (toId(item) != null) {
+ return (Map<K, T>) intern(item);
+ }
+ Map<K,T> copy = new LinkedHashMap<K,T>();
+ for (Entry<K,T> entry : item.entrySet()) {
+ T value = entry.getValue();
+ if (value instanceof Map) {
+ copy.put(entry.getKey(), (T)compact((Map)value, level+1));
+ } else {
+ copy.put(entry.getKey(), (T)compact((DistanceNode)value));
+ }
+ }
+ return ImmutableMap.copyOf(copy);
+ }
+ DistanceNode compact(DistanceNode item) {
+ if (toId(item) != null) {
+ return (DistanceNode) intern(item);
+ }
+ final DistanceTable distanceTable = item.getDistanceTable();
+ if (distanceTable == null || distanceTable.isEmpty()) {
+ return new DistanceNode(item.distance);
+ } else {
+ return new StringDistanceNode(item.distance, compact((StringDistanceTable)((StringDistanceNode)item).distanceTable));
+ }
+ }
+ }
+
+ @Deprecated
+ public StringDistanceTable internalGetDistanceTable() {
+ return (StringDistanceTable) languageDesired2Supported;
+ }
+
+ public static void main(String[] args) {
+ // for (Entry<String, Collection<String>> entry : containerToContained.asMap().entrySet()) {
+ // System.out.println(entry.getKey() + "\t⥢" + entry.getValue() + "; " + containerToFinalContained.get(entry.getKey()));
+ // }
+ // final Multimap<String,String> regionToMacros = ImmutableMultimap.copyOf(Multimaps.invertFrom(containerToContained, TreeMultimap.create()));
+ // for (Entry<String, Collection<String>> entry : regionToMacros.asMap().entrySet()) {
+ // System.out.println(entry.getKey() + "\t⥤ " + entry.getValue());
+ // }
+ if (PRINT_OVERRIDES) {
+ System.out.println(getDefault().toString(true));
+ }
+ DistanceTable table = getDefault().languageDesired2Supported;
+ DistanceTable compactedTable = table.compact();
+ if (!table.equals(compactedTable)) {
+ throw new IllegalArgumentException("Compaction isn't equal");
+ }
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/XLocaleMatcher.java b/android_icu4j/src/main/java/android/icu/impl/locale/XLocaleMatcher.java
new file mode 100644
index 000000000..a5e142183
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/locale/XLocaleMatcher.java
@@ -0,0 +1,474 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.locale;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import android.icu.impl.locale.XCldrStub.ImmutableMultimap;
+import android.icu.impl.locale.XCldrStub.ImmutableSet;
+import android.icu.impl.locale.XCldrStub.LinkedHashMultimap;
+import android.icu.impl.locale.XCldrStub.Multimap;
+import android.icu.impl.locale.XLikelySubtags.LSR;
+import android.icu.impl.locale.XLocaleDistance.DistanceOption;
+import android.icu.util.LocalePriorityList;
+import android.icu.util.Output;
+import android.icu.util.ULocale;
+
+/**
+ * Immutable class that picks best match between user's desired locales and application's supported locales.
+ * @author markdavis
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class XLocaleMatcher {
+ private static final LSR UND = new LSR("und","","");
+ private static final ULocale UND_LOCALE = new ULocale("und");
+
+ // normally the default values, but can be set via constructor
+
+ private final XLocaleDistance localeDistance;
+ private final int thresholdDistance;
+ private final int demotionPerAdditionalDesiredLocale;
+ private final DistanceOption distanceOption;
+
+ // built based on application's supported languages in constructor
+
+ private final Map<LSR, Set<ULocale>> supportedLanguages; // the locales in the collection are ordered!
+ private final Set<ULocale> exactSupportedLocales; // the locales in the collection are ordered!
+ private final ULocale defaultLanguage;
+
+
+ public static class Builder {
+ private Set<ULocale> supportedLanguagesList;
+ private int thresholdDistance = -1;
+ private int demotionPerAdditionalDesiredLocale = -1;;
+ private ULocale defaultLanguage;
+ private XLocaleDistance localeDistance;
+ private DistanceOption distanceOption;
+ /**
+ * @param languagePriorityList the languagePriorityList to set
+ * @return this Builder object
+ */
+ public Builder setSupportedLocales(String languagePriorityList) {
+ this.supportedLanguagesList = asSet(LocalePriorityList.add(languagePriorityList).build());
+ return this;
+ }
+ public Builder setSupportedLocales(LocalePriorityList languagePriorityList) {
+ this.supportedLanguagesList = asSet(languagePriorityList);
+ return this;
+ }
+ public Builder setSupportedLocales(Set<ULocale> languagePriorityList) {
+ this.supportedLanguagesList = languagePriorityList;
+ return this;
+ }
+
+ /**
+ * @param thresholdDistance the thresholdDistance to set, with -1 = default
+ * @return this Builder object
+ */
+ public Builder setThresholdDistance(int thresholdDistance) {
+ this.thresholdDistance = thresholdDistance;
+ return this;
+ }
+ /**
+ * @param demotionPerAdditionalDesiredLocale the demotionPerAdditionalDesiredLocale to set, with -1 = default
+ * @return this Builder object
+ */
+ public Builder setDemotionPerAdditionalDesiredLocale(int demotionPerAdditionalDesiredLocale) {
+ this.demotionPerAdditionalDesiredLocale = demotionPerAdditionalDesiredLocale;
+ return this;
+ }
+
+ /**
+ * @param localeDistance the localeDistance to set, with default = XLocaleDistance.getDefault().
+ * @return this Builder object
+ */
+ public Builder setLocaleDistance(XLocaleDistance localeDistance) {
+ this.localeDistance = localeDistance;
+ return this;
+ }
+
+ /**
+ * Set the default language, with null = default = first supported language
+ * @param defaultLanguage the default language
+ * @return this Builder object
+ */
+ public Builder setDefaultLanguage(ULocale defaultLanguage) {
+ this.defaultLanguage = defaultLanguage;
+ return this;
+ }
+
+ /**
+ * If true, then the language differences are smaller than than script differences.
+ * This is used in situations (such as maps) where it is better to fall back to the same script than a similar language.
+ * @param distanceOption the distance option
+ * @return this Builder object
+ */
+ public Builder setDistanceOption(DistanceOption distanceOption) {
+ this.distanceOption = distanceOption;
+ return this;
+ }
+
+ public XLocaleMatcher build() {
+ return new XLocaleMatcher(this);
+ }
+ }
+
+ /**
+ * Returns a builder used in chaining parameters for building a Locale Matcher.
+ * @return this Builder object
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Convenience method */
+ public XLocaleMatcher(String supportedLocales) {
+ this(builder().setSupportedLocales(supportedLocales));
+ }
+ /** Convenience method */
+ public XLocaleMatcher(LocalePriorityList supportedLocales) {
+ this(builder().setSupportedLocales(supportedLocales));
+ }
+ /** Convenience method */
+ public XLocaleMatcher(Set<ULocale> supportedLocales) {
+ this(builder().setSupportedLocales(supportedLocales));
+ }
+
+ /**
+ * Create a locale matcher with the given parameters.
+ * @param supportedLocales
+ * @param thresholdDistance
+ * @param demotionPerAdditionalDesiredLocale
+ * @param localeDistance
+ * @param likelySubtags
+ */
+ private XLocaleMatcher(Builder builder) {
+ localeDistance = builder.localeDistance == null ? XLocaleDistance.getDefault()
+ : builder.localeDistance;
+ thresholdDistance = builder.thresholdDistance < 0 ? localeDistance.getDefaultScriptDistance()
+ : builder.thresholdDistance;
+ // only do AFTER above are set
+ Set<LSR> paradigms = extractLsrSet(localeDistance.getParadigms());
+ final Multimap<LSR, ULocale> temp2 = extractLsrMap(builder.supportedLanguagesList, paradigms);
+ supportedLanguages = temp2.asMap();
+ exactSupportedLocales = ImmutableSet.copyOf(temp2.values());
+ defaultLanguage = builder.defaultLanguage != null ? builder.defaultLanguage
+ : supportedLanguages.isEmpty() ? null
+ : supportedLanguages.entrySet().iterator().next().getValue().iterator().next(); // first language
+ demotionPerAdditionalDesiredLocale = builder.demotionPerAdditionalDesiredLocale < 0 ? localeDistance.getDefaultRegionDistance()+1
+ : builder.demotionPerAdditionalDesiredLocale;
+ distanceOption = builder.distanceOption;
+ }
+
+ // Result is not immutable!
+ private Set<LSR> extractLsrSet(Set<ULocale> languagePriorityList) {
+ Set<LSR> result = new LinkedHashSet<LSR>();
+ for (ULocale item : languagePriorityList) {
+ final LSR max = item.equals(UND_LOCALE) ? UND : LSR.fromMaximalized(item);
+ result.add(max);
+ }
+ return result;
+ }
+
+ private Multimap<LSR,ULocale> extractLsrMap(Set<ULocale> languagePriorityList, Set<LSR> priorities) {
+ Multimap<LSR, ULocale> builder = LinkedHashMultimap.create();
+ for (ULocale item : languagePriorityList) {
+ final LSR max = item.equals(UND_LOCALE) ? UND : LSR.fromMaximalized(item);
+ builder.put(max, item);
+ }
+ if (builder.size() > 1 && priorities != null) {
+ // for the supported list, we put any priorities before all others, except for the first.
+ Multimap<LSR, ULocale> builder2 = LinkedHashMultimap.create();
+
+ // copy the long way so the priorities are in the same order as in the original
+ boolean first = true;
+ for (Entry<LSR, Set<ULocale>> entry : builder.asMap().entrySet()) {
+ final LSR key = entry.getKey();
+ if (first || priorities.contains(key)) {
+ builder2.putAll(key, entry.getValue());
+ first = false;
+ }
+ }
+ // now copy the rest
+ builder2.putAll(builder);
+ if (!builder2.equals(builder)) {
+ throw new IllegalArgumentException();
+ }
+ builder = builder2;
+ }
+ return ImmutableMultimap.copyOf(builder);
+ }
+
+
+ /** Convenience method */
+ public ULocale getBestMatch(ULocale ulocale) {
+ return getBestMatch(ulocale, null);
+ }
+ /** Convenience method */
+ public ULocale getBestMatch(String languageList) {
+ return getBestMatch(LocalePriorityList.add(languageList).build(), null);
+ }
+ /** Convenience method */
+ public ULocale getBestMatch(ULocale... locales) {
+ return getBestMatch(new LinkedHashSet<ULocale>(Arrays.asList(locales)), null);
+ }
+ /** Convenience method */
+ public ULocale getBestMatch(Set<ULocale> desiredLanguages) {
+ return getBestMatch(desiredLanguages, null);
+ }
+ /** Convenience method */
+ public ULocale getBestMatch(LocalePriorityList desiredLanguages) {
+ return getBestMatch(desiredLanguages, null);
+ }
+ /** Convenience method */
+ public ULocale getBestMatch(LocalePriorityList desiredLanguages, Output<ULocale> outputBestDesired) {
+ return getBestMatch(asSet(desiredLanguages), outputBestDesired);
+ }
+
+ // TODO add LocalePriorityList method asSet() for ordered Set view backed by LocalePriorityList
+ private static Set<ULocale> asSet(LocalePriorityList languageList) {
+ Set<ULocale> temp = new LinkedHashSet<ULocale>(); // maintain order
+ for (ULocale locale : languageList) {
+ temp.add(locale);
+ };
+ return temp;
+ }
+
+ /**
+ * Get the best match between the desired languages and supported languages
+ * @param desiredLanguages Typically the supplied user's languages, in order of preference, with best first.
+ * @param outputBestDesired The one of the desired languages that matched best.
+ * Set to null if the best match was not below the threshold distance.
+ * @return the best match.
+ */
+ public ULocale getBestMatch(Set<ULocale> desiredLanguages, Output<ULocale> outputBestDesired) {
+ // fast path for singleton
+ if (desiredLanguages.size() == 1) {
+ return getBestMatch(desiredLanguages.iterator().next(), outputBestDesired);
+ }
+ // TODO produce optimized version for single desired ULocale
+ Multimap<LSR, ULocale> desiredLSRs = extractLsrMap(desiredLanguages,null);
+ int bestDistance = Integer.MAX_VALUE;
+ ULocale bestDesiredLocale = null;
+ Collection<ULocale> bestSupportedLocales = null;
+ int delta = 0;
+ mainLoop:
+ for (final Entry<LSR, ULocale> desiredLsrAndLocale : desiredLSRs.entries()) {
+ // quick check for exact match
+ ULocale desiredLocale = desiredLsrAndLocale.getValue();
+ LSR desiredLSR = desiredLsrAndLocale.getKey();
+ if (delta < bestDistance) {
+ if (exactSupportedLocales.contains(desiredLocale)) {
+ if (outputBestDesired != null) {
+ outputBestDesired.value = desiredLocale;
+ }
+ return desiredLocale;
+ }
+ // quick check for maximized locale
+ Collection<ULocale> found = supportedLanguages.get(desiredLSR);
+ if (found != null) {
+ // if we find one in the set, return first (lowest). We already know the exact one isn't there.
+ if (outputBestDesired != null) {
+ outputBestDesired.value = desiredLocale;
+ }
+ return found.iterator().next();
+ }
+ }
+ for (final Entry<LSR, Set<ULocale>> supportedLsrAndLocale : supportedLanguages.entrySet()) {
+ int distance = delta + localeDistance.distanceRaw(desiredLSR, supportedLsrAndLocale.getKey(),
+ thresholdDistance, distanceOption);
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestDesiredLocale = desiredLocale;
+ bestSupportedLocales = supportedLsrAndLocale.getValue();
+ if (distance == 0) {
+ break mainLoop;
+ }
+ }
+ }
+ delta += demotionPerAdditionalDesiredLocale;
+ }
+ if (bestDistance >= thresholdDistance) {
+ if (outputBestDesired != null) {
+ outputBestDesired.value = null;
+ }
+ return defaultLanguage;
+ }
+ if (outputBestDesired != null) {
+ outputBestDesired.value = bestDesiredLocale;
+ }
+ // pick exact match if there is one
+ if (bestSupportedLocales.contains(bestDesiredLocale)) {
+ return bestDesiredLocale;
+ }
+ // otherwise return first supported, combining variants and extensions from bestDesired
+ return bestSupportedLocales.iterator().next();
+ }
+
+ /**
+ * Get the best match between the desired languages and supported languages
+ * @param desiredLocale the supplied user's language.
+ * @param outputBestDesired The one of the desired languages that matched best.
+ * Set to null if the best match was not below the threshold distance.
+ * @return the best match.
+ */
+ public ULocale getBestMatch(ULocale desiredLocale, Output<ULocale> outputBestDesired) {
+ int bestDistance = Integer.MAX_VALUE;
+ ULocale bestDesiredLocale = null;
+ Collection<ULocale> bestSupportedLocales = null;
+
+ // quick check for exact match, with hack for und
+ final LSR desiredLSR = desiredLocale.equals(UND_LOCALE) ? UND : LSR.fromMaximalized(desiredLocale);
+
+ if (exactSupportedLocales.contains(desiredLocale)) {
+ if (outputBestDesired != null) {
+ outputBestDesired.value = desiredLocale;
+ }
+ return desiredLocale;
+ }
+ // quick check for maximized locale
+ if (distanceOption == DistanceOption.NORMAL) {
+ Collection<ULocale> found = supportedLanguages.get(desiredLSR);
+ if (found != null) {
+ // if we find one in the set, return first (lowest). We already know the exact one isn't there.
+ if (outputBestDesired != null) {
+ outputBestDesired.value = desiredLocale;
+ }
+ return found.iterator().next();
+ }
+ }
+ for (final Entry<LSR, Set<ULocale>> supportedLsrAndLocale : supportedLanguages.entrySet()) {
+ int distance = localeDistance.distanceRaw(desiredLSR, supportedLsrAndLocale.getKey(),
+ thresholdDistance, distanceOption);
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestDesiredLocale = desiredLocale;
+ bestSupportedLocales = supportedLsrAndLocale.getValue();
+ if (distance == 0) {
+ break;
+ }
+ }
+ }
+ if (bestDistance >= thresholdDistance) {
+ if (outputBestDesired != null) {
+ outputBestDesired.value = null;
+ }
+ return defaultLanguage;
+ }
+ if (outputBestDesired != null) {
+ outputBestDesired.value = bestDesiredLocale;
+ }
+ // pick exact match if there is one
+ if (bestSupportedLocales.contains(bestDesiredLocale)) {
+ return bestDesiredLocale;
+ }
+ // otherwise return first supported, combining variants and extensions from bestDesired
+ return bestSupportedLocales.iterator().next();
+ }
+
+ /** Combine features of the desired locale into those of the supported, and return result. */
+ public static ULocale combine(ULocale bestSupported, ULocale bestDesired) {
+ // for examples of extensions, variants, see
+ // http://unicode.org/repos/cldr/tags/latest/common/bcp47/
+ // http://unicode.org/repos/cldr/tags/latest/common/validity/variant.xml
+
+ if (!bestSupported.equals(bestDesired) && bestDesired != null) {
+ // add region, variants, extensions
+ ULocale.Builder b = new ULocale.Builder().setLocale(bestSupported);
+
+ // copy the region from the desired, if there is one
+ String region = bestDesired.getCountry();
+ if (!region.isEmpty()) {
+ b.setRegion(region);
+ }
+
+ // copy the variants from desired, if there is one
+ // note that this will override any subvariants. Eg "sco-ulster-fonipa" + "…-fonupa" => "sco-fonupa" (nuking ulster)
+ String variants = bestDesired.getVariant();
+ if (!variants.isEmpty()) {
+ b.setVariant(variants);
+ }
+
+ // copy the extensions from desired, if there are any
+ // note that this will override any subkeys. Eg "th-u-nu-latn-ca-buddhist" + "…-u-nu-native" => "th-u-nu-native" (nuking calendar)
+ for (char extensionKey : bestDesired.getExtensionKeys()) {
+ b.setExtension(extensionKey, bestDesired.getExtension(extensionKey));
+ }
+ bestSupported = b.build();
+ }
+ return bestSupported;
+ }
+
+ /** Returns the distance between the two languages. The values are not necessarily symmetric.
+ * @param desired A locale desired by the user
+ * @param supported A locale supported by a program.
+ * @return A return of 0 is a complete match, and 100 is a failure case (above the thresholdDistance).
+ * A language is first maximized with add likely subtags, then compared.
+ */
+ public int distance(ULocale desired, ULocale supported) {
+ return localeDistance.distanceRaw(
+ LSR.fromMaximalized(desired),
+ LSR.fromMaximalized(supported), thresholdDistance, distanceOption);
+ }
+
+ /** Convenience method */
+ public int distance(String desiredLanguage, String supportedLanguage) {
+ return localeDistance.distanceRaw(
+ LSR.fromMaximalized(new ULocale(desiredLanguage)),
+ LSR.fromMaximalized(new ULocale(supportedLanguage)),
+ thresholdDistance, distanceOption);
+ }
+
+ @Override
+ public String toString() {
+ return exactSupportedLocales.toString();
+ }
+
+ /** Return the inverse of the distance: that is, 1-distance(desired, supported) */
+ public double match(ULocale desired, ULocale supported) {
+ return (100-distance(desired, supported))/100.0;
+ }
+
+ /**
+ * Returns a fraction between 0 and 1, where 1 means that the languages are a
+ * perfect match, and 0 means that they are completely different. This is (100-distance(desired, supported))/100.0.
+ * <br>Note that
+ * the precise values may change over time; no code should be made dependent
+ * on the values remaining constant.
+ * @param desired Desired locale
+ * @param desiredMax Maximized locale (using likely subtags)
+ * @param supported Supported locale
+ * @param supportedMax Maximized locale (using likely subtags)
+ * @return value between 0 and 1, inclusive.
+ * @deprecated Use the form with 2 parameters instead.
+ */
+ @Deprecated
+ public double match(ULocale desired, ULocale desiredMax, ULocale supported, ULocale supportedMax) {
+ return match(desired, supported);
+ }
+
+ /**
+ * Canonicalize a locale (language). Note that for now, it is canonicalizing
+ * according to CLDR conventions (he vs iw, etc), since that is what is needed
+ * for likelySubtags.
+ * @param ulocale language/locale code
+ * @return ULocale with remapped subtags.
+ */
+ public ULocale canonicalize(ULocale ulocale) {
+ // TODO
+ return null;
+ }
+
+ /**
+ * @return the thresholdDistance. Any distance above this value is treated as a match failure.
+ */
+ public int getThresholdDistance() {
+ return thresholdDistance;
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/AffixPatternUtils.java b/android_icu4j/src/main/java/android/icu/impl/number/AffixPatternUtils.java
new file mode 100644
index 000000000..27a450e3b
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/AffixPatternUtils.java
@@ -0,0 +1,552 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import android.icu.text.DecimalFormatSymbols;
+import android.icu.text.NumberFormat.Field;
+
+/**
+ * Performs manipulations on affix patterns: the prefix and suffix strings associated with a decimal
+ * format pattern. For example:
+ *
+ * <table>
+ * <tr><th>Affix Pattern</th><th>Example Unescaped (Formatted) String</th></tr>
+ * <tr><td>abc</td><td>abc</td></tr>
+ * <tr><td>ab-</td><td>ab−</td></tr>
+ * <tr><td>ab'-'</td><td>ab-</td></tr>
+ * <tr><td>ab''</td><td>ab'</td></tr>
+ * </table>
+ *
+ * To manually iterate over tokens in a literal string, use the following pattern, which is designed
+ * to be efficient.
+ *
+ * <pre>
+ * long tag = 0L;
+ * while (AffixPatternUtils.hasNext(tag, patternString)) {
+ * tag = AffixPatternUtils.nextToken(tag, patternString);
+ * int typeOrCp = AffixPatternUtils.getTypeOrCp(tag);
+ * switch (typeOrCp) {
+ * case AffixPatternUtils.TYPE_MINUS_SIGN:
+ * // Current token is a minus sign.
+ * break;
+ * case AffixPatternUtils.TYPE_PLUS_SIGN:
+ * // Current token is a plus sign.
+ * break;
+ * case AffixPatternUtils.TYPE_PERCENT:
+ * // Current token is a percent sign.
+ * break;
+ * case AffixPatternUtils.TYPE_PERMILLE:
+ * // Current token is a permille sign.
+ * break;
+ * case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
+ * // Current token is a single currency sign.
+ * break;
+ * case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
+ * // Current token is a double currency sign.
+ * break;
+ * case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
+ * // Current token is a triple currency sign.
+ * break;
+ * case AffixPatternUtils.TYPE_CURRENCY_OVERFLOW:
+ * // Current token has four or more currency signs.
+ * break;
+ * default:
+ * // Current token is an arbitrary code point.
+ * // The variable typeOrCp is the code point.
+ * break;
+ * }
+ * }
+ * </pre>
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class AffixPatternUtils {
+
+ private static final int STATE_BASE = 0;
+ private static final int STATE_FIRST_QUOTE = 1;
+ private static final int STATE_INSIDE_QUOTE = 2;
+ private static final int STATE_AFTER_QUOTE = 3;
+ private static final int STATE_FIRST_CURR = 4;
+ private static final int STATE_SECOND_CURR = 5;
+ private static final int STATE_THIRD_CURR = 6;
+ private static final int STATE_OVERFLOW_CURR = 7;
+
+ private static final int TYPE_CODEPOINT = 0;
+
+ /** Represents a minus sign symbol '-'. */
+ public static final int TYPE_MINUS_SIGN = -1;
+
+ /** Represents a plus sign symbol '+'. */
+ public static final int TYPE_PLUS_SIGN = -2;
+
+ /** Represents a percent sign symbol '%'. */
+ public static final int TYPE_PERCENT = -3;
+
+ /** Represents a permille sign symbol '‰'. */
+ public static final int TYPE_PERMILLE = -4;
+
+ /** Represents a single currency symbol '¤'. */
+ public static final int TYPE_CURRENCY_SINGLE = -5;
+
+ /** Represents a double currency symbol '¤¤'. */
+ public static final int TYPE_CURRENCY_DOUBLE = -6;
+
+ /** Represents a triple currency symbol '¤¤¤'. */
+ public static final int TYPE_CURRENCY_TRIPLE = -7;
+
+ /** Represents a sequence of four or more currency symbols. */
+ public static final int TYPE_CURRENCY_OVERFLOW = -15;
+
+ /**
+ * Estimates the number of code points present in an unescaped version of the affix pattern string
+ * (one that would be returned by {@link #unescape}), assuming that all interpolated symbols
+ * consume one code point and that currencies consume as many code points as their symbol width.
+ * Used for computing padding width.
+ *
+ * @param patternString The original string whose width will be estimated.
+ * @return The length of the unescaped string.
+ */
+ public static int unescapedLength(CharSequence patternString) {
+ if (patternString == null) return 0;
+ int state = STATE_BASE;
+ int offset = 0;
+ int length = 0;
+ for (; offset < patternString.length(); ) {
+ int cp = Character.codePointAt(patternString, offset);
+
+ switch (state) {
+ case STATE_BASE:
+ if (cp == '\'') {
+ // First quote
+ state = STATE_FIRST_QUOTE;
+ } else {
+ // Unquoted symbol
+ length++;
+ }
+ break;
+ case STATE_FIRST_QUOTE:
+ if (cp == '\'') {
+ // Repeated quote
+ length++;
+ state = STATE_BASE;
+ } else {
+ // Quoted code point
+ length++;
+ state = STATE_INSIDE_QUOTE;
+ }
+ break;
+ case STATE_INSIDE_QUOTE:
+ if (cp == '\'') {
+ // End of quoted sequence
+ state = STATE_AFTER_QUOTE;
+ } else {
+ // Quoted code point
+ length++;
+ }
+ break;
+ case STATE_AFTER_QUOTE:
+ if (cp == '\'') {
+ // Double quote inside of quoted sequence
+ length++;
+ state = STATE_INSIDE_QUOTE;
+ } else {
+ // Unquoted symbol
+ length++;
+ }
+ break;
+ default:
+ throw new AssertionError();
+ }
+
+ offset += Character.charCount(cp);
+ }
+
+ switch (state) {
+ case STATE_FIRST_QUOTE:
+ case STATE_INSIDE_QUOTE:
+ throw new IllegalArgumentException("Unterminated quote: \"" + patternString + "\"");
+ default:
+ break;
+ }
+
+ return length;
+ }
+
+ /**
+ * Takes a string and escapes (quotes) characters that have special meaning in the affix pattern
+ * syntax. This function does not reverse-lookup symbols.
+ *
+ * <p>Example input: "-$x"; example output: "'-'$x"
+ *
+ * @param input The string to be escaped.
+ * @param output The string builder to which to append the escaped string.
+ * @return The number of chars (UTF-16 code units) appended to the output.
+ */
+ public static int escape(CharSequence input, StringBuilder output) {
+ if (input == null) return 0;
+ int state = STATE_BASE;
+ int offset = 0;
+ int startLength = output.length();
+ for (; offset < input.length(); ) {
+ int cp = Character.codePointAt(input, offset);
+
+ switch (cp) {
+ case '\'':
+ output.append("''");
+ break;
+
+ case '-':
+ case '+':
+ case '%':
+ case '‰':
+ case '¤':
+ if (state == STATE_BASE) {
+ output.append('\'');
+ output.appendCodePoint(cp);
+ state = STATE_INSIDE_QUOTE;
+ } else {
+ output.appendCodePoint(cp);
+ }
+ break;
+
+ default:
+ if (state == STATE_INSIDE_QUOTE) {
+ output.append('\'');
+ output.appendCodePoint(cp);
+ state = STATE_BASE;
+ } else {
+ output.appendCodePoint(cp);
+ }
+ break;
+ }
+ offset += Character.charCount(cp);
+ }
+
+ if (state == STATE_INSIDE_QUOTE) {
+ output.append('\'');
+ }
+
+ return output.length() - startLength;
+ }
+
+ /**
+ * Executes the unescape state machine. Replaces the unquoted characters "-", "+", "%", and "‰"
+ * with their localized equivalents. Replaces "¤", "¤¤", and "¤¤¤" with the three argument
+ * strings.
+ *
+ * <p>Example input: "'-'¤x"; example output: "-$x"
+ *
+ * @param affixPattern The original string to be unescaped.
+ * @param symbols An instance of {@link DecimalFormatSymbols} for the locale of interest.
+ * @param currency1 The string to replace "¤".
+ * @param currency2 The string to replace "¤¤".
+ * @param currency3 The string to replace "¤¤¤".
+ * @param minusSign The string to replace "-". If null, symbols.getMinusSignString() is used.
+ * @param output The {@link NumberStringBuilder} to which the result will be appended.
+ */
+ public static void unescape(
+ CharSequence affixPattern,
+ DecimalFormatSymbols symbols,
+ String currency1,
+ String currency2,
+ String currency3,
+ String minusSign,
+ NumberStringBuilder output) {
+ if (affixPattern == null || affixPattern.length() == 0) return;
+ if (minusSign == null) minusSign = symbols.getMinusSignString();
+ long tag = 0L;
+ while (hasNext(tag, affixPattern)) {
+ tag = nextToken(tag, affixPattern);
+ int typeOrCp = getTypeOrCp(tag);
+ switch (typeOrCp) {
+ case TYPE_MINUS_SIGN:
+ output.append(minusSign, Field.SIGN);
+ break;
+ case TYPE_PLUS_SIGN:
+ output.append(symbols.getPlusSignString(), Field.SIGN);
+ break;
+ case TYPE_PERCENT:
+ output.append(symbols.getPercentString(), Field.PERCENT);
+ break;
+ case TYPE_PERMILLE:
+ output.append(symbols.getPerMillString(), Field.PERMILLE);
+ break;
+ case TYPE_CURRENCY_SINGLE:
+ output.append(currency1, Field.CURRENCY);
+ break;
+ case TYPE_CURRENCY_DOUBLE:
+ output.append(currency2, Field.CURRENCY);
+ break;
+ case TYPE_CURRENCY_TRIPLE:
+ output.append(currency3, Field.CURRENCY);
+ break;
+ case TYPE_CURRENCY_OVERFLOW:
+ output.append("\uFFFD", Field.CURRENCY);
+ break;
+ default:
+ output.appendCodePoint(typeOrCp, null);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Checks whether the given affix pattern contains at least one token of the given type, which is
+ * one of the constants "TYPE_" in {@link AffixPatternUtils}.
+ *
+ * @param affixPattern The affix pattern to check.
+ * @param type The token type.
+ * @return true if the affix pattern contains the given token type; false otherwise.
+ */
+ public static boolean containsType(CharSequence affixPattern, int type) {
+ if (affixPattern == null || affixPattern.length() == 0) return false;
+ long tag = 0L;
+ while (hasNext(tag, affixPattern)) {
+ tag = nextToken(tag, affixPattern);
+ if (getTypeOrCp(tag) == type) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks whether the specified affix pattern has any unquoted currency symbols ("¤").
+ *
+ * @param affixPattern The string to check for currency symbols.
+ * @return true if the literal has at least one unquoted currency symbol; false otherwise.
+ */
+ public static boolean hasCurrencySymbols(CharSequence affixPattern) {
+ if (affixPattern == null || affixPattern.length() == 0) return false;
+ long tag = 0L;
+ while (hasNext(tag, affixPattern)) {
+ tag = nextToken(tag, affixPattern);
+ int typeOrCp = getTypeOrCp(tag);
+ if (typeOrCp == AffixPatternUtils.TYPE_CURRENCY_SINGLE
+ || typeOrCp == AffixPatternUtils.TYPE_CURRENCY_DOUBLE
+ || typeOrCp == AffixPatternUtils.TYPE_CURRENCY_TRIPLE
+ || typeOrCp == AffixPatternUtils.TYPE_CURRENCY_OVERFLOW) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Replaces all occurrences of tokens with the given type with the given replacement char.
+ *
+ * @param affixPattern The source affix pattern (does not get modified).
+ * @param type The token type.
+ * @param replacementChar The char to substitute in place of chars of the given token type.
+ * @return A string containing the new affix pattern.
+ */
+ public static String replaceType(CharSequence affixPattern, int type, char replacementChar) {
+ if (affixPattern == null || affixPattern.length() == 0) return "";
+ char[] chars = affixPattern.toString().toCharArray();
+ long tag = 0L;
+ while (hasNext(tag, affixPattern)) {
+ tag = nextToken(tag, affixPattern);
+ if (getTypeOrCp(tag) == type) {
+ int offset = getOffset(tag);
+ chars[offset - 1] = replacementChar;
+ }
+ }
+ return new String(chars);
+ }
+
+ /**
+ * Returns the next token from the affix pattern.
+ *
+ * @param tag A bitmask used for keeping track of state from token to token. The initial value
+ * should be 0L.
+ * @param patternString The affix pattern.
+ * @return The bitmask tag to pass to the next call of this method to retrieve the following token
+ * (never negative), or -1 if there were no more tokens in the affix pattern.
+ * @see #hasNext
+ */
+ public static long nextToken(long tag, CharSequence patternString) {
+ int offset = getOffset(tag);
+ int state = getState(tag);
+ for (; offset < patternString.length(); ) {
+ int cp = Character.codePointAt(patternString, offset);
+ int count = Character.charCount(cp);
+
+ switch (state) {
+ case STATE_BASE:
+ switch (cp) {
+ case '\'':
+ state = STATE_FIRST_QUOTE;
+ offset += count;
+ // continue to the next code point
+ break;
+ case '-':
+ return makeTag(offset + count, TYPE_MINUS_SIGN, STATE_BASE, 0);
+ case '+':
+ return makeTag(offset + count, TYPE_PLUS_SIGN, STATE_BASE, 0);
+ case '%':
+ return makeTag(offset + count, TYPE_PERCENT, STATE_BASE, 0);
+ case '‰':
+ return makeTag(offset + count, TYPE_PERMILLE, STATE_BASE, 0);
+ case '¤':
+ state = STATE_FIRST_CURR;
+ offset += count;
+ // continue to the next code point
+ break;
+ default:
+ return makeTag(offset + count, TYPE_CODEPOINT, STATE_BASE, cp);
+ }
+ break;
+ case STATE_FIRST_QUOTE:
+ if (cp == '\'') {
+ return makeTag(offset + count, TYPE_CODEPOINT, STATE_BASE, cp);
+ } else {
+ return makeTag(offset + count, TYPE_CODEPOINT, STATE_INSIDE_QUOTE, cp);
+ }
+ case STATE_INSIDE_QUOTE:
+ if (cp == '\'') {
+ state = STATE_AFTER_QUOTE;
+ offset += count;
+ // continue to the next code point
+ break;
+ } else {
+ return makeTag(offset + count, TYPE_CODEPOINT, STATE_INSIDE_QUOTE, cp);
+ }
+ case STATE_AFTER_QUOTE:
+ if (cp == '\'') {
+ return makeTag(offset + count, TYPE_CODEPOINT, STATE_INSIDE_QUOTE, cp);
+ } else {
+ state = STATE_BASE;
+ // re-evaluate this code point
+ break;
+ }
+ case STATE_FIRST_CURR:
+ if (cp == '¤') {
+ state = STATE_SECOND_CURR;
+ offset += count;
+ // continue to the next code point
+ break;
+ } else {
+ return makeTag(offset, TYPE_CURRENCY_SINGLE, STATE_BASE, 0);
+ }
+ case STATE_SECOND_CURR:
+ if (cp == '¤') {
+ state = STATE_THIRD_CURR;
+ offset += count;
+ // continue to the next code point
+ break;
+ } else {
+ return makeTag(offset, TYPE_CURRENCY_DOUBLE, STATE_BASE, 0);
+ }
+ case STATE_THIRD_CURR:
+ if (cp == '¤') {
+ state = STATE_OVERFLOW_CURR;
+ offset += count;
+ // continue to the next code point
+ break;
+ } else {
+ return makeTag(offset, TYPE_CURRENCY_TRIPLE, STATE_BASE, 0);
+ }
+ case STATE_OVERFLOW_CURR:
+ if (cp == '¤') {
+ offset += count;
+ // continue to the next code point and loop back to this state
+ break;
+ } else {
+ return makeTag(offset, TYPE_CURRENCY_OVERFLOW, STATE_BASE, 0);
+ }
+ default:
+ throw new AssertionError();
+ }
+ }
+ // End of string
+ switch (state) {
+ case STATE_BASE:
+ // No more tokens in string.
+ return -1L;
+ case STATE_FIRST_QUOTE:
+ case STATE_INSIDE_QUOTE:
+ // For consistent behavior with the JDK and ICU 58, throw an exception here.
+ throw new IllegalArgumentException(
+ "Unterminated quote in pattern affix: \"" + patternString + "\"");
+ case STATE_AFTER_QUOTE:
+ // No more tokens in string.
+ return -1L;
+ case STATE_FIRST_CURR:
+ return makeTag(offset, TYPE_CURRENCY_SINGLE, STATE_BASE, 0);
+ case STATE_SECOND_CURR:
+ return makeTag(offset, TYPE_CURRENCY_DOUBLE, STATE_BASE, 0);
+ case STATE_THIRD_CURR:
+ return makeTag(offset, TYPE_CURRENCY_TRIPLE, STATE_BASE, 0);
+ case STATE_OVERFLOW_CURR:
+ return makeTag(offset, TYPE_CURRENCY_OVERFLOW, STATE_BASE, 0);
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Returns whether the affix pattern string has any more tokens to be retrieved from a call to
+ * {@link #nextToken}.
+ *
+ * @param tag The bitmask tag of the previous token, as returned by {@link #nextToken}.
+ * @param string The affix pattern.
+ * @return true if there are more tokens to consume; false otherwise.
+ */
+ public static boolean hasNext(long tag, CharSequence string) {
+ assert tag >= 0;
+ int state = getState(tag);
+ int offset = getOffset(tag);
+ // Special case: the last character in string is an end quote.
+ if (state == STATE_INSIDE_QUOTE
+ && offset == string.length() - 1
+ && string.charAt(offset) == '\'') {
+ return false;
+ } else if (state != STATE_BASE) {
+ return true;
+ } else {
+ return offset < string.length();
+ }
+ }
+
+ /**
+ * This function helps determine the identity of the token consumed by {@link #nextToken}.
+ * Converts from a bitmask tag, based on a call to {@link #nextToken}, to its corresponding symbol
+ * type or code point.
+ *
+ * @param tag The bitmask tag of the current token, as returned by {@link #nextToken}.
+ * @return If less than zero, a symbol type corresponding to one of the <code>TYPE_</code>
+ * constants, such as {@link #TYPE_MINUS_SIGN}. If greater than or equal to zero, a literal
+ * code point.
+ */
+ public static int getTypeOrCp(long tag) {
+ assert tag >= 0;
+ int type = getType(tag);
+ return (type == 0) ? getCodePoint(tag) : -type;
+ }
+
+ private static long makeTag(int offset, int type, int state, int cp) {
+ long tag = 0L;
+ tag |= offset;
+ tag |= (-(long) type) << 32;
+ tag |= ((long) state) << 36;
+ tag |= ((long) cp) << 40;
+ assert tag >= 0;
+ return tag;
+ }
+
+ static int getOffset(long tag) {
+ return (int) (tag & 0xffffffff);
+ }
+
+ static int getType(long tag) {
+ return (int) ((tag >>> 32) & 0xf);
+ }
+
+ static int getState(long tag) {
+ return (int) ((tag >>> 36) & 0xf);
+ }
+
+ static int getCodePoint(long tag) {
+ return (int) (tag >>> 40);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/Endpoint.java b/android_icu4j/src/main/java/android/icu/impl/number/Endpoint.java
new file mode 100644
index 000000000..211b6a85e
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/Endpoint.java
@@ -0,0 +1,302 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import android.icu.impl.number.Format.BeforeTargetAfterFormat;
+import android.icu.impl.number.Format.SingularFormat;
+import android.icu.impl.number.Format.TargetFormat;
+import android.icu.impl.number.formatters.BigDecimalMultiplier;
+import android.icu.impl.number.formatters.CompactDecimalFormat;
+import android.icu.impl.number.formatters.CurrencyFormat;
+import android.icu.impl.number.formatters.MagnitudeMultiplier;
+import android.icu.impl.number.formatters.MeasureFormat;
+import android.icu.impl.number.formatters.PaddingFormat;
+import android.icu.impl.number.formatters.PositiveDecimalFormat;
+import android.icu.impl.number.formatters.PositiveNegativeAffixFormat;
+import android.icu.impl.number.formatters.RoundingFormat;
+import android.icu.impl.number.formatters.ScientificFormat;
+import android.icu.text.DecimalFormatSymbols;
+import android.icu.text.PluralRules;
+import android.icu.util.ULocale;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class Endpoint {
+ // public static Format from(DecimalFormatSymbols symbols, Properties properties)
+ // throws ParseException {
+ // Format format = new PositiveIntegerFormat(symbols, properties);
+ // // TODO: integer-only format
+ // format = new PositiveDecimalFormat((SelfContainedFormat) format, symbols, properties);
+ // if (properties.useCompactDecimalFormat()) {
+ // format = CompactDecimalFormat.getInstance((SingularFormat) format, symbols, properties);
+ // } else {
+ // format =
+ // PositiveNegativeAffixFormat.getInstance((SingularFormat) format, symbols, properties);
+ // }
+ // if (properties.useRoundingInterval()) {
+ // format = new IntervalRoundingFormat((SingularFormat) format, properties);
+ // } else if (properties.useSignificantDigits()) {
+ // format = new SignificantDigitsFormat((SingularFormat) format, properties);
+ // } else if (properties.useFractionFormat()) {
+ // format = new RoundingFormat((SingularFormat) format, properties);
+ // }
+ // return format;
+ // }
+
+ public static interface IProperties {
+ static PluralRules DEFAULT_PLURAL_RULES = null;
+
+ public PluralRules getPluralRules();
+
+ public IProperties setPluralRules(PluralRules pluralRules);
+ }
+
+ public static Format fromBTA(Properties properties) {
+ return fromBTA(properties, getSymbols());
+ }
+
+ public static SingularFormat fromBTA(Properties properties, Locale locale) {
+ return fromBTA(properties, getSymbols(locale));
+ }
+
+ public static SingularFormat fromBTA(Properties properties, ULocale uLocale) {
+ return fromBTA(properties, getSymbols(uLocale));
+ }
+
+ public static SingularFormat fromBTA(String pattern) {
+ return fromBTA(getProperties(pattern), getSymbols());
+ }
+
+ public static SingularFormat fromBTA(String pattern, Locale locale) {
+ return fromBTA(getProperties(pattern), getSymbols(locale));
+ }
+
+ public static SingularFormat fromBTA(String pattern, ULocale uLocale) {
+ return fromBTA(getProperties(pattern), getSymbols(uLocale));
+ }
+
+ public static SingularFormat fromBTA(String pattern, DecimalFormatSymbols symbols) {
+ return fromBTA(getProperties(pattern), symbols);
+ }
+
+ public static SingularFormat fromBTA(Properties properties, DecimalFormatSymbols symbols) {
+
+ if (symbols == null) throw new IllegalArgumentException("symbols must not be null");
+
+ // TODO: This fast track results in an improvement of about 10ns during formatting. See if
+ // there is a way to implement it more elegantly.
+ boolean canUseFastTrack = true;
+ PluralRules rules = getPluralRules(symbols.getULocale(), properties);
+ BeforeTargetAfterFormat format = new Format.BeforeTargetAfterFormat(rules);
+ TargetFormat target = new PositiveDecimalFormat(symbols, properties);
+ format.setTargetFormat(target);
+ // TODO: integer-only format?
+ if (MagnitudeMultiplier.useMagnitudeMultiplier(properties)) {
+ canUseFastTrack = false;
+ format.addBeforeFormat(MagnitudeMultiplier.getInstance(properties));
+ }
+ if (BigDecimalMultiplier.useMultiplier(properties)) {
+ canUseFastTrack = false;
+ format.addBeforeFormat(BigDecimalMultiplier.getInstance(properties));
+ }
+ if (MeasureFormat.useMeasureFormat(properties)) {
+ canUseFastTrack = false;
+ format.addBeforeFormat(MeasureFormat.getInstance(symbols, properties));
+ }
+ if (CurrencyFormat.useCurrency(properties)) {
+ canUseFastTrack = false;
+ if (CompactDecimalFormat.useCompactDecimalFormat(properties)) {
+ format.addBeforeFormat(CompactDecimalFormat.getInstance(symbols, properties));
+ } else if (ScientificFormat.useScientificNotation(properties)) {
+ // TODO: Should the currency rounder or scientific rounder be used in this case?
+ // For now, default to using the scientific rounder.
+ format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties));
+ format.addBeforeFormat(ScientificFormat.getInstance(symbols, properties));
+ } else {
+ format.addBeforeFormat(CurrencyFormat.getCurrencyRounder(symbols, properties));
+ format.addBeforeFormat(CurrencyFormat.getCurrencyModifier(symbols, properties));
+ }
+ } else {
+ if (CompactDecimalFormat.useCompactDecimalFormat(properties)) {
+ canUseFastTrack = false;
+ format.addBeforeFormat(CompactDecimalFormat.getInstance(symbols, properties));
+ } else if (ScientificFormat.useScientificNotation(properties)) {
+ canUseFastTrack = false;
+ format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties));
+ format.addBeforeFormat(ScientificFormat.getInstance(symbols, properties));
+ } else {
+ format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties));
+ format.addBeforeFormat(RoundingFormat.getDefaultOrNoRounder(properties));
+ }
+ }
+ if (PaddingFormat.usePadding(properties)) {
+ canUseFastTrack = false;
+ format.addAfterFormat(PaddingFormat.getInstance(properties));
+ }
+ if (canUseFastTrack) {
+ return new Format.PositiveNegativeRounderTargetFormat(
+ PositiveNegativeAffixFormat.getInstance(symbols, properties),
+ RoundingFormat.getDefaultOrNoRounder(properties),
+ target);
+ } else {
+ return format;
+ }
+ }
+
+ public static String staticFormat(FormatQuantity input, Properties properties) {
+ return staticFormat(input, properties, getSymbols());
+ }
+
+ public static String staticFormat(FormatQuantity input, Properties properties, Locale locale) {
+ return staticFormat(input, properties, getSymbols(locale));
+ }
+
+ public static String staticFormat(FormatQuantity input, Properties properties, ULocale uLocale) {
+ return staticFormat(input, properties, getSymbols(uLocale));
+ }
+
+ public static String staticFormat(FormatQuantity input, String pattern) {
+ return staticFormat(input, getProperties(pattern), getSymbols());
+ }
+
+ public static String staticFormat(FormatQuantity input, String pattern, Locale locale) {
+ return staticFormat(input, getProperties(pattern), getSymbols(locale));
+ }
+
+ public static String staticFormat(FormatQuantity input, String pattern, ULocale uLocale) {
+ return staticFormat(input, getProperties(pattern), getSymbols(uLocale));
+ }
+
+ public static String staticFormat(
+ FormatQuantity input, String pattern, DecimalFormatSymbols symbols) {
+ return staticFormat(input, getProperties(pattern), symbols);
+ }
+
+ public static String staticFormat(
+ FormatQuantity input, Properties properties, DecimalFormatSymbols symbols) {
+ PluralRules rules = null;
+ ModifierHolder mods = Format.threadLocalModifierHolder.get().clear();
+ NumberStringBuilder sb = Format.threadLocalStringBuilder.get().clear();
+ int length = 0;
+
+ // Pre-processing
+ if (!input.isNaN()) {
+ if (MagnitudeMultiplier.useMagnitudeMultiplier(properties)) {
+ MagnitudeMultiplier.getInstance(properties).before(input, mods, rules);
+ }
+ if (BigDecimalMultiplier.useMultiplier(properties)) {
+ BigDecimalMultiplier.getInstance(properties).before(input, mods, rules);
+ }
+ if (MeasureFormat.useMeasureFormat(properties)) {
+ rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties);
+ MeasureFormat.getInstance(symbols, properties).before(input, mods, rules);
+ }
+ if (CompactDecimalFormat.useCompactDecimalFormat(properties)) {
+ rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties);
+ CompactDecimalFormat.apply(input, mods, rules, symbols, properties);
+ } else if (CurrencyFormat.useCurrency(properties)) {
+ rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties);
+ CurrencyFormat.getCurrencyRounder(symbols, properties).before(input, mods, rules);
+ CurrencyFormat.getCurrencyModifier(symbols, properties).before(input, mods, rules);
+ } else if (ScientificFormat.useScientificNotation(properties)) {
+ // TODO: Is it possible to combine significant digits with currency?
+ PositiveNegativeAffixFormat.getInstance(symbols, properties).before(input, mods, rules);
+ ScientificFormat.getInstance(symbols, properties).before(input, mods, rules);
+ } else {
+ PositiveNegativeAffixFormat.apply(input, mods, symbols, properties);
+ RoundingFormat.getDefaultOrNoRounder(properties).before(input, mods, rules);
+ }
+ }
+
+ // Primary format step
+ length += new PositiveDecimalFormat(symbols, properties).target(input, sb, 0);
+ length += mods.applyStrong(sb, 0, length);
+
+ // Post-processing
+ if (PaddingFormat.usePadding(properties)) {
+ length += PaddingFormat.getInstance(properties).after(mods, sb, 0, length);
+ }
+ length += mods.applyAll(sb, 0, length);
+
+ return sb.toString();
+ }
+
+ private static final ThreadLocal<Map<ULocale, DecimalFormatSymbols>> threadLocalSymbolsCache =
+ new ThreadLocal<Map<ULocale, DecimalFormatSymbols>>() {
+ @Override
+ protected Map<ULocale, DecimalFormatSymbols> initialValue() {
+ return new HashMap<ULocale, DecimalFormatSymbols>();
+ }
+ };
+
+ private static DecimalFormatSymbols getSymbols() {
+ ULocale uLocale = ULocale.getDefault();
+ return getSymbols(uLocale);
+ }
+
+ private static DecimalFormatSymbols getSymbols(Locale locale) {
+ ULocale uLocale = ULocale.forLocale(locale);
+ return getSymbols(uLocale);
+ }
+
+ private static DecimalFormatSymbols getSymbols(ULocale uLocale) {
+ if (uLocale == null) uLocale = ULocale.getDefault();
+ DecimalFormatSymbols symbols = threadLocalSymbolsCache.get().get(uLocale);
+ if (symbols == null) {
+ symbols = DecimalFormatSymbols.getInstance(uLocale);
+ threadLocalSymbolsCache.get().put(uLocale, symbols);
+ }
+ return symbols;
+ }
+
+ private static final ThreadLocal<Map<String, Properties>> threadLocalPropertiesCache =
+ new ThreadLocal<Map<String, Properties>>() {
+ @Override
+ protected Map<String, Properties> initialValue() {
+ return new HashMap<String, Properties>();
+ }
+ };
+
+ private static Properties getProperties(String pattern) {
+ if (pattern == null) pattern = "#";
+ Properties properties = threadLocalPropertiesCache.get().get(pattern);
+ if (properties == null) {
+ properties = PatternString.parseToProperties(pattern);
+ threadLocalPropertiesCache.get().put(pattern.intern(), properties);
+ }
+ return properties;
+ }
+
+ private static final ThreadLocal<Map<ULocale, PluralRules>> threadLocalRulesCache =
+ new ThreadLocal<Map<ULocale, PluralRules>>() {
+ @Override
+ protected Map<ULocale, PluralRules> initialValue() {
+ return new HashMap<ULocale, PluralRules>();
+ }
+ };
+
+ private static PluralRules getPluralRules(ULocale uLocale, Properties properties) {
+ if (properties.getPluralRules() != null) {
+ return properties.getPluralRules();
+ }
+
+ // Backwards compatibility: CurrencyPluralInfo wraps its own copy of PluralRules
+ if (properties.getCurrencyPluralInfo() != null) {
+ return properties.getCurrencyPluralInfo().getPluralRules();
+ }
+
+ if (uLocale == null) uLocale = ULocale.getDefault();
+ PluralRules rules = threadLocalRulesCache.get().get(uLocale);
+ if (rules == null) {
+ rules = PluralRules.forLocale(uLocale);
+ threadLocalRulesCache.get().put(uLocale, rules);
+ }
+ return rules;
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/Exportable.java b/android_icu4j/src/main/java/android/icu/impl/number/Exportable.java
new file mode 100644
index 000000000..12fff9998
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/Exportable.java
@@ -0,0 +1,17 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+/**
+ * This is a small interface I made to assist with converting from a formatter pipeline object to a
+ * pattern string. It allows classes to "export" themselves to a property bag, which in turn can be
+ * passed to {@link PatternString#propertiesToString(Properties)} to generate the pattern string.
+ *
+ * <p>Depending on the new API we expose, this process might not be necessary if we persist the
+ * property bag in the current DecimalFormat shim.
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public interface Exportable {
+ public void export(Properties properties);
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/Format.java b/android_icu4j/src/main/java/android/icu/impl/number/Format.java
new file mode 100644
index 000000000..ecbc80daa
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/Format.java
@@ -0,0 +1,281 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.text.AttributedCharacterIterator;
+import java.text.FieldPosition;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Deque;
+
+import android.icu.text.PluralRules;
+
+// TODO: Get a better name for this base class.
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public abstract class Format {
+
+ protected static final ThreadLocal<NumberStringBuilder> threadLocalStringBuilder =
+ new ThreadLocal<NumberStringBuilder>() {
+ @Override
+ protected NumberStringBuilder initialValue() {
+ return new NumberStringBuilder();
+ }
+ };
+
+ protected static final ThreadLocal<ModifierHolder> threadLocalModifierHolder =
+ new ThreadLocal<ModifierHolder>() {
+ @Override
+ protected ModifierHolder initialValue() {
+ return new ModifierHolder();
+ }
+ };
+
+ public String format(FormatQuantity... inputs) {
+ // Setup
+ Deque<FormatQuantity> inputDeque = new ArrayDeque<FormatQuantity>();
+ inputDeque.addAll(Arrays.asList(inputs));
+ ModifierHolder modDeque = threadLocalModifierHolder.get().clear();
+ NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
+
+ // Primary "recursion" step, calling the implementation's process method
+ int length = process(inputDeque, modDeque, sb, 0);
+
+ // Resolve remaining affixes
+ modDeque.applyAll(sb, 0, length);
+ return sb.toString();
+ }
+
+ /** A Format that works on only one number. */
+ public abstract static class SingularFormat extends Format implements Exportable {
+
+ public String format(FormatQuantity input) {
+ NumberStringBuilder sb = formatToStringBuilder(input);
+ return sb.toString();
+ }
+
+ public void format(FormatQuantity input, StringBuffer output) {
+ NumberStringBuilder sb = formatToStringBuilder(input);
+ output.append(sb);
+ }
+
+ public String format(FormatQuantity input, FieldPosition fp) {
+ NumberStringBuilder sb = formatToStringBuilder(input);
+ sb.populateFieldPosition(fp, 0);
+ return sb.toString();
+ }
+
+ public void format(FormatQuantity input, StringBuffer output, FieldPosition fp) {
+ NumberStringBuilder sb = formatToStringBuilder(input);
+ sb.populateFieldPosition(fp, output.length());
+ output.append(sb);
+ }
+
+ public AttributedCharacterIterator formatToCharacterIterator(FormatQuantity input) {
+ NumberStringBuilder sb = formatToStringBuilder(input);
+ return sb.getIterator();
+ }
+
+ private NumberStringBuilder formatToStringBuilder(FormatQuantity input) {
+ // Setup
+ ModifierHolder modDeque = threadLocalModifierHolder.get().clear();
+ NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
+
+ // Primary "recursion" step, calling the implementation's process method
+ int length = process(input, modDeque, sb, 0);
+
+ // Resolve remaining affixes
+ length += modDeque.applyAll(sb, 0, length);
+ return sb;
+ }
+
+ @Override
+ public int process(
+ Deque<FormatQuantity> input,
+ ModifierHolder mods,
+ NumberStringBuilder string,
+ int startIndex) {
+ return process(input.removeFirst(), mods, string, startIndex);
+ }
+
+ public abstract int process(
+ FormatQuantity input, ModifierHolder mods, NumberStringBuilder string, int startIndex);
+ }
+
+ public static class BeforeTargetAfterFormat extends SingularFormat {
+ // The formatters are kept as individual fields to avoid extra object creation overhead.
+ private BeforeFormat before1 = null;
+ private BeforeFormat before2 = null;
+ private BeforeFormat before3 = null;
+ private TargetFormat target = null;
+ private AfterFormat after1 = null;
+ private AfterFormat after2 = null;
+ private AfterFormat after3 = null;
+ private final PluralRules rules;
+
+ public BeforeTargetAfterFormat(PluralRules rules) {
+ this.rules = rules;
+ }
+
+ public void addBeforeFormat(BeforeFormat before) {
+ if (before1 == null) {
+ before1 = before;
+ } else if (before2 == null) {
+ before2 = before;
+ } else if (before3 == null) {
+ before3 = before;
+ } else {
+ throw new IllegalArgumentException("Only three BeforeFormats are allowed at a time");
+ }
+ }
+
+ public void setTargetFormat(TargetFormat target) {
+ this.target = target;
+ }
+
+ public void addAfterFormat(AfterFormat after) {
+ if (after1 == null) {
+ after1 = after;
+ } else if (after2 == null) {
+ after2 = after;
+ } else if (after3 == null) {
+ after3 = after;
+ } else {
+ throw new IllegalArgumentException("Only three AfterFormats are allowed at a time");
+ }
+ }
+
+ @Override
+ public String format(FormatQuantity input) {
+ ModifierHolder mods = threadLocalModifierHolder.get().clear();
+ NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
+ int length = process(input, mods, sb, 0);
+ mods.applyAll(sb, 0, length);
+ return sb.toString();
+ }
+
+ @Override
+ public int process(
+ FormatQuantity input, ModifierHolder mods, NumberStringBuilder string, int startIndex) {
+ // Special case: modifiers are skipped for NaN
+ int length = 0;
+ if (!input.isNaN()) {
+ if (before1 != null) {
+ before1.before(input, mods, rules);
+ }
+ if (before2 != null) {
+ before2.before(input, mods, rules);
+ }
+ if (before3 != null) {
+ before3.before(input, mods, rules);
+ }
+ }
+ length = target.target(input, string, startIndex);
+ length += mods.applyStrong(string, startIndex, startIndex + length);
+ if (after1 != null) {
+ length += after1.after(mods, string, startIndex, startIndex + length);
+ }
+ if (after2 != null) {
+ length += after2.after(mods, string, startIndex, startIndex + length);
+ }
+ if (after3 != null) {
+ length += after3.after(mods, string, startIndex, startIndex + length);
+ }
+ return length;
+ }
+
+ @Override
+ public void export(Properties properties) {
+ if (before1 != null) {
+ before1.export(properties);
+ }
+ if (before2 != null) {
+ before2.export(properties);
+ }
+ if (before3 != null) {
+ before3.export(properties);
+ }
+ target.export(properties);
+ if (after1 != null) {
+ after1.export(properties);
+ }
+ if (after2 != null) {
+ after2.export(properties);
+ }
+ if (after3 != null) {
+ after3.export(properties);
+ }
+ }
+ }
+
+ public static class PositiveNegativeRounderTargetFormat extends SingularFormat {
+ private final Modifier.PositiveNegativeModifier positiveNegative;
+ private final Rounder rounder;
+ private final TargetFormat target;
+
+ public PositiveNegativeRounderTargetFormat(
+ Modifier.PositiveNegativeModifier positiveNegative, Rounder rounder, TargetFormat target) {
+ this.positiveNegative = positiveNegative;
+ this.rounder = rounder;
+ this.target = target;
+ }
+
+ @Override
+ public String format(FormatQuantity input) {
+ NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
+ process(input, null, sb, 0);
+ return sb.toString();
+ }
+
+ @Override
+ public int process(
+ FormatQuantity input, ModifierHolder mods, NumberStringBuilder string, int startIndex) {
+ // Special case: modifiers are skipped for NaN
+ Modifier mod = null;
+ rounder.apply(input);
+ if (!input.isNaN() && positiveNegative != null) {
+ mod = positiveNegative.getModifier(input.isNegative());
+ }
+ int length = target.target(input, string, startIndex);
+ if (mod != null) {
+ length += mod.apply(string, 0, length);
+ }
+ return length;
+ }
+
+ @Override
+ public void export(Properties properties) {
+ rounder.export(properties);
+ positiveNegative.export(properties);
+ target.export(properties);
+ }
+ }
+
+ public abstract static class BeforeFormat implements Exportable {
+ protected abstract void before(FormatQuantity input, ModifierHolder mods);
+
+ @SuppressWarnings("unused")
+ public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) {
+ before(input, mods);
+ }
+ }
+
+ public static interface TargetFormat extends Exportable {
+ public abstract int target(FormatQuantity input, NumberStringBuilder string, int startIndex);
+ }
+
+ public static interface AfterFormat extends Exportable {
+ public abstract int after(
+ ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex);
+ }
+
+ // Instead of Dequeue<BigDecimal>, it could be Deque<Quantity> where
+ // we control the API of Quantity
+ public abstract int process(
+ Deque<FormatQuantity> inputs,
+ ModifierHolder outputMods,
+ NumberStringBuilder outputString,
+ int startIndex);
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity.java b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity.java
new file mode 100644
index 000000000..30f64857a
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity.java
@@ -0,0 +1,185 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+
+import android.icu.impl.StandardPlural;
+import android.icu.text.PluralRules;
+
+/**
+ * An interface representing a number to be processed by the decimal formatting pipeline. Includes
+ * methods for rounding, plural rules, and decimal digit extraction.
+ *
+ * <p>By design, this is NOT IMMUTABLE and NOT THREAD SAFE. It is intended to be an intermediate
+ * object holding state during a pass through the decimal formatting pipeline.
+ *
+ * <p>Implementations of this interface are free to use any internal storage mechanism.
+ *
+ * <p>TODO: Should I change this to an abstract class so that logic for min/max digits doesn't need
+ * to be copied to every implementation?
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public interface FormatQuantity extends PluralRules.IFixedDecimal {
+
+ /**
+ * Sets the minimum and maximum digits that this {@link FormatQuantity} should generate. This
+ * method does not perform rounding.
+ *
+ * @param minInt The minimum number of integer digits.
+ * @param maxInt The maximum number of integer digits.
+ * @param minFrac The minimum number of fraction digits.
+ * @param maxFrac The maximum number of fraction digits.
+ */
+ public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac);
+
+ /**
+ * Rounds the number to a specified interval, such as 0.05.
+ *
+ * <p>If rounding to a power of ten, use the more efficient {@link #roundToMagnitude} instead.
+ *
+ * @param roundingInterval The increment to which to round.
+ * @param mathContext The {@link MathContext} to use if rounding is necessary. Undefined behavior
+ * if null.
+ */
+ public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext);
+
+ /**
+ * Rounds the number to a specified magnitude (power of ten).
+ *
+ * @param roundingMagnitude The power of ten to which to round. For example, a value of -2 will
+ * round to 2 decimal places.
+ * @param mathContext The {@link MathContext} to use if rounding is necessary. Undefined behavior
+ * if null.
+ */
+ public void roundToMagnitude(int roundingMagnitude, MathContext mathContext);
+
+ /**
+ * Rounds the number to an infinite number of decimal points. This has no effect except for
+ * forcing the double in {@link FormatQuantityBCD} to adopt its exact representation.
+ */
+ public void roundToInfinity();
+
+ /**
+ * Multiply the internal value.
+ *
+ * @param multiplicand The value by which to multiply.
+ */
+ public void multiplyBy(BigDecimal multiplicand);
+
+ /**
+ * Scales the number by a power of ten. For example, if the value is currently "1234.56", calling
+ * this method with delta=-3 will change the value to "1.23456".
+ *
+ * @param delta The number of magnitudes of ten to change by.
+ */
+ public void adjustMagnitude(int delta);
+
+ /**
+ * @return The power of ten corresponding to the most significant nonzero digit.
+ * @throws ArithmeticException If the value represented is zero.
+ */
+ public int getMagnitude() throws ArithmeticException;
+
+ /** @return Whether the value represented by this {@link FormatQuantity} is zero. */
+ public boolean isZero();
+
+ /** @return Whether the value represented by this {@link FormatQuantity} is less than zero. */
+ public boolean isNegative();
+
+ /** @return Whether the value represented by this {@link FormatQuantity} is infinite. */
+ @Override
+ public boolean isInfinite();
+
+ /** @return Whether the value represented by this {@link FormatQuantity} is not a number. */
+ @Override
+ public boolean isNaN();
+
+ /** @return The value contained in this {@link FormatQuantity} approximated as a double. */
+ public double toDouble();
+
+ public BigDecimal toBigDecimal();
+
+ public int maxRepresentableDigits();
+
+ // TODO: Should this method be removed, since FormatQuantity implements IFixedDecimal now?
+ /**
+ * Computes the plural form for this number based on the specified set of rules.
+ *
+ * @param rules A {@link PluralRules} object representing the set of rules.
+ * @return The {@link StandardPlural} according to the PluralRules. If the plural form is not in
+ * the set of standard plurals, {@link StandardPlural#OTHER} is returned instead.
+ */
+ public StandardPlural getStandardPlural(PluralRules rules);
+
+ // /**
+ // * @return The number of fraction digits, always in the closed interval [minFrac, maxFrac].
+ // * @see #setIntegerFractionLength(int, int, int, int)
+ // */
+ // public int fractionCount();
+ //
+ // /**
+ // * @return The number of integer digits, always in the closed interval [minInt, maxInt].
+ // * @see #setIntegerFractionLength(int, int, int, int)
+ // */
+ // public int integerCount();
+ //
+ // /**
+ // * @param index The index of the fraction digit relative to the decimal place, or 1 minus the
+ // * digit's power of ten.
+ // * @return The digit at the specified index. Undefined if index is greater than maxInt or less
+ // * than 0.
+ // * @see #fractionCount()
+ // */
+ // public byte getFractionDigit(int index);
+ //
+ // /**
+ // * @param index The index of the integer digit relative to the decimal place, or the digit's power
+ // * of ten.
+ // * @return The digit at the specified index. Undefined if index is greater than maxInt or less
+ // * than 0.
+ // * @see #integerCount()
+ // */
+ // public byte getIntegerDigit(int index);
+
+ /**
+ * Gets the digit at the specified magnitude. For example, if the represented number is 12.3,
+ * getDigit(-1) returns 3, since 3 is the digit corresponding to 10^-1.
+ *
+ * @param magnitude The magnitude of the digit.
+ * @return The digit at the specified magnitude.
+ */
+ public byte getDigit(int magnitude);
+
+ /**
+ * Gets the largest power of ten that needs to be displayed. The value returned by this function
+ * will be bounded between minInt and maxInt.
+ *
+ * @return The highest-magnitude digit to be displayed.
+ */
+ public int getUpperDisplayMagnitude();
+
+ /**
+ * Gets the smallest power of ten that needs to be displayed. The value returned by this function
+ * will be bounded between -minFrac and -maxFrac.
+ *
+ * @return The lowest-magnitude digit to be displayed.
+ */
+ public int getLowerDisplayMagnitude();
+
+ /**
+ * Like clone, but without the restrictions of the Cloneable interface clone.
+ *
+ * @return A copy of this instance which can be mutated without affecting this instance.
+ */
+ public FormatQuantity createCopy();
+
+ public void copyFrom(FormatQuantity other);
+
+ /**
+ * This method is for internal testing only.
+ */
+ public long getPositionFingerprint();
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity1.java b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity1.java
new file mode 100644
index 000000000..4276deb5b
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity1.java
@@ -0,0 +1,858 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+import android.icu.impl.StandardPlural;
+import android.icu.text.PluralRules;
+import android.icu.text.PluralRules.Operand;
+
+/**
+ * This is an older implementation of FormatQuantity. A newer, faster implementation is
+ * FormatQuantity2. I kept this implementation around because it was useful for testing purposes
+ * (being able to compare the output of one implementation with the other).
+ *
+ * <p>This class is NOT IMMUTABLE and NOT THREAD SAFE and is intended to be used by a single thread
+ * to format a number through a formatter, which is thread-safe.
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class FormatQuantity1 implements FormatQuantity {
+ // Four positions: left optional '(', left required '[', right required ']', right optional ')'.
+ // These four positions determine which digits are displayed in the output string. They do NOT
+ // affect rounding. These positions are internal-only and can be specified only by the public
+ // endpoints like setFractionLength, setIntegerLength, and setSignificantDigits, among others.
+ //
+ // * Digits between lReqPos and rReqPos are in the "required zone" and are always displayed.
+ // * Digits between lOptPos and rOptPos but outside the required zone are in the "optional zone"
+ // and are displayed unless they are trailing off the left or right edge of the number and
+ // have a numerical value of zero. In order to be "trailing", the digits need to be beyond
+ // the decimal point in their respective directions.
+ // * Digits outside of the "optional zone" are never displayed.
+ //
+ // See the table below for illustrative examples.
+ //
+ // +---------+---------+---------+---------+------------+------------------------+--------------+
+ // | lOptPos | lReqPos | rReqPos | rOptPos | number | positions | en-US string |
+ // +---------+---------+---------+---------+------------+------------------------+--------------+
+ // | 5 | 2 | -1 | -5 | 1234.567 | ( 12[34.5]67 ) | 1,234.567 |
+ // | 3 | 2 | -1 | -5 | 1234.567 | 1(2[34.5]67 ) | 234.567 |
+ // | 3 | 2 | -1 | -2 | 1234.567 | 1(2[34.5]6)7 | 234.56 |
+ // | 6 | 4 | 2 | -5 | 123456789. | 123(45[67]89. ) | 456,789. |
+ // | 6 | 4 | 2 | 1 | 123456789. | 123(45[67]8)9. | 456,780. |
+ // | -1 | -1 | -3 | -4 | 0.123456 | 0.1([23]4)56 | .0234 |
+ // | 6 | 4 | -2 | -2 | 12.3 | ( [ 12.3 ]) | 0012.30 |
+ // +---------+---------+---------+---------+------------+------------------------+--------------+
+ //
+ private int lOptPos = Integer.MAX_VALUE;
+ private int lReqPos = 0;
+ private int rReqPos = 0;
+ private int rOptPos = Integer.MIN_VALUE;
+
+ // Internally, attempt to use a long to store the number. A long can hold numbers between 18 and
+ // 19 digits, covering the vast majority of use cases. We store three values: the long itself,
+ // the "scale" of the long (the power of 10 represented by the rightmost digit in the long), and
+ // the "precision" (the number of digits in the long). "primary" and "primaryScale" are the only
+ // two variables that are required for representing the number in memory. "primaryPrecision" is
+ // saved only for the sake of performance enhancements when performing certain operations. It can
+ // always be re-computed from "primary" and "primaryScale".
+ private long primary;
+ private int primaryScale;
+ private int primaryPrecision;
+
+ // If the decimal can't fit into the long, fall back to a BigDecimal.
+ private BigDecimal fallback;
+
+ // Other properties
+ private int flags;
+ private static final int NEGATIVE_FLAG = 1;
+ private static final int INFINITY_FLAG = 2;
+ private static final int NAN_FLAG = 4;
+ private static final long[] POWERS_OF_TEN = {
+ 1L,
+ 10L,
+ 100L,
+ 1000L,
+ 10000L,
+ 100000L,
+ 1000000L,
+ 10000000L,
+ 100000000L,
+ 1000000000L,
+ 10000000000L,
+ 100000000000L,
+ 1000000000000L,
+ 10000000000000L,
+ 100000000000000L,
+ 1000000000000000L,
+ 10000000000000000L,
+ 100000000000000000L,
+ 1000000000000000000L
+ };
+
+ @Override
+ public int maxRepresentableDigits() {
+ return Integer.MAX_VALUE;
+ }
+
+ public FormatQuantity1(long input) {
+ if (input < 0) {
+ setNegative(true);
+ input *= -1;
+ }
+
+ primary = input;
+ primaryScale = 0;
+ primaryPrecision = computePrecision(primary);
+ fallback = null;
+ }
+
+ /**
+ * Creates a FormatQuantity from the given double value. Internally attempts several strategies
+ * for converting the double to an exact representation, falling back on a BigDecimal if it fails
+ * to do so.
+ *
+ * @param input The double to represent by this FormatQuantity.
+ */
+ public FormatQuantity1(double input) {
+ if (input < 0) {
+ setNegative(true);
+ input *= -1;
+ }
+
+ // First try reading from IEEE bits. This is trivial only for doubles in [2^52, 2^64). If it
+ // fails, we wasted only a few CPU cycles.
+ long ieeeBits = Double.doubleToLongBits(input);
+ int exponent = (int) ((ieeeBits & 0x7ff0000000000000L) >> 52) - 0x3ff;
+ if (exponent >= 52 && exponent <= 63) {
+ // We can convert this double directly to a long.
+ long mantissa = (ieeeBits & 0x000fffffffffffffL) + 0x0010000000000000L;
+ primary = (mantissa << (exponent - 52));
+ primaryScale = 0;
+ primaryPrecision = computePrecision(primary);
+ return;
+ }
+
+ // Now try parsing the string produced by Double.toString().
+ String temp = Double.toString(input);
+ try {
+ if (temp.length() == 3 && temp.equals("0.0")) {
+ // Case 1: Zero.
+ primary = 0L;
+ primaryScale = 0;
+ primaryPrecision = 0;
+ } else if (temp.indexOf('E') != -1) {
+ // Case 2: Exponential notation.
+ assert temp.indexOf('.') == 1;
+ int expPos = temp.indexOf('E');
+ primary = Long.parseLong(temp.charAt(0) + temp.substring(2, expPos));
+ primaryScale = Integer.parseInt(temp.substring(expPos + 1)) - (expPos - 1) + 1;
+ primaryPrecision = expPos - 1;
+ } else if (temp.charAt(0) == '0') {
+ // Case 3: Fraction-only number.
+ assert temp.indexOf('.') == 1;
+ primary = Long.parseLong(temp.substring(2)); // ignores leading zeros
+ primaryScale = 2 - temp.length();
+ primaryPrecision = computePrecision(primary);
+ } else if (temp.charAt(temp.length() - 1) == '0') {
+ // Case 4: Integer-only number.
+ assert temp.indexOf('.') == temp.length() - 2;
+ int rightmostNonzeroDigitIndex = temp.length() - 3;
+ while (temp.charAt(rightmostNonzeroDigitIndex) == '0') {
+ rightmostNonzeroDigitIndex -= 1;
+ }
+ primary = Long.parseLong(temp.substring(0, rightmostNonzeroDigitIndex + 1));
+ primaryScale = temp.length() - rightmostNonzeroDigitIndex - 3;
+ primaryPrecision = rightmostNonzeroDigitIndex + 1;
+ } else if (temp.equals("Infinity")) {
+ // Case 5: Infinity.
+ primary = 0;
+ setInfinity(true);
+ } else if (temp.equals("NaN")) {
+ // Case 6: NaN.
+ primary = 0;
+ setNaN(true);
+ } else {
+ // Case 7: Number with both a fraction and an integer.
+ int decimalPos = temp.indexOf('.');
+ primary = Long.parseLong(temp.substring(0, decimalPos) + temp.substring(decimalPos + 1));
+ primaryScale = decimalPos - temp.length() + 1;
+ primaryPrecision = temp.length() - 1;
+ }
+ } catch (NumberFormatException e) {
+ // The digits of the double can't fit into the long.
+ primary = -1;
+ fallback = new BigDecimal(temp);
+ }
+ }
+
+ static final double LOG_2_OF_TEN = 3.32192809489;
+
+ public FormatQuantity1(double input, boolean fast) {
+ if (input < 0) {
+ setNegative(true);
+ input *= -1;
+ }
+
+ // Our strategy is to read all digits that are *guaranteed* to be valid without delving into
+ // the IEEE rounding rules. This strategy might not end up with a perfect representation of
+ // the fractional part of the double.
+ long ieeeBits = Double.doubleToLongBits(input);
+ int exponent = (int) ((ieeeBits & 0x7ff0000000000000L) >> 52) - 0x3ff;
+ long mantissa = (ieeeBits & 0x000fffffffffffffL) + 0x0010000000000000L;
+ if (exponent > 63) {
+ throw new IllegalArgumentException(); // FIXME
+ } else if (exponent >= 52) {
+ primary = (mantissa << (exponent - 52));
+ primaryScale = 0;
+ primaryPrecision = computePrecision(primary);
+ return;
+ } else if (exponent >= 0) {
+ int shift = 52 - exponent;
+ primary = (mantissa >> shift); // integer part
+ int fractionCount = (int) (shift / LOG_2_OF_TEN);
+ long fraction = (mantissa - (primary << shift)) + 1L; // TODO: Explain the +1L
+ primary *= POWERS_OF_TEN[fractionCount];
+ for (int i = 0; i < fractionCount; i++) {
+ long times10 = (fraction * 10L);
+ long digit = times10 >> shift;
+ assert digit >= 0 && digit < 10;
+ primary += digit * POWERS_OF_TEN[fractionCount - i - 1];
+ fraction = times10 & ((1L << shift) - 1);
+ }
+ primaryScale = -fractionCount;
+ primaryPrecision = computePrecision(primary);
+ } else {
+ throw new IllegalArgumentException(); // FIXME
+ }
+ }
+
+ public FormatQuantity1(BigDecimal decimal) {
+ if (decimal.compareTo(BigDecimal.ZERO) < 0) {
+ setNegative(true);
+ decimal = decimal.negate();
+ }
+
+ primary = -1;
+ if (decimal.compareTo(BigDecimal.ZERO) == 0) {
+ fallback = BigDecimal.ZERO;
+ } else {
+ fallback = decimal;
+ }
+ }
+
+ public FormatQuantity1(FormatQuantity1 other) {
+ copyFrom(other);
+ }
+
+ @Override
+ public FormatQuantity1 createCopy() {
+ return new FormatQuantity1(this);
+ }
+
+ /**
+ * Make the internal state of this FormatQuantity equal to another FormatQuantity.
+ *
+ * @param other The template FormatQuantity. All properties from this FormatQuantity will be
+ * copied into this FormatQuantity.
+ */
+ @Override
+ public void copyFrom(FormatQuantity other) {
+ // TODO: Check before casting
+ FormatQuantity1 _other = (FormatQuantity1) other;
+ lOptPos = _other.lOptPos;
+ lReqPos = _other.lReqPos;
+ rReqPos = _other.rReqPos;
+ rOptPos = _other.rOptPos;
+ primary = _other.primary;
+ primaryScale = _other.primaryScale;
+ primaryPrecision = _other.primaryPrecision;
+ fallback = _other.fallback;
+ flags = _other.flags;
+ }
+
+ @Override
+ public long getPositionFingerprint() {
+ long fingerprint = 0;
+ fingerprint ^= lOptPos;
+ fingerprint ^= (lReqPos << 16);
+ fingerprint ^= ((long) rReqPos << 32);
+ fingerprint ^= ((long) rOptPos << 48);
+ return fingerprint;
+ }
+
+ /**
+ * Utility method to compute the number of digits ("precision") in a long.
+ *
+ * @param input The long (which can't contain more than 19 digits).
+ * @return The precision of the long.
+ */
+ private static int computePrecision(long input) {
+ int precision = 0;
+ while (input > 0) {
+ input /= 10;
+ precision++;
+ }
+ return precision;
+ }
+
+ /**
+ * Changes the internal representation from a long to a BigDecimal. Used only for operations that
+ * don't support longs.
+ */
+ private void convertToBigDecimal() {
+ if (primary == -1) {
+ return;
+ }
+
+ fallback = new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
+ primary = -1;
+ }
+
+ @Override
+ public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac) {
+ // Graceful failures for bogus input
+ minInt = Math.max(0, minInt);
+ maxInt = Math.max(0, maxInt);
+ minFrac = Math.max(0, minFrac);
+ maxFrac = Math.max(0, maxFrac);
+
+ // The minima must be less than or equal to the maxima
+ if (maxInt < minInt) {
+ minInt = maxInt;
+ }
+ if (maxFrac < minFrac) {
+ minFrac = maxFrac;
+ }
+
+ // Displaying neither integer nor fraction digits is not allowed
+ if (maxInt == 0 && maxFrac == 0) {
+ maxInt = Integer.MAX_VALUE;
+ maxFrac = Integer.MAX_VALUE;
+ }
+
+ // Save values into internal state
+ // Negation is safe for minFrac/maxFrac because -Integer.MAX_VALUE > Integer.MIN_VALUE
+ lOptPos = maxInt;
+ lReqPos = minInt;
+ rReqPos = -minFrac;
+ rOptPos = -maxFrac;
+ }
+
+ @Override
+ public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext) {
+ BigDecimal d =
+ (primary == -1) ? fallback : new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
+ if (isNegative()) d = d.negate();
+ d = d.divide(roundingInterval, 0, mathContext.getRoundingMode()).multiply(roundingInterval);
+ if (isNegative()) d = d.negate();
+ fallback = d;
+ primary = -1;
+ }
+
+ @Override
+ public void roundToMagnitude(int roundingMagnitude, MathContext mathContext) {
+ if (roundingMagnitude < -1000) {
+ roundToInfinity();
+ return;
+ }
+ if (primary == -1) {
+ if (isNegative()) fallback = fallback.negate();
+ fallback = fallback.setScale(-roundingMagnitude, mathContext.getRoundingMode());
+ if (isNegative()) fallback = fallback.negate();
+ // Enforce the math context.
+ fallback = fallback.round(mathContext);
+ } else {
+ int relativeScale = primaryScale - roundingMagnitude;
+ if (relativeScale < -18) {
+ // No digits will remain after rounding the number.
+ primary = 0L;
+ primaryScale = roundingMagnitude;
+ primaryPrecision = 0;
+ } else if (relativeScale < 0) {
+ // This is the harder case, when we need to perform the rounding logic.
+ // First check if the rightmost digits are already zero, where we can skip rounding.
+ if ((primary % POWERS_OF_TEN[0 - relativeScale]) == 0) {
+ // No rounding is necessary.
+ } else {
+ // TODO: Make this more efficient. Temporarily, convert to a BigDecimal and back again.
+ BigDecimal temp = new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
+ if (isNegative()) temp = temp.negate();
+ temp = temp.setScale(-roundingMagnitude, mathContext.getRoundingMode());
+ if (isNegative()) temp = temp.negate();
+ temp = temp.scaleByPowerOfTen(-roundingMagnitude);
+ primary = temp.longValueExact(); // should never throw
+ primaryScale = roundingMagnitude;
+ primaryPrecision = computePrecision(primary);
+ }
+ } else {
+ // No rounding is necessary. All digits are to the left of the rounding magnitude.
+ }
+ // Enforce the math context.
+ primary = new BigDecimal(primary).round(mathContext).longValueExact();
+ primaryPrecision = computePrecision(primary);
+ }
+ }
+
+ @Override
+ public void roundToInfinity() {
+ // noop
+ }
+
+ /**
+ * Multiply the internal number by the specified multiplicand. This method forces the internal
+ * representation into a BigDecimal. If you are multiplying by a power of 10, use {@link
+ * #adjustMagnitude} instead.
+ *
+ * @param multiplicand The number to be passed to {@link BigDecimal#multiply}.
+ */
+ @Override
+ public void multiplyBy(BigDecimal multiplicand) {
+ convertToBigDecimal();
+ fallback = fallback.multiply(multiplicand);
+ if (fallback.compareTo(BigDecimal.ZERO) < 0) {
+ setNegative(!isNegative());
+ fallback = fallback.negate();
+ }
+ }
+
+ /**
+ * Divide the internal number by the specified quotient. This method forces the internal
+ * representation into a BigDecimal. If you are dividing by a power of 10, use {@link
+ * #adjustMagnitude} instead.
+ *
+ * @param divisor The number to be passed to {@link BigDecimal#divide}.
+ * @param scale The scale of the final rounded number. More negative means more decimal places.
+ * @param mathContext The math context to use if rounding is necessary.
+ */
+ @SuppressWarnings("unused")
+ private void divideBy(BigDecimal divisor, int scale, MathContext mathContext) {
+ convertToBigDecimal();
+ // Negate the scale because BigDecimal's scale is defined as the inverse of our scale
+ fallback = fallback.divide(divisor, -scale, mathContext.getRoundingMode());
+ if (fallback.compareTo(BigDecimal.ZERO) < 0) {
+ setNegative(!isNegative());
+ fallback = fallback.negate();
+ }
+ }
+
+ @Override
+ public boolean isZero() {
+ if (primary == -1) {
+ return fallback.compareTo(BigDecimal.ZERO) == 0;
+ } else {
+ return primary == 0;
+ }
+ }
+
+ /** @return The power of ten of the highest digit represented by this FormatQuantity */
+ @Override
+ public int getMagnitude() throws ArithmeticException {
+ int scale = (primary == -1) ? scaleBigDecimal(fallback) : primaryScale;
+ int precision = (primary == -1) ? precisionBigDecimal(fallback) : primaryPrecision;
+ if (precision == 0) {
+ throw new ArithmeticException("Magnitude is not well-defined for zero");
+ } else {
+ return scale + precision - 1;
+ }
+ }
+
+ /**
+ * Changes the magnitude of this FormatQuantity. If the indices of the represented digits had been
+ * previously specified, those indices are moved relative to the FormatQuantity.
+ *
+ * <p>This method does NOT perform rounding.
+ *
+ * @param delta The number of powers of ten to shift (positive shifts to the left).
+ */
+ @Override
+ public void adjustMagnitude(int delta) {
+ if (primary == -1) {
+ fallback = fallback.scaleByPowerOfTen(delta);
+ } else {
+ primaryScale = addOrMaxValue(primaryScale, delta);
+ }
+ }
+
+ private static int addOrMaxValue(int a, int b) {
+ // Check for overflow, and return min/max value if overflow occurs.
+ if (b < 0 && a + b > a) {
+ return Integer.MIN_VALUE;
+ } else if (b > 0 && a + b < a) {
+ return Integer.MAX_VALUE;
+ }
+ return a + b;
+ }
+
+ /** @return If the number represented by this FormatQuantity is less than zero */
+ @Override
+ public boolean isNegative() {
+ return (flags & NEGATIVE_FLAG) != 0;
+ }
+
+ private void setNegative(boolean isNegative) {
+ flags = (flags & (~NEGATIVE_FLAG)) | (isNegative ? NEGATIVE_FLAG : 0);
+ }
+
+ @Override
+ public boolean isInfinite() {
+ return (flags & INFINITY_FLAG) != 0;
+ }
+
+ private void setInfinity(boolean isInfinity) {
+ flags = (flags & (~INFINITY_FLAG)) | (isInfinity ? INFINITY_FLAG : 0);
+ }
+
+ @Override
+ public boolean isNaN() {
+ return (flags & NAN_FLAG) != 0;
+ }
+
+ private void setNaN(boolean isNaN) {
+ flags = (flags & (~NAN_FLAG)) | (isNaN ? NAN_FLAG : 0);
+ }
+
+ /**
+ * Returns a representation of this FormatQuantity as a double, with possible loss of information.
+ */
+ @Override
+ public double toDouble() {
+ double result;
+ if (primary == -1) {
+ result = fallback.doubleValue();
+ } else {
+ // TODO: Make this more efficient
+ result = primary;
+ for (int i = 0; i < primaryScale; i++) {
+ result *= 10.;
+ }
+ for (int i = 0; i > primaryScale; i--) {
+ result /= 10.;
+ }
+ }
+ return isNegative() ? -result : result;
+ }
+
+ @Override
+ public BigDecimal toBigDecimal() {
+ BigDecimal result;
+ if (primary != -1) {
+ result = new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
+ } else {
+ result = fallback;
+ }
+ return isNegative() ? result.negate() : result;
+ }
+
+ @Override
+ public StandardPlural getStandardPlural(PluralRules rules) {
+ if (rules == null) {
+ // Fail gracefully if the user didn't provide a PluralRules
+ return StandardPlural.OTHER;
+ } else {
+ // TODO: Avoid converting to a double for the sake of PluralRules
+ String ruleString = rules.select(toDouble());
+ return StandardPlural.orOtherFromString(ruleString);
+ }
+ }
+
+ @Override
+ public double getPluralOperand(Operand operand) {
+ // TODO: This is a temporary hack.
+ return new PluralRules.FixedDecimal(toDouble()).getPluralOperand(operand);
+ }
+
+ public boolean hasNextFraction() {
+ if (rReqPos < 0) {
+ // We are in the required zone.
+ return true;
+ } else if (rOptPos >= 0) {
+ // We are in the forbidden zone.
+ return false;
+ } else {
+ // We are in the optional zone.
+ if (primary == -1) {
+ return fallback.remainder(BigDecimal.ONE).compareTo(BigDecimal.ZERO) > 0;
+ } else {
+ if (primaryScale <= -19) {
+ // The number is a fraction so small that it consists of only fraction digits.
+ return primary > 0;
+ } else if (primaryScale < 0) {
+ // Check if we have a fraction part.
+ long factor = POWERS_OF_TEN[0 - primaryScale];
+ return ((primary % factor) != 0);
+ } else {
+ // The lowest digit in the long has magnitude greater than -1.
+ return false;
+ }
+ }
+ }
+ }
+
+ public byte nextFraction() {
+ byte returnValue;
+ if (primary == -1) {
+ BigDecimal temp = fallback.multiply(BigDecimal.TEN);
+ returnValue = temp.setScale(0, RoundingMode.FLOOR).remainder(BigDecimal.TEN).byteValue();
+ fallback = fallback.setScale(0, RoundingMode.FLOOR).add(temp.remainder(BigDecimal.ONE));
+ } else {
+ if (primaryScale <= -20) {
+ // The number is a fraction so small that it has no first fraction digit.
+ primaryScale += 1;
+ returnValue = 0;
+ } else if (primaryScale < 0) {
+ // Extract the fraction digit out of the middle of the long.
+ long factor = POWERS_OF_TEN[0 - primaryScale - 1];
+ long temp1 = primary / factor;
+ long temp2 = primary % factor;
+ returnValue = (byte) (temp1 % 10); // not necessarily nonzero
+ primary = ((temp1 / 10) * factor) + temp2;
+ primaryScale += 1;
+ if (temp1 != 0) {
+ primaryPrecision -= 1;
+ }
+ } else {
+ // The lowest digit in the long has magnitude greater than -1.
+ returnValue = 0;
+ }
+ }
+
+ // Update digit brackets
+ if (lOptPos < 0) {
+ lOptPos += 1;
+ }
+ if (lReqPos < 0) {
+ lReqPos += 1;
+ }
+ if (rReqPos < 0) {
+ rReqPos += 1;
+ }
+ if (rOptPos < 0) {
+ rOptPos += 1;
+ }
+
+ assert returnValue >= 0;
+ return returnValue;
+ }
+
+ public boolean hasNextInteger() {
+ if (lReqPos > 0) {
+ // We are in the required zone.
+ return true;
+ } else if (lOptPos <= 0) {
+ // We are in the forbidden zone.
+ return false;
+ } else {
+ // We are in the optional zone.
+ if (primary == -1) {
+ return fallback.setScale(0, RoundingMode.FLOOR).compareTo(BigDecimal.ZERO) > 0;
+ } else {
+ if (primaryScale < -18) {
+ // The number is a fraction so small that it has no integer part.
+ return false;
+ } else if (primaryScale < 0) {
+ // Check if we have an integer part.
+ long factor = POWERS_OF_TEN[0 - primaryScale];
+ return ((primary % factor) != primary); // equivalent: ((primary / 10) != 0)
+ } else {
+ // The lowest digit in the long has magnitude of at least 0.
+ return primary != 0;
+ }
+ }
+ }
+ }
+
+ private int integerCount() {
+ int digitsRemaining;
+ if (primary == -1) {
+ digitsRemaining = precisionBigDecimal(fallback) + scaleBigDecimal(fallback);
+ } else {
+ digitsRemaining = primaryPrecision + primaryScale;
+ }
+ return Math.min(Math.max(digitsRemaining, lReqPos), lOptPos);
+ }
+
+ private int fractionCount() {
+ // TODO: This is temporary.
+ FormatQuantity1 copy = new FormatQuantity1(this);
+ int fractionCount = 0;
+ while (copy.hasNextFraction()) {
+ copy.nextFraction();
+ fractionCount++;
+ }
+ return fractionCount;
+ }
+
+ @Override
+ public int getUpperDisplayMagnitude() {
+ return integerCount() - 1;
+ }
+
+ @Override
+ public int getLowerDisplayMagnitude() {
+ return -fractionCount();
+ }
+
+ // @Override
+ // public byte getIntegerDigit(int index) {
+ // return getDigitPos(index);
+ // }
+ //
+ // @Override
+ // public byte getFractionDigit(int index) {
+ // return getDigitPos(-index - 1);
+ // }
+
+ @Override
+ public byte getDigit(int magnitude) {
+ // TODO: This is temporary.
+ FormatQuantity1 copy = new FormatQuantity1(this);
+ if (magnitude < 0) {
+ for (int p = -1; p > magnitude; p--) {
+ copy.nextFraction();
+ }
+ return copy.nextFraction();
+ } else {
+ for (int p = 0; p < magnitude; p++) {
+ copy.nextInteger();
+ }
+ return copy.nextInteger();
+ }
+ }
+
+ public byte nextInteger() {
+ byte returnValue;
+ if (primary == -1) {
+ returnValue = fallback.setScale(0, RoundingMode.FLOOR).remainder(BigDecimal.TEN).byteValue();
+ BigDecimal temp = fallback.divide(BigDecimal.TEN).setScale(0, RoundingMode.FLOOR);
+ fallback = fallback.remainder(BigDecimal.ONE).add(temp);
+ } else {
+ if (primaryScale < -18) {
+ // The number is a fraction so small that it has no integer part.
+ returnValue = 0;
+ } else if (primaryScale < 0) {
+ // Extract the integer digit out of the middle of the long. In many ways, this is the heart
+ // of the digit iterator algorithm.
+ long factor = POWERS_OF_TEN[0 - primaryScale];
+ if ((primary % factor) != primary) { // equivalent: ((primary / 10) != 0)
+ returnValue = (byte) ((primary / factor) % 10);
+ long temp = (primary / 10);
+ primary = temp - (temp % factor) + (primary % factor);
+ primaryPrecision -= 1;
+ } else {
+ returnValue = 0;
+ }
+ } else if (primaryScale == 0) {
+ // Fast-path for primaryScale == 0 (otherwise equivalent to previous step).
+ if (primary != 0) {
+ returnValue = (byte) (primary % 10);
+ primary /= 10;
+ primaryPrecision -= 1;
+ } else {
+ returnValue = 0;
+ }
+ } else {
+ // The lowest digit in the long has magnitude greater than 0.
+ primaryScale -= 1;
+ returnValue = 0;
+ }
+ }
+
+ // Update digit brackets
+ if (lOptPos > 0) {
+ lOptPos -= 1;
+ }
+ if (lReqPos > 0) {
+ lReqPos -= 1;
+ }
+ if (rReqPos > 0) {
+ rReqPos -= 1;
+ }
+ if (rOptPos > 0) {
+ rOptPos -= 1;
+ }
+
+ assert returnValue >= 0;
+ return returnValue;
+ }
+
+ /**
+ * Helper method to compute the precision of a BigDecimal by our definition of precision, which is
+ * that the number zero gets precision zero.
+ *
+ * @param decimal The BigDecimal whose precision to compute.
+ * @return The precision by our definition.
+ */
+ private static int precisionBigDecimal(BigDecimal decimal) {
+ if (decimal.compareTo(BigDecimal.ZERO) == 0) {
+ return 0;
+ } else {
+ return decimal.precision();
+ }
+ }
+
+ /**
+ * Helper method to compute the scale of a BigDecimal by our definition of scale, which is that
+ * deeper fractions result in negative scales as opposed to positive scales.
+ *
+ * @param decimal The BigDecimal whose scale to compute.
+ * @return The scale by our definition.
+ */
+ private static int scaleBigDecimal(BigDecimal decimal) {
+ return -decimal.scale();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<FormatQuantity1 ");
+ if (primary == -1) {
+ sb.append(lOptPos > 1000 ? "max" : lOptPos);
+ sb.append(":");
+ sb.append(lReqPos);
+ sb.append(":");
+ sb.append(rReqPos);
+ sb.append(":");
+ sb.append(rOptPos < -1000 ? "min" : rOptPos);
+ sb.append(" ");
+ sb.append(fallback.toString());
+ } else {
+ String digits = Long.toString(primary);
+ int iDec = digits.length() + primaryScale;
+ int iLP = iDec - toRange(lOptPos, -1000, 1000);
+ int iLB = iDec - toRange(lReqPos, -1000, 1000);
+ int iRB = iDec - toRange(rReqPos, -1000, 1000);
+ int iRP = iDec - toRange(rOptPos, -1000, 1000);
+ iDec = Math.max(Math.min(iDec, digits.length() + 1), -1);
+ iLP = Math.max(Math.min(iLP, digits.length() + 1), -1);
+ iLB = Math.max(Math.min(iLB, digits.length() + 1), -1);
+ iRB = Math.max(Math.min(iRB, digits.length() + 1), -1);
+ iRP = Math.max(Math.min(iRP, digits.length() + 1), -1);
+
+ for (int i = -1; i <= digits.length() + 1; i++) {
+ if (i == iLP) sb.append('(');
+ if (i == iLB) sb.append('[');
+ if (i == iDec) sb.append('.');
+ if (i == iRB) sb.append(']');
+ if (i == iRP) sb.append(')');
+ if (i >= 0 && i < digits.length()) sb.append(digits.charAt(i));
+ else sb.append('\u00A0');
+ }
+ }
+ sb.append(">");
+ return sb.toString();
+ }
+
+ private static int toRange(int i, int lo, int hi) {
+ if (i < lo) {
+ return lo;
+ } else if (i > hi) {
+ return hi;
+ } else {
+ return i;
+ }
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity2.java b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity2.java
new file mode 100644
index 000000000..71b40a1a3
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity2.java
@@ -0,0 +1,180 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public final class FormatQuantity2 extends FormatQuantityBCD {
+
+ /**
+ * The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map
+ * to one digit. For example, the number "12345" in BCD is "0x12345".
+ *
+ * <p>Whenever bcd changes internally, {@link #compact()} must be called, except in special cases
+ * like setting the digit to zero.
+ */
+ private long bcd;
+
+ @Override
+ public int maxRepresentableDigits() {
+ return 16;
+ }
+
+ public FormatQuantity2(long input) {
+ setToLong(input);
+ }
+
+ public FormatQuantity2(int input) {
+ setToInt(input);
+ }
+
+ public FormatQuantity2(double input) {
+ setToDouble(input);
+ }
+
+ public FormatQuantity2(BigInteger input) {
+ setToBigInteger(input);
+ }
+
+ public FormatQuantity2(BigDecimal input) {
+ setToBigDecimal(input);
+ }
+
+ public FormatQuantity2(FormatQuantity2 other) {
+ copyFrom(other);
+ }
+
+ @Override
+ protected byte getDigitPos(int position) {
+ if (position < 0 || position >= 16) return 0;
+ return (byte) ((bcd >>> (position * 4)) & 0xf);
+ }
+
+ @Override
+ protected void setDigitPos(int position, byte value) {
+ assert position >= 0 && position < 16;
+ int shift = position * 4;
+ bcd = bcd & ~(0xfL << shift) | ((long) value << shift);
+ }
+
+ @Override
+ protected void shiftLeft(int numDigits) {
+ assert precision + numDigits <= 16;
+ bcd <<= (numDigits * 4);
+ scale -= numDigits;
+ precision += numDigits;
+ }
+
+ @Override
+ protected void shiftRight(int numDigits) {
+ bcd >>>= (numDigits * 4);
+ scale += numDigits;
+ precision -= numDigits;
+ }
+
+ @Override
+ protected void setBcdToZero() {
+ bcd = 0L;
+ scale = 0;
+ precision = 0;
+ isApproximate = false;
+ origDouble = 0;
+ origDelta = 0;
+ }
+
+ @Override
+ protected void readIntToBcd(int n) {
+ assert n != 0;
+ long result = 0L;
+ int i = 16;
+ for (; n != 0; n /= 10, i--) {
+ result = (result >>> 4) + (((long) n % 10) << 60);
+ }
+ // ints can't overflow the 16 digits in the BCD, so scale is always zero
+ bcd = result >>> (i * 4);
+ scale = 0;
+ precision = 16 - i;
+ }
+
+ @Override
+ protected void readLongToBcd(long n) {
+ assert n != 0;
+ long result = 0L;
+ int i = 16;
+ for (; n != 0L; n /= 10L, i--) {
+ result = (result >>> 4) + ((n % 10) << 60);
+ }
+ int adjustment = (i > 0) ? i : 0;
+ bcd = result >>> (adjustment * 4);
+ scale = (i < 0) ? -i : 0;
+ precision = 16 - i;
+ }
+
+ @Override
+ protected void readBigIntegerToBcd(BigInteger n) {
+ assert n.signum() != 0;
+ long result = 0L;
+ int i = 16;
+ for (; n.signum() != 0; i--) {
+ BigInteger[] temp = n.divideAndRemainder(BigInteger.TEN);
+ result = (result >>> 4) + (temp[1].longValue() << 60);
+ n = temp[0];
+ }
+ int adjustment = (i > 0) ? i : 0;
+ bcd = result >>> (adjustment * 4);
+ scale = (i < 0) ? -i : 0;
+ }
+
+ @Override
+ protected BigDecimal bcdToBigDecimal() {
+ long tempLong = 0L;
+ for (int shift = (precision - 1); shift >= 0; shift--) {
+ tempLong = tempLong * 10 + getDigitPos(shift);
+ }
+ BigDecimal result = BigDecimal.valueOf(tempLong);
+ result = result.scaleByPowerOfTen(scale);
+ if (isNegative()) result = result.negate();
+ return result;
+ }
+
+ @Override
+ protected void compact() {
+ // Special handling for 0
+ if (bcd == 0L) {
+ scale = 0;
+ precision = 0;
+ return;
+ }
+
+ // Compact the number (remove trailing zeros)
+ int delta = Long.numberOfTrailingZeros(bcd) / 4;
+ bcd >>>= delta * 4;
+ scale += delta;
+
+ // Compute precision
+ precision = 16 - (Long.numberOfLeadingZeros(bcd) / 4);
+ }
+
+ @Override
+ protected void copyBcdFrom(FormatQuantity _other) {
+ FormatQuantity2 other = (FormatQuantity2) _other;
+ bcd = other.bcd;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "<FormatQuantity2 %s:%d:%d:%s %016XE%d>",
+ (lOptPos > 1000 ? "max" : String.valueOf(lOptPos)),
+ lReqPos,
+ rReqPos,
+ (rOptPos < -1000 ? "min" : String.valueOf(rOptPos)),
+ bcd,
+ scale);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity3.java b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity3.java
new file mode 100644
index 000000000..56163605c
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity3.java
@@ -0,0 +1,229 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public final class FormatQuantity3 extends FormatQuantityBCD {
+
+ /**
+ * The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map
+ * to one digit. For example, the number "12345" in BCD is "0x12345".
+ *
+ * <p>Whenever bcd changes internally, {@link #compact()} must be called, except in special cases
+ * like setting the digit to zero.
+ */
+ private byte[] bcd = new byte[100];
+
+ @Override
+ public int maxRepresentableDigits() {
+ return Integer.MAX_VALUE;
+ }
+
+ public FormatQuantity3(long input) {
+ setToLong(input);
+ }
+
+ public FormatQuantity3(int input) {
+ setToInt(input);
+ }
+
+ public FormatQuantity3(double input) {
+ setToDouble(input);
+ }
+
+ public FormatQuantity3(BigInteger input) {
+ setToBigInteger(input);
+ }
+
+ public FormatQuantity3(BigDecimal input) {
+ setToBigDecimal(input);
+ }
+
+ public FormatQuantity3(FormatQuantity3 other) {
+ copyFrom(other);
+ }
+
+ @Override
+ protected byte getDigitPos(int position) {
+ if (position < 0 || position > precision) return 0;
+ return bcd[position];
+ }
+
+ @Override
+ protected void setDigitPos(int position, byte value) {
+ assert position >= 0;
+ ensureCapacity(position + 1);
+ bcd[position] = value;
+ }
+
+ @Override
+ protected void shiftLeft(int numDigits) {
+ ensureCapacity(precision + numDigits);
+ int i = precision + numDigits - 1;
+ for (; i >= numDigits; i--) {
+ bcd[i] = bcd[i - numDigits];
+ }
+ for (; i >= 0; i--) {
+ bcd[i] = 0;
+ }
+ scale -= numDigits;
+ precision += numDigits;
+ }
+
+ @Override
+ protected void shiftRight(int numDigits) {
+ int i = 0;
+ for (; i < precision - numDigits; i++) {
+ bcd[i] = bcd[i + numDigits];
+ }
+ for (; i < precision; i++) {
+ bcd[i] = 0;
+ }
+ scale += numDigits;
+ precision -= numDigits;
+ }
+
+ @Override
+ protected void setBcdToZero() {
+ for (int i = 0; i < precision; i++) {
+ bcd[i] = (byte) 0;
+ }
+ scale = 0;
+ precision = 0;
+ isApproximate = false;
+ origDouble = 0;
+ origDelta = 0;
+ }
+
+ @Override
+ protected void readIntToBcd(int n) {
+ assert n != 0;
+ int i = 0;
+ for (; n != 0L; n /= 10L, i++) {
+ bcd[i] = (byte) (n % 10);
+ }
+ scale = 0;
+ precision = i;
+ }
+
+ private static final byte[] LONG_MIN_VALUE =
+ new byte[] {8, 0, 8, 5, 7, 7, 4, 5, 8, 6, 3, 0, 2, 7, 3, 3, 2, 2, 9};
+
+ @Override
+ protected void readLongToBcd(long n) {
+ assert n != 0;
+ if (n == Long.MIN_VALUE) {
+ // Can't consume via the normal path.
+ System.arraycopy(LONG_MIN_VALUE, 0, bcd, 0, LONG_MIN_VALUE.length);
+ scale = 0;
+ precision = LONG_MIN_VALUE.length;
+ return;
+ }
+ int i = 0;
+ for (; n != 0L; n /= 10L, i++) {
+ bcd[i] = (byte) (n % 10);
+ }
+ scale = 0;
+ precision = i;
+ }
+
+ @Override
+ protected void readBigIntegerToBcd(BigInteger n) {
+ assert n.signum() != 0;
+ int i = 0;
+ for (; n.signum() != 0; i++) {
+ BigInteger[] temp = n.divideAndRemainder(BigInteger.TEN);
+ ensureCapacity(i + 1);
+ bcd[i] = temp[1].byteValue();
+ n = temp[0];
+ }
+ scale = 0;
+ precision = i;
+ }
+
+ @Override
+ protected BigDecimal bcdToBigDecimal() {
+ // Converting to a string here is faster than doing BigInteger/BigDecimal arithmetic.
+ return new BigDecimal(toDumbString());
+ }
+
+ private String toDumbString() {
+ StringBuilder sb = new StringBuilder();
+ if (isNegative()) sb.append('-');
+ if (precision == 0) {
+ sb.append('0');
+ return sb.toString();
+ }
+ for (int i = precision - 1; i >= 0; i--) {
+ sb.append(getDigitPos(i));
+ }
+ if (scale != 0) {
+ sb.append('E');
+ sb.append(scale);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ protected void compact() {
+ // Special handling for 0
+ boolean isZero = true;
+ for (int i = 0; i < precision; i++) {
+ if (bcd[i] != 0) {
+ isZero = false;
+ break;
+ }
+ }
+ if (isZero) {
+ scale = 0;
+ precision = 0;
+ return;
+ }
+
+ // Compact the number (remove trailing zeros)
+ int delta = 0;
+ for (; bcd[delta] == 0; delta++) ;
+ shiftRight(delta);
+
+ // Compute precision
+ int leading = precision - 1;
+ for (; leading >= 0 && bcd[leading] == 0; leading--) ;
+ precision = leading + 1;
+ }
+
+ private void ensureCapacity(int capacity) {
+ if (bcd.length >= capacity) return;
+ byte[] bcd1 = new byte[capacity * 2];
+ System.arraycopy(bcd, 0, bcd1, 0, bcd.length);
+ bcd = bcd1;
+ }
+
+ @Override
+ protected void copyBcdFrom(FormatQuantity _other) {
+ FormatQuantity3 other = (FormatQuantity3) _other;
+ System.arraycopy(other.bcd, 0, bcd, 0, bcd.length);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 30; i >= 0; i--) {
+ sb.append(bcd[i]);
+ }
+ return String.format(
+ "<FormatQuantity3 %s:%d:%d:%s %s%s%d>",
+ (lOptPos > 1000 ? "max" : String.valueOf(lOptPos)),
+ lReqPos,
+ rReqPos,
+ (rOptPos < -1000 ? "min" : String.valueOf(rOptPos)),
+ sb,
+ "E",
+ scale);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity4.java b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity4.java
new file mode 100644
index 000000000..050c69da2
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantity4.java
@@ -0,0 +1,415 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public final class FormatQuantity4 extends FormatQuantityBCD {
+
+ /**
+ * The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map
+ * to one digit. For example, the number "12345" in BCD is "0x12345".
+ *
+ * <p>Whenever bcd changes internally, {@link #compact()} must be called, except in special cases
+ * like setting the digit to zero.
+ */
+ private byte[] bcdBytes;
+
+ private long bcdLong = 0L;
+
+ private boolean usingBytes = false;
+
+ @Override
+ public int maxRepresentableDigits() {
+ return Integer.MAX_VALUE;
+ }
+
+ public FormatQuantity4() {
+ setBcdToZero();
+ }
+
+ public FormatQuantity4(long input) {
+ setToLong(input);
+ }
+
+ public FormatQuantity4(int input) {
+ setToInt(input);
+ }
+
+ public FormatQuantity4(double input) {
+ setToDouble(input);
+ }
+
+ public FormatQuantity4(BigInteger input) {
+ setToBigInteger(input);
+ }
+
+ public FormatQuantity4(BigDecimal input) {
+ setToBigDecimal(input);
+ }
+
+ public FormatQuantity4(FormatQuantity4 other) {
+ copyFrom(other);
+ }
+
+ public FormatQuantity4(Number number) {
+ if (number instanceof Long) {
+ setToLong(number.longValue());
+ } else if (number instanceof Integer) {
+ setToInt(number.intValue());
+ } else if (number instanceof Double) {
+ setToDouble(number.doubleValue());
+ } else if (number instanceof BigInteger) {
+ setToBigInteger((BigInteger) number);
+ } else if (number instanceof BigDecimal) {
+ setToBigDecimal((BigDecimal) number);
+ } else if (number instanceof android.icu.math.BigDecimal) {
+ setToBigDecimal(((android.icu.math.BigDecimal) number).toBigDecimal());
+ } else {
+ throw new IllegalArgumentException(
+ "Number is of an unsupported type: " + number.getClass().getName());
+ }
+ }
+
+ @Override
+ protected byte getDigitPos(int position) {
+ if (usingBytes) {
+ if (position < 0 || position > precision) return 0;
+ return bcdBytes[position];
+ } else {
+ if (position < 0 || position >= 16) return 0;
+ return (byte) ((bcdLong >>> (position * 4)) & 0xf);
+ }
+ }
+
+ @Override
+ protected void setDigitPos(int position, byte value) {
+ assert position >= 0;
+ if (usingBytes) {
+ ensureCapacity(position + 1);
+ bcdBytes[position] = value;
+ } else if (position >= 16) {
+ switchStorage();
+ ensureCapacity(position + 1);
+ bcdBytes[position] = value;
+ } else {
+ int shift = position * 4;
+ bcdLong = bcdLong & ~(0xfL << shift) | ((long) value << shift);
+ }
+ }
+
+ @Override
+ protected void shiftLeft(int numDigits) {
+ if (!usingBytes && precision + numDigits > 16) {
+ switchStorage();
+ }
+ if (usingBytes) {
+ ensureCapacity(precision + numDigits);
+ int i = precision + numDigits - 1;
+ for (; i >= numDigits; i--) {
+ bcdBytes[i] = bcdBytes[i - numDigits];
+ }
+ for (; i >= 0; i--) {
+ bcdBytes[i] = 0;
+ }
+ } else {
+ bcdLong <<= (numDigits * 4);
+ }
+ scale -= numDigits;
+ precision += numDigits;
+ }
+
+ @Override
+ protected void shiftRight(int numDigits) {
+ if (usingBytes) {
+ int i = 0;
+ for (; i < precision - numDigits; i++) {
+ bcdBytes[i] = bcdBytes[i + numDigits];
+ }
+ for (; i < precision; i++) {
+ bcdBytes[i] = 0;
+ }
+ } else {
+ bcdLong >>>= (numDigits * 4);
+ }
+ scale += numDigits;
+ precision -= numDigits;
+ }
+
+ @Override
+ protected void setBcdToZero() {
+ if (usingBytes) {
+ for (int i = 0; i < precision; i++) {
+ bcdBytes[i] = (byte) 0;
+ }
+ }
+ usingBytes = false;
+ bcdLong = 0L;
+ scale = 0;
+ precision = 0;
+ isApproximate = false;
+ origDouble = 0;
+ origDelta = 0;
+ }
+
+ @Override
+ protected void readIntToBcd(int n) {
+ assert n != 0;
+ // ints always fit inside the long implementation.
+ long result = 0L;
+ int i = 16;
+ for (; n != 0; n /= 10, i--) {
+ result = (result >>> 4) + (((long) n % 10) << 60);
+ }
+ usingBytes = false;
+ bcdLong = result >>> (i * 4);
+ scale = 0;
+ precision = 16 - i;
+ }
+
+ @Override
+ protected void readLongToBcd(long n) {
+ assert n != 0;
+ if (n >= 10000000000000000L) {
+ ensureCapacity();
+ int i = 0;
+ for (; n != 0L; n /= 10L, i++) {
+ bcdBytes[i] = (byte) (n % 10);
+ }
+ usingBytes = true;
+ scale = 0;
+ precision = i;
+ } else {
+ long result = 0L;
+ int i = 16;
+ for (; n != 0L; n /= 10L, i--) {
+ result = (result >>> 4) + ((n % 10) << 60);
+ }
+ assert i >= 0;
+ usingBytes = false;
+ bcdLong = result >>> (i * 4);
+ scale = 0;
+ precision = 16 - i;
+ }
+ }
+
+ @Override
+ protected void readBigIntegerToBcd(BigInteger n) {
+ assert n.signum() != 0;
+ ensureCapacity(); // allocate initial byte array
+ int i = 0;
+ for (; n.signum() != 0; i++) {
+ BigInteger[] temp = n.divideAndRemainder(BigInteger.TEN);
+ ensureCapacity(i + 1);
+ bcdBytes[i] = temp[1].byteValue();
+ n = temp[0];
+ }
+ usingBytes = true;
+ scale = 0;
+ precision = i;
+ }
+
+ @Override
+ protected BigDecimal bcdToBigDecimal() {
+ if (usingBytes) {
+ // Converting to a string here is faster than doing BigInteger/BigDecimal arithmetic.
+ StringBuilder sb = new StringBuilder();
+ if (isNegative()) sb.append('-');
+ assert precision > 0;
+ for (int i = precision - 1; i >= 0; i--) {
+ sb.append(getDigitPos(i));
+ }
+ if (scale != 0) {
+ sb.append('E');
+ sb.append(scale);
+ }
+ return new BigDecimal(sb.toString());
+ } else {
+ long tempLong = 0L;
+ for (int shift = (precision - 1); shift >= 0; shift--) {
+ tempLong = tempLong * 10 + getDigitPos(shift);
+ }
+ BigDecimal result = BigDecimal.valueOf(tempLong);
+ result = result.scaleByPowerOfTen(scale);
+ if (isNegative()) result = result.negate();
+ return result;
+ }
+ }
+
+ @Override
+ protected void compact() {
+ if (usingBytes) {
+ int delta = 0;
+ for (; delta < precision && bcdBytes[delta] == 0; delta++) ;
+ if (delta == precision) {
+ // Number is zero
+ setBcdToZero();
+ return;
+ } else {
+ // Remove trailing zeros
+ shiftRight(delta);
+ }
+
+ // Compute precision
+ int leading = precision - 1;
+ for (; leading >= 0 && bcdBytes[leading] == 0; leading--) ;
+ precision = leading + 1;
+
+ // Switch storage mechanism if possible
+ if (precision <= 16) {
+ switchStorage();
+ }
+
+ } else {
+ if (bcdLong == 0L) {
+ // Number is zero
+ setBcdToZero();
+ return;
+ }
+
+ // Compact the number (remove trailing zeros)
+ int delta = Long.numberOfTrailingZeros(bcdLong) / 4;
+ bcdLong >>>= delta * 4;
+ scale += delta;
+
+ // Compute precision
+ precision = 16 - (Long.numberOfLeadingZeros(bcdLong) / 4);
+ }
+ }
+
+ /** Ensure that a byte array of at least 40 digits is allocated. */
+ private void ensureCapacity() {
+ ensureCapacity(40);
+ }
+
+ private void ensureCapacity(int capacity) {
+ if (capacity == 0) return;
+ if (bcdBytes == null) {
+ bcdBytes = new byte[capacity];
+ } else if (bcdBytes.length < capacity) {
+ byte[] bcd1 = new byte[capacity * 2];
+ System.arraycopy(bcdBytes, 0, bcd1, 0, bcdBytes.length);
+ bcdBytes = bcd1;
+ }
+ }
+
+ /** Switches the internal storage mechanism between the 64-bit long and the byte array. */
+ private void switchStorage() {
+ if (usingBytes) {
+ // Change from bytes to long
+ bcdLong = 0L;
+ for (int i = precision - 1; i >= 0; i--) {
+ bcdLong <<= 4;
+ bcdLong |= bcdBytes[i];
+ bcdBytes[i] = 0;
+ }
+ usingBytes = false;
+ } else {
+ // Change from long to bytes
+ ensureCapacity();
+ for (int i = 0; i < precision; i++) {
+ bcdBytes[i] = (byte) (bcdLong & 0xf);
+ bcdLong >>>= 4;
+ }
+ usingBytes = true;
+ }
+ }
+
+ @Override
+ protected void copyBcdFrom(FormatQuantity _other) {
+ FormatQuantity4 other = (FormatQuantity4) _other;
+ if (other.usingBytes) {
+ usingBytes = true;
+ ensureCapacity(other.precision);
+ System.arraycopy(other.bcdBytes, 0, bcdBytes, 0, other.precision);
+ } else {
+ usingBytes = false;
+ bcdLong = other.bcdLong;
+ }
+ }
+
+ /**
+ * Checks whether the bytes stored in this instance are all valid. For internal unit testing only.
+ *
+ * @return An error message if this instance is invalid, or null if this instance is healthy.
+ * @deprecated This API is for ICU internal use only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public String checkHealth() {
+ if (usingBytes) {
+ if (bcdLong != 0) return "Value in bcdLong but we are in byte mode";
+ if (precision == 0) return "Zero precision but we are in byte mode";
+ if (precision > bcdBytes.length) return "Precision exceeds length of byte array";
+ if (getDigitPos(precision - 1) == 0) return "Most significant digit is zero in byte mode";
+ if (getDigitPos(0) == 0) return "Least significant digit is zero in long mode";
+ for (int i = 0; i < precision; i++) {
+ if (getDigitPos(i) >= 10) return "Digit exceeding 10 in byte array";
+ if (getDigitPos(i) < 0) return "Digit below 0 in byte array";
+ }
+ for (int i = precision; i < bcdBytes.length; i++) {
+ if (getDigitPos(i) != 0) return "Nonzero digits outside of range in byte array";
+ }
+ } else {
+ if (bcdBytes != null) {
+ for (int i = 0; i < bcdBytes.length; i++) {
+ if (bcdBytes[i] != 0) return "Nonzero digits in byte array but we are in long mode";
+ }
+ }
+ if (precision == 0 && bcdLong != 0) return "Value in bcdLong even though precision is zero";
+ if (precision > 16) return "Precision exceeds length of long";
+ if (precision != 0 && getDigitPos(precision - 1) == 0)
+ return "Most significant digit is zero in long mode";
+ if (precision != 0 && getDigitPos(0) == 0)
+ return "Least significant digit is zero in long mode";
+ for (int i = 0; i < precision; i++) {
+ if (getDigitPos(i) >= 10) return "Digit exceeding 10 in long";
+ if (getDigitPos(i) < 0) return "Digit below 0 in long (?!)";
+ }
+ for (int i = precision; i < 16; i++) {
+ if (getDigitPos(i) != 0) return "Nonzero digits outside of range in long";
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks whether this {@link FormatQuantity4} is using its internal byte array storage mechanism.
+ *
+ * @return true if an internal byte array is being used; false if a long is being used.
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public boolean usingBytes() {
+ return usingBytes;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ if (usingBytes) {
+ for (int i = precision - 1; i >= 0; i--) {
+ sb.append(bcdBytes[i]);
+ }
+ } else {
+ sb.append(Long.toHexString(bcdLong));
+ }
+ return String.format(
+ "<FormatQuantity4 %s:%d:%d:%s %s %s%s%d>",
+ (lOptPos > 1000 ? "max" : String.valueOf(lOptPos)),
+ lReqPos,
+ rReqPos,
+ (rOptPos < -1000 ? "min" : String.valueOf(rOptPos)),
+ (usingBytes ? "bytes" : "long"),
+ sb,
+ "E",
+ scale);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantityBCD.java b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantityBCD.java
new file mode 100644
index 000000000..11eaffcd3
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantityBCD.java
@@ -0,0 +1,919 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.text.FieldPosition;
+
+import android.icu.impl.StandardPlural;
+import android.icu.text.PluralRules;
+import android.icu.text.PluralRules.Operand;
+import android.icu.text.UFieldPosition;
+
+/**
+ * Represents numbers and digit display properties using Binary Coded Decimal (BCD).
+ *
+ * @implements {@link FormatQuantity}
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public abstract class FormatQuantityBCD implements FormatQuantity {
+
+ /**
+ * The power of ten corresponding to the least significant digit in the BCD. For example, if this
+ * object represents the number "3.14", the BCD will be "0x314" and the scale will be -2.
+ *
+ * <p>Note that in {@link java.math.BigDecimal}, the scale is defined differently: the number of
+ * digits after the decimal place, which is the negative of our definition of scale.
+ */
+ protected int scale;
+
+ /**
+ * The number of digits in the BCD. For example, "1007" has BCD "0x1007" and precision 4. The
+ * maximum precision is 16 since a long can hold only 16 digits.
+ *
+ * <p>This value must be re-calculated whenever the value in bcd changes by using {@link
+ * #computePrecisionAndCompact()}.
+ */
+ protected int precision;
+
+ /**
+ * A bitmask of properties relating to the number represented by this object.
+ *
+ * @see #NEGATIVE_FLAG
+ * @see #INFINITY_FLAG
+ * @see #NAN_FLAG
+ */
+ protected int flags;
+
+ protected static final int NEGATIVE_FLAG = 1;
+ protected static final int INFINITY_FLAG = 2;
+ protected static final int NAN_FLAG = 4;
+
+ // The following three fields relate to the double-to-ascii fast path algorithm.
+ // When a double is given to FormatQuantityBCD, it is converted to using a fast algorithm. The
+ // fast algorithm guarantees correctness to only the first ~12 digits of the double. The process
+ // of rounding the number ensures that the converted digits are correct, falling back to a slow-
+ // path algorithm if required. Therefore, if a FormatQuantity is constructed from a double, it
+ // is *required* that roundToMagnitude(), roundToIncrement(), or roundToInfinity() is called. If
+ // you don't round, assertions will fail in certain other methods if you try calling them.
+
+ /**
+ * The original number provided by the user and which is represented in BCD. Used when we need to
+ * re-compute the BCD for an exact double representation.
+ */
+ protected double origDouble;
+
+ /**
+ * The change in magnitude relative to the original double. Used when we need to re-compute the
+ * BCD for an exact double representation.
+ */
+ protected int origDelta;
+
+ /**
+ * Whether the value in the BCD comes from the double fast path without having been rounded to
+ * ensure correctness
+ */
+ protected boolean isApproximate;
+
+ // Four positions: left optional '(', left required '[', right required ']', right optional ')'.
+ // These four positions determine which digits are displayed in the output string. They do NOT
+ // affect rounding. These positions are internal-only and can be specified only by the public
+ // endpoints like setFractionLength, setIntegerLength, and setSignificantDigits, among others.
+ //
+ // * Digits between lReqPos and rReqPos are in the "required zone" and are always displayed.
+ // * Digits between lOptPos and rOptPos but outside the required zone are in the "optional zone"
+ // and are displayed unless they are trailing off the left or right edge of the number and
+ // have a numerical value of zero. In order to be "trailing", the digits need to be beyond
+ // the decimal point in their respective directions.
+ // * Digits outside of the "optional zone" are never displayed.
+ //
+ // See the table below for illustrative examples.
+ //
+ // +---------+---------+---------+---------+------------+------------------------+--------------+
+ // | lOptPos | lReqPos | rReqPos | rOptPos | number | positions | en-US string |
+ // +---------+---------+---------+---------+------------+------------------------+--------------+
+ // | 5 | 2 | -1 | -5 | 1234.567 | ( 12[34.5]67 ) | 1,234.567 |
+ // | 3 | 2 | -1 | -5 | 1234.567 | 1(2[34.5]67 ) | 234.567 |
+ // | 3 | 2 | -1 | -2 | 1234.567 | 1(2[34.5]6)7 | 234.56 |
+ // | 6 | 4 | 2 | -5 | 123456789. | 123(45[67]89. ) | 456,789. |
+ // | 6 | 4 | 2 | 1 | 123456789. | 123(45[67]8)9. | 456,780. |
+ // | -1 | -1 | -3 | -4 | 0.123456 | 0.1([23]4)56 | .0234 |
+ // | 6 | 4 | -2 | -2 | 12.3 | ( [ 12.3 ]) | 0012.30 |
+ // +---------+---------+---------+---------+------------+------------------------+--------------+
+ //
+ protected int lOptPos = Integer.MAX_VALUE;
+ protected int lReqPos = 0;
+ protected int rReqPos = 0;
+ protected int rOptPos = Integer.MIN_VALUE;
+
+ @Override
+ public void copyFrom(FormatQuantity _other) {
+ copyBcdFrom(_other);
+ FormatQuantityBCD other = (FormatQuantityBCD) _other;
+ lOptPos = other.lOptPos;
+ lReqPos = other.lReqPos;
+ rReqPos = other.rReqPos;
+ rOptPos = other.rOptPos;
+ scale = other.scale;
+ precision = other.precision;
+ flags = other.flags;
+ origDouble = other.origDouble;
+ origDelta = other.origDelta;
+ isApproximate = other.isApproximate;
+ }
+
+ public FormatQuantityBCD clear() {
+ lOptPos = Integer.MAX_VALUE;
+ lReqPos = 0;
+ rReqPos = 0;
+ rOptPos = Integer.MIN_VALUE;
+ flags = 0;
+ setBcdToZero(); // sets scale, precision, hasDouble, origDouble, origDelta, and BCD data
+ return this;
+ }
+
+ @Override
+ public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac) {
+ // Validation should happen outside of FormatQuantity, e.g., in the Rounder class.
+ assert minInt >= 0;
+ assert maxInt >= minInt;
+ assert minFrac >= 0;
+ assert maxFrac >= minFrac;
+
+ // Save values into internal state
+ // Negation is safe for minFrac/maxFrac because -Integer.MAX_VALUE > Integer.MIN_VALUE
+ lOptPos = maxInt;
+ lReqPos = minInt;
+ rReqPos = -minFrac;
+ rOptPos = -maxFrac;
+ }
+
+ @Override
+ public long getPositionFingerprint() {
+ long fingerprint = 0;
+ fingerprint ^= lOptPos;
+ fingerprint ^= (lReqPos << 16);
+ fingerprint ^= ((long) rReqPos << 32);
+ fingerprint ^= ((long) rOptPos << 48);
+ return fingerprint;
+ }
+
+ @Override
+ public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext) {
+ // TODO: Avoid converting back and forth to BigDecimal.
+ BigDecimal temp = toBigDecimal();
+ temp =
+ temp.divide(roundingInterval, 0, mathContext.getRoundingMode())
+ .multiply(roundingInterval)
+ .round(mathContext);
+ if (temp.signum() == 0) {
+ setBcdToZero(); // keeps negative flag for -0.0
+ } else {
+ setToBigDecimal(temp);
+ }
+ }
+
+ @Override
+ public void multiplyBy(BigDecimal multiplicand) {
+ BigDecimal temp = toBigDecimal();
+ temp = temp.multiply(multiplicand);
+ setToBigDecimal(temp);
+ }
+
+ @Override
+ public int getMagnitude() throws ArithmeticException {
+ if (precision == 0) {
+ throw new ArithmeticException("Magnitude is not well-defined for zero");
+ } else {
+ return scale + precision - 1;
+ }
+ }
+
+ @Override
+ public void adjustMagnitude(int delta) {
+ if (precision != 0) {
+ scale += delta;
+ origDelta += delta;
+ }
+ }
+
+ @Override
+ public StandardPlural getStandardPlural(PluralRules rules) {
+ if (rules == null) {
+ // Fail gracefully if the user didn't provide a PluralRules
+ return StandardPlural.OTHER;
+ } else {
+ @SuppressWarnings("deprecation")
+ String ruleString = rules.select(this);
+ return StandardPlural.orOtherFromString(ruleString);
+ }
+ }
+
+ @Override
+ public double getPluralOperand(Operand operand) {
+ // If this assertion fails, you need to call roundToInfinity() or some other rounding method.
+ // See the comment at the top of this file explaining the "isApproximate" field.
+ assert !isApproximate;
+
+ switch (operand) {
+ case i:
+ return toLong();
+ case f:
+ return toFractionLong(true);
+ case t:
+ return toFractionLong(false);
+ case v:
+ return fractionCount();
+ case w:
+ return fractionCountWithoutTrailingZeros();
+ default:
+ return Math.abs(toDouble());
+ }
+ }
+
+ /**
+ * If the given {@link FieldPosition} is a {@link UFieldPosition}, populates it with the fraction
+ * length and fraction long value. If the argument is not a {@link UFieldPosition}, nothing
+ * happens.
+ *
+ * @param fp The {@link UFieldPosition} to populate.
+ */
+ public void populateUFieldPosition(FieldPosition fp) {
+ if (fp instanceof UFieldPosition) {
+ ((UFieldPosition) fp)
+ .setFractionDigits((int) getPluralOperand(Operand.v), (long) getPluralOperand(Operand.f));
+ }
+ }
+
+ @Override
+ public int getUpperDisplayMagnitude() {
+ // If this assertion fails, you need to call roundToInfinity() or some other rounding method.
+ // See the comment at the top of this file explaining the "isApproximate" field.
+ assert !isApproximate;
+
+ int magnitude = scale + precision;
+ int result = (lReqPos > magnitude) ? lReqPos : (lOptPos < magnitude) ? lOptPos : magnitude;
+ return result - 1;
+ }
+
+ @Override
+ public int getLowerDisplayMagnitude() {
+ // If this assertion fails, you need to call roundToInfinity() or some other rounding method.
+ // See the comment at the top of this file explaining the "isApproximate" field.
+ assert !isApproximate;
+
+ int magnitude = scale;
+ int result = (rReqPos < magnitude) ? rReqPos : (rOptPos > magnitude) ? rOptPos : magnitude;
+ return result;
+ }
+
+ @Override
+ public byte getDigit(int magnitude) {
+ // If this assertion fails, you need to call roundToInfinity() or some other rounding method.
+ // See the comment at the top of this file explaining the "isApproximate" field.
+ assert !isApproximate;
+
+ return getDigitPos(magnitude - scale);
+ }
+
+ private int fractionCount() {
+ return -getLowerDisplayMagnitude();
+ }
+
+ private int fractionCountWithoutTrailingZeros() {
+ return Math.max(-scale, 0);
+ }
+
+ @Override
+ public boolean isNegative() {
+ return (flags & NEGATIVE_FLAG) != 0;
+ }
+
+ @Override
+ public boolean isInfinite() {
+ return (flags & INFINITY_FLAG) != 0;
+ }
+
+ @Override
+ public boolean isNaN() {
+ return (flags & NAN_FLAG) != 0;
+ }
+
+ @Override
+ public boolean isZero() {
+ return precision == 0;
+ }
+
+ @Override
+ public FormatQuantity createCopy() {
+ if (this instanceof FormatQuantity2) {
+ return new FormatQuantity2((FormatQuantity2) this);
+ } else if (this instanceof FormatQuantity3) {
+ return new FormatQuantity3((FormatQuantity3) this);
+ } else if (this instanceof FormatQuantity4) {
+ return new FormatQuantity4((FormatQuantity4) this);
+ } else {
+ throw new IllegalArgumentException("Don't know how to copy " + this.getClass());
+ }
+ }
+
+ public void setToInt(int n) {
+ setBcdToZero();
+ flags = 0;
+ if (n < 0) {
+ flags |= NEGATIVE_FLAG;
+ n = -n;
+ }
+ if (n != 0) {
+ _setToInt(n);
+ compact();
+ }
+ }
+
+ private void _setToInt(int n) {
+ if (n == Integer.MIN_VALUE) {
+ readLongToBcd(-(long) n);
+ } else {
+ readIntToBcd(n);
+ }
+ }
+
+ public void setToLong(long n) {
+ setBcdToZero();
+ flags = 0;
+ if (n < 0) {
+ flags |= NEGATIVE_FLAG;
+ n = -n;
+ }
+ if (n != 0) {
+ _setToLong(n);
+ compact();
+ }
+ }
+
+ private void _setToLong(long n) {
+ if (n == Long.MIN_VALUE) {
+ readBigIntegerToBcd(BigInteger.valueOf(n).negate());
+ } else if (n <= Integer.MAX_VALUE) {
+ readIntToBcd((int) n);
+ } else {
+ readLongToBcd(n);
+ }
+ }
+
+ public void setToBigInteger(BigInteger n) {
+ setBcdToZero();
+ flags = 0;
+ if (n.signum() == -1) {
+ flags |= NEGATIVE_FLAG;
+ n = n.negate();
+ }
+ if (n.signum() != 0) {
+ _setToBigInteger(n);
+ compact();
+ }
+ }
+
+ private void _setToBigInteger(BigInteger n) {
+ if (n.bitLength() < 32) {
+ readIntToBcd(n.intValue());
+ } else if (n.bitLength() < 64) {
+ readLongToBcd(n.longValue());
+ } else {
+ readBigIntegerToBcd(n);
+ }
+ }
+
+ /**
+ * Sets the internal BCD state to represent the value in the given double.
+ *
+ * @param n The value to consume.
+ */
+ public void setToDouble(double n) {
+ setBcdToZero();
+ flags = 0;
+ // Double.compare() handles +0.0 vs -0.0
+ if (Double.compare(n, 0.0) < 0) {
+ flags |= NEGATIVE_FLAG;
+ n = -n;
+ }
+ if (Double.isNaN(n)) {
+ flags |= NAN_FLAG;
+ } else if (Double.isInfinite(n)) {
+ flags |= INFINITY_FLAG;
+ } else if (n != 0) {
+ _setToDoubleFast(n);
+ compact();
+ }
+ }
+
+ private static final double[] DOUBLE_MULTIPLIERS = {
+ 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16,
+ 1e17, 1e18, 1e19, 1e20, 1e21
+ };
+
+ /**
+ * Uses double multiplication and division to get the number into integer space before converting
+ * to digits. Since double arithmetic is inexact, the resulting digits may not be accurate.
+ */
+ private void _setToDoubleFast(double n) {
+ long ieeeBits = Double.doubleToLongBits(n);
+ int exponent = (int) ((ieeeBits & 0x7ff0000000000000L) >> 52) - 0x3ff;
+
+ // Not all integers can be represented exactly for exponent > 52
+ if (exponent <= 52 && (long) n == n) {
+ _setToLong((long) n);
+ return;
+ }
+
+ isApproximate = true;
+ origDouble = n;
+ origDelta = 0;
+
+ // 3.3219... is log2(10)
+ int fracLength = (int) ((52 - exponent) / 3.32192809489);
+ if (fracLength >= 0) {
+ int i = fracLength;
+ // 1e22 is the largest exact double.
+ for (; i >= 22; i -= 22) n *= 1e22;
+ n *= DOUBLE_MULTIPLIERS[i];
+ } else {
+ int i = fracLength;
+ // 1e22 is the largest exact double.
+ for (; i <= -22; i += 22) n /= 1e22;
+ n /= DOUBLE_MULTIPLIERS[-i];
+ }
+ long result = Math.round(n);
+ if (result != 0) {
+ _setToLong(result);
+ scale -= fracLength;
+ }
+ }
+
+ /**
+ * Uses Double.toString() to obtain an exact accurate representation of the double, overwriting it
+ * into the BCD. This method can be called at any point after {@link #_setToDoubleFast} while
+ * {@link #isApproximate} is still true.
+ */
+ private void convertToAccurateDouble() {
+ double n = origDouble;
+ assert n != 0;
+ int delta = origDelta;
+ setBcdToZero();
+
+ // Call the slow oracle function
+ String temp = Double.toString(n);
+
+ if (temp.indexOf('E') != -1) {
+ // Case 1: Exponential notation.
+ assert temp.indexOf('.') == 1;
+ int expPos = temp.indexOf('E');
+ _setToLong(Long.parseLong(temp.charAt(0) + temp.substring(2, expPos)));
+ scale += Integer.parseInt(temp.substring(expPos + 1)) - (expPos - 1) + 1;
+ } else if (temp.charAt(0) == '0') {
+ // Case 2: Fraction-only number.
+ assert temp.indexOf('.') == 1;
+ _setToLong(Long.parseLong(temp.substring(2)));
+ scale += 2 - temp.length();
+ } else if (temp.charAt(temp.length() - 1) == '0') {
+ // Case 3: Integer-only number.
+ // Note: this path should not normally happen, because integer-only numbers are captured
+ // before the approximate double logic is performed.
+ assert temp.indexOf('.') == temp.length() - 2;
+ assert temp.length() - 2 <= 18;
+ _setToLong(Long.parseLong(temp.substring(0, temp.length() - 2)));
+ // no need to adjust scale
+ } else {
+ // Case 4: Number with both a fraction and an integer.
+ int decimalPos = temp.indexOf('.');
+ _setToLong(Long.parseLong(temp.substring(0, decimalPos) + temp.substring(decimalPos + 1)));
+ scale += decimalPos - temp.length() + 1;
+ }
+ scale += delta;
+ compact();
+ explicitExactDouble = true;
+ }
+
+ /**
+ * Whether this {@link FormatQuantity4} has been explicitly converted to an exact double. true if
+ * backed by a double that was explicitly converted via convertToAccurateDouble; false otherwise.
+ * Used for testing.
+ *
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated public boolean explicitExactDouble = false;
+
+ /**
+ * Sets the internal BCD state to represent the value in the given BigDecimal.
+ *
+ * @param n The value to consume.
+ */
+ public void setToBigDecimal(BigDecimal n) {
+ setBcdToZero();
+ flags = 0;
+ if (n.signum() == -1) {
+ flags |= NEGATIVE_FLAG;
+ n = n.negate();
+ }
+ if (n.signum() != 0) {
+ _setToBigDecimal(n);
+ compact();
+ }
+ }
+
+ private void _setToBigDecimal(BigDecimal n) {
+ int fracLength = n.scale();
+ n = n.scaleByPowerOfTen(fracLength);
+ BigInteger bi = n.toBigInteger();
+ _setToBigInteger(bi);
+ scale -= fracLength;
+ }
+
+ /**
+ * Returns a long approximating the internal BCD. A long can only represent the integral part of
+ * the number.
+ *
+ * @return A double representation of the internal BCD.
+ */
+ protected long toLong() {
+ long result = 0L;
+ for (int magnitude = scale + precision - 1; magnitude >= 0; magnitude--) {
+ result = result * 10 + getDigitPos(magnitude - scale);
+ }
+ return result;
+ }
+
+ /**
+ * 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.
+ */
+ protected long toFractionLong(boolean includeTrailingZeros) {
+ long result = 0L;
+ int magnitude = -1;
+ for (;
+ (magnitude >= scale || (includeTrailingZeros && magnitude >= rReqPos))
+ && magnitude >= rOptPos;
+ magnitude--) {
+ result = result * 10 + getDigitPos(magnitude - scale);
+ }
+ return result;
+ }
+
+ /**
+ * Returns a double approximating the internal BCD. The double may not retain all of the
+ * information encoded in the BCD if the BCD represents a number out of range of a double.
+ *
+ * @return A double representation of the internal BCD.
+ */
+ @Override
+ public double toDouble() {
+ if (isApproximate) {
+ return toDoubleFromOriginal();
+ }
+
+ if (isNaN()) {
+ return Double.NaN;
+ } else if (isInfinite()) {
+ return isNegative() ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
+ }
+
+ long tempLong = 0L;
+ int lostDigits = precision - Math.min(precision, 17);
+ for (int shift = precision - 1; shift >= lostDigits; shift--) {
+ tempLong = tempLong * 10 + getDigitPos(shift);
+ }
+ double result = tempLong;
+ int _scale = scale + lostDigits;
+ if (_scale >= 0) {
+ // 1e22 is the largest exact double.
+ int i = _scale;
+ for (; i >= 22; i -= 22) result *= 1e22;
+ result *= DOUBLE_MULTIPLIERS[i];
+ } else {
+ // 1e22 is the largest exact double.
+ int i = _scale;
+ for (; i <= -22; i += 22) result /= 1e22;
+ result /= DOUBLE_MULTIPLIERS[-i];
+ }
+ if (isNegative()) result = -result;
+ return result;
+ }
+
+ @Override
+ public BigDecimal toBigDecimal() {
+ if (isApproximate) {
+ // Converting to a BigDecimal requires Double.toString().
+ convertToAccurateDouble();
+ }
+ return bcdToBigDecimal();
+ }
+
+ protected double toDoubleFromOriginal() {
+ double result = origDouble;
+ int delta = origDelta;
+ if (delta >= 0) {
+ // 1e22 is the largest exact double.
+ for (; delta >= 22; delta -= 22) result *= 1e22;
+ result *= DOUBLE_MULTIPLIERS[delta];
+ } else {
+ // 1e22 is the largest exact double.
+ for (; delta <= -22; delta += 22) result /= 1e22;
+ result /= DOUBLE_MULTIPLIERS[-delta];
+ }
+ if (isNegative()) result *= -1;
+ return result;
+ }
+
+ private static int safeSubtract(int a, int b) {
+ int diff = a - b;
+ if (b < 0 && diff < a) return Integer.MAX_VALUE;
+ if (b > 0 && diff > a) return Integer.MIN_VALUE;
+ return diff;
+ }
+
+ @Override
+ public void roundToMagnitude(int magnitude, MathContext mathContext) {
+ // The position in the BCD at which rounding will be performed; digits to the right of position
+ // will be rounded away.
+ // TODO: Andy: There was a test failure because of integer overflow here. Should I do
+ // "safe subtraction" everywhere in the code? What's the nicest way to do it?
+ int position = safeSubtract(magnitude, scale);
+
+ // Enforce the number of digits required by the MathContext.
+ int _mcPrecision = mathContext.getPrecision();
+ if (magnitude == Integer.MAX_VALUE
+ || (_mcPrecision > 0 && precision - position > _mcPrecision)) {
+ position = precision - _mcPrecision;
+ }
+
+ if (position <= 0 && !isApproximate) {
+ // All digits are to the left of the rounding magnitude.
+ } else if (precision == 0) {
+ // No rounding for zero.
+ } else {
+ // Perform rounding logic.
+ // "leading" = most significant digit to the right of rounding
+ // "trailing" = least significant digit to the left of rounding
+ byte leadingDigit = getDigitPos(safeSubtract(position, 1));
+ byte trailingDigit = getDigitPos(position);
+
+ // Compute which section of the number we are in.
+ // EDGE means we are at the bottom or top edge, like 1.000 or 1.999 (used by doubles)
+ // LOWER means we are between the bottom edge and the midpoint, like 1.391
+ // MIDPOINT means we are exactly in the middle, like 1.500
+ // UPPER means we are between the midpoint and the top edge, like 1.916
+ int section = RoundingUtils.SECTION_MIDPOINT;
+ if (!isApproximate) {
+ if (leadingDigit < 5) {
+ section = RoundingUtils.SECTION_LOWER;
+ } else if (leadingDigit > 5) {
+ section = RoundingUtils.SECTION_UPPER;
+ } else {
+ for (int p = safeSubtract(position, 2); p >= 0; p--) {
+ if (getDigitPos(p) != 0) {
+ section = RoundingUtils.SECTION_UPPER;
+ break;
+ }
+ }
+ }
+ } else {
+ int p = safeSubtract(position, 2);
+ int minP = Math.max(0, precision - 14);
+ if (leadingDigit == 0) {
+ section = -1;
+ for (; p >= minP; p--) {
+ if (getDigitPos(p) != 0) {
+ section = RoundingUtils.SECTION_LOWER;
+ break;
+ }
+ }
+ } else if (leadingDigit == 4) {
+ for (; p >= minP; p--) {
+ if (getDigitPos(p) != 9) {
+ section = RoundingUtils.SECTION_LOWER;
+ break;
+ }
+ }
+ } else if (leadingDigit == 5) {
+ for (; p >= minP; p--) {
+ if (getDigitPos(p) != 0) {
+ section = RoundingUtils.SECTION_UPPER;
+ break;
+ }
+ }
+ } else if (leadingDigit == 9) {
+ section = -2;
+ for (; p >= minP; p--) {
+ if (getDigitPos(p) != 9) {
+ section = RoundingUtils.SECTION_UPPER;
+ break;
+ }
+ }
+ } else if (leadingDigit < 5) {
+ section = RoundingUtils.SECTION_LOWER;
+ } else {
+ section = RoundingUtils.SECTION_UPPER;
+ }
+
+ boolean roundsAtMidpoint =
+ RoundingUtils.roundsAtMidpoint(mathContext.getRoundingMode().ordinal());
+ if (safeSubtract(position, 1) < precision - 14
+ || (roundsAtMidpoint && section == RoundingUtils.SECTION_MIDPOINT)
+ || (!roundsAtMidpoint && section < 0 /* i.e. at upper or lower edge */)) {
+ // Oops! This means that we have to get the exact representation of the double, because
+ // the zone of uncertainty is along the rounding boundary.
+ convertToAccurateDouble();
+ roundToMagnitude(magnitude, mathContext); // start over
+ return;
+ }
+
+ // Turn off the approximate double flag, since the value is now confirmed to be exact.
+ isApproximate = false;
+ origDouble = 0.0;
+ origDelta = 0;
+
+ if (position <= 0) {
+ // All digits are to the left of the rounding magnitude.
+ return;
+ }
+
+ // Good to continue rounding.
+ if (section == -1) section = RoundingUtils.SECTION_LOWER;
+ if (section == -2) section = RoundingUtils.SECTION_UPPER;
+ }
+
+ boolean roundDown =
+ RoundingUtils.getRoundingDirection(
+ (trailingDigit % 2) == 0,
+ isNegative(),
+ section,
+ mathContext.getRoundingMode().ordinal(),
+ this);
+
+ // Perform truncation
+ if (position >= precision) {
+ setBcdToZero();
+ scale = magnitude;
+ } else {
+ shiftRight(position);
+ }
+
+ // Bubble the result to the higher digits
+ if (!roundDown) {
+ if (trailingDigit == 9) {
+ int bubblePos = 0;
+ // Note: in the long implementation, the most digits BCD can have at this point is 15,
+ // so bubblePos <= 15 and getDigitPos(bubblePos) is safe.
+ for (; getDigitPos(bubblePos) == 9; bubblePos++) {}
+ shiftRight(bubblePos); // shift off the trailing 9s
+ }
+ byte digit0 = getDigitPos(0);
+ assert digit0 != 9;
+ setDigitPos(0, (byte) (digit0 + 1));
+ precision += 1; // in case an extra digit got added
+ }
+
+ compact();
+ }
+ }
+
+ @Override
+ public void roundToInfinity() {
+ if (isApproximate) {
+ convertToAccurateDouble();
+ }
+ }
+
+ /**
+ * Appends a digit, optionally with one or more leading zeros, to the end of the value represented
+ * by this FormatQuantity.
+ *
+ * <p>The primary use of this method is to construct numbers during a parsing loop. It allows
+ * parsing to take advantage of the digit list infrastructure primarily designed for formatting.
+ *
+ * @param value The digit to append.
+ * @param leadingZeros The number of zeros to append before the digit. For example, if the value
+ * in this instance starts as 12.3, and you append a 4 with 1 leading zero, the value becomes
+ * 12.304.
+ * @param appendAsInteger If true, increase the magnitude of existing digits to make room for the
+ * new digit. If false, append to the end like a fraction digit. If true, there must not be
+ * any fraction digits already in the number.
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public void appendDigit(byte value, int leadingZeros, boolean appendAsInteger) {
+ assert leadingZeros >= 0;
+
+ // Zero requires special handling to maintain the invariant that the least-significant digit
+ // in the BCD is nonzero.
+ if (value == 0) {
+ if (appendAsInteger && precision != 0) {
+ scale += leadingZeros + 1;
+ }
+ return;
+ }
+
+ // Deal with trailing zeros
+ if (scale > 0) {
+ leadingZeros += scale;
+ if (appendAsInteger) {
+ scale = 0;
+ }
+ }
+
+ // Append digit
+ shiftLeft(leadingZeros + 1);
+ setDigitPos(0, value);
+
+ // Fix scale if in integer mode
+ if (appendAsInteger) {
+ scale += leadingZeros + 1;
+ }
+ }
+
+ /**
+ * Returns a single digit from the BCD list. No internal state is changed by calling this method.
+ *
+ * @param position The position of the digit to pop, counted in BCD units from the least
+ * significant digit. If outside the range supported by the implementation, zero is returned.
+ * @return The digit at the specified location.
+ */
+ protected abstract byte getDigitPos(int position);
+
+ /**
+ * Sets the digit in the BCD list. This method only sets the digit; it is the caller's
+ * responsibility to call {@link #compact} after setting the digit.
+ *
+ * @param position The position of the digit to pop, counted in BCD units from the least
+ * significant digit. If outside the range supported by the implementation, an AssertionError
+ * is thrown.
+ * @param value The digit to set at the specified location.
+ */
+ protected abstract void setDigitPos(int position, byte value);
+
+ /**
+ * Adds zeros to the end of the BCD list. This will result in an invalid BCD representation; it is
+ * the caller's responsibility to do further manipulation and then call {@link #compact}.
+ *
+ * @param numDigits The number of zeros to add.
+ */
+ protected abstract void shiftLeft(int numDigits);
+
+ protected abstract void shiftRight(int numDigits);
+
+ /**
+ * Sets the internal representation to zero. Clears any values stored in scale, precision,
+ * hasDouble, origDouble, origDelta, and BCD data.
+ */
+ protected abstract void setBcdToZero();
+
+ /**
+ * Sets the internal BCD state to represent the value in the given int. The int is guaranteed to
+ * be either positive. The internal state is guaranteed to be empty when this method is called.
+ *
+ * @param n The value to consume.
+ */
+ protected abstract void readIntToBcd(int input);
+
+ /**
+ * Sets the internal BCD state to represent the value in the given long. The long is guaranteed to
+ * be either positive. The internal state is guaranteed to be empty when this method is called.
+ *
+ * @param n The value to consume.
+ */
+ protected abstract void readLongToBcd(long input);
+
+ /**
+ * Sets the internal BCD state to represent the value in the given BigInteger. The BigInteger is
+ * guaranteed to be positive, and it is guaranteed to be larger than Long.MAX_VALUE. The internal
+ * state is guaranteed to be empty when this method is called.
+ *
+ * @param n The value to consume.
+ */
+ protected abstract void readBigIntegerToBcd(BigInteger input);
+
+ /**
+ * Returns a BigDecimal encoding the internal BCD value.
+ *
+ * @return A BigDecimal representation of the internal BCD.
+ */
+ protected abstract BigDecimal bcdToBigDecimal();
+
+ protected abstract void copyBcdFrom(FormatQuantity _other);
+
+ /**
+ * Removes trailing zeros from the BCD (adjusting the scale as required) and then computes the
+ * precision. The precision is the number of digits in the number up through the greatest nonzero
+ * digit.
+ *
+ * <p>This method must always be called when bcd changes in order for assumptions to be correct in
+ * methods like {@link #fractionCount()}.
+ */
+ protected abstract void compact();
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantitySelector.java b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantitySelector.java
new file mode 100644
index 000000000..c82ca8f48
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/FormatQuantitySelector.java
@@ -0,0 +1,54 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/** @author sffc
+ * @hide Only a subset of ICU is exposed in Android*/
+public class FormatQuantitySelector {
+ public static FormatQuantityBCD from(int input) {
+ return new FormatQuantity4(input);
+ }
+
+ public static FormatQuantityBCD from(long input) {
+ return new FormatQuantity4(input);
+ }
+
+ public static FormatQuantityBCD from(double input) {
+ return new FormatQuantity4(input);
+ }
+
+ public static FormatQuantityBCD from(BigInteger input) {
+ return new FormatQuantity4(input);
+ }
+
+ public static FormatQuantityBCD from(BigDecimal input) {
+ return new FormatQuantity4(input);
+ }
+
+ public static FormatQuantityBCD from(android.icu.math.BigDecimal input) {
+ return from(input.toBigDecimal());
+ }
+
+ public static FormatQuantityBCD from(Number number) {
+ if (number instanceof Long) {
+ return from(number.longValue());
+ } else if (number instanceof Integer) {
+ return from(number.intValue());
+ } else if (number instanceof Double) {
+ return from(number.doubleValue());
+ } else if (number instanceof BigInteger) {
+ return from((BigInteger) number);
+ } else if (number instanceof BigDecimal) {
+ return from((BigDecimal) number);
+ } else if (number instanceof android.icu.math.BigDecimal) {
+ return from((android.icu.math.BigDecimal) number);
+ } else {
+ throw new IllegalArgumentException(
+ "Number is of an unsupported type: " + number.getClass().getName());
+ }
+ }
+}
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
new file mode 100644
index 000000000..6fa3c5e6a
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/Modifier.java
@@ -0,0 +1,130 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import android.icu.impl.StandardPlural;
+import android.icu.impl.number.modifiers.ConstantAffixModifier;
+import android.icu.impl.number.modifiers.GeneralPluralModifier;
+import android.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
+import android.icu.impl.number.modifiers.SimpleModifier;
+
+/**
+ * A Modifier is an immutable object that can be passed through the formatting pipeline until it is
+ * finally applied to the string builder. A Modifier usually contains a prefix and a suffix that are
+ * applied, but it could contain something else, like a {@link android.icu.text.SimpleFormatter}
+ * pattern.
+ *
+ * @see PositiveNegativeAffixModifier
+ * @see ConstantAffixModifier
+ * @see GeneralPluralModifier
+ * @see SimpleModifier
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public interface Modifier {
+
+ /**
+ * Apply this Modifier to the string builder.
+ *
+ * @param output The string builder to which to apply this modifier.
+ * @param leftIndex The left index of the string within the builder. Equal to 0 when only one
+ * number is being formatted.
+ * @param rightIndex The right index of the string within the string builder. Equal to length-1
+ * when only one number is being formatted.
+ * @return The number of characters (UTF-16 code units) that were added to the string builder.
+ */
+ public int apply(NumberStringBuilder output, int leftIndex, int rightIndex);
+
+ /**
+ * The number of characters that {@link #apply} would add to the string builder.
+ *
+ * @return The number of characters (UTF-16 code units) that would be added to a string builder.
+ */
+ public int length();
+
+ /**
+ * Whether this modifier is strong. If a modifier is strong, it should always be applied
+ * immediately and not allowed to bubble up. With regard to padding, strong modifiers are
+ * considered to be on the inside of the prefix and suffix.
+ *
+ * @return Whether the modifier is strong.
+ */
+ public boolean isStrong();
+
+ /**
+ * Gets the prefix string associated with this modifier, defined as the string that will be
+ * inserted at leftIndex when {@link #apply} is called.
+ *
+ * @return The prefix string. Will not be null.
+ */
+ public String getPrefix();
+
+ /**
+ * Gets the prefix string associated with this modifier, defined as the string that will be
+ * inserted at rightIndex when {@link #apply} is called.
+ *
+ * @return The suffix string. Will not be null.
+ */
+ public String getSuffix();
+
+ /**
+ * An interface for a modifier that contains both a positive and a negative form. Note that a
+ * class implementing {@link PositiveNegativeModifier} is not necessarily a {@link Modifier}
+ * itself. Rather, it returns a {@link Modifier} when {@link #getModifier} is called.
+ */
+ public static interface PositiveNegativeModifier extends Exportable {
+ /**
+ * Converts this {@link PositiveNegativeModifier} to a {@link Modifier} given the negative sign.
+ *
+ * @param isNegative true if the negative form of this modifier should be used; false if the
+ * positive form should be used.
+ * @return A Modifier corresponding to the negative sign.
+ */
+ public Modifier getModifier(boolean isNegative);
+ }
+
+ /**
+ * An interface for a modifier that contains both a positive and a negative form for all six
+ * standard plurals. Note that a class implementing {@link PositiveNegativePluralModifier} is not
+ * necessarily a {@link Modifier} itself. Rather, it returns a {@link Modifier} when {@link
+ * #getModifier} is called.
+ */
+ public static interface PositiveNegativePluralModifier extends Exportable {
+ /**
+ * Converts this {@link PositiveNegativePluralModifier} to a {@link Modifier} given the negative
+ * sign and the standard plural.
+ *
+ * @param plural The StandardPlural to use.
+ * @param isNegative true if the negative form of this modifier should be used; false if the
+ * positive form should be used.
+ * @return A Modifier corresponding to the negative sign.
+ */
+ public Modifier getModifier(StandardPlural plural, boolean isNegative);
+ }
+
+ /**
+ * An interface for a modifier that is represented internally by a prefix string and a suffix
+ * string.
+ */
+ public static interface AffixModifier extends Modifier {}
+
+ /**
+ * A starter implementation with defaults for some of the basic methods.
+ *
+ * <p>Implements {@link PositiveNegativeModifier} only so that instances of this class can be used when
+ * a {@link PositiveNegativeModifier} is required.
+ */
+ public abstract static class BaseModifier extends Format.BeforeFormat
+ implements Modifier, PositiveNegativeModifier {
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+ mods.add(this);
+ }
+
+ @Override
+ public Modifier getModifier(boolean isNegative) {
+ return this;
+ }
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/ModifierHolder.java b/android_icu4j/src/main/java/android/icu/impl/number/ModifierHolder.java
new file mode 100644
index 000000000..5c8d4a06b
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/ModifierHolder.java
@@ -0,0 +1,110 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.util.ArrayDeque;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class ModifierHolder {
+ private ArrayDeque<Modifier> mods = new ArrayDeque<Modifier>();
+
+ // Using five separate fields instead of the ArrayDeque saves about 10ns at the expense of
+ // worse code.
+ // TODO: Decide which implementation to use.
+
+ // private Modifier mod1 = null;
+ // private Modifier mod2 = null;
+ // private Modifier mod3 = null;
+ // private Modifier mod4 = null;
+ // private Modifier mod5 = null;
+
+ public ModifierHolder clear() {
+ // mod1 = null;
+ // mod2 = null;
+ // mod3 = null;
+ // mod4 = null;
+ // mod5 = null;
+ mods.clear();
+ return this;
+ }
+
+ public void add(Modifier modifier) {
+ // if (mod1 == null) {
+ // mod1 = modifier;
+ // } else if (mod2 == null) {
+ // mod2 = modifier;
+ // } else if (mod3 == null) {
+ // mod3 = modifier;
+ // } else if (mod4 == null) {
+ // mod4 = modifier;
+ // } else if (mod5 == null) {
+ // mod5 = modifier;
+ // } else {
+ // throw new IndexOutOfBoundsException();
+ // }
+ if (modifier != null) mods.addFirst(modifier);
+ }
+
+ public Modifier peekLast() {
+ return mods.peekLast();
+ }
+
+ public Modifier removeLast() {
+ return mods.removeLast();
+ }
+
+ public int applyAll(NumberStringBuilder string, int leftIndex, int rightIndex) {
+ int addedLength = 0;
+ // if (mod5 != null) {
+ // addedLength += mod5.apply(string, leftIndex, rightIndex + addedLength);
+ // mod5 = null;
+ // }
+ // if (mod4 != null) {
+ // addedLength += mod4.apply(string, leftIndex, rightIndex + addedLength);
+ // mod4 = null;
+ // }
+ // if (mod3 != null) {
+ // addedLength += mod3.apply(string, leftIndex, rightIndex + addedLength);
+ // mod3 = null;
+ // }
+ // if (mod2 != null) {
+ // addedLength += mod2.apply(string, leftIndex, rightIndex + addedLength);
+ // mod2 = null;
+ // }
+ // if (mod1 != null) {
+ // addedLength += mod1.apply(string, leftIndex, rightIndex + addedLength);
+ // mod1 = null;
+ // }
+ while (!mods.isEmpty()) {
+ Modifier mod = mods.removeFirst();
+ addedLength += mod.apply(string, leftIndex, rightIndex + addedLength);
+ }
+ return addedLength;
+ }
+
+ public int applyStrong(NumberStringBuilder string, int leftIndex, int rightIndex) {
+ int addedLength = 0;
+ while (!mods.isEmpty() && mods.peekFirst().isStrong()) {
+ Modifier mod = mods.removeFirst();
+ addedLength += mod.apply(string, leftIndex, rightIndex + addedLength);
+ }
+ return addedLength;
+ }
+
+ public int totalLength() {
+ int length = 0;
+ // if (mod1 != null) length += mod1.length();
+ // if (mod2 != null) length += mod2.length();
+ // if (mod3 != null) length += mod3.length();
+ // if (mod4 != null) length += mod4.length();
+ // if (mod5 != null) length += mod5.length();
+ for (Modifier mod : mods) {
+ if (mod == null) continue;
+ length += mod.length();
+ }
+ return length;
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/NumberStringBuilder.java b/android_icu4j/src/main/java/android/icu/impl/number/NumberStringBuilder.java
new file mode 100644
index 000000000..97ff5e515
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/NumberStringBuilder.java
@@ -0,0 +1,415 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedString;
+import java.text.FieldPosition;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import android.icu.text.NumberFormat;
+import android.icu.text.NumberFormat.Field;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class NumberStringBuilder implements CharSequence {
+ private char[] chars;
+ private Field[] fields;
+ private int zero;
+ private int length;
+
+ public NumberStringBuilder() {
+ this(40);
+ }
+
+ public NumberStringBuilder(int capacity) {
+ chars = new char[capacity];
+ fields = new Field[capacity];
+ zero = capacity / 2;
+ length = 0;
+ }
+
+ public NumberStringBuilder(NumberStringBuilder source) {
+ this(source.chars.length);
+ zero = source.zero;
+ length = source.length;
+ System.arraycopy(source.chars, zero, chars, zero, length);
+ System.arraycopy(source.fields, zero, fields, zero, length);
+ }
+
+ @Override
+ public int length() {
+ return length;
+ }
+
+ @Override
+ public char charAt(int index) {
+ if (index < 0 || index > length) {
+ throw new IndexOutOfBoundsException();
+ }
+ return chars[zero + index];
+ }
+
+ /**
+ * Appends the specified codePoint to the end of the string.
+ *
+ * @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);
+ }
+
+ /**
+ * Inserts the specified codePoint at the specified index in the string.
+ *
+ * @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) {
+ int count = Character.charCount(codePoint);
+ int position = prepareForInsert(index, count);
+ Character.toChars(codePoint, chars, position);
+ fields[position] = field;
+ if (count == 2) fields[position + 1] = field;
+ return count;
+ }
+
+ /**
+ * Appends the specified CharSequence to the end of the string.
+ *
+ * @return The number of chars added, which is the length of CharSequence.
+ */
+ public int append(CharSequence sequence, Field field) {
+ return insert(length, sequence, field);
+ }
+
+ /**
+ * Inserts the specified CharSequence at the specified index in the string.
+ *
+ * @return The number of chars added, which is the length of CharSequence.
+ */
+ public int insert(int index, CharSequence sequence, Field field) {
+ if (sequence.length() == 0) {
+ // Nothing to insert.
+ return 0;
+ } else if (sequence.length() == 1) {
+ // Fast path: on a single-char string, using insertCodePoint below is 70% faster than the
+ // CharSequence method: 12.2 ns versus 41.9 ns for five operations on my Linux x86-64.
+ return insertCodePoint(index, sequence.charAt(0), field);
+ } else {
+ return insert(index, sequence, 0, sequence.length(), field);
+ }
+ }
+
+ /**
+ * Inserts the specified CharSequence at the specified index in the string, reading from the
+ * CharSequence from start (inclusive) to end (exclusive).
+ *
+ * @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) {
+ int count = end - start;
+ int position = prepareForInsert(index, count);
+ for (int i = 0; i < count; i++) {
+ chars[position + i] = sequence.charAt(start + i);
+ fields[position + i] = field;
+ }
+ return count;
+ }
+
+ /**
+ * Appends the chars in the specified char array to the end of the string, and associates them
+ * with the fields in the specified field array, which must have the same length as chars.
+ *
+ * @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);
+ }
+
+ /**
+ * Inserts the chars in the specified char array at the specified index in the string, and
+ * associates them with the fields in the specified field array, which must have the same length
+ * as chars.
+ *
+ * @return The number of chars added, which is the length of the char array.
+ */
+ public int insert(int index, char[] chars, Field[] fields) {
+ assert fields == null || chars.length == fields.length;
+ int count = chars.length;
+ if (count == 0) return 0; // nothing to insert
+ int position = prepareForInsert(index, count);
+ for (int i = 0; i < count; i++) {
+ this.chars[position + i] = chars[i];
+ this.fields[position + i] = fields == null ? null : fields[i];
+ }
+ return count;
+ }
+
+ /**
+ * Appends the contents of another {@link NumberStringBuilder} to the end of this instance.
+ *
+ * @return The number of chars added, which is the length of the other {@link
+ * NumberStringBuilder}.
+ */
+ public int append(NumberStringBuilder other) {
+ return insert(length, other);
+ }
+
+ /**
+ * Inserts the contents of another {@link NumberStringBuilder} into this instance at the given
+ * index.
+ *
+ * @return The number of chars added, which is the length of the other {@link
+ * NumberStringBuilder}.
+ */
+ public int insert(int index, NumberStringBuilder other) {
+ if (this == other) {
+ throw new IllegalArgumentException("Cannot call insert/append on myself");
+ }
+ int count = other.length;
+ if (count == 0) return 0; // nothing to insert
+ int position = prepareForInsert(index, count);
+ for (int i = 0; i < count; i++) {
+ this.chars[position + i] = other.chars[other.zero + i];
+ this.fields[position + i] = other.fields[other.zero + i];
+ }
+ return count;
+ }
+
+ /**
+ * Shifts around existing data if necessary to make room for new characters.
+ *
+ * @param index The location in the string where the operation is to take place.
+ * @param count The number of chars (UTF-16 code units) to be inserted at that location.
+ * @return The position in the char array to insert the chars.
+ */
+ private int prepareForInsert(int index, int count) {
+ if (index == 0 && zero - count >= 0) {
+ // Append to start
+ zero -= count;
+ length += count;
+ return zero;
+ } else if (index == length && zero + length + count < chars.length) {
+ // Append to end
+ length += count;
+ return zero + length - count;
+ } else {
+ // Move chars around and/or allocate more space
+ return prepareForInsertHelper(index, count);
+ }
+ }
+
+ private int prepareForInsertHelper(int index, int count) {
+ // Keeping this code out of prepareForInsert() increases the speed of append operations.
+ if (length + count > chars.length) {
+ char[] newChars = new char[(length + count) * 2];
+ Field[] newFields = new Field[(length + count) * 2];
+ int newZero = newChars.length / 2 - (length + count) / 2;
+ System.arraycopy(chars, zero, newChars, newZero, index);
+ System.arraycopy(chars, zero + index, newChars, newZero + index + count, length - index);
+ System.arraycopy(fields, zero, newFields, newZero, index);
+ System.arraycopy(fields, zero + index, newFields, newZero + index + count, length - index);
+ chars = newChars;
+ fields = newFields;
+ zero = newZero;
+ length += count;
+ } else {
+ int newZero = chars.length / 2 - (length + count) / 2;
+ System.arraycopy(chars, zero, chars, newZero, length);
+ System.arraycopy(chars, newZero + index, chars, newZero + index + count, length - index);
+ System.arraycopy(fields, zero, fields, newZero, length);
+ System.arraycopy(fields, newZero + index, fields, newZero + index + count, length - index);
+ zero = newZero;
+ length += count;
+ }
+ return zero + index;
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ if (start < 0 || end > length || end < start) {
+ throw new IndexOutOfBoundsException();
+ }
+ NumberStringBuilder other = new NumberStringBuilder(this);
+ other.zero = zero + start;
+ other.length = end - start;
+ return other;
+ }
+
+ /**
+ * Returns the string represented by the characters in this string builder.
+ *
+ * <p>For a string intended be used for debugging, use {@link #toDebugString}.
+ */
+ @Override
+ public String toString() {
+ return new String(chars, zero, length);
+ }
+
+ private static final Map<Field, Character> fieldToDebugChar = new HashMap<Field, Character>();
+
+ static {
+ fieldToDebugChar.put(NumberFormat.Field.SIGN, '-');
+ fieldToDebugChar.put(NumberFormat.Field.INTEGER, 'i');
+ fieldToDebugChar.put(NumberFormat.Field.FRACTION, 'f');
+ fieldToDebugChar.put(NumberFormat.Field.EXPONENT, 'e');
+ fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SIGN, '+');
+ fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SYMBOL, 'E');
+ fieldToDebugChar.put(NumberFormat.Field.DECIMAL_SEPARATOR, '.');
+ fieldToDebugChar.put(NumberFormat.Field.GROUPING_SEPARATOR, ',');
+ fieldToDebugChar.put(NumberFormat.Field.PERCENT, '%');
+ fieldToDebugChar.put(NumberFormat.Field.PERMILLE, '‰');
+ fieldToDebugChar.put(NumberFormat.Field.CURRENCY, '$');
+ }
+
+ /**
+ * Returns a string that includes field information, for debugging purposes.
+ *
+ * <p>For example, if the string is "-12.345", the debug string will be something like
+ * "&lt;NumberStringBuilder [-123.45] [-iii.ff]&gt;"
+ *
+ * @return A string for debugging purposes.
+ */
+ public String toDebugString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<NumberStringBuilder [");
+ sb.append(this.toString());
+ sb.append("] [");
+ for (int i = zero; i < zero + length; i++) {
+ if (fields[i] == null) {
+ sb.append('n');
+ } else {
+ sb.append(fieldToDebugChar.get(fields[i]));
+ }
+ }
+ sb.append("]>");
+ return sb.toString();
+ }
+
+ /** @return A new array containing the contents of this string builder. */
+ public char[] toCharArray() {
+ return Arrays.copyOfRange(chars, zero, zero + length);
+ }
+
+ /** @return A new array containing the field values of this string builder. */
+ public Field[] toFieldArray() {
+ return Arrays.copyOfRange(fields, zero, zero + length);
+ }
+
+ /**
+ * @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) {
+ if (chars.length != length) return false;
+ if (fields.length != length) return false;
+ for (int i = 0; i < length; i++) {
+ if (this.chars[zero + i] != chars[i]) return false;
+ if (this.fields[zero + i] != fields[i]) return false;
+ }
+ return true;
+ }
+
+ /**
+ * @param other The instance to compare.
+ * @return Whether the contents of this instance is currently equal to the given instance.
+ */
+ public boolean contentEquals(NumberStringBuilder other) {
+ if (length != other.length) return false;
+ for (int i = 0; i < length; i++) {
+ if (chars[zero + i] != other.chars[other.zero + i]) return false;
+ if (fields[zero + i] != other.fields[other.zero + i]) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Populates the given {@link FieldPosition} based on this string builder.
+ *
+ * @param fp The FieldPosition to populate.
+ * @param offset An offset to add to the field position index; can be zero.
+ */
+ public void populateFieldPosition(FieldPosition fp, int offset) {
+ java.text.Format.Field rawField = fp.getFieldAttribute();
+
+ if (rawField == null) {
+ // Backwards compatibility: read from fp.getField()
+ if (fp.getField() == NumberFormat.INTEGER_FIELD) {
+ rawField = NumberFormat.Field.INTEGER;
+ } else if (fp.getField() == NumberFormat.FRACTION_FIELD) {
+ rawField = NumberFormat.Field.FRACTION;
+ } else {
+ // No field is set
+ return;
+ }
+ }
+
+ if (!(rawField instanceof android.icu.text.NumberFormat.Field)) {
+ throw new IllegalArgumentException(
+ "You must pass an instance of android.icu.text.NumberFormat.Field as your FieldPosition attribute. You passed: "
+ + rawField.getClass().toString());
+ }
+ /* android.icu.text.NumberFormat. */ Field field = (Field) rawField;
+
+ boolean seenStart = false;
+ int fractionStart = -1;
+ for (int i = zero; i <= zero + length; i++) {
+ Field _field = (i < zero + length) ? fields[i] : null;
+ if (seenStart && field != _field) {
+ // Special case: GROUPING_SEPARATOR counts as an INTEGER.
+ if (field == NumberFormat.Field.INTEGER && _field == NumberFormat.Field.GROUPING_SEPARATOR)
+ continue;
+ fp.setEndIndex(i - zero + offset);
+ break;
+ } else if (!seenStart && field == _field) {
+ fp.setBeginIndex(i - zero + offset);
+ seenStart = true;
+ }
+ if (_field == NumberFormat.Field.INTEGER || _field == NumberFormat.Field.DECIMAL_SEPARATOR) {
+ fractionStart = i - zero + 1;
+ }
+ }
+
+ // Backwards compatibility: FRACTION needs to start after INTEGER if empty
+ if (field == NumberFormat.Field.FRACTION && !seenStart) {
+ fp.setBeginIndex(fractionStart);
+ fp.setEndIndex(fractionStart);
+ }
+ }
+
+ public AttributedCharacterIterator getIterator() {
+ AttributedString as = new AttributedString(toString());
+ Field current = null;
+ int currentStart = -1;
+ for (int i = 0; i < length; i++) {
+ Field field = fields[i + zero];
+ if (current == NumberFormat.Field.INTEGER && field == NumberFormat.Field.GROUPING_SEPARATOR) {
+ // Special case: GROUPING_SEPARATOR counts as an INTEGER.
+ as.addAttribute(
+ NumberFormat.Field.GROUPING_SEPARATOR, NumberFormat.Field.GROUPING_SEPARATOR, i, i + 1);
+ } else if (current != field) {
+ if (current != null) {
+ as.addAttribute(current, current, currentStart, i);
+ }
+ current = field;
+ currentStart = i;
+ }
+ }
+ if (current != null) {
+ as.addAttribute(current, current, currentStart, length);
+ }
+ return as.getIterator();
+ }
+
+ public NumberStringBuilder clear() {
+ zero = chars.length / 2;
+ length = 0;
+ return this;
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/PNAffixGenerator.java b/android_icu4j/src/main/java/android/icu/impl/number/PNAffixGenerator.java
new file mode 100644
index 000000000..0f7cf8b08
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/PNAffixGenerator.java
@@ -0,0 +1,283 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import android.icu.impl.number.Modifier.AffixModifier;
+import android.icu.impl.number.formatters.CompactDecimalFormat;
+import android.icu.impl.number.formatters.PositiveNegativeAffixFormat;
+import android.icu.impl.number.formatters.PositiveNegativeAffixFormat.IProperties;
+import android.icu.impl.number.modifiers.ConstantAffixModifier;
+import android.icu.impl.number.modifiers.ConstantMultiFieldModifier;
+import android.icu.text.DecimalFormatSymbols;
+import android.icu.text.NumberFormat.Field;
+
+/**
+ * A class to convert from a bag of prefix/suffix properties into a positive and negative {@link
+ * Modifier}. This is a standard implementation used by {@link PositiveNegativeAffixFormat}, {@link
+ * CompactDecimalFormat}, {@link Parse}, and others.
+ *
+ * <p>This class is is intended to be an efficient generator for instances of Modifier by a single
+ * thread during construction of a formatter or during static formatting. It uses internal caching
+ * to avoid creating new Modifier objects when possible. It is NOT THREAD SAFE and NOT IMMUTABLE.
+ *
+ * <p>The thread-local instance of this class provided by {@link #getThreadLocalInstance} should be
+ * used in most cases instead of constructing a new instance of the object.
+ *
+ * <p>This class also handles the logic of assigning positive signs, negative signs, and currency
+ * signs according to the LDML specification.
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class PNAffixGenerator {
+ public static class Result {
+ public AffixModifier positive = null;
+ public AffixModifier negative = null;
+ }
+
+ protected static final ThreadLocal<PNAffixGenerator> threadLocalInstance =
+ new ThreadLocal<PNAffixGenerator>() {
+ @Override
+ protected PNAffixGenerator initialValue() {
+ return new PNAffixGenerator();
+ }
+ };
+
+ public static PNAffixGenerator getThreadLocalInstance() {
+ return threadLocalInstance.get();
+ }
+
+ // These instances are used internally and cached to avoid object creation. The resultInstance
+ // also serves as a 1-element cache to avoid creating objects when subsequent calls have
+ // identical prefixes and suffixes. This happens, for example, when consuming CDF data.
+ private Result resultInstance = new Result();
+ private NumberStringBuilder sb1 = new NumberStringBuilder();
+ private NumberStringBuilder sb2 = new NumberStringBuilder();
+ private NumberStringBuilder sb3 = new NumberStringBuilder();
+ private NumberStringBuilder sb4 = new NumberStringBuilder();
+ private NumberStringBuilder sb5 = new NumberStringBuilder();
+ private NumberStringBuilder sb6 = new NumberStringBuilder();
+
+ /**
+ * Generates modifiers using default currency symbols.
+ *
+ * @param symbols The symbols to interpolate for minus, plus, percent, permille, and currency.
+ * @param properties The bag of properties to convert.
+ * @return The positive and negative {@link Modifier}.
+ */
+ public Result getModifiers(
+ DecimalFormatSymbols symbols, PositiveNegativeAffixFormat.IProperties properties) {
+ // If this method is used, the user doesn't care about currencies. Default the currency symbols
+ // to the information we can get from the DecimalFormatSymbols instance.
+ return getModifiers(
+ symbols,
+ symbols.getCurrencySymbol(),
+ symbols.getInternationalCurrencySymbol(),
+ symbols.getCurrencySymbol(),
+ properties);
+ }
+
+ /**
+ * Generates modifiers using the specified currency symbol for all three lengths of currency
+ * placeholders in the pattern string.
+ *
+ * @param symbols The symbols to interpolate for minus, plus, percent, and permille.
+ * @param currencySymbol The currency symbol.
+ * @param properties The bag of properties to convert.
+ * @return The positive and negative {@link Modifier}.
+ */
+ public Result getModifiers(
+ DecimalFormatSymbols symbols,
+ String currencySymbol,
+ PositiveNegativeAffixFormat.IProperties properties) {
+ // If this method is used, the user doesn't cares about currencies but doesn't care about
+ // supporting all three sizes of currency placeholders. Use the one provided string for all
+ // three sizes of placeholders.
+ return getModifiers(symbols, currencySymbol, currencySymbol, currencySymbol, properties);
+ }
+
+ /**
+ * Generates modifiers using the three specified strings to replace the three lengths of currency
+ * placeholders: "¤", "¤¤", and "¤¤¤".
+ *
+ * @param symbols The symbols to interpolate for minus, plus, percent, and permille.
+ * @param curr1 The string to replace "¤".
+ * @param curr2 The string to replace "¤¤".
+ * @param curr3 The string to replace "¤¤¤".
+ * @param properties The bag of properties to convert.
+ * @return The positive and negative {@link Modifier}.
+ */
+ public Result getModifiers(
+ DecimalFormatSymbols symbols,
+ String curr1,
+ String curr2,
+ String curr3,
+ PositiveNegativeAffixFormat.IProperties properties) {
+
+ // Use a different code path for handling affixes with "always show plus sign"
+ if (properties.getSignAlwaysShown()) {
+ return getModifiersWithPlusSign(symbols, curr1, curr2, curr3, properties);
+ }
+
+ String ppp = properties.getPositivePrefixPattern();
+ String psp = properties.getPositiveSuffixPattern();
+ String npp = properties.getNegativePrefixPattern();
+ String nsp = properties.getNegativeSuffixPattern();
+
+ // Set sb1/sb2 to the positive prefix/suffix.
+ sb1.clear();
+ sb2.clear();
+ AffixPatternUtils.unescape(ppp, symbols, curr1, curr2, curr3, null, sb1);
+ AffixPatternUtils.unescape(psp, symbols, curr1, curr2, curr3, null, sb2);
+ setPositiveResult(sb1, sb2, properties);
+
+ // Set sb1/sb2 to the negative prefix/suffix.
+ if (npp == null && nsp == null) {
+ // Negative prefix defaults to positive prefix prepended with the minus sign.
+ // Negative suffix defaults to positive suffix.
+ sb1.insert(0, symbols.getMinusSignString(), Field.SIGN);
+ } else {
+ sb1.clear();
+ sb2.clear();
+ AffixPatternUtils.unescape(npp, symbols, curr1, curr2, curr3, null, sb1);
+ AffixPatternUtils.unescape(nsp, symbols, curr1, curr2, curr3, null, sb2);
+ }
+ setNegativeResult(sb1, sb2, properties);
+
+ return resultInstance;
+ }
+
+ private Result getModifiersWithPlusSign(
+ DecimalFormatSymbols symbols,
+ String curr1,
+ String curr2,
+ String curr3,
+ IProperties properties) {
+
+ String ppp = properties.getPositivePrefixPattern();
+ String psp = properties.getPositiveSuffixPattern();
+ String npp = properties.getNegativePrefixPattern();
+ String nsp = properties.getNegativeSuffixPattern();
+
+ // There are three cases, listed below with their expected outcomes.
+ // TODO: Should we handle the cases when the positive subpattern has a '+' already?
+ //
+ // 1) No negative subpattern.
+ // Positive => Positive subpattern prepended with '+'
+ // Negative => Positive subpattern prepended with '-'
+ // 2) Negative subpattern does not have '-'.
+ // Positive => Positive subpattern prepended with '+'
+ // Negative => Negative subpattern
+ // 3) Negative subpattern has '-'.
+ // Positive => Negative subpattern with '+' substituted for '-'
+ // Negative => Negative subpattern
+
+ if (npp != null || nsp != null) {
+ // Case 2 or Case 3
+ sb1.clear();
+ sb2.clear();
+ sb3.clear();
+ sb4.clear();
+ AffixPatternUtils.unescape(npp, symbols, curr1, curr2, curr3, null, sb1);
+ AffixPatternUtils.unescape(nsp, symbols, curr1, curr2, curr3, null, sb2);
+ AffixPatternUtils.unescape(
+ npp, symbols, curr1, curr2, curr3, symbols.getPlusSignString(), sb3);
+ AffixPatternUtils.unescape(
+ nsp, symbols, curr1, curr2, curr3, symbols.getPlusSignString(), sb4);
+ if (!charSequenceEquals(sb1, sb3) || !charSequenceEquals(sb2, sb4)) {
+ // Case 3. The plus sign substitution was successful.
+ setPositiveResult(sb3, sb4, properties);
+ setNegativeResult(sb1, sb2, properties);
+ return resultInstance;
+ } else {
+ // Case 2. There was no minus sign. Set the negative result and fall through.
+ setNegativeResult(sb1, sb2, properties);
+ }
+ }
+
+ // Case 1 or 2. Set sb1/sb2 to the positive prefix/suffix.
+ sb1.clear();
+ sb2.clear();
+ AffixPatternUtils.unescape(ppp, symbols, curr1, curr2, curr3, null, sb1);
+ AffixPatternUtils.unescape(psp, symbols, curr1, curr2, curr3, null, sb2);
+
+ if (npp == null && nsp == null) {
+ // Case 1. Compute the negative result from the positive subpattern.
+ sb3.clear();
+ sb3.append(symbols.getMinusSignString(), Field.SIGN);
+ sb3.append(sb1);
+ setNegativeResult(sb3, sb2, properties);
+ }
+
+ // Case 1 or 2. Prepend a '+' sign to the positive prefix.
+ sb1.insert(0, symbols.getPlusSignString(), Field.SIGN);
+ setPositiveResult(sb1, sb2, properties);
+
+ return resultInstance;
+ }
+
+ private void setPositiveResult(
+ NumberStringBuilder prefix, NumberStringBuilder suffix, IProperties properties) {
+ // Override with custom affixes. We need to put these into NumberStringBuilders so that they
+ // have the same datatype as the incoming prefix and suffix (important when testing for field
+ // equality in contentEquals).
+ // TODO: It is a little inefficient that we copy String -> NumberStringBuilder -> Modifier.
+ // Consider re-working the logic so that fewer copies are required.
+ String _prefix = properties.getPositivePrefix();
+ String _suffix = properties.getPositiveSuffix();
+ if (_prefix != null) {
+ prefix = sb5.clear();
+ prefix.append(_prefix, null);
+ }
+ if (_suffix != null) {
+ suffix = sb6.clear();
+ suffix.append(_suffix, null);
+ }
+ if (prefix.length() == 0 && suffix.length() == 0) {
+ resultInstance.positive = ConstantAffixModifier.EMPTY;
+ return;
+ }
+ if (resultInstance.positive != null
+ && (resultInstance.positive instanceof ConstantMultiFieldModifier)
+ && ((ConstantMultiFieldModifier) resultInstance.positive).contentEquals(prefix, suffix)) {
+ // Use the cached modifier
+ return;
+ }
+ resultInstance.positive = new ConstantMultiFieldModifier(prefix, suffix, false);
+ }
+
+ private void setNegativeResult(
+ NumberStringBuilder prefix, NumberStringBuilder suffix, IProperties properties) {
+ String _prefix = properties.getNegativePrefix();
+ String _suffix = properties.getNegativeSuffix();
+ if (_prefix != null) {
+ prefix = sb5.clear();
+ prefix.append(_prefix, null);
+ }
+ if (_suffix != null) {
+ suffix = sb6.clear();
+ suffix.append(_suffix, null);
+ }
+ if (prefix.length() == 0 && suffix.length() == 0) {
+ resultInstance.negative = ConstantAffixModifier.EMPTY;
+ return;
+ }
+ if (resultInstance.negative != null
+ && (resultInstance.negative instanceof ConstantMultiFieldModifier)
+ && ((ConstantMultiFieldModifier) resultInstance.negative).contentEquals(prefix, suffix)) {
+ // Use the cached modifier
+ return;
+ }
+ resultInstance.negative = new ConstantMultiFieldModifier(prefix, suffix, false);
+ }
+
+ /** A null-safe equals method for CharSequences. */
+ private static boolean charSequenceEquals(CharSequence a, CharSequence b) {
+ if (a == b) return true;
+ if (a == null || b == null) return false;
+ if (a.length() != b.length()) return false;
+ for (int i = 0; i < a.length(); i++) {
+ if (a.charAt(i) != b.charAt(i)) return false;
+ }
+ return true;
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/Parse.java b/android_icu4j/src/main/java/android/icu/impl/number/Parse.java
new file mode 100644
index 000000000..b78a48502
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/Parse.java
@@ -0,0 +1,2498 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import android.icu.impl.StandardPlural;
+import android.icu.impl.TextTrieMap;
+import android.icu.impl.number.formatters.BigDecimalMultiplier;
+import android.icu.impl.number.formatters.CurrencyFormat;
+import android.icu.impl.number.formatters.MagnitudeMultiplier;
+import android.icu.impl.number.formatters.PaddingFormat;
+import android.icu.impl.number.formatters.PositiveDecimalFormat;
+import android.icu.impl.number.formatters.PositiveNegativeAffixFormat;
+import android.icu.impl.number.formatters.ScientificFormat;
+import android.icu.lang.UCharacter;
+import android.icu.text.CurrencyPluralInfo;
+import android.icu.text.DecimalFormatSymbols;
+import android.icu.text.NumberFormat;
+import android.icu.text.UnicodeSet;
+import android.icu.util.Currency;
+import android.icu.util.Currency.CurrencyStringInfo;
+import android.icu.util.CurrencyAmount;
+import android.icu.util.ULocale;
+
+/**
+ * A parser designed to convert an arbitrary human-generated string to its best representation as a
+ * number: a long, a BigInteger, or a BigDecimal.
+ *
+ * <p>The parser may traverse multiple parse paths in the same strings if there is ambiguity. For
+ * example, the string "12,345.67" has two main interpretations: it could be "12.345" in a locale
+ * that uses '.' as the grouping separator, or it could be "12345.67" in a locale that uses ',' as
+ * the grouping separator. Since the second option has a longer parse path (consumes more of the
+ * input string), the parser will accept the second option.
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class Parse {
+
+ /** Controls the set of rules for parsing a string. */
+ public static enum ParseMode {
+ /**
+ * Lenient mode should be used if you want to accept malformed user input. It will use
+ * heuristics to attempt to parse through typographical errors in the string.
+ */
+ LENIENT,
+
+ /**
+ * Strict mode should be used if you want to require that the input is well-formed. More
+ * specifically, it differs from lenient mode in the following ways:
+ *
+ * <ul>
+ * <li>Grouping widths must match the grouping settings. For example, "12,3,45" will fail if
+ * the grouping width is 3, as in the pattern "#,##0".
+ * <li>The string must contain a complete prefix and suffix. For example, if the pattern is
+ * "{#};(#)", then "{123}" or "(123)" would match, but "{123", "123}", and "123" would all
+ * fail. (The latter strings would be accepted in lenient mode.)
+ * <li>Whitespace may not appear at arbitrary places in the string. In lenient mode,
+ * whitespace is allowed to occur arbitrarily before and after prefixes and exponent
+ * separators.
+ * <li>Leading grouping separators are not allowed, as in ",123".
+ * <li>Minus and plus signs can only appear if specified in the pattern. In lenient mode, a
+ * plus or minus sign can always precede a number.
+ * <li>The set of characters that can be interpreted as a decimal or grouping separator is
+ * smaller.
+ * <li><strong>If currency parsing is enabled,</strong> currencies must only appear where
+ * specified in either the current pattern string or in a valid pattern string for the
+ * current locale. For example, if the pattern is "¤0.00", then "$1.23" would match, but
+ * "1.23$" would fail to match.
+ * </ul>
+ */
+ STRICT,
+
+ /**
+ * Fast mode should be used in applications that don't require prefixes and suffixes to match.
+ *
+ * <p>In addition to ignoring prefixes and suffixes, fast mode performs the following
+ * optimizations:
+ *
+ * <ul>
+ * <li>Ignores digit strings from {@link DecimalFormatSymbols} and only uses the code point's
+ * Unicode digit property. If you are not using custom digit strings, this should not
+ * cause a change in behavior.
+ * <li>Instead of traversing multiple possible parse paths, a "greedy" parsing strategy is
+ * used, which might mean that fast mode won't accept strings that lenient or strict mode
+ * would accept. Since prefix and suffix strings are ignored, this is not an issue unless
+ * you are using custom symbols.
+ * </ul>
+ */
+ FAST,
+ }
+
+ /** The set of properties required for {@link Parse}. Accepts a {@link Properties} object. */
+ public static interface IProperties
+ extends PositiveNegativeAffixFormat.IProperties,
+ PaddingFormat.IProperties,
+ CurrencyFormat.ICurrencyProperties,
+ BigDecimalMultiplier.IProperties,
+ MagnitudeMultiplier.IProperties,
+ PositiveDecimalFormat.IProperties,
+ ScientificFormat.IProperties {
+
+ boolean DEFAULT_PARSE_INTEGER_ONLY = false;
+
+ /** @see #setParseIntegerOnly */
+ public boolean getParseIntegerOnly();
+
+ /**
+ * Whether to ignore the fractional part of numbers. For example, parses "123.4" to "123"
+ * instead of "123.4".
+ *
+ * @param parseIntegerOnly true to parse integers only; false to parse integers with their
+ * fraction parts
+ * @return The property bag, for chaining.
+ */
+ public IProperties setParseIntegerOnly(boolean parseIntegerOnly);
+
+ boolean DEFAULT_PARSE_NO_EXPONENT = false;
+
+ /** @see #setParseNoExponent */
+ public boolean getParseNoExponent();
+
+ /**
+ * Whether to ignore the exponential part of numbers. For example, parses "123E4" to "123"
+ * instead of "1230000".
+ *
+ * @param parseIgnoreExponent true to ignore exponents; false to parse them.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setParseNoExponent(boolean parseIgnoreExponent);
+
+ boolean DEFAULT_DECIMAL_PATTERN_MATCH_REQUIRED = false;
+
+ /** @see #setDecimalPatternMatchRequired */
+ public boolean getDecimalPatternMatchRequired();
+
+ /**
+ * Whether to require that the presence of decimal point matches the pattern. If a decimal point
+ * is not present, but the pattern contained a decimal point, parse will not succeed: null will
+ * be returned from <code>parse()</code>, and an error index will be set in the {@link
+ * ParsePosition}.
+ *
+ * @param decimalPatternMatchRequired true to set an error if decimal is not present
+ * @return The property bag, for chaining.
+ */
+ public IProperties setDecimalPatternMatchRequired(boolean decimalPatternMatchRequired);
+
+ ParseMode DEFAULT_PARSE_MODE = null;
+
+ /** @see #setParseMode */
+ public ParseMode getParseMode();
+
+ /**
+ * Controls certain rules for how strict this parser is when reading strings. See {@link
+ * ParseMode#LENIENT} and {@link ParseMode#STRICT}.
+ *
+ * @param parseMode Either {@link ParseMode#LENIENT} or {@link ParseMode#STRICT}.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setParseMode(ParseMode parseMode);
+
+ boolean DEFAULT_PARSE_TO_BIG_DECIMAL = false;
+
+ /** @see #setParseToBigDecimal */
+ public boolean getParseToBigDecimal();
+
+ /**
+ * Whether to always return a BigDecimal from {@link Parse#parse} and all other parse methods.
+ * By default, a Long or a BigInteger are returned when possible.
+ *
+ * @param parseToBigDecimal true to always return a BigDecimal; false to return a Long or a
+ * BigInteger when possible.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setParseToBigDecimal(boolean parseToBigDecimal);
+
+ boolean DEFAULT_PARSE_CASE_SENSITIVE = false;
+
+ /** @see #setParseCaseSensitive */
+ public boolean getParseCaseSensitive();
+
+ /**
+ * Whether to require cases to match when parsing strings; default is true. Case sensitivity
+ * applies to prefixes, suffixes, the exponent separator, the symbol "NaN", and the infinity
+ * symbol. Grouping separators, decimal separators, and padding are always case-sensitive.
+ * Currencies are always case-insensitive.
+ *
+ * <p>This setting is ignored in fast mode. In fast mode, strings are always compared in a
+ * case-sensitive way.
+ *
+ * @param parseCaseSensitive true to be case-sensitive when parsing; false to allow any case.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setParseCaseSensitive(boolean parseCaseSensitive);
+
+ GroupingMode DEFAULT_PARSE_GROUPING_MODE = null;
+
+ /** @see #setParseGroupingMode */
+ public GroupingMode getParseGroupingMode();
+
+ /**
+ * Sets the strategy used during parsing when a code point needs to be interpreted as either a
+ * decimal separator or a grouping separator.
+ *
+ * <p>The comma, period, space, and apostrophe have different meanings in different locales. For
+ * example, in <em>en-US</em> and most American locales, the period is used as a decimal
+ * separator, but in <em>es-PY</em> and most European locales, it is used as a grouping
+ * separator.
+ *
+ * <p>Suppose you are in <em>fr-FR</em> the parser encounters the string "1.234". In
+ * <em>fr-FR</em>, the grouping is a space and the decimal is a comma. The <em>grouping
+ * mode</em> is a mechanism to let you specify whether to accept the string as 1234
+ * (GroupingMode.DEFAULT) or whether to reject it since the separators don't match
+ * (GroupingMode.RESTRICTED).
+ *
+ * <p>When resolving grouping separators, it is the <em>equivalence class</em> of separators
+ * that is considered. For example, a period is seen as equal to a fixed set of other
+ * period-like characters.
+ *
+ * @param parseGroupingMode The {@link GroupingMode} to use; either DEFAULT or RESTRICTED.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setParseGroupingMode(GroupingMode parseGroupingMode);
+ }
+
+ /**
+ * An enum containing the choices for strategy in parsing when choosing between grouping and
+ * decimal separators.
+ */
+ public static enum GroupingMode {
+ /**
+ * Accept decimal equivalents as decimals, and if that fails, accept all equivalence classes
+ * (periods, commas, and whitespace-like) as grouping. This is a more lenient strategy.
+ *
+ * <p>For example, if the formatter's current locale is <em>fr-FR</em>, then "1.234" will parse
+ * as 1234, even though <em>fr-FR</em> does not use a period as the grouping separator.
+ */
+ DEFAULT,
+
+ /**
+ * Accept decimal equivalents as decimals and grouping equivalents as grouping. This strategy is
+ * more strict.
+ *
+ * <p>For example, if the formatter's current locale is <em>fr-FR</em>, then "1.234" will fail
+ * to parse since <em>fr-FR</em> does not use a period as the grouping separator.
+ */
+ RESTRICTED
+ }
+
+ /**
+ * @see Parse#parse(String, ParsePosition, ParseMode, boolean, boolean, IProperties,
+ * DecimalFormatSymbols)
+ */
+ private static enum StateName {
+ BEFORE_PREFIX,
+ AFTER_PREFIX,
+ AFTER_INTEGER_DIGIT,
+ AFTER_FRACTION_DIGIT,
+ AFTER_EXPONENT_SEPARATOR,
+ AFTER_EXPONENT_DIGIT,
+ BEFORE_SUFFIX,
+ BEFORE_SUFFIX_SEEN_EXPONENT,
+ AFTER_SUFFIX,
+ INSIDE_CURRENCY,
+ INSIDE_DIGIT,
+ INSIDE_STRING,
+ INSIDE_AFFIX_PATTERN;
+ }
+
+ // TODO: Does this set make sense for the whitespace characters?
+ private static final UnicodeSet UNISET_WHITESPACE =
+ new UnicodeSet("[[:whitespace:][\\u2000-\\u200D]]").freeze();
+
+ // BiDi characters are skipped over and ignored at any point in the string, even in strict mode.
+ private static final UnicodeSet UNISET_BIDI =
+ new UnicodeSet("[[\\u200E\\u200F\\u061C]]").freeze();
+
+ // TODO: Re-generate these sets from the database. They probably haven't been updated in a while.
+ private static final UnicodeSet UNISET_PERIOD_LIKE =
+ new UnicodeSet("[.\\u2024\\u3002\\uFE12\\uFE52\\uFF0E\\uFF61]").freeze();
+ private static final UnicodeSet UNISET_STRICT_PERIOD_LIKE =
+ new UnicodeSet("[.\\u2024\\uFE52\\uFF0E\\uFF61]").freeze();
+ private static final UnicodeSet UNISET_COMMA_LIKE =
+ new UnicodeSet("[,\\u060C\\u066B\\u3001\\uFE10\\uFE11\\uFE50\\uFE51\\uFF0C\\uFF64]").freeze();
+ private static final UnicodeSet UNISET_STRICT_COMMA_LIKE =
+ new UnicodeSet("[,\\u066B\\uFE10\\uFE50\\uFF0C]").freeze();
+ private static final UnicodeSet UNISET_OTHER_GROUPING_SEPARATORS =
+ new UnicodeSet(
+ "[\\ '\\u00A0\\u066C\\u2000-\\u200A\\u2018\\u2019\\u202F\\u205F\\u3000\\uFF07]")
+ .freeze();
+
+ // For parse return value calculation.
+ private static final BigDecimal MIN_LONG_AS_BIG_DECIMAL = new BigDecimal(Long.MIN_VALUE);
+ private static final BigDecimal MAX_LONG_AS_BIG_DECIMAL = new BigDecimal(Long.MAX_VALUE);
+
+ private enum SeparatorType {
+ COMMA_LIKE,
+ PERIOD_LIKE,
+ OTHER_GROUPING,
+ UNKNOWN;
+
+ static SeparatorType fromCp(int cp, ParseMode mode) {
+ if (mode == ParseMode.FAST) {
+ return SeparatorType.UNKNOWN;
+ } else if (mode == ParseMode.STRICT) {
+ if (UNISET_STRICT_COMMA_LIKE.contains(cp)) return COMMA_LIKE;
+ if (UNISET_STRICT_PERIOD_LIKE.contains(cp)) return PERIOD_LIKE;
+ if (UNISET_OTHER_GROUPING_SEPARATORS.contains(cp)) return OTHER_GROUPING;
+ return UNKNOWN;
+ } else {
+ if (UNISET_COMMA_LIKE.contains(cp)) return COMMA_LIKE;
+ if (UNISET_PERIOD_LIKE.contains(cp)) return PERIOD_LIKE;
+ if (UNISET_OTHER_GROUPING_SEPARATORS.contains(cp)) return OTHER_GROUPING;
+ return UNKNOWN;
+ }
+ }
+ }
+
+ private static enum DigitType {
+ INTEGER,
+ FRACTION,
+ EXPONENT
+ }
+
+ /**
+ * Holds a snapshot in time of a single parse path. This includes the digits seen so far, the
+ * current state name, and other properties like the grouping separator used on this parse path,
+ * details about the exponent and negative signs, etc.
+ */
+ private static class StateItem {
+ // Parser state:
+ // The "trailingChars" is used to keep track of how many characters from the end of the string
+ // are ignorable and should be removed from the parse position should this item be accepted.
+ // The "score" is used to help rank two otherwise equivalent parse paths. Currently, the only
+ // function giving points to the score is prefix/suffix.
+ StateName name;
+ int trailingCount;
+ int score;
+
+ // Numerical value:
+ FormatQuantity4 fq = new FormatQuantity4();
+ int numDigits;
+ int trailingZeros;
+ int exponent;
+
+ // Other items that we've seen:
+ int groupingCp;
+ long groupingWidths;
+ String isoCode;
+ boolean sawNegative;
+ boolean sawNegativeExponent;
+ boolean sawCurrency;
+ boolean sawNaN;
+ boolean sawInfinity;
+ AffixHolder affix;
+ boolean sawPrefix;
+ boolean sawSuffix;
+ boolean sawDecimalPoint;
+ boolean sawExponentDigit;
+
+ // Data for intermediate parsing steps:
+ StateName returnTo1;
+ StateName returnTo2;
+ // For string literals:
+ CharSequence currentString;
+ int currentOffset;
+ boolean currentTrailing;
+ // For affix patterns:
+ CharSequence currentAffixPattern;
+ long currentStepwiseParserTag;
+ // For currency:
+ TextTrieMap<CurrencyStringInfo>.ParseState currentCurrencyTrieState;
+ // For multi-code-point digits:
+ TextTrieMap<Byte>.ParseState currentDigitTrieState;
+ DigitType currentDigitType;
+
+ // Identification for path tracing:
+ final char id;
+ String path;
+
+ StateItem(char _id) {
+ id = _id;
+ }
+
+ /**
+ * Clears the instance so that it can be re-used.
+ *
+ * @return Myself, for chaining.
+ */
+ StateItem clear() {
+ // Parser state:
+ name = StateName.BEFORE_PREFIX;
+ trailingCount = 0;
+ score = 0;
+
+ // Numerical value:
+ fq.clear();
+ numDigits = 0;
+ trailingZeros = 0;
+ exponent = 0;
+
+ // Other items we've seen:
+ groupingCp = -1;
+ groupingWidths = 0L;
+ isoCode = null;
+ sawNegative = false;
+ sawNegativeExponent = false;
+ sawCurrency = false;
+ sawNaN = false;
+ sawInfinity = false;
+ affix = null;
+ sawPrefix = false;
+ sawSuffix = false;
+ sawDecimalPoint = false;
+ sawExponentDigit = false;
+
+ // Data for intermediate parsing steps:
+ returnTo1 = null;
+ returnTo2 = null;
+ currentString = null;
+ currentOffset = 0;
+ currentTrailing = false;
+ currentAffixPattern = null;
+ currentStepwiseParserTag = 0L;
+ currentCurrencyTrieState = null;
+ currentDigitTrieState = null;
+ currentDigitType = null;
+
+ // Identification for path tracing:
+ // id is constant and is not cleared
+ path = "";
+
+ return this;
+ }
+
+ /**
+ * Sets the internal value of this instance equal to another instance.
+ *
+ * <p>newName and cpOrN1 are required as parameters to this function because every time a code
+ * point is consumed and a state item is copied, both of the corresponding fields should be
+ * updated; it would be an error if they weren't updated.
+ *
+ * @param other The instance to copy from.
+ * @param newName The state name that the new copy should take on.
+ * @param trailing If positive, record this code point as trailing; if negative, reset the
+ * trailing count to zero.
+ * @return Myself, for chaining.
+ */
+ StateItem copyFrom(StateItem other, StateName newName, int trailing) {
+ // Parser state:
+ name = newName;
+ score = other.score;
+
+ // Either reset trailingCount or add the width of the current code point.
+ trailingCount = (trailing < 0) ? 0 : other.trailingCount + Character.charCount(trailing);
+
+ // Numerical value:
+ fq.copyFrom(other.fq);
+ numDigits = other.numDigits;
+ trailingZeros = other.trailingZeros;
+ exponent = other.exponent;
+
+ // Other items we've seen:
+ groupingCp = other.groupingCp;
+ groupingWidths = other.groupingWidths;
+ isoCode = other.isoCode;
+ sawNegative = other.sawNegative;
+ sawNegativeExponent = other.sawNegativeExponent;
+ sawCurrency = other.sawCurrency;
+ sawNaN = other.sawNaN;
+ sawInfinity = other.sawInfinity;
+ affix = other.affix;
+ sawPrefix = other.sawPrefix;
+ sawSuffix = other.sawSuffix;
+ sawDecimalPoint = other.sawDecimalPoint;
+ sawExponentDigit = other.sawExponentDigit;
+
+ // Data for intermediate parsing steps:
+ returnTo1 = other.returnTo1;
+ returnTo2 = other.returnTo2;
+ currentString = other.currentString;
+ currentOffset = other.currentOffset;
+ currentTrailing = other.currentTrailing;
+ currentAffixPattern = other.currentAffixPattern;
+ currentStepwiseParserTag = other.currentStepwiseParserTag;
+ currentCurrencyTrieState = other.currentCurrencyTrieState;
+ currentDigitTrieState = other.currentDigitTrieState;
+ currentDigitType = other.currentDigitType;
+
+ // Record source node if debugging
+ if (DEBUGGING) {
+ path = other.path + other.id;
+ }
+
+ return this;
+ }
+
+ /**
+ * Adds a digit to the internal representation of this instance.
+ *
+ * @param digit The digit that was read from the string.
+ * @param type Whether the digit occured after the decimal point.
+ */
+ void appendDigit(byte digit, DigitType type) {
+ if (type == DigitType.EXPONENT) {
+ sawExponentDigit = true;
+ int newExponent = exponent * 10 + digit;
+ if (newExponent < exponent) {
+ // overflow
+ exponent = Integer.MAX_VALUE;
+ } else {
+ exponent = newExponent;
+ }
+ } else {
+ numDigits++;
+ if (type == DigitType.FRACTION && digit == 0) {
+ trailingZeros++;
+ } else if (type == DigitType.FRACTION) {
+ fq.appendDigit(digit, trailingZeros, false);
+ trailingZeros = 0;
+ } else {
+ fq.appendDigit(digit, 0, true);
+ }
+ }
+ }
+
+ /** @return Whether or not this item contains a valid number. */
+ public boolean hasNumber() {
+ return numDigits > 0 || sawNaN || sawInfinity;
+ }
+
+ /**
+ * Converts the internal digits from this instance into a Number, preferring a Long, then a
+ * BigInteger, then a BigDecimal. A Double is used for NaN, infinity, and -0.0.
+ *
+ * @return The Number. Never null.
+ */
+ Number toNumber(IProperties properties) {
+ // Check for NaN, infinity, and -0.0
+ if (sawNaN) {
+ return Double.NaN;
+ }
+ if (sawInfinity) {
+ if (sawNegative) {
+ return Double.NEGATIVE_INFINITY;
+ } else {
+ return Double.POSITIVE_INFINITY;
+ }
+ }
+ if (fq.isZero() && sawNegative) {
+ return -0.0;
+ }
+
+ // Check for exponent overflow
+ boolean forceBigDecimal = properties.getParseToBigDecimal();
+ if (exponent == Integer.MAX_VALUE) {
+ if (sawNegativeExponent && sawNegative) {
+ return -0.0;
+ } else if (sawNegativeExponent) {
+ return 0.0;
+ } else if (sawNegative) {
+ return Double.NEGATIVE_INFINITY;
+ } else {
+ return Double.POSITIVE_INFINITY;
+ }
+ } else if (exponent > 1000) {
+ // BigDecimals can handle huge values better than BigIntegers.
+ forceBigDecimal = true;
+ }
+
+ // Multipliers must be applied in reverse.
+ BigDecimal multiplier = properties.getMultiplier();
+ if (properties.getMagnitudeMultiplier() != 0) {
+ if (multiplier == null) multiplier = BigDecimal.ONE;
+ multiplier = multiplier.scaleByPowerOfTen(properties.getMagnitudeMultiplier());
+ }
+ int delta = (sawNegativeExponent ? -1 : 1) * exponent;
+
+ // We need to use a math context in order to prevent non-terminating decimal expansions.
+ // This is only used when dividing by the multiplier.
+ MathContext mc = RoundingUtils.getMathContextOr34Digits(properties);
+
+ // Construct the output number.
+ // This is the only step during fast-mode parsing that incurs object creations.
+ BigDecimal result = fq.toBigDecimal();
+ if (sawNegative) result = result.negate();
+ result = result.scaleByPowerOfTen(delta);
+ if (multiplier != null) {
+ result = result.divide(multiplier, mc);
+ }
+ result = result.stripTrailingZeros();
+ if (forceBigDecimal || result.scale() > 0) {
+ return result;
+ } else if (result.compareTo(MIN_LONG_AS_BIG_DECIMAL) >= 0
+ && result.compareTo(MAX_LONG_AS_BIG_DECIMAL) <= 0) {
+ return result.longValueExact();
+ } else {
+ return result.toBigIntegerExact();
+ }
+ }
+
+ /**
+ * Converts the internal digits to a number, and also associates the number with the parsed
+ * currency.
+ *
+ * @return The CurrencyAmount. Never null.
+ */
+ public CurrencyAmount toCurrencyAmount(IProperties properties) {
+ assert isoCode != null;
+ Number number = toNumber(properties);
+ Currency currency = Currency.getInstance(isoCode);
+ return new CurrencyAmount(number, currency);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("[");
+ sb.append(path);
+ sb.append("] ");
+ sb.append(name.name());
+ if (name == StateName.INSIDE_STRING) {
+ sb.append("{");
+ sb.append(currentString);
+ sb.append(":");
+ sb.append(currentOffset);
+ sb.append("}");
+ }
+ if (name == StateName.INSIDE_AFFIX_PATTERN) {
+ sb.append("{");
+ sb.append(currentAffixPattern);
+ sb.append(":");
+ sb.append(AffixPatternUtils.getOffset(currentStepwiseParserTag) - 1);
+ sb.append("}");
+ }
+ sb.append(" ");
+ sb.append(fq.toBigDecimal());
+ sb.append(" grouping:");
+ sb.append(groupingCp == -1 ? new char[] {'?'} : Character.toChars(groupingCp));
+ sb.append(" widths:");
+ sb.append(Long.toHexString(groupingWidths));
+ sb.append(" seen:");
+ sb.append(sawNegative ? 1 : 0);
+ sb.append(sawNegativeExponent ? 1 : 0);
+ sb.append(sawNaN ? 1 : 0);
+ sb.append(sawInfinity ? 1 : 0);
+ sb.append(sawPrefix ? 1 : 0);
+ sb.append(sawSuffix ? 1 : 0);
+ sb.append(sawDecimalPoint ? 1 : 0);
+ sb.append(" trailing:");
+ sb.append(trailingCount);
+ sb.append(" score:");
+ sb.append(score);
+ sb.append(" affix:");
+ sb.append(affix);
+ sb.append(" currency:");
+ sb.append(isoCode);
+ return sb.toString();
+ }
+ }
+
+ /**
+ * Holds an ordered list of {@link StateItem} and other metadata about the string to be parsed.
+ * There are two internal arrays of {@link StateItem}, which are swapped back and forth in order
+ * to avoid object creations. The items in one array can be populated at the same time that items
+ * in the other array are being read from.
+ */
+ private static class ParserState {
+
+ // Basic ParserStateItem lists:
+ StateItem[] items = new StateItem[16];
+ StateItem[] prevItems = new StateItem[16];
+ int length;
+ int prevLength;
+
+ // Properties and Symbols memory:
+ IProperties properties;
+ DecimalFormatSymbols symbols;
+ ParseMode mode;
+ boolean caseSensitive;
+ boolean parseCurrency;
+ GroupingMode groupingMode;
+
+ // Other pre-computed fields:
+ int decimalCp1;
+ int decimalCp2;
+ int groupingCp1;
+ int groupingCp2;
+ SeparatorType decimalType1;
+ SeparatorType decimalType2;
+ SeparatorType groupingType1;
+ SeparatorType groupingType2;
+
+ TextTrieMap<Byte> digitTrie;
+ Set<AffixHolder> affixHolders = new HashSet<AffixHolder>();
+
+ ParserState() {
+ for (int i = 0; i < items.length; i++) {
+ items[i] = new StateItem((char) ('A' + i));
+ prevItems[i] = new StateItem((char) ('A' + i));
+ }
+ }
+
+ /**
+ * Clears the internal state in order to prepare for parsing a new string.
+ *
+ * @return Myself, for chaining.
+ */
+ ParserState clear() {
+ length = 0;
+ prevLength = 0;
+ digitTrie = null;
+ affixHolders.clear();
+ return this;
+ }
+
+ /**
+ * Swaps the internal arrays of {@link StateItem}. Sets the length of the primary list to zero,
+ * so that it can be appended to.
+ */
+ void swap() {
+ StateItem[] temp = prevItems;
+ prevItems = items;
+ items = temp;
+ prevLength = length;
+ length = 0;
+ }
+
+ /**
+ * Swaps the internal arrays of {@link StateItem}. Sets the length of the primary list to the
+ * length of the previous list, so that it can be read from.
+ */
+ void swapBack() {
+ StateItem[] temp = prevItems;
+ prevItems = items;
+ items = temp;
+ length = prevLength;
+ prevLength = 0;
+ }
+
+ /**
+ * Gets the next available {@link StateItem} from the primary list for writing. This method
+ * should be thought of like a list append method, except that there are no object creations
+ * taking place.
+ *
+ * <p>It is the caller's responsibility to call either {@link StateItem#clear} or {@link
+ * StateItem#copyFrom} on the returned object.
+ *
+ * @return A dirty {@link StateItem}.
+ */
+ StateItem getNext() {
+ if (length >= items.length) {
+ // TODO: What to do here? Expand the array?
+ // This case is rare and would happen only with specially designed input.
+ // For now, just overwrite the last entry.
+ length = items.length - 1;
+ }
+ StateItem item = items[length];
+ length++;
+ return item;
+ }
+
+ /** @return The index of the last inserted StateItem via a call to {@link #getNext}. */
+ public int lastInsertedIndex() {
+ assert length > 0;
+ return length - 1;
+ }
+
+ /**
+ * Gets a {@link StateItem} from the primary list. Assumes that the item has already been added
+ * via a call to {@link #getNext}.
+ *
+ * @param i The index of the item to get.
+ * @return The item.
+ */
+ public StateItem getItem(int i) {
+ assert i >= 0 && i < length;
+ return items[i];
+ }
+ }
+
+ /**
+ * A wrapper for affixes. Affixes can be string-based or pattern-based, and they can come from
+ * several sources, including the property bag and the locale paterns from CLDR data.
+ */
+ private static class AffixHolder {
+ final String p; // prefix
+ final String s; // suffix
+ final boolean strings;
+ final boolean negative;
+
+ static final AffixHolder EMPTY_POSITIVE = new AffixHolder("", "", true, false);
+ static final AffixHolder EMPTY_NEGATIVE = new AffixHolder("", "", true, true);
+
+ static void addToState(ParserState state, IProperties properties) {
+ AffixHolder pp = fromPropertiesPositivePattern(properties);
+ AffixHolder np = fromPropertiesNegativePattern(properties);
+ AffixHolder ps = fromPropertiesPositiveString(properties);
+ AffixHolder ns = fromPropertiesNegativeString(properties);
+ if (pp != null) state.affixHolders.add(pp);
+ if (ps != null) state.affixHolders.add(ps);
+ if (np != null) state.affixHolders.add(np);
+ if (ns != null) state.affixHolders.add(ns);
+ }
+
+ static AffixHolder fromPropertiesPositivePattern(IProperties properties) {
+ String ppp = properties.getPositivePrefixPattern();
+ String psp = properties.getPositiveSuffixPattern();
+ if (properties.getSignAlwaysShown()) {
+ // TODO: This logic is somewhat duplicated from PNAffixGenerator.
+ boolean foundSign = false;
+ String npp = properties.getNegativePrefixPattern();
+ String nsp = properties.getNegativeSuffixPattern();
+ if (AffixPatternUtils.containsType(npp, AffixPatternUtils.TYPE_MINUS_SIGN)) {
+ foundSign = true;
+ ppp = AffixPatternUtils.replaceType(npp, AffixPatternUtils.TYPE_MINUS_SIGN, '+');
+ }
+ if (AffixPatternUtils.containsType(nsp, AffixPatternUtils.TYPE_MINUS_SIGN)) {
+ foundSign = true;
+ psp = AffixPatternUtils.replaceType(nsp, AffixPatternUtils.TYPE_MINUS_SIGN, '+');
+ }
+ if (!foundSign) {
+ ppp = "+" + ppp;
+ }
+ }
+ return getInstance(ppp, psp, false, false);
+ }
+
+ static AffixHolder fromPropertiesNegativePattern(IProperties properties) {
+ String npp = properties.getNegativePrefixPattern();
+ String nsp = properties.getNegativeSuffixPattern();
+ if (npp == null && nsp == null) {
+ npp = properties.getPositivePrefixPattern();
+ nsp = properties.getPositiveSuffixPattern();
+ if (npp == null) {
+ npp = "-";
+ } else {
+ npp = "-" + npp;
+ }
+ }
+ return getInstance(npp, nsp, false, true);
+ }
+
+ static AffixHolder fromPropertiesPositiveString(IProperties properties) {
+ String pp = properties.getPositivePrefix();
+ String ps = properties.getPositiveSuffix();
+ if (pp == null && ps == null) return null;
+ return getInstance(pp, ps, true, false);
+ }
+
+ static AffixHolder fromPropertiesNegativeString(IProperties properties) {
+ String np = properties.getNegativePrefix();
+ String ns = properties.getNegativeSuffix();
+ if (np == null && ns == null) return null;
+ return getInstance(np, ns, true, true);
+ }
+
+ static AffixHolder getInstance(String p, String s, boolean strings, boolean negative) {
+ if (p == null && s == null) return negative ? EMPTY_NEGATIVE : EMPTY_POSITIVE;
+ if (p == null) p = "";
+ if (s == null) s = "";
+ if (p.length() == 0 && s.length() == 0) return negative ? EMPTY_NEGATIVE : EMPTY_POSITIVE;
+ return new AffixHolder(p, s, strings, negative);
+ }
+
+ AffixHolder(String pp, String sp, boolean strings, boolean negative) {
+ this.p = pp;
+ this.s = sp;
+ this.strings = strings;
+ this.negative = negative;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null) return false;
+ if (this == other) return true;
+ if (!(other instanceof AffixHolder)) return false;
+ AffixHolder _other = (AffixHolder) other;
+ if (!p.equals(_other.p)) return false;
+ if (!s.equals(_other.s)) return false;
+ if (strings != _other.strings) return false;
+ if (negative != _other.negative) return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return p.hashCode() ^ s.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{");
+ sb.append(p);
+ sb.append("|");
+ sb.append(s);
+ sb.append("|");
+ sb.append(strings ? 'S' : 'P');
+ sb.append("}");
+ return sb.toString();
+ }
+ }
+
+ /**
+ * A class that holds information about all currency affix patterns for the locale. This allows
+ * the parser to accept currencies in any format that are valid for the locale.
+ */
+ private static class CurrencyAffixPatterns {
+ private final Set<AffixHolder> set = new HashSet<AffixHolder>();
+
+ private static final ConcurrentHashMap<ULocale, CurrencyAffixPatterns> currencyAffixPatterns =
+ new ConcurrentHashMap<ULocale, CurrencyAffixPatterns>();
+
+ static void addToState(ULocale uloc, ParserState state) {
+ CurrencyAffixPatterns value = currencyAffixPatterns.get(uloc);
+ if (value == null) {
+ // There can be multiple threads computing the same CurrencyAffixPatterns simultaneously,
+ // but that scenario is harmless.
+ CurrencyAffixPatterns newValue = new CurrencyAffixPatterns(uloc);
+ currencyAffixPatterns.putIfAbsent(uloc, newValue);
+ value = currencyAffixPatterns.get(uloc);
+ }
+ state.affixHolders.addAll(value.set);
+ }
+
+ private CurrencyAffixPatterns(ULocale uloc) {
+ // Get the basic currency pattern.
+ String pattern = NumberFormat.getPatternForStyle(uloc, NumberFormat.CURRENCYSTYLE);
+ addPattern(pattern);
+
+ // Get the currency plural patterns.
+ // TODO: Update this after CurrencyPluralInfo is replaced.
+ CurrencyPluralInfo pluralInfo = CurrencyPluralInfo.getInstance(uloc);
+ for (StandardPlural plural : StandardPlural.VALUES) {
+ pattern = pluralInfo.getCurrencyPluralPattern(plural.getKeyword());
+ addPattern(pattern);
+ }
+ }
+
+ private static final ThreadLocal<Properties> threadLocalProperties =
+ new ThreadLocal<Properties>() {
+ @Override
+ protected Properties initialValue() {
+ return new Properties();
+ }
+ };
+
+ private void addPattern(String pattern) {
+ Properties properties = threadLocalProperties.get();
+ try {
+ PatternString.parseToExistingProperties(pattern, properties);
+ } catch (IllegalArgumentException e) {
+ // This should only happen if there is a bug in CLDR data. Fail silently.
+ }
+ set.add(AffixHolder.fromPropertiesPositivePattern(properties));
+ set.add(AffixHolder.fromPropertiesNegativePattern(properties));
+ }
+ }
+
+ /**
+ * Makes a {@link TextTrieMap} for parsing digit strings. A trie is required only if the digit
+ * strings are longer than one code point. In order for this to be the case, the user would have
+ * needed to specify custom multi-character digits, like "(0)".
+ *
+ * @param digitStrings The list of digit strings from DecimalFormatSymbols.
+ * @return A trie, or null if a trie is not required.
+ */
+ static TextTrieMap<Byte> makeDigitTrie(String[] digitStrings) {
+ boolean requiresTrie = false;
+ for (int i = 0; i < 10; i++) {
+ String str = digitStrings[i];
+ if (Character.charCount(Character.codePointAt(str, 0)) != str.length()) {
+ requiresTrie = true;
+ break;
+ }
+ }
+ if (!requiresTrie) return null;
+
+ // TODO: Consider caching the tries so they don't need to be re-created run to run.
+ // (Low-priority since multi-character digits are rare in practice)
+ TextTrieMap<Byte> trieMap = new TextTrieMap<Byte>(false);
+ for (int i = 0; i < 10; i++) {
+ trieMap.put(digitStrings[i], (byte) i);
+ }
+ return trieMap;
+ }
+
+ protected static final ThreadLocal<ParserState> threadLocalParseState =
+ new ThreadLocal<ParserState>() {
+ @Override
+ protected ParserState initialValue() {
+ return new ParserState();
+ }
+ };
+
+ protected static final ThreadLocal<ParsePosition> threadLocalParsePosition =
+ new ThreadLocal<ParsePosition>() {
+ @Override
+ protected ParsePosition initialValue() {
+ return new ParsePosition(0);
+ }
+ };
+
+ /**
+ * @deprecated This API is ICU internal only. TODO: Remove this set from ScientificNumberFormat.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public static final UnicodeSet UNISET_PLUS =
+ new UnicodeSet(
+ 0x002B, 0x002B, 0x207A, 0x207A, 0x208A, 0x208A, 0x2795, 0x2795, 0xFB29, 0xFB29,
+ 0xFE62, 0xFE62, 0xFF0B, 0xFF0B)
+ .freeze();
+
+ /**
+ * @deprecated This API is ICU internal only. TODO: Remove this set from ScientificNumberFormat.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public static final UnicodeSet UNISET_MINUS =
+ new UnicodeSet(
+ 0x002D, 0x002D, 0x207B, 0x207B, 0x208B, 0x208B, 0x2212, 0x2212, 0x2796, 0x2796,
+ 0xFE63, 0xFE63, 0xFF0D, 0xFF0D)
+ .freeze();
+
+ public static Number parse(String input, IProperties properties, DecimalFormatSymbols symbols) {
+ ParsePosition ppos = threadLocalParsePosition.get();
+ ppos.setIndex(0);
+ return parse(input, ppos, properties, symbols);
+ }
+
+ // TODO: DELETE ME once debugging is finished
+ public static volatile boolean DEBUGGING = false;
+
+ /**
+ * Implements an iterative parser that maintains a lists of possible states at each code point in
+ * the string. At each code point in the string, the list of possible states is updated based on
+ * the states coming from the previous code point. The parser stops when it reaches the end of the
+ * string or when there are no possible parse paths remaining in the string.
+ *
+ * <p>TODO: This API is not fully flushed out. Right now this is internal-only.
+ *
+ * @param input The string to parse.
+ * @param ppos A {@link ParsePosition} to hold the index at which parsing stopped.
+ * @param properties A property bag, used only for determining the prefix/suffix strings and the
+ * padding character.
+ * @param symbols A {@link DecimalFormatSymbols} object, used for determining locale-specific
+ * symbols for grouping/decimal separators, digit strings, and prefix/suffix substitutions.
+ * @return A Number matching the parser's best interpretation of the string.
+ */
+ public static Number parse(
+ CharSequence input,
+ ParsePosition ppos,
+ IProperties properties,
+ DecimalFormatSymbols symbols) {
+ StateItem best = _parse(input, ppos, false, properties, symbols);
+ return (best == null) ? null : best.toNumber(properties);
+ }
+
+ public static CurrencyAmount parseCurrency(
+ String input, IProperties properties, DecimalFormatSymbols symbols) throws ParseException {
+ return parseCurrency(input, null, properties, symbols);
+ }
+
+ public static CurrencyAmount parseCurrency(
+ CharSequence input, ParsePosition ppos, IProperties properties, DecimalFormatSymbols symbols)
+ throws ParseException {
+ if (ppos == null) {
+ ppos = threadLocalParsePosition.get();
+ ppos.setIndex(0);
+ ppos.setErrorIndex(-1);
+ }
+ StateItem best = _parse(input, ppos, true, properties, symbols);
+ return (best == null) ? null : best.toCurrencyAmount(properties);
+ }
+
+ private static StateItem _parse(
+ CharSequence input,
+ ParsePosition ppos,
+ boolean parseCurrency,
+ IProperties properties,
+ DecimalFormatSymbols symbols) {
+
+ if (input == null || ppos == null || properties == null || symbols == null) {
+ throw new IllegalArgumentException("All arguments are required for parse.");
+ }
+
+ ParseMode mode = properties.getParseMode();
+ if (mode == null) mode = ParseMode.LENIENT;
+ boolean integerOnly = properties.getParseIntegerOnly();
+ boolean ignoreExponent = properties.getParseNoExponent();
+ boolean ignoreGrouping = properties.getGroupingSize() < 0;
+
+ // Set up the initial state
+ ParserState state = threadLocalParseState.get().clear();
+ state.properties = properties;
+ state.symbols = symbols;
+ state.mode = mode;
+ state.parseCurrency = parseCurrency;
+ state.groupingMode = properties.getParseGroupingMode();
+ if (state.groupingMode == null) state.groupingMode = GroupingMode.DEFAULT;
+ state.caseSensitive = properties.getParseCaseSensitive();
+ state.decimalCp1 = Character.codePointAt(symbols.getDecimalSeparatorString(), 0);
+ state.decimalCp2 = Character.codePointAt(symbols.getMonetaryDecimalSeparatorString(), 0);
+ state.groupingCp1 = Character.codePointAt(symbols.getGroupingSeparatorString(), 0);
+ state.groupingCp2 = Character.codePointAt(symbols.getMonetaryGroupingSeparatorString(), 0);
+ state.decimalType1 = SeparatorType.fromCp(state.decimalCp1, mode);
+ state.decimalType2 = SeparatorType.fromCp(state.decimalCp2, mode);
+ state.groupingType1 = SeparatorType.fromCp(state.groupingCp1, mode);
+ state.groupingType2 = SeparatorType.fromCp(state.groupingCp2, mode);
+ StateItem initialStateItem = state.getNext().clear();
+ initialStateItem.name = StateName.BEFORE_PREFIX;
+
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ state.digitTrie = makeDigitTrie(symbols.getDigitStringsLocal());
+ AffixHolder.addToState(state, properties);
+ if (parseCurrency) {
+ CurrencyAffixPatterns.addToState(symbols.getULocale(), state);
+ }
+ }
+
+ if (DEBUGGING) {
+ System.out.println("Parsing: " + input);
+ System.out.println(properties);
+ System.out.println(state.affixHolders);
+ }
+
+ // Start walking through the string, one codepoint at a time. Backtracking is not allowed. This
+ // is to enforce linear runtime and prevent cases that could result in an infinite loop.
+ int offset = ppos.getIndex();
+ for (; offset < input.length(); ) {
+ int cp = Character.codePointAt(input, offset);
+ state.swap();
+ for (int i = 0; i < state.prevLength; i++) {
+ StateItem item = state.prevItems[i];
+ if (DEBUGGING) {
+ System.out.println(":" + offset + item.id + " " + item);
+ }
+
+ // In the switch statement below, if you see a line like:
+ // if (state.length > 0 && mode == ParseMode.FAST) break;
+ // it is used for accelerating the fast parse mode. The check is performed only in the
+ // states BEFORE_PREFIX, AFTER_INTEGER_DIGIT, and AFTER_FRACTION_DIGIT, which are the
+ // most common states.
+
+ switch (item.name) {
+ case BEFORE_PREFIX:
+ // Beginning of string
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptMinusOrPlusSign(cp, StateName.BEFORE_PREFIX, state, item, false);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ }
+ acceptIntegerDigit(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptBidi(cp, StateName.BEFORE_PREFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptWhitespace(cp, StateName.BEFORE_PREFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptPadding(cp, StateName.BEFORE_PREFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptNan(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptInfinity(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ if (!integerOnly) {
+ acceptDecimalPoint(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ acceptPrefix(cp, StateName.AFTER_PREFIX, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ if (!ignoreGrouping) {
+ acceptGrouping(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ }
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.BEFORE_PREFIX, state, item);
+ }
+ }
+ break;
+
+ case AFTER_PREFIX:
+ // Prefix is consumed
+ acceptBidi(cp, StateName.AFTER_PREFIX, state, item);
+ acceptPadding(cp, StateName.AFTER_PREFIX, state, item);
+ acceptNan(cp, StateName.BEFORE_SUFFIX, state, item);
+ acceptInfinity(cp, StateName.BEFORE_SUFFIX, state, item);
+ acceptIntegerDigit(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+ if (!integerOnly) {
+ acceptDecimalPoint(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptWhitespace(cp, StateName.AFTER_PREFIX, state, item);
+ if (!ignoreGrouping) {
+ acceptGrouping(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+ }
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.AFTER_PREFIX, state, item);
+ }
+ }
+ break;
+
+ case AFTER_INTEGER_DIGIT:
+ // Previous character was an integer digit (or grouping/whitespace)
+ acceptIntegerDigit(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ if (!integerOnly) {
+ acceptDecimalPoint(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ }
+ if (!ignoreGrouping) {
+ acceptGrouping(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ }
+ acceptBidi(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptPadding(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ if (!ignoreExponent) {
+ acceptExponentSeparator(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptWhitespace(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ // TODO(sffc): acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.BEFORE_SUFFIX, state, item);
+ }
+ }
+ break;
+
+ case AFTER_FRACTION_DIGIT:
+ // We encountered a decimal point
+ acceptFractionDigit(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptBidi(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptPadding(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ if (!ignoreExponent) {
+ acceptExponentSeparator(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptWhitespace(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ // TODO(sffc): acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.BEFORE_SUFFIX, state, item);
+ }
+ }
+ break;
+
+ case AFTER_EXPONENT_SEPARATOR:
+ acceptBidi(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item);
+ acceptMinusOrPlusSign(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item, true);
+ acceptExponentDigit(cp, StateName.AFTER_EXPONENT_DIGIT, state, item);
+ break;
+
+ case AFTER_EXPONENT_DIGIT:
+ acceptBidi(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ acceptPadding(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ acceptExponentDigit(cp, StateName.AFTER_EXPONENT_DIGIT, state, item);
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptWhitespace(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ // TODO(sffc): acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false);
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ }
+ }
+ break;
+
+ case BEFORE_SUFFIX:
+ // Accept whitespace, suffixes, and exponent separators
+ acceptBidi(cp, StateName.BEFORE_SUFFIX, state, item);
+ acceptPadding(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (!ignoreExponent) {
+ acceptExponentSeparator(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptWhitespace(cp, StateName.BEFORE_SUFFIX, state, item);
+ // TODO(sffc): acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false);
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.BEFORE_SUFFIX, state, item);
+ }
+ }
+ break;
+
+ case BEFORE_SUFFIX_SEEN_EXPONENT:
+ // Accept whitespace and suffixes but not exponent separators
+ acceptBidi(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ acceptPadding(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptWhitespace(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ // TODO(sffc): acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item, false);
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ }
+ }
+ break;
+
+ case AFTER_SUFFIX:
+ if ((mode == ParseMode.LENIENT || mode == ParseMode.FAST) && parseCurrency) {
+ // Continue traversing in case there is a currency symbol to consume
+ acceptBidi(cp, StateName.AFTER_SUFFIX, state, item);
+ acceptPadding(cp, StateName.AFTER_SUFFIX, state, item);
+ acceptWhitespace(cp, StateName.AFTER_SUFFIX, state, item);
+ // TODO(sffc): acceptMinusOrPlusSign(cp, StateName.AFTER_SUFFIX, state, item, false);
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.AFTER_SUFFIX, state, item);
+ }
+ }
+ // Otherwise, do not accept any more characters.
+ break;
+
+ case INSIDE_CURRENCY:
+ acceptCurrencyOffset(cp, state, item);
+ break;
+
+ case INSIDE_DIGIT:
+ acceptDigitTrieOffset(cp, state, item);
+ break;
+
+ case INSIDE_STRING:
+ acceptStringOffset(cp, state, item);
+ break;
+
+ case INSIDE_AFFIX_PATTERN:
+ acceptAffixPatternOffset(cp, state, item);
+ break;
+ }
+ }
+
+ if (state.length == 0) {
+ // No parse paths continue past this point. We have found the longest parsable string
+ // from the input. Restore previous state without the offset and break.
+ state.swapBack();
+ break;
+ }
+
+ offset += Character.charCount(cp);
+ }
+
+ // Post-processing
+ if (state.length == 0) {
+ if (DEBUGGING) {
+ System.out.println("No matches found");
+ System.out.println("- - - - - - - - - -");
+ }
+ return null;
+ } else {
+
+ // Loop through the candidates. "continue" skips a candidate as invalid.
+ StateItem best = null;
+ outer:
+ for (int i = 0; i < state.length; i++) {
+ StateItem item = state.items[i];
+
+ if (DEBUGGING) {
+ System.out.println(":end " + item);
+ }
+
+ // Check that at least one digit was read.
+ if (!item.hasNumber()) {
+ if (DEBUGGING) System.out.println("-> rejected due to no number value");
+ continue;
+ }
+
+ if (mode == ParseMode.STRICT) {
+ // Perform extra checks for strict mode.
+ // We require that the affixes match.
+ boolean sawPrefix = item.sawPrefix || (item.affix != null && item.affix.p.isEmpty());
+ boolean sawSuffix = item.sawSuffix || (item.affix != null && item.affix.s.isEmpty());
+ boolean hasEmptyAffix =
+ state.affixHolders.contains(AffixHolder.EMPTY_POSITIVE)
+ || state.affixHolders.contains(AffixHolder.EMPTY_NEGATIVE);
+ if (sawPrefix && sawSuffix) {
+ // OK
+ } else if (!sawPrefix && !sawSuffix && hasEmptyAffix) {
+ // OK
+ } else {
+ // Has a prefix or suffix that doesn't match
+ if (DEBUGGING) System.out.println("-> rejected due to mismatched prefix/suffix");
+ continue;
+ }
+
+ // Check for scientific notation.
+ if (properties.getMinimumExponentDigits() > 0 && !item.sawExponentDigit) {
+ if (DEBUGGING) System.out.println("-> reject due to lack of exponent");
+ continue;
+ }
+
+ // Check that grouping sizes are valid.
+ int grouping1 = properties.getGroupingSize();
+ int grouping2 = properties.getSecondaryGroupingSize();
+ grouping1 = grouping1 > 0 ? grouping1 : grouping2;
+ grouping2 = grouping2 > 0 ? grouping2 : grouping1;
+ long groupingWidths = item.groupingWidths;
+ int numGroupingRegions = 16 - Long.numberOfLeadingZeros(groupingWidths) / 4;
+ // If the last grouping is zero, accept strings like "1," but reject string like "1,.23"
+ // Strip off multiple last-groupings to handle cases like "123,," or "123 "
+ while (numGroupingRegions > 1 && (groupingWidths & 0xf) == 0) {
+ if (item.sawDecimalPoint) {
+ if (DEBUGGING) System.out.println("-> rejected due to decimal point after grouping");
+ continue outer;
+ } else {
+ groupingWidths >>>= 4;
+ numGroupingRegions--;
+ }
+ }
+ if (grouping1 < 0) {
+ // OK (no grouping data available)
+ } else if (numGroupingRegions <= 1) {
+ // OK (no grouping digits)
+ } else if ((groupingWidths & 0xf) != grouping1) {
+ // First grouping size is invalid
+ if (DEBUGGING) System.out.println("-> rejected due to first grouping violation");
+ continue;
+ } else if (((groupingWidths >>> ((numGroupingRegions - 1) * 4)) & 0xf) > grouping2) {
+ // String like "1234,567" where the highest grouping is too large
+ if (DEBUGGING) System.out.println("-> rejected due to final grouping violation");
+ continue;
+ } else {
+ for (int j = 1; j < numGroupingRegions - 1; j++) {
+ if (((groupingWidths >>> (j * 4)) & 0xf) != grouping2) {
+ // A grouping size somewhere in the middle is invalid
+ if (DEBUGGING) System.out.println("-> rejected due to inner grouping violation");
+ continue outer;
+ }
+ }
+ }
+ }
+
+ // Optionally require that the presence of a decimal point matches the pattern.
+ if (properties.getDecimalPatternMatchRequired()
+ && item.sawDecimalPoint != PositiveDecimalFormat.allowsDecimalPoint(properties)) {
+ if (DEBUGGING) System.out.println("-> rejected due to decimal point violation");
+ continue;
+ }
+
+ // When parsing currencies, require that a currency symbol was found.
+ if (parseCurrency && !item.sawCurrency) {
+ if (DEBUGGING) System.out.println("-> rejected due to lack of currency");
+ continue;
+ }
+
+ // If we get here, then this candidate is acceptable.
+ // Use the earliest candidate in the list, or the one with the highest score, or the
+ // one with the fewest trailing digits.
+ if (best == null) {
+ best = item;
+ } else if (item.score > best.score) {
+ best = item;
+ } else if (item.trailingCount < best.trailingCount) {
+ best = item;
+ }
+ }
+
+ if (DEBUGGING) {
+ System.out.println("- - - - - - - - - -");
+ }
+
+ if (best != null) {
+ ppos.setIndex(offset - best.trailingCount);
+ return best;
+ } else {
+ ppos.setErrorIndex(offset);
+ return null;
+ }
+ }
+ }
+
+ /**
+ * If <code>cp</code> is whitespace (as determined by the unicode set {@link #UNISET_WHITESPACE}),
+ * copies <code>item</code> to the new list in <code>state</code> and sets its state name to
+ * <code>nextName</code>.
+ *
+ * @param cp The code point to check.
+ * @param nextName The new state name if the check passes.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptWhitespace(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ if (UNISET_WHITESPACE.contains(cp)) {
+ state.getNext().copyFrom(item, nextName, cp);
+ }
+ }
+
+ /**
+ * If <code>cp</code> is a bidi control character (as determined by the unicode set {@link
+ * #UNISET_BIDI}), copies <code>item</code> to the new list in <code>state</code> and sets its
+ * state name to <code>nextName</code>.
+ *
+ * @param cp The code point to check.
+ * @param nextName The new state name if the check passes.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptBidi(int cp, StateName nextName, ParserState state, StateItem item) {
+ if (UNISET_BIDI.contains(cp)) {
+ state.getNext().copyFrom(item, nextName, cp);
+ }
+ }
+
+ /**
+ * If <code>cp</code> is a padding character (as determined by {@link ParserState#paddingCp}),
+ * copies <code>item</code> to the new list in <code>state</code> and sets its state name to
+ * <code>nextName</code>.
+ *
+ * @param cp The code point to check.
+ * @param nextName The new state name if the check passes.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptPadding(int cp, StateName nextName, ParserState state, StateItem item) {
+ CharSequence padding = state.properties.getPadString();
+ if (padding == null || padding.length() == 0) return;
+ int referenceCp = Character.codePointAt(padding, 0);
+ if (cp == referenceCp) {
+ state.getNext().copyFrom(item, nextName, cp);
+ }
+ }
+
+ private static void acceptIntegerDigit(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ acceptDigitHelper(cp, nextName, state, item, DigitType.INTEGER);
+ }
+
+ private static void acceptFractionDigit(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ acceptDigitHelper(cp, nextName, state, item, DigitType.FRACTION);
+ }
+
+ private static void acceptExponentDigit(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ acceptDigitHelper(cp, nextName, state, item, DigitType.EXPONENT);
+ }
+
+ /**
+ * If <code>cp</code> is a digit character (as determined by either {@link UCharacter#digit} or
+ * {@link ParserState#digitCps}), copies <code>item</code> to the new list in <code>state</code>
+ * and sets its state name to one determined by <code>type</code>. Also copies the digit into a
+ * field in the new item determined by <code>type</code>.
+ *
+ * @param cp The code point to check.
+ * @param nextName The state to set if a digit is accepted.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ * @param type The digit type, which determines the next state and the field into which to insert
+ * the digit.
+ */
+ private static void acceptDigitHelper(
+ int cp, StateName nextName, ParserState state, StateItem item, DigitType type) {
+ // Check the Unicode digit character property
+ byte digit = (byte) UCharacter.digit(cp, 10);
+ StateItem next = null;
+
+ // Look for the digit:
+ if (digit >= 0) {
+ // Code point is a number
+ next = state.getNext().copyFrom(item, nextName, -1);
+ }
+
+ // Do not perform the expensive string manipulations in fast mode.
+ if (digit < 0 && (state.mode == ParseMode.LENIENT || state.mode == ParseMode.STRICT)) {
+ if (state.digitTrie == null) {
+ // Check custom digits, all of which are at most one code point
+ for (byte d = 0; d < 10; d++) {
+ int referenceCp = Character.codePointAt(state.symbols.getDigitStringsLocal()[d], 0);
+ if (cp == referenceCp) {
+ digit = d;
+ next = state.getNext().copyFrom(item, nextName, -1);
+ }
+ }
+ } else {
+ // Custom digits have more than one code point
+ acceptDigitTrie(cp, nextName, state, item, type);
+ }
+ }
+
+ // Save state
+ recordDigit(next, digit, type);
+ }
+
+ /**
+ * Helper function for {@link acceptDigit} and {@link acceptDigitTrie} to save a complete digit in
+ * a state item and update grouping widths.
+ *
+ * @param next The new StateItem
+ * @param digit The digit to record
+ * @param type The type of the digit to record (INTEGER, FRACTION, or EXPONENT)
+ */
+ private static void recordDigit(StateItem next, byte digit, DigitType type) {
+ if (next == null) return;
+ next.appendDigit(digit, type);
+ if (type == DigitType.INTEGER && (next.groupingWidths & 0xf) < 15) {
+ next.groupingWidths++;
+ }
+ }
+
+ /**
+ * If <code>cp</code> is a sign (as determined by the unicode sets {@link #UNISET_PLUS} and {@link
+ * #UNISET_MINUS}), copies <code>item</code> to the new list in <code>state</code>. Loops back to
+ * the same state name.
+ *
+ * @param cp The code point to check.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptMinusOrPlusSign(
+ int cp, StateName nextName, ParserState state, StateItem item, boolean exponent) {
+ acceptMinusSign(cp, nextName, null, state, item, exponent);
+ acceptPlusSign(cp, nextName, null, state, item, exponent);
+ }
+
+ private static long acceptMinusSign(
+ int cp,
+ StateName returnTo1,
+ StateName returnTo2,
+ ParserState state,
+ StateItem item,
+ boolean exponent) {
+ if (UNISET_MINUS.contains(cp)) {
+ StateItem next = state.getNext().copyFrom(item, returnTo1, -1);
+ next.returnTo1 = returnTo2;
+ if (exponent) {
+ next.sawNegativeExponent = true;
+ } else {
+ next.sawNegative = true;
+ }
+ return 1L << state.lastInsertedIndex();
+ } else {
+ return 0L;
+ }
+ }
+
+ private static long acceptPlusSign(
+ int cp,
+ StateName returnTo1,
+ StateName returnTo2,
+ ParserState state,
+ StateItem item,
+ boolean exponent) {
+ if (UNISET_PLUS.contains(cp)) {
+ StateItem next = state.getNext().copyFrom(item, returnTo1, -1);
+ next.returnTo1 = returnTo2;
+ return 1L << state.lastInsertedIndex();
+ } else {
+ return 0L;
+ }
+ }
+
+ /**
+ * If <code>cp</code> is a grouping separator (as determined by the unicode set {@link
+ * #UNISET_GROUPING}), copies <code>item</code> to the new list in <code>state</code> and loops
+ * back to the same state. Also accepts if <code>cp</code> is the locale-specific grouping
+ * separator in {@link ParserState#groupingCp}, in which case the {@link
+ * StateItem#usesLocaleSymbols} flag is also set.
+ *
+ * @param cp The code point to check.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptGrouping(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ // Do not accept mixed grouping separators in the same string.
+ if (item.groupingCp == -1) {
+ // First time seeing a grouping separator.
+ SeparatorType cpType = SeparatorType.fromCp(cp, state.mode);
+
+ // Always accept if exactly the same as the locale grouping separator.
+ if (cp != state.groupingCp1 && cp != state.groupingCp2) {
+ // Reject if not in one of the three primary equivalence classes.
+ if (cpType == SeparatorType.UNKNOWN) {
+ return;
+ }
+ if (state.groupingMode == GroupingMode.RESTRICTED) {
+ // Reject if not in the same class as the locale grouping separator.
+ if (cpType != state.groupingType1 || cpType != state.groupingType2) {
+ return;
+ }
+ } else {
+ // Reject if in the same class as the decimal separator.
+ if (cpType == SeparatorType.COMMA_LIKE
+ && (state.decimalType1 == SeparatorType.COMMA_LIKE
+ || state.decimalType2 == SeparatorType.COMMA_LIKE)) {
+ return;
+ }
+ if (cpType == SeparatorType.PERIOD_LIKE
+ && (state.decimalType1 == SeparatorType.PERIOD_LIKE
+ || state.decimalType2 == SeparatorType.PERIOD_LIKE)) {
+ return;
+ }
+ }
+ }
+
+ // A match was found.
+ StateItem next = state.getNext().copyFrom(item, nextName, cp);
+ next.groupingCp = cp;
+ next.groupingWidths <<= 4;
+ } else {
+ // Have already seen a grouping separator.
+ if (cp == item.groupingCp) {
+ StateItem next = state.getNext().copyFrom(item, nextName, cp);
+ next.groupingWidths <<= 4;
+ }
+ }
+ }
+
+ /**
+ * If <code>cp</code> is a decimal (as determined by the unicode set {@link #UNISET_DECIMAL}),
+ * copies <code>item</code> to the new list in <code>state</code> and goes to {@link
+ * StateName#AFTER_FRACTION_DIGIT}. Also accepts if <code>cp</code> is the locale-specific decimal
+ * point in {@link ParserState#decimalCp}, in which case the {@link StateItem#usesLocaleSymbols}
+ * flag is also set.
+ *
+ * @param cp The code point to check.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptDecimalPoint(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ if (cp == item.groupingCp) {
+ // Don't accept a decimal point that is the same as the grouping separator
+ return;
+ }
+
+ SeparatorType cpType = SeparatorType.fromCp(cp, state.mode);
+
+ // We require that the decimal separator be in the same class as the locale.
+ if (cpType != state.decimalType1 && cpType != state.decimalType2) {
+ return;
+ }
+
+ // If in UNKNOWN or OTHER, require an exact match.
+ if (cpType == SeparatorType.OTHER_GROUPING || cpType == SeparatorType.UNKNOWN) {
+ if (cp != state.decimalCp1 && cp != state.decimalCp2) {
+ return;
+ }
+ }
+
+ // A match was found.
+ StateItem next = state.getNext().copyFrom(item, nextName, -1);
+ next.sawDecimalPoint = true;
+ }
+
+ private static void acceptNan(int cp, StateName nextName, ParserState state, StateItem item) {
+ CharSequence nan = state.symbols.getNaN();
+ long added = acceptString(cp, nextName, null, state, item, nan, 0, false);
+
+ // Set state in the items that were added by the function call
+ for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+ if (((1L << i) & added) != 0) {
+ state.getItem(i).sawNaN = true;
+ }
+ }
+ }
+
+ private static void acceptInfinity(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ CharSequence inf = state.symbols.getInfinity();
+ long added = acceptString(cp, nextName, null, state, item, inf, 0, false);
+
+ // Set state in the items that were added by the function call
+ for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+ if (((1L << i) & added) != 0) {
+ state.getItem(i).sawInfinity = true;
+ }
+ }
+ }
+
+ private static void acceptExponentSeparator(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ CharSequence exp = state.symbols.getExponentSeparator();
+ acceptString(cp, nextName, null, state, item, exp, 0, true);
+ }
+
+ private static void acceptPrefix(int cp, StateName nextName, ParserState state, StateItem item) {
+ for (AffixHolder holder : state.affixHolders) {
+ acceptAffixHolder(cp, nextName, state, item, holder, true);
+ }
+ }
+
+ private static void acceptSuffix(int cp, StateName nextName, ParserState state, StateItem item) {
+ if (item.affix != null) {
+ acceptAffixHolder(cp, nextName, state, item, item.affix, false);
+ } else {
+ for (AffixHolder holder : state.affixHolders) {
+ acceptAffixHolder(cp, nextName, state, item, holder, false);
+ }
+ }
+ }
+
+ private static void acceptAffixHolder(
+ int cp,
+ StateName nextName,
+ ParserState state,
+ StateItem item,
+ AffixHolder holder,
+ boolean prefix) {
+ if (holder == null) return;
+ String str = prefix ? holder.p : holder.s;
+ long added;
+ if (holder.strings) {
+ added = acceptString(cp, nextName, null, state, item, str, 0, false);
+ } else {
+ added =
+ acceptAffixPattern(cp, nextName, state, item, str, AffixPatternUtils.nextToken(0, str));
+ }
+ // Record state in the added entries
+ for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+ if (((1L << i) & added) != 0) {
+ StateItem next = state.getItem(i);
+ next.affix = holder;
+ if (prefix) next.sawPrefix = true;
+ if (!prefix) next.sawSuffix = true;
+ if (holder.negative) next.sawNegative = true;
+ // 10 point reward for consuming a prefix/suffix:
+ next.score += 10;
+ // 1 point reward for positive holders (if there is ambiguity, we want to favor positive):
+ if (!holder.negative) next.score += 1;
+ // 5 point reward for affix holders that have an empty prefix or suffix (we won't see them again):
+ if (!next.sawPrefix && holder.p.isEmpty()) next.score += 5;
+ if (!next.sawSuffix && holder.s.isEmpty()) next.score += 5;
+ }
+ }
+ }
+
+ private static long acceptStringOffset(int cp, ParserState state, StateItem item) {
+ return acceptString(
+ cp,
+ item.returnTo1,
+ item.returnTo2,
+ state,
+ item,
+ item.currentString,
+ item.currentOffset,
+ item.currentTrailing);
+ }
+
+ /**
+ * Accepts a code point if the code point is compatible with the string at the given offset.
+ * Handles runs of ignorable characters.
+ *
+ * <p>This method will add either one or two {@link StateItem} to the {@link ParserState}.
+ *
+ * @param cp The current code point, which will be checked for a match to the string.
+ * @param ret1 The state to return to after reaching the end of the string.
+ * @param ret2 The state to save in <code>returnTo1</code> after reaching the end of the string.
+ * Set to null if returning to the main state loop.
+ * @param trailing true if this string should be ignored for the purposes of recording trailing
+ * code points; false if it trailing count should be reset after reading the string.
+ * @param state The current {@link ParserState}
+ * @param item The current {@link StateItem}
+ * @param str The string against which to check for a match.
+ * @param offset The number of chars into the string. Initial value should be 0.
+ * @param trailing false if this string is strong and should reset trailing count to zero when it
+ * is fully consumed.
+ * @return A bitmask where the bits correspond to the items that were added. Set to 0L if no items
+ * were added.
+ */
+ private static long acceptString(
+ int cp,
+ StateName ret1,
+ StateName ret2,
+ ParserState state,
+ StateItem item,
+ CharSequence str,
+ int offset,
+ boolean trailing) {
+ if (str == null || str.length() == 0) return 0L;
+ return acceptStringOrAffixPatternWithIgnorables(
+ cp, ret1, ret2, state, item, str, offset, trailing, true);
+ }
+
+ private static long acceptStringNonIgnorable(
+ int cp,
+ StateName ret1,
+ StateName ret2,
+ ParserState state,
+ StateItem item,
+ CharSequence str,
+ boolean trailing,
+ int referenceCp,
+ long firstOffsetOrTag,
+ long nextOffsetOrTag) {
+ long added = 0L;
+ int firstOffset = (int) firstOffsetOrTag;
+ int nextOffset = (int) nextOffsetOrTag;
+ if (codePointEquals(referenceCp, cp, state)) {
+ if (firstOffset < str.length()) {
+ added |= acceptStringHelper(cp, ret1, ret2, state, item, str, firstOffset, trailing);
+ }
+ if (nextOffset >= str.length()) {
+ added |= acceptStringHelper(cp, ret1, ret2, state, item, str, nextOffset, trailing);
+ }
+ return added;
+ } else {
+ return 0L;
+ }
+ }
+
+ /**
+ * Internal method that is used to step to the next code point of a string or exit the string if
+ * at the end.
+ *
+ * @param cp See {@link #acceptString}
+ * @param returnTo1 See {@link #acceptString}
+ * @param returnTo2 See {@link #acceptString}
+ * @param state See {@link #acceptString}
+ * @param item See {@link #acceptString}
+ * @param str See {@link #acceptString}
+ * @param newOffset The offset at which the next step should start. If past the end of the string,
+ * exit the string and return to the outer loop.
+ * @param trailing See {@link #acceptString}
+ * @return Bitmask containing one entry, the one that was added.
+ */
+ private static long acceptStringHelper(
+ int cp,
+ StateName returnTo1,
+ StateName returnTo2,
+ ParserState state,
+ StateItem item,
+ CharSequence str,
+ int newOffset,
+ boolean trailing) {
+ StateItem next = state.getNext().copyFrom(item, null, cp);
+ next.score += 1; // reward for consuming a cp from string
+ if (newOffset < str.length()) {
+ // String has more code points.
+ next.name = StateName.INSIDE_STRING;
+ next.returnTo1 = returnTo1;
+ next.returnTo2 = returnTo2;
+ next.currentString = str;
+ next.currentOffset = newOffset;
+ next.currentTrailing = trailing;
+ } else {
+ // We've reached the end of the string.
+ next.name = returnTo1;
+ if (!trailing) next.trailingCount = 0;
+ next.returnTo1 = returnTo2;
+ next.returnTo2 = null;
+ }
+ return 1L << state.lastInsertedIndex();
+ }
+
+ private static long acceptAffixPatternOffset(int cp, ParserState state, StateItem item) {
+ return acceptAffixPattern(
+ cp, item.returnTo1, state, item, item.currentAffixPattern, item.currentStepwiseParserTag);
+ }
+
+ /**
+ * Accepts a code point if the code point is compatible with the affix pattern at the offset
+ * encoded in the tag argument.
+ *
+ * @param cp The current code point, which will be checked for a match to the string.
+ * @param returnTo The state to return to after reaching the end of the string.
+ * @param state The current {@link ParserState}
+ * @param item The current {@link StateItem}
+ * @param str The string containing the affix pattern.
+ * @param tag The current state of the stepwise parser. Initial value should be 0L.
+ * @return A bitmask where the bits correspond to the items that were added. Set to 0L if no items
+ * were added.
+ */
+ private static long acceptAffixPattern(
+ int cp, StateName ret1, ParserState state, StateItem item, CharSequence str, long tag) {
+ if (str == null || str.length() == 0) return 0L;
+ return acceptStringOrAffixPatternWithIgnorables(
+ cp, ret1, null, state, item, str, tag, false, false);
+ }
+
+ private static long acceptAffixPatternNonIgnorable(
+ int cp,
+ StateName returnTo,
+ ParserState state,
+ StateItem item,
+ CharSequence str,
+ int typeOrCp,
+ long firstTag,
+ long nextTag) {
+
+ // Convert from the returned tag to a code point, string, or currency to check
+ int resolvedCp = -1;
+ CharSequence resolvedStr = null;
+ boolean resolvedMinusSign = false;
+ boolean resolvedPlusSign = false;
+ boolean resolvedCurrency = false;
+ if (typeOrCp < 0) {
+ // Symbol
+ switch (typeOrCp) {
+ case AffixPatternUtils.TYPE_MINUS_SIGN:
+ resolvedMinusSign = true;
+ break;
+ case AffixPatternUtils.TYPE_PLUS_SIGN:
+ resolvedPlusSign = true;
+ break;
+ case AffixPatternUtils.TYPE_PERCENT:
+ resolvedStr = state.symbols.getPercentString();
+ if (resolvedStr.length() != 1 || resolvedStr.charAt(0) != '%') {
+ resolvedCp = '%'; // accept ASCII percent as well as locale percent
+ }
+ break;
+ case AffixPatternUtils.TYPE_PERMILLE:
+ resolvedStr = state.symbols.getPerMillString();
+ if (resolvedStr.length() != 1 || resolvedStr.charAt(0) != '‰') {
+ resolvedCp = '‰'; // accept ASCII permille as well as locale permille
+ }
+ break;
+ case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
+ case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
+ case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
+ resolvedCurrency = true;
+ break;
+ default:
+ throw new AssertionError();
+ }
+ } else {
+ resolvedCp = typeOrCp;
+ }
+
+ long added = 0L;
+ if (resolvedCp >= 0 && codePointEquals(cp, resolvedCp, state)) {
+ if (firstTag >= 0) {
+ added |= acceptAffixPatternHelper(cp, returnTo, state, item, str, firstTag);
+ }
+ if (nextTag < 0) {
+ added |= acceptAffixPatternHelper(cp, returnTo, state, item, str, nextTag);
+ }
+ }
+ if (resolvedMinusSign) {
+ if (firstTag >= 0) {
+ added |= acceptMinusSign(cp, StateName.INSIDE_AFFIX_PATTERN, returnTo, state, item, false);
+ }
+ if (nextTag < 0) {
+ added |= acceptMinusSign(cp, returnTo, null, state, item, false);
+ }
+ if (added == 0L) {
+ // Also attempt to accept custom minus sign string
+ String mss = state.symbols.getMinusSignString();
+ int mssCp = Character.codePointAt(mss, 0);
+ if (mss.length() != Character.charCount(mssCp) || !UNISET_MINUS.contains(mssCp)) {
+ resolvedStr = mss;
+ }
+ }
+ }
+ if (resolvedPlusSign) {
+ if (firstTag >= 0) {
+ added |= acceptPlusSign(cp, StateName.INSIDE_AFFIX_PATTERN, returnTo, state, item, false);
+ }
+ if (nextTag < 0) {
+ added |= acceptPlusSign(cp, returnTo, null, state, item, false);
+ }
+ if (added == 0L) {
+ // Also attempt to accept custom plus sign string
+ String pss = state.symbols.getPlusSignString();
+ int pssCp = Character.codePointAt(pss, 0);
+ if (pss.length() != Character.charCount(pssCp) || !UNISET_MINUS.contains(pssCp)) {
+ resolvedStr = pss;
+ }
+ }
+ }
+ if (resolvedStr != null) {
+ if (firstTag >= 0) {
+ added |=
+ acceptString(
+ cp, StateName.INSIDE_AFFIX_PATTERN, returnTo, state, item, resolvedStr, 0, false);
+ }
+ if (nextTag < 0) {
+ added |= acceptString(cp, returnTo, null, state, item, resolvedStr, 0, false);
+ }
+ }
+ if (resolvedCurrency) {
+ if (firstTag >= 0) {
+ added |= acceptCurrency(cp, StateName.INSIDE_AFFIX_PATTERN, returnTo, state, item);
+ }
+ if (nextTag < 0) {
+ added |= acceptCurrency(cp, returnTo, null, state, item);
+ }
+ }
+
+ // Set state in the items that were added by the function calls
+ for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+ if (((1L << i) & added) != 0) {
+ state.getItem(i).currentAffixPattern = str;
+ state.getItem(i).currentStepwiseParserTag = firstTag;
+ }
+ }
+ return added;
+ }
+
+ /**
+ * Internal method that is used to step to the next token of a affix pattern or exit the affix
+ * pattern if at the end.
+ *
+ * @param cp See {@link #acceptAffixPattern}
+ * @param returnTo1 See {@link #acceptAffixPattern}
+ * @param state See {@link #acceptAffixPattern}
+ * @param item See {@link #acceptAffixPattern}
+ * @param str See {@link #acceptAffixPattern}
+ * @param newOffset The tag corresponding to the next token in the affix pattern that should be
+ * recorded and consumed in a future call to {@link #acceptAffixPatternOffset}.
+ * @return Bitmask containing one entry, the one that was added.
+ */
+ private static long acceptAffixPatternHelper(
+ int cp,
+ StateName returnTo,
+ ParserState state,
+ StateItem item,
+ CharSequence str,
+ long newTag) {
+ StateItem next = state.getNext().copyFrom(item, null, cp);
+ next.score += 1; // reward for consuming a cp from pattern
+ if (newTag >= 0) {
+ // Additional tokens in affix string.
+ next.name = StateName.INSIDE_AFFIX_PATTERN;
+ next.returnTo1 = returnTo;
+ next.currentAffixPattern = str;
+ next.currentStepwiseParserTag = newTag;
+ } else {
+ // Reached last token in affix string.
+ next.name = returnTo;
+ next.trailingCount = 0;
+ next.returnTo1 = null;
+ }
+ return 1L << state.lastInsertedIndex();
+ }
+
+ /**
+ * Consumes tokens from a string or affix pattern following ICU's rules for handling of whitespace
+ * and bidi control characters (collectively called "ignorables"). The methods {@link
+ * #acceptStringHelper}, {@link #acceptAffixPatternHelper}, {@link #acceptStringNonIgnorable}, and
+ * {@link #acceptAffixPatternNonIgnorable} will be called by this method to actually add parse
+ * paths.
+ *
+ * <p>In the "NonIgnorable" functions, two arguments are passed: firstOffsetOrTag and
+ * nextOffsetOrTag. These two arguments should add parse paths according to the following rules:
+ *
+ * <pre>
+ * if (firstOffsetOrTag is valid or inside string boundary) {
+ * // Add parse path going to firstOffsetOrTag
+ * }
+ * if (nextOffsetOrTag is invalid or beyond string boundary) {
+ * // Add parse path leaving the string
+ * }
+ * </pre>
+ *
+ * <p>Note that there may be multiple parse paths added by these lines. This is important in order
+ * to properly handle runs of ignorables.
+ *
+ * @param cp See {@link #acceptString} and {@link #acceptAffixPattern}
+ * @param ret1 See {@link #acceptString} and {@link #acceptAffixPattern}
+ * @param ret2 See {@link #acceptString} (affix pattern can pass null)
+ * @param state See {@link #acceptString} and {@link #acceptAffixPattern}
+ * @param item See {@link #acceptString} and {@link #acceptAffixPattern}
+ * @param str See {@link #acceptString} and {@link #acceptAffixPattern}
+ * @param offsetOrTag The current int offset for strings, or the current tag for affix patterns.
+ * @param trailing See {@link #acceptString} (affix patterns can pass false)
+ * @param isString true if the parameters correspond to a string; false if they correspond to an
+ * affix pattern.
+ * @return A bitmask containing the entries that were added.
+ */
+ private static long acceptStringOrAffixPatternWithIgnorables(
+ int cp,
+ StateName ret1,
+ StateName ret2 /* String only */,
+ ParserState state,
+ StateItem item,
+ CharSequence str,
+ long offsetOrTag /* offset for string; tag for affix pattern */,
+ boolean trailing /* String only */,
+ boolean isString) {
+
+ // Runs of ignorables (whitespace and bidi control marks) can occur at the beginning, middle,
+ // or end of the reference string, or a run across the entire string.
+ //
+ // - A run at the beginning or in the middle corresponds to a run of length *zero or more*
+ // in the input.
+ // - A run at the end need to be matched exactly.
+ // - A string that contains only ignorable characters also needs to be matched exactly.
+ //
+ // Because the behavior differs, we need logic here to determine which case we have.
+
+ int typeOrCp =
+ isString
+ ? Character.codePointAt(str, (int) offsetOrTag)
+ : AffixPatternUtils.getTypeOrCp(offsetOrTag);
+
+ if (isIgnorable(typeOrCp, state)) {
+ // Look for the next nonignorable code point
+ int nextTypeOrCp = typeOrCp;
+ long prevOffsetOrTag;
+ long nextOffsetOrTag = offsetOrTag;
+ long firstOffsetOrTag = 0L;
+ while (true) {
+ prevOffsetOrTag = nextOffsetOrTag;
+ nextOffsetOrTag =
+ isString
+ ? nextOffsetOrTag + Character.charCount(nextTypeOrCp)
+ : AffixPatternUtils.nextToken(nextOffsetOrTag, str);
+ if (firstOffsetOrTag == 0L) firstOffsetOrTag = nextOffsetOrTag;
+ if (isString ? nextOffsetOrTag >= str.length() : nextOffsetOrTag < 0) {
+ // Integer.MIN_VALUE is an invalid value for either a type or a cp;
+ // use it to indicate the end of the string.
+ nextTypeOrCp = Integer.MIN_VALUE;
+ break;
+ }
+ nextTypeOrCp =
+ isString
+ ? Character.codePointAt(str, (int) nextOffsetOrTag)
+ : AffixPatternUtils.getTypeOrCp(nextOffsetOrTag);
+ if (!isIgnorable(nextTypeOrCp, state)) break;
+ }
+
+ if (nextTypeOrCp == Integer.MIN_VALUE) {
+ // Run at end or string that contains only ignorable characters.
+ if (codePointEquals(cp, typeOrCp, state)) {
+ // Step forward and also exit the string if not at very end.
+ // RETURN
+ long added = 0L;
+ added |=
+ isString
+ ? acceptStringHelper(
+ cp, ret1, ret2, state, item, str, (int) firstOffsetOrTag, trailing)
+ : acceptAffixPatternHelper(cp, ret1, state, item, str, firstOffsetOrTag);
+ if (firstOffsetOrTag != nextOffsetOrTag) {
+ added |=
+ isString
+ ? acceptStringHelper(
+ cp, ret1, ret2, state, item, str, (int) nextOffsetOrTag, trailing)
+ : acceptAffixPatternHelper(cp, ret1, state, item, str, nextOffsetOrTag);
+ }
+ return added;
+ } else {
+ // Code point does not exactly match the run at end.
+ // RETURN
+ return 0L;
+ }
+ } else {
+ // Run at beginning or in middle.
+ if (isIgnorable(cp, state)) {
+ // Consume the ignorable.
+ // RETURN
+ return isString
+ ? acceptStringHelper(
+ cp, ret1, ret2, state, item, str, (int) prevOffsetOrTag, trailing)
+ : acceptAffixPatternHelper(cp, ret1, state, item, str, prevOffsetOrTag);
+ } else {
+ // Go to nonignorable cp.
+ // FALL THROUGH
+ }
+ }
+
+ // Fall through to the nonignorable code point found above.
+ assert nextTypeOrCp != Integer.MIN_VALUE;
+ typeOrCp = nextTypeOrCp;
+ offsetOrTag = nextOffsetOrTag;
+ }
+ assert !isIgnorable(typeOrCp, state);
+
+ // Look for the next nonignorable code point after this nonignorable code point
+ // to determine if we are at the end of the string.
+ int nextTypeOrCp = typeOrCp;
+ long nextOffsetOrTag = offsetOrTag;
+ long firstOffsetOrTag = 0L;
+ while (true) {
+ nextOffsetOrTag =
+ isString
+ ? nextOffsetOrTag + Character.charCount(nextTypeOrCp)
+ : AffixPatternUtils.nextToken(nextOffsetOrTag, str);
+ if (firstOffsetOrTag == 0L) firstOffsetOrTag = nextOffsetOrTag;
+ if (isString ? nextOffsetOrTag >= str.length() : nextOffsetOrTag < 0) {
+ nextTypeOrCp = -1;
+ break;
+ }
+ nextTypeOrCp =
+ isString
+ ? Character.codePointAt(str, (int) nextOffsetOrTag)
+ : AffixPatternUtils.getTypeOrCp(nextOffsetOrTag);
+ if (!isIgnorable(nextTypeOrCp, state)) break;
+ }
+
+ // Nonignorable logic.
+ return isString
+ ? acceptStringNonIgnorable(
+ cp, ret1, ret2, state, item, str, trailing, typeOrCp, firstOffsetOrTag, nextOffsetOrTag)
+ : acceptAffixPatternNonIgnorable(
+ cp, ret1, state, item, str, typeOrCp, firstOffsetOrTag, nextOffsetOrTag);
+ }
+
+ /**
+ * This method can add up to four items to the new list in <code>state</code>.
+ *
+ * <p>If <code>cp</code> is equal to any known ISO code or long name, copies <code>item</code> to
+ * the new list in <code>state</code> and sets its ISO code to the corresponding currency.
+ *
+ * <p>If <code>cp</code> is the first code point of any ISO code or long name having more them one
+ * code point in length, copies <code>item</code> to the new list in <code>state</code> along with
+ * an instance of {@link TextTrieMap.ParseState} for tracking the following code points.
+ *
+ * @param cp The code point to check.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptCurrency(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ acceptCurrency(cp, nextName, null, state, item);
+ }
+
+ private static long acceptCurrency(
+ int cp, StateName returnTo1, StateName returnTo2, ParserState state, StateItem item) {
+ if (item.sawCurrency) return 0L;
+ long added = 0L;
+
+ // Accept from local currency information
+ String str1, str2;
+ Currency currency = state.properties.getCurrency();
+ if (currency != null) {
+ str1 = currency.getName(state.symbols.getULocale(), Currency.SYMBOL_NAME, null);
+ str2 = currency.getCurrencyCode();
+ // TODO: Should we also accept long names? In currency mode, they are in the CLDR data.
+ } else {
+ currency = state.symbols.getCurrency();
+ str1 = state.symbols.getCurrencySymbol();
+ str2 = state.symbols.getInternationalCurrencySymbol();
+ }
+ added |= acceptString(cp, returnTo1, returnTo2, state, item, str1, 0, false);
+ added |= acceptString(cp, returnTo1, returnTo2, state, item, str2, 0, false);
+ for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+ if (((1L << i) & added) != 0) {
+ state.getItem(i).sawCurrency = true;
+ state.getItem(i).isoCode = str2;
+ }
+ }
+
+ // Accept from CLDR data
+ if (state.parseCurrency) {
+ ULocale uloc = state.symbols.getULocale();
+ TextTrieMap<Currency.CurrencyStringInfo>.ParseState trie1 =
+ Currency.openParseState(uloc, cp, Currency.LONG_NAME);
+ TextTrieMap<Currency.CurrencyStringInfo>.ParseState trie2 =
+ Currency.openParseState(uloc, cp, Currency.SYMBOL_NAME);
+ added |= acceptCurrencyHelper(cp, returnTo1, returnTo2, state, item, trie1);
+ added |= acceptCurrencyHelper(cp, returnTo1, returnTo2, state, item, trie2);
+ }
+
+ return added;
+ }
+
+ /**
+ * If <code>cp</code> is the next code point of any currency, copies <code>item</code> to the new
+ * list in <code>state</code> along with an instance of {@link TextTrieMap.ParseState} for
+ * tracking the following code points.
+ *
+ * <p>This method should only be called in a state following {@link #acceptCurrency}.
+ *
+ * @param cp The code point to check.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptCurrencyOffset(int cp, ParserState state, StateItem item) {
+ acceptCurrencyHelper(
+ cp, item.returnTo1, item.returnTo2, state, item, item.currentCurrencyTrieState);
+ }
+
+ private static long acceptCurrencyHelper(
+ int cp,
+ StateName returnTo1,
+ StateName returnTo2,
+ ParserState state,
+ StateItem item,
+ TextTrieMap<Currency.CurrencyStringInfo>.ParseState trieState) {
+ if (trieState == null) return 0L;
+ trieState.accept(cp);
+ long added = 0L;
+ Iterator<Currency.CurrencyStringInfo> currentMatches = trieState.getCurrentMatches();
+ if (currentMatches != null) {
+ // Match on current code point
+ // TODO: What should happen with multiple currency matches?
+ StateItem next = state.getNext().copyFrom(item, returnTo1, -1);
+ next.returnTo1 = returnTo2;
+ next.returnTo2 = null;
+ next.sawCurrency = true;
+ next.isoCode = currentMatches.next().getISOCode();
+ added |= 1L << state.lastInsertedIndex();
+ }
+ if (!trieState.atEnd()) {
+ // Prepare for matches on future code points
+ StateItem next = state.getNext().copyFrom(item, StateName.INSIDE_CURRENCY, -1);
+ next.returnTo1 = returnTo1;
+ next.returnTo2 = returnTo2;
+ next.currentCurrencyTrieState = trieState;
+ added |= 1L << state.lastInsertedIndex();
+ }
+ return added;
+ }
+
+ private static long acceptDigitTrie(
+ int cp, StateName nextName, ParserState state, StateItem item, DigitType type) {
+ assert state.digitTrie != null;
+ TextTrieMap<Byte>.ParseState trieState = state.digitTrie.openParseState(cp);
+ if (trieState == null) return 0L;
+ return acceptDigitTrieHelper(cp, nextName, state, item, type, trieState);
+ }
+
+ private static void acceptDigitTrieOffset(int cp, ParserState state, StateItem item) {
+ acceptDigitTrieHelper(
+ cp, item.returnTo1, state, item, item.currentDigitType, item.currentDigitTrieState);
+ }
+
+ private static long acceptDigitTrieHelper(
+ int cp,
+ StateName returnTo1,
+ ParserState state,
+ StateItem item,
+ DigitType type,
+ TextTrieMap<Byte>.ParseState trieState) {
+ if (trieState == null) return 0L;
+ trieState.accept(cp);
+ long added = 0L;
+ Iterator<Byte> currentMatches = trieState.getCurrentMatches();
+ if (currentMatches != null) {
+ // Match on current code point
+ byte digit = currentMatches.next();
+ StateItem next = state.getNext().copyFrom(item, returnTo1, -1);
+ next.returnTo1 = null;
+ recordDigit(next, digit, type);
+ added |= 1L << state.lastInsertedIndex();
+ }
+ if (!trieState.atEnd()) {
+ // Prepare for matches on future code points
+ StateItem next = state.getNext().copyFrom(item, StateName.INSIDE_DIGIT, -1);
+ next.returnTo1 = returnTo1;
+ next.currentDigitTrieState = trieState;
+ next.currentDigitType = type;
+ added |= 1L << state.lastInsertedIndex();
+ }
+ return added;
+ }
+
+ /**
+ * Checks whether the two given code points are equal after applying case mapping as requested in
+ * the ParserState.
+ *
+ * @see #acceptString
+ * @see #acceptAffixPattern
+ */
+ private static boolean codePointEquals(int cp1, int cp2, ParserState state) {
+ if (!state.caseSensitive) {
+ cp1 = UCharacter.foldCase(cp1, true);
+ cp2 = UCharacter.foldCase(cp2, true);
+ }
+ return cp1 == cp2;
+ }
+
+ /**
+ * Checks whether the given code point is "ignorable" and should be skipped. BiDi control marks
+ * are always ignorable, and whitespace is ignorable in lenient mode.
+ *
+ * <p>Returns false if cp is negative.
+ *
+ * @param cp The code point to test.
+ * @param state The current {@link ParserState}, used for determining strict mode.
+ * @return true if cp is ignorable; false otherwise.
+ */
+ private static boolean isIgnorable(int cp, ParserState state) {
+ if (cp < 0) return false;
+ if (UNISET_BIDI.contains(cp)) return true;
+ return state.mode == ParseMode.LENIENT && UNISET_WHITESPACE.contains(cp);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/PatternString.java b/android_icu4j/src/main/java/android/icu/impl/number/PatternString.java
new file mode 100644
index 000000000..eeca68916
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/PatternString.java
@@ -0,0 +1,907 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.math.BigDecimal;
+
+import android.icu.impl.number.formatters.PaddingFormat;
+import android.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import android.icu.text.DecimalFormatSymbols;
+
+/**
+ * Handles parsing and creation of the compact pattern string representation of a decimal format.
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class PatternString {
+
+ /**
+ * Parses a pattern string into a new property bag.
+ *
+ * @param pattern The pattern string, like "#,##0.00"
+ * @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
+ * increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
+ * as CurrencyUsage, is to be used instead. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link
+ * #IGNORE_ROUNDING_IF_CURRENCY}, or {@link #IGNORE_ROUNDING_NEVER}.
+ * @return A property bag object.
+ * @throws IllegalArgumentException If there is a syntax error in the pattern string.
+ */
+ public static Properties parseToProperties(String pattern, int ignoreRounding) {
+ Properties properties = new Properties();
+ LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding);
+ return properties;
+ }
+
+ public static Properties parseToProperties(String pattern) {
+ return parseToProperties(pattern, PatternString.IGNORE_ROUNDING_NEVER);
+ }
+
+ /**
+ * Parses a pattern string into an existing property bag. All properties that can be encoded into
+ * a pattern string will be overwritten with either their default value or with the value coming
+ * from the pattern string. Properties that cannot be encoded into a pattern string, such as
+ * rounding mode, are not modified.
+ *
+ * @param pattern The pattern string, like "#,##0.00"
+ * @param properties The property bag object to overwrite.
+ * @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
+ * increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
+ * as CurrencyUsage, is to be used instead. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link
+ * #IGNORE_ROUNDING_IF_CURRENCY}, or {@link #IGNORE_ROUNDING_NEVER}.
+ * @throws IllegalArgumentException If there was a syntax error in the pattern string.
+ */
+ public static void parseToExistingProperties(
+ String pattern, Properties properties, int ignoreRounding) {
+ LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding);
+ }
+
+ public static void parseToExistingProperties(String pattern, Properties properties) {
+ parseToExistingProperties(pattern, properties, PatternString.IGNORE_ROUNDING_NEVER);
+ }
+
+ /**
+ * Creates a pattern string from a property bag.
+ *
+ * <p>Since pattern strings support only a subset of the functionality available in a property
+ * bag, a new property bag created from the string returned by this function may not be the same
+ * as the original property bag.
+ *
+ * @param properties The property bag to serialize.
+ * @return A pattern string approximately serializing the property bag.
+ */
+ public static String propertiesToString(Properties properties) {
+ StringBuilder sb = new StringBuilder();
+
+ // Convenience references
+ // The Math.min() calls prevent DoS
+ int dosMax = 100;
+ int groupingSize = Math.min(properties.getSecondaryGroupingSize(), dosMax);
+ int firstGroupingSize = Math.min(properties.getGroupingSize(), dosMax);
+ int paddingWidth = Math.min(properties.getFormatWidth(), dosMax);
+ PadPosition paddingLocation = properties.getPadPosition();
+ String paddingString = properties.getPadString();
+ int minInt = Math.max(Math.min(properties.getMinimumIntegerDigits(), dosMax), 0);
+ int maxInt = Math.min(properties.getMaximumIntegerDigits(), dosMax);
+ int minFrac = Math.max(Math.min(properties.getMinimumFractionDigits(), dosMax), 0);
+ int maxFrac = Math.min(properties.getMaximumFractionDigits(), dosMax);
+ int minSig = Math.min(properties.getMinimumSignificantDigits(), dosMax);
+ int maxSig = Math.min(properties.getMaximumSignificantDigits(), dosMax);
+ boolean alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown();
+ int exponentDigits = Math.min(properties.getMinimumExponentDigits(), dosMax);
+ boolean exponentShowPlusSign = properties.getExponentSignAlwaysShown();
+ String pp = properties.getPositivePrefix();
+ String ppp = properties.getPositivePrefixPattern();
+ String ps = properties.getPositiveSuffix();
+ String psp = properties.getPositiveSuffixPattern();
+ String np = properties.getNegativePrefix();
+ String npp = properties.getNegativePrefixPattern();
+ String ns = properties.getNegativeSuffix();
+ String nsp = properties.getNegativeSuffixPattern();
+
+ // Prefixes
+ if (ppp != null) sb.append(ppp);
+ AffixPatternUtils.escape(pp, sb);
+ int afterPrefixPos = sb.length();
+
+ // Figure out the grouping sizes.
+ int grouping1, grouping2, grouping;
+ if (groupingSize != Math.min(dosMax, Properties.DEFAULT_SECONDARY_GROUPING_SIZE)
+ && firstGroupingSize != Math.min(dosMax, Properties.DEFAULT_GROUPING_SIZE)
+ && groupingSize != firstGroupingSize) {
+ grouping = groupingSize;
+ grouping1 = groupingSize;
+ grouping2 = firstGroupingSize;
+ } else if (groupingSize != Math.min(dosMax, Properties.DEFAULT_SECONDARY_GROUPING_SIZE)) {
+ grouping = groupingSize;
+ grouping1 = 0;
+ grouping2 = groupingSize;
+ } else if (firstGroupingSize != Math.min(dosMax, Properties.DEFAULT_GROUPING_SIZE)) {
+ grouping = groupingSize;
+ grouping1 = 0;
+ grouping2 = firstGroupingSize;
+ } else {
+ grouping = 0;
+ grouping1 = 0;
+ grouping2 = 0;
+ }
+ int groupingLength = grouping1 + grouping2 + 1;
+
+ // Figure out the digits we need to put in the pattern.
+ BigDecimal roundingInterval = properties.getRoundingIncrement();
+ StringBuilder digitsString = new StringBuilder();
+ int digitsStringScale = 0;
+ if (maxSig != Math.min(dosMax, Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS)) {
+ // Significant Digits.
+ while (digitsString.length() < minSig) {
+ digitsString.append('@');
+ }
+ while (digitsString.length() < maxSig) {
+ digitsString.append('#');
+ }
+ } else if (roundingInterval != Properties.DEFAULT_ROUNDING_INCREMENT) {
+ // Rounding Interval.
+ digitsStringScale = -roundingInterval.scale();
+ // TODO: Check for DoS here?
+ String str = roundingInterval.scaleByPowerOfTen(roundingInterval.scale()).toPlainString();
+ if (str.charAt(0) == '\'') {
+ // TODO: Unsupported operation exception or fail silently?
+ digitsString.append(str, 1, str.length());
+ } else {
+ digitsString.append(str);
+ }
+ }
+ while (digitsString.length() + digitsStringScale < minInt) {
+ digitsString.insert(0, '0');
+ }
+ while (-digitsStringScale < minFrac) {
+ digitsString.append('0');
+ digitsStringScale--;
+ }
+
+ // Write the digits to the string builder
+ int m0 = Math.max(groupingLength, digitsString.length() + digitsStringScale);
+ m0 = (maxInt != dosMax) ? Math.max(maxInt, m0) - 1 : m0 - 1;
+ int mN = (maxFrac != dosMax) ? Math.min(-maxFrac, digitsStringScale) : digitsStringScale;
+ for (int magnitude = m0; magnitude >= mN; magnitude--) {
+ int di = digitsString.length() + digitsStringScale - magnitude - 1;
+ if (di < 0 || di >= digitsString.length()) {
+ sb.append('#');
+ } else {
+ sb.append(digitsString.charAt(di));
+ }
+ if (magnitude > grouping2 && grouping > 0 && (magnitude - grouping2) % grouping == 0) {
+ sb.append(',');
+ } else if (magnitude > 0 && magnitude == grouping2) {
+ sb.append(',');
+ } else if (magnitude == 0 && (alwaysShowDecimal || mN < 0)) {
+ sb.append('.');
+ }
+ }
+
+ // Exponential notation
+ if (exponentDigits != Math.min(dosMax, Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS)) {
+ sb.append('E');
+ if (exponentShowPlusSign) {
+ sb.append('+');
+ }
+ for (int i = 0; i < exponentDigits; i++) {
+ sb.append('0');
+ }
+ }
+
+ // Suffixes
+ int beforeSuffixPos = sb.length();
+ if (psp != null) sb.append(psp);
+ AffixPatternUtils.escape(ps, sb);
+
+ // Resolve Padding
+ if (paddingWidth != Properties.DEFAULT_FORMAT_WIDTH) {
+ while (paddingWidth - sb.length() > 0) {
+ sb.insert(afterPrefixPos, '#');
+ beforeSuffixPos++;
+ }
+ int addedLength;
+ switch (paddingLocation) {
+ case BEFORE_PREFIX:
+ addedLength = escapePaddingString(paddingString, sb, 0);
+ sb.insert(0, '*');
+ afterPrefixPos += addedLength + 1;
+ beforeSuffixPos += addedLength + 1;
+ break;
+ case AFTER_PREFIX:
+ addedLength = escapePaddingString(paddingString, sb, afterPrefixPos);
+ sb.insert(afterPrefixPos, '*');
+ afterPrefixPos += addedLength + 1;
+ beforeSuffixPos += addedLength + 1;
+ break;
+ case BEFORE_SUFFIX:
+ escapePaddingString(paddingString, sb, beforeSuffixPos);
+ sb.insert(beforeSuffixPos, '*');
+ break;
+ case AFTER_SUFFIX:
+ sb.append('*');
+ escapePaddingString(paddingString, sb, sb.length());
+ break;
+ }
+ }
+
+ // Negative affixes
+ // Ignore if the negative prefix pattern is "-" and the negative suffix is empty
+ if (np != null
+ || ns != null
+ || (npp == null && nsp != null)
+ || (npp != null && (npp.length() != 1 || npp.charAt(0) != '-' || nsp.length() != 0))) {
+ sb.append(';');
+ if (npp != null) sb.append(npp);
+ AffixPatternUtils.escape(np, sb);
+ // Copy the positive digit format into the negative.
+ // This is optional; the pattern is the same as if '#' were appended here instead.
+ sb.append(sb, afterPrefixPos, beforeSuffixPos);
+ if (nsp != null) sb.append(nsp);
+ AffixPatternUtils.escape(ns, sb);
+ }
+
+ return sb.toString();
+ }
+
+ /** @return The number of chars inserted. */
+ private static int escapePaddingString(CharSequence input, StringBuilder output, int startIndex) {
+ if (input == null || input.length() == 0) input = PaddingFormat.FALLBACK_PADDING_STRING;
+ int startLength = output.length();
+ if (input.length() == 1) {
+ if (input.equals("'")) {
+ output.insert(startIndex, "''");
+ } else {
+ output.insert(startIndex, input);
+ }
+ } else {
+ output.insert(startIndex, '\'');
+ int offset = 1;
+ for (int i = 0; i < input.length(); i++) {
+ // it's okay to deal in chars here because the quote mark is the only interesting thing.
+ char ch = input.charAt(i);
+ if (ch == '\'') {
+ output.insert(startIndex + offset, "''");
+ offset += 2;
+ } else {
+ output.insert(startIndex + offset, ch);
+ offset += 1;
+ }
+ }
+ output.insert(startIndex + offset, '\'');
+ }
+ return output.length() - startLength;
+ }
+
+ /**
+ * Converts a pattern between standard notation and localized notation. Localized notation means
+ * that instead of using generic placeholders in the pattern, you use the corresponding
+ * locale-specific characters instead. For example, in locale <em>fr-FR</em>, the period in the
+ * pattern "0.000" means "decimal" in standard notation (as it does in every other locale), but it
+ * means "grouping" in localized notation.
+ *
+ * <p>A greedy string-substitution strategy is used to substitute locale symbols. If two symbols
+ * are ambiguous or have the same prefix, the result is not well-defined.
+ *
+ * <p>Locale symbols are not allowed to contain the ASCII quote character.
+ *
+ * @param input The pattern to convert.
+ * @param symbols The symbols corresponding to the localized pattern.
+ * @param toLocalized true to convert from standard to localized notation; false to convert from
+ * localized to standard notation.
+ * @return The pattern expressed in the other notation.
+ * @deprecated ICU 59 This method is provided for backwards compatibility and should not be used
+ * in any new code.
+ */
+ @Deprecated
+ public static String convertLocalized(
+ String input, DecimalFormatSymbols symbols, boolean toLocalized) {
+ if (input == null) return null;
+
+ // Construct a table of strings to be converted between localized and standard.
+ String[][] table = new String[21][2];
+ int standIdx = toLocalized ? 0 : 1;
+ int localIdx = toLocalized ? 1 : 0;
+ table[0][standIdx] = "%";
+ table[0][localIdx] = symbols.getPercentString();
+ table[1][standIdx] = "‰";
+ table[1][localIdx] = symbols.getPerMillString();
+ table[2][standIdx] = ".";
+ table[2][localIdx] = symbols.getDecimalSeparatorString();
+ table[3][standIdx] = ",";
+ table[3][localIdx] = symbols.getGroupingSeparatorString();
+ table[4][standIdx] = "-";
+ table[4][localIdx] = symbols.getMinusSignString();
+ table[5][standIdx] = "+";
+ table[5][localIdx] = symbols.getPlusSignString();
+ table[6][standIdx] = ";";
+ table[6][localIdx] = Character.toString(symbols.getPatternSeparator());
+ table[7][standIdx] = "@";
+ table[7][localIdx] = Character.toString(symbols.getSignificantDigit());
+ table[8][standIdx] = "E";
+ table[8][localIdx] = symbols.getExponentSeparator();
+ table[9][standIdx] = "*";
+ table[9][localIdx] = Character.toString(symbols.getPadEscape());
+ table[10][standIdx] = "#";
+ table[10][localIdx] = Character.toString(symbols.getDigit());
+ for (int i = 0; i < 10; i++) {
+ table[11 + i][standIdx] = Character.toString((char) ('0' + i));
+ table[11 + i][localIdx] = symbols.getDigitStringsLocal()[i];
+ }
+
+ // Special case: quotes are NOT allowed to be in any localIdx strings.
+ // Substitute them with '’' instead.
+ for (int i = 0; i < table.length; i++) {
+ table[i][localIdx] = table[i][localIdx].replace('\'', '’');
+ }
+
+ // Iterate through the string and convert.
+ // State table:
+ // 0 => base state
+ // 1 => first char inside a quoted sequence in input and output string
+ // 2 => inside a quoted sequence in input and output string
+ // 3 => first char after a close quote in input string;
+ // close quote still needs to be written to output string
+ // 4 => base state in input string; inside quoted sequence in output string
+ // 5 => first char inside a quoted sequence in input string;
+ // inside quoted sequence in output string
+ StringBuilder result = new StringBuilder();
+ int state = 0;
+ outer:
+ for (int offset = 0; offset < input.length(); offset++) {
+ char ch = input.charAt(offset);
+
+ // Handle a quote character (state shift)
+ if (ch == '\'') {
+ if (state == 0) {
+ result.append('\'');
+ state = 1;
+ continue;
+ } else if (state == 1) {
+ result.append('\'');
+ state = 0;
+ continue;
+ } else if (state == 2) {
+ state = 3;
+ continue;
+ } else if (state == 3) {
+ result.append('\'');
+ result.append('\'');
+ state = 1;
+ continue;
+ } else if (state == 4) {
+ state = 5;
+ continue;
+ } else {
+ assert state == 5;
+ result.append('\'');
+ result.append('\'');
+ state = 4;
+ continue;
+ }
+ }
+
+ if (state == 0 || state == 3 || state == 4) {
+ for (String[] pair : table) {
+ // Perform a greedy match on this symbol string
+ if (input.regionMatches(offset, pair[0], 0, pair[0].length())) {
+ // Skip ahead past this region for the next iteration
+ offset += pair[0].length() - 1;
+ if (state == 3 || state == 4) {
+ result.append('\'');
+ state = 0;
+ }
+ result.append(pair[1]);
+ continue outer;
+ }
+ }
+ // No replacement found. Check if a special quote is necessary
+ for (String[] pair : table) {
+ if (input.regionMatches(offset, pair[1], 0, pair[1].length())) {
+ if (state == 0) {
+ result.append('\'');
+ state = 4;
+ }
+ result.append(ch);
+ continue outer;
+ }
+ }
+ // Still nothing. Copy the char verbatim. (Add a close quote if necessary)
+ if (state == 3 || state == 4) {
+ result.append('\'');
+ state = 0;
+ }
+ result.append(ch);
+ } else {
+ assert state == 1 || state == 2 || state == 5;
+ result.append(ch);
+ state = 2;
+ }
+ }
+ // Resolve final quotes
+ if (state == 3 || state == 4) {
+ result.append('\'');
+ state = 0;
+ }
+ if (state != 0) {
+ throw new IllegalArgumentException("Malformed localized pattern: unterminated quote");
+ }
+ return result.toString();
+ }
+
+ public static final int IGNORE_ROUNDING_NEVER = 0;
+ public static final int IGNORE_ROUNDING_IF_CURRENCY = 1;
+ public static final int IGNORE_ROUNDING_ALWAYS = 2;
+
+ /** Implements a recursive descent parser for decimal format patterns. */
+ static class LdmlDecimalPatternParser {
+
+ /**
+ * An internal, intermediate data structure used for storing parse results before they are
+ * finalized into a DecimalFormatPattern.Builder.
+ */
+ private static class PatternParseResult {
+ SubpatternParseResult positive = new SubpatternParseResult();
+ SubpatternParseResult negative = null;
+
+ /** Finalizes the temporary data stored in the PatternParseResult to the Builder. */
+ void saveToProperties(Properties properties, int _ignoreRounding) {
+ // Translate from PatternState to Properties.
+ // Note that most data from "negative" is ignored per the specification of DecimalFormat.
+
+ boolean ignoreRounding;
+ if (_ignoreRounding == IGNORE_ROUNDING_NEVER) {
+ ignoreRounding = false;
+ } else if (_ignoreRounding == IGNORE_ROUNDING_IF_CURRENCY) {
+ ignoreRounding = positive.hasCurrencySign;
+ } else {
+ assert _ignoreRounding == IGNORE_ROUNDING_ALWAYS;
+ ignoreRounding = true;
+ }
+
+ // Grouping settings
+ if (positive.groupingSizes[1] != -1) {
+ properties.setGroupingSize(positive.groupingSizes[0]);
+ } else {
+ properties.setGroupingSize(Properties.DEFAULT_GROUPING_SIZE);
+ }
+ if (positive.groupingSizes[2] != -1) {
+ properties.setSecondaryGroupingSize(positive.groupingSizes[1]);
+ } else {
+ properties.setSecondaryGroupingSize(Properties.DEFAULT_SECONDARY_GROUPING_SIZE);
+ }
+
+ // For backwards compatibility, require that the pattern emit at least one min digit.
+ int minInt, minFrac;
+ if (positive.totalIntegerDigits == 0 && positive.maximumFractionDigits > 0) {
+ // patterns like ".##"
+ minInt = 0;
+ minFrac = Math.max(1, positive.minimumFractionDigits);
+ } else if (positive.minimumIntegerDigits == 0 && positive.minimumFractionDigits == 0) {
+ // patterns like "#.##"
+ minInt = 1;
+ minFrac = 0;
+ } else {
+ minInt = positive.minimumIntegerDigits;
+ minFrac = positive.minimumFractionDigits;
+ }
+
+ // Rounding settings
+ // Don't set basic rounding when there is a currency sign; defer to CurrencyUsage
+ if (positive.minimumSignificantDigits > 0) {
+ properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
+ properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
+ properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+ properties.setMinimumSignificantDigits(positive.minimumSignificantDigits);
+ properties.setMaximumSignificantDigits(positive.maximumSignificantDigits);
+ } else if (!positive.rounding.isZero()) {
+ if (!ignoreRounding) {
+ properties.setMinimumFractionDigits(minFrac);
+ properties.setMaximumFractionDigits(positive.maximumFractionDigits);
+ properties.setRoundingIncrement(positive.rounding.toBigDecimal());
+ } else {
+ properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
+ properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
+ properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+ }
+ properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS);
+ properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS);
+ } else {
+ if (!ignoreRounding) {
+ properties.setMinimumFractionDigits(minFrac);
+ properties.setMaximumFractionDigits(positive.maximumFractionDigits);
+ properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+ } else {
+ properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
+ properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
+ properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+ }
+ properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS);
+ properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS);
+ }
+
+ // If the pattern ends with a '.' then force the decimal point.
+ if (positive.hasDecimal && positive.maximumFractionDigits == 0) {
+ properties.setDecimalSeparatorAlwaysShown(true);
+ } else {
+ properties.setDecimalSeparatorAlwaysShown(false);
+ }
+
+ // Scientific notation settings
+ if (positive.exponentDigits > 0) {
+ properties.setExponentSignAlwaysShown(positive.exponentShowPlusSign);
+ properties.setMinimumExponentDigits(positive.exponentDigits);
+ if (positive.minimumSignificantDigits == 0) {
+ // patterns without '@' can define max integer digits, used for engineering notation
+ properties.setMinimumIntegerDigits(positive.minimumIntegerDigits);
+ properties.setMaximumIntegerDigits(positive.totalIntegerDigits);
+ } else {
+ // patterns with '@' cannot define max integer digits
+ properties.setMinimumIntegerDigits(1);
+ properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS);
+ }
+ } else {
+ properties.setExponentSignAlwaysShown(Properties.DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN);
+ properties.setMinimumExponentDigits(Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS);
+ properties.setMinimumIntegerDigits(minInt);
+ properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS);
+ }
+
+ // Padding settings
+ if (positive.padding.length() > 0) {
+ // The width of the positive prefix and suffix templates are included in the padding
+ int paddingWidth =
+ positive.paddingWidth
+ + AffixPatternUtils.unescapedLength(positive.prefix)
+ + AffixPatternUtils.unescapedLength(positive.suffix);
+ properties.setFormatWidth(paddingWidth);
+ if (positive.padding.length() == 1) {
+ properties.setPadString(positive.padding.toString());
+ } else if (positive.padding.length() == 2) {
+ if (positive.padding.charAt(0) == '\'') {
+ properties.setPadString("'");
+ } else {
+ properties.setPadString(positive.padding.toString());
+ }
+ } else {
+ properties.setPadString(
+ positive.padding.subSequence(1, positive.padding.length() - 1).toString());
+ }
+ assert positive.paddingLocation != null;
+ properties.setPadPosition(positive.paddingLocation);
+ } else {
+ properties.setFormatWidth(Properties.DEFAULT_FORMAT_WIDTH);
+ properties.setPadString(Properties.DEFAULT_PAD_STRING);
+ properties.setPadPosition(Properties.DEFAULT_PAD_POSITION);
+ }
+
+ // Set the affixes
+ // Always call the setter, even if the prefixes are empty, especially in the case of the
+ // negative prefix pattern, to prevent default values from overriding the pattern.
+ properties.setPositivePrefixPattern(positive.prefix.toString());
+ properties.setPositiveSuffixPattern(positive.suffix.toString());
+ if (negative != null) {
+ properties.setNegativePrefixPattern(negative.prefix.toString());
+ properties.setNegativeSuffixPattern(negative.suffix.toString());
+ } else {
+ properties.setNegativePrefixPattern(null);
+ properties.setNegativeSuffixPattern(null);
+ }
+
+ // Set the magnitude multiplier
+ if (positive.hasPercentSign) {
+ properties.setMagnitudeMultiplier(2);
+ } else if (positive.hasPerMilleSign) {
+ properties.setMagnitudeMultiplier(3);
+ } else {
+ properties.setMagnitudeMultiplier(Properties.DEFAULT_MAGNITUDE_MULTIPLIER);
+ }
+ }
+ }
+
+ private static class SubpatternParseResult {
+ int[] groupingSizes = new int[] {0, -1, -1};
+ int minimumIntegerDigits = 0;
+ int totalIntegerDigits = 0;
+ int minimumFractionDigits = 0;
+ int maximumFractionDigits = 0;
+ int minimumSignificantDigits = 0;
+ int maximumSignificantDigits = 0;
+ boolean hasDecimal = false;
+ int paddingWidth = 0;
+ PadPosition paddingLocation = null;
+ FormatQuantity4 rounding = new FormatQuantity4();
+ boolean exponentShowPlusSign = false;
+ int exponentDigits = 0;
+ boolean hasPercentSign = false;
+ boolean hasPerMilleSign = false;
+ boolean hasCurrencySign = false;
+
+ StringBuilder padding = new StringBuilder();
+ StringBuilder prefix = new StringBuilder();
+ StringBuilder suffix = new StringBuilder();
+ }
+
+ /** An internal class used for tracking the cursor during parsing of a pattern string. */
+ private static class ParserState {
+ final String pattern;
+ int offset;
+
+ ParserState(String pattern) {
+ this.pattern = pattern;
+ this.offset = 0;
+ }
+
+ int peek() {
+ if (offset == pattern.length()) {
+ return -1;
+ } else {
+ return pattern.codePointAt(offset);
+ }
+ }
+
+ int next() {
+ int codePoint = peek();
+ offset += Character.charCount(codePoint);
+ return codePoint;
+ }
+
+ IllegalArgumentException toParseException(String message) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Malformed pattern for ICU DecimalFormat: \"");
+ sb.append(pattern);
+ sb.append("\": ");
+ sb.append(message);
+ sb.append(" at position ");
+ sb.append(offset);
+ return new IllegalArgumentException(sb.toString());
+ }
+ }
+
+ static void parse(String pattern, Properties properties, int ignoreRounding) {
+ if (pattern == null || pattern.length() == 0) {
+ // Backwards compatibility requires that we reset to the default values.
+ // TODO: Only overwrite the properties that "saveToProperties" normally touches?
+ properties.clear();
+ return;
+ }
+
+ // TODO: Use whitespace characters from PatternProps
+ // TODO: Use thread locals here.
+ ParserState state = new ParserState(pattern);
+ PatternParseResult result = new PatternParseResult();
+ consumePattern(state, result);
+ result.saveToProperties(properties, ignoreRounding);
+ }
+
+ private static void consumePattern(ParserState state, PatternParseResult result) {
+ // pattern := subpattern (';' subpattern)?
+ consumeSubpattern(state, result.positive);
+ if (state.peek() == ';') {
+ state.next(); // consume the ';'
+ result.negative = new SubpatternParseResult();
+ consumeSubpattern(state, result.negative);
+ }
+ if (state.peek() != -1) {
+ throw state.toParseException("Found unquoted special character");
+ }
+ }
+
+ private static void consumeSubpattern(ParserState state, SubpatternParseResult result) {
+ // subpattern := literals? number exponent? literals?
+ consumePadding(state, result, PadPosition.BEFORE_PREFIX);
+ consumeAffix(state, result, result.prefix);
+ consumePadding(state, result, PadPosition.AFTER_PREFIX);
+ consumeFormat(state, result);
+ consumeExponent(state, result);
+ consumePadding(state, result, PadPosition.BEFORE_SUFFIX);
+ consumeAffix(state, result, result.suffix);
+ consumePadding(state, result, PadPosition.AFTER_SUFFIX);
+ }
+
+ private static void consumePadding(
+ ParserState state, SubpatternParseResult result, PadPosition paddingLocation) {
+ if (state.peek() != '*') {
+ return;
+ }
+ result.paddingLocation = paddingLocation;
+ state.next(); // consume the '*'
+ consumeLiteral(state, result.padding);
+ }
+
+ private static void consumeAffix(
+ ParserState state, SubpatternParseResult result, StringBuilder destination) {
+ // literals := { literal }
+ while (true) {
+ switch (state.peek()) {
+ case '#':
+ case '@':
+ case ';':
+ case '*':
+ case '.':
+ case ',':
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case -1:
+ // Characters that cannot appear unquoted in a literal
+ return;
+
+ case '%':
+ result.hasPercentSign = true;
+ break;
+
+ case '‰':
+ result.hasPerMilleSign = true;
+ break;
+
+ case '¤':
+ result.hasCurrencySign = true;
+ break;
+ }
+ consumeLiteral(state, destination);
+ }
+ }
+
+ private static void consumeLiteral(ParserState state, StringBuilder destination) {
+ if (state.peek() == -1) {
+ throw state.toParseException("Expected unquoted literal but found EOL");
+ } else if (state.peek() == '\'') {
+ destination.appendCodePoint(state.next()); // consume the starting quote
+ while (state.peek() != '\'') {
+ if (state.peek() == -1) {
+ throw state.toParseException("Expected quoted literal but found EOL");
+ } else {
+ destination.appendCodePoint(state.next()); // consume a quoted character
+ }
+ }
+ destination.appendCodePoint(state.next()); // consume the ending quote
+ } else {
+ // consume a non-quoted literal character
+ destination.appendCodePoint(state.next());
+ }
+ }
+
+ private static void consumeFormat(ParserState state, SubpatternParseResult result) {
+ consumeIntegerFormat(state, result);
+ if (state.peek() == '.') {
+ state.next(); // consume the decimal point
+ result.hasDecimal = true;
+ result.paddingWidth += 1;
+ consumeFractionFormat(state, result);
+ }
+ }
+
+ private static void consumeIntegerFormat(ParserState state, SubpatternParseResult result) {
+ boolean seenSignificantDigitMarker = false;
+ boolean seenDigit = false;
+
+ while (true) {
+ switch (state.peek()) {
+ case ',':
+ result.paddingWidth += 1;
+ result.groupingSizes[2] = result.groupingSizes[1];
+ result.groupingSizes[1] = result.groupingSizes[0];
+ result.groupingSizes[0] = 0;
+ break;
+
+ case '#':
+ if (seenDigit) throw state.toParseException("# cannot follow 0 before decimal point");
+ result.paddingWidth += 1;
+ result.groupingSizes[0] += 1;
+ result.totalIntegerDigits += (seenSignificantDigitMarker ? 0 : 1);
+ // no change to result.minimumIntegerDigits
+ // no change to result.minimumSignificantDigits
+ result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0);
+ result.rounding.appendDigit((byte) 0, 0, true);
+ break;
+
+ case '@':
+ seenSignificantDigitMarker = true;
+ if (seenDigit) throw state.toParseException("Cannot mix 0 and @");
+ result.paddingWidth += 1;
+ result.groupingSizes[0] += 1;
+ result.totalIntegerDigits += 1;
+ // no change to result.minimumIntegerDigits
+ result.minimumSignificantDigits += 1;
+ result.maximumSignificantDigits += 1;
+ result.rounding.appendDigit((byte) 0, 0, true);
+ break;
+
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ seenDigit = true;
+ if (seenSignificantDigitMarker) throw state.toParseException("Cannot mix @ and 0");
+ // TODO: Crash here if we've seen the significant digit marker? See NumberFormatTestCases.txt
+ result.paddingWidth += 1;
+ result.groupingSizes[0] += 1;
+ result.totalIntegerDigits += 1;
+ result.minimumIntegerDigits += 1;
+ // no change to result.minimumSignificantDigits
+ // no change to result.maximumSignificantDigits
+ result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true);
+ break;
+
+ default:
+ return;
+ }
+ state.next(); // consume the symbol
+ }
+ }
+
+ private static void consumeFractionFormat(ParserState state, SubpatternParseResult result) {
+ int zeroCounter = 0;
+ boolean seenHash = false;
+ while (true) {
+ switch (state.peek()) {
+ case '#':
+ seenHash = true;
+ result.paddingWidth += 1;
+ // no change to result.minimumFractionDigits
+ result.maximumFractionDigits += 1;
+ zeroCounter++;
+ break;
+
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ if (seenHash) throw state.toParseException("0 cannot follow # after decimal point");
+ result.paddingWidth += 1;
+ result.minimumFractionDigits += 1;
+ result.maximumFractionDigits += 1;
+ if (state.peek() == '0') {
+ zeroCounter++;
+ } else {
+ result.rounding.appendDigit((byte) (state.peek() - '0'), zeroCounter, false);
+ zeroCounter = 0;
+ }
+ break;
+
+ default:
+ return;
+ }
+ state.next(); // consume the symbol
+ }
+ }
+
+ private static void consumeExponent(ParserState state, SubpatternParseResult result) {
+ if (state.peek() != 'E') {
+ return;
+ }
+ state.next(); // consume the E
+ result.paddingWidth++;
+ if (state.peek() == '+') {
+ state.next(); // consume the +
+ result.exponentShowPlusSign = true;
+ result.paddingWidth++;
+ }
+ while (state.peek() == '0') {
+ state.next(); // consume the 0
+ result.exponentDigits += 1;
+ result.paddingWidth++;
+ }
+ }
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/Properties.java b/android_icu4j/src/main/java/android/icu/impl/number/Properties.java
new file mode 100644
index 000000000..bbe9548a9
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/Properties.java
@@ -0,0 +1,1059 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Map;
+
+import android.icu.impl.number.Parse.GroupingMode;
+import android.icu.impl.number.Parse.ParseMode;
+import android.icu.impl.number.formatters.BigDecimalMultiplier;
+import android.icu.impl.number.formatters.CompactDecimalFormat;
+import android.icu.impl.number.formatters.CurrencyFormat;
+import android.icu.impl.number.formatters.CurrencyFormat.CurrencyStyle;
+import android.icu.impl.number.formatters.MagnitudeMultiplier;
+import android.icu.impl.number.formatters.MeasureFormat;
+import android.icu.impl.number.formatters.PaddingFormat;
+import android.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import android.icu.impl.number.formatters.PositiveDecimalFormat;
+import android.icu.impl.number.formatters.PositiveNegativeAffixFormat;
+import android.icu.impl.number.formatters.ScientificFormat;
+import android.icu.impl.number.rounders.IncrementRounder;
+import android.icu.impl.number.rounders.MagnitudeRounder;
+import android.icu.impl.number.rounders.SignificantDigitsRounder;
+import android.icu.text.CompactDecimalFormat.CompactStyle;
+import android.icu.text.CurrencyPluralInfo;
+import android.icu.text.DecimalFormat.SignificantDigitsMode;
+import android.icu.text.MeasureFormat.FormatWidth;
+import android.icu.text.PluralRules;
+import android.icu.util.Currency;
+import android.icu.util.Currency.CurrencyUsage;
+import android.icu.util.MeasureUnit;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class Properties
+ implements Cloneable,
+ Serializable,
+ PositiveDecimalFormat.IProperties,
+ PositiveNegativeAffixFormat.IProperties,
+ MagnitudeMultiplier.IProperties,
+ ScientificFormat.IProperties,
+ MeasureFormat.IProperties,
+ CompactDecimalFormat.IProperties,
+ PaddingFormat.IProperties,
+ BigDecimalMultiplier.IProperties,
+ CurrencyFormat.IProperties,
+ Parse.IProperties,
+ IncrementRounder.IProperties,
+ MagnitudeRounder.IProperties,
+ SignificantDigitsRounder.IProperties,
+ Endpoint.IProperties {
+
+ private static final Properties DEFAULT = new Properties();
+
+ /** Auto-generated. */
+ private static final long serialVersionUID = 4095518955889349243L;
+
+ // The setters in this class should NOT have any side-effects or perform any validation. It is
+ // up to the consumer of the property bag to deal with property validation.
+
+ // The fields are all marked "transient" because custom serialization is being used.
+
+ /*--------------------------------------------------------------------------------------------+/
+ /| IMPORTANT! |/
+ /| WHEN ADDING A NEW PROPERTY, add it here, in #_clear(), in #_copyFrom(), in #equals(), |/
+ /| and in #_hashCode(). |/
+ /| |/
+ /| The unit test PropertiesTest will catch if you forget to add it to #clear(), #copyFrom(), |/
+ /| or #equals(), but it will NOT catch if you forget to add it to #hashCode(). |/
+ /+--------------------------------------------------------------------------------------------*/
+
+ private transient Map<String, Map<String, String>> compactCustomData;
+ private transient CompactStyle compactStyle;
+ private transient Currency currency;
+ private transient CurrencyPluralInfo currencyPluralInfo;
+ private transient CurrencyStyle currencyStyle;
+ private transient CurrencyUsage currencyUsage;
+ private transient boolean decimalPatternMatchRequired;
+ private transient boolean decimalSeparatorAlwaysShown;
+ private transient boolean exponentSignAlwaysShown;
+ private transient int formatWidth;
+ private transient int groupingSize;
+ private transient int magnitudeMultiplier;
+ private transient MathContext mathContext;
+ private transient int maximumFractionDigits;
+ private transient int maximumIntegerDigits;
+ private transient int maximumSignificantDigits;
+ private transient FormatWidth measureFormatWidth;
+ private transient MeasureUnit measureUnit;
+ private transient int minimumExponentDigits;
+ private transient int minimumFractionDigits;
+ private transient int minimumGroupingDigits;
+ private transient int minimumIntegerDigits;
+ private transient int minimumSignificantDigits;
+ private transient BigDecimal multiplier;
+ private transient String negativePrefix;
+ private transient String negativePrefixPattern;
+ private transient String negativeSuffix;
+ private transient String negativeSuffixPattern;
+ private transient PadPosition padPosition;
+ private transient String padString;
+ private transient boolean parseCaseSensitive;
+ private transient GroupingMode parseGroupingMode;
+ private transient boolean parseIntegerOnly;
+ private transient ParseMode parseMode;
+ private transient boolean parseNoExponent;
+ private transient boolean parseToBigDecimal;
+ private transient PluralRules pluralRules;
+ private transient String positivePrefix;
+ private transient String positivePrefixPattern;
+ private transient String positiveSuffix;
+ private transient String positiveSuffixPattern;
+ private transient BigDecimal roundingIncrement;
+ private transient RoundingMode roundingMode;
+ private transient int secondaryGroupingSize;
+ private transient boolean signAlwaysShown;
+ private transient SignificantDigitsMode significantDigitsMode;
+
+ /*--------------------------------------------------------------------------------------------+/
+ /| IMPORTANT! |/
+ /| WHEN ADDING A NEW PROPERTY, add it here, in #_clear(), in #_copyFrom(), in #equals(), |/
+ /| and in #_hashCode(). |/
+ /| |/
+ /| The unit test PropertiesTest will catch if you forget to add it to #clear(), #copyFrom(), |/
+ /| or #equals(), but it will NOT catch if you forget to add it to #hashCode(). |/
+ /+--------------------------------------------------------------------------------------------*/
+
+ public Properties() {
+ clear();
+ }
+
+ private Properties _clear() {
+ compactCustomData = DEFAULT_COMPACT_CUSTOM_DATA;
+ compactStyle = DEFAULT_COMPACT_STYLE;
+ currency = DEFAULT_CURRENCY;
+ currencyPluralInfo = DEFAULT_CURRENCY_PLURAL_INFO;
+ currencyStyle = DEFAULT_CURRENCY_STYLE;
+ currencyUsage = DEFAULT_CURRENCY_USAGE;
+ decimalPatternMatchRequired = DEFAULT_DECIMAL_PATTERN_MATCH_REQUIRED;
+ decimalSeparatorAlwaysShown = DEFAULT_DECIMAL_SEPARATOR_ALWAYS_SHOWN;
+ exponentSignAlwaysShown = DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN;
+ formatWidth = DEFAULT_FORMAT_WIDTH;
+ groupingSize = DEFAULT_GROUPING_SIZE;
+ magnitudeMultiplier = DEFAULT_MAGNITUDE_MULTIPLIER;
+ mathContext = DEFAULT_MATH_CONTEXT;
+ maximumFractionDigits = DEFAULT_MAXIMUM_FRACTION_DIGITS;
+ maximumIntegerDigits = DEFAULT_MAXIMUM_INTEGER_DIGITS;
+ maximumSignificantDigits = DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS;
+ measureFormatWidth = DEFAULT_MEASURE_FORMAT_WIDTH;
+ measureUnit = DEFAULT_MEASURE_UNIT;
+ minimumExponentDigits = DEFAULT_MINIMUM_EXPONENT_DIGITS;
+ minimumFractionDigits = DEFAULT_MINIMUM_FRACTION_DIGITS;
+ minimumGroupingDigits = DEFAULT_MINIMUM_GROUPING_DIGITS;
+ minimumIntegerDigits = DEFAULT_MINIMUM_INTEGER_DIGITS;
+ minimumSignificantDigits = DEFAULT_MINIMUM_SIGNIFICANT_DIGITS;
+ multiplier = DEFAULT_MULTIPLIER;
+ negativePrefix = DEFAULT_NEGATIVE_PREFIX;
+ negativePrefixPattern = DEFAULT_NEGATIVE_PREFIX_PATTERN;
+ negativeSuffix = DEFAULT_NEGATIVE_SUFFIX;
+ negativeSuffixPattern = DEFAULT_NEGATIVE_SUFFIX_PATTERN;
+ padPosition = DEFAULT_PAD_POSITION;
+ padString = DEFAULT_PAD_STRING;
+ parseCaseSensitive = DEFAULT_PARSE_CASE_SENSITIVE;
+ parseGroupingMode = DEFAULT_PARSE_GROUPING_MODE;
+ parseIntegerOnly = DEFAULT_PARSE_INTEGER_ONLY;
+ parseMode = DEFAULT_PARSE_MODE;
+ parseNoExponent = DEFAULT_PARSE_NO_EXPONENT;
+ parseToBigDecimal = DEFAULT_PARSE_TO_BIG_DECIMAL;
+ pluralRules = DEFAULT_PLURAL_RULES;
+ positivePrefix = DEFAULT_POSITIVE_PREFIX;
+ positivePrefixPattern = DEFAULT_POSITIVE_PREFIX_PATTERN;
+ positiveSuffix = DEFAULT_POSITIVE_SUFFIX;
+ positiveSuffixPattern = DEFAULT_POSITIVE_SUFFIX_PATTERN;
+ roundingIncrement = DEFAULT_ROUNDING_INCREMENT;
+ roundingMode = DEFAULT_ROUNDING_MODE;
+ secondaryGroupingSize = DEFAULT_SECONDARY_GROUPING_SIZE;
+ signAlwaysShown = DEFAULT_SIGN_ALWAYS_SHOWN;
+ significantDigitsMode = DEFAULT_SIGNIFICANT_DIGITS_MODE;
+ return this;
+ }
+
+ private Properties _copyFrom(Properties other) {
+ compactCustomData = other.compactCustomData;
+ compactStyle = other.compactStyle;
+ currency = other.currency;
+ currencyPluralInfo = other.currencyPluralInfo;
+ currencyStyle = other.currencyStyle;
+ currencyUsage = other.currencyUsage;
+ decimalPatternMatchRequired = other.decimalPatternMatchRequired;
+ decimalSeparatorAlwaysShown = other.decimalSeparatorAlwaysShown;
+ exponentSignAlwaysShown = other.exponentSignAlwaysShown;
+ formatWidth = other.formatWidth;
+ groupingSize = other.groupingSize;
+ magnitudeMultiplier = other.magnitudeMultiplier;
+ mathContext = other.mathContext;
+ maximumFractionDigits = other.maximumFractionDigits;
+ maximumIntegerDigits = other.maximumIntegerDigits;
+ maximumSignificantDigits = other.maximumSignificantDigits;
+ measureFormatWidth = other.measureFormatWidth;
+ measureUnit = other.measureUnit;
+ minimumExponentDigits = other.minimumExponentDigits;
+ minimumFractionDigits = other.minimumFractionDigits;
+ minimumGroupingDigits = other.minimumGroupingDigits;
+ minimumIntegerDigits = other.minimumIntegerDigits;
+ minimumSignificantDigits = other.minimumSignificantDigits;
+ multiplier = other.multiplier;
+ negativePrefix = other.negativePrefix;
+ negativePrefixPattern = other.negativePrefixPattern;
+ negativeSuffix = other.negativeSuffix;
+ negativeSuffixPattern = other.negativeSuffixPattern;
+ padPosition = other.padPosition;
+ padString = other.padString;
+ parseCaseSensitive = other.parseCaseSensitive;
+ parseGroupingMode = other.parseGroupingMode;
+ parseIntegerOnly = other.parseIntegerOnly;
+ parseMode = other.parseMode;
+ parseNoExponent = other.parseNoExponent;
+ parseToBigDecimal = other.parseToBigDecimal;
+ pluralRules = other.pluralRules;
+ positivePrefix = other.positivePrefix;
+ positivePrefixPattern = other.positivePrefixPattern;
+ positiveSuffix = other.positiveSuffix;
+ positiveSuffixPattern = other.positiveSuffixPattern;
+ roundingIncrement = other.roundingIncrement;
+ roundingMode = other.roundingMode;
+ secondaryGroupingSize = other.secondaryGroupingSize;
+ signAlwaysShown = other.signAlwaysShown;
+ significantDigitsMode = other.significantDigitsMode;
+ return this;
+ }
+
+ private boolean _equals(Properties other) {
+ boolean eq = true;
+ eq = eq && _equalsHelper(compactCustomData, other.compactCustomData);
+ eq = eq && _equalsHelper(compactStyle, other.compactStyle);
+ eq = eq && _equalsHelper(currency, other.currency);
+ eq = eq && _equalsHelper(currencyPluralInfo, other.currencyPluralInfo);
+ eq = eq && _equalsHelper(currencyStyle, other.currencyStyle);
+ eq = eq && _equalsHelper(currencyUsage, other.currencyUsage);
+ eq = eq && _equalsHelper(decimalPatternMatchRequired, other.decimalPatternMatchRequired);
+ eq = eq && _equalsHelper(decimalSeparatorAlwaysShown, other.decimalSeparatorAlwaysShown);
+ eq = eq && _equalsHelper(exponentSignAlwaysShown, other.exponentSignAlwaysShown);
+ eq = eq && _equalsHelper(formatWidth, other.formatWidth);
+ eq = eq && _equalsHelper(groupingSize, other.groupingSize);
+ eq = eq && _equalsHelper(magnitudeMultiplier, other.magnitudeMultiplier);
+ eq = eq && _equalsHelper(mathContext, other.mathContext);
+ eq = eq && _equalsHelper(maximumFractionDigits, other.maximumFractionDigits);
+ eq = eq && _equalsHelper(maximumIntegerDigits, other.maximumIntegerDigits);
+ eq = eq && _equalsHelper(maximumSignificantDigits, other.maximumSignificantDigits);
+ eq = eq && _equalsHelper(measureFormatWidth, other.measureFormatWidth);
+ eq = eq && _equalsHelper(measureUnit, other.measureUnit);
+ eq = eq && _equalsHelper(minimumExponentDigits, other.minimumExponentDigits);
+ eq = eq && _equalsHelper(minimumFractionDigits, other.minimumFractionDigits);
+ eq = eq && _equalsHelper(minimumGroupingDigits, other.minimumGroupingDigits);
+ eq = eq && _equalsHelper(minimumIntegerDigits, other.minimumIntegerDigits);
+ eq = eq && _equalsHelper(minimumSignificantDigits, other.minimumSignificantDigits);
+ eq = eq && _equalsHelper(multiplier, other.multiplier);
+ eq = eq && _equalsHelper(negativePrefix, other.negativePrefix);
+ eq = eq && _equalsHelper(negativePrefixPattern, other.negativePrefixPattern);
+ eq = eq && _equalsHelper(negativeSuffix, other.negativeSuffix);
+ eq = eq && _equalsHelper(negativeSuffixPattern, other.negativeSuffixPattern);
+ eq = eq && _equalsHelper(padPosition, other.padPosition);
+ eq = eq && _equalsHelper(padString, other.padString);
+ eq = eq && _equalsHelper(parseCaseSensitive, other.parseCaseSensitive);
+ eq = eq && _equalsHelper(parseGroupingMode, other.parseGroupingMode);
+ eq = eq && _equalsHelper(parseIntegerOnly, other.parseIntegerOnly);
+ eq = eq && _equalsHelper(parseMode, other.parseMode);
+ eq = eq && _equalsHelper(parseNoExponent, other.parseNoExponent);
+ eq = eq && _equalsHelper(parseToBigDecimal, other.parseToBigDecimal);
+ eq = eq && _equalsHelper(pluralRules, other.pluralRules);
+ eq = eq && _equalsHelper(positivePrefix, other.positivePrefix);
+ eq = eq && _equalsHelper(positivePrefixPattern, other.positivePrefixPattern);
+ eq = eq && _equalsHelper(positiveSuffix, other.positiveSuffix);
+ eq = eq && _equalsHelper(positiveSuffixPattern, other.positiveSuffixPattern);
+ eq = eq && _equalsHelper(roundingIncrement, other.roundingIncrement);
+ eq = eq && _equalsHelper(roundingMode, other.roundingMode);
+ eq = eq && _equalsHelper(secondaryGroupingSize, other.secondaryGroupingSize);
+ eq = eq && _equalsHelper(signAlwaysShown, other.signAlwaysShown);
+ eq = eq && _equalsHelper(significantDigitsMode, other.significantDigitsMode);
+ return eq;
+ }
+
+ private boolean _equalsHelper(boolean mine, boolean theirs) {
+ return mine == theirs;
+ }
+
+ private boolean _equalsHelper(int mine, int theirs) {
+ return mine == theirs;
+ }
+
+ private boolean _equalsHelper(Object mine, Object theirs) {
+ if (mine == theirs) return true;
+ if (mine == null) return false;
+ return mine.equals(theirs);
+ }
+
+ private int _hashCode() {
+ int hashCode = 0;
+ hashCode ^= _hashCodeHelper(compactCustomData);
+ hashCode ^= _hashCodeHelper(compactStyle);
+ hashCode ^= _hashCodeHelper(currency);
+ hashCode ^= _hashCodeHelper(currencyPluralInfo);
+ hashCode ^= _hashCodeHelper(currencyStyle);
+ hashCode ^= _hashCodeHelper(currencyUsage);
+ hashCode ^= _hashCodeHelper(decimalPatternMatchRequired);
+ hashCode ^= _hashCodeHelper(decimalSeparatorAlwaysShown);
+ hashCode ^= _hashCodeHelper(exponentSignAlwaysShown);
+ hashCode ^= _hashCodeHelper(formatWidth);
+ hashCode ^= _hashCodeHelper(groupingSize);
+ hashCode ^= _hashCodeHelper(magnitudeMultiplier);
+ hashCode ^= _hashCodeHelper(mathContext);
+ hashCode ^= _hashCodeHelper(maximumFractionDigits);
+ hashCode ^= _hashCodeHelper(maximumIntegerDigits);
+ hashCode ^= _hashCodeHelper(maximumSignificantDigits);
+ hashCode ^= _hashCodeHelper(measureFormatWidth);
+ hashCode ^= _hashCodeHelper(measureUnit);
+ hashCode ^= _hashCodeHelper(minimumExponentDigits);
+ hashCode ^= _hashCodeHelper(minimumFractionDigits);
+ hashCode ^= _hashCodeHelper(minimumGroupingDigits);
+ hashCode ^= _hashCodeHelper(minimumIntegerDigits);
+ hashCode ^= _hashCodeHelper(minimumSignificantDigits);
+ hashCode ^= _hashCodeHelper(multiplier);
+ hashCode ^= _hashCodeHelper(negativePrefix);
+ hashCode ^= _hashCodeHelper(negativePrefixPattern);
+ hashCode ^= _hashCodeHelper(negativeSuffix);
+ hashCode ^= _hashCodeHelper(negativeSuffixPattern);
+ hashCode ^= _hashCodeHelper(padPosition);
+ hashCode ^= _hashCodeHelper(padString);
+ hashCode ^= _hashCodeHelper(parseCaseSensitive);
+ hashCode ^= _hashCodeHelper(parseGroupingMode);
+ hashCode ^= _hashCodeHelper(parseIntegerOnly);
+ hashCode ^= _hashCodeHelper(parseMode);
+ hashCode ^= _hashCodeHelper(parseNoExponent);
+ hashCode ^= _hashCodeHelper(parseToBigDecimal);
+ hashCode ^= _hashCodeHelper(pluralRules);
+ hashCode ^= _hashCodeHelper(positivePrefix);
+ hashCode ^= _hashCodeHelper(positivePrefixPattern);
+ hashCode ^= _hashCodeHelper(positiveSuffix);
+ hashCode ^= _hashCodeHelper(positiveSuffixPattern);
+ hashCode ^= _hashCodeHelper(roundingIncrement);
+ hashCode ^= _hashCodeHelper(roundingMode);
+ hashCode ^= _hashCodeHelper(secondaryGroupingSize);
+ hashCode ^= _hashCodeHelper(signAlwaysShown);
+ hashCode ^= _hashCodeHelper(significantDigitsMode);
+ return hashCode;
+ }
+
+ private int _hashCodeHelper(boolean value) {
+ return value ? 1 : 0;
+ }
+
+ private int _hashCodeHelper(int value) {
+ return value * 13;
+ }
+
+ private int _hashCodeHelper(Object value) {
+ if (value == null) return 0;
+ return value.hashCode();
+ }
+
+ public Properties clear() {
+ return _clear();
+ }
+
+ /** Creates and returns a shallow copy of the property bag. */
+ @Override
+ public Properties clone() {
+ // super.clone() returns a shallow copy.
+ try {
+ return (Properties) super.clone();
+ } catch (CloneNotSupportedException e) {
+ // Should never happen since super is Object
+ throw new UnsupportedOperationException(e);
+ }
+ }
+
+ /**
+ * Shallow-copies the properties from the given property bag into this property bag.
+ *
+ * @param other The property bag from which to copy and which will not be modified.
+ * @return The current property bag (the one modified by this operation), for chaining.
+ */
+ public Properties copyFrom(Properties other) {
+ return _copyFrom(other);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null) return false;
+ if (this == other) return true;
+ if (!(other instanceof Properties)) return false;
+ return _equals((Properties) other);
+ }
+
+ /// BEGIN GETTERS/SETTERS ///
+
+ @Override
+ public Map<String, Map<String, String>> getCompactCustomData() {
+ return compactCustomData;
+ }
+
+ @Override
+ public CompactStyle getCompactStyle() {
+ return compactStyle;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ @Deprecated
+ public CurrencyPluralInfo getCurrencyPluralInfo() {
+ return currencyPluralInfo;
+ }
+
+ @Override
+ public CurrencyStyle getCurrencyStyle() {
+ return currencyStyle;
+ }
+
+ @Override
+ public CurrencyUsage getCurrencyUsage() {
+ return currencyUsage;
+ }
+
+ @Override
+ public boolean getDecimalPatternMatchRequired() {
+ return decimalPatternMatchRequired;
+ }
+
+ @Override
+ public boolean getDecimalSeparatorAlwaysShown() {
+ return decimalSeparatorAlwaysShown;
+ }
+
+ @Override
+ public boolean getExponentSignAlwaysShown() {
+ return exponentSignAlwaysShown;
+ }
+
+ @Override
+ public int getFormatWidth() {
+ return formatWidth;
+ }
+
+ @Override
+ public int getGroupingSize() {
+ return groupingSize;
+ }
+
+ @Override
+ public int getMagnitudeMultiplier() {
+ return magnitudeMultiplier;
+ }
+
+ @Override
+ public MathContext getMathContext() {
+ return mathContext;
+ }
+
+ @Override
+ public int getMaximumFractionDigits() {
+ return maximumFractionDigits;
+ }
+
+ @Override
+ public int getMaximumIntegerDigits() {
+ return maximumIntegerDigits;
+ }
+
+ @Override
+ public int getMaximumSignificantDigits() {
+ return maximumSignificantDigits;
+ }
+
+ @Override
+ public FormatWidth getMeasureFormatWidth() {
+ return measureFormatWidth;
+ }
+
+ @Override
+ public MeasureUnit getMeasureUnit() {
+ return measureUnit;
+ }
+
+ @Override
+ public int getMinimumExponentDigits() {
+ return minimumExponentDigits;
+ }
+
+ @Override
+ public int getMinimumFractionDigits() {
+ return minimumFractionDigits;
+ }
+
+ @Override
+ public int getMinimumGroupingDigits() {
+ return minimumGroupingDigits;
+ }
+
+ @Override
+ public int getMinimumIntegerDigits() {
+ return minimumIntegerDigits;
+ }
+
+ @Override
+ public int getMinimumSignificantDigits() {
+ return minimumSignificantDigits;
+ }
+
+ @Override
+ public BigDecimal getMultiplier() {
+ return multiplier;
+ }
+
+ @Override
+ public String getNegativePrefix() {
+ return negativePrefix;
+ }
+
+ @Override
+ public String getNegativePrefixPattern() {
+ return negativePrefixPattern;
+ }
+
+ @Override
+ public String getNegativeSuffix() {
+ return negativeSuffix;
+ }
+
+ @Override
+ public String getNegativeSuffixPattern() {
+ return negativeSuffixPattern;
+ }
+
+ @Override
+ public PadPosition getPadPosition() {
+ return padPosition;
+ }
+
+ @Override
+ public String getPadString() {
+ return padString;
+ }
+
+ @Override
+ public boolean getParseCaseSensitive() {
+ return parseCaseSensitive;
+ }
+
+ @Override
+ public GroupingMode getParseGroupingMode() {
+ return parseGroupingMode;
+ }
+
+ @Override
+ public boolean getParseIntegerOnly() {
+ return parseIntegerOnly;
+ }
+
+ @Override
+ public ParseMode getParseMode() {
+ return parseMode;
+ }
+
+ @Override
+ public boolean getParseNoExponent() {
+ return parseNoExponent;
+ }
+
+ @Override
+ public boolean getParseToBigDecimal() {
+ return parseToBigDecimal;
+ }
+
+ @Override
+ public PluralRules getPluralRules() {
+ return pluralRules;
+ }
+
+ @Override
+ public String getPositivePrefix() {
+ return positivePrefix;
+ }
+
+ @Override
+ public String getPositivePrefixPattern() {
+ return positivePrefixPattern;
+ }
+
+ @Override
+ public String getPositiveSuffix() {
+ return positiveSuffix;
+ }
+
+ @Override
+ public String getPositiveSuffixPattern() {
+ return positiveSuffixPattern;
+ }
+
+ @Override
+ public BigDecimal getRoundingIncrement() {
+ return roundingIncrement;
+ }
+
+ @Override
+ public RoundingMode getRoundingMode() {
+ return roundingMode;
+ }
+
+ @Override
+ public int getSecondaryGroupingSize() {
+ return secondaryGroupingSize;
+ }
+
+ @Override
+ public boolean getSignAlwaysShown() {
+ return signAlwaysShown;
+ }
+
+ @Override
+ public SignificantDigitsMode getSignificantDigitsMode() {
+ return significantDigitsMode;
+ }
+
+ @Override
+ public int hashCode() {
+ return _hashCode();
+ }
+
+ /** Custom serialization: re-create object from serialized properties. */
+ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
+ ois.defaultReadObject();
+
+ // Initialize to empty
+ clear();
+
+ // Extra int for possible future use
+ ois.readInt();
+
+ // 1) How many fields were serialized?
+ int count = ois.readInt();
+
+ // 2) Read each field by its name and value
+ for (int i = 0; i < count; i++) {
+ String name = (String) ois.readObject();
+ Object value = ois.readObject();
+
+ // Get the field reference
+ Field field = null;
+ try {
+ field = Properties.class.getDeclaredField(name);
+ } catch (NoSuchFieldException e) {
+ // The field name does not exist! Possibly corrupted serialization. Ignore this entry.
+ continue;
+ } catch (SecurityException e) {
+ // Should not happen
+ throw new AssertionError(e);
+ }
+
+ // NOTE: If the type of a field were changed in the future, this would be the place to check:
+ // If the variable `value` is the old type, perform any conversions necessary.
+
+ // Save value into the field
+ try {
+ field.set(this, value);
+ } catch (IllegalArgumentException e) {
+ // Should not happen
+ throw new AssertionError(e);
+ } catch (IllegalAccessException e) {
+ // Should not happen
+ throw new AssertionError(e);
+ }
+ }
+ }
+
+ @Override
+ public Properties setCompactCustomData(Map<String, Map<String, String>> compactCustomData) {
+ // TODO: compactCustomData is not immutable.
+ this.compactCustomData = compactCustomData;
+ return this;
+ }
+
+ @Override
+ public Properties setCompactStyle(CompactStyle compactStyle) {
+ this.compactStyle = compactStyle;
+ return this;
+ }
+
+ @Override
+ public Properties setCurrency(Currency currency) {
+ this.currency = currency;
+ return this;
+ }
+
+ @Override
+ @Deprecated
+ public Properties setCurrencyPluralInfo(CurrencyPluralInfo currencyPluralInfo) {
+ // TODO: In order to maintain immutability, we have to perform a clone here.
+ // It would be better to just retire CurrencyPluralInfo entirely.
+ if (currencyPluralInfo != null) {
+ currencyPluralInfo = (CurrencyPluralInfo) currencyPluralInfo.clone();
+ }
+ this.currencyPluralInfo = currencyPluralInfo;
+ return this;
+ }
+
+ @Override
+ public Properties setCurrencyStyle(CurrencyStyle currencyStyle) {
+ this.currencyStyle = currencyStyle;
+ return this;
+ }
+
+ @Override
+ public Properties setCurrencyUsage(CurrencyUsage currencyUsage) {
+ this.currencyUsage = currencyUsage;
+ return this;
+ }
+
+ @Override
+ public Properties setDecimalPatternMatchRequired(boolean decimalPatternMatchRequired) {
+ this.decimalPatternMatchRequired = decimalPatternMatchRequired;
+ return this;
+ }
+
+ @Override
+ public Properties setDecimalSeparatorAlwaysShown(boolean alwaysShowDecimal) {
+ this.decimalSeparatorAlwaysShown = alwaysShowDecimal;
+ return this;
+ }
+
+ @Override
+ public Properties setExponentSignAlwaysShown(boolean exponentSignAlwaysShown) {
+ this.exponentSignAlwaysShown = exponentSignAlwaysShown;
+ return this;
+ }
+
+ @Override
+ public Properties setFormatWidth(int paddingWidth) {
+ this.formatWidth = paddingWidth;
+ return this;
+ }
+
+ @Override
+ public Properties setGroupingSize(int groupingSize) {
+ this.groupingSize = groupingSize;
+ return this;
+ }
+
+ @Override
+ public Properties setMagnitudeMultiplier(int magnitudeMultiplier) {
+ this.magnitudeMultiplier = magnitudeMultiplier;
+ return this;
+ }
+
+ @Override
+ public Properties setMathContext(MathContext mathContext) {
+ this.mathContext = mathContext;
+ return this;
+ }
+
+ @Override
+ public Properties setMaximumFractionDigits(int maximumFractionDigits) {
+ this.maximumFractionDigits = maximumFractionDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMaximumIntegerDigits(int maximumIntegerDigits) {
+ this.maximumIntegerDigits = maximumIntegerDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMaximumSignificantDigits(int maximumSignificantDigits) {
+ this.maximumSignificantDigits = maximumSignificantDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMeasureFormatWidth(FormatWidth measureFormatWidth) {
+ this.measureFormatWidth = measureFormatWidth;
+ return this;
+ }
+
+ @Override
+ public Properties setMeasureUnit(MeasureUnit measureUnit) {
+ this.measureUnit = measureUnit;
+ return this;
+ }
+
+ @Override
+ public Properties setMinimumExponentDigits(int exponentDigits) {
+ this.minimumExponentDigits = exponentDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMinimumFractionDigits(int minimumFractionDigits) {
+ this.minimumFractionDigits = minimumFractionDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMinimumGroupingDigits(int minimumGroupingDigits) {
+ this.minimumGroupingDigits = minimumGroupingDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMinimumIntegerDigits(int minimumIntegerDigits) {
+ this.minimumIntegerDigits = minimumIntegerDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMinimumSignificantDigits(int minimumSignificantDigits) {
+ this.minimumSignificantDigits = minimumSignificantDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMultiplier(BigDecimal multiplier) {
+ this.multiplier = multiplier;
+ return this;
+ }
+
+ @Override
+ public Properties setNegativePrefix(String negativePrefix) {
+ this.negativePrefix = negativePrefix;
+ return this;
+ }
+
+ @Override
+ public Properties setNegativePrefixPattern(String negativePrefixPattern) {
+ this.negativePrefixPattern = negativePrefixPattern;
+ return this;
+ }
+
+ @Override
+ public Properties setNegativeSuffix(String negativeSuffix) {
+ this.negativeSuffix = negativeSuffix;
+ return this;
+ }
+
+ @Override
+ public Properties setNegativeSuffixPattern(String negativeSuffixPattern) {
+ this.negativeSuffixPattern = negativeSuffixPattern;
+ return this;
+ }
+
+ @Override
+ public Properties setPadPosition(PadPosition paddingLocation) {
+ this.padPosition = paddingLocation;
+ return this;
+ }
+
+ @Override
+ public Properties setPadString(String paddingString) {
+ this.padString = paddingString;
+ return this;
+ }
+
+ @Override
+ public Properties setParseCaseSensitive(boolean parseCaseSensitive) {
+ this.parseCaseSensitive = parseCaseSensitive;
+ return this;
+ }
+
+ @Override
+ public Properties setParseGroupingMode(GroupingMode parseGroupingMode) {
+ this.parseGroupingMode = parseGroupingMode;
+ return this;
+ }
+
+ @Override
+ public Properties setParseIntegerOnly(boolean parseIntegerOnly) {
+ this.parseIntegerOnly = parseIntegerOnly;
+ return this;
+ }
+
+ @Override
+ public Properties setParseMode(ParseMode parseMode) {
+ this.parseMode = parseMode;
+ return this;
+ }
+
+ @Override
+ public Properties setParseNoExponent(boolean parseNoExponent) {
+ this.parseNoExponent = parseNoExponent;
+ return this;
+ }
+
+ @Override
+ public Properties setParseToBigDecimal(boolean parseToBigDecimal) {
+ this.parseToBigDecimal = parseToBigDecimal;
+ return this;
+ }
+
+ @Override
+ public Properties setPluralRules(PluralRules pluralRules) {
+ this.pluralRules = pluralRules;
+ return this;
+ }
+
+ @Override
+ public Properties setPositivePrefix(String positivePrefix) {
+ this.positivePrefix = positivePrefix;
+ return this;
+ }
+
+ @Override
+ public Properties setPositivePrefixPattern(String positivePrefixPattern) {
+ this.positivePrefixPattern = positivePrefixPattern;
+ return this;
+ }
+
+ @Override
+ public Properties setPositiveSuffix(String positiveSuffix) {
+ this.positiveSuffix = positiveSuffix;
+ return this;
+ }
+
+ @Override
+ public Properties setPositiveSuffixPattern(String positiveSuffixPattern) {
+ this.positiveSuffixPattern = positiveSuffixPattern;
+ return this;
+ }
+
+ @Override
+ public Properties setRoundingIncrement(BigDecimal roundingIncrement) {
+ this.roundingIncrement = roundingIncrement;
+ return this;
+ }
+
+ @Override
+ public Properties setRoundingMode(RoundingMode roundingMode) {
+ this.roundingMode = roundingMode;
+ return this;
+ }
+
+ @Override
+ public Properties setSecondaryGroupingSize(int secondaryGroupingSize) {
+ this.secondaryGroupingSize = secondaryGroupingSize;
+ return this;
+ }
+
+ @Override
+ public Properties setSignAlwaysShown(boolean signAlwaysShown) {
+ this.signAlwaysShown = signAlwaysShown;
+ return this;
+ }
+
+ @Override
+ public Properties setSignificantDigitsMode(SignificantDigitsMode significantDigitsMode) {
+ this.significantDigitsMode = significantDigitsMode;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("<Properties");
+ toStringBare(result);
+ result.append(">");
+ return result.toString();
+ }
+
+ /**
+ * Appends a string containing properties that differ from the default, but without being
+ * surrounded by &lt;Properties&gt;.
+ */
+ public void toStringBare(StringBuilder result) {
+ Field[] fields = Properties.class.getDeclaredFields();
+ for (Field field : fields) {
+ Object myValue, defaultValue;
+ try {
+ myValue = field.get(this);
+ defaultValue = field.get(DEFAULT);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ continue;
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ continue;
+ }
+ if (myValue == null && defaultValue == null) {
+ continue;
+ } else if (myValue == null || defaultValue == null) {
+ result.append(" " + field.getName() + ":" + myValue);
+ } else if (!myValue.equals(defaultValue)) {
+ result.append(" " + field.getName() + ":" + myValue);
+ }
+ }
+ }
+
+ /**
+ * Custom serialization: save fields along with their name, so that fields can be easily added in
+ * the future in any order. Only save fields that differ from their default value.
+ */
+ private void writeObject(ObjectOutputStream oos) throws IOException {
+ oos.defaultWriteObject();
+
+ // Extra int for possible future use
+ oos.writeInt(0);
+
+ ArrayList<Field> fieldsToSerialize = new ArrayList<Field>();
+ ArrayList<Object> valuesToSerialize = new ArrayList<Object>();
+ Field[] fields = Properties.class.getDeclaredFields();
+ for (Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers())) {
+ continue;
+ }
+ try {
+ Object myValue = field.get(this);
+ if (myValue == null) {
+ // All *Object* values default to null; no need to serialize.
+ continue;
+ }
+ Object defaultValue = field.get(DEFAULT);
+ if (!myValue.equals(defaultValue)) {
+ fieldsToSerialize.add(field);
+ valuesToSerialize.add(myValue);
+ }
+ } catch (IllegalArgumentException e) {
+ // Should not happen
+ throw new AssertionError(e);
+ } catch (IllegalAccessException e) {
+ // Should not happen
+ throw new AssertionError(e);
+ }
+ }
+
+ // 1) How many fields are to be serialized?
+ int count = fieldsToSerialize.size();
+ oos.writeInt(count);
+
+ // 2) Write each field with its name and value
+ for (int i = 0; i < count; i++) {
+ Field field = fieldsToSerialize.get(i);
+ Object value = valuesToSerialize.get(i);
+ oos.writeObject(field.getName());
+ oos.writeObject(value);
+ }
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/Rounder.java b/android_icu4j/src/main/java/android/icu/impl/number/Rounder.java
new file mode 100644
index 000000000..1a286cf6a
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/Rounder.java
@@ -0,0 +1,250 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+import android.icu.impl.number.formatters.CompactDecimalFormat;
+import android.icu.impl.number.formatters.ScientificFormat;
+
+/**
+ * The base class for a Rounder used by ICU Decimal Format.
+ *
+ * <p>A Rounder must implement the method {@link #apply}. An implementation must:
+ *
+ * <ol>
+ * <li>Either have the code <code>applyDefaults(input);</code> in its apply function, or otherwise
+ * ensure that minFrac, maxFrac, minInt, and maxInt are obeyed, paying special attention to
+ * the case when the input is zero.
+ * <li>Call one of {@link FormatQuantity#roundToIncrement}, {@link
+ * FormatQuantity#roundToMagnitude}, or {@link FormatQuantity#roundToInfinity} on the input.
+ * </ol>
+ *
+ * <p>In order to be used by {@link CompactDecimalFormat} and {@link ScientificFormat}, among
+ * others, your rounder must be stable upon <em>decreasing</em> the magnitude of the input number.
+ * For example, if your rounder converts "999" to "1000", it must also convert "99.9" to "100" and
+ * "0.999" to "1". (The opposite does not need to be the case: you can round "0.999" to "1" but keep
+ * "999" as "999".)
+ *
+ * @see android.icu.impl.number.rounders.MagnitudeRounder
+ * @see android.icu.impl.number.rounders.IncrementRounder
+ * @see android.icu.impl.number.rounders.SignificantDigitsRounder
+ * @see android.icu.impl.number.rounders.NoRounder
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public abstract class Rounder extends Format.BeforeFormat {
+
+ public static interface IBasicRoundingProperties {
+
+ static int DEFAULT_MINIMUM_INTEGER_DIGITS = -1;
+
+ /** @see #setMinimumIntegerDigits */
+ public int getMinimumIntegerDigits();
+
+ /**
+ * Sets the minimum number of digits to display before the decimal point. If the number has
+ * fewer than this number of digits, the number will be padded with zeros. The pattern "#00.0#",
+ * for example, corresponds to 2 minimum integer digits, and the number 5.3 would be formatted
+ * as "05.3" in locale <em>en-US</em>.
+ *
+ * @param minimumIntegerDigits The minimum number of integer digits to output.
+ * @return The property bag, for chaining.
+ */
+ public IBasicRoundingProperties setMinimumIntegerDigits(int minimumIntegerDigits);
+
+ static int DEFAULT_MAXIMUM_INTEGER_DIGITS = -1;
+
+ /** @see #setMaximumIntegerDigits */
+ public int getMaximumIntegerDigits();
+
+ /**
+ * Sets the maximum number of digits to display before the decimal point. If the number has more
+ * than this number of digits, the extra digits will be truncated. For example, if maximum
+ * integer digits is 2, and you attempt to format the number 1970, you will get "70" in locale
+ * <em>en-US</em>. It is not possible to specify the maximum integer digits using a pattern
+ * string, except in the special case of a scientific format pattern.
+ *
+ * @param maximumIntegerDigits The maximum number of integer digits to output.
+ * @return The property bag, for chaining.
+ */
+ public IBasicRoundingProperties setMaximumIntegerDigits(int maximumIntegerDigits);
+
+ static int DEFAULT_MINIMUM_FRACTION_DIGITS = -1;
+
+ /** @see #setMinimumFractionDigits */
+ public int getMinimumFractionDigits();
+
+ /**
+ * Sets the minimum number of digits to display after the decimal point. If the number has fewer
+ * than this number of digits, the number will be padded with zeros. The pattern "#00.0#", for
+ * example, corresponds to 1 minimum fraction digit, and the number 456 would be formatted as
+ * "456.0" in locale <em>en-US</em>.
+ *
+ * @param minimumFractionDigits The minimum number of fraction digits to output.
+ * @return The property bag, for chaining.
+ */
+ public IBasicRoundingProperties setMinimumFractionDigits(int minimumFractionDigits);
+
+ static int DEFAULT_MAXIMUM_FRACTION_DIGITS = -1;
+
+ /** @see #setMaximumFractionDigits */
+ public int getMaximumFractionDigits();
+
+ /**
+ * Sets the maximum number of digits to display after the decimal point. If the number has fewer
+ * than this number of digits, the number will be rounded off using the rounding mode specified
+ * by {@link #setRoundingMode(RoundingMode)}. The pattern "#00.0#", for example, corresponds to
+ * 2 maximum fraction digits, and the number 456.789 would be formatted as "456.79" in locale
+ * <em>en-US</em> with the default rounding mode. Note that the number 456.999 would be
+ * formatted as "457.0" given the same configurations.
+ *
+ * @param maximumFractionDigits The maximum number of fraction digits to output.
+ * @return The property bag, for chaining.
+ */
+ public IBasicRoundingProperties setMaximumFractionDigits(int maximumFractionDigits);
+
+ static RoundingMode DEFAULT_ROUNDING_MODE = null;
+
+ /** @see #setRoundingMode */
+ public RoundingMode getRoundingMode();
+
+ /**
+ * Sets the rounding mode, which determines under which conditions extra decimal places are
+ * rounded either up or down. See {@link RoundingMode} for details on the choices of rounding
+ * mode. The default if not set explicitly is {@link RoundingMode#HALF_EVEN}.
+ *
+ * <p>This setting is ignored if {@link #setMathContext} is used.
+ *
+ * @param roundingMode The rounding mode to use when rounding is required.
+ * @return The property bag, for chaining.
+ * @see RoundingMode
+ * @see #setMathContext
+ */
+ public IBasicRoundingProperties setRoundingMode(RoundingMode roundingMode);
+
+ static MathContext DEFAULT_MATH_CONTEXT = null;
+
+ /** @see #setMathContext */
+ public MathContext getMathContext();
+
+ /**
+ * Sets the {@link MathContext} to be used during math and rounding operations. A MathContext
+ * encapsulates a RoundingMode and the number of significant digits in the output.
+ *
+ * @param mathContext The math context to use when rounding is required.
+ * @return The property bag, for chaining.
+ * @see MathContext
+ * @see #setRoundingMode
+ */
+ public IBasicRoundingProperties setMathContext(MathContext mathContext);
+ }
+
+ public static interface MultiplierGenerator {
+ public int getMultiplier(int magnitude);
+ }
+
+ // Properties available to all rounding strategies
+ protected final MathContext mathContext;
+ protected final int minInt;
+ protected final int maxInt;
+ protected final int minFrac;
+ protected final int maxFrac;
+
+ /**
+ * Constructor that uses integer and fraction digit lengths from IBasicRoundingProperties.
+ *
+ * @param properties
+ */
+ protected Rounder(IBasicRoundingProperties properties) {
+ mathContext = RoundingUtils.getMathContextOrUnlimited(properties);
+
+ int _maxInt = properties.getMaximumIntegerDigits();
+ int _minInt = properties.getMinimumIntegerDigits();
+ int _maxFrac = properties.getMaximumFractionDigits();
+ int _minFrac = properties.getMinimumFractionDigits();
+
+ // 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;
+ maxFrac = _maxFrac < 0 ? Integer.MAX_VALUE : _maxFrac < minFrac ? minFrac : _maxFrac;
+ minInt = 0;
+ maxInt = _maxInt < 0 ? Integer.MAX_VALUE : _maxInt;
+ } else {
+ // Force a digit before the decimal point.
+ minFrac = _minFrac < 0 ? 0 : _minFrac;
+ maxFrac = _maxFrac < 0 ? Integer.MAX_VALUE : _maxFrac < minFrac ? minFrac : _maxFrac;
+ minInt = _minInt <= 0 ? 1 : _minInt;
+ maxInt = _maxInt < 0 ? Integer.MAX_VALUE : _maxInt < minInt ? minInt : _maxInt;
+ }
+ }
+
+ /**
+ * Perform rounding and specification of integer and fraction digit lengths on the input quantity.
+ * Calling this method will change the state of the FormatQuantity.
+ *
+ * @param input The {@link FormatQuantity} to be modified and rounded.
+ */
+ public abstract void apply(FormatQuantity input);
+
+ /**
+ * Rounding can affect the magnitude. First we attempt to adjust according to the original
+ * magnitude, and if the magnitude changes, we adjust according to a magnitude one greater. Note
+ * that this algorithm assumes that increasing the multiplier never increases the number of digits
+ * that can be displayed.
+ *
+ * @param input The quantity to be rounded.
+ * @param mg The implementation that returns magnitude adjustment based on a given starting
+ * magnitude.
+ * @return The multiplier that was chosen to best fit the input.
+ */
+ public int chooseMultiplierAndApply(FormatQuantity input, MultiplierGenerator mg) {
+ // TODO: Avoid the object creation here.
+ FormatQuantity copy = input.createCopy();
+
+ int magnitude = input.getMagnitude();
+ int multiplier = mg.getMultiplier(magnitude);
+ input.adjustMagnitude(multiplier);
+ apply(input);
+ if (input.getMagnitude() == magnitude + multiplier + 1) {
+ magnitude += 1;
+ input.copyFrom(copy);
+ multiplier = mg.getMultiplier(magnitude);
+ input.adjustMagnitude(multiplier);
+ assert input.getMagnitude() == magnitude + multiplier - 1;
+ apply(input);
+ assert input.getMagnitude() == magnitude + multiplier;
+ }
+
+ return multiplier;
+ }
+
+ /**
+ * Implementations can call this method to perform default logic for min/max digits. This method
+ * performs logic for handling of a zero input.
+ *
+ * @param input The digits being formatted.
+ */
+ protected void applyDefaults(FormatQuantity input) {
+ input.setIntegerFractionLength(minInt, maxInt, minFrac, maxFrac);
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+ apply(input);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ properties.setMathContext(mathContext);
+ properties.setRoundingMode(mathContext.getRoundingMode());
+ properties.setMinimumFractionDigits(minFrac);
+ properties.setMinimumIntegerDigits(minInt);
+ properties.setMaximumFractionDigits(maxFrac);
+ properties.setMaximumIntegerDigits(maxInt);
+ }
+}
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
new file mode 100644
index 000000000..5595f89ab
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/RoundingUtils.java
@@ -0,0 +1,167 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+import android.icu.impl.number.Rounder.IBasicRoundingProperties;
+
+/** @author sffc
+ * @hide Only a subset of ICU is exposed in Android*/
+public class RoundingUtils {
+
+ public static final int SECTION_LOWER = 1;
+ public static final int SECTION_MIDPOINT = 2;
+ public static final int SECTION_UPPER = 3;
+
+ /**
+ * Converts a rounding mode and metadata about the quantity being rounded to a boolean determining
+ * whether the value should be rounded toward infinity or toward zero.
+ *
+ * <p>The parameters are of type int because benchmarks on an x86-64 processor against OpenJDK
+ * showed that ints were demonstrably faster than enums in switch statements.
+ *
+ * @param isEven Whether the digit immediately before the rounding magnitude is even.
+ * @param isNegative Whether the quantity is negative.
+ * @param section Whether the part of the quantity to the right of the rounding magnitude is
+ * exactly halfway between two digits, whether it is in the lower part (closer to zero), or
+ * whether it is in the upper part (closer to infinity). See {@link #SECTION_LOWER}, {@link
+ * #SECTION_MIDPOINT}, and {@link #SECTION_UPPER}.
+ * @param roundingMode The integer version of the {@link RoundingMode}, which you can get via
+ * {@link RoundingMode#ordinal}.
+ * @param reference A reference object to be used when throwing an ArithmeticException.
+ * @return true if the number should be rounded toward zero; false if it should be rounded toward
+ * infinity.
+ */
+ public static boolean getRoundingDirection(
+ boolean isEven, boolean isNegative, int section, int roundingMode, Object reference) {
+ switch (roundingMode) {
+ case BigDecimal.ROUND_UP:
+ // round away from zero
+ return false;
+
+ case BigDecimal.ROUND_DOWN:
+ // round toward zero
+ return true;
+
+ case BigDecimal.ROUND_CEILING:
+ // round toward positive infinity
+ return isNegative;
+
+ case BigDecimal.ROUND_FLOOR:
+ // round toward negative infinity
+ return !isNegative;
+
+ case BigDecimal.ROUND_HALF_UP:
+ switch (section) {
+ case SECTION_MIDPOINT:
+ return false;
+ case SECTION_LOWER:
+ return true;
+ case SECTION_UPPER:
+ return false;
+ }
+ break;
+
+ case BigDecimal.ROUND_HALF_DOWN:
+ switch (section) {
+ case SECTION_MIDPOINT:
+ return true;
+ case SECTION_LOWER:
+ return true;
+ case SECTION_UPPER:
+ return false;
+ }
+ break;
+
+ case BigDecimal.ROUND_HALF_EVEN:
+ switch (section) {
+ case SECTION_MIDPOINT:
+ return isEven;
+ case SECTION_LOWER:
+ return true;
+ case SECTION_UPPER:
+ return false;
+ }
+ break;
+ }
+
+ // Rounding mode UNNECESSARY
+ throw new ArithmeticException("Rounding is required on " + reference.toString());
+ }
+
+ /**
+ * Gets whether the given rounding mode's rounding boundary is at the midpoint. The rounding
+ * boundary is the point at which a number switches from being rounded down to being rounded up.
+ * For example, with rounding mode HALF_EVEN, HALF_UP, or HALF_DOWN, the rounding boundary is at
+ * the midpoint, and this function would return true. However, for UP, DOWN, CEILING, and FLOOR,
+ * the rounding boundary is at the "edge", and this function would return false.
+ *
+ * @param roundingMode The integer version of the {@link RoundingMode}.
+ * @return true if rounding mode is HALF_EVEN, HALF_UP, or HALF_DOWN; false otherwise.
+ */
+ public static boolean roundsAtMidpoint(int roundingMode) {
+ switch (roundingMode) {
+ case BigDecimal.ROUND_UP:
+ case BigDecimal.ROUND_DOWN:
+ case BigDecimal.ROUND_CEILING:
+ case BigDecimal.ROUND_FLOOR:
+ return false;
+
+ default:
+ return true;
+ }
+ }
+
+ private static final MathContext[] MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED =
+ new MathContext[RoundingMode.values().length];
+
+ private static final MathContext[] MATH_CONTEXT_BY_ROUNDING_MODE_34_DIGITS =
+ new MathContext[RoundingMode.values().length];
+
+ static {
+ for (int i = 0; i < MATH_CONTEXT_BY_ROUNDING_MODE_34_DIGITS.length; i++) {
+ MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED[i] = new MathContext(0, RoundingMode.valueOf(i));
+ MATH_CONTEXT_BY_ROUNDING_MODE_34_DIGITS[i] = new MathContext(34);
+ }
+ }
+
+ /**
+ * Gets the user-specified math context out of the property bag. If there is none, falls back to a
+ * math context with unlimited precision and the user-specified rounding mode, which defaults to
+ * HALF_EVEN (the IEEE 754R default).
+ *
+ * @param properties The property bag.
+ * @return A {@link MathContext}. Never null.
+ */
+ public static MathContext getMathContextOrUnlimited(IBasicRoundingProperties properties) {
+ MathContext mathContext = properties.getMathContext();
+ if (mathContext == null) {
+ RoundingMode roundingMode = properties.getRoundingMode();
+ if (roundingMode == null) roundingMode = RoundingMode.HALF_EVEN;
+ mathContext = MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED[roundingMode.ordinal()];
+ }
+ return mathContext;
+ }
+
+ /**
+ * Gets the user-specified math context out of the property bag. If there is none, falls back to a
+ * math context with 34 digits of precision (the 128-bit IEEE 754R default) and the user-specified
+ * rounding mode, which defaults to HALF_EVEN (the IEEE 754R default).
+ *
+ * @param properties The property bag.
+ * @return A {@link MathContext}. Never null.
+ */
+ public static MathContext getMathContextOr34Digits(IBasicRoundingProperties properties) {
+ MathContext mathContext = properties.getMathContext();
+ if (mathContext == null) {
+ RoundingMode roundingMode = properties.getRoundingMode();
+ if (roundingMode == null) roundingMode = RoundingMode.HALF_EVEN;
+ mathContext = MATH_CONTEXT_BY_ROUNDING_MODE_34_DIGITS[roundingMode.ordinal()];
+ }
+ return mathContext;
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/formatters/BigDecimalMultiplier.java b/android_icu4j/src/main/java/android/icu/impl/number/formatters/BigDecimalMultiplier.java
new file mode 100644
index 000000000..e9cf1eb40
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/formatters/BigDecimalMultiplier.java
@@ -0,0 +1,61 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.formatters;
+
+import java.math.BigDecimal;
+
+import android.icu.impl.number.Format.BeforeFormat;
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.ModifierHolder;
+import android.icu.impl.number.Properties;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class BigDecimalMultiplier extends BeforeFormat {
+ public static interface IProperties {
+
+ static BigDecimal DEFAULT_MULTIPLIER = null;
+
+ /** @see #setMultiplier */
+ public BigDecimal getMultiplier();
+
+ /**
+ * Multiply all numbers by this amount before formatting.
+ *
+ * @param multiplier The amount to multiply by.
+ * @return The property bag, for chaining.
+ * @see MagnitudeMultiplier
+ */
+ public IProperties setMultiplier(BigDecimal multiplier);
+ }
+
+ public static boolean useMultiplier(IProperties properties) {
+ return properties.getMultiplier() != IProperties.DEFAULT_MULTIPLIER;
+ }
+
+ private final BigDecimal multiplier;
+
+ public static BigDecimalMultiplier getInstance(IProperties properties) {
+ if (properties.getMultiplier() == null) {
+ throw new IllegalArgumentException("The multiplier must be present for MultiplierFormat");
+ }
+ // TODO: Intelligently fall back to a MagnitudeMultiplier if the multiplier is a power of ten?
+ return new BigDecimalMultiplier(properties);
+ }
+
+ private BigDecimalMultiplier(IProperties properties) {
+ this.multiplier = properties.getMultiplier();
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+ input.multiplyBy(multiplier);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ properties.setMultiplier(multiplier);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/formatters/CompactDecimalFormat.java b/android_icu4j/src/main/java/android/icu/impl/number/formatters/CompactDecimalFormat.java
new file mode 100644
index 000000000..525d43201
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/formatters/CompactDecimalFormat.java
@@ -0,0 +1,565 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.formatters;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.MissingResourceException;
+
+import android.icu.impl.ICUData;
+import android.icu.impl.ICUResourceBundle;
+import android.icu.impl.StandardPlural;
+import android.icu.impl.UResource;
+import android.icu.impl.number.Format;
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.Modifier;
+import android.icu.impl.number.Modifier.PositiveNegativeModifier;
+import android.icu.impl.number.ModifierHolder;
+import android.icu.impl.number.PNAffixGenerator;
+import android.icu.impl.number.PatternString;
+import android.icu.impl.number.Properties;
+import android.icu.impl.number.Rounder;
+import android.icu.impl.number.modifiers.ConstantAffixModifier;
+import android.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
+import android.icu.impl.number.rounders.SignificantDigitsRounder;
+import android.icu.text.CompactDecimalFormat.CompactStyle;
+import android.icu.text.DecimalFormat.SignificantDigitsMode;
+import android.icu.text.DecimalFormatSymbols;
+import android.icu.text.NumberFormat;
+import android.icu.text.NumberingSystem;
+import android.icu.text.PluralRules;
+import android.icu.util.ULocale;
+import android.icu.util.UResourceBundle;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class CompactDecimalFormat extends Format.BeforeFormat {
+ public static interface IProperties
+ extends RoundingFormat.IProperties, CurrencyFormat.ICurrencyProperties {
+
+ static CompactStyle DEFAULT_COMPACT_STYLE = null;
+
+ /** @see #setCompactStyle */
+ public CompactStyle getCompactStyle();
+
+ /**
+ * Use compact decimal formatting with the specified {@link CompactStyle}. CompactStyle.SHORT
+ * produces output like "10K" in locale <em>en-US</em>, whereas CompactStyle.LONG produces
+ * output like "10 thousand" in that locale.
+ *
+ * @param compactStyle The style of prefixes/suffixes to append.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setCompactStyle(CompactStyle compactStyle);
+
+ static Map<String, Map<String, String>> DEFAULT_COMPACT_CUSTOM_DATA = null;
+
+ /** @see #setCompactCustomData */
+ public Map<String, Map<String, String>> getCompactCustomData();
+
+ /**
+ * Specifies custom data to be used instead of CLDR data when constructing a
+ * CompactDecimalFormat. The argument should be a map with the following structure:
+ *
+ * <pre>
+ * {
+ * "1000": {
+ * "one": "0 thousand",
+ * "other": "0 thousand"
+ * },
+ * "10000": {
+ * "one": "00 thousand",
+ * "other": "00 thousand"
+ * },
+ * // ...
+ * }
+ * </pre>
+ *
+ * This API endpoint is used by the CLDR Survey Tool.
+ *
+ * @param compactCustomData A map with the above structure.
+ * @return The property bag, for chaining.
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public IProperties setCompactCustomData(Map<String, Map<String, String>> compactCustomData);
+ }
+
+ public static boolean useCompactDecimalFormat(IProperties properties) {
+ return properties.getCompactStyle() != IProperties.DEFAULT_COMPACT_STYLE;
+ }
+
+ static final int MAX_DIGITS = 15;
+
+ // Properties
+ private final CompactDecimalData data;
+ private final Rounder rounder;
+ private final PositiveNegativeModifier defaultMod;
+ private final CompactStyle style; // retained for exporting only
+
+ public static CompactDecimalFormat getInstance(
+ DecimalFormatSymbols symbols, IProperties properties) {
+ return new CompactDecimalFormat(symbols, properties);
+ }
+
+ private static final int DEFAULT_MIN_SIG = 1;
+ private static final int DEFAULT_MAX_SIG = 2;
+ private static final SignificantDigitsMode DEFAULT_SIG_MODE =
+ SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION;
+
+ private static final ThreadLocal<Properties> threadLocalProperties =
+ new ThreadLocal<Properties>() {
+ @Override
+ protected Properties initialValue() {
+ return new Properties();
+ }
+ };
+
+ private static Rounder getRounder(IProperties properties) {
+ // Use rounding settings if they were specified, or else use the default CDF rounder.
+ // TODO: Detecting and overriding significant digits here is a bit of a hack, since detection
+ // is also performed in the "RoundingFormat.getDefaultOrNull" method.
+ // It would be more elegant to call some sort of "fallback" copy method.
+ Rounder rounder = null;
+ if (!SignificantDigitsRounder.useSignificantDigits(properties)) {
+ rounder = RoundingFormat.getDefaultOrNull(properties);
+ }
+ if (rounder == null) {
+ int _minSig = properties.getMinimumSignificantDigits();
+ int _maxSig = properties.getMaximumSignificantDigits();
+ SignificantDigitsMode _mode = properties.getSignificantDigitsMode();
+ Properties rprops = threadLocalProperties.get().clear();
+ // Settings needing possible override:
+ rprops.setMinimumSignificantDigits(_minSig > 0 ? _minSig : DEFAULT_MIN_SIG);
+ rprops.setMaximumSignificantDigits(_maxSig > 0 ? _maxSig : DEFAULT_MAX_SIG);
+ rprops.setSignificantDigitsMode(_mode != null ? _mode : DEFAULT_SIG_MODE);
+ // TODO: Should copyFrom() be used instead? It requires a cast.
+ // Settings to copy verbatim:
+ rprops.setRoundingMode(properties.getRoundingMode());
+ rprops.setMinimumFractionDigits(properties.getMinimumFractionDigits());
+ rprops.setMaximumFractionDigits(properties.getMaximumFractionDigits());
+ rprops.setMinimumIntegerDigits(properties.getMinimumIntegerDigits());
+ rprops.setMaximumIntegerDigits(properties.getMaximumIntegerDigits());
+ rounder = SignificantDigitsRounder.getInstance(rprops);
+ }
+ return rounder;
+ }
+
+ protected static final ThreadLocal<Map<CompactDecimalFingerprint, CompactDecimalData>>
+ threadLocalDataCache =
+ new ThreadLocal<Map<CompactDecimalFingerprint, CompactDecimalData>>() {
+ @Override
+ protected Map<CompactDecimalFingerprint, CompactDecimalData> initialValue() {
+ return new HashMap<CompactDecimalFingerprint, CompactDecimalData>();
+ }
+ };
+
+ private static CompactDecimalData getData(
+ DecimalFormatSymbols symbols, CompactDecimalFingerprint fingerprint) {
+ // See if we already have a data object based on the fingerprint
+ CompactDecimalData data = threadLocalDataCache.get().get(fingerprint);
+ if (data != null) return data;
+
+ // Make data bundle object
+ data = new CompactDecimalData();
+ ULocale ulocale = symbols.getULocale();
+ CompactDecimalDataSink sink = new CompactDecimalDataSink(data, symbols, fingerprint);
+ String nsName = NumberingSystem.getInstance(ulocale).getName();
+ ICUResourceBundle rb =
+ (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, ulocale);
+ internalPopulateData(nsName, rb, sink, data);
+ if (data.isEmpty() && fingerprint.compactStyle == CompactStyle.LONG) {
+ // No long data is available; load short data instead
+ sink.compactStyle = CompactStyle.SHORT;
+ internalPopulateData(nsName, rb, sink, data);
+ }
+ threadLocalDataCache.get().put(fingerprint, data);
+ return data;
+ }
+
+ private static void internalPopulateData(
+ String nsName, ICUResourceBundle rb, CompactDecimalDataSink sink, CompactDecimalData data) {
+ try {
+ rb.getAllItemsWithFallback("NumberElements/" + nsName, sink);
+ } catch (MissingResourceException e) {
+ // Fall back to latn
+ }
+ if (data.isEmpty() && !nsName.equals("latn")) {
+ rb.getAllItemsWithFallback("NumberElements/latn", sink);
+ }
+ if (sink.exception != null) {
+ throw sink.exception;
+ }
+ }
+
+ private static PositiveNegativeModifier getDefaultMod(
+ DecimalFormatSymbols symbols, CompactDecimalFingerprint fingerprint) {
+ ULocale uloc = symbols.getULocale();
+ String pattern;
+ if (fingerprint.compactType == CompactType.CURRENCY) {
+ pattern = NumberFormat.getPatternForStyle(uloc, NumberFormat.CURRENCYSTYLE);
+ } else {
+ pattern = NumberFormat.getPatternForStyle(uloc, NumberFormat.NUMBERSTYLE);
+ }
+ // TODO: Clean this up; avoid the extra object creations.
+ // TODO: Currency may also need to override grouping settings, not just affixes.
+ Properties properties = PatternString.parseToProperties(pattern);
+ PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
+ PNAffixGenerator.Result result =
+ pnag.getModifiers(symbols, fingerprint.currencySymbol, properties);
+ return new PositiveNegativeAffixModifier(result.positive, result.negative);
+ }
+
+ private CompactDecimalFormat(DecimalFormatSymbols symbols, IProperties properties) {
+ CompactDecimalFingerprint fingerprint = new CompactDecimalFingerprint(symbols, properties);
+ this.rounder = getRounder(properties);
+ // Short-circuit and use custom data if provided
+ if (properties.getCompactCustomData() != null) {
+ this.data = createDataFromCustom(symbols, fingerprint, properties.getCompactCustomData());
+ } else {
+ this.data = getData(symbols, fingerprint);
+ }
+ this.defaultMod = getDefaultMod(symbols, fingerprint);
+ this.style = properties.getCompactStyle(); // for exporting only
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) {
+ apply(input, mods, rules, rounder, data, defaultMod);
+ }
+
+ @Override
+ protected void before(FormatQuantity input, ModifierHolder mods) {
+ throw new UnsupportedOperationException();
+ }
+
+ public static void apply(
+ FormatQuantity input,
+ ModifierHolder mods,
+ PluralRules rules,
+ DecimalFormatSymbols symbols,
+ IProperties properties) {
+ CompactDecimalFingerprint fingerprint = new CompactDecimalFingerprint(symbols, properties);
+ Rounder rounder = getRounder(properties);
+ CompactDecimalData data = getData(symbols, fingerprint);
+ PositiveNegativeModifier defaultMod = getDefaultMod(symbols, fingerprint);
+ apply(input, mods, rules, rounder, data, defaultMod);
+ }
+
+ private static void apply(
+ FormatQuantity input,
+ ModifierHolder mods,
+ PluralRules rules,
+ Rounder rounder,
+ CompactDecimalData data,
+ PositiveNegativeModifier defaultMod) {
+
+ // Treat zero as if it had magnitude 0
+ int magnitude;
+ if (input.isZero()) {
+ magnitude = 0;
+ rounder.apply(input);
+ } else {
+ int multiplier = rounder.chooseMultiplierAndApply(input, data);
+ magnitude = input.getMagnitude() - multiplier;
+ }
+
+ StandardPlural plural = input.getStandardPlural(rules);
+ boolean isNegative = input.isNegative();
+ Modifier mod = data.getModifier(magnitude, plural, isNegative);
+ if (mod == null) {
+ // Use the default (non-compact) modifier.
+ mod = defaultMod.getModifier(isNegative);
+ }
+ mods.add(mod);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ properties.setCompactStyle(style);
+ rounder.export(properties);
+ }
+
+ static class CompactDecimalData implements Rounder.MultiplierGenerator {
+
+ // A dummy object used when a "0" compact decimal entry is encountered. This is necessary
+ // in order to prevent falling back to root.
+ private static final Modifier USE_FALLBACK = new ConstantAffixModifier();
+
+ final Modifier[] mods;
+ final byte[] multipliers;
+ boolean isEmpty;
+ int largestMagnitude;
+
+ CompactDecimalData() {
+ mods = new Modifier[(MAX_DIGITS + 1) * StandardPlural.COUNT * 2];
+ multipliers = new byte[MAX_DIGITS + 1];
+ isEmpty = true;
+ largestMagnitude = -1;
+ }
+
+ boolean isEmpty() {
+ return isEmpty;
+ }
+
+ @Override
+ public int getMultiplier(int magnitude) {
+ if (magnitude < 0) {
+ return 0;
+ }
+ if (magnitude > largestMagnitude) {
+ magnitude = largestMagnitude;
+ }
+ return multipliers[magnitude];
+ }
+
+ int setOrGetMultiplier(int magnitude, byte multiplier) {
+ if (multipliers[magnitude] != 0) {
+ return multipliers[magnitude];
+ }
+ multipliers[magnitude] = multiplier;
+ isEmpty = false;
+ if (magnitude > largestMagnitude) largestMagnitude = magnitude;
+ return multiplier;
+ }
+
+ Modifier getModifier(int magnitude, StandardPlural plural, boolean isNegative) {
+ if (magnitude < 0) {
+ return null;
+ }
+ if (magnitude > largestMagnitude) {
+ magnitude = largestMagnitude;
+ }
+ Modifier mod = mods[modIndex(magnitude, plural, isNegative)];
+ if (mod == null && plural != StandardPlural.OTHER) {
+ // Fall back to "other" plural variant
+ mod = mods[modIndex(magnitude, StandardPlural.OTHER, isNegative)];
+ }
+ if (mod == USE_FALLBACK) {
+ // Return null if USE_FALLBACK is present
+ mod = null;
+ }
+ return mod;
+ }
+
+ public boolean has(int magnitude, StandardPlural plural) {
+ // Return true if USE_FALLBACK is present
+ return mods[modIndex(magnitude, plural, false)] != null;
+ }
+
+ void setModifiers(Modifier positive, Modifier negative, int magnitude, StandardPlural plural) {
+ mods[modIndex(magnitude, plural, false)] = positive;
+ mods[modIndex(magnitude, plural, true)] = negative;
+ isEmpty = false;
+ if (magnitude > largestMagnitude) largestMagnitude = magnitude;
+ }
+
+ void setNoFallback(int magnitude, StandardPlural plural) {
+ setModifiers(USE_FALLBACK, USE_FALLBACK, magnitude, plural);
+ }
+
+ private static final int modIndex(int magnitude, StandardPlural plural, boolean isNegative) {
+ return magnitude * StandardPlural.COUNT * 2 + plural.ordinal() * 2 + (isNegative ? 1 : 0);
+ }
+ }
+
+ // Should this be public or internal?
+ static enum CompactType {
+ DECIMAL,
+ CURRENCY
+ }
+
+ static class CompactDecimalFingerprint {
+ // TODO: Add more stuff to the fingerprint, like the symbols used by PNAffixGenerator
+ final CompactStyle compactStyle;
+ final CompactType compactType;
+ final ULocale uloc;
+ final String currencySymbol;
+
+ CompactDecimalFingerprint(DecimalFormatSymbols symbols, IProperties properties) {
+ // CompactDecimalFormat does not need to worry about the same constraints as non-compact
+ // currency formatting needs to consider, like the currency rounding mode and the currency
+ // long names with plural forms.
+ if (properties.getCurrency() != CurrencyFormat.ICurrencyProperties.DEFAULT_CURRENCY) {
+ compactType = CompactType.CURRENCY;
+ currencySymbol = CurrencyFormat.getCurrencySymbol(symbols, properties);
+ } else {
+ compactType = CompactType.DECIMAL;
+ currencySymbol = ""; // fallback; should remain unused
+ }
+ compactStyle = properties.getCompactStyle();
+ uloc = symbols.getULocale();
+ }
+
+ @Override
+ public boolean equals(Object _other) {
+ if (_other == null) return false;
+ CompactDecimalFingerprint other = (CompactDecimalFingerprint) _other;
+ if (this == other) return true;
+ if (compactStyle != other.compactStyle) return false;
+ if (compactType != other.compactType) return false;
+ if (currencySymbol != other.currencySymbol) {
+ // String comparison with null handling
+ if (currencySymbol == null || other.currencySymbol == null) return false;
+ if (!currencySymbol.equals(other.currencySymbol)) return false;
+ }
+ if (!uloc.equals(other.uloc)) return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = 0;
+ if (compactStyle != null) hashCode ^= compactStyle.hashCode();
+ if (compactType != null) hashCode ^= compactType.hashCode();
+ if (uloc != null) hashCode ^= uloc.hashCode();
+ if (currencySymbol != null) hashCode ^= currencySymbol.hashCode();
+ return hashCode;
+ }
+ }
+
+ private static final class CompactDecimalDataSink extends UResource.Sink {
+
+ CompactDecimalData data;
+ DecimalFormatSymbols symbols;
+ CompactStyle compactStyle;
+ CompactType compactType;
+ String currencySymbol;
+ PNAffixGenerator pnag;
+ IllegalArgumentException exception;
+
+ /*
+ * NumberElements{ <-- top (numbering system table)
+ * latn{ <-- patternsTable (one per numbering system)
+ * patternsLong{ <-- formatsTable (one per pattern)
+ * decimalFormat{ <-- powersOfTenTable (one per format)
+ * 1000{ <-- pluralVariantsTable (one per power of ten)
+ * one{"0 thousand"} <-- plural variant and template
+ */
+
+ public CompactDecimalDataSink(
+ CompactDecimalData data,
+ DecimalFormatSymbols symbols,
+ CompactDecimalFingerprint fingerprint) {
+ this.data = data;
+ this.symbols = symbols;
+ compactType = fingerprint.compactType;
+ currencySymbol = fingerprint.currencySymbol;
+ compactStyle = fingerprint.compactStyle;
+ pnag = PNAffixGenerator.getThreadLocalInstance();
+ }
+
+ @Override
+ public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
+ UResource.Table patternsTable = value.getTable();
+ for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) {
+ if (key.contentEquals("patternsShort") && compactStyle == CompactStyle.SHORT) {
+ } else if (key.contentEquals("patternsLong") && compactStyle == CompactStyle.LONG) {
+ } else {
+ continue;
+ }
+
+ // traverse into the table of formats
+ UResource.Table formatsTable = value.getTable();
+ for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) {
+ if (key.contentEquals("decimalFormat") && compactType == CompactType.DECIMAL) {
+ } else if (key.contentEquals("currencyFormat") && compactType == CompactType.CURRENCY) {
+ } else {
+ continue;
+ }
+
+ // traverse into the table of powers of ten
+ UResource.Table powersOfTenTable = value.getTable();
+ for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) {
+ try {
+
+ // Assumes that the keys are always of the form "10000" where the magnitude is the
+ // length of the key minus one
+ byte magnitude = (byte) (key.length() - 1);
+
+ // Silently ignore divisors that are too big.
+ if (magnitude >= MAX_DIGITS) continue;
+
+ // Iterate over the plural variants ("one", "other", etc)
+ UResource.Table pluralVariantsTable = value.getTable();
+ for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
+
+ // Skip this magnitude/plural if we already have it from a child locale.
+ StandardPlural plural = StandardPlural.fromString(key.toString());
+ if (data.has(magnitude, plural)) {
+ continue;
+ }
+
+ // The value "0" means that we need to use the default pattern and not fall back
+ // to parent locales. Example locale where this is relevant: 'it'.
+ String patternString = value.toString();
+ if (patternString.equals("0")) {
+ data.setNoFallback(magnitude, plural);
+ continue;
+ }
+
+ // The magnitude multiplier is the difference between the magnitude and the number
+ // of zeros in the pattern, getMinimumIntegerDigits.
+ Properties properties = PatternString.parseToProperties(patternString);
+ byte _multiplier = (byte) -(magnitude - properties.getMinimumIntegerDigits() + 1);
+ if (_multiplier != data.setOrGetMultiplier(magnitude, _multiplier)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Different number of zeros for same power of ten in compact decimal format data for locale '%s', style '%s', type '%s'",
+ symbols.getULocale().toString(),
+ compactStyle.toString(),
+ compactType.toString()));
+ }
+
+ PNAffixGenerator.Result result =
+ pnag.getModifiers(symbols, currencySymbol, properties);
+ data.setModifiers(result.positive, result.negative, magnitude, plural);
+ }
+
+ } catch (IllegalArgumentException e) {
+ exception = e;
+ continue;
+ }
+ }
+
+ // We want only one table of compact decimal formats, so if we get here, stop consuming.
+ // The data.isEmpty() check will prevent further bundles from being traversed.
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Uses data from the custom powersToPluralsToPatterns map instead of an ICUResourceBundle to
+ * populate an instance of CompactDecimalData.
+ */
+ static CompactDecimalData createDataFromCustom(
+ DecimalFormatSymbols symbols,
+ CompactDecimalFingerprint fingerprint,
+ Map<String, Map<String, String>> powersToPluralsToPatterns) {
+ CompactDecimalData data = new CompactDecimalData();
+ PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
+ for (Map.Entry<String, Map<String, String>> magnitudeEntry :
+ powersToPluralsToPatterns.entrySet()) {
+ byte magnitude = (byte) (magnitudeEntry.getKey().length() - 1);
+ for (Map.Entry<String, String> pluralEntry : magnitudeEntry.getValue().entrySet()) {
+ StandardPlural plural = StandardPlural.fromString(pluralEntry.getKey().toString());
+ String patternString = pluralEntry.getValue().toString();
+ Properties properties = PatternString.parseToProperties(patternString);
+ byte _multiplier = (byte) -(magnitude - properties.getMinimumIntegerDigits() + 1);
+ if (_multiplier != data.setOrGetMultiplier(magnitude, _multiplier)) {
+ throw new IllegalArgumentException(
+ "Different number of zeros for same power of ten in custom compact decimal format data");
+ }
+ PNAffixGenerator.Result result =
+ pnag.getModifiers(symbols, fingerprint.currencySymbol, properties);
+ data.setModifiers(result.positive, result.negative, magnitude, plural);
+ }
+ }
+ return data;
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/formatters/CurrencyFormat.java b/android_icu4j/src/main/java/android/icu/impl/number/formatters/CurrencyFormat.java
new file mode 100644
index 000000000..7b219f34c
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/formatters/CurrencyFormat.java
@@ -0,0 +1,315 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.formatters;
+
+import java.math.BigDecimal;
+
+import android.icu.impl.StandardPlural;
+import android.icu.impl.number.AffixPatternUtils;
+import android.icu.impl.number.PNAffixGenerator;
+import android.icu.impl.number.PatternString;
+import android.icu.impl.number.Properties;
+import android.icu.impl.number.Rounder;
+import android.icu.impl.number.modifiers.GeneralPluralModifier;
+import android.icu.impl.number.rounders.IncrementRounder;
+import android.icu.impl.number.rounders.MagnitudeRounder;
+import android.icu.impl.number.rounders.SignificantDigitsRounder;
+import android.icu.text.CurrencyPluralInfo;
+import android.icu.text.DecimalFormatSymbols;
+import android.icu.util.Currency;
+import android.icu.util.Currency.CurrencyUsage;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class CurrencyFormat {
+
+ public enum CurrencyStyle {
+ SYMBOL,
+ ISO_CODE;
+ }
+
+ public static interface ICurrencyProperties {
+ static Currency DEFAULT_CURRENCY = null;
+
+ /** @see #setCurrency */
+ public Currency getCurrency();
+
+ /**
+ * Use the specified currency to substitute currency placeholders ('¤') in the pattern string.
+ *
+ * @param currency The currency.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setCurrency(Currency currency);
+
+ static CurrencyStyle DEFAULT_CURRENCY_STYLE = null;
+
+ /** @see #setCurrencyStyle */
+ public CurrencyStyle getCurrencyStyle();
+
+ /**
+ * Use the specified {@link CurrencyStyle} to replace currency placeholders ('¤').
+ * CurrencyStyle.SYMBOL will use the short currency symbol, like "$" or "€", whereas
+ * CurrencyStyle.ISO_CODE will use the ISO 4217 currency code, like "USD" or "EUR".
+ *
+ * <p>For long currency names, use {@link MeasureFormat.IProperties#setMeasureUnit}.
+ *
+ * @param currencyStyle The currency style. Defaults to CurrencyStyle.SYMBOL.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setCurrencyStyle(CurrencyStyle currencyStyle);
+
+ /**
+ * An old enum that specifies how currencies should be rounded. It contains a subset of the
+ * functionality supported by RoundingInterval.
+ */
+ static Currency.CurrencyUsage DEFAULT_CURRENCY_USAGE = null;
+
+ /** @see #setCurrencyUsage */
+ public Currency.CurrencyUsage getCurrencyUsage();
+
+ /**
+ * Use the specified {@link CurrencyUsage} instance, which provides default rounding rules for
+ * the currency in two styles, CurrencyUsage.CASH and CurrencyUsage.STANDARD.
+ *
+ * <p>The CurrencyUsage specified here will not be used unless there is a currency placeholder
+ * in the pattern.
+ *
+ * @param currencyUsage The currency usage. Defaults to CurrencyUsage.STANDARD.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setCurrencyUsage(Currency.CurrencyUsage currencyUsage);
+
+ static CurrencyPluralInfo DEFAULT_CURRENCY_PLURAL_INFO = null;
+
+ /** @see #setCurrencyPluralInfo */
+ @Deprecated
+ public CurrencyPluralInfo getCurrencyPluralInfo();
+
+ /**
+ * Use the specified {@link CurrencyPluralInfo} instance when formatting currency long names.
+ *
+ * @param currencyPluralInfo The currency plural info object.
+ * @return The property bag, for chaining.
+ * @deprecated Use {@link MeasureFormat.IProperties#setMeasureUnit} with a Currency instead.
+ */
+ @Deprecated
+ public IProperties setCurrencyPluralInfo(CurrencyPluralInfo currencyPluralInfo);
+ }
+
+ public static interface IProperties
+ extends ICurrencyProperties,
+ RoundingFormat.IProperties,
+ PositiveNegativeAffixFormat.IProperties {}
+
+ /**
+ * Returns true if the currency is set in The property bag or if currency symbols are present in
+ * the prefix/suffix pattern.
+ */
+ public static boolean useCurrency(IProperties properties) {
+ return ((properties.getCurrency() != null)
+ || properties.getCurrencyPluralInfo() != null
+ || properties.getCurrencyUsage() != null
+ || AffixPatternUtils.hasCurrencySymbols(properties.getPositivePrefixPattern())
+ || AffixPatternUtils.hasCurrencySymbols(properties.getPositiveSuffixPattern())
+ || AffixPatternUtils.hasCurrencySymbols(properties.getNegativePrefixPattern())
+ || AffixPatternUtils.hasCurrencySymbols(properties.getNegativeSuffixPattern()));
+ }
+
+ /**
+ * Returns the effective currency symbol based on the input. If {@link
+ * ICurrencyProperties#setCurrencyStyle} was set to {@link CurrencyStyle#ISO_CODE}, the ISO Code
+ * will be returned; otherwise, the currency symbol, like "$", will be returned.
+ *
+ * @param symbols The current {@link DecimalFormatSymbols} instance
+ * @param properties The current property bag
+ * @return The currency symbol string, e.g., to substitute '¤' in a decimal pattern string.
+ */
+ public static String getCurrencySymbol(
+ DecimalFormatSymbols symbols, ICurrencyProperties properties) {
+ // If the user asked for ISO Code, return the ISO Code instead of the symbol
+ CurrencyStyle style = properties.getCurrencyStyle();
+ if (style == CurrencyStyle.ISO_CODE) {
+ return getCurrencyIsoCode(symbols, properties);
+ }
+
+ // Get the currency symbol
+ Currency currency = properties.getCurrency();
+ if (currency == null) {
+ return symbols.getCurrencySymbol();
+ } else if (currency.equals(symbols.getCurrency())) {
+ // The user may have set a custom currency symbol in DecimalFormatSymbols.
+ return symbols.getCurrencySymbol();
+ } else {
+ // Use the canonical symbol.
+ return currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
+ }
+ }
+
+ /**
+ * Returns the currency ISO code based on the input, like "USD".
+ *
+ * @param symbols The current {@link DecimalFormatSymbols} instance
+ * @param properties The current property bag
+ * @return The currency ISO code string, e.g., to substitute '¤¤' in a decimal pattern string.
+ */
+ public static String getCurrencyIsoCode(
+ DecimalFormatSymbols symbols, ICurrencyProperties properties) {
+ Currency currency = properties.getCurrency();
+ if (currency == null) {
+ // If a currency object was not provided, use the string from symbols
+ // Note: symbols.getCurrency().getCurrencyCode() won't work here because
+ // DecimalFormatSymbols#setInternationalCurrencySymbol() does not update the
+ // immutable internal currency instance.
+ return symbols.getInternationalCurrencySymbol();
+ } else if (currency.equals(symbols.getCurrency())) {
+ // The user may have set a custom currency symbol in DecimalFormatSymbols.
+ return symbols.getInternationalCurrencySymbol();
+ } else {
+ // Use the canonical currency code.
+ return currency.getCurrencyCode();
+ }
+ }
+
+ /**
+ * Returns the currency long name on the input, like "US dollars".
+ *
+ * @param symbols The current {@link DecimalFormatSymbols} instance
+ * @param properties The current property bag
+ * @param plural The plural form
+ * @return The currency long name string, e.g., to substitute '¤¤¤' in a decimal pattern string.
+ */
+ public static String getCurrencyLongName(
+ DecimalFormatSymbols symbols, ICurrencyProperties properties, StandardPlural plural) {
+ // Attempt to get a currency object first from properties then from symbols
+ Currency currency = properties.getCurrency();
+ if (currency == null) {
+ currency = symbols.getCurrency();
+ }
+
+ // If no currency object is available, fall back to the currency symbol
+ if (currency == null) {
+ return getCurrencySymbol(symbols, properties);
+ }
+
+ // Get the long name
+ return currency.getName(
+ symbols.getULocale(), Currency.PLURAL_LONG_NAME, plural.getKeyword(), null);
+ }
+
+ public static GeneralPluralModifier getCurrencyModifier(
+ DecimalFormatSymbols symbols, IProperties properties) {
+
+ PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
+ String sym = getCurrencySymbol(symbols, properties);
+ String iso = getCurrencyIsoCode(symbols, properties);
+
+ // Previously, the user was also able to specify '¤¤' and '¤¤¤' directly into the prefix or
+ // suffix, which is how the user specified whether they wanted the ISO code or long name.
+ // For backwards compatibility support, that feature is implemented here.
+
+ CurrencyPluralInfo info = properties.getCurrencyPluralInfo();
+ GeneralPluralModifier mod = new GeneralPluralModifier();
+ Properties temp = new Properties();
+ for (StandardPlural plural : StandardPlural.VALUES) {
+ String longName = getCurrencyLongName(symbols, properties, plural);
+
+ PNAffixGenerator.Result result;
+ if (info == null) {
+ // CurrencyPluralInfo is not available.
+ result = pnag.getModifiers(symbols, sym, iso, longName, properties);
+ } else {
+ // CurrencyPluralInfo is available. Use it to generate affixes for long name support.
+ String pluralPattern = info.getCurrencyPluralPattern(plural.getKeyword());
+ PatternString.parseToExistingProperties(
+ pluralPattern, temp, PatternString.IGNORE_ROUNDING_ALWAYS);
+ result = pnag.getModifiers(symbols, sym, iso, longName, temp);
+ }
+ mod.put(plural, result.positive, result.negative);
+ }
+ return mod;
+ }
+
+ private static final Currency DEFAULT_CURRENCY = Currency.getInstance("XXX");
+
+ public static void populateCurrencyRounderProperties(
+ Properties destination, DecimalFormatSymbols symbols, IProperties properties) {
+
+ Currency currency = properties.getCurrency();
+ if (currency == null) {
+ // Fall back to the DecimalFormatSymbols currency instance.
+ currency = symbols.getCurrency();
+ }
+ if (currency == null) {
+ // There is a currency symbol in the pattern, but we have no currency available to use.
+ // Use the default currency instead so that we can still apply currency usage rules.
+ currency = DEFAULT_CURRENCY;
+ }
+
+ CurrencyUsage _currencyUsage = properties.getCurrencyUsage();
+ int _minFrac = properties.getMinimumFractionDigits();
+ int _maxFrac = properties.getMaximumFractionDigits();
+
+ CurrencyUsage effectiveCurrencyUsage =
+ (_currencyUsage != null) ? _currencyUsage : CurrencyUsage.STANDARD;
+ double incrementDouble = currency.getRoundingIncrement(effectiveCurrencyUsage);
+ int fractionDigits = currency.getDefaultFractionDigits(effectiveCurrencyUsage);
+
+ destination.setRoundingMode(properties.getRoundingMode());
+ destination.setMinimumIntegerDigits(properties.getMinimumIntegerDigits());
+ destination.setMaximumIntegerDigits(properties.getMaximumIntegerDigits());
+
+ if (_currencyUsage == null && (_minFrac >= 0 || _maxFrac >= 0)) {
+ // User override of fraction length
+ if (_minFrac < 0) {
+ destination.setMinimumFractionDigits(fractionDigits < _maxFrac ? fractionDigits : _maxFrac);
+ destination.setMaximumFractionDigits(_maxFrac);
+ } else if (_maxFrac < 0) {
+ destination.setMinimumFractionDigits(_minFrac);
+ destination.setMaximumFractionDigits(fractionDigits > _minFrac ? fractionDigits : _minFrac);
+ } else {
+ destination.setMinimumFractionDigits(_minFrac);
+ destination.setMaximumFractionDigits(_maxFrac);
+ }
+ } else {
+ // Currency rounding
+ destination.setMinimumFractionDigits(fractionDigits);
+ destination.setMaximumFractionDigits(fractionDigits);
+ }
+
+ if (incrementDouble > 0.0) {
+ BigDecimal incrementBigDecimal;
+ BigDecimal _roundingIncrement = properties.getRoundingIncrement();
+ if (_roundingIncrement != null) {
+ incrementBigDecimal = _roundingIncrement;
+ } else {
+ incrementBigDecimal = BigDecimal.valueOf(incrementDouble);
+ }
+ destination.setRoundingIncrement(incrementBigDecimal);
+ } else {
+ }
+ }
+
+ private static final ThreadLocal<Properties> threadLocalProperties =
+ new ThreadLocal<Properties>() {
+ @Override
+ protected Properties initialValue() {
+ return new Properties();
+ }
+ };
+
+ public static Rounder getCurrencyRounder(DecimalFormatSymbols symbols, IProperties properties) {
+ if (SignificantDigitsRounder.useSignificantDigits(properties)) {
+ return SignificantDigitsRounder.getInstance(properties);
+ }
+ Properties cprops = threadLocalProperties.get().clear();
+ populateCurrencyRounderProperties(cprops, symbols, properties);
+ if (cprops.getRoundingIncrement() != null) {
+ return IncrementRounder.getInstance(cprops);
+ } else {
+ return MagnitudeRounder.getInstance(cprops);
+ }
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/formatters/MagnitudeMultiplier.java b/android_icu4j/src/main/java/android/icu/impl/number/formatters/MagnitudeMultiplier.java
new file mode 100644
index 000000000..553c4ad75
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/formatters/MagnitudeMultiplier.java
@@ -0,0 +1,63 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.formatters;
+
+import android.icu.impl.number.Format;
+import android.icu.impl.number.Format.BeforeFormat;
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.ModifierHolder;
+import android.icu.impl.number.Properties;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class MagnitudeMultiplier extends Format.BeforeFormat {
+ private static final MagnitudeMultiplier DEFAULT = new MagnitudeMultiplier(0);
+
+ public static interface IProperties {
+
+ static int DEFAULT_MAGNITUDE_MULTIPLIER = 0;
+
+ /** @see #setMagnitudeMultiplier */
+ public int getMagnitudeMultiplier();
+
+ /**
+ * Multiply all numbers by this power of ten before formatting. Negative multipliers reduce the
+ * magnitude and make numbers smaller (closer to zero).
+ *
+ * @param magnitudeMultiplier The number of powers of ten to scale.
+ * @return The property bag, for chaining.
+ * @see BigDecimalMultiplier
+ */
+ public IProperties setMagnitudeMultiplier(int magnitudeMultiplier);
+ }
+
+ public static boolean useMagnitudeMultiplier(IProperties properties) {
+ return properties.getMagnitudeMultiplier() != IProperties.DEFAULT_MAGNITUDE_MULTIPLIER;
+ }
+
+ // Properties
+ final int delta;
+
+ public static BeforeFormat getInstance(Properties properties) {
+ if (properties.getMagnitudeMultiplier() == 0) {
+ return DEFAULT;
+ }
+ return new MagnitudeMultiplier(properties.getMagnitudeMultiplier());
+ }
+
+ private MagnitudeMultiplier(int delta) {
+ this.delta = delta;
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+ input.adjustMagnitude(delta);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ properties.setMagnitudeMultiplier(delta);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/formatters/MeasureFormat.java b/android_icu4j/src/main/java/android/icu/impl/number/formatters/MeasureFormat.java
new file mode 100644
index 000000000..39eadf59f
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/formatters/MeasureFormat.java
@@ -0,0 +1,77 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.formatters;
+
+import android.icu.impl.StandardPlural;
+import android.icu.impl.number.modifiers.GeneralPluralModifier;
+import android.icu.impl.number.modifiers.SimpleModifier;
+import android.icu.text.DecimalFormatSymbols;
+import android.icu.text.MeasureFormat.FormatWidth;
+import android.icu.util.MeasureUnit;
+import android.icu.util.ULocale;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class MeasureFormat {
+
+ public static interface IProperties {
+
+ static MeasureUnit DEFAULT_MEASURE_UNIT = null;
+
+ /** @see #setMeasureUnit */
+ public MeasureUnit getMeasureUnit();
+
+ /**
+ * Apply prefixes and suffixes for the specified {@link MeasureUnit} to the formatted number.
+ *
+ * @param measureUnit The measure unit.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setMeasureUnit(MeasureUnit measureUnit);
+
+ static FormatWidth DEFAULT_MEASURE_FORMAT_WIDTH = null;
+
+ /** @see #setMeasureFormatWidth */
+ public FormatWidth getMeasureFormatWidth();
+
+ /**
+ * Use the specified {@link FormatWidth} when choosing the style of measure unit prefix/suffix.
+ *
+ * <p>Must be used in conjunction with {@link #setMeasureUnit}.
+ *
+ * @param measureFormatWidth The width style. Defaults to FormatWidth.WIDE.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setMeasureFormatWidth(FormatWidth measureFormatWidth);
+ }
+
+ public static boolean useMeasureFormat(IProperties properties) {
+ return properties.getMeasureUnit() != IProperties.DEFAULT_MEASURE_UNIT;
+ }
+
+ public static GeneralPluralModifier getInstance(DecimalFormatSymbols symbols, IProperties properties) {
+ ULocale uloc = symbols.getULocale();
+ MeasureUnit unit = properties.getMeasureUnit();
+ FormatWidth width = properties.getMeasureFormatWidth();
+
+ if (unit == null) {
+ throw new IllegalArgumentException("A measure unit is required for MeasureFormat");
+ }
+ if (width == null) {
+ width = FormatWidth.WIDE;
+ }
+
+ // Temporarily, create a MeasureFormat instance for its data loading capability
+ // TODO: Move data loading directly into this class file
+ android.icu.text.MeasureFormat mf = android.icu.text.MeasureFormat.getInstance(uloc, width);
+ GeneralPluralModifier mod = new GeneralPluralModifier();
+ for (StandardPlural plural : StandardPlural.VALUES) {
+ String formatString = null;
+ mf.getPluralFormatter(unit, width, plural.ordinal());
+ mod.put(plural, new SimpleModifier(formatString, null, false));
+ }
+ return mod;
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/formatters/PaddingFormat.java b/android_icu4j/src/main/java/android/icu/impl/number/formatters/PaddingFormat.java
new file mode 100644
index 000000000..6d047555f
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/formatters/PaddingFormat.java
@@ -0,0 +1,177 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.formatters;
+
+import android.icu.impl.number.Format.AfterFormat;
+import android.icu.impl.number.ModifierHolder;
+import android.icu.impl.number.NumberStringBuilder;
+import android.icu.impl.number.Properties;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class PaddingFormat implements AfterFormat {
+ public enum PadPosition {
+ BEFORE_PREFIX,
+ AFTER_PREFIX,
+ BEFORE_SUFFIX,
+ AFTER_SUFFIX;
+
+ public static PadPosition fromOld(int old) {
+ switch (old) {
+ case android.icu.text.DecimalFormat.PAD_BEFORE_PREFIX:
+ return PadPosition.BEFORE_PREFIX;
+ case android.icu.text.DecimalFormat.PAD_AFTER_PREFIX:
+ return PadPosition.AFTER_PREFIX;
+ case android.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX:
+ return PadPosition.BEFORE_SUFFIX;
+ case android.icu.text.DecimalFormat.PAD_AFTER_SUFFIX:
+ return PadPosition.AFTER_SUFFIX;
+ default:
+ throw new IllegalArgumentException("Don't know how to map " + old);
+ }
+ }
+
+ public int toOld() {
+ switch (this) {
+ case BEFORE_PREFIX:
+ return android.icu.text.DecimalFormat.PAD_BEFORE_PREFIX;
+ case AFTER_PREFIX:
+ return android.icu.text.DecimalFormat.PAD_AFTER_PREFIX;
+ case BEFORE_SUFFIX:
+ return android.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX;
+ case AFTER_SUFFIX:
+ return android.icu.text.DecimalFormat.PAD_AFTER_SUFFIX;
+ default:
+ return -1; // silence compiler errors
+ }
+ }
+ }
+
+ public static interface IProperties {
+
+ static int DEFAULT_FORMAT_WIDTH = 0;
+
+ /** @see #setFormatWidth */
+ public int getFormatWidth();
+
+ /**
+ * Sets the minimum width of the string output by the formatting pipeline. For example, if
+ * padding is enabled and paddingWidth is set to 6, formatting the number "3.14159" with the
+ * pattern "0.00" will result in "··3.14" if '·' is your padding string.
+ *
+ * <p>If the number is longer than your padding width, the number will display as if no padding
+ * width had been specified, which may result in strings longer than the padding width.
+ *
+ * <p>Width is counted in UTF-16 code units.
+ *
+ * @param formatWidth The output width.
+ * @return The property bag, for chaining.
+ * @see #setPadPosition
+ * @see #setPadString
+ */
+ public IProperties setFormatWidth(int formatWidth);
+
+ static String DEFAULT_PAD_STRING = null;
+
+ /** @see #setPadString */
+ public String getPadString();
+
+ /**
+ * Sets the string used for padding. The string should contain a single character or grapheme
+ * cluster.
+ *
+ * <p>Must be used in conjunction with {@link #setFormatWidth}.
+ *
+ * @param paddingString The padding string. Defaults to an ASCII space (U+0020).
+ * @return The property bag, for chaining.
+ * @see #setFormatWidth
+ */
+ public IProperties setPadString(String paddingString);
+
+ static PadPosition DEFAULT_PAD_POSITION = null;
+
+ /** @see #setPadPosition */
+ public PadPosition getPadPosition();
+
+ /**
+ * Sets the location where the padding string is to be inserted to maintain the padding width:
+ * one of BEFORE_PREFIX, AFTER_PREFIX, BEFORE_SUFFIX, or AFTER_SUFFIX.
+ *
+ * <p>Must be used in conjunction with {@link #setFormatWidth}.
+ *
+ * @param padPosition The output width.
+ * @return The property bag, for chaining.
+ * @see #setFormatWidth
+ */
+ public IProperties setPadPosition(PadPosition padPosition);
+ }
+
+ public static final String FALLBACK_PADDING_STRING = "\u0020"; // i.e. a space
+
+ public static boolean usePadding(IProperties properties) {
+ return properties.getFormatWidth() != IProperties.DEFAULT_FORMAT_WIDTH;
+ }
+
+ public static AfterFormat getInstance(IProperties properties) {
+ return new PaddingFormat(
+ properties.getFormatWidth(),
+ properties.getPadString(),
+ properties.getPadPosition());
+ }
+
+ // Properties
+ private final int paddingWidth;
+ private final String paddingString;
+ private final PadPosition paddingLocation;
+
+ private PaddingFormat(
+ int paddingWidth, String paddingString, PadPosition paddingLocation) {
+ this.paddingWidth = paddingWidth > 0 ? paddingWidth : 10; // TODO: Is this a sensible default?
+ this.paddingString = paddingString != null ? paddingString : FALLBACK_PADDING_STRING;
+ this.paddingLocation =
+ paddingLocation != null ? paddingLocation : PadPosition.BEFORE_PREFIX;
+ }
+
+ @Override
+ public int after(ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex) {
+
+ // TODO: Count code points instead of code units?
+ int requiredPadding = paddingWidth - (rightIndex - leftIndex) - mods.totalLength();
+
+ if (requiredPadding <= 0) {
+ // Skip padding, but still apply modifiers to be consistent
+ return mods.applyAll(string, leftIndex, rightIndex);
+ }
+
+ int length = 0;
+ if (paddingLocation == PadPosition.AFTER_PREFIX) {
+ length += addPadding(requiredPadding, string, leftIndex);
+ } else if (paddingLocation == PadPosition.BEFORE_SUFFIX) {
+ length += addPadding(requiredPadding, string, rightIndex);
+ }
+ length += mods.applyAll(string, leftIndex, rightIndex + length);
+ if (paddingLocation == PadPosition.BEFORE_PREFIX) {
+ length += addPadding(requiredPadding, string, leftIndex);
+ } else if (paddingLocation == PadPosition.AFTER_SUFFIX) {
+ length += addPadding(requiredPadding, string, rightIndex + length);
+ }
+
+ return length;
+ }
+
+ private int addPadding(int requiredPadding, NumberStringBuilder string, int index) {
+ for (int i = 0; i < requiredPadding; i++) {
+ string.insert(index, paddingString, null);
+ }
+ return paddingString.length() * requiredPadding;
+ }
+
+ @Override
+ public void export(Properties properties) {
+ properties.setFormatWidth(paddingWidth);
+ properties.setPadString(paddingString);
+ properties.setPadPosition(paddingLocation);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/formatters/PositiveDecimalFormat.java b/android_icu4j/src/main/java/android/icu/impl/number/formatters/PositiveDecimalFormat.java
new file mode 100644
index 000000000..1a458efb1
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/formatters/PositiveDecimalFormat.java
@@ -0,0 +1,238 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.formatters;
+
+import android.icu.impl.number.Format;
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.NumberStringBuilder;
+import android.icu.impl.number.Properties;
+import android.icu.text.DecimalFormatSymbols;
+import android.icu.text.NumberFormat;
+import android.icu.text.NumberFormat.Field;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class PositiveDecimalFormat implements Format.TargetFormat {
+
+ public static interface IProperties extends CurrencyFormat.IProperties {
+
+ static int DEFAULT_GROUPING_SIZE = -1;
+
+ /** @see #setGroupingSize */
+ public int getGroupingSize();
+
+ /**
+ * Sets the number of digits between grouping separators. For example, the <em>en-US</em> locale
+ * uses a grouping size of 3, so the number 1234567 would be formatted as "1,234,567". For
+ * locales whose grouping sizes vary with magnitude, see {@link #setSecondaryGroupingSize(int)}.
+ *
+ * @param groupingSize The primary grouping size.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setGroupingSize(int groupingSize);
+
+ static int DEFAULT_SECONDARY_GROUPING_SIZE = -1;
+
+ /** @see #setSecondaryGroupingSize */
+ public int getSecondaryGroupingSize();
+
+ /**
+ * Sets the number of digits between grouping separators higher than the least-significant
+ * grouping separator. For example, the locale <em>hi</em> uses a primary grouping size of 3 and
+ * a secondary grouping size of 2, so the number 1234567 would be formatted as "12,34,567".
+ *
+ * <p>The two levels of grouping separators can be specified in the pattern string. For example,
+ * the <em>hi</em> locale's default decimal format pattern is "#,##,##0.###".
+ *
+ * @param secondaryGroupingSize The secondary grouping size.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setSecondaryGroupingSize(int secondaryGroupingSize);
+
+ static boolean DEFAULT_DECIMAL_SEPARATOR_ALWAYS_SHOWN = false;
+
+ /** @see #setDecimalSeparatorAlwaysShown */
+ public boolean getDecimalSeparatorAlwaysShown();
+
+ /**
+ * Sets whether to always show the decimal point, even if the number doesn't require one. For
+ * example, if always show decimal is true, the number 123 would be formatted as "123." in
+ * locale <em>en-US</em>.
+ *
+ * @param decimalSeparatorAlwaysShown Whether to show the decimal point when it is optional.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setDecimalSeparatorAlwaysShown(boolean decimalSeparatorAlwaysShown);
+
+ static int DEFAULT_MINIMUM_GROUPING_DIGITS = 1;
+
+ /** @see #setMinimumGroupingDigits */
+ public int getMinimumGroupingDigits();
+
+ /**
+ * Sets the minimum number of digits required to be beyond the first grouping separator in order
+ * to enable grouping. For example, if the minimum grouping digits is 2, then 1234 would be
+ * formatted as "1234" but 12345 would be formatted as "12,345" in <em>en-US</em>. Note that
+ * 1234567 would still be formatted as "1,234,567", not "1234,567".
+ *
+ * @param minimumGroupingDigits How many digits must appear before a grouping separator before
+ * enabling grouping.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setMinimumGroupingDigits(int minimumGroupingDigits);
+ }
+
+ public static boolean useGrouping(IProperties properties) {
+ return properties.getGroupingSize() != IProperties.DEFAULT_GROUPING_SIZE
+ || properties.getSecondaryGroupingSize() != IProperties.DEFAULT_SECONDARY_GROUPING_SIZE;
+ }
+
+ public static boolean allowsDecimalPoint(IProperties properties) {
+ return properties.getDecimalSeparatorAlwaysShown()
+ || properties.getMaximumFractionDigits() != 0;
+ }
+
+ // Properties
+ private final boolean alwaysShowDecimal;
+ private final int primaryGroupingSize;
+ private final int secondaryGroupingSize;
+ private final int minimumGroupingDigits;
+
+ // Symbols
+ private final String infinityString;
+ private final String nanString;
+ private final String groupingSeparator;
+ private final String decimalSeparator;
+ private final String[] digitStrings;
+ private final int codePointZero;
+
+ public PositiveDecimalFormat(DecimalFormatSymbols symbols, IProperties properties) {
+ int _primary = properties.getGroupingSize();
+ int _secondary = properties.getSecondaryGroupingSize();
+ primaryGroupingSize = _primary > 0 ? _primary : _secondary > 0 ? _secondary : 0;
+ secondaryGroupingSize = _secondary > 0 ? _secondary : primaryGroupingSize;
+
+ minimumGroupingDigits = properties.getMinimumGroupingDigits();
+ alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown();
+ infinityString = symbols.getInfinity();
+ nanString = symbols.getNaN();
+
+ if (CurrencyFormat.useCurrency(properties)) {
+ groupingSeparator = symbols.getMonetaryGroupingSeparatorString();
+ decimalSeparator = symbols.getMonetaryDecimalSeparatorString();
+ } else {
+ groupingSeparator = symbols.getGroupingSeparatorString();
+ decimalSeparator = symbols.getDecimalSeparatorString();
+ }
+
+ // Check to see if we can use code points instead of strings (~15% format performance boost)
+ int _codePointZero = -1;
+ String[] _digitStrings = symbols.getDigitStringsLocal();
+ for (int i = 0; i < _digitStrings.length; i++) {
+ int cp = Character.codePointAt(_digitStrings[i], 0);
+ int cc = Character.charCount(cp);
+ if (cc != _digitStrings[i].length()) {
+ _codePointZero = -1;
+ break;
+ } else if (i == 0) {
+ _codePointZero = cp;
+ } else if (cp != _codePointZero + i) {
+ _codePointZero = -1;
+ break;
+ }
+ }
+ if (_codePointZero != -1) {
+ digitStrings = null;
+ codePointZero = _codePointZero;
+ } else {
+ digitStrings = symbols.getDigitStrings(); // makes a copy
+ codePointZero = -1;
+ }
+ }
+
+ @Override
+ public int target(FormatQuantity input, NumberStringBuilder string, int startIndex) {
+ int length = 0;
+
+ if (input.isInfinite()) {
+ length += string.insert(startIndex, infinityString, NumberFormat.Field.INTEGER);
+
+ } else if (input.isNaN()) {
+ length += string.insert(startIndex, nanString, NumberFormat.Field.INTEGER);
+
+ } else {
+ // Add the integer digits
+ length += addIntegerDigits(input, string, startIndex);
+
+ // Add the decimal point
+ if (input.getLowerDisplayMagnitude() < 0 || alwaysShowDecimal) {
+ length +=
+ string.insert(
+ startIndex + length, decimalSeparator, NumberFormat.Field.DECIMAL_SEPARATOR);
+ }
+
+ // Add the fraction digits
+ length += addFractionDigits(input, string, startIndex + length);
+ }
+
+ return length;
+ }
+
+ private int addIntegerDigits(FormatQuantity input, NumberStringBuilder string, int startIndex) {
+ int length = 0;
+ int integerCount = input.getUpperDisplayMagnitude() + 1;
+ for (int i = 0; i < integerCount; i++) {
+ // Add grouping separator
+ if (primaryGroupingSize > 0
+ && i == primaryGroupingSize
+ && integerCount - i >= minimumGroupingDigits) {
+ length +=
+ string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR);
+ } else if (secondaryGroupingSize > 0
+ && i > primaryGroupingSize
+ && (i - primaryGroupingSize) % secondaryGroupingSize == 0) {
+ length +=
+ string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR);
+ }
+
+ // Get and append the next digit value
+ byte nextDigit = input.getDigit(i);
+ length += addDigit(nextDigit, string, startIndex, NumberFormat.Field.INTEGER);
+ }
+
+ return length;
+ }
+
+ private int addFractionDigits(FormatQuantity input, NumberStringBuilder string, int index) {
+ int length = 0;
+ int fractionCount = -input.getLowerDisplayMagnitude();
+ for (int i = 0; i < fractionCount; i++) {
+ // Get and append the next digit value
+ byte nextDigit = input.getDigit(-i - 1);
+ length += addDigit(nextDigit, string, index + length, NumberFormat.Field.FRACTION);
+ }
+ return length;
+ }
+
+ private int addDigit(byte digit, NumberStringBuilder outputString, int index, Field field) {
+ if (codePointZero != -1) {
+ return outputString.insertCodePoint(index, codePointZero + digit, field);
+ } else {
+ return outputString.insert(index, digitStrings[digit], field);
+ }
+ }
+
+ @Override
+ public void export(Properties properties) {
+ // For backwards compatibility, export 0 as secondary grouping if primary and secondary are the same
+ int effectiveSecondaryGroupingSize =
+ secondaryGroupingSize == primaryGroupingSize ? 0 : secondaryGroupingSize;
+
+ properties.setDecimalSeparatorAlwaysShown(alwaysShowDecimal);
+ properties.setGroupingSize(primaryGroupingSize);
+ properties.setSecondaryGroupingSize(effectiveSecondaryGroupingSize);
+ properties.setMinimumGroupingDigits(minimumGroupingDigits);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/formatters/PositiveNegativeAffixFormat.java b/android_icu4j/src/main/java/android/icu/impl/number/formatters/PositiveNegativeAffixFormat.java
new file mode 100644
index 000000000..941d890ba
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/formatters/PositiveNegativeAffixFormat.java
@@ -0,0 +1,258 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.formatters;
+
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.ModifierHolder;
+import android.icu.impl.number.PNAffixGenerator;
+import android.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
+import android.icu.text.DecimalFormatSymbols;
+
+/**
+ * The implementation of this class is a thin wrapper around {@link PNAffixGenerator}, a utility
+ * used by this and other classes, including {@link CompactDecimalFormat} and {@link Parse}, to
+ * efficiently convert from the abstract properties in the property bag to actual prefix and suffix
+ * strings.
+ */
+
+/**
+ * This class is responsible for adding the positive/negative prefixes and suffixes from the decimal
+ * format pattern. Properties are set using the following methods:
+ *
+ * <ul>
+ * <li>{@link IProperties#setPositivePrefix(String)}
+ * <li>{@link IProperties#setPositiveSuffix(String)}
+ * <li>{@link IProperties#setNegativePrefix(String)}
+ * <li>{@link IProperties#setNegativeSuffix(String)}
+ * <li>{@link IProperties#setPositivePrefixPattern(String)}
+ * <li>{@link IProperties#setPositiveSuffixPattern(String)}
+ * <li>{@link IProperties#setNegativePrefixPattern(String)}
+ * <li>{@link IProperties#setNegativeSuffixPattern(String)}
+ * </ul>
+ *
+ * If one of the first four methods is used (those of the form <code>setXxxYyy</code>), the value
+ * will be interpreted literally. If one of the second four methods is used (those of the form
+ * <code>setXxxYyyPattern</code>), locale-specific symbols for the plus sign, minus sign, percent
+ * sign, permille sign, and currency sign will be substituted into the string, according to Unicode
+ * Technical Standard #35 (LDML) section 3.2.
+ *
+ * <p>Literal characters can be used in the <code>setXxxYyyPattern</code> methods by using quotes;
+ * for example, to display a literal "%" sign, you can set the pattern <code>'%'</code>. To display
+ * a literal quote, use two quotes in a row, like <code>''</code>.
+ *
+ * <p>If a value is set in both a <code>setXxxYyy</code> method and in the corresponding <code>
+ * setXxxYyyPattern</code> method, the one set in <code>setXxxYyy</code> takes precedence.
+ *
+ * <p>For more information on formatting currencies, see {@link CurrencyFormat}.
+ *
+ * <p>The parameter is taken by reference by these methods into the property bag, meaning that if a
+ * mutable object like StringBuilder is passed, changes to the StringBuilder will be reflected in
+ * the property bag. However, upon creation of a finalized formatter object, all prefixes and
+ * suffixes will be converted to strings and will stop reflecting changes in the property bag.
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class PositiveNegativeAffixFormat {
+
+ public static interface IProperties {
+
+ static String DEFAULT_POSITIVE_PREFIX = null;
+
+ /** @see #setPositivePrefix */
+ public String getPositivePrefix();
+
+ /**
+ * Sets the prefix to prepend to positive numbers. The prefix will be interpreted literally. For
+ * example, if you set a positive prefix of <code>p</code>, then the number 123 will be
+ * formatted as "p123" in the locale <em>en-US</em>.
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param positivePrefix The CharSequence to prepend to positive numbers.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setPositivePrefixPattern
+ */
+ public IProperties setPositivePrefix(String positivePrefix);
+
+ static String DEFAULT_POSITIVE_SUFFIX = null;
+
+ /** @see #setPositiveSuffix */
+ public String getPositiveSuffix();
+
+ /**
+ * Sets the suffix to append to positive numbers. The suffix will be interpreted literally. For
+ * example, if you set a positive suffix of <code>p</code>, then the number 123 will be
+ * formatted as "123p" in the locale <em>en-US</em>.
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param positiveSuffix The CharSequence to append to positive numbers.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setPositiveSuffixPattern
+ */
+ public IProperties setPositiveSuffix(String positiveSuffix);
+
+ static String DEFAULT_NEGATIVE_PREFIX = null;
+
+ /** @see #setNegativePrefix */
+ public String getNegativePrefix();
+
+ /**
+ * Sets the prefix to prepend to negative numbers. The prefix will be interpreted literally. For
+ * example, if you set a negative prefix of <code>n</code>, then the number -123 will be
+ * formatted as "n123" in the locale <em>en-US</em>. Note that if the negative prefix is left unset,
+ * the locale's minus sign is used.
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param negativePrefix The CharSequence to prepend to negative numbers.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setNegativePrefixPattern
+ */
+ public IProperties setNegativePrefix(String negativePrefix);
+
+ static String DEFAULT_NEGATIVE_SUFFIX = null;
+
+ /** @see #setNegativeSuffix */
+ public String getNegativeSuffix();
+
+ /**
+ * Sets the suffix to append to negative numbers. The suffix will be interpreted literally. For
+ * example, if you set a suffix prefix of <code>n</code>, then the number -123 will be formatted
+ * as "-123n" in the locale <em>en-US</em>. Note that the minus sign is prepended by default unless
+ * otherwise specified in either the pattern string or in one of the {@link #setNegativePrefix}
+ * methods.
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param negativeSuffix The CharSequence to append to negative numbers.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setNegativeSuffixPattern
+ */
+ public IProperties setNegativeSuffix(String negativeSuffix);
+
+ static String DEFAULT_POSITIVE_PREFIX_PATTERN = null;
+
+ /** @see #setPositivePrefixPattern */
+ public String getPositivePrefixPattern();
+
+ /**
+ * Sets the prefix to prepend to positive numbers. Locale-specific symbols will be substituted
+ * into the string according to Unicode Technical Standard #35 (LDML).
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param positivePrefixPattern The CharSequence to prepend to positive numbers after locale
+ * symbol substitutions take place.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setPositivePrefix
+ */
+ public IProperties setPositivePrefixPattern(String positivePrefixPattern);
+
+ static String DEFAULT_POSITIVE_SUFFIX_PATTERN = null;
+
+ /** @see #setPositiveSuffixPattern */
+ public String getPositiveSuffixPattern();
+
+ /**
+ * Sets the suffix to append to positive numbers. Locale-specific symbols will be substituted
+ * into the string according to Unicode Technical Standard #35 (LDML).
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param positiveSuffixPattern The CharSequence to append to positive numbers after locale
+ * symbol substitutions take place.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setPositiveSuffix
+ */
+ public IProperties setPositiveSuffixPattern(String positiveSuffixPattern);
+
+ static String DEFAULT_NEGATIVE_PREFIX_PATTERN = null;
+
+ /** @see #setNegativePrefixPattern */
+ public String getNegativePrefixPattern();
+
+ /**
+ * Sets the prefix to prepend to negative numbers. Locale-specific symbols will be substituted
+ * into the string according to Unicode Technical Standard #35 (LDML).
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param negativePrefixPattern The CharSequence to prepend to negative numbers after locale
+ * symbol substitutions take place.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setNegativePrefix
+ */
+ public IProperties setNegativePrefixPattern(String negativePrefixPattern);
+
+ static String DEFAULT_NEGATIVE_SUFFIX_PATTERN = null;
+
+ /** @see #setNegativeSuffixPattern */
+ public String getNegativeSuffixPattern();
+
+ /**
+ * Sets the suffix to append to negative numbers. Locale-specific symbols will be substituted
+ * into the string according to Unicode Technical Standard #35 (LDML).
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param negativeSuffixPattern The CharSequence to append to negative numbers after locale
+ * symbol substitutions take place.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setNegativeSuffix
+ */
+ public IProperties setNegativeSuffixPattern(String negativeSuffixPattern);
+
+ static boolean DEFAULT_SIGN_ALWAYS_SHOWN = false;
+
+ /** @see #setSignAlwaysShown */
+ public boolean getSignAlwaysShown();
+
+ /**
+ * Sets whether to always display of a plus sign on positive numbers.
+ *
+ * <p>If the location of the negative sign is specified by the decimal format pattern (or by the
+ * negative prefix/suffix pattern methods), a plus sign is substituted into that location, in
+ * accordance with Unicode Technical Standard #35 (LDML) section 3.2.1. Otherwise, the plus sign
+ * is prepended to the number. For example, if the decimal format pattern <code>#;#-</code> is
+ * used, then formatting 123 would result in "123+" in the locale <em>en-US</em>.
+ *
+ * <p>This method should be used <em>instead of</em> setting the positive prefix/suffix. The
+ * behavior is undefined if alwaysShowPlusSign is set but the positive prefix/suffix already
+ * contains a plus sign.
+ *
+ * @param plusSignAlwaysShown Whether positive numbers should display a plus sign.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setSignAlwaysShown(boolean plusSignAlwaysShown);
+ }
+
+ public static PositiveNegativeAffixModifier getInstance(DecimalFormatSymbols symbols, IProperties properties) {
+ PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
+ PNAffixGenerator.Result result = pnag.getModifiers(symbols, properties);
+ return new PositiveNegativeAffixModifier(result.positive, result.negative);
+ }
+
+ // TODO: Investigate static interface methods (Java 8 only?)
+ public static void apply(
+ FormatQuantity input,
+ ModifierHolder mods,
+ DecimalFormatSymbols symbols,
+ IProperties properties) {
+ PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
+ PNAffixGenerator.Result result = pnag.getModifiers(symbols, properties);
+ if (input.isNegative()) {
+ mods.add(result.negative);
+ } else {
+ mods.add(result.positive);
+ }
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/formatters/RangeFormat.java b/android_icu4j/src/main/java/android/icu/impl/number/formatters/RangeFormat.java
new file mode 100644
index 000000000..7606ff922
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/formatters/RangeFormat.java
@@ -0,0 +1,62 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+// THIS CLASS IS A PROOF OF CONCEPT ONLY.
+// IT REQUIRES ADDITIONAL DISCUSION ABOUT ITS DESIGN AND IMPLEMENTATION.
+
+package android.icu.impl.number.formatters;
+
+import java.util.Deque;
+
+import android.icu.impl.number.Format;
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.ModifierHolder;
+import android.icu.impl.number.NumberStringBuilder;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class RangeFormat extends Format {
+ // Primary settings
+ private final String separator;
+
+ // Child formatters
+ private final Format left;
+ private final Format right;
+
+ public RangeFormat(Format left, Format right, String separator) {
+ this.separator = separator; // TODO: This would be loaded from locale data.
+ this.left = left;
+ this.right = right;
+
+ if (left == null || right == null) {
+ throw new IllegalArgumentException("Both child formatters are required for RangeFormat");
+ }
+ }
+
+ @Override
+ public int process(
+ Deque<FormatQuantity> inputs,
+ ModifierHolder mods,
+ NumberStringBuilder string,
+ int startIndex) {
+ ModifierHolder lMods = new ModifierHolder();
+ ModifierHolder rMods = new ModifierHolder();
+ int lLen = left.process(inputs, lMods, string, startIndex);
+ int rLen = right.process(inputs, rMods, string, startIndex + lLen);
+
+ // Bubble up any modifiers that are shared between the two sides
+ while (lMods.peekLast() != null && lMods.peekLast() == rMods.peekLast()) {
+ mods.add(lMods.removeLast());
+ rMods.removeLast();
+ }
+
+ // Apply the remaining modifiers
+ lLen += lMods.applyAll(string, startIndex, startIndex + lLen);
+ rLen += rMods.applyAll(string, startIndex + lLen, startIndex + lLen + rLen);
+
+ int sLen = string.insert(startIndex + lLen, separator, null);
+
+ return lLen + sLen + rLen;
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/formatters/RoundingFormat.java b/android_icu4j/src/main/java/android/icu/impl/number/formatters/RoundingFormat.java
new file mode 100644
index 000000000..435103f71
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/formatters/RoundingFormat.java
@@ -0,0 +1,45 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.formatters;
+
+import android.icu.impl.number.Rounder;
+import android.icu.impl.number.Rounder.IBasicRoundingProperties;
+import android.icu.impl.number.rounders.IncrementRounder;
+import android.icu.impl.number.rounders.MagnitudeRounder;
+import android.icu.impl.number.rounders.NoRounder;
+import android.icu.impl.number.rounders.SignificantDigitsRounder;
+
+// TODO: Figure out a better place to put these methods.
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class RoundingFormat {
+
+ public static interface IProperties
+ extends IBasicRoundingProperties,
+ IncrementRounder.IProperties,
+ MagnitudeRounder.IProperties,
+ SignificantDigitsRounder.IProperties {}
+
+ public static Rounder getDefaultOrNoRounder(IProperties properties) {
+ Rounder candidate = getDefaultOrNull(properties);
+ if (candidate == null) {
+ candidate = NoRounder.getInstance(properties);
+ }
+ return candidate;
+ }
+
+ public static Rounder getDefaultOrNull(IProperties properties) {
+ if (SignificantDigitsRounder.useSignificantDigits(properties)) {
+ return SignificantDigitsRounder.getInstance(properties);
+ } else if (IncrementRounder.useRoundingIncrement(properties)) {
+ return IncrementRounder.getInstance(properties);
+ } else if (MagnitudeRounder.useFractionFormat(properties)) {
+ return MagnitudeRounder.getInstance(properties);
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/formatters/ScientificFormat.java b/android_icu4j/src/main/java/android/icu/impl/number/formatters/ScientificFormat.java
new file mode 100644
index 000000000..422e6b66c
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/formatters/ScientificFormat.java
@@ -0,0 +1,242 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.formatters;
+
+import android.icu.impl.number.Format;
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.FormatQuantitySelector;
+import android.icu.impl.number.ModifierHolder;
+import android.icu.impl.number.Properties;
+import android.icu.impl.number.Rounder;
+import android.icu.impl.number.modifiers.ConstantAffixModifier;
+import android.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
+import android.icu.impl.number.rounders.IncrementRounder;
+import android.icu.impl.number.rounders.SignificantDigitsRounder;
+import android.icu.text.DecimalFormatSymbols;
+import android.icu.text.NumberFormat;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class ScientificFormat extends Format.BeforeFormat implements Rounder.MultiplierGenerator {
+
+ public static interface IProperties
+ extends RoundingFormat.IProperties, CurrencyFormat.IProperties {
+
+ static boolean DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN = false;
+
+ /** @see #setExponentSignAlwaysShown */
+ public boolean getExponentSignAlwaysShown();
+
+ /**
+ * Sets whether to show the plus sign in the exponent part of numbers with a zero or positive
+ * exponent. For example, the number "1200" with the pattern "0.0E0" would be formatted as
+ * "1.2E+3" instead of "1.2E3" in <em>en-US</em>.
+ *
+ * @param exponentSignAlwaysShown Whether to show the plus sign in positive exponents.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setExponentSignAlwaysShown(boolean exponentSignAlwaysShown);
+
+ static int DEFAULT_MINIMUM_EXPONENT_DIGITS = -1;
+
+ /** @see #setMinimumExponentDigits */
+ public int getMinimumExponentDigits();
+
+ /**
+ * Sets the minimum number of digits to display in the exponent. For example, the number "1200"
+ * with the pattern "0.0E00", which has 2 exponent digits, would be formatted as "1.2E03" in
+ * <em>en-US</em>.
+ *
+ * @param minimumExponentDigits The minimum number of digits to display in the exponent field.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setMinimumExponentDigits(int minimumExponentDigits);
+ }
+
+ public static boolean useScientificNotation(IProperties properties) {
+ return properties.getMinimumExponentDigits() != IProperties.DEFAULT_MINIMUM_EXPONENT_DIGITS;
+ }
+
+ private static final ThreadLocal<Properties> threadLocalProperties =
+ new ThreadLocal<Properties>() {
+ @Override
+ protected Properties initialValue() {
+ return new Properties();
+ }
+ };
+
+ public static ScientificFormat getInstance(DecimalFormatSymbols symbols, IProperties properties) {
+ // If significant digits or rounding interval are specified through normal means, we use those.
+ // Otherwise, we use the special significant digit rules for scientific notation.
+ Rounder rounder;
+ if (IncrementRounder.useRoundingIncrement(properties)) {
+ rounder = IncrementRounder.getInstance(properties);
+ } else if (SignificantDigitsRounder.useSignificantDigits(properties)) {
+ rounder = SignificantDigitsRounder.getInstance(properties);
+ } else {
+ Properties rprops = threadLocalProperties.get().clear();
+
+ int minInt = properties.getMinimumIntegerDigits();
+ int maxInt = properties.getMaximumIntegerDigits();
+ int minFrac = properties.getMinimumFractionDigits();
+ int maxFrac = properties.getMaximumFractionDigits();
+
+ // If currency is in use, pull information from CurrencyUsage.
+ if (CurrencyFormat.useCurrency(properties)) {
+ // Use rprops as the vehicle (it is still clean)
+ CurrencyFormat.populateCurrencyRounderProperties(rprops, symbols, properties);
+ minFrac = rprops.getMinimumFractionDigits();
+ maxFrac = rprops.getMaximumFractionDigits();
+ rprops.clear();
+ }
+
+ // TODO: Mark/Andy, take a look at this logic and see if it makes sense to you.
+ // I fiddled with the settings and fallbacks to make the unit tests pass, but I
+ // don't feel that it's the "right way" to do things.
+
+ if (minInt < 0) minInt = 0;
+ if (maxInt < minInt) maxInt = minInt;
+ if (minFrac < 0) minFrac = 0;
+ if (maxFrac < minFrac) maxFrac = minFrac;
+
+ rprops.setRoundingMode(properties.getRoundingMode());
+
+ if (minInt == 0 && maxFrac == 0) {
+ // Special case for the pattern "#E0" with no significant digits specified.
+ rprops.setMinimumSignificantDigits(1);
+ rprops.setMaximumSignificantDigits(Integer.MAX_VALUE);
+ } else if (minInt == 0 && minFrac == 0) {
+ // Special case for patterns like "#.##E0" with no significant digits specified.
+ rprops.setMinimumSignificantDigits(1);
+ rprops.setMaximumSignificantDigits(1 + maxFrac);
+ } else {
+ rprops.setMinimumSignificantDigits(minInt + minFrac);
+ rprops.setMaximumSignificantDigits(minInt + maxFrac);
+ }
+ rprops.setMinimumIntegerDigits(maxInt == 0 ? 0 : Math.max(1, minInt + minFrac - maxFrac));
+ rprops.setMaximumIntegerDigits(maxInt);
+ rprops.setMinimumFractionDigits(Math.max(0, minFrac + minInt - maxInt));
+ rprops.setMaximumFractionDigits(maxFrac);
+ rounder = SignificantDigitsRounder.getInstance(rprops);
+ }
+
+ return new ScientificFormat(symbols, properties, rounder);
+ }
+
+ public static ScientificFormat getInstance(
+ DecimalFormatSymbols symbols, IProperties properties, Rounder rounder) {
+ return new ScientificFormat(symbols, properties, rounder);
+ }
+
+ // Properties
+ private final boolean exponentShowPlusSign;
+ private final int exponentDigits;
+ private final int minInt;
+ private final int maxInt;
+ private final int interval;
+ private final Rounder rounder;
+ private final ConstantAffixModifier separatorMod;
+ private final PositiveNegativeAffixModifier signMod;
+
+ // Symbols
+ private final String[] digitStrings;
+
+ private ScientificFormat(DecimalFormatSymbols symbols, IProperties properties, Rounder rounder) {
+ exponentShowPlusSign = properties.getExponentSignAlwaysShown();
+ exponentDigits = Math.max(1, properties.getMinimumExponentDigits());
+
+ // Calculate minInt/maxInt for the purposes of engineering notation:
+ // 0 <= minInt <= maxInt < 8
+ // The values are validated separately for rounding. This scheme needs to prevent OOM issues
+ // (see #13118). Note that the bound 8 on integer digits is historic.
+ int _maxInt = properties.getMaximumIntegerDigits();
+ int _minInt = properties.getMinimumIntegerDigits();
+ // Bug #13289: if maxInt > minInt > 1, then minInt should be 1 for the
+ // purposes of engineering notatation.
+ if (_maxInt > _minInt && _minInt > 1) {
+ _minInt = 1;
+ }
+ minInt = _minInt < 0 ? 0 : _minInt >= 8 ? 1 : _minInt;
+ maxInt = _maxInt < _minInt ? _minInt : _maxInt >= 8 ? _minInt : _maxInt;
+ assert 0 <= minInt && minInt <= maxInt && maxInt < 8;
+
+ interval = maxInt < 1 ? 1 : maxInt;
+ this.rounder = rounder;
+ digitStrings = symbols.getDigitStrings(); // makes a copy
+
+ separatorMod =
+ new ConstantAffixModifier(
+ "", symbols.getExponentSeparator(), NumberFormat.Field.EXPONENT_SYMBOL, true);
+ signMod =
+ new PositiveNegativeAffixModifier(
+ new ConstantAffixModifier(
+ "",
+ exponentShowPlusSign ? symbols.getPlusSignString() : "",
+ NumberFormat.Field.EXPONENT_SIGN,
+ true),
+ new ConstantAffixModifier(
+ "", symbols.getMinusSignString(), NumberFormat.Field.EXPONENT_SIGN, true));
+ }
+
+ private static final ThreadLocal<StringBuilder> threadLocalStringBuilder =
+ new ThreadLocal<StringBuilder>() {
+ @Override
+ protected StringBuilder initialValue() {
+ return new StringBuilder();
+ }
+ };
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+
+ // Treat zero as if it had magnitude 0
+ int exponent;
+ if (input.isZero()) {
+ rounder.apply(input);
+ exponent = 0;
+ } else {
+ exponent = -rounder.chooseMultiplierAndApply(input, this);
+ }
+
+ // Format the exponent part of the scientific format.
+ // Insert digits starting from the left so that append can be used.
+ // TODO: Use thread locals here.
+ FormatQuantity exponentQ = FormatQuantitySelector.from(exponent);
+ StringBuilder exponentSB = threadLocalStringBuilder.get();
+ exponentSB.setLength(0);
+ exponentQ.setIntegerFractionLength(exponentDigits, Integer.MAX_VALUE, 0, 0);
+ for (int i = exponentQ.getUpperDisplayMagnitude(); i >= 0; i--) {
+ exponentSB.append(digitStrings[exponentQ.getDigit(i)]);
+ }
+
+ // Add modifiers from the outside in.
+ mods.add(
+ new ConstantAffixModifier("", exponentSB.toString(), NumberFormat.Field.EXPONENT, true));
+ mods.add(signMod.getModifier(exponent < 0));
+ mods.add(separatorMod);
+ }
+
+ @Override
+ public int getMultiplier(int magnitude) {
+ int digitsShown = ((magnitude % interval + interval) % interval) + 1;
+ if (digitsShown < minInt) {
+ digitsShown = minInt;
+ } else if (digitsShown > maxInt) {
+ digitsShown = maxInt;
+ }
+ int retval = digitsShown - magnitude - 1;
+ return retval;
+ }
+
+ @Override
+ public void export(Properties properties) {
+ properties.setMinimumExponentDigits(exponentDigits);
+ properties.setExponentSignAlwaysShown(exponentShowPlusSign);
+
+ // Set the transformed object into the property bag. This may result in a pattern string that
+ // uses different syntax from the original, but it will be functionally equivalent.
+ rounder.export(properties);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/formatters/StrongAffixFormat.java b/android_icu4j/src/main/java/android/icu/impl/number/formatters/StrongAffixFormat.java
new file mode 100644
index 000000000..87166104a
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/formatters/StrongAffixFormat.java
@@ -0,0 +1,50 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.formatters;
+
+import java.util.Deque;
+
+import android.icu.impl.number.Format;
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.ModifierHolder;
+import android.icu.impl.number.NumberStringBuilder;
+import android.icu.impl.number.Properties;
+
+// TODO: This class isn't currently being used anywhere. Consider removing it.
+
+/** Attaches all prefixes and suffixes at this point in the render tree without bubbling up.
+ * @hide Only a subset of ICU is exposed in Android*/
+public class StrongAffixFormat extends Format implements Format.AfterFormat {
+ private final Format child;
+
+ public StrongAffixFormat(Format child) {
+ this.child = child;
+
+ if (child == null) {
+ throw new IllegalArgumentException("A child formatter is required for StrongAffixFormat");
+ }
+ }
+
+ @Override
+ public int process(
+ Deque<FormatQuantity> inputs,
+ ModifierHolder mods,
+ NumberStringBuilder string,
+ int startIndex) {
+ int length = child.process(inputs, mods, string, startIndex);
+ length += mods.applyAll(string, startIndex, startIndex + length);
+ return length;
+ }
+
+ @Override
+ public int after(
+ ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex) {
+ return mods.applyAll(string, leftIndex, rightIndex);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ // Nothing to do.
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/modifiers/ConstantAffixModifier.java b/android_icu4j/src/main/java/android/icu/impl/number/modifiers/ConstantAffixModifier.java
new file mode 100644
index 000000000..43645637b
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/modifiers/ConstantAffixModifier.java
@@ -0,0 +1,107 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.modifiers;
+
+import android.icu.impl.number.Modifier;
+import android.icu.impl.number.Modifier.AffixModifier;
+import android.icu.impl.number.NumberStringBuilder;
+import android.icu.impl.number.Properties;
+import android.icu.text.NumberFormat.Field;
+
+/** The canonical implementation of {@link Modifier}, containing a prefix and suffix string.
+ * @hide Only a subset of ICU is exposed in Android*/
+public class ConstantAffixModifier extends Modifier.BaseModifier implements AffixModifier {
+
+ // TODO: Avoid making a new instance by default if prefix and suffix are empty
+ public static final AffixModifier EMPTY = new ConstantAffixModifier();
+
+ private final String prefix;
+ private final String suffix;
+ private final Field field;
+ private final boolean strong;
+
+ /**
+ * Constructs an instance with the given strings.
+ *
+ * <p>The arguments need to be Strings, not CharSequences, because Strings are immutable but
+ * CharSequences are not.
+ *
+ * @param prefix The prefix string.
+ * @param suffix The suffix string.
+ * @param field The field type to be associated with this modifier. Can be null.
+ * @param strong Whether this modifier should be strongly applied.
+ * @see Field
+ */
+ public ConstantAffixModifier(String prefix, String suffix, Field field, boolean strong) {
+ // Use an empty string instead of null if we are given null
+ // TODO: Consider returning a null modifier if both prefix and suffix are empty.
+ this.prefix = (prefix == null ? "" : prefix);
+ this.suffix = (suffix == null ? "" : suffix);
+ this.field = field;
+ this.strong = strong;
+ }
+
+ /**
+ * Constructs a new instance with an empty prefix, suffix, and field.
+ */
+ public ConstantAffixModifier() {
+ prefix = "";
+ suffix = "";
+ field = null;
+ strong = false;
+ }
+
+ @Override
+ public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+ // Insert the suffix first since inserting the prefix will change the rightIndex
+ int length = output.insert(rightIndex, suffix, field);
+ length += output.insert(leftIndex, prefix, field);
+ return length;
+ }
+
+ @Override
+ public int length() {
+ return prefix.length() + suffix.length();
+ }
+
+ @Override
+ public boolean isStrong() {
+ return strong;
+ }
+
+ @Override
+ public String getPrefix() {
+ return prefix;
+ }
+
+ @Override
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public boolean contentEquals(CharSequence _prefix, CharSequence _suffix) {
+ if (_prefix == null && !prefix.isEmpty()) return false;
+ if (_suffix == null && !suffix.isEmpty()) return false;
+ if (_prefix != null && prefix.length() != _prefix.length()) return false;
+ if (_suffix != null && suffix.length() != _suffix.length()) return false;
+ for (int i = 0; i < prefix.length(); i++) {
+ if (prefix.charAt(i) != _prefix.charAt(i)) return false;
+ }
+ for (int i = 0; i < suffix.length(); i++) {
+ if (suffix.charAt(i) != _suffix.charAt(i)) return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "<ConstantAffixModifier(%d) prefix:'%s' suffix:'%s'>", length(), prefix, suffix);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/modifiers/ConstantMultiFieldModifier.java b/android_icu4j/src/main/java/android/icu/impl/number/modifiers/ConstantMultiFieldModifier.java
new file mode 100644
index 000000000..055cf25dd
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/modifiers/ConstantMultiFieldModifier.java
@@ -0,0 +1,95 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.modifiers;
+
+import android.icu.impl.number.Modifier;
+import android.icu.impl.number.Modifier.AffixModifier;
+import android.icu.impl.number.NumberStringBuilder;
+import android.icu.impl.number.Properties;
+import android.icu.text.NumberFormat.Field;
+
+/**
+ * An implementation of {@link Modifier} that allows for multiple types of fields in the same
+ * modifier. Constructed based on the contents of two {@link NumberStringBuilder} instances (one for
+ * the prefix, one for the suffix).
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class ConstantMultiFieldModifier extends Modifier.BaseModifier implements AffixModifier {
+
+ // TODO: Avoid making a new instance by default if prefix and suffix are empty
+ public static final ConstantMultiFieldModifier EMPTY = new ConstantMultiFieldModifier();
+
+ private final char[] prefixChars;
+ private final char[] suffixChars;
+ private final Field[] prefixFields;
+ private final Field[] suffixFields;
+ private final String prefix;
+ private final String suffix;
+ private final boolean strong;
+
+ public ConstantMultiFieldModifier(
+ NumberStringBuilder prefix, NumberStringBuilder suffix, boolean strong) {
+ prefixChars = prefix.toCharArray();
+ suffixChars = suffix.toCharArray();
+ prefixFields = prefix.toFieldArray();
+ suffixFields = suffix.toFieldArray();
+ this.prefix = new String(prefixChars);
+ this.suffix = new String(suffixChars);
+ this.strong = strong;
+ }
+
+ private ConstantMultiFieldModifier() {
+ prefixChars = new char[0];
+ suffixChars = new char[0];
+ prefixFields = new Field[0];
+ suffixFields = new Field[0];
+ prefix = "";
+ suffix = "";
+ strong = false;
+ }
+
+ @Override
+ public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+ // Insert the suffix first since inserting the prefix will change the rightIndex
+ int length = output.insert(rightIndex, suffixChars, suffixFields);
+ length += output.insert(leftIndex, prefixChars, prefixFields);
+ return length;
+ }
+
+ @Override
+ public int length() {
+ return prefixChars.length + suffixChars.length;
+ }
+
+ @Override
+ public boolean isStrong() {
+ return strong;
+ }
+
+ @Override
+ public String getPrefix() {
+ return prefix;
+ }
+
+ @Override
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public boolean contentEquals(NumberStringBuilder prefix, NumberStringBuilder suffix) {
+ return prefix.contentEquals(prefixChars, prefixFields)
+ && suffix.contentEquals(suffixChars, suffixFields);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "<ConstantMultiFieldModifier(%d) prefix:'%s' suffix:'%s'>", length(), prefix, suffix);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/modifiers/GeneralPluralModifier.java b/android_icu4j/src/main/java/android/icu/impl/number/modifiers/GeneralPluralModifier.java
new file mode 100644
index 000000000..e1215562f
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/modifiers/GeneralPluralModifier.java
@@ -0,0 +1,78 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.modifiers;
+
+import android.icu.impl.StandardPlural;
+import android.icu.impl.number.Format;
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.Modifier;
+import android.icu.impl.number.ModifierHolder;
+import android.icu.impl.number.Properties;
+import android.icu.text.PluralRules;
+
+// TODO: Is it okay that this class is not completely immutable? Right now it is internal-only.
+// Freezable or Builder could be used if necessary.
+
+/**
+ * A basic implementation of {@link android.icu.impl.number.Modifier.PositiveNegativePluralModifier}
+ * that is built on the fly using its <code>put</code> methods.
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class GeneralPluralModifier extends Format.BeforeFormat
+ implements Modifier.PositiveNegativePluralModifier {
+ /**
+ * A single array for modifiers. Even elements are positive; odd elements are negative. The
+ * elements 2i and 2i+1 belong to the StandardPlural with ordinal i.
+ */
+ private final Modifier[] mods;
+
+ public GeneralPluralModifier() {
+ this.mods = new Modifier[StandardPlural.COUNT * 2];
+ }
+
+ /** Adds a positive/negative-agnostic modifier for the specified plural form. */
+ public void put(StandardPlural plural, Modifier modifier) {
+ put(plural, modifier, modifier);
+ }
+
+ /** Adds a positive and a negative modifier for the specified plural form. */
+ public void put(StandardPlural plural, Modifier positive, Modifier negative) {
+ assert mods[plural.ordinal() * 2] == null;
+ assert mods[plural.ordinal() * 2 + 1] == null;
+ assert positive != null;
+ assert negative != null;
+ mods[plural.ordinal() * 2] = positive;
+ mods[plural.ordinal() * 2 + 1] = negative;
+ }
+
+ @Override
+ public Modifier getModifier(StandardPlural plural, boolean isNegative) {
+ Modifier mod = mods[plural.ordinal() * 2 + (isNegative ? 1 : 0)];
+ if (mod == null) {
+ mod = mods[StandardPlural.OTHER.ordinal()*2 + (isNegative ? 1 : 0)];
+ }
+ if (mod == null) {
+ throw new UnsupportedOperationException();
+ }
+ return mod;
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) {
+ mods.add(getModifier(input.getStandardPlural(rules), input.isNegative()));
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void export(Properties properties) {
+ // Since we can export only one affix pair, do the one for "other".
+ Modifier positive = getModifier(StandardPlural.OTHER, false);
+ Modifier negative = getModifier(StandardPlural.OTHER, true);
+ PositiveNegativeAffixModifier.exportPositiveNegative(properties, positive, negative);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/modifiers/PositiveNegativeAffixModifier.java b/android_icu4j/src/main/java/android/icu/impl/number/modifiers/PositiveNegativeAffixModifier.java
new file mode 100644
index 000000000..95ca54b91
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/modifiers/PositiveNegativeAffixModifier.java
@@ -0,0 +1,55 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.modifiers;
+
+import android.icu.impl.number.Format;
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.Modifier;
+import android.icu.impl.number.Modifier.AffixModifier;
+import android.icu.impl.number.ModifierHolder;
+import android.icu.impl.number.Properties;
+
+/** A class containing a positive form and a negative form of {@link ConstantAffixModifier}.
+ * @hide Only a subset of ICU is exposed in Android*/
+public class PositiveNegativeAffixModifier extends Format.BeforeFormat
+ implements Modifier.PositiveNegativeModifier {
+ private final AffixModifier positive;
+ private final AffixModifier negative;
+
+ /**
+ * Constructs an instance using the two {@link ConstantMultiFieldModifier} classes for positive
+ * and negative.
+ *
+ * @param positive The positive-form Modifier.
+ * @param negative The negative-form Modifier.
+ */
+ public PositiveNegativeAffixModifier(AffixModifier positive, AffixModifier negative) {
+ this.positive = positive;
+ this.negative = negative;
+ }
+
+ @Override
+ public Modifier getModifier(boolean isNegative) {
+ return isNegative ? negative : positive;
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+ Modifier mod = getModifier(input.isNegative());
+ mods.add(mod);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ exportPositiveNegative(properties, positive, negative);
+ }
+
+ /** Internal method used to export a positive and negative modifier to a property bag. */
+ static void exportPositiveNegative(Properties properties, Modifier positive, Modifier negative) {
+ properties.setPositivePrefix(positive.getPrefix().isEmpty() ? null : positive.getPrefix());
+ properties.setPositiveSuffix(positive.getSuffix().isEmpty() ? null : positive.getSuffix());
+ properties.setNegativePrefix(negative.getPrefix().isEmpty() ? null : negative.getPrefix());
+ properties.setNegativeSuffix(negative.getSuffix().isEmpty() ? null : negative.getSuffix());
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/modifiers/SimpleModifier.java b/android_icu4j/src/main/java/android/icu/impl/number/modifiers/SimpleModifier.java
new file mode 100644
index 000000000..0c83b4388
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/modifiers/SimpleModifier.java
@@ -0,0 +1,132 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.modifiers;
+
+import android.icu.impl.SimpleFormatterImpl;
+import android.icu.impl.number.Modifier;
+import android.icu.impl.number.NumberStringBuilder;
+import android.icu.impl.number.Properties;
+import android.icu.text.NumberFormat.Field;
+
+/**
+ * The second primary implementation of {@link Modifier}, this one consuming a {@link
+ * android.icu.text.SimpleFormatter} pattern.
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class SimpleModifier extends Modifier.BaseModifier {
+ private final String compiledPattern;
+ private final Field field;
+ private final boolean strong;
+
+ /** Creates a modifier that uses the SimpleFormatter string formats. */
+ public SimpleModifier(String compiledPattern, Field field, boolean strong) {
+ this.compiledPattern = (compiledPattern == null) ? "\u0001\u0000" : compiledPattern;
+ this.field = field;
+ this.strong = strong;
+ }
+
+ @Override
+ public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+ return formatAsPrefixSuffix(compiledPattern, output, leftIndex, rightIndex, field);
+ }
+
+ @Override
+ public int length() {
+ // TODO: Make a separate method for computing the length only?
+ return formatAsPrefixSuffix(compiledPattern, null, -1, -1, field);
+ }
+
+ @Override
+ public boolean isStrong() {
+ return strong;
+ }
+
+ @Override
+ public String getPrefix() {
+ // TODO: Implement this when MeasureFormat is ready.
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getSuffix() {
+ // TODO: Implement this when MeasureFormat is ready.
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 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 compiledPattern Compiled form of a pattern string.
+ * @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 static int formatAsPrefixSuffix(
+ String compiledPattern,
+ NumberStringBuilder result,
+ int startIndex,
+ int endIndex,
+ Field field) {
+ assert SimpleFormatterImpl.getArgumentLimit(compiledPattern) == 1;
+ int ARG_NUM_LIMIT = 0x100;
+ int length = 0, offset = 2;
+ if (compiledPattern.charAt(1) != '\u0000') {
+ int prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT;
+ if (result != null) {
+ result.insert(startIndex, compiledPattern, 2, 2 + prefixLength, field);
+ }
+ length += prefixLength;
+ offset = 3 + prefixLength;
+ }
+ if (offset < compiledPattern.length()) {
+ int suffixLength = compiledPattern.charAt(offset) - ARG_NUM_LIMIT;
+ if (result != null) {
+ result.insert(
+ endIndex + length, compiledPattern, offset + 1, offset + suffixLength + 1, field);
+ }
+ length += suffixLength;
+ }
+ return length;
+ }
+
+ /** TODO: Move this to a test file somewhere, once we figure out what to do with the method. */
+ public static void testFormatAsPrefixSuffix() {
+ String[] patterns = {"{0}", "X{0}Y", "XX{0}YYY", "{0}YY", "XXXX{0}"};
+ Object[][] outputs = {{"", 0, 0}, {"abcde", 0, 0}, {"abcde", 2, 2}, {"abcde", 1, 3}};
+ String[][] expecteds = {
+ {"", "XY", "XXYYY", "YY", "XXXX"},
+ {"abcde", "XYabcde", "XXYYYabcde", "YYabcde", "XXXXabcde"},
+ {"abcde", "abXYcde", "abXXYYYcde", "abYYcde", "abXXXXcde"},
+ {"abcde", "aXbcYde", "aXXbcYYYde", "abcYYde", "aXXXXbcde"}
+ };
+ for (int i = 0; i < patterns.length; i++) {
+ for (int j = 0; j < outputs.length; j++) {
+ String pattern = patterns[i];
+ String compiledPattern =
+ SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, new StringBuilder(), 1, 1);
+ NumberStringBuilder output = new NumberStringBuilder();
+ output.append((String) outputs[j][0], null);
+ formatAsPrefixSuffix(
+ compiledPattern, output, (Integer) outputs[j][1], (Integer) outputs[j][2], null);
+ String expected = expecteds[j][i];
+ String actual = output.toString();
+ assert expected.equals(actual);
+ }
+ }
+ }
+
+ @Override
+ public void export(Properties properties) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/rounders/IncrementRounder.java b/android_icu4j/src/main/java/android/icu/impl/number/rounders/IncrementRounder.java
new file mode 100644
index 000000000..285bf0a71
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/rounders/IncrementRounder.java
@@ -0,0 +1,71 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.rounders;
+
+import java.math.BigDecimal;
+
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.Properties;
+import android.icu.impl.number.Rounder;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class IncrementRounder extends Rounder {
+
+ public static interface IProperties extends IBasicRoundingProperties {
+
+ static BigDecimal DEFAULT_ROUNDING_INCREMENT = null;
+
+ /** @see #setRoundingIncrement */
+ public BigDecimal getRoundingIncrement();
+
+ /**
+ * Sets the increment to which to round numbers. For example, with a rounding interval of 0.05,
+ * the number 11.17 would be formatted as "11.15" in locale <em>en-US</em> with the default
+ * rounding mode.
+ *
+ * <p>You can use either a rounding increment or significant digits, but not both at the same
+ * time.
+ *
+ * <p>The rounding increment can be specified in a pattern string. For example, the pattern
+ * "#,##0.05" corresponds to a rounding interval of 0.05 with 1 minimum integer digit and a
+ * grouping size of 3.
+ *
+ * @param roundingIncrement The interval to which to round.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setRoundingIncrement(BigDecimal roundingIncrement);
+ }
+
+ public static boolean useRoundingIncrement(IProperties properties) {
+ return properties.getRoundingIncrement() != IProperties.DEFAULT_ROUNDING_INCREMENT;
+ }
+
+ private final BigDecimal roundingIncrement;
+
+ public static IncrementRounder getInstance(IProperties properties) {
+ return new IncrementRounder(properties);
+ }
+
+ private IncrementRounder(IProperties properties) {
+ super(properties);
+ if (properties.getRoundingIncrement().compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("Rounding interval must be greater than zero");
+ }
+ roundingIncrement = properties.getRoundingIncrement();
+ }
+
+ @Override
+ public void apply(FormatQuantity input) {
+ input.roundToIncrement(roundingIncrement, mathContext);
+ applyDefaults(input);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ super.export(properties);
+ properties.setRoundingIncrement(roundingIncrement);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/rounders/MagnitudeRounder.java b/android_icu4j/src/main/java/android/icu/impl/number/rounders/MagnitudeRounder.java
new file mode 100644
index 000000000..8820d81d6
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/rounders/MagnitudeRounder.java
@@ -0,0 +1,34 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.rounders;
+
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.Rounder;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class MagnitudeRounder extends Rounder {
+
+ public static interface IProperties extends IBasicRoundingProperties {}
+
+ public static boolean useFractionFormat(IProperties properties) {
+ return properties.getMinimumFractionDigits() != IProperties.DEFAULT_MINIMUM_FRACTION_DIGITS
+ || properties.getMaximumFractionDigits() != IProperties.DEFAULT_MAXIMUM_FRACTION_DIGITS;
+ }
+
+ public static MagnitudeRounder getInstance(IBasicRoundingProperties properties) {
+ return new MagnitudeRounder(properties);
+ }
+
+ private MagnitudeRounder(IBasicRoundingProperties properties) {
+ super(properties);
+ }
+
+ @Override
+ public void apply(FormatQuantity input) {
+ input.roundToMagnitude(-maxFrac, mathContext);
+ applyDefaults(input);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/rounders/NoRounder.java b/android_icu4j/src/main/java/android/icu/impl/number/rounders/NoRounder.java
new file mode 100644
index 000000000..ac293d19c
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/rounders/NoRounder.java
@@ -0,0 +1,26 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.rounders;
+
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.Rounder;
+
+/** Sets the integer and fraction length based on the properties, but does not perform rounding.
+ * @hide Only a subset of ICU is exposed in Android*/
+public final class NoRounder extends Rounder {
+
+ public static NoRounder getInstance(IBasicRoundingProperties properties) {
+ return new NoRounder(properties);
+ }
+
+ private NoRounder(IBasicRoundingProperties properties) {
+ super(properties);
+ }
+
+ @Override
+ public void apply(FormatQuantity input) {
+ applyDefaults(input);
+ input.roundToInfinity();
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/rounders/SignificantDigitsRounder.java b/android_icu4j/src/main/java/android/icu/impl/number/rounders/SignificantDigitsRounder.java
new file mode 100644
index 000000000..fea6e014d
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/number/rounders/SignificantDigitsRounder.java
@@ -0,0 +1,209 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package android.icu.impl.number.rounders;
+
+import java.math.RoundingMode;
+
+import android.icu.impl.number.FormatQuantity;
+import android.icu.impl.number.Properties;
+import android.icu.impl.number.Rounder;
+import android.icu.text.DecimalFormat.SignificantDigitsMode;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class SignificantDigitsRounder extends Rounder {
+
+ public static interface IProperties extends Rounder.IBasicRoundingProperties {
+
+ static int DEFAULT_MINIMUM_SIGNIFICANT_DIGITS = -1;
+
+ /** @see #setMinimumSignificantDigits */
+ public int getMinimumSignificantDigits();
+
+ /**
+ * Sets the minimum number of significant digits to display. If, after rounding to the number of
+ * significant digits specified by {@link #setMaximumSignificantDigits}, the number of remaining
+ * significant digits is less than the minimum, the number will be padded with zeros. For
+ * example, if minimum significant digits is 3, the number 5.8 will be formatted as "5.80" in
+ * locale <em>en-US</em>. Note that minimum significant digits is relevant only when numbers
+ * have digits after the decimal point.
+ *
+ * <p>If both minimum significant digits and minimum integer/fraction digits are set at the same
+ * time, both values will be respected, and the one that results in the greater number of
+ * padding zeros will be used. For example, formatting the number 73 with 3 minimum significant
+ * digits and 2 minimum fraction digits will produce "73.00".
+ *
+ * <p>The number of significant digits can be specified in a pattern string using the '@'
+ * character. For example, the pattern "@@#" corresponds to a minimum of 2 and a maximum of 3
+ * significant digits.
+ *
+ * @param minimumSignificantDigits The minimum number of significant digits to display.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setMinimumSignificantDigits(int minimumSignificantDigits);
+
+ static int DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS = -1;
+
+ /** @see #setMaximumSignificantDigits */
+ public int getMaximumSignificantDigits();
+
+ /**
+ * Sets the maximum number of significant digits to display. The number of significant digits is
+ * equal to the number of digits counted from the leftmost nonzero digit through the rightmost
+ * nonzero digit; for example, the number "2010" has 3 significant digits. If the number has
+ * more significant digits than specified here, the extra significant digits will be rounded off
+ * using the rounding mode specified by {@link #setRoundingMode(RoundingMode)}. For example, if
+ * maximum significant digits is 3, the number 1234.56 will be formatted as "1230" in locale
+ * <em>en-US</em> with the default rounding mode.
+ *
+ * <p>If both maximum significant digits and maximum integer/fraction digits are set at the same
+ * time, the behavior is undefined.
+ *
+ * <p>The number of significant digits can be specified in a pattern string using the '@'
+ * character. For example, the pattern "@@#" corresponds to a minimum of 2 and a maximum of 3
+ * significant digits.
+ *
+ * @param maximumSignificantDigits The maximum number of significant digits to display.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setMaximumSignificantDigits(int maximumSignificantDigits);
+
+ static SignificantDigitsMode DEFAULT_SIGNIFICANT_DIGITS_MODE = null;
+
+ /** @see #setSignificantDigitsMode */
+ public SignificantDigitsMode getSignificantDigitsMode();
+
+ /**
+ * Sets the strategy used when reconciling significant digits versus integer and fraction
+ * lengths.
+ *
+ * @param significantDigitsMode One of the options from {@link SignificantDigitsMode}.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setSignificantDigitsMode(SignificantDigitsMode significantDigitsMode);
+ }
+
+ public static boolean useSignificantDigits(IProperties properties) {
+ return properties.getMinimumSignificantDigits()
+ != IProperties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS
+ || properties.getMaximumSignificantDigits()
+ != IProperties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS
+ || properties.getSignificantDigitsMode() != IProperties.DEFAULT_SIGNIFICANT_DIGITS_MODE;
+ }
+
+ public static SignificantDigitsRounder getInstance(IProperties properties) {
+ return new SignificantDigitsRounder(properties);
+ }
+
+ private final int minSig;
+ private final int maxSig;
+ private final SignificantDigitsMode mode;
+
+ private SignificantDigitsRounder(IProperties properties) {
+ super(properties);
+ int _minSig = properties.getMinimumSignificantDigits();
+ int _maxSig = properties.getMaximumSignificantDigits();
+ minSig = _minSig < 1 ? 1 : _minSig > 1000 ? 1000 : _minSig;
+ maxSig = _maxSig < 0 ? 1000 : _maxSig < minSig ? minSig : _maxSig > 1000 ? 1000 : _maxSig;
+ SignificantDigitsMode _mode = properties.getSignificantDigitsMode();
+ mode = _mode == null ? SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION : _mode;
+ }
+
+ @Override
+ public void apply(FormatQuantity input) {
+
+ int magnitude, effectiveMag, magMinSig, magMaxSig;
+
+ if (input.isZero()) {
+ // Treat zero as if magnitude corresponded to the minimum number of zeros
+ magnitude = minInt - 1;
+ } else {
+ magnitude = input.getMagnitude();
+ }
+ effectiveMag = Math.min(magnitude + 1, maxInt);
+ magMinSig = effectiveMag - minSig;
+ magMaxSig = effectiveMag - maxSig;
+
+ // Step 1: pick the rounding magnitude and apply.
+ int roundingMagnitude;
+ switch (mode) {
+ case OVERRIDE_MAXIMUM_FRACTION:
+ // Always round to maxSig.
+ // Of the six possible orders:
+ // Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins
+ // Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins
+ // Case 3: minSig, minFrac, maxFrac, maxSig -- maxSig wins
+ // Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins
+ // Case 5: minFrac, minSig, maxFrac, maxSig -- maxSig wins
+ // Case 6: minFrac, maxFrac, minSig, maxSig -- maxSig wins
+ roundingMagnitude = magMaxSig;
+ break;
+ case RESPECT_MAXIMUM_FRACTION:
+ // Round to the strongest of maxFrac, maxInt, and maxSig.
+ // Of the six possible orders:
+ // Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins
+ // Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins
+ // Case 3: minSig, minFrac, maxFrac, maxSig -- maxFrac wins --> differs from default
+ // Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins
+ // Case 5: minFrac, minSig, maxFrac, maxSig -- maxFrac wins --> differs from default
+ // Case 6: minFrac, maxFrac, minSig, maxSig -- maxFrac wins --> differs from default
+ //
+ // Math.max() picks the rounding magnitude farthest to the left (most significant).
+ // Math.min() picks the rounding magnitude farthest to the right (least significant).
+ roundingMagnitude = Math.max(-maxFrac, magMaxSig);
+ break;
+ case ENSURE_MINIMUM_SIGNIFICANT:
+ // Round to the strongest of maxFrac and maxSig, and always ensure minSig.
+ // Of the six possible orders:
+ // Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins
+ // Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins
+ // Case 3: minSig, minFrac, maxFrac, maxSig -- maxFrac wins --> differs from default
+ // Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins
+ // Case 5: minFrac, minSig, maxFrac, maxSig -- maxFrac wins --> differs from default
+ // Case 6: minFrac, maxFrac, minSig, maxSig -- minSig wins --> differs from default
+ roundingMagnitude = Math.min(magMinSig, Math.max(-maxFrac, magMaxSig));
+ break;
+ default:
+ throw new AssertionError();
+ }
+ input.roundToMagnitude(roundingMagnitude, mathContext);
+
+ // In case magnitude changed:
+ if (input.isZero()) {
+ magnitude = minInt - 1;
+ } else {
+ magnitude = input.getMagnitude();
+ }
+ effectiveMag = Math.min(magnitude + 1, maxInt);
+ magMinSig = effectiveMag - minSig;
+ magMaxSig = effectiveMag - maxSig;
+
+ // Step 2: pick the number of visible digits.
+ switch (mode) {
+ case OVERRIDE_MAXIMUM_FRACTION:
+ // Ensure minSig is always displayed.
+ input.setIntegerFractionLength(
+ minInt, maxInt, Math.max(minFrac, -magMinSig), Integer.MAX_VALUE);
+ break;
+ case RESPECT_MAXIMUM_FRACTION:
+ // Ensure minSig is displayed, unless doing so is in violation of maxFrac.
+ input.setIntegerFractionLength(
+ minInt, maxInt, Math.min(maxFrac, Math.max(minFrac, -magMinSig)), maxFrac);
+ break;
+ case ENSURE_MINIMUM_SIGNIFICANT:
+ // Follow minInt/minFrac, but ensure all digits are allowed to be visible.
+ input.setIntegerFractionLength(minInt, maxInt, minFrac, Integer.MAX_VALUE);
+ break;
+ }
+ }
+
+ @Override
+ public void export(Properties properties) {
+ super.export(properties);
+ properties.setMinimumSignificantDigits(minSig);
+ properties.setMaximumSignificantDigits(maxSig);
+ properties.setSignificantDigitsMode(mode);
+ }
+}
diff --git a/android_icu4j/src/main/java/android/icu/text/CompactDecimalDataCache.java b/android_icu4j/src/main/java/android/icu/text/CompactDecimalDataCache.java
deleted file mode 100644
index dbb03d083..000000000
--- a/android_icu4j/src/main/java/android/icu/text/CompactDecimalDataCache.java
+++ /dev/null
@@ -1,525 +0,0 @@
-/* GENERATED SOURCE. DO NOT MODIFY. */
-// © 2016 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html#License
-/*
- *******************************************************************************
- * Copyright (C) 2012-2016, International Business Machines Corporation and
- * others. All Rights Reserved.
- *******************************************************************************
- */
-package android.icu.text;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.MissingResourceException;
-
-import android.icu.impl.ICUCache;
-import android.icu.impl.ICUData;
-import android.icu.impl.ICUResourceBundle;
-import android.icu.impl.SimpleCache;
-import android.icu.impl.UResource;
-import android.icu.text.DecimalFormat.Unit;
-import android.icu.util.ULocale;
-import android.icu.util.UResourceBundle;
-
-/**
- * A cache containing data by locale for {@link CompactDecimalFormat}
- *
- * @author Travis Keep
- */
-class CompactDecimalDataCache {
-
- private static final String SHORT_STYLE = "short";
- private static final String LONG_STYLE = "long";
- private static final String SHORT_CURRENCY_STYLE = "shortCurrency";
- private static final String NUMBER_ELEMENTS = "NumberElements";
- private static final String PATTERNS_LONG = "patternsLong";
- private static final String PATTERNS_SHORT = "patternsShort";
- private static final String DECIMAL_FORMAT = "decimalFormat";
- private static final String CURRENCY_FORMAT = "currencyFormat";
- private static final String LATIN_NUMBERING_SYSTEM = "latn";
-
- private static enum PatternsTableKey { PATTERNS_LONG, PATTERNS_SHORT };
- private static enum FormatsTableKey { DECIMAL_FORMAT, CURRENCY_FORMAT };
-
- public static final String OTHER = "other";
-
- /**
- * We can specify prefixes or suffixes for values with up to 15 digits,
- * less than 10^15.
- */
- static final int MAX_DIGITS = 15;
-
- private final ICUCache<ULocale, DataBundle> cache =
- new SimpleCache<ULocale, DataBundle>();
-
- /**
- * Data contains the compact decimal data for a particular locale. Data consists
- * of one array and two hashmaps. The index of the divisors array as well
- * as the arrays stored in the values of the two hashmaps correspond
- * to log10 of the number being formatted, so when formatting 12,345, the 4th
- * index of the arrays should be used. Divisors contain the number to divide
- * by before doing formatting. In the case of english, <code>divisors[4]</code>
- * is 1000. So to format 12,345, divide by 1000 to get 12. Then use
- * PluralRules with the current locale to figure out which of the 6 plural variants
- * 12 matches: "zero", "one", "two", "few", "many", or "other." Prefixes and
- * suffixes are maps whose key is the plural variant and whose values are
- * arrays of strings with indexes corresponding to log10 of the original number.
- * these arrays contain the prefix or suffix to use.
- *
- * Each array in data is 15 in length, and every index is filled.
- *
- * @author Travis Keep
- *
- */
- static class Data {
- long[] divisors;
- Map<String, DecimalFormat.Unit[]> units;
- boolean fromFallback;
-
- Data(long[] divisors, Map<String, DecimalFormat.Unit[]> units)
- {
- this.divisors = divisors;
- this.units = units;
- }
-
- public boolean isEmpty() {
- return units == null || units.isEmpty();
- }
- }
-
- /**
- * DataBundle contains compact decimal data for all the styles in a particular
- * locale. Currently available styles are short and long for decimals, and
- * short only for currencies.
- *
- * @author Travis Keep
- */
- static class DataBundle {
- Data shortData;
- Data longData;
- Data shortCurrencyData;
-
- private DataBundle(Data shortData, Data longData, Data shortCurrencyData) {
- this.shortData = shortData;
- this.longData = longData;
- this.shortCurrencyData = shortCurrencyData;
- }
-
- private static DataBundle createEmpty() {
- return new DataBundle(
- new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>()),
- new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>()),
- new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>())
- );
- }
- }
-
- /**
- * Sink for enumerating all of the compact decimal format patterns.
- *
- * More specific bundles (en_GB) are enumerated before their parents (en_001, en, root):
- * Only store a value if it is still missing, that is, it has not been overridden.
- */
- private static final class CompactDecimalDataSink extends UResource.Sink {
-
- private DataBundle dataBundle; // Where to save values when they are read
- private ULocale locale; // The locale we are traversing (for exception messages)
- private boolean isLatin; // Whether or not we are traversing the Latin table
- private boolean isFallback; // Whether or not we are traversing the Latin table as fallback
-
- /*
- * NumberElements{ <-- top (numbering system table)
- * latn{ <-- patternsTable (one per numbering system)
- * patternsLong{ <-- formatsTable (one per pattern)
- * decimalFormat{ <-- powersOfTenTable (one per format)
- * 1000{ <-- pluralVariantsTable (one per power of ten)
- * one{"0 thousand"} <-- plural variant and template
- */
-
- public CompactDecimalDataSink(DataBundle dataBundle, ULocale locale) {
- this.dataBundle = dataBundle;
- this.locale = locale;
- }
-
- @Override
- public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
- // SPECIAL CASE: Don't consume root in the non-Latin numbering system
- if (isRoot && !isLatin) { return; }
-
- UResource.Table patternsTable = value.getTable();
- for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) {
-
- // patterns table: check for patternsShort or patternsLong
- PatternsTableKey patternsTableKey;
- if (key.contentEquals(PATTERNS_SHORT)) {
- patternsTableKey = PatternsTableKey.PATTERNS_SHORT;
- } else if (key.contentEquals(PATTERNS_LONG)) {
- patternsTableKey = PatternsTableKey.PATTERNS_LONG;
- } else {
- continue;
- }
-
- // traverse into the table of formats
- UResource.Table formatsTable = value.getTable();
- for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) {
-
- // formats table: check for decimalFormat or currencyFormat
- FormatsTableKey formatsTableKey;
- if (key.contentEquals(DECIMAL_FORMAT)) {
- formatsTableKey = FormatsTableKey.DECIMAL_FORMAT;
- } else if (key.contentEquals(CURRENCY_FORMAT)) {
- formatsTableKey = FormatsTableKey.CURRENCY_FORMAT;
- } else {
- continue;
- }
-
- // Set the current style and destination based on the lvl1 and lvl2 keys
- String style = null;
- Data destination = null;
- if (patternsTableKey == PatternsTableKey.PATTERNS_LONG
- && formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
- style = LONG_STYLE;
- destination = dataBundle.longData;
- } else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
- && formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
- style = SHORT_STYLE;
- destination = dataBundle.shortData;
- } else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
- && formatsTableKey == FormatsTableKey.CURRENCY_FORMAT) {
- style = SHORT_CURRENCY_STYLE;
- destination = dataBundle.shortCurrencyData;
- } else {
- // Silently ignore this case
- continue;
- }
-
- // SPECIAL CASE: RULES FOR WHETHER OR NOT TO CONSUME THIS TABLE:
- // 1) Don't consume longData if shortData was consumed from the non-Latin
- // locale numbering system
- // 2) Don't consume longData for the first time if this is the root bundle and
- // shortData is already populated from a more specific locale. Note that if
- // both longData and shortData are both only in root, longData will be
- // consumed since it is alphabetically before shortData in the bundle.
- if (isFallback
- && style == LONG_STYLE
- && !dataBundle.shortData.isEmpty()
- && !dataBundle.shortData.fromFallback) {
- continue;
- }
- if (isRoot
- && style == LONG_STYLE
- && dataBundle.longData.isEmpty()
- && !dataBundle.shortData.isEmpty()) {
- continue;
- }
-
- // Set the "fromFallback" flag on the data object
- destination.fromFallback = isFallback;
-
- // traverse into the table of powers of ten
- UResource.Table powersOfTenTable = value.getTable();
- for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) {
-
- // This value will always be some even power of 10. e.g 10000.
- long power10 = Long.parseLong(key.toString());
- int log10Value = (int) Math.log10(power10);
-
- // Silently ignore divisors that are too big.
- if (log10Value >= MAX_DIGITS) continue;
-
- // Iterate over the plural variants ("one", "other", etc)
- UResource.Table pluralVariantsTable = value.getTable();
- for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
- // TODO: Use StandardPlural rather than String.
- String pluralVariant = key.toString();
- String template = value.toString();
-
- // Copy the data into the in-memory data bundle (do not overwrite
- // existing values)
- int numZeros = populatePrefixSuffix(
- pluralVariant, log10Value, template, locale, style, destination, false);
-
- // If populatePrefixSuffix returns -1, it means that this key has been
- // encountered already.
- if (numZeros < 0) {
- continue;
- }
-
- // Set the divisor, which is based on the number of zeros in the template
- // string. If the divisor from here is different from the one previously
- // stored, it means that the number of zeros in different plural variants
- // differs; throw an exception.
- long divisor = calculateDivisor(power10, numZeros);
- if (destination.divisors[log10Value] != 0L
- && destination.divisors[log10Value] != divisor) {
- throw new IllegalArgumentException("Plural variant '" + pluralVariant
- + "' template '" + template
- + "' for 10^" + log10Value
- + " has wrong number of zeros in " + localeAndStyle(locale, style));
- }
- destination.divisors[log10Value] = divisor;
- }
- }
- }
- }
- }
- }
-
- /**
- * Fetch data for a particular locale. Clients must not modify any part of the returned data. Portions of returned
- * data may be shared so modifying it will have unpredictable results.
- */
- DataBundle get(ULocale locale) {
- DataBundle result = cache.get(locale);
- if (result == null) {
- result = load(locale);
- cache.put(locale, result);
- }
- return result;
- }
-
- private static DataBundle load(ULocale ulocale) throws MissingResourceException {
- DataBundle dataBundle = DataBundle.createEmpty();
- String nsName = NumberingSystem.getInstance(ulocale).getName();
- ICUResourceBundle r = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
- ulocale);
- CompactDecimalDataSink sink = new CompactDecimalDataSink(dataBundle, ulocale);
- sink.isFallback = false;
-
- // First load the number elements data from nsName if nsName is not Latin.
- if (!nsName.equals(LATIN_NUMBERING_SYSTEM)) {
- sink.isLatin = false;
-
- try {
- r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + nsName, sink);
- } catch (MissingResourceException e) {
- // Silently ignore and use Latin
- }
-
- // Set the "isFallback" flag for when we read Latin
- sink.isFallback = true;
- }
-
- // Now load Latin, which will fill in things that were left out from above.
- sink.isLatin = true;
- r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + LATIN_NUMBERING_SYSTEM, sink);
-
- // If longData is empty, default it to be equal to shortData
- if (dataBundle.longData.isEmpty()) {
- dataBundle.longData = dataBundle.shortData;
- }
-
- // Check for "other" variants in each of the three data classes
- checkForOtherVariants(dataBundle.longData, ulocale, LONG_STYLE);
- checkForOtherVariants(dataBundle.shortData, ulocale, SHORT_STYLE);
- checkForOtherVariants(dataBundle.shortCurrencyData, ulocale, SHORT_CURRENCY_STYLE);
-
- // Resolve missing elements
- fillInMissing(dataBundle.longData);
- fillInMissing(dataBundle.shortData);
- fillInMissing(dataBundle.shortCurrencyData);
-
- // Return the data bundle
- return dataBundle;
- }
-
-
- /**
- * Populates prefix and suffix information for a particular plural variant
- * and index (log10 value).
- * @param pluralVariant e.g "one", "other"
- * @param idx the index (log10 value of the number) 0 <= idx < MAX_DIGITS
- * @param template e.g "00K"
- * @param locale the locale
- * @param style the style
- * @param destination Extracted prefix and suffix stored here.
- * @return number of zeros found before any decimal point in template, or -1 if it was not saved.
- */
- private static int populatePrefixSuffix(
- String pluralVariant, int idx, String template, ULocale locale, String style,
- Data destination, boolean overwrite) {
- int firstIdx = template.indexOf("0");
- int lastIdx = template.lastIndexOf("0");
- if (firstIdx == -1) {
- throw new IllegalArgumentException(
- "Expect at least one zero in template '" + template +
- "' for variant '" +pluralVariant + "' for 10^" + idx +
- " in " + localeAndStyle(locale, style));
- }
- String prefix = template.substring(0, firstIdx);
- String suffix = template.substring(lastIdx + 1);
-
- // Save the unit, and return -1 if it was not saved
- boolean saved = saveUnit(new DecimalFormat.Unit(prefix, suffix), pluralVariant, idx, destination.units, overwrite);
- if (!saved) {
- return -1;
- }
-
- // If there is effectively no prefix or suffix, ignore the actual
- // number of 0's and act as if the number of 0's matches the size
- // of the number
- if (prefix.trim().length() == 0 && suffix.trim().length() == 0) {
- return idx + 1;
- }
-
- // Calculate number of zeros before decimal point.
- int i = firstIdx + 1;
- while (i <= lastIdx && template.charAt(i) == '0') {
- i++;
- }
- return i - firstIdx;
- }
-
- /**
- * Calculate a divisor based on the magnitude and number of zeros in the
- * template string.
- * @param power10
- * @param numZeros
- * @return
- */
- private static long calculateDivisor(long power10, int numZeros) {
- // We craft our divisor such that when we divide by it, we get a
- // number with the same number of digits as zeros found in the
- // plural variant templates. If our magnitude is 10000 and we have
- // two 0's in our plural variants, then we want a divisor of 1000.
- // Note that if we have 43560 which is of same magnitude as 10000.
- // When we divide by 1000 we a quotient which rounds to 44 (2 digits)
- long divisor = power10;
- for (int i = 1; i < numZeros; i++) {
- divisor /= 10;
- }
- return divisor;
- }
-
-
- /**
- * Returns locale and style. Used to form useful messages in thrown exceptions.
- *
- * Note: This is not covered by unit tests since no exceptions are thrown on the default CLDR data. It is too
- * cumbersome to cover via reflection.
- *
- * @param locale the locale
- * @param style the style
- */
- private static String localeAndStyle(ULocale locale, String style) {
- return "locale '" + locale + "' style '" + style + "'";
- }
-
- /**
- * Checks to make sure that an "other" variant is present in all powers of 10.
- * @param data
- */
- private static void checkForOtherVariants(Data data, ULocale locale, String style) {
- DecimalFormat.Unit[] otherByBase = data.units.get(OTHER);
-
- if (otherByBase == null) {
- throw new IllegalArgumentException("No 'other' plural variants defined in "
- + localeAndStyle(locale, style));
- }
-
- // Check all other plural variants, and make sure that if any of them are populated, then
- // other is also populated
- for (Map.Entry<String, Unit[]> entry : data.units.entrySet()) {
- if (entry.getKey() == OTHER) continue;
- DecimalFormat.Unit[] variantByBase = entry.getValue();
- for (int log10Value = 0; log10Value < MAX_DIGITS; log10Value++) {
- if (variantByBase[log10Value] != null && otherByBase[log10Value] == null) {
- throw new IllegalArgumentException(
- "No 'other' plural variant defined for 10^" + log10Value
- + " but a '" + entry.getKey() + "' variant is defined"
- + " in " +localeAndStyle(locale, style));
- }
- }
- }
- }
-
- /**
- * After reading information from resource bundle into a Data object, there
- * is guarantee that it is complete.
- *
- * This method fixes any incomplete data it finds within <code>result</code>.
- * It looks at each log10 value applying the two rules.
- * <p>
- * If no prefix is defined for the "other" variant, use the divisor, prefixes and
- * suffixes for all defined variants from the previous log10. For log10 = 0,
- * use all empty prefixes and suffixes and a divisor of 1.
- * </p><p>
- * Otherwise, examine each plural variant defined for the given log10 value.
- * If it has no prefix and suffix for a particular variant, use the one from the
- * "other" variant.
- * </p>
- *
- * @param result this instance is fixed in-place.
- */
- private static void fillInMissing(Data result) {
- // Initially we assume that previous divisor is 1 with no prefix or suffix.
- long lastDivisor = 1L;
- for (int i = 0; i < result.divisors.length; i++) {
- if (result.units.get(OTHER)[i] == null) {
- result.divisors[i] = lastDivisor;
- copyFromPreviousIndex(i, result.units);
- } else {
- lastDivisor = result.divisors[i];
- propagateOtherToMissing(i, result.units);
- }
- }
- }
-
- private static void propagateOtherToMissing(
- int idx, Map<String, DecimalFormat.Unit[]> units) {
- DecimalFormat.Unit otherVariantValue = units.get(OTHER)[idx];
- for (DecimalFormat.Unit[] byBase : units.values()) {
- if (byBase[idx] == null) {
- byBase[idx] = otherVariantValue;
- }
- }
- }
-
- private static void copyFromPreviousIndex(int idx, Map<String, DecimalFormat.Unit[]> units) {
- for (DecimalFormat.Unit[] byBase : units.values()) {
- if (idx == 0) {
- byBase[idx] = DecimalFormat.NULL_UNIT;
- } else {
- byBase[idx] = byBase[idx - 1];
- }
- }
- }
-
- private static boolean saveUnit(
- DecimalFormat.Unit unit, String pluralVariant, int idx,
- Map<String, DecimalFormat.Unit[]> units,
- boolean overwrite) {
- DecimalFormat.Unit[] byBase = units.get(pluralVariant);
- if (byBase == null) {
- byBase = new DecimalFormat.Unit[MAX_DIGITS];
- units.put(pluralVariant, byBase);
- }
-
- // Don't overwrite a pre-existing value unless the "overwrite" flag is true.
- if (!overwrite && byBase[idx] != null) {
- return false;
- }
-
- // Save the value and return
- byBase[idx] = unit;
- return true;
- }
-
- /**
- * Fetches a prefix or suffix given a plural variant and log10 value. If it
- * can't find the given variant, it falls back to "other".
- * @param prefixOrSuffix the prefix or suffix map
- * @param variant the plural variant
- * @param base log10 value. 0 <= base < MAX_DIGITS.
- * @return the prefix or suffix.
- */
- static DecimalFormat.Unit getUnit(
- Map<String, DecimalFormat.Unit[]> units, String variant, int base) {
- DecimalFormat.Unit[] byBase = units.get(variant);
- if (byBase == null) {
- byBase = units.get(CompactDecimalDataCache.OTHER);
- }
- return byBase[base];
- }
-}
diff --git a/android_icu4j/src/main/java/android/icu/text/CompactDecimalFormat.java b/android_icu4j/src/main/java/android/icu/text/CompactDecimalFormat.java
index b928ebdf9..ed6d6c5b9 100644
--- a/android_icu4j/src/main/java/android/icu/text/CompactDecimalFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/CompactDecimalFormat.java
@@ -10,554 +10,116 @@
package android.icu.text;
-import java.io.IOException;
-import java.io.NotSerializableException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.math.BigDecimal;
-import java.math.BigInteger;
-import java.text.AttributedCharacterIterator;
-import java.text.FieldPosition;
import java.text.ParsePosition;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.regex.Pattern;
-import android.icu.text.CompactDecimalDataCache.Data;
-import android.icu.text.PluralRules.FixedDecimal;
-import android.icu.util.Currency;
+import android.icu.impl.number.PatternString;
+import android.icu.impl.number.Properties;
import android.icu.util.CurrencyAmount;
-import android.icu.util.Output;
import android.icu.util.ULocale;
/**
- * The CompactDecimalFormat produces abbreviated numbers, suitable for display in environments will limited real estate.
- * For example, 'Hits: 1.2B' instead of 'Hits: 1,200,000,000'. The format will be appropriate for the given language,
- * such as "1,2 Mrd." for German.
- * <p>
- * For numbers under 1000 trillion (under 10^15, such as 123,456,789,012,345), the result will be short for supported
- * languages. However, the result may sometimes exceed 7 characters, such as when there are combining marks or thin
- * characters. In such cases, the visual width in fonts should still be short.
- * <p>
- * By default, there are 2 significant digits. After creation, if more than three significant digits are set (with
- * setMaximumSignificantDigits), or if a fixed number of digits are set (with setMaximumIntegerDigits or
- * setMaximumFractionDigits), then result may be wider.
- * <p>
- * The "short" style is also capable of formatting currency amounts, such as "$1.2M" instead of "$1,200,000.00" (English) or
- * "5,3 Mio. €" instead of "5.300.000,00 €" (German). Localized data concerning longer formats is not available yet in
- * the Unicode CLDR. Because of this, attempting to format a currency amount using the "long" style will produce
- * an UnsupportedOperationException.
+ * The CompactDecimalFormat produces abbreviated numbers, suitable for display in environments will
+ * limited real estate. For example, 'Hits: 1.2B' instead of 'Hits: 1,200,000,000'. The format will
+ * be appropriate for the given language, such as "1,2 Mrd." for German.
*
- * At this time, negative numbers and parsing are not supported, and will produce an UnsupportedOperationException.
- * Resetting the pattern prefixes or suffixes is not supported; the method calls are ignored.
- * <p>
- * Note that important methods, like setting the number of decimals, will be moved up from DecimalFormat to
- * NumberFormat.
+ * <p>For numbers under 1000 trillion (under 10^15, such as 123,456,789,012,345), the result will be
+ * short for supported languages. However, the result may sometimes exceed 7 characters, such as
+ * when there are combining marks or thin characters. In such cases, the visual width in fonts
+ * should still be short.
+ *
+ * <p>By default, there are 2 significant digits. After creation, if more than three significant
+ * digits are set (with setMaximumSignificantDigits), or if a fixed number of digits are set (with
+ * setMaximumIntegerDigits or setMaximumFractionDigits), then result may be wider.
+ *
+ * <p>The "short" style is also capable of formatting currency amounts, such as "$1.2M" instead of
+ * "$1,200,000.00" (English) or "5,3 Mio. €" instead of "5.300.000,00 €" (German). Localized data
+ * concerning longer formats is not available yet in the Unicode CLDR. Because of this, attempting
+ * to format a currency amount using the "long" style will produce an UnsupportedOperationException.
+ *
+ * <p>At this time, negative numbers and parsing are not supported, and will produce an
+ * UnsupportedOperationException. Resetting the pattern prefixes or suffixes is not supported; the
+ * method calls are ignored.
+ *
+ * <p>Note that important methods, like setting the number of decimals, will be moved up from
+ * DecimalFormat to NumberFormat.
*
* @author markdavis
*/
public class CompactDecimalFormat extends DecimalFormat {
- private static final long serialVersionUID = 4716293295276629682L;
-
-// private static final int POSITIVE_PREFIX = 0, POSITIVE_SUFFIX = 1, AFFIX_SIZE = 2;
- private static final CompactDecimalDataCache cache = new CompactDecimalDataCache();
-
- private final Map<String, DecimalFormat.Unit[]> units;
- private final Map<String, DecimalFormat.Unit[]> currencyUnits;
- private final long[] divisor;
- private final long[] currencyDivisor;
- private final Map<String, Unit> pluralToCurrencyAffixes;
- private CompactStyle style;
-
- // null if created internally using explicit prefixes and suffixes.
- private final PluralRules pluralRules;
-
- /**
- * Style parameter for CompactDecimalFormat.
- */
- public enum CompactStyle {
- /**
- * Short version, like "1.2T"
- */
- SHORT,
- /**
- * Longer version, like "1.2 trillion", if available. May return same result as SHORT if not.
- */
- LONG
- }
-
- /**
- * Create a CompactDecimalFormat appropriate for a locale. The result may
- * be affected by the number system in the locale, such as ar-u-nu-latn.
- *
- * @param locale the desired locale
- * @param style the compact style
- */
- public static CompactDecimalFormat getInstance(ULocale locale, CompactStyle style) {
- return new CompactDecimalFormat(locale, style);
- }
-
- /**
- * Create a CompactDecimalFormat appropriate for a locale. The result may
- * be affected by the number system in the locale, such as ar-u-nu-latn.
- *
- * @param locale the desired locale
- * @param style the compact style
- */
- public static CompactDecimalFormat getInstance(Locale locale, CompactStyle style) {
- return new CompactDecimalFormat(ULocale.forLocale(locale), style);
- }
-
- /**
- * The public mechanism is CompactDecimalFormat.getInstance().
- *
- * @param locale
- * the desired locale
- * @param style
- * the compact style
- */
- CompactDecimalFormat(ULocale locale, CompactStyle style) {
- this.pluralRules = PluralRules.forLocale(locale);
- DecimalFormat format = (DecimalFormat) NumberFormat.getInstance(locale);
- CompactDecimalDataCache.Data data = getData(locale, style);
- CompactDecimalDataCache.Data currencyData = getCurrencyData(locale);
- this.units = data.units;
- this.divisor = data.divisors;
- this.currencyUnits = currencyData.units;
- this.currencyDivisor = currencyData.divisors;
- this.style = style;
- pluralToCurrencyAffixes = null;
-
-// DecimalFormat currencyFormat = (DecimalFormat) NumberFormat.getCurrencyInstance(locale);
-// // TODO fix to use plural-dependent affixes
-// Unit currency = new Unit(currencyFormat.getPositivePrefix(), currencyFormat.getPositiveSuffix());
-// pluralToCurrencyAffixes = new HashMap<String,Unit>();
-// for (String key : pluralRules.getKeywords()) {
-// pluralToCurrencyAffixes.put(key, currency);
-// }
-// // TODO fix to get right symbol for the count
-
- finishInit(style, format.toPattern(), format.getDecimalFormatSymbols());
- }
-
- /**
- * Create a short number "from scratch". Intended for internal use. The prefix, suffix, and divisor arrays are
- * parallel, and provide the information for each power of 10. When formatting a value, the correct power of 10 is
- * found, then the value is divided by the divisor, and the prefix and suffix are set (using
- * setPositivePrefix/Suffix).
- *
- * @param pattern
- * A number format pattern. Note that the prefix and suffix are discarded, and the decimals are
- * overridden by default.
- * @param formatSymbols
- * Decimal format symbols, typically from a locale.
- * @param style
- * compact style.
- * @param divisor
- * An array of prefix values, one for each power of 10 from 0 to 14
- * @param pluralAffixes
- * A map from plural categories to affixes.
- * @param currencyAffixes
- * A map from plural categories to currency affixes.
- * @param debugCreationErrors
- * A collection of strings for debugging. If null on input, then any errors found will be added to that
- * collection instead of throwing exceptions.
- * @deprecated This API is ICU internal only.
- * @hide draft / provisional / internal are hidden on Android
- */
- @Deprecated
- public CompactDecimalFormat(String pattern, DecimalFormatSymbols formatSymbols,
- CompactStyle style, PluralRules pluralRules,
- long[] divisor, Map<String,String[][]> pluralAffixes, Map<String, String[]> currencyAffixes,
- Collection<String> debugCreationErrors) {
-
- this.pluralRules = pluralRules;
- this.units = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors);
- this.currencyUnits = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors);
- if (!pluralRules.getKeywords().equals(this.units.keySet())) {
- debugCreationErrors.add("Missmatch in pluralCategories, should be: " + pluralRules.getKeywords() + ", was actually " + this.units.keySet());
- }
- this.divisor = divisor.clone();
- this.currencyDivisor = divisor.clone();
- if (currencyAffixes == null) {
- pluralToCurrencyAffixes = null;
- } else {
- pluralToCurrencyAffixes = new HashMap<String,Unit>();
- for (Entry<String, String[]> s : currencyAffixes.entrySet()) {
- String[] pair = s.getValue();
- pluralToCurrencyAffixes.put(s.getKey(), new Unit(pair[0], pair[1]));
- }
- }
- finishInit(style, pattern, formatSymbols);
- }
-
- private void finishInit(CompactStyle style, String pattern, DecimalFormatSymbols formatSymbols) {
- applyPattern(pattern);
- setDecimalFormatSymbols(formatSymbols);
- setMaximumSignificantDigits(2); // default significant digits
- setSignificantDigitsUsed(true);
- if (style == CompactStyle.SHORT) {
- setGroupingUsed(false);
- }
- setCurrency(null);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean equals(Object obj) {
- if (obj == null)
- return false;
- if (!super.equals(obj))
- return false; // super does class check
- CompactDecimalFormat other = (CompactDecimalFormat) obj;
- return mapsAreEqual(units, other.units)
- && Arrays.equals(divisor, other.divisor)
- && (pluralToCurrencyAffixes == other.pluralToCurrencyAffixes
- || pluralToCurrencyAffixes != null && pluralToCurrencyAffixes.equals(other.pluralToCurrencyAffixes))
- && pluralRules.equals(other.pluralRules);
- }
-
- private boolean mapsAreEqual(
- Map<String, DecimalFormat.Unit[]> lhs, Map<String, DecimalFormat.Unit[]> rhs) {
- if (lhs.size() != rhs.size()) {
- return false;
- }
- // For each MapEntry in lhs, see if there is a matching one in rhs.
- for (Map.Entry<String, DecimalFormat.Unit[]> entry : lhs.entrySet()) {
- DecimalFormat.Unit[] value = rhs.get(entry.getKey());
- if (value == null || !Arrays.equals(entry.getValue(), value)) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
- return format(number, null, toAppendTo, pos);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
- if (!(obj instanceof Number)) {
- throw new IllegalArgumentException();
- }
- Number number = (Number) obj;
- Amount amount = toAmount(number.doubleValue(), null, null);
- return super.formatToCharacterIterator(amount.getQty(), amount.getUnit());
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
- return format((double) number, toAppendTo, pos);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) {
- return format(number.doubleValue(), toAppendTo, pos);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
- return format(number.doubleValue(), toAppendTo, pos);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public StringBuffer format(android.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
- return format(number.doubleValue(), toAppendTo, pos);
- }
- /**
- * {@inheritDoc}
- * @deprecated This API might change or be removed in a future release.
- * @hide draft / provisional / internal are hidden on Android
- */
- @Override
- @Deprecated
- public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) {
- return format(currAmt.getNumber().doubleValue(), currAmt.getCurrency(), toAppendTo, pos);
- }
-
- /**
- * Parsing is currently unsupported, and throws an UnsupportedOperationException.
- */
- @Override
- public Number parse(String text, ParsePosition parsePosition) {
- throw new UnsupportedOperationException();
- }
-
- // DISALLOW Serialization, at least while draft
-
- private void writeObject(ObjectOutputStream out) throws IOException {
- throw new NotSerializableException();
- }
-
- private void readObject(ObjectInputStream in) throws IOException {
- throw new NotSerializableException();
- }
-
- /* INTERNALS */
- private StringBuffer format(double number, Currency curr, StringBuffer toAppendTo, FieldPosition pos) {
- if (curr != null && style == CompactStyle.LONG) {
- throw new UnsupportedOperationException("CompactDecimalFormat does not support LONG style for currency.");
- }
-
- // Compute the scaled amount, prefix, and suffix appropriate for the number's magnitude.
- Output<Unit> currencyUnit = new Output<Unit>();
- Amount amount = toAmount(number, curr, currencyUnit);
- Unit unit = amount.getUnit();
-
- // Note that currencyUnit is a remnant. In almost all cases, it will be null.
- StringBuffer prefix = new StringBuffer();
- StringBuffer suffix = new StringBuffer();
- if (currencyUnit.value != null) {
- currencyUnit.value.writePrefix(prefix);
- }
- unit.writePrefix(prefix);
- unit.writeSuffix(suffix);
- if (currencyUnit.value != null) {
- currencyUnit.value.writeSuffix(suffix);
- }
-
- if (curr == null) {
- // Prevent locking when not formatting a currency number.
- toAppendTo.append(escape(prefix.toString()));
- super.format(amount.getQty(), toAppendTo, pos);
- toAppendTo.append(escape(suffix.toString()));
-
- } else {
- // To perform the formatting, we set this DecimalFormat's pattern to have the correct prefix, suffix,
- // and currency, and then reset it back to what it was before.
- // This has to be synchronized since this information is held in the state of the DecimalFormat object.
- synchronized(this) {
-
- String originalPattern = this.toPattern();
- Currency originalCurrency = this.getCurrency();
- StringBuffer newPattern = new StringBuffer();
-
- // Write prefixes and suffixes to the pattern. Note that we have to apply it to both halves of a
- // positive/negative format (separated by ';')
- int semicolonPos = originalPattern.indexOf(';');
- newPattern.append(prefix);
- if (semicolonPos != -1) {
- newPattern.append(originalPattern, 0, semicolonPos);
- newPattern.append(suffix);
- newPattern.append(';');
- newPattern.append(prefix);
- }
- newPattern.append(originalPattern, semicolonPos + 1, originalPattern.length());
- newPattern.append(suffix);
-
- // Overwrite the pattern and currency.
- setCurrency(curr);
- applyPattern(newPattern.toString());
-
- // Actually perform the formatting.
- super.format(amount.getQty(), toAppendTo, pos);
-
- // Reset the pattern and currency.
- setCurrency(originalCurrency);
- applyPattern(originalPattern);
- }
- }
- return toAppendTo;
- }
-
- private static final Pattern UNESCAPE_QUOTE = Pattern.compile("((?<!'))'");
-
- private static String escape(String string) {
- if (string.indexOf('\'') >= 0) {
- return UNESCAPE_QUOTE.matcher(string).replaceAll("$1");
- }
- return string;
- }
-
- private Amount toAmount(double number, Currency curr, Output<Unit> currencyUnit) {
- // We do this here so that the prefix or suffix we choose is always consistent
- // with the rounding we do. This way, 999999 -> 1M instead of 1000K.
- boolean negative = isNumberNegative(number);
- number = adjustNumberAsInFormatting(number);
- int base = number <= 1.0d ? 0 : (int) Math.log10(number);
- if (base >= CompactDecimalDataCache.MAX_DIGITS) {
- base = CompactDecimalDataCache.MAX_DIGITS - 1;
- }
- if (curr != null) {
- number /= currencyDivisor[base];
- } else {
- number /= divisor[base];
- }
- String pluralVariant = getPluralForm(getFixedDecimal(number, toDigitList(number)));
- if (pluralToCurrencyAffixes != null && currencyUnit != null) {
- currencyUnit.value = pluralToCurrencyAffixes.get(pluralVariant);
- }
- if (negative) {
- number = -number;
- }
- if ( curr != null ) {
- return new Amount(number, CompactDecimalDataCache.getUnit(currencyUnits, pluralVariant, base));
- } else {
- return new Amount(number, CompactDecimalDataCache.getUnit(units, pluralVariant, base));
- }
- }
-
- private void recordError(Collection<String> creationErrors, String errorMessage) {
- if (creationErrors == null) {
- throw new IllegalArgumentException(errorMessage);
- }
- creationErrors.add(errorMessage);
- }
-
- /**
- * Manufacture the unit list from arrays
- */
- private Map<String, DecimalFormat.Unit[]> otherPluralVariant(Map<String, String[][]> pluralCategoryToPower10ToAffix,
- long[] divisor, Collection<String> debugCreationErrors) {
-
- // check for bad divisors
- if (divisor.length < CompactDecimalDataCache.MAX_DIGITS) {
- recordError(debugCreationErrors, "Must have at least " + CompactDecimalDataCache.MAX_DIGITS + " prefix items.");
- }
- long oldDivisor = 0;
- for (int i = 0; i < divisor.length; ++i) {
-
- // divisor must be a power of 10, and must be less than or equal to 10^i
- int log = (int) Math.log10(divisor[i]);
- if (log > i) {
- recordError(debugCreationErrors, "Divisor[" + i + "] must be less than or equal to 10^" + i
- + ", but is: " + divisor[i]);
- }
- long roundTrip = (long) Math.pow(10.0d, log);
- if (roundTrip != divisor[i]) {
- recordError(debugCreationErrors, "Divisor[" + i + "] must be a power of 10, but is: " + divisor[i]);
- }
-
- if (divisor[i] < oldDivisor) {
- recordError(debugCreationErrors, "Bad divisor, the divisor for 10E" + i + "(" + divisor[i]
- + ") is less than the divisor for the divisor for 10E" + (i - 1) + "(" + oldDivisor + ")");
- }
- oldDivisor = divisor[i];
- }
-
- Map<String, DecimalFormat.Unit[]> result = new HashMap<String, DecimalFormat.Unit[]>();
- Map<String,Integer> seen = new HashMap<String,Integer>();
-
- String[][] defaultPower10ToAffix = pluralCategoryToPower10ToAffix.get("other");
-
- for (Entry<String, String[][]> pluralCategoryAndPower10ToAffix : pluralCategoryToPower10ToAffix.entrySet()) {
- String pluralCategory = pluralCategoryAndPower10ToAffix.getKey();
- String[][] power10ToAffix = pluralCategoryAndPower10ToAffix.getValue();
-
- // we can't have one of the arrays be of different length
- if (power10ToAffix.length != divisor.length) {
- recordError(debugCreationErrors, "Prefixes & suffixes must be present for all divisors " + pluralCategory);
- }
- DecimalFormat.Unit[] units = new DecimalFormat.Unit[power10ToAffix.length];
- for (int i = 0; i < power10ToAffix.length; i++) {
- String[] pair = power10ToAffix[i];
- if (pair == null) {
- pair = defaultPower10ToAffix[i];
- }
-
- // we can't have bad pair
- if (pair.length != 2 || pair[0] == null || pair[1] == null) {
- recordError(debugCreationErrors, "Prefix or suffix is null for " + pluralCategory + ", " + i + ", " + Arrays.asList(pair));
- continue;
- }
-
- // we can't have two different indexes with the same display
- int log = (int) Math.log10(divisor[i]);
- String key = pair[0] + "\uFFFF" + pair[1] + "\uFFFF" + (i - log);
- Integer old = seen.get(key);
- if (old == null) {
- seen.put(key, i);
- } else if (old != i) {
- recordError(debugCreationErrors, "Collision between values for " + i + " and " + old
- + " for [prefix/suffix/index-log(divisor)" + key.replace('\uFFFF', ';'));
- }
-
- units[i] = new Unit(pair[0], pair[1]);
- }
- result.put(pluralCategory, units);
- }
- return result;
- }
-
- private String getPluralForm(FixedDecimal fixedDecimal) {
- if (pluralRules == null) {
- return CompactDecimalDataCache.OTHER;
- }
- return pluralRules.select(fixedDecimal);
- }
-
- /**
- * Gets the data for a particular locale and style. If style is unrecognized,
- * we just return data for CompactStyle.SHORT.
- * @param locale The locale.
- * @param style The style.
- * @return The data which must not be modified.
- */
- private Data getData(ULocale locale, CompactStyle style) {
- CompactDecimalDataCache.DataBundle bundle = cache.get(locale);
- switch (style) {
- case SHORT:
- return bundle.shortData;
- case LONG:
- return bundle.longData;
- default:
- return bundle.shortData;
- }
- }
- /**
- * Gets the currency data for a particular locale.
- * Currently only short currency format is supported, since that is
- * the only form in CLDR.
- * @param locale The locale.
- * @return The data which must not be modified.
- */
- private Data getCurrencyData(ULocale locale) {
- CompactDecimalDataCache.DataBundle bundle = cache.get(locale);
- return bundle.shortCurrencyData;
- }
-
- private static class Amount {
- private final double qty;
- private final Unit unit;
-
- public Amount(double qty, Unit unit) {
- this.qty = qty;
- this.unit = unit;
- }
-
- public double getQty() {
- return qty;
- }
-
- public Unit getUnit() {
- return unit;
- }
- }
+ private static final long serialVersionUID = 4716293295276629682L;
+
+ /**
+ * Style parameter for CompactDecimalFormat.
+ */
+ public enum CompactStyle {
+ /**
+ * Short version, like "1.2T"
+ */
+ SHORT,
+ /**
+ * Longer version, like "1.2 trillion", if available. May return same result as SHORT if not.
+ */
+ LONG
+ }
+
+ /**
+ * Creates a CompactDecimalFormat appropriate for a locale. The result may be affected by the
+ * number system in the locale, such as ar-u-nu-latn.
+ *
+ * @param locale the desired locale
+ * @param style the compact style
+ */
+ public static CompactDecimalFormat getInstance(ULocale locale, CompactStyle style) {
+ return new CompactDecimalFormat(locale, style);
+ }
+
+ /**
+ * Creates a CompactDecimalFormat appropriate for a locale. The result may be affected by the
+ * number system in the locale, such as ar-u-nu-latn.
+ *
+ * @param locale the desired locale
+ * @param style the compact style
+ */
+ public static CompactDecimalFormat getInstance(Locale locale, CompactStyle style) {
+ return new CompactDecimalFormat(ULocale.forLocale(locale), style);
+ }
+
+ /**
+ * The public mechanism is CompactDecimalFormat.getInstance().
+ *
+ * @param locale the desired locale
+ * @param style the compact style
+ */
+ CompactDecimalFormat(ULocale locale, CompactStyle style) {
+ // Use the locale's default pattern
+ String pattern = getPattern(locale, 0);
+ symbols = DecimalFormatSymbols.getInstance(locale);
+ properties = new Properties();
+ properties.setCompactStyle(style);
+ exportedProperties = new Properties();
+ setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_ALWAYS);
+ if (style == CompactStyle.SHORT) {
+ // TODO: This was setGroupingUsed(false) in ICU 58. Is it okay that I changed it for ICU 59?
+ properties.setMinimumGroupingDigits(2);
+ }
+ refreshFormatter();
+ }
+
+ /**
+ * Parsing is currently unsupported, and throws an UnsupportedOperationException.
+ */
+ @Override
+ public Number parse(String text, ParsePosition parsePosition) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Parsing is currently unsupported, and throws an UnsupportedOperationException.
+ */
+ @Override
+ public CurrencyAmount parseCurrency(CharSequence text, ParsePosition parsePosition) {
+ throw new UnsupportedOperationException();
+ }
}
diff --git a/android_icu4j/src/main/java/android/icu/text/CurrencyPluralInfo.java b/android_icu4j/src/main/java/android/icu/text/CurrencyPluralInfo.java
index a5f27efef..eaf2ccce1 100644
--- a/android_icu4j/src/main/java/android/icu/text/CurrencyPluralInfo.java
+++ b/android_icu4j/src/main/java/android/icu/text/CurrencyPluralInfo.java
@@ -137,7 +137,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
}
/**
- * Set plural rules. These are initially set in the constructor based on the locale,
+ * Set plural rules. These are initially set in the constructor based on the locale,
* and usually do not need to be changed.
*
* @param ruleDescription new plural rule description
@@ -150,6 +150,10 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
* Set currency plural patterns. These are initially set in the constructor based on the
* locale, and usually do not need to be changed.
*
+ * The decimal digits part of the pattern cannot be specified via this method. All plural
+ * forms will use the same decimal pattern as set in the constructor of DecimalFormat. For
+ * example, you can't set "0.0" for plural "few" but "0.00" for plural "many".
+ *
* @param pluralCount the plural count for which the currency pattern will
* be overridden.
* @param pattern the new currency plural pattern
@@ -172,6 +176,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
/**
* Standard override
*/
+ @Override
public Object clone() {
try {
CurrencyPluralInfo other = (CurrencyPluralInfo) super.clone();
@@ -195,6 +200,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
/**
* Override equals
*/
+ @Override
public boolean equals(Object a) {
if (a instanceof CurrencyPluralInfo) {
CurrencyPluralInfo other = (CurrencyPluralInfo)a;
@@ -203,18 +209,20 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
}
return false;
}
-
+
/**
- * Mock implementation of hashCode(). This implementation always returns a constant
- * value. When Java assertion is enabled, this method triggers an assertion failure.
+ * Override hashCode
+ *
* @deprecated This API is ICU internal only.
* @hide original deprecated declaration
* @hide draft / provisional / internal are hidden on Android
*/
+ @Override
@Deprecated
public int hashCode() {
- assert false : "hashCode not designed";
- return 42;
+ return pluralCountToCurrencyUnitPattern.hashCode()
+ ^ pluralRules.hashCode()
+ ^ ulocale.hashCode();
}
/**
@@ -256,7 +264,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
private void setupCurrencyPluralPattern(ULocale uloc) {
pluralCountToCurrencyUnitPattern = new HashMap<String, String>();
-
+
String numberStylePattern = NumberFormat.getPattern(uloc, NumberFormat.NUMBERSTYLE);
// Split the number style pattern into pos and neg if applicable
int separatorIndex = numberStylePattern.indexOf(";");
@@ -269,7 +277,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
for (Map.Entry<String, String> e : map.entrySet()) {
String pluralCount = e.getKey();
String pattern = e.getValue();
-
+
// replace {0} with numberStylePattern
// and {1} with triple currency sign
String patternWithNumber = pattern.replace("{0}", numberStylePattern);
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 53b7d442e..ae3bd5909 100644
--- a/android_icu4j/src/main/java/android/icu/text/DateFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/DateFormat.java
@@ -418,17 +418,15 @@ public abstract class DateFormat extends UFormat {
* <strong>[icu]</strong> FieldPosition selector for 'b' field alignment.
* No related Calendar field.
* This displays the fixed day period (am/pm/midnight/noon).
- * @hide draft / provisional / internal are hidden on Android
*/
- final static int AM_PM_MIDNIGHT_NOON_FIELD = 35;
+ public final static int AM_PM_MIDNIGHT_NOON_FIELD = 35;
/**
* <strong>[icu]</strong> FieldPosition selector for 'B' field alignment.
* No related Calendar field.
* This displays the flexible day period.
- * @hide draft / provisional / internal are hidden on Android
*/
- final static int FLEXIBLE_DAY_PERIOD_FIELD = 36;
+ public final static int FLEXIBLE_DAY_PERIOD_FIELD = 36;
/**
* <strong>[icu]</strong> FieldPosition selector time separator,
@@ -2268,13 +2266,11 @@ public abstract class DateFormat extends UFormat {
/**
* <strong>[icu]</strong> Constant identifying the am/pm/midnight/noon field.
- * @hide draft / provisional / internal are hidden on Android
*/
public static final Field AM_PM_MIDNIGHT_NOON = new Field("am/pm/midnight/noon", -1);
/**
* <strong>[icu]</strong> Constant identifying the flexible day period field.
- * @hide draft / provisional / internal are hidden on Android
*/
public static final Field FLEXIBLE_DAY_PERIOD = new Field("flexible day period", -1);
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 57c590088..431b90a28 100644
--- a/android_icu4j/src/main/java/android/icu/text/DateFormatSymbols.java
+++ b/android_icu4j/src/main/java/android/icu/text/DateFormatSymbols.java
@@ -2119,57 +2119,7 @@ public class DateFormatSymbols implements Serializable, Cloneable {
/**
* Returns the {@link DateFormatSymbols} object that should be used to format a
* calendar system's dates in the given locale.
- * <p>
- * <b>Subclassing:</b><br>
- * When creating a new Calendar subclass, you must create the
- * {@link ResourceBundle ResourceBundle}
- * containing its {@link DateFormatSymbols DateFormatSymbols} in a specific place.
- * The resource bundle name is based on the calendar's fully-specified
- * class name, with ".resources" inserted at the end of the package name
- * (just before the class name) and "Symbols" appended to the end.
- * For example, the bundle corresponding to "android.icu.util.HebrewCalendar"
- * is "android.icu.impl.data.HebrewCalendarSymbols".
- * <p>
- * Within the ResourceBundle, this method searches for five keys:
- * <ul>
- * <li><b>DayNames</b> -
- * An array of strings corresponding to each possible
- * value of the <code>DAY_OF_WEEK</code> field. Even though
- * <code>DAY_OF_WEEK</code> starts with <code>SUNDAY</code> = 1,
- * This array is 0-based; the name for Sunday goes in the
- * first position, at index 0. If this key is not found
- * in the bundle, the day names are inherited from the
- * default <code>DateFormatSymbols</code> for the requested locale.
- *
- * <li><b>DayAbbreviations</b> -
- * An array of abbreviated day names corresponding
- * to the values in the "DayNames" array. If this key
- * is not found in the resource bundle, the "DayNames"
- * values are used instead. If neither key is found,
- * the day abbreviations are inherited from the default
- * <code>DateFormatSymbols</code> for the locale.
- *
- * <li><b>MonthNames</b> -
- * An array of strings corresponding to each possible
- * value of the <code>MONTH</code> field. If this key is not found
- * in the bundle, the month names are inherited from the
- * default <code>DateFormatSymbols</code> for the requested locale.
- *
- * <li><b>MonthAbbreviations</b> -
- * An array of abbreviated day names corresponding
- * to the values in the "MonthNames" array. If this key
- * is not found in the resource bundle, the "MonthNames"
- * values are used instead. If neither key is found,
- * the day abbreviations are inherited from the default
- * <code>DateFormatSymbols</code> for the locale.
*
- * <li><b>Eras</b> -
- * An array of strings corresponding to each possible
- * value of the <code>ERA</code> field. If this key is not found
- * in the bundle, the era names are inherited from the
- * default <code>DateFormatSymbols</code> for the requested locale.
- * </ul>
- * <p>
* @param cal The calendar system whose date format symbols are desired.
* @param locale The locale whose symbols are desired.
*
@@ -2182,57 +2132,6 @@ public class DateFormatSymbols implements Serializable, Cloneable {
/**
* Returns the {@link DateFormatSymbols} object that should be used to format a
* calendar system's dates in the given locale.
- * <p>
- * <b>Subclassing:</b><br>
- * When creating a new Calendar subclass, you must create the
- * {@link ResourceBundle ResourceBundle}
- * containing its {@link DateFormatSymbols DateFormatSymbols} in a specific place.
- * The resource bundle name is based on the calendar's fully-specified
- * class name, with ".resources" inserted at the end of the package name
- * (just before the class name) and "Symbols" appended to the end.
- * For example, the bundle corresponding to "android.icu.util.HebrewCalendar"
- * is "android.icu.impl.data.HebrewCalendarSymbols".
- * <p>
- * Within the ResourceBundle, this method searches for five keys:
- * <ul>
- * <li><b>DayNames</b> -
- * An array of strings corresponding to each possible
- * value of the <code>DAY_OF_WEEK</code> field. Even though
- * <code>DAY_OF_WEEK</code> starts with <code>SUNDAY</code> = 1,
- * This array is 0-based; the name for Sunday goes in the
- * first position, at index 0. If this key is not found
- * in the bundle, the day names are inherited from the
- * default <code>DateFormatSymbols</code> for the requested locale.
- *
- * <li><b>DayAbbreviations</b> -
- * An array of abbreviated day names corresponding
- * to the values in the "DayNames" array. If this key
- * is not found in the resource bundle, the "DayNames"
- * values are used instead. If neither key is found,
- * the day abbreviations are inherited from the default
- * <code>DateFormatSymbols</code> for the locale.
- *
- * <li><b>MonthNames</b> -
- * An array of strings corresponding to each possible
- * value of the <code>MONTH</code> field. If this key is not found
- * in the bundle, the month names are inherited from the
- * default <code>DateFormatSymbols</code> for the requested locale.
- *
- * <li><b>MonthAbbreviations</b> -
- * An array of abbreviated day names corresponding
- * to the values in the "MonthNames" array. If this key
- * is not found in the resource bundle, the "MonthNames"
- * values are used instead. If neither key is found,
- * the day abbreviations are inherited from the default
- * <code>DateFormatSymbols</code> for the locale.
- *
- * <li><b>Eras</b> -
- * An array of strings corresponding to each possible
- * value of the <code>ERA</code> field. If this key is not found
- * in the bundle, the era names are inherited from the
- * default <code>DateFormatSymbols</code> for the requested locale.
- * </ul>
- * <p>
* @param cal The calendar system whose date format symbols are desired.
* @param locale The ulocale whose symbols are desired.
*
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 3e1c818ab..ec815d971 100644
--- a/android_icu4j/src/main/java/android/icu/text/DateIntervalFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/DateIntervalFormat.java
@@ -865,8 +865,13 @@ public class DateIntervalFormat extends UFormat {
otherPos.setEndIndex(0);
datePortion = fDateFormat.format(fromCalendar, datePortion, otherPos);
adjustPosition(fDateTimeFormat, fallbackRange, pos, datePortion.toString(), otherPos, pos);
- fallbackRange = SimpleFormatterImpl.formatRawPattern(
- fDateTimeFormat, 2, 2, fallbackRange, datePortion);
+ // Android patch (CLDR ticket #10321) begin.
+ MessageFormat msgFmt = new MessageFormat("");
+ msgFmt.applyPattern(fDateTimeFormat, MessagePattern.ApostropheMode.DOUBLE_REQUIRED);
+ StringBuffer fallbackRangeBuffer = new StringBuffer(128);
+ fallbackRange = msgFmt.format(new Object[] { fallbackRange, datePortion },
+ fallbackRangeBuffer, new FieldPosition(0)).toString();
+ // Android patch (CLDR ticket #10321) end.
}
appendTo.append(fallbackRange);
if (formatDatePlusTimeRange) {
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 fa9d0cbf5..d4a421648 100644
--- a/android_icu4j/src/main/java/android/icu/text/DecimalFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/DecimalFormat.java
@@ -1,36 +1,33 @@
/* GENERATED SOURCE. DO NOT MODIFY. */
-// © 2016 and later: Unicode, Inc. and others.
+// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
-/*
- *******************************************************************************
- * Copyright (C) 1996-2016, International Business Machines Corporation and
- * others. All Rights Reserved.
- *******************************************************************************
- */
package android.icu.text;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
+import java.io.ObjectStreamField;
import java.math.BigInteger;
+import java.math.RoundingMode;
import java.text.AttributedCharacterIterator;
-import java.text.AttributedString;
-import java.text.ChoiceFormat;
import java.text.FieldPosition;
-import java.text.Format;
+import java.text.ParseException;
import java.text.ParsePosition;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Set;
-
-import android.icu.impl.ICUConfig;
-import android.icu.impl.PatternProps;
-import android.icu.impl.Utility;
+
+import android.icu.impl.number.Endpoint;
+import android.icu.impl.number.Format.SingularFormat;
+import android.icu.impl.number.FormatQuantity4;
+import android.icu.impl.number.Parse;
+import android.icu.impl.number.PatternString;
+import android.icu.impl.number.Properties;
+import android.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import android.icu.impl.number.formatters.PositiveDecimalFormat;
+import android.icu.impl.number.formatters.ScientificFormat;
+import android.icu.impl.number.rounders.SignificantDigitsRounder;
import android.icu.lang.UCharacter;
import android.icu.math.BigDecimal;
import android.icu.math.MathContext;
-import android.icu.text.PluralRules.FixedDecimal;
+import android.icu.text.PluralRules.IFixedDecimal;
import android.icu.util.Currency;
import android.icu.util.Currency.CurrencyUsage;
import android.icu.util.CurrencyAmount;
@@ -38,248 +35,100 @@ import android.icu.util.ULocale;
import android.icu.util.ULocale.Category;
/**
- * <strong>[icu enhancement]</strong> ICU's replacement for {@link java.text.DecimalFormat}.&nbsp;Methods, fields, and other functionality specific to ICU are labeled '<strong>[icu]</strong>'.
+ * <strong>[icu enhancement]</strong> ICU's replacement for {@link java.text.DecimalFormat}.&nbsp;Methods, fields, and other functionality specific to ICU are labeled '<strong>[icu]</strong>'. <code>DecimalFormat</code> is the primary
+ * concrete subclass of {@link NumberFormat}. It has a variety of features designed to make it
+ * possible to parse and format numbers in any locale, including support for Western, Arabic, or
+ * Indic digits. It supports different flavors of numbers, including integers ("123"), fixed-point
+ * numbers ("123.4"), scientific notation ("1.23E4"), percentages ("12%"), and currency amounts
+ * ("$123.00", "USD123.00", "123.00 US dollars"). All of these flavors can be easily localized.
+ *
+ * <p>To obtain a number formatter for a specific locale (including the default locale), call one of
+ * NumberFormat's factory methods such as {@link NumberFormat#getInstance}. Do not call
+ * DecimalFormat constructors directly unless you know what you are doing.
+ *
+ * <p>DecimalFormat aims to comply with the specification <a
+ * href="http://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns">UTS #35</a>. Read
+ * the specification for more information on how all the properties in DecimalFormat fit together.
+ *
+ * <h3>Example Usage</h3>
*
- * <code>DecimalFormat</code> is a concrete subclass of {@link NumberFormat} that formats
- * decimal numbers. It has a variety of features designed to make it possible to parse and
- * format numbers in any locale, including support for Western, Arabic, or Indic digits.
- * It also supports different flavors of numbers, including integers ("123"), fixed-point
- * numbers ("123.4"), scientific notation ("1.23E4"), percentages ("12%"), and currency
- * amounts ("$123.00", "USD123.00", "123.00 US dollars"). All of these flavors can be
- * easily localized.
+ * <p>Customize settings on a DecimalFormat instance from the NumberFormat factory:
*
- * <p>To obtain a {@link NumberFormat} for a specific locale (including the default
- * locale) call one of <code>NumberFormat</code>'s factory methods such as {@link
- * NumberFormat#getInstance}. Do not call the <code>DecimalFormat</code> constructors
- * directly, unless you know what you are doing, since the {@link NumberFormat} factory
- * methods may return subclasses other than <code>DecimalFormat</code>. If you need to
- * customize the format object, do something like this:
+ * <blockquote>
*
- * <blockquote><pre>
+ * <pre>
* NumberFormat f = NumberFormat.getInstance(loc);
* if (f instanceof DecimalFormat) {
* ((DecimalFormat) f).setDecimalSeparatorAlwaysShown(true);
- * }</pre></blockquote>
+ * ((DecimalFormat) f).setMinimumGroupingDigits(2);
+ * }
+ * </pre>
*
- * <p><strong>Example Usage</strong>
+ * </blockquote>
*
- * Print out a number using the localized number, currency, and percent
- * format for each locale.
+ * <p>Quick and dirty print out a number using the localized number, currency, and percent format
+ * for each locale:
*
- * <blockquote><pre>
- * Locale[] locales = NumberFormat.getAvailableLocales();
- * double myNumber = -1234.56;
- * NumberFormat format;
- * for (int j=0; j&lt;3; ++j) {
- * System.out.println("FORMAT");
- * for (int i = 0; i &lt; locales.length; ++i) {
- * if (locales[i].getCountry().length() == 0) {
- * // Skip language-only locales
- * continue;
- * }
- * System.out.print(locales[i].getDisplayName());
- * switch (j) {
- * case 0:
- * format = NumberFormat.getInstance(locales[i]); break;
- * case 1:
- * format = NumberFormat.getCurrencyInstance(locales[i]); break;
- * default:
- * format = NumberFormat.getPercentInstance(locales[i]); break;
- * }
- * try {
- * // Assume format is a DecimalFormat
- * System.out.print(": " + ((DecimalFormat) format).toPattern()
- * + " -&gt; " + form.format(myNumber));
- * } catch (Exception e) {}
- * try {
- * System.out.println(" -&gt; " + format.parse(form.format(myNumber)));
- * } catch (ParseException e) {}
- * }
- * }</pre></blockquote>
+ * <blockquote>
*
- * <p>Another example use getInstance(style).<br>
- * Print out a number using the localized number, currency, percent,
- * scientific, integer, iso currency, and plural currency format for each locale.
+ * <pre>
+ * for (ULocale uloc : ULocale.getAvailableLocales()) {
+ * System.out.print(uloc + ":\t");
+ * System.out.print(NumberFormat.getInstance(uloc).format(1.23));
+ * System.out.print("\t");
+ * System.out.print(NumberFormat.getCurrencyInstance(uloc).format(1.23));
+ * System.out.print("\t");
+ * System.out.print(NumberFormat.getPercentInstance(uloc).format(1.23));
+ * System.out.println();
+ * }
+ * </pre>
*
- * <blockquote><pre>
- * ULocale locale = new ULocale("en_US");
- * double myNumber = 1234.56;
- * for (int j=NumberFormat.NUMBERSTYLE; j&lt;=NumberFormat.PLURALCURRENCYSTYLE; ++j) {
- * NumberFormat format = NumberFormat.getInstance(locale, j);
- * try {
- * // Assume format is a DecimalFormat
- * System.out.print(": " + ((DecimalFormat) format).toPattern()
- * + " -&gt; " + form.format(myNumber));
- * } catch (Exception e) {}
- * try {
- * System.out.println(" -&gt; " + format.parse(form.format(myNumber)));
- * } catch (ParseException e) {}
- * }</pre></blockquote>
+ * </blockquote>
*
- * <h3>Patterns</h3>
+ * <h3>Properties and Symbols</h3>
*
- * <p>A <code>DecimalFormat</code> consists of a <em>pattern</em> and a set of
- * <em>symbols</em>. The pattern may be set directly using {@link #applyPattern}, or
- * indirectly using other API methods which manipulate aspects of the pattern, such as the
- * minimum number of integer digits. The symbols are stored in a {@link
- * DecimalFormatSymbols} object. When using the {@link NumberFormat} factory methods, the
- * pattern and symbols are read from ICU's locale data.
+ * <p>A DecimalFormat object encapsulates a set of <em>properties</em> and a set of
+ * <em>symbols</em>. Grouping size, rounding mode, and affixes are examples of properties. Locale
+ * digits and the characters used for grouping and decimal separators are examples of symbols.
*
- * <h4>Special Pattern Characters</h4>
+ * <p>To set a custom set of symbols, use {@link #setDecimalFormatSymbols}. Use the various other
+ * setters in this class to set custom values for the properties.
*
- * <p>Many characters in a pattern are taken literally; they are matched during parsing
- * and output unchanged during formatting. Special characters, on the other hand, stand
- * for other characters, strings, or classes of characters. For example, the '#'
- * character is replaced by a localized digit. Often the replacement character is the
- * same as the pattern character; in the U.S. locale, the ',' grouping character is
- * replaced by ','. However, the replacement is still happening, and if the symbols are
- * modified, the grouping character changes. Some special characters affect the behavior
- * of the formatter by their presence; for example, if the percent character is seen, then
- * the value is multiplied by 100 before being displayed.
+ * <h3>Rounding</h3>
*
- * <p>To insert a special character in a pattern as a literal, that is, without any
- * special meaning, the character must be quoted. There are some exceptions to this which
- * are noted below.
+ * <p>DecimalFormat provides three main strategies to specify the position at which numbers should
+ * be rounded:
*
- * <p>The characters listed here are used in non-localized patterns. Localized patterns
- * use the corresponding characters taken from this formatter's {@link
- * DecimalFormatSymbols} object instead, and these characters lose their special status.
- * Two exceptions are the currency sign and quote, which are not localized.
+ * <ol>
+ * <li><strong>Magnitude:</strong> Display a fixed number of fraction digits; this is the most
+ * common form.
+ * <li><strong>Increment:</strong> Round numbers to the closest multiple of a certain increment,
+ * such as 0.05. This is common in currencies.
+ * <li><strong>Significant Digits:</strong> Round numbers such that a fixed number of nonzero
+ * digits are shown. This is most common in scientific notation.
+ * </ol>
*
- * <blockquote>
- * <table border=0 cellspacing=3 cellpadding=0 summary="Chart showing symbol,
- * location, localized, and meaning.">
- * <tr style="background-color: #ccccff">
- * <th align=left>Symbol
- * <th align=left>Location
- * <th align=left>Localized?
- * <th align=left>Meaning
- * <tr style="vertical-align: top;">
- * <td><code>0</code>
- * <td>Number
- * <td>Yes
- * <td>Digit
- * <tr style="vertical-align: top; background-color: #eeeeff;">
- * <td><code>1-9</code>
- * <td>Number
- * <td>Yes
- * <td>'1' through '9' indicate rounding.
- * <tr style="vertical-align: top;">
- * <td><code>@</code>
- * <td>Number
- * <td>No
- * <td>Significant digit
- * <tr style="vertical-align: top; background-color: #eeeeff;">
- * <td><code>#</code>
- * <td>Number
- * <td>Yes
- * <td>Digit, zero shows as absent
- * <tr style="vertical-align: top;">
- * <td><code>.</code>
- * <td>Number
- * <td>Yes
- * <td>Decimal separator or monetary decimal separator
- * <tr style="vertical-align: top; background-color: #eeeeff;">
- * <td><code>-</code>
- * <td>Number
- * <td>Yes
- * <td>Minus sign
- * <tr style="vertical-align: top;">
- * <td><code>,</code>
- * <td>Number
- * <td>Yes
- * <td>Grouping separator
- * <tr style="vertical-align: top; background-color: #eeeeff;">
- * <td><code>E</code>
- * <td>Number
- * <td>Yes
- * <td>Separates mantissa and exponent in scientific notation.
- * <em>Need not be quoted in prefix or suffix.</em>
- * <tr style="vertical-align: top;">
- * <td><code>+</code>
- * <td>Exponent
- * <td>Yes
- * <td>Prefix positive exponents with localized plus sign.
- * <em>Need not be quoted in prefix or suffix.</em>
- * <tr style="vertical-align: top; background-color: #eeeeff;">
- * <td><code>;</code>
- * <td>Subpattern boundary
- * <td>Yes
- * <td>Separates positive and negative subpatterns
- * <tr style="vertical-align: top;">
- * <td><code>%</code>
- * <td>Prefix or suffix
- * <td>Yes
- * <td>Multiply by 100 and show as percentage
- * <tr style="vertical-align: top; background-color: #eeeeff;">
- * <td><code>&#92;u2030</code>
- * <td>Prefix or suffix
- * <td>Yes
- * <td>Multiply by 1000 and show as per mille
- * <tr style="vertical-align: top;">
- * <td><code>&#164;</code> (<code>&#92;u00A4</code>)
- * <td>Prefix or suffix
- * <td>No
- * <td>Currency sign, replaced by currency symbol. If
- * doubled, replaced by international currency symbol.
- * If tripled, replaced by currency plural names, for example,
- * "US dollar" or "US dollars" for America.
- * If present in a pattern, the monetary decimal separator
- * is used instead of the decimal separator.
- * <tr style="vertical-align: top; background-color: #eeeeff;">
- * <td><code>'</code>
- * <td>Prefix or suffix
- * <td>No
- * <td>Used to quote special characters in a prefix or suffix,
- * for example, <code>"'#'#"</code> formats 123 to
- * <code>"#123"</code>. To create a single quote
- * itself, use two in a row: <code>"# o''clock"</code>.
- * <tr style="vertical-align: top;">
- * <td><code>*</code>
- * <td>Prefix or suffix boundary
- * <td>Yes
- * <td>Pad escape, precedes pad character
- * </table>
- * </blockquote>
+ * <p>It is also possible to specify the <em>rounding mode</em> to use. The default rounding mode is
+ * "half even", which rounds numbers to their closest increment, with ties broken in favor of
+ * trailing numbers being even. For more information, see {@link #setRoundingMode} and <a
+ * href="http://userguide.icu-project.org/formatparse/numbers/rounding-modes">the ICU User
+ * Guide</a>.
*
- * <p>A <code>DecimalFormat</code> pattern contains a postive and negative subpattern, for
- * example, "#,##0.00;(#,##0.00)". Each subpattern has a prefix, a numeric part, and a
- * suffix. If there is no explicit negative subpattern, the negative subpattern is the
- * localized minus sign prefixed to the positive subpattern. That is, "0.00" alone is
- * equivalent to "0.00;-0.00". If there is an explicit negative subpattern, it serves
- * only to specify the negative prefix and suffix; the number of digits, minimal digits,
- * and other characteristics are ignored in the negative subpattern. That means that
- * "#,##0.0#;(#)" has precisely the same result as "#,##0.0#;(#,##0.0#)".
+ * <h3>Pattern Strings</h3>
*
- * <p>The prefixes, suffixes, and various symbols used for infinity, digits, thousands
- * separators, decimal separators, etc. may be set to arbitrary values, and they will
- * appear properly during formatting. However, care must be taken that the symbols and
- * strings do not conflict, or parsing will be unreliable. For example, either the
- * positive and negative prefixes or the suffixes must be distinct for {@link #parse} to
- * be able to distinguish positive from negative values. Another example is that the
- * decimal separator and thousands separator should be distinct characters, or parsing
- * will be impossible.
+ * <p>A <em>pattern string</em> is a way to serialize some of the available properties for decimal
+ * formatting. However, not all properties are capable of being serialized into a pattern string;
+ * see {@link #applyPattern} for more information.
*
- * <p>The <em>grouping separator</em> is a character that separates clusters of integer
- * digits to make large numbers more legible. It commonly used for thousands, but in some
- * locales it separates ten-thousands. The <em>grouping size</em> is the number of digits
- * between the grouping separators, such as 3 for "100,000,000" or 4 for "1 0000
- * 0000". There are actually two different grouping sizes: One used for the least
- * significant integer digits, the <em>primary grouping size</em>, and one used for all
- * others, the <em>secondary grouping size</em>. In most locales these are the same, but
- * sometimes they are different. For example, if the primary grouping interval is 3, and
- * the secondary is 2, then this corresponds to the pattern "#,##,##0", and the number
- * 123456789 is formatted as "12,34,56,789". If a pattern contains multiple grouping
- * separators, the interval between the last one and the end of the integer defines the
- * primary grouping size, and the interval between the last two defines the secondary
- * grouping size. All others are ignored, so "#,##,###,####" == "###,###,####" ==
- * "##,#,###,####".
+ * <p>Most users should not need to interface with pattern strings directly.
*
- * <p>Illegal patterns, such as "#.#.#" or "#.###,###", will cause
- * <code>DecimalFormat</code> to throw an {@link IllegalArgumentException} with a message
- * that describes the problem.
+ * <p>ICU DecimalFormat aims to follow the specification for pattern strings in <a
+ * href="http://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns">UTS #35</a>.
+ * Refer to that specification for more information on pattern string syntax.
*
- * <h4>Pattern BNF</h4>
+ * <h4>Pattern String BNF</h4>
+ *
+ * The following BNF is used when parsing the pattern string into property values:
*
* <pre>
* pattern := subpattern (';' subpattern)?
@@ -301,5874 +150,2310 @@ import android.icu.util.ULocale.Category;
* C..D any character from C up to D, inclusive
* S-T characters in S, except those in T
* </pre>
- * The first subpattern is for positive numbers. The second (optional)
- * subpattern is for negative numbers.
- *
- * <p>Not indicated in the BNF syntax above:
- *
- * <ul>
- *
- * <li>The grouping separator ',' can occur inside the integer and sigDigits
- * elements, between any two pattern characters of that element, as long as the integer or
- * sigDigits element is not followed by the exponent element.
- *
- * <li>Two grouping intervals are recognized: That between the decimal point and the first
- * grouping symbol, and that between the first and second grouping symbols. These
- * intervals are identical in most locales, but in some locales they differ. For example,
- * the pattern &quot;#,##,###&quot; formats the number 123456789 as
- * &quot;12,34,56,789&quot;.
- *
- * <li>The pad specifier <code>padSpec</code> may appear before the prefix, after the
- * prefix, before the suffix, after the suffix, or not at all.
- *
- * <li>In place of '0', the digits '1' through '9' may be used to indicate a rounding
- * increment.
- *
- * </ul>
- *
- * <h4>Parsing</h4>
- *
- * <p><code>DecimalFormat</code> parses all Unicode characters that represent decimal
- * digits, as defined by {@link UCharacter#digit}. In addition,
- * <code>DecimalFormat</code> also recognizes as digits the ten consecutive characters
- * starting with the localized zero digit defined in the {@link DecimalFormatSymbols}
- * object. During formatting, the {@link DecimalFormatSymbols}-based digits are output.
- *
- * <p>During parsing, grouping separators are ignored.
*
- * <p>For currency parsing, the formatter is able to parse every currency style formats no
- * matter which style the formatter is constructed with. For example, a formatter
- * instance gotten from NumberFormat.getInstance(ULocale, NumberFormat.CURRENCYSTYLE) can
- * parse formats such as "USD1.00" and "3.00 US dollars".
+ * <p>The first subpattern is for positive numbers. The second (optional) subpattern is for negative
+ * numbers.
*
- * <p>If {@link #parse(String, ParsePosition)} fails to parse a string, it returns
- * <code>null</code> and leaves the parse position unchanged. The convenience method
- * {@link #parse(String)} indicates parse failure by throwing a {@link
- * java.text.ParseException}.
- *
- * <p>Parsing an extremely large or small absolute value (such as 1.0E10000 or 1.0E-10000)
- * requires huge memory allocation for representing the parsed number. Such input may expose
- * a risk of DoS attacks. To prevent huge memory allocation triggered by such inputs,
- * <code>DecimalFormat</code> internally limits of maximum decimal digits to be 1000. Thus,
- * an input string resulting more than 1000 digits in plain decimal representation (non-exponent)
- * will be treated as either overflow (positive/negative infinite) or underflow (+0.0/-0.0).
- *
- * <h4>Formatting</h4>
- *
- * <p>Formatting is guided by several parameters, all of which can be specified either
- * using a pattern or using the API. The following description applies to formats that do
- * not use <a href="#sci">scientific notation</a> or <a href="#sigdig">significant
- * digits</a>.
- *
- * <ul><li>If the number of actual integer digits exceeds the <em>maximum integer
- * digits</em>, then only the least significant digits are shown. For example, 1997 is
- * formatted as "97" if the maximum integer digits is set to 2.
- *
- * <li>If the number of actual integer digits is less than the <em>minimum integer
- * digits</em>, then leading zeros are added. For example, 1997 is formatted as "01997"
- * if the minimum integer digits is set to 5.
- *
- * <li>If the number of actual fraction digits exceeds the <em>maximum fraction
- * digits</em>, then half-even rounding it performed to the maximum fraction digits. For
- * example, 0.125 is formatted as "0.12" if the maximum fraction digits is 2. This
- * behavior can be changed by specifying a rounding increment and a rounding mode.
- *
- * <li>If the number of actual fraction digits is less than the <em>minimum fraction
- * digits</em>, then trailing zeros are added. For example, 0.125 is formatted as
- * "0.1250" if the mimimum fraction digits is set to 4.
- *
- * <li>Trailing fractional zeros are not displayed if they occur <em>j</em> positions
- * after the decimal, where <em>j</em> is less than the maximum fraction digits. For
- * example, 0.10004 is formatted as "0.1" if the maximum fraction digits is four or less.
- * </ul>
- *
- * <p><strong>Special Values</strong>
- *
- * <p><code>NaN</code> is represented as a single character, typically
- * <code>&#92;uFFFD</code>. This character is determined by the {@link
- * DecimalFormatSymbols} object. This is the only value for which the prefixes and
- * suffixes are not used.
- *
- * <p>Infinity is represented as a single character, typically <code>&#92;u221E</code>,
- * with the positive or negative prefixes and suffixes applied. The infinity character is
- * determined by the {@link DecimalFormatSymbols} object.
- *
- * <h4><a name="sci">Scientific Notation</a></h4>
- *
- * <p>Numbers in scientific notation are expressed as the product of a mantissa and a
- * power of ten, for example, 1234 can be expressed as 1.234 x 10<sup>3</sup>. The
- * mantissa is typically in the half-open interval [1.0, 10.0) or sometimes [0.0, 1.0),
- * but it need not be. <code>DecimalFormat</code> supports arbitrary mantissas.
- * <code>DecimalFormat</code> can be instructed to use scientific notation through the API
- * or through the pattern. In a pattern, the exponent character immediately followed by
- * one or more digit characters indicates scientific notation. Example: "0.###E0" formats
- * the number 1234 as "1.234E3".
- *
- * <ul>
- *
- * <li>The number of digit characters after the exponent character gives the minimum
- * exponent digit count. There is no maximum. Negative exponents are formatted using the
- * localized minus sign, <em>not</em> the prefix and suffix from the pattern. This allows
- * patterns such as "0.###E0 m/s". To prefix positive exponents with a localized plus
- * sign, specify '+' between the exponent and the digits: "0.###E+0" will produce formats
- * "1E+1", "1E+0", "1E-1", etc. (In localized patterns, use the localized plus sign
- * rather than '+'.)
- *
- * <li>The minimum number of integer digits is achieved by adjusting the exponent.
- * Example: 0.00123 formatted with "00.###E0" yields "12.3E-4". This only happens if
- * there is no maximum number of integer digits. If there is a maximum, then the minimum
- * number of integer digits is fixed at one.
- *
- * <li>The maximum number of integer digits, if present, specifies the exponent grouping.
- * The most common use of this is to generate <em>engineering notation</em>, in which the
- * exponent is a multiple of three, e.g., "##0.###E0". The number 12345 is formatted
- * using "##0.####E0" as "12.345E3".
- *
- * <li>When using scientific notation, the formatter controls the digit counts using
- * significant digits logic. The maximum number of significant digits limits the total
- * number of integer and fraction digits that will be shown in the mantissa; it does not
- * affect parsing. For example, 12345 formatted with "##0.##E0" is "12.3E3". See the
- * section on significant digits for more details.
- *
- * <li>The number of significant digits shown is determined as follows: If
- * areSignificantDigitsUsed() returns false, then the minimum number of significant digits
- * shown is one, and the maximum number of significant digits shown is the sum of the
- * <em>minimum integer</em> and <em>maximum fraction</em> digits, and is unaffected by the
- * maximum integer digits. If this sum is zero, then all significant digits are shown.
- * If areSignificantDigitsUsed() returns true, then the significant digit counts are
- * specified by getMinimumSignificantDigits() and getMaximumSignificantDigits(). In this
- * case, the number of integer digits is fixed at one, and there is no exponent grouping.
- *
- * <li>Exponential patterns may not contain grouping separators.
- *
- * </ul>
- *
- * <h4><a name="sigdig">Significant Digits</a></h4>
- *
- * <code>DecimalFormat</code> has two ways of controlling how many digits are shows: (a)
- * significant digits counts, or (b) integer and fraction digit counts. Integer and
- * fraction digit counts are described above. When a formatter is using significant
- * digits counts, the number of integer and fraction digits is not specified directly, and
- * the formatter settings for these counts are ignored. Instead, the formatter uses
- * however many integer and fraction digits are required to display the specified number
- * of significant digits. Examples:
- *
- * <blockquote>
- * <table border=0 cellspacing=3 cellpadding=0>
- * <tr style="background-color: #ccccff">
- * <th align=left>Pattern
- * <th align=left>Minimum significant digits
- * <th align=left>Maximum significant digits
- * <th align=left>Number
- * <th align=left>Output of format()
- * <tr style="vertical-align: top;">
- * <td><code>@@@</code>
- * <td>3
- * <td>3
- * <td>12345
- * <td><code>12300</code>
- * <tr style="vertical-align: top; background-color: #eeeeff;">
- * <td><code>@@@</code>
- * <td>3
- * <td>3
- * <td>0.12345
- * <td><code>0.123</code>
- * <tr style="vertical-align: top;">
- * <td><code>@@##</code>
- * <td>2
- * <td>4
- * <td>3.14159
- * <td><code>3.142</code>
- * <tr style="vertical-align: top; background-color: #eeeeff;">
- * <td><code>@@##</code>
- * <td>2
- * <td>4
- * <td>1.23004
- * <td><code>1.23</code>
- * </table>
- * </blockquote>
+ * <p>Not indicated in the BNF syntax above:
*
* <ul>
- *
- * <li>Significant digit counts may be expressed using patterns that specify a minimum and
- * maximum number of significant digits. These are indicated by the <code>'@'</code> and
- * <code>'#'</code> characters. The minimum number of significant digits is the number of
- * <code>'@'</code> characters. The maximum number of significant digits is the number of
- * <code>'@'</code> characters plus the number of <code>'#'</code> characters following on
- * the right. For example, the pattern <code>"@@@"</code> indicates exactly 3 significant
- * digits. The pattern <code>"@##"</code> indicates from 1 to 3 significant digits.
- * Trailing zero digits to the right of the decimal separator are suppressed after the
- * minimum number of significant digits have been shown. For example, the pattern
- * <code>"@##"</code> formats the number 0.1203 as <code>"0.12"</code>.
- *
- * <li>If a pattern uses significant digits, it may not contain a decimal separator, nor
- * the <code>'0'</code> pattern character. Patterns such as <code>"@00"</code> or
- * <code>"@.###"</code> are disallowed.
- *
- * <li>Any number of <code>'#'</code> characters may be prepended to the left of the
- * leftmost <code>'@'</code> character. These have no effect on the minimum and maximum
- * significant digits counts, but may be used to position grouping separators. For
- * example, <code>"#,#@#"</code> indicates a minimum of one significant digits, a maximum
- * of two significant digits, and a grouping size of three.
- *
- * <li>In order to enable significant digits formatting, use a pattern containing the
- * <code>'@'</code> pattern character. Alternatively, call {@link
- * #setSignificantDigitsUsed setSignificantDigitsUsed(true)}.
- *
- * <li>In order to disable significant digits formatting, use a pattern that does not
- * contain the <code>'@'</code> pattern character. Alternatively, call {@link
- * #setSignificantDigitsUsed setSignificantDigitsUsed(false)}.
- *
- * <li>The number of significant digits has no effect on parsing.
- *
- * <li>Significant digits may be used together with exponential notation. Such patterns
- * are equivalent to a normal exponential pattern with a minimum and maximum integer digit
- * count of one, a minimum fraction digit count of <code>getMinimumSignificantDigits() -
- * 1</code>, and a maximum fraction digit count of <code>getMaximumSignificantDigits() -
- * 1</code>. For example, the pattern <code>"@@###E0"</code> is equivalent to
- * <code>"0.0###E0"</code>.
- *
- * <li>If signficant digits are in use, then the integer and fraction digit counts, as set
- * via the API, are ignored. If significant digits are not in use, then the signficant
- * digit counts, as set via the API, are ignored.
- *
+ * <li>The grouping separator ',' can occur inside the integer and sigDigits elements, between any
+ * two pattern characters of that element, as long as the integer or sigDigits element is not
+ * followed by the exponent element.
+ * <li>Two grouping intervals are recognized: That between the decimal point and the first
+ * grouping symbol, and that between the first and second grouping symbols. These intervals
+ * are identical in most locales, but in some locales they differ. For example, the pattern
+ * &quot;#,##,###&quot; formats the number 123456789 as &quot;12,34,56,789&quot;.
+ * <li>The pad specifier <code>padSpec</code> may appear before the prefix, after the prefix,
+ * before the suffix, after the suffix, or not at all.
+ * <li>In place of '0', the digits '1' through '9' may be used to indicate a rounding increment.
* </ul>
*
- * <h4>Padding</h4>
+ * <h3>Parsing</h3>
*
- * <p><code>DecimalFormat</code> supports padding the result of {@link #format} to a
- * specific width. Padding may be specified either through the API or through the pattern
- * syntax. In a pattern the pad escape character, followed by a single pad character,
- * causes padding to be parsed and formatted. The pad escape character is '*' in
- * unlocalized patterns, and can be localized using {@link
- * DecimalFormatSymbols#setPadEscape}. For example, <code>"$*x#,##0.00"</code> formats
- * 123 to <code>"$xx123.00"</code>, and 1234 to <code>"$1,234.00"</code>.
+ * <p>DecimalFormat aims to be able to parse anything that it can output as a formatted string.
*
- * <ul>
+ * <p>There are two primary parse modes: <em>lenient</em> and <em>strict</em>. Lenient mode should
+ * be used if the goal is to parse user input to a number; strict mode should be used if the goal is
+ * validation. The default is lenient mode. For more information, see {@link #setParseStrict}.
*
- * <li>When padding is in effect, the width of the positive subpattern, including prefix
- * and suffix, determines the format width. For example, in the pattern <code>"* #0
- * o''clock"</code>, the format width is 10.
+ * <p><code>DecimalFormat</code> parses all Unicode characters that represent decimal digits, as
+ * defined by {@link UCharacter#digit}. In addition, <code>DecimalFormat</code> also recognizes as
+ * digits the ten consecutive characters starting with the localized zero digit defined in the
+ * {@link DecimalFormatSymbols} object. During formatting, the {@link DecimalFormatSymbols}-based
+ * digits are output.
*
- * <li>The width is counted in 16-bit code units (Java <code>char</code>s).
+ * <p>Grouping separators are ignored in lenient mode (default). In strict mode, grouping separators
+ * must match the locale-specified grouping sizes.
*
- * <li>Some parameters which usually do not matter have meaning when padding is used,
- * because the pattern width is significant with padding. In the pattern "*
- * ##,##,#,##0.##", the format width is 14. The initial characters "##,##," do not affect
- * the grouping size or maximum integer digits, but they do affect the format width.
+ * <p>When using {@link #parseCurrency}, all currencies are accepted, not just the currency
+ * currently set in the formatter. In addition, the formatter is able to parse every currency style
+ * format for a particular locale no matter which style the formatter is constructed with. For
+ * example, a formatter instance gotten from NumberFormat.getInstance(ULocale,
+ * NumberFormat.CURRENCYSTYLE) can parse both "USD1.00" and "3.00 US dollars".
*
- * <li>Padding may be inserted at one of four locations: before the prefix, after the
- * prefix, before the suffix, or after the suffix. If padding is specified in any other
- * location, {@link #applyPattern} throws an {@link IllegalArgumentException}. If there
- * is no prefix, before the prefix and after the prefix are equivalent, likewise for the
- * suffix.
+ * <p>Whitespace characters (lenient mode) and bidi control characters (lenient and strict mode),
+ * collectively called "ignorables", do not need to match in identity or quantity between the
+ * pattern string and the input string. For example, the pattern "# %" matches "35 %" (with a single
+ * space), "35%" (with no space), "35&nbsp;%" (with a non-breaking space), and "35&nbsp; %" (with
+ * multiple spaces). Arbitrary ignorables are also allowed at boundaries between the parts of the
+ * number: prefix, number, exponent separator, and suffix.
*
- * <li>When specified in a pattern, the 16-bit <code>char</code> immediately following the
- * pad escape is the pad character. This may be any character, including a special pattern
- * character. That is, the pad escape <em>escapes</em> the following character. If there
- * is no character after the pad escape, then the pattern is illegal.
- *
- * </ul>
- *
- * <p>
- * <strong>Rounding</strong>
- *
- * <p><code>DecimalFormat</code> supports rounding to a specific increment. For example,
- * 1230 rounded to the nearest 50 is 1250. 1.234 rounded to the nearest 0.65 is 1.3. The
- * rounding increment may be specified through the API or in a pattern. To specify a
- * rounding increment in a pattern, include the increment in the pattern itself. "#,#50"
- * specifies a rounding increment of 50. "#,##0.05" specifies a rounding increment of
- * 0.05.
- *
- * <ul>
+ * <p>If {@link #parse(String, ParsePosition)} fails to parse a string, it returns <code>null</code>
+ * and leaves the parse position unchanged. The convenience method {@link #parse(String)} indicates
+ * parse failure by throwing a {@link java.text.ParseException}.
*
- * <li>Rounding only affects the string produced by formatting. It does not affect
- * parsing or change any numerical values.
+ * <p>Under the hood, a state table parsing engine is used. To debug a parsing failure during
+ * development, use the following pattern to print details about the state table transitions:
*
- * <li>A <em>rounding mode</em> determines how values are rounded; see the {@link
- * android.icu.math.BigDecimal} documentation for a description of the modes. Rounding
- * increments specified in patterns use the default mode, {@link
- * android.icu.math.BigDecimal#ROUND_HALF_EVEN}.
- *
- * <li>Some locales use rounding in their currency formats to reflect the smallest
- * currency denomination.
- *
- * <li>In a pattern, digits '1' through '9' specify rounding, but otherwise behave
- * identically to digit '0'.
+ * <pre>
+ * android.icu.impl.number.Parse.DEBUGGING = true;
+ * df.parse("123.45", ppos);
+ * android.icu.impl.number.Parse.DEBUGGING = false;
+ * </pre>
*
- * </ul>
+ * <h3>Thread Safety and Best Practices</h3>
*
- * <h4>Synchronization</h4>
+ * <p>Starting with ICU 59, instances of DecimalFormat are thread-safe.
*
- * <p><code>DecimalFormat</code> objects are not synchronized. Multiple threads should
- * not access one formatter concurrently.
+ * <p>Under the hood, DecimalFormat maintains an immutable formatter object that is rebuilt whenever
+ * any of the property setters are called. It is therefore best practice to call property setters
+ * only during construction and not when formatting numbers online.
*
- * @see java.text.Format
- * @see NumberFormat
- * @author Mark Davis
- * @author Alan Liu
+ * @see java.text.Format
+ * @see NumberFormat
*/
public class DecimalFormat extends NumberFormat {
- /**
- * Creates a DecimalFormat using the default pattern and symbols for the default
- * <code>FORMAT</code> locale. This is a convenient way to obtain a DecimalFormat when
- * internationalization is not the main concern.
- *
- * <p>To obtain standard formats for a given locale, use the factory methods on
- * NumberFormat such as getNumberInstance. These factories will return the most
- * appropriate sub-class of NumberFormat for a given locale.
- *
- * @see NumberFormat#getInstance
- * @see NumberFormat#getNumberInstance
- * @see NumberFormat#getCurrencyInstance
- * @see NumberFormat#getPercentInstance
- * @see Category#FORMAT
- */
- public DecimalFormat() {
- ULocale def = ULocale.getDefault(Category.FORMAT);
- String pattern = getPattern(def, 0);
- // Always applyPattern after the symbols are set
- this.symbols = new DecimalFormatSymbols(def);
- setCurrency(Currency.getInstance(def));
- applyPatternWithoutExpandAffix(pattern, false);
- if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
- currencyPluralInfo = new CurrencyPluralInfo(def);
- // the exact pattern is not known until the plural count is known.
- // so, no need to expand affix now.
- } else {
- expandAffixAdjustWidth(null);
- }
- }
-
- /**
- * Creates a DecimalFormat from the given pattern and the symbols for the default
- * <code>FORMAT</code> locale. This is a convenient way to obtain a DecimalFormat when
- * internationalization is not the main concern.
- *
- * <p>To obtain standard formats for a given locale, use the factory methods on
- * NumberFormat such as getNumberInstance. These factories will return the most
- * appropriate sub-class of NumberFormat for a given locale.
- *
- * @param pattern A non-localized pattern string.
- * @throws IllegalArgumentException if the given pattern is invalid.
- * @see NumberFormat#getInstance
- * @see NumberFormat#getNumberInstance
- * @see NumberFormat#getCurrencyInstance
- * @see NumberFormat#getPercentInstance
- * @see Category#FORMAT
- */
- public DecimalFormat(String pattern) {
- // Always applyPattern after the symbols are set
- ULocale def = ULocale.getDefault(Category.FORMAT);
- this.symbols = new DecimalFormatSymbols(def);
- setCurrency(Currency.getInstance(def));
- applyPatternWithoutExpandAffix(pattern, false);
- if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
- currencyPluralInfo = new CurrencyPluralInfo(def);
- } else {
- expandAffixAdjustWidth(null);
- }
- }
-
- /**
- * Creates a DecimalFormat from the given pattern and symbols. Use this constructor
- * when you need to completely customize the behavior of the format.
- *
- * <p>To obtain standard formats for a given locale, use the factory methods on
- * NumberFormat such as getInstance or getCurrencyInstance. If you need only minor
- * adjustments to a standard format, you can modify the format returned by a
- * NumberFormat factory method.
- *
- * @param pattern a non-localized pattern string
- * @param symbols the set of symbols to be used
- * @exception IllegalArgumentException if the given pattern is invalid
- * @see NumberFormat#getInstance
- * @see NumberFormat#getNumberInstance
- * @see NumberFormat#getCurrencyInstance
- * @see NumberFormat#getPercentInstance
- * @see DecimalFormatSymbols
- */
- public DecimalFormat(String pattern, DecimalFormatSymbols symbols) {
- createFromPatternAndSymbols(pattern, symbols);
- }
-
- private void createFromPatternAndSymbols(String pattern, DecimalFormatSymbols inputSymbols) {
- // Always applyPattern after the symbols are set
- symbols = (DecimalFormatSymbols) inputSymbols.clone();
- if (pattern.indexOf(CURRENCY_SIGN) >= 0) {
- // Only spend time with currency symbols when we're going to display it.
- // Also set some defaults before the apply pattern.
- setCurrencyForSymbols();
- }
- applyPatternWithoutExpandAffix(pattern, false);
- if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
- currencyPluralInfo = new CurrencyPluralInfo(symbols.getULocale());
- } else {
- expandAffixAdjustWidth(null);
- }
- }
-
- /**
- * Creates a DecimalFormat from the given pattern, symbols, information used for
- * currency plural format, and format style. Use this constructor when you need to
- * completely customize the behavior of the format.
- *
- * <p>To obtain standard formats for a given locale, use the factory methods on
- * NumberFormat such as getInstance or getCurrencyInstance.
- *
- * <p>If you need only minor adjustments to a standard format, you can modify the
- * format returned by a NumberFormat factory method using the setters.
- *
- * <p>If you want to completely customize a decimal format, using your own
- * DecimalFormatSymbols (such as group separators) and your own information for
- * currency plural formatting (such as plural rule and currency plural patterns), you
- * can use this constructor.
- *
- * @param pattern a non-localized pattern string
- * @param symbols the set of symbols to be used
- * @param infoInput the information used for currency plural format, including
- * currency plural patterns and plural rules.
- * @param style the decimal formatting style, it is one of the following values:
- * NumberFormat.NUMBERSTYLE; NumberFormat.CURRENCYSTYLE; NumberFormat.PERCENTSTYLE;
- * NumberFormat.SCIENTIFICSTYLE; NumberFormat.INTEGERSTYLE;
- * NumberFormat.ISOCURRENCYSTYLE; NumberFormat.PLURALCURRENCYSTYLE;
- */
- public DecimalFormat(String pattern, DecimalFormatSymbols symbols, CurrencyPluralInfo infoInput,
- int style) {
- CurrencyPluralInfo info = infoInput;
- if (style == NumberFormat.PLURALCURRENCYSTYLE) {
- info = (CurrencyPluralInfo) infoInput.clone();
- }
- create(pattern, symbols, info, style);
- }
-
- private void create(String pattern, DecimalFormatSymbols inputSymbols, CurrencyPluralInfo info,
- int inputStyle) {
- if (inputStyle != NumberFormat.PLURALCURRENCYSTYLE) {
- createFromPatternAndSymbols(pattern, inputSymbols);
+ /** New serialization in ICU 59: declare different version from ICU 58. */
+ private static final long serialVersionUID = 864413376551465018L;
+
+ /**
+ * One non-transient field such that deserialization can determine the version of the class. This
+ * field has existed since the very earliest versions of DecimalFormat.
+ */
+ @SuppressWarnings("unused")
+ private final int serialVersionOnStream = 5;
+
+ //=====================================================================================//
+ // INSTANCE FIELDS //
+ //=====================================================================================//
+
+ // Fields are package-private, so that subclasses can use them.
+ // properties should be final, but clone won't work if we make it final.
+ // All fields are transient because custom serialization is used.
+
+ /**
+ * The property bag corresponding to user-specified settings and settings from the pattern string.
+ * In principle this should be final, but serialize and clone won't work if it is final. Does not
+ * need to be volatile because the reference never changes.
+ */
+ /* final */ transient Properties properties;
+
+ /**
+ * The symbols for the current locale. Volatile because threads may read and write at the same
+ * time.
+ */
+ transient volatile DecimalFormatSymbols symbols;
+
+ /**
+ * The pre-computed formatter object. Setters cause this to be re-computed atomically. The {@link
+ * #format} method uses the formatter directly without needing to synchronize. Volatile because
+ * threads may read and write at the same time.
+ */
+ transient volatile SingularFormat formatter;
+
+ /**
+ * The effective properties as exported from the formatter object. Volatile because threads may
+ * read and write at the same time.
+ */
+ transient volatile Properties exportedProperties;
+
+ //=====================================================================================//
+ // CONSTRUCTORS //
+ //=====================================================================================//
+
+ /**
+ * Creates a DecimalFormat based on the number pattern and symbols for the default locale. This is
+ * a convenient way to obtain a DecimalFormat instance when internationalization is not the main
+ * concern.
+ *
+ * <p>Most users should call the factory methods on NumberFormat, such as {@link
+ * NumberFormat#getNumberInstance}, which return localized formatter objects, instead of the
+ * DecimalFormat constructors.
+ *
+ * @see NumberFormat#getInstance
+ * @see NumberFormat#getNumberInstance
+ * @see NumberFormat#getCurrencyInstance
+ * @see NumberFormat#getPercentInstance
+ * @see Category#FORMAT
+ */
+ public DecimalFormat() {
+ // Use the locale's default pattern
+ ULocale def = ULocale.getDefault(ULocale.Category.FORMAT);
+ String pattern = getPattern(def, NumberFormat.NUMBERSTYLE);
+ symbols = getDefaultSymbols();
+ properties = new Properties();
+ exportedProperties = new Properties();
+ // Regression: ignore pattern rounding information if the pattern has currency symbols.
+ setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
+ refreshFormatter();
+ }
+
+ /**
+ * Creates a DecimalFormat based on the given pattern, using symbols for the default locale. This
+ * is a convenient way to obtain a DecimalFormat instance when internationalization is not the
+ * main concern.
+ *
+ * <p>Most users should call the factory methods on NumberFormat, such as {@link
+ * NumberFormat#getNumberInstance}, which return localized formatter objects, instead of the
+ * DecimalFormat constructors.
+ *
+ * @param pattern A pattern string such as "#,##0.00" conforming to <a
+ * href="http://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns">UTS
+ * #35</a>.
+ * @throws IllegalArgumentException if the given pattern is invalid.
+ * @see NumberFormat#getInstance
+ * @see NumberFormat#getNumberInstance
+ * @see NumberFormat#getCurrencyInstance
+ * @see NumberFormat#getPercentInstance
+ * @see Category#FORMAT
+ */
+ public DecimalFormat(String pattern) {
+ symbols = getDefaultSymbols();
+ properties = new Properties();
+ exportedProperties = new Properties();
+ // Regression: ignore pattern rounding information if the pattern has currency symbols.
+ setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
+ refreshFormatter();
+ }
+
+ /**
+ * Creates a DecimalFormat based on the given pattern and symbols. Use this constructor if you
+ * want complete control over the behavior of the formatter.
+ *
+ * <p>Most users should call the factory methods on NumberFormat, such as {@link
+ * NumberFormat#getNumberInstance}, which return localized formatter objects, instead of the
+ * DecimalFormat constructors.
+ *
+ * @param pattern A pattern string such as "#,##0.00" conforming to <a
+ * href="http://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns">UTS
+ * #35</a>.
+ * @param symbols The set of symbols to be used.
+ * @exception IllegalArgumentException if the given pattern is invalid
+ * @see NumberFormat#getInstance
+ * @see NumberFormat#getNumberInstance
+ * @see NumberFormat#getCurrencyInstance
+ * @see NumberFormat#getPercentInstance
+ * @see DecimalFormatSymbols
+ */
+ public DecimalFormat(String pattern, DecimalFormatSymbols symbols) {
+ this.symbols = (DecimalFormatSymbols) symbols.clone();
+ properties = new Properties();
+ exportedProperties = new Properties();
+ // Regression: ignore pattern rounding information if the pattern has currency symbols.
+ setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
+ refreshFormatter();
+ }
+
+ /**
+ * Creates a DecimalFormat based on the given pattern and symbols, with additional control over
+ * the behavior of currency. The style argument determines whether currency rounding rules should
+ * override the pattern, and the {@link CurrencyPluralInfo} object is used for customizing the
+ * plural forms used for currency long names.
+ *
+ * <p>Most users should call the factory methods on NumberFormat, such as {@link
+ * NumberFormat#getNumberInstance}, which return localized formatter objects, instead of the
+ * DecimalFormat constructors.
+ *
+ * @param pattern a non-localized pattern string
+ * @param symbols the set of symbols to be used
+ * @param infoInput the information used for currency plural format, including currency plural
+ * patterns and plural rules.
+ * @param style the decimal formatting style, it is one of the following values:
+ * NumberFormat.NUMBERSTYLE; NumberFormat.CURRENCYSTYLE; NumberFormat.PERCENTSTYLE;
+ * NumberFormat.SCIENTIFICSTYLE; NumberFormat.INTEGERSTYLE; NumberFormat.ISOCURRENCYSTYLE;
+ * NumberFormat.PLURALCURRENCYSTYLE;
+ */
+ public DecimalFormat(
+ String pattern, DecimalFormatSymbols symbols, CurrencyPluralInfo infoInput, int style) {
+ this(pattern, symbols, style);
+ properties.setCurrencyPluralInfo(infoInput);
+ refreshFormatter();
+ }
+
+ /** Package-private constructor used by NumberFormat. */
+ DecimalFormat(String pattern, DecimalFormatSymbols symbols, int choice) {
+ this.symbols = (DecimalFormatSymbols) symbols.clone();
+ properties = new Properties();
+ exportedProperties = new Properties();
+ // If choice is a currency type, ignore the rounding information.
+ if (choice == CURRENCYSTYLE
+ || choice == ISOCURRENCYSTYLE
+ || choice == ACCOUNTINGCURRENCYSTYLE
+ || choice == CASHCURRENCYSTYLE
+ || choice == STANDARDCURRENCYSTYLE
+ || choice == PLURALCURRENCYSTYLE) {
+ setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_ALWAYS);
+ } else {
+ setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
+ }
+ refreshFormatter();
+ }
+
+ private static DecimalFormatSymbols getDefaultSymbols() {
+ return DecimalFormatSymbols.getInstance();
+ }
+
+ /**
+ * Parses the given pattern string and overwrites the settings specified in the pattern string.
+ * The properties corresponding to the following setters are overwritten, either with their
+ * default values or with the value specified in the pattern string:
+ *
+ * <ol>
+ * <li>{@link #setDecimalSeparatorAlwaysShown}
+ * <li>{@link #setExponentSignAlwaysShown}
+ * <li>{@link #setFormatWidth}
+ * <li>{@link #setGroupingSize}
+ * <li>{@link #setMultiplier} (percent/permille)
+ * <li>{@link #setMaximumFractionDigits}
+ * <li>{@link #setMaximumIntegerDigits}
+ * <li>{@link #setMaximumSignificantDigits}
+ * <li>{@link #setMinimumExponentDigits}
+ * <li>{@link #setMinimumFractionDigits}
+ * <li>{@link #setMinimumIntegerDigits}
+ * <li>{@link #setMinimumSignificantDigits}
+ * <li>{@link #setPadPosition}
+ * <li>{@link #setPadCharacter}
+ * <li>{@link #setRoundingIncrement}
+ * <li>{@link #setSecondaryGroupingSize}
+ * </ol>
+ *
+ * All other settings remain untouched.
+ *
+ * <p>For more information on pattern strings, see <a
+ * href="http://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns">UTS #35</a>.
+ */
+ public synchronized void applyPattern(String pattern) {
+ setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_NEVER);
+ // Backwards compatibility: clear out user-specified prefix and suffix,
+ // as well as CurrencyPluralInfo.
+ properties.setPositivePrefix(null);
+ properties.setNegativePrefix(null);
+ properties.setPositiveSuffix(null);
+ properties.setNegativeSuffix(null);
+ properties.setCurrencyPluralInfo(null);
+ refreshFormatter();
+ }
+
+ /**
+ * Converts the given string to standard notation and then parses it using {@link #applyPattern}.
+ * This method is provided for backwards compatibility and should not be used in new projects.
+ *
+ * <p>Localized notation means that instead of using generic placeholders in the pattern, you use
+ * the corresponding locale-specific characters instead. For example, in locale <em>fr-FR</em>,
+ * the period in the pattern "0.000" means "decimal" in standard notation (as it does in every
+ * other locale), but it means "grouping" in localized notation.
+ *
+ * @param localizedPattern The pattern string in localized notation.
+ */
+ public synchronized void applyLocalizedPattern(String localizedPattern) {
+ String pattern = PatternString.convertLocalized(localizedPattern, symbols, false);
+ applyPattern(pattern);
+ }
+
+ //=====================================================================================//
+ // CLONE AND SERIALIZE //
+ //=====================================================================================//
+
+ /***/
+ @Override
+ public Object clone() {
+ DecimalFormat other = (DecimalFormat) super.clone();
+ other.symbols = (DecimalFormatSymbols) symbols.clone();
+ other.properties = properties.clone();
+ other.exportedProperties = new Properties();
+ other.refreshFormatter();
+ return other;
+ }
+
+ /**
+ * Custom serialization: save property bag and symbols; the formatter object can be re-created
+ * from just that amount of information.
+ */
+ private synchronized void writeObject(ObjectOutputStream oos) throws IOException {
+ // ICU 59 custom serialization.
+ // Write class metadata and serialVersionOnStream field:
+ oos.defaultWriteObject();
+ // Extra int for possible future use:
+ oos.writeInt(0);
+ // 1) Property Bag
+ oos.writeObject(properties);
+ // 2) DecimalFormatSymbols
+ oos.writeObject(symbols);
+ }
+
+ /**
+ * Custom serialization: re-create object from serialized property bag and symbols. Also supports
+ * reading from the legacy (pre-ICU4J 59) format and converting it to the new form.
+ */
+ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
+ ObjectInputStream.GetField fieldGetter = ois.readFields();
+ ObjectStreamField[] serializedFields = fieldGetter.getObjectStreamClass().getFields();
+ int serialVersion = fieldGetter.get("serialVersionOnStream", -1);
+
+ if (serialVersion > 5) {
+ throw new IOException(
+ "Cannot deserialize newer android.icu.text.DecimalFormat (v" + serialVersion + ")");
+ } else if (serialVersion == 5) {
+ ///// ICU 59+ SERIALIZATION FORMAT /////
+ // We expect this field and no other fields:
+ if (serializedFields.length > 1) {
+ throw new IOException("Too many fields when reading serial version 5");
+ }
+ // Extra int for possible future use:
+ ois.readInt();
+ // 1) Property Bag
+ properties = (Properties) ois.readObject();
+ // 2) DecimalFormatSymbols
+ symbols = (DecimalFormatSymbols) ois.readObject();
+ // Re-build transient fields
+ exportedProperties = new Properties();
+ refreshFormatter();
+ } else {
+ ///// LEGACY SERIALIZATION FORMAT /////
+ properties = new Properties();
+ // Loop through the fields. Not all fields necessarily exist in the serialization.
+ String pp = null, ppp = null, ps = null, psp = null;
+ String np = null, npp = null, ns = null, nsp = null;
+ for (ObjectStreamField field : serializedFields) {
+ String name = field.getName();
+ if (name.equals("decimalSeparatorAlwaysShown")) {
+ setDecimalSeparatorAlwaysShown(fieldGetter.get("decimalSeparatorAlwaysShown", false));
+ } else if (name.equals("exponentSignAlwaysShown")) {
+ setExponentSignAlwaysShown(fieldGetter.get("exponentSignAlwaysShown", false));
+ } else if (name.equals("formatWidth")) {
+ setFormatWidth(fieldGetter.get("formatWidth", 0));
+ } else if (name.equals("groupingSize")) {
+ setGroupingSize(fieldGetter.get("groupingSize", (byte) 3));
+ } else if (name.equals("groupingSize2")) {
+ setSecondaryGroupingSize(fieldGetter.get("groupingSize2", (byte) 0));
+ } else if (name.equals("maxSignificantDigits")) {
+ setMaximumSignificantDigits(fieldGetter.get("maxSignificantDigits", 6));
+ } else if (name.equals("minExponentDigits")) {
+ setMinimumExponentDigits(fieldGetter.get("minExponentDigits", (byte) 0));
+ } else if (name.equals("minSignificantDigits")) {
+ setMinimumSignificantDigits(fieldGetter.get("minSignificantDigits", 1));
+ } else if (name.equals("multiplier")) {
+ setMultiplier(fieldGetter.get("multiplier", 1));
+ } else if (name.equals("pad")) {
+ setPadCharacter(fieldGetter.get("pad", '\u0020'));
+ } else if (name.equals("padPosition")) {
+ setPadPosition(fieldGetter.get("padPosition", 0));
+ } else if (name.equals("parseBigDecimal")) {
+ setParseBigDecimal(fieldGetter.get("parseBigDecimal", false));
+ } else if (name.equals("parseRequireDecimalPoint")) {
+ setDecimalPatternMatchRequired(fieldGetter.get("parseRequireDecimalPoint", false));
+ } else if (name.equals("roundingMode")) {
+ setRoundingMode(fieldGetter.get("roundingMode", 0));
+ } else if (name.equals("useExponentialNotation")) {
+ setScientificNotation(fieldGetter.get("useExponentialNotation", false));
+ } else if (name.equals("useSignificantDigits")) {
+ setSignificantDigitsUsed(fieldGetter.get("useSignificantDigits", false));
+ } else if (name.equals("currencyPluralInfo")) {
+ setCurrencyPluralInfo((CurrencyPluralInfo) fieldGetter.get("currencyPluralInfo", null));
+ } else if (name.equals("mathContext")) {
+ setMathContextICU((MathContext) fieldGetter.get("mathContext", null));
+ } else if (name.equals("negPrefixPattern")) {
+ npp = (String) fieldGetter.get("negPrefixPattern", null);
+ } else if (name.equals("negSuffixPattern")) {
+ nsp = (String) fieldGetter.get("negSuffixPattern", null);
+ } else if (name.equals("negativePrefix")) {
+ np = (String) fieldGetter.get("negativePrefix", null);
+ } else if (name.equals("negativeSuffix")) {
+ ns = (String) fieldGetter.get("negativeSuffix", null);
+ } else if (name.equals("posPrefixPattern")) {
+ ppp = (String) fieldGetter.get("posPrefixPattern", null);
+ } else if (name.equals("posSuffixPattern")) {
+ psp = (String) fieldGetter.get("posSuffixPattern", null);
+ } else if (name.equals("positivePrefix")) {
+ pp = (String) fieldGetter.get("positivePrefix", null);
+ } else if (name.equals("positiveSuffix")) {
+ ps = (String) fieldGetter.get("positiveSuffix", null);
+ } else if (name.equals("roundingIncrement")) {
+ setRoundingIncrement((java.math.BigDecimal) fieldGetter.get("roundingIncrement", null));
+ } else if (name.equals("symbols")) {
+ setDecimalFormatSymbols((DecimalFormatSymbols) fieldGetter.get("symbols", null));
} else {
- // Always applyPattern after the symbols are set
- symbols = (DecimalFormatSymbols) inputSymbols.clone();
- currencyPluralInfo = info;
- // the pattern used in format is not fixed until formatting, in which, the
- // number is known and will be used to pick the right pattern based on plural
- // count. Here, set the pattern as the pattern of plural count == "other".
- // For most locale, the patterns are probably the same for all plural
- // count. If not, the right pattern need to be re-applied during format.
- String currencyPluralPatternForOther =
- currencyPluralInfo.getCurrencyPluralPattern("other");
- applyPatternWithoutExpandAffix(currencyPluralPatternForOther, false);
- setCurrencyForSymbols();
- }
- style = inputStyle;
- }
-
- /**
- * Creates a DecimalFormat for currency plural format from the given pattern, symbols,
- * and style.
- */
- DecimalFormat(String pattern, DecimalFormatSymbols inputSymbols, int style) {
- CurrencyPluralInfo info = null;
- if (style == NumberFormat.PLURALCURRENCYSTYLE) {
- info = new CurrencyPluralInfo(inputSymbols.getULocale());
- }
- create(pattern, inputSymbols, info, style);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) {
- return format(number, result, fieldPosition, false);
- }
-
- // See if number is negative.
- // usage: isNegative(multiply(numberToBeFormatted));
- private boolean isNegative(double number) {
- // Detecting whether a double is negative is easy with the exception of the value
- // -0.0. This is a double which has a zero mantissa (and exponent), but a negative
- // sign bit. It is semantically distinct from a zero with a positive sign bit, and
- // this distinction is important to certain kinds of computations. However, it's a
- // little tricky to detect, since (-0.0 == 0.0) and !(-0.0 < 0.0). How then, you
- // may ask, does it behave distinctly from +0.0? Well, 1/(-0.0) ==
- // -Infinity. Proper detection of -0.0 is needed to deal with the issues raised by
- // bugs 4106658, 4106667, and 4147706. Liu 7/6/98.
- return (number < 0.0) || (number == 0.0 && 1 / number < 0.0);
- }
-
- // Rounds the number and strips of the negative sign.
- // usage: round(multiply(numberToBeFormatted))
- private double round(double number) {
- boolean isNegative = isNegative(number);
- if (isNegative)
- number = -number;
-
- // Apply rounding after multiplier
- if (roundingDouble > 0.0) {
- // number = roundingDouble
- // * round(number / roundingDouble, roundingMode, isNegative);
- return round(
- number, roundingDouble, roundingDoubleReciprocal, roundingMode,
- isNegative);
- }
- return number;
- }
-
- // Multiplies given number by multipler (if there is one) returning the new
- // number. If there is no multiplier, returns the number passed in unchanged.
- private double multiply(double number) {
- if (multiplier != 1) {
- return number * multiplier;
- }
- return number;
- }
-
- // [Spark/CDL] The actual method to format number. If boolean value
- // parseAttr == true, then attribute information will be recorded.
- private StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition,
- boolean parseAttr) {
- fieldPosition.setBeginIndex(0);
- fieldPosition.setEndIndex(0);
-
- if (Double.isNaN(number)) {
- if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
- fieldPosition.setBeginIndex(result.length());
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
- fieldPosition.setBeginIndex(result.length());
- }
-
- result.append(symbols.getNaN());
- // TODO: Combine setting a single FieldPosition or adding to an AttributedCharacterIterator
- // into a function like recordAttribute(FieldAttribute, begin, end).
-
- // [Spark/CDL] Add attribute for NaN here.
- // result.append(symbols.getNaN());
- if (parseAttr) {
- addAttribute(Field.INTEGER, result.length() - symbols.getNaN().length(),
- result.length());
- }
- if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
- fieldPosition.setEndIndex(result.length());
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
- fieldPosition.setEndIndex(result.length());
- }
-
- addPadding(result, fieldPosition, 0, 0);
- return result;
- }
-
- // Do this BEFORE checking to see if value is negative or infinite and
- // before rounding.
- number = multiply(number);
- boolean isNegative = isNegative(number);
- number = round(number);
-
- if (Double.isInfinite(number)) {
- int prefixLen = appendAffix(result, isNegative, true, fieldPosition, parseAttr);
-
- if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
- fieldPosition.setBeginIndex(result.length());
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
- fieldPosition.setBeginIndex(result.length());
- }
-
- // [Spark/CDL] Add attribute for infinity here.
- result.append(symbols.getInfinity());
- if (parseAttr) {
- addAttribute(Field.INTEGER, result.length() - symbols.getInfinity().length(),
- result.length());
- }
- if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
- fieldPosition.setEndIndex(result.length());
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
- fieldPosition.setEndIndex(result.length());
- }
-
- int suffixLen = appendAffix(result, isNegative, false, fieldPosition, parseAttr);
-
- addPadding(result, fieldPosition, prefixLen, suffixLen);
- return result;
- }
-
- int precision = precision(false);
-
- // This is to fix rounding for scientific notation. See ticket:10542.
- // This code should go away when a permanent fix is done for ticket:9931.
- //
- // This block of code only executes for scientific notation so it will not interfere with the
- // previous fix in {@link #resetActualRounding} for fixed decimal numbers.
- // Moreover this code only runs when there is rounding to be done (precision > 0) and when the
- // rounding mode is something other than ROUND_HALF_EVEN.
- // This block of code does the correct rounding of number in advance so that it will fit into
- // the number of digits indicated by precision. In this way, we avoid using the default
- // ROUND_HALF_EVEN behavior of DigitList. For example, if number = 0.003016 and roundingMode =
- // ROUND_DOWN and precision = 3 then after this code executes, number = 0.00301 (3 significant digits)
- if (useExponentialNotation && precision > 0 && number != 0.0 && roundingMode != BigDecimal.ROUND_HALF_EVEN) {
- int log10RoundingIncr = 1 - precision + (int) Math.floor(Math.log10(Math.abs(number)));
- double roundingIncReciprocal = 0.0;
- double roundingInc = 0.0;
- if (log10RoundingIncr < 0) {
- roundingIncReciprocal =
- BigDecimal.ONE.movePointRight(-log10RoundingIncr).doubleValue();
- } else {
- roundingInc =
- BigDecimal.ONE.movePointRight(log10RoundingIncr).doubleValue();
- }
- number = DecimalFormat.round(number, roundingInc, roundingIncReciprocal, roundingMode, isNegative);
- }
- // End fix for ticket:10542
-
- // At this point we are guaranteed a nonnegative finite
- // number.
- synchronized (digitList) {
- digitList.set(number, precision, !useExponentialNotation &&
- !areSignificantDigitsUsed());
- return subformat(number, result, fieldPosition, isNegative, false, parseAttr);
- }
- }
-
+ // The following fields are ignored:
+ // "PARSE_MAX_EXPONENT"
+ // "currencySignCount"
+ // "style"
+ // "attributes"
+ // "currencyChoice"
+ // "formatPattern"
+ // "currencyUsage" => ignore this because the old code puts currencyUsage directly into min/max fraction.
+ }
+ }
+ // Resolve affixes
+ if (npp == null) {
+ properties.setNegativePrefix(np);
+ } else {
+ properties.setNegativePrefixPattern(npp);
+ }
+ if (nsp == null) {
+ properties.setNegativeSuffix(ns);
+ } else {
+ properties.setNegativeSuffixPattern(nsp);
+ }
+ if (ppp == null) {
+ properties.setPositivePrefix(pp);
+ } else {
+ properties.setPositivePrefixPattern(ppp);
+ }
+ if (psp == null) {
+ properties.setPositiveSuffix(ps);
+ } else {
+ properties.setPositiveSuffixPattern(psp);
+ }
+ // Extract values from parent NumberFormat class. Have to use reflection here.
+ java.lang.reflect.Field getter;
+ try {
+ getter = NumberFormat.class.getDeclaredField("groupingUsed");
+ getter.setAccessible(true);
+ setGroupingUsed((Boolean) getter.get(this));
+ getter = NumberFormat.class.getDeclaredField("parseIntegerOnly");
+ getter.setAccessible(true);
+ setParseIntegerOnly((Boolean) getter.get(this));
+ getter = NumberFormat.class.getDeclaredField("maximumIntegerDigits");
+ getter.setAccessible(true);
+ setMaximumIntegerDigits((Integer) getter.get(this));
+ getter = NumberFormat.class.getDeclaredField("minimumIntegerDigits");
+ getter.setAccessible(true);
+ setMinimumIntegerDigits((Integer) getter.get(this));
+ getter = NumberFormat.class.getDeclaredField("maximumFractionDigits");
+ getter.setAccessible(true);
+ setMaximumFractionDigits((Integer) getter.get(this));
+ getter = NumberFormat.class.getDeclaredField("minimumFractionDigits");
+ getter.setAccessible(true);
+ setMinimumFractionDigits((Integer) getter.get(this));
+ getter = NumberFormat.class.getDeclaredField("currency");
+ getter.setAccessible(true);
+ setCurrency((Currency) getter.get(this));
+ getter = NumberFormat.class.getDeclaredField("parseStrict");
+ getter.setAccessible(true);
+ setParseStrict((Boolean) getter.get(this));
+ } catch (IllegalArgumentException e) {
+ throw new IOException(e);
+ } catch (IllegalAccessException e) {
+ throw new IOException(e);
+ } catch (NoSuchFieldException e) {
+ throw new IOException(e);
+ } catch (SecurityException e) {
+ throw new IOException(e);
+ }
+ // Finish initialization
+ if (symbols == null) {
+ symbols = getDefaultSymbols();
+ }
+ exportedProperties = new Properties();
+ refreshFormatter();
+ }
+ }
+
+ //=====================================================================================//
+ // FORMAT AND PARSE APIS //
+ //=====================================================================================//
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) {
+ FormatQuantity4 fq = new FormatQuantity4(number);
+ formatter.format(fq, result, fieldPosition);
+ fq.populateUFieldPosition(fieldPosition);
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition) {
+ FormatQuantity4 fq = new FormatQuantity4(number);
+ formatter.format(fq, result, fieldPosition);
+ fq.populateUFieldPosition(fieldPosition);
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public StringBuffer format(BigInteger number, StringBuffer result, FieldPosition fieldPosition) {
+ FormatQuantity4 fq = new FormatQuantity4(number);
+ formatter.format(fq, result, fieldPosition);
+ fq.populateUFieldPosition(fieldPosition);
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public StringBuffer format(
+ java.math.BigDecimal number, StringBuffer result, FieldPosition fieldPosition) {
+ FormatQuantity4 fq = new FormatQuantity4(number);
+ formatter.format(fq, result, fieldPosition);
+ fq.populateUFieldPosition(fieldPosition);
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public StringBuffer format(BigDecimal number, StringBuffer result, FieldPosition fieldPosition) {
+ FormatQuantity4 fq = new FormatQuantity4(number.toBigDecimal());
+ formatter.format(fq, result, fieldPosition);
+ fq.populateUFieldPosition(fieldPosition);
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
+ if (!(obj instanceof Number)) throw new IllegalArgumentException();
+ Number number = (Number) obj;
+ FormatQuantity4 fq = new FormatQuantity4(number);
+ AttributedCharacterIterator result = formatter.formatToCharacterIterator(fq);
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) {
+ // TODO: This is ugly (although not as ugly as it was in ICU 58).
+ // Currency should be a free parameter, not in property bag. Fix in ICU 60.
+ Properties cprops = threadLocalProperties.get();
+ SingularFormat fmt = null;
+ synchronized (this) {
+ // Use the pre-compiled formatter if possible. Otherwise, copy the properties
+ // and build our own formatter.
+ // TODO: Consider using a static format path here.
+ if (currAmt.getCurrency().equals(properties.getCurrency())) {
+ fmt = formatter;
+ } else {
+ cprops.copyFrom(properties);
+ }
+ }
+ if (fmt == null) {
+ cprops.setCurrency(currAmt.getCurrency());
+ fmt = Endpoint.fromBTA(cprops, symbols);
+ }
+ FormatQuantity4 fq = new FormatQuantity4(currAmt.getNumber());
+ fmt.format(fq, toAppendTo, pos);
+ fq.populateUFieldPosition(pos);
+ return toAppendTo;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Number parse(String text, ParsePosition parsePosition) {
+ Properties pprops = threadLocalProperties.get();
+ synchronized (this) {
+ pprops.copyFrom(properties);
+ }
+ // Backwards compatibility: use currency parse mode if this is a currency instance
+ Number result = Parse.parse(text, parsePosition, pprops, symbols);
+ // Backwards compatibility: return android.icu.math.BigDecimal
+ if (result instanceof java.math.BigDecimal) {
+ result = safeConvertBigDecimal((java.math.BigDecimal) result);
+ }
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CurrencyAmount parseCurrency(CharSequence text, ParsePosition parsePosition) {
+ try {
+ CurrencyAmount result = Parse.parseCurrency(text, parsePosition, properties, symbols);
+ if (result == null) return null;
+ Number number = result.getNumber();
+ // Backwards compatibility: return android.icu.math.BigDecimal
+ if (number instanceof java.math.BigDecimal) {
+ number = safeConvertBigDecimal((java.math.BigDecimal) number);
+ result = new CurrencyAmount(number, result.getCurrency());
+ }
+ return result;
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ //=====================================================================================//
+ // GETTERS AND SETTERS //
+ //=====================================================================================//
+
+ /**
+ * Returns a copy of the decimal format symbols used by this formatter.
+ *
+ * @return desired DecimalFormatSymbols
+ * @see DecimalFormatSymbols
+ */
+ public synchronized DecimalFormatSymbols getDecimalFormatSymbols() {
+ return (DecimalFormatSymbols) symbols.clone();
+ }
+
+ /**
+ * Sets the decimal format symbols used by this formatter. The formatter uses a copy of the
+ * provided symbols.
+ *
+ * @param newSymbols desired DecimalFormatSymbols
+ * @see DecimalFormatSymbols
+ */
+ public synchronized void setDecimalFormatSymbols(DecimalFormatSymbols newSymbols) {
+ symbols = (DecimalFormatSymbols) newSymbols.clone();
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>Affixes:</strong> Gets the positive prefix string currently being used to format
+ * numbers.
+ *
+ * <p>If the affix was specified via the pattern, the string returned by this method will have
+ * locale symbols substituted in place of special characters according to the LDML specification.
+ * If the affix was specified via {@link #setPositivePrefix}, the string will be returned
+ * literally.
+ *
+ * @return The string being prepended to positive numbers.
+ */
+ public synchronized String getPositivePrefix() {
+ String result = exportedProperties.getPositivePrefix();
+ return (result == null) ? "" : result;
+ }
+
+ /**
+ * <strong>Affixes:</strong> Sets the string to prepend to positive numbers. For example, if you
+ * set the value "#", then the number 123 will be formatted as "#123" in the locale
+ * <em>en-US</em>.
+ *
+ * <p>Using this method overrides the affix specified via the pattern, and unlike the pattern, the
+ * string given to this method will be interpreted literally WITHOUT locale symbol substitutions.
+ *
+ * @param prefix The literal string to prepend to positive numbers.
+ */
+ public synchronized void setPositivePrefix(String prefix) {
+ if (prefix == null) {
+ throw new NullPointerException();
+ }
+ properties.setPositivePrefix(prefix);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>Affixes:</strong> Gets the negative prefix string currently being used to format
+ * numbers.
+ *
+ * <p>If the affix was specified via the pattern, the string returned by this method will have
+ * locale symbols substituted in place of special characters according to the LDML specification.
+ * If the affix was specified via {@link #setNegativePrefix}, the string will be returned
+ * literally.
+ *
+ * @return The string being prepended to negative numbers.
+ */
+ public synchronized String getNegativePrefix() {
+ String result = exportedProperties.getNegativePrefix();
+ return (result == null) ? "" : result;
+ }
+
+ /**
+ * <strong>Affixes:</strong> Sets the string to prepend to negative numbers. For example, if you
+ * set the value "#", then the number -123 will be formatted as "#123" in the locale
+ * <em>en-US</em> (overriding the implicit default '-' in the pattern).
+ *
+ * <p>Using this method overrides the affix specified via the pattern, and unlike the pattern, the
+ * string given to this method will be interpreted literally WITHOUT locale symbol substitutions.
+ *
+ * @param prefix The literal string to prepend to negative numbers.
+ */
+ public synchronized void setNegativePrefix(String prefix) {
+ if (prefix == null) {
+ throw new NullPointerException();
+ }
+ properties.setNegativePrefix(prefix);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>Affixes:</strong> Gets the positive suffix string currently being used to format
+ * numbers.
+ *
+ * <p>If the affix was specified via the pattern, the string returned by this method will have
+ * locale symbols substituted in place of special characters according to the LDML specification.
+ * If the affix was specified via {@link #setPositiveSuffix}, the string will be returned
+ * literally.
+ *
+ * @return The string being appended to positive numbers.
+ */
+ public synchronized String getPositiveSuffix() {
+ String result = exportedProperties.getPositiveSuffix();
+ return (result == null) ? "" : result;
+ }
+
+ /**
+ * <strong>Affixes:</strong> Sets the string to append to positive numbers. For example, if you
+ * set the value "#", then the number 123 will be formatted as "123#" in the locale
+ * <em>en-US</em>.
+ *
+ * <p>Using this method overrides the affix specified via the pattern, and unlike the pattern, the
+ * string given to this method will be interpreted literally WITHOUT locale symbol substitutions.
+ *
+ * @param suffix The literal string to append to positive numbers.
+ */
+ public synchronized void setPositiveSuffix(String suffix) {
+ if (suffix == null) {
+ throw new NullPointerException();
+ }
+ properties.setPositiveSuffix(suffix);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>Affixes:</strong> Gets the negative suffix string currently being used to format
+ * numbers.
+ *
+ * <p>If the affix was specified via the pattern, the string returned by this method will have
+ * locale symbols substituted in place of special characters according to the LDML specification.
+ * If the affix was specified via {@link #setNegativeSuffix}, the string will be returned
+ * literally.
+ *
+ * @return The string being appended to negative numbers.
+ */
+ public synchronized String getNegativeSuffix() {
+ String result = exportedProperties.getNegativeSuffix();
+ return (result == null) ? "" : result;
+ }
+
+ /**
+ * <strong>Affixes:</strong> Sets the string to append to negative numbers. For example, if you
+ * set the value "#", then the number 123 will be formatted as "123#" in the locale
+ * <em>en-US</em>.
+ *
+ * <p>Using this method overrides the affix specified via the pattern, and unlike the pattern, the
+ * string given to this method will be interpreted literally WITHOUT locale symbol substitutions.
+ *
+ * @param suffix The literal string to append to negative numbers.
+ */
+ public synchronized void setNegativeSuffix(String suffix) {
+ if (suffix == null) {
+ throw new NullPointerException();
+ }
+ properties.setNegativeSuffix(suffix);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns whether the sign is being shown on positive numbers.
+ *
+ * @see #setSignAlwaysShown
+ * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public synchronized boolean getSignAlwaysShown() {
+ // This is not in the exported properties
+ return properties.getSignAlwaysShown();
+ }
+
+ /**
+ * Sets whether to always shown the plus sign ('+' in <em>en</em>) on positive numbers. The rules
+ * in UTS #35 section 3.2.1 will be followed to ensure a locale-aware placement of the sign.
+ *
+ * <p>More specifically, the following strategy will be used to place the plus sign:
+ *
+ * <ol>
+ * <li><em>Patterns without a negative subpattern:</em> The locale's plus sign will be prepended
+ * to the positive prefix.
+ * <li><em>Patterns with a negative subpattern without a '-' sign (e.g., accounting):</em> The
+ * locale's plus sign will be prepended to the positive prefix, as in case 1.
+ * <li><em>Patterns with a negative subpattern that has a '-' sign:</em> The locale's plus sign
+ * will substitute the '-' in the negative subpattern. The positive subpattern will be
+ * unused.
+ * </ol>
+ *
+ * This method is designed to be used <em>instead of</em> applying a pattern containing an
+ * explicit plus sign, such as "+0;-0". The behavior when combining this method with explicit plus
+ * signs in the pattern is undefined.
+ *
+ * @param value true to always show a sign; false to hide the sign on positive numbers.
+ * @deprecated ICU 59: This API is technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public synchronized void setSignAlwaysShown(boolean value) {
+ properties.setSignAlwaysShown(value);
+ refreshFormatter();
+ }
+
+ /**
+ * Returns the multiplier being applied to numbers before they are formatted.
+ *
+ * @see #setMultiplier
+ */
+ public synchronized int getMultiplier() {
+ if (properties.getMultiplier() != null) {
+ return properties.getMultiplier().intValue();
+ } else {
+ return (int) Math.pow(10, properties.getMagnitudeMultiplier());
+ }
+ }
+
+ /**
+ * Sets a number that will be used to multiply all numbers prior to formatting. For example, when
+ * formatting percents, a multiplier of 100 can be used.
+ *
+ * <p>If a percent or permille sign is specified in the pattern, the multiplier is automatically
+ * set to 100 or 1000, respectively.
+ *
+ * <p>If the number specified here is a power of 10, a more efficient code path will be used.
+ *
+ * @param multiplier The number by which all numbers passed to {@link #format} will be multiplied.
+ * @throws IllegalArgumentException If the given multiplier is zero.
+ */
+ public synchronized void setMultiplier(int multiplier) {
+ if (multiplier == 0) {
+ throw new IllegalArgumentException("Multiplier must be nonzero.");
+ }
+
+ // Try to convert to a magnitude multiplier first
+ int delta = 0;
+ int value = multiplier;
+ while (multiplier != 1) {
+ delta++;
+ int temp = value / 10;
+ if (temp * 10 != value) {
+ delta = -1;
+ break;
+ }
+ value = temp;
+ }
+ if (delta != -1) {
+ properties.setMagnitudeMultiplier(delta);
+ } else {
+ properties.setMultiplier(java.math.BigDecimal.valueOf(multiplier));
+ }
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns the increment to which numbers are being rounded.
+ *
+ * @see #setRoundingIncrement
+ */
+ public synchronized java.math.BigDecimal getRoundingIncrement() {
+ return exportedProperties.getRoundingIncrement();
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Rounding and Digit Limits:</strong> Sets an increment, or interval, to which
+ * numbers are rounded. For example, a rounding increment of 0.05 will cause the number 1.23 to be
+ * rounded to 1.25 in the default rounding mode.
+ *
+ * <p>The rounding increment can be specified via the pattern string: for example, the pattern
+ * "#,##0.05" encodes a rounding increment of 0.05.
+ *
+ * <p>The rounding increment is applied <em>after</em> any multipliers might take effect; for
+ * example, in scientific notation or when {@link #setMultiplier} is used.
+ *
+ * <p>See {@link #setMaximumFractionDigits} and {@link #setMaximumSignificantDigits} for two other
+ * ways of specifying rounding strategies.
+ *
+ * @param increment The increment to which numbers are to be rounded.
+ * @see #setRoundingMode
+ * @see #setMaximumFractionDigits
+ * @see #setMaximumSignificantDigits
+ */
+ public synchronized void setRoundingIncrement(java.math.BigDecimal increment) {
+ // Backwards compatibility: ignore rounding increment if zero,
+ // and instead set maximum fraction digits.
+ if (increment != null && increment.compareTo(java.math.BigDecimal.ZERO) == 0) {
+ properties.setMaximumFractionDigits(Integer.MAX_VALUE);
+ return;
+ }
+
+ properties.setRoundingIncrement(increment);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Rounding and Digit Limits:</strong> Overload of {@link
+ * #setRoundingIncrement(java.math.BigDecimal)}.
+ *
+ * @param increment The increment to which numbers are to be rounded.
+ * @see #setRoundingIncrement
+ */
+ public synchronized void setRoundingIncrement(BigDecimal increment) {
+ java.math.BigDecimal javaBigDecimal = (increment == null) ? null : increment.toBigDecimal();
+ setRoundingIncrement(javaBigDecimal);
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Rounding and Digit Limits:</strong> Overload of {@link
+ * #setRoundingIncrement(java.math.BigDecimal)}.
+ *
+ * @param increment The increment to which numbers are to be rounded.
+ * @see #setRoundingIncrement
+ */
+ public synchronized void setRoundingIncrement(double increment) {
+ if (increment == 0) {
+ setRoundingIncrement((java.math.BigDecimal) null);
+ } else {
+ java.math.BigDecimal javaBigDecimal = java.math.BigDecimal.valueOf(increment);
+ setRoundingIncrement(javaBigDecimal);
+ }
+ }
+
+ /**
+ * Returns the rounding mode being used to round numbers.
+ *
+ * @see #setRoundingMode
+ */
+ @Override
+ public synchronized int getRoundingMode() {
+ RoundingMode mode = exportedProperties.getRoundingMode();
+ return (mode == null) ? 0 : mode.ordinal();
+ }
+
+ /**
+ * <strong>Rounding and Digit Limits:</strong> Sets the {@link RoundingMode} used to round
+ * numbers. The default rounding mode is HALF_EVEN, which rounds decimals to their closest whole
+ * number, and rounds to the closest even number if at the midpoint.
+ *
+ * <p>For more detail on rounding modes, see <a
+ * href="http://userguide.icu-project.org/formatparse/numbers/rounding-modes">the ICU User
+ * Guide</a>.
+ *
+ * <p>For backwards compatibility, the rounding mode is specified as an int argument, which can be
+ * from either the constants in {@link BigDecimal} or the ordinal value of {@link RoundingMode}.
+ * The following two calls are functionally equivalent.
+ *
+ * <pre>
+ * df.setRoundingMode(BigDecimal.ROUND_CEILING);
+ * df.setRoundingMode(RoundingMode.CEILING.ordinal());
+ * </pre>
+ *
+ * @param roundingMode The integer constant rounding mode to use when formatting numbers.
+ */
+ @Override
+ public synchronized void setRoundingMode(int roundingMode) {
+ properties.setRoundingMode(RoundingMode.valueOf(roundingMode));
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns the {@link java.math.MathContext} being used to round numbers.
+ *
+ * @see #setMathContext
+ */
+ public synchronized java.math.MathContext getMathContext() {
+ java.math.MathContext mathContext = exportedProperties.getMathContext();
+ assert mathContext != null;
+ return mathContext;
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Rounding and Digit Limits:</strong> Sets the {@link java.math.MathContext} used
+ * to round numbers. A "math context" encodes both a rounding mode and a number of significant
+ * digits. Most users should call {@link #setRoundingMode} and/or {@link
+ * #setMaximumSignificantDigits} instead of this method.
+ *
+ * <p>When formatting, since no division is ever performed, the default MathContext is unlimited
+ * significant digits. However, when division occurs during parsing to correct for percentages and
+ * multipliers, a MathContext of 34 digits, the IEEE 754R Decimal128 standard, is used by default.
+ * If you require more than 34 digits when parsing, you can set a custom MathContext using this
+ * method.
+ *
+ * @param mathContext The MathContext to use when rounding numbers.
+ * @see java.math.MathContext
+ */
+ public synchronized void setMathContext(java.math.MathContext mathContext) {
+ properties.setMathContext(mathContext);
+ refreshFormatter();
+ }
+
+ // Remember the ICU math context form in order to be able to return it from the API.
+ // NOTE: This value is not serialized. (should it be?)
+ private transient int icuMathContextForm = MathContext.PLAIN;
+
+ /**
+ * <strong>[icu]</strong> Returns the {@link android.icu.math.MathContext} being used to round numbers.
+ *
+ * @see #setMathContext
+ */
+ public synchronized MathContext getMathContextICU() {
+ java.math.MathContext mathContext = getMathContext();
+ return new MathContext(
+ mathContext.getPrecision(),
+ icuMathContextForm,
+ false,
+ mathContext.getRoundingMode().ordinal());
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Rounding and Digit Limits:</strong> Overload of {@link #setMathContext} for
+ * {@link android.icu.math.MathContext}.
+ *
+ * @param mathContextICU The MathContext to use when rounding numbers.
+ * @see #setMathContext(java.math.MathContext)
+ */
+ public synchronized void setMathContextICU(MathContext mathContextICU) {
+ icuMathContextForm = mathContextICU.getForm();
+ java.math.MathContext mathContext;
+ if (mathContextICU.getLostDigits()) {
+ // The getLostDigits() feature in ICU MathContext means "throw an ArithmeticException if
+ // rounding causes digits to be lost". That feature is called RoundingMode.UNNECESSARY in
+ // Java MathContext.
+ mathContext = new java.math.MathContext(mathContextICU.getDigits(), RoundingMode.UNNECESSARY);
+ } else {
+ mathContext =
+ new java.math.MathContext(
+ mathContextICU.getDigits(), RoundingMode.valueOf(mathContextICU.getRoundingMode()));
+ }
+ setMathContext(mathContext);
+ }
+
+ /**
+ * Returns the effective minimum number of digits before the decimal separator.
+ *
+ * @see #setMinimumIntegerDigits
+ */
+ @Override
+ public synchronized int getMinimumIntegerDigits() {
+ return exportedProperties.getMinimumIntegerDigits();
+ }
+
+ /**
+ * <strong>Rounding and Digit Limits:</strong> Sets the minimum number of digits to display before
+ * the decimal separator. If the number has fewer than this many digits, the number is padded with
+ * zeros.
+ *
+ * <p>For example, if minimum integer digits is 3, the number 12.3 will be printed as "001.23".
+ *
+ * <p>Minimum integer and minimum and maximum fraction digits can be specified via the pattern
+ * string. For example, "#,#00.00#" has 2 minimum integer digits, 2 minimum fraction digits, and 3
+ * maximum fraction digits. Note that it is not possible to specify maximium integer digits in the
+ * pattern except in scientific notation.
+ *
+ * <p>If minimum and maximum integer, fraction, or significant digits conflict with each other,
+ * the most recently specified value is used. For example, if there is a formatter with minInt=5,
+ * and then you set maxInt=3, then minInt will be changed to 3.
+ *
+ * @param value The minimum number of digits before the decimal separator.
+ */
+ @Override
+ public synchronized void setMinimumIntegerDigits(int value) {
+ // For backwards compatibility, conflicting min/max need to keep the most recent setting.
+ int max = properties.getMaximumIntegerDigits();
+ if (max >= 0 && max < value) {
+ properties.setMaximumIntegerDigits(value);
+ }
+ properties.setMinimumIntegerDigits(value);
+ refreshFormatter();
+ }
+
+ /**
+ * Returns the effective maximum number of digits before the decimal separator.
+ *
+ * @see #setMaximumIntegerDigits
+ */
+ @Override
+ public synchronized int getMaximumIntegerDigits() {
+ return exportedProperties.getMaximumIntegerDigits();
+ }
+
+ /**
+ * <strong>Rounding and Digit Limits:</strong> Sets the maximum number of digits to display before
+ * the decimal separator. If the number has more than this many digits, the number is truncated.
+ *
+ * <p>For example, if maximum integer digits is 3, the number 12345 will be printed as "345".
+ *
+ * <p>Minimum integer and minimum and maximum fraction digits can be specified via the pattern
+ * string. For example, "#,#00.00#" has 2 minimum integer digits, 2 minimum fraction digits, and 3
+ * maximum fraction digits. Note that it is not possible to specify maximium integer digits in the
+ * pattern except in scientific notation.
+ *
+ * <p>If minimum and maximum integer, fraction, or significant digits conflict with each other,
+ * the most recently specified value is used. For example, if there is a formatter with minInt=5,
+ * and then you set maxInt=3, then minInt will be changed to 3.
+ *
+ * @param value The maximum number of digits before the decimal separator.
+ */
+ @Override
+ public synchronized void setMaximumIntegerDigits(int value) {
+ int min = properties.getMinimumIntegerDigits();
+ if (min >= 0 && min > value) {
+ properties.setMinimumIntegerDigits(value);
+ }
+ properties.setMaximumIntegerDigits(value);
+ refreshFormatter();
+ }
+
+ /**
+ * Returns the effective minimum number of integer digits after the decimal separator.
+ *
+ * @see #setMaximumIntegerDigits
+ */
+ @Override
+ public synchronized int getMinimumFractionDigits() {
+ return exportedProperties.getMinimumFractionDigits();
+ }
+
+ /**
+ * <strong>Rounding and Digit Limits:</strong> Sets the minimum number of digits to display after
+ * the decimal separator. If the number has fewer than this many digits, the number is padded with
+ * zeros.
+ *
+ * <p>For example, if minimum fraction digits is 2, the number 123.4 will be printed as "123.40".
+ *
+ * <p>Minimum integer and minimum and maximum fraction digits can be specified via the pattern
+ * string. For example, "#,#00.00#" has 2 minimum integer digits, 2 minimum fraction digits, and 3
+ * maximum fraction digits. Note that it is not possible to specify maximium integer digits in the
+ * pattern except in scientific notation.
+ *
+ * <p>If minimum and maximum integer, fraction, or significant digits conflict with each other,
+ * the most recently specified value is used. For example, if there is a formatter with minInt=5,
+ * and then you set maxInt=3, then minInt will be changed to 3.
+ *
+ * <p>See {@link #setRoundingIncrement} and {@link #setMaximumSignificantDigits} for two other
+ * ways of specifying rounding strategies.
+ *
+ * @param value The minimum number of integer digits after the decimal separator.
+ * @see #setRoundingMode
+ * @see #setRoundingIncrement
+ * @see #setMaximumSignificantDigits
+ */
+ @Override
+ public synchronized void setMinimumFractionDigits(int value) {
+ int max = properties.getMaximumFractionDigits();
+ if (max >= 0 && max < value) {
+ properties.setMaximumFractionDigits(value);
+ }
+ properties.setMinimumFractionDigits(value);
+ refreshFormatter();
+ }
+
+ /**
+ * Returns the effective maximum number of integer digits after the decimal separator.
+ *
+ * @see #setMaximumIntegerDigits
+ */
+ @Override
+ public synchronized int getMaximumFractionDigits() {
+ return exportedProperties.getMaximumFractionDigits();
+ }
+
+ /**
+ * <strong>Rounding and Digit Limits:</strong> Sets the maximum number of digits to display after
+ * the decimal separator. If the number has more than this many digits, the number is rounded
+ * according to the rounding mode.
+ *
+ * <p>For example, if maximum fraction digits is 2, the number 123.456 will be printed as
+ * "123.46".
+ *
+ * <p>Minimum integer and minimum and maximum fraction digits can be specified via the pattern
+ * string. For example, "#,#00.00#" has 2 minimum integer digits, 2 minimum fraction digits, and 3
+ * maximum fraction digits. Note that it is not possible to specify maximium integer digits in the
+ * pattern except in scientific notation.
+ *
+ * <p>If minimum and maximum integer, fraction, or significant digits conflict with each other,
+ * the most recently specified value is used. For example, if there is a formatter with minInt=5,
+ * and then you set maxInt=3, then minInt will be changed to 3.
+ *
+ * @param value The maximum number of integer digits after the decimal separator.
+ * @see #setRoundingMode
+ */
+ @Override
+ public synchronized void setMaximumFractionDigits(int value) {
+ int min = properties.getMinimumFractionDigits();
+ if (min >= 0 && min > value) {
+ properties.setMinimumFractionDigits(value);
+ }
+ properties.setMaximumFractionDigits(value);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns whether significant digits are being used in rounding.
+ *
+ * @see #setSignificantDigitsUsed
+ */
+ public synchronized boolean areSignificantDigitsUsed() {
+ return SignificantDigitsRounder.useSignificantDigits(properties);
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Rounding and Digit Limits:</strong> Sets whether significant digits are to be
+ * used in rounding.
+ *
+ * <p>Calling <code>df.setSignificantDigitsUsed(true)</code> is functionally equivalent to:
+ *
+ * <pre>
+ * df.setMinimumSignificantDigits(1);
+ * df.setMaximumSignificantDigits(6);
+ * </pre>
+ *
+ * @param useSignificantDigits true to enable significant digit rounding; false to disable it.
+ */
+ public synchronized void setSignificantDigitsUsed(boolean useSignificantDigits) {
+ if (useSignificantDigits) {
+ // These are the default values from the old implementation.
+ properties.setMinimumSignificantDigits(1);
+ properties.setMaximumSignificantDigits(6);
+ } else {
+ properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS);
+ properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS);
+ properties.setSignificantDigitsMode(null);
+ }
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns the effective minimum number of significant digits displayed.
+ *
+ * @see #setMinimumSignificantDigits
+ */
+ public synchronized int getMinimumSignificantDigits() {
+ return exportedProperties.getMinimumSignificantDigits();
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Rounding and Digit Limits:</strong> Sets the minimum number of significant
+ * digits to be displayed. If the number of significant digits is less than this value, the number
+ * will be padded with zeros as necessary.
+ *
+ * <p>For example, if minimum significant digits is 3 and the number is 1.2, the number will be
+ * printed as "1.20".
+ *
+ * <p>If minimum and maximum integer, fraction, or significant digits conflict with each other,
+ * the most recently specified value is used. For example, if there is a formatter with minInt=5,
+ * and then you set maxInt=3, then minInt will be changed to 3.
+ *
+ * @param value The minimum number of significant digits to display.
+ */
+ public synchronized void setMinimumSignificantDigits(int value) {
+ int max = properties.getMaximumSignificantDigits();
+ if (max >= 0 && max < value) {
+ properties.setMaximumSignificantDigits(value);
+ }
+ properties.setMinimumSignificantDigits(value);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns the effective maximum number of significant digits displayed.
+ *
+ * @see #setMaximumSignificantDigits
+ */
+ public synchronized int getMaximumSignificantDigits() {
+ return exportedProperties.getMaximumSignificantDigits();
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Rounding and Digit Limits:</strong> Sets the maximum number of significant
+ * digits to be displayed. If the number of significant digits in the number exceeds this value,
+ * the number will be rounded according to the current rounding mode.
+ *
+ * <p>For example, if maximum significant digits is 3 and the number is 12345, the number will be
+ * printed as "12300".
+ *
+ * <p>If minimum and maximum integer, fraction, or significant digits conflict with each other,
+ * the most recently specified value is used. For example, if there is a formatter with minInt=5,
+ * and then you set maxInt=3, then minInt will be changed to 3.
+ *
+ * <p>See {@link #setRoundingIncrement} and {@link #setMaximumFractionDigits} for two other ways
+ * of specifying rounding strategies.
+ *
+ * @param value The maximum number of significant digits to display.
+ * @see #setRoundingMode
+ * @see #setRoundingIncrement
+ * @see #setMaximumFractionDigits
+ */
+ public synchronized void setMaximumSignificantDigits(int value) {
+ int min = properties.getMinimumSignificantDigits();
+ if (min >= 0 && min > value) {
+ properties.setMinimumSignificantDigits(value);
+ }
+ properties.setMaximumSignificantDigits(value);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns the current significant digits mode.
+ *
+ * @see #setSignificantDigitsMode
+ * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public synchronized SignificantDigitsMode getSignificantDigitsMode() {
+ return exportedProperties.getSignificantDigitsMode();
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Rounding and Digit Limits:</strong> Sets the strategy used for resolving
+ * minimum/maximum significant digits when minimum/maximum integer and/or fraction digits are
+ * specified. There are three modes:
+ *
+ * <ul>
+ * <li>Mode A: OVERRIDE_MAXIMUM_FRACTION. This is the default. Settings in maximum fraction are
+ * ignored.
+ * <li>Mode B: RESPECT_MAXIMUM_FRACTION. Round to maximum fraction even if doing so will prevent
+ * minimum significant from being respected.
+ * <li>Mode C: ENSURE_MINIMUM_SIGNIFICANT. Respect maximum fraction, but always ensure that
+ * minimum significant digits are shown.
+ * </ul>
+ *
+ * <p>The following table illustrates the difference. Below, minFrac=1, maxFrac=2, minSig=3, and
+ * maxSig=4:
+ *
+ * <pre>
+ * Mode A | Mode B | Mode C
+ * ---------+----------+----------
+ * 12340.0 | 12340.0 | 12340.0
+ * 1234.0 | 1234.0 | 1234.0
+ * 123.4 | 123.4 | 123.4
+ * 12.34 | 12.34 | 12.34
+ * 1.234 | 1.23 | 1.23
+ * 0.1234 | 0.12 | 0.123
+ * 0.01234 | 0.01 | 0.0123
+ * 0.001234 | 0.00 | 0.00123
+ * </pre>
+ *
+ * @param mode The significant digits mode to use.
+ * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public synchronized void setSignificantDigitsMode(SignificantDigitsMode mode) {
+ properties.setSignificantDigitsMode(mode);
+ refreshFormatter();
+ }
+
+ /**
+ * Returns the minimum number of characters in formatted output.
+ *
+ * @see #setFormatWidth
+ */
+ public synchronized int getFormatWidth() {
+ return exportedProperties.getFormatWidth();
+ }
+
+ /**
+ * <strong>Padding:</strong> Sets the minimum width of the string output by the formatting
+ * pipeline. For example, if padding is enabled and paddingWidth is set to 6, formatting the
+ * number "3.14159" with the pattern "0.00" will result in "··3.14" if '·' is your padding string.
+ *
+ * <p>If the number is longer than your padding width, the number will display as if no padding
+ * width had been specified, which may result in strings longer than the padding width.
+ *
+ * <p>Padding can be specified in the pattern string using the '*' symbol. For example, the format
+ * "*x######0" has a format width of 7 and a pad character of 'x'.
+ *
+ * <p>Padding is currently counted in UTF-16 code units; see <a
+ * href="http://bugs.icu-project.org/trac/ticket/13034">ticket #13034</a> for more information.
+ *
+ * @param width The minimum number of characters in the output.
+ * @see #setPadCharacter
+ * @see #setPadPosition
+ */
+ public synchronized void setFormatWidth(int width) {
+ properties.setFormatWidth(width);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns the character used for padding.
+ *
+ * @see #setPadCharacter
+ */
+ public synchronized char getPadCharacter() {
+ CharSequence paddingString = exportedProperties.getPadString();
+ if (paddingString == null) {
+ return '.'; // TODO: Is this the correct behavior?
+ } else {
+ return paddingString.charAt(0);
+ }
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Padding:</strong> Sets the character used to pad numbers that are narrower than
+ * the width specified in {@link #setFormatWidth}.
+ *
+ * <p>In the pattern string, the padding character is the token that follows '*' before or after
+ * the prefix or suffix.
+ *
+ * @param padChar The character used for padding.
+ * @see #setFormatWidth
+ */
+ public synchronized void setPadCharacter(char padChar) {
+ properties.setPadString(Character.toString(padChar));
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns the position used for padding.
+ *
+ * @see #setPadPosition
+ */
+ public synchronized int getPadPosition() {
+ PadPosition loc = exportedProperties.getPadPosition();
+ return (loc == null) ? PAD_BEFORE_PREFIX : loc.toOld();
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Padding:</strong> Sets the position where to insert the pad character when
+ * narrower than the width specified in {@link #setFormatWidth}. For example, consider the pattern
+ * "P123S" with padding width 8 and padding char "*". The four positions are:
+ *
+ * <ul>
+ * <li>{@link DecimalFormat#PAD_BEFORE_PREFIX} ⇒ "***P123S"
+ * <li>{@link DecimalFormat#PAD_AFTER_PREFIX} ⇒ "P***123S"
+ * <li>{@link DecimalFormat#PAD_BEFORE_SUFFIX} ⇒ "P123***S"
+ * <li>{@link DecimalFormat#PAD_AFTER_SUFFIX} ⇒ "P123S***"
+ * </ul>
+ *
+ * @param padPos The position used for padding.
+ * @see #setFormatWidth
+ */
+ public synchronized void setPadPosition(int padPos) {
+ properties.setPadPosition(PadPosition.fromOld(padPos));
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns whether scientific (exponential) notation is enabled on this formatter.
+ *
+ * @see #setScientificNotation
+ */
+ public synchronized boolean isScientificNotation() {
+ return ScientificFormat.useScientificNotation(properties);
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Scientific Notation:</strong> Sets whether this formatter should print in
+ * scientific (exponential) notation. For example, if scientific notation is enabled, the number
+ * 123000 will be printed as "1.23E5" in locale <em>en-US</em>. A locale-specific symbol is used
+ * as the exponent separator.
+ *
+ * <p>Calling <code>df.setScientificNotation(true)</code> is functionally equivalent to calling
+ * <code>df.setMinimumExponentDigits(1)</code>.
+ *
+ * @param useScientific true to enable scientific notation; false to disable it.
+ * @see #setMinimumExponentDigits
+ */
+ public synchronized void setScientificNotation(boolean useScientific) {
+ if (useScientific) {
+ properties.setMinimumExponentDigits(1);
+ } else {
+ properties.setMinimumExponentDigits(Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS);
+ }
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns the minimum number of digits printed in the exponent in scientific notation.
+ *
+ * @see #setMinimumExponentDigits
+ */
+ public synchronized byte getMinimumExponentDigits() {
+ return (byte) exportedProperties.getMinimumExponentDigits();
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Scientific Notation:</strong> Sets the minimum number of digits to be printed in
+ * the exponent. For example, if minimum exponent digits is 3, the number 123000 will be printed
+ * as "1.23E005".
+ *
+ * <p>This setting corresponds to the number of zeros after the 'E' in a pattern string such as
+ * "0.00E000".
+ *
+ * @param minExpDig The minimum number of digits in the exponent.
+ */
+ public synchronized void setMinimumExponentDigits(byte minExpDig) {
+ properties.setMinimumExponentDigits(minExpDig);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns whether the sign (plus or minus) is always printed in scientific notation.
+ *
+ * @see #setExponentSignAlwaysShown
+ */
+ public synchronized boolean isExponentSignAlwaysShown() {
+ return exportedProperties.getExponentSignAlwaysShown();
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Scientific Notation:</strong> Sets whether the sign (plus or minus) is always to
+ * be shown in the exponent in scientific notation. For example, if this setting is enabled, the
+ * number 123000 will be printed as "1.23E+5" in locale <em>en-US</em>. The number 0.0000123 will
+ * always be printed as "1.23E-5" in locale <em>en-US</em> whether or not this setting is enabled.
+ *
+ * <p>This setting corresponds to the '+' in a pattern such as "0.00E+0".
+ *
+ * @param expSignAlways true to always shown the sign in the exponent; false to show it for
+ * negatives but not positives.
+ */
+ public synchronized void setExponentSignAlwaysShown(boolean expSignAlways) {
+ properties.setExponentSignAlwaysShown(expSignAlways);
+ refreshFormatter();
+ }
+
+ /**
+ * Returns whether or not grouping separators are being printed in the output.
+ *
+ * @see #setGroupingUsed
+ */
+ @Override
+ public synchronized boolean isGroupingUsed() {
+ return PositiveDecimalFormat.useGrouping(properties);
+ }
+
+ /**
+ * <strong>Grouping:</strong> Sets whether grouping is to be used when formatting numbers.
+ * Grouping means whether the thousands, millions, billions, and larger powers of ten should be
+ * separated by a grouping separator (a comma in <em>en-US</em>).
+ *
+ * <p>For example, if grouping is enabled, 12345 will be printed as "12,345" in <em>en-US</em>. If
+ * grouping were disabled, it would instead be printed as simply "12345".
+ *
+ * <p>Calling <code>df.setGroupingUsed(true)</code> is functionally equivalent to setting grouping
+ * size to 3, as in <code>df.setGroupingSize(3)</code>.
+ *
+ * @param enabled true to enable grouping separators; false to disable them.
+ * @see #setGroupingSize
+ * @see #setSecondaryGroupingSize
+ */
+ @Override
+ public synchronized void setGroupingUsed(boolean enabled) {
+ if (enabled) {
+ // Set to a reasonable default value
+ properties.setGroupingSize(3);
+ } else {
+ properties.setGroupingSize(Properties.DEFAULT_GROUPING_SIZE);
+ properties.setSecondaryGroupingSize(Properties.DEFAULT_SECONDARY_GROUPING_SIZE);
+ }
+ refreshFormatter();
+ }
+
+ /**
+ * Returns the primary grouping size in use.
+ *
+ * @see #setGroupingSize
+ */
+ public synchronized int getGroupingSize() {
+ return exportedProperties.getGroupingSize();
+ }
+
+ /**
+ * <strong>Grouping:</strong> Sets the primary grouping size (distance between grouping
+ * separators) used when formatting large numbers. For most locales, this defaults to 3: the
+ * number of digits between the ones and thousands place, between thousands and millions, and so
+ * forth.
+ *
+ * <p>For example, with a grouping size of 3, the number 1234567 will be formatted as "1,234,567".
+ *
+ * <p>Grouping size can also be specified in the pattern: for example, "#,##0" corresponds to a
+ * grouping size of 3.
+ *
+ * @param width The grouping size to use.
+ * @see #setSecondaryGroupingSize
+ */
+ public synchronized void setGroupingSize(int width) {
+ properties.setGroupingSize(width);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns the secondary grouping size in use.
+ *
+ * @see #setSecondaryGroupingSize
+ */
+ public synchronized int getSecondaryGroupingSize() {
+ return exportedProperties.getSecondaryGroupingSize();
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Grouping:</strong> Sets the secondary grouping size (distance between grouping
+ * separators after the first separator) used when formatting large numbers. In many south Asian
+ * locales, this is set to 2.
+ *
+ * <p>For example, with primary grouping size 3 and secondary grouping size 2, the number 1234567
+ * will be formatted as "12,34,567".
+ *
+ * <p>Grouping size can also be specified in the pattern: for example, "#,##,##0" corresponds to a
+ * primary grouping size of 3 and a secondary grouping size of 2.
+ *
+ * @param width The secondary grouping size to use.
+ * @see #setGroupingSize
+ */
+ public synchronized void setSecondaryGroupingSize(int width) {
+ properties.setSecondaryGroupingSize(width);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns the minimum number of digits before grouping is triggered.
+ *
+ * @see #setMinimumGroupingDigits
+ * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public synchronized int getMinimumGroupingDigits() {
+ return properties.getMinimumGroupingDigits();
+ }
+
+ /**
+ * <strong>[icu]</strong> Sets the minimum number of digits that must be before the first grouping separator in
+ * order for the grouping separator to be printed. For example, if minimum grouping digits is set
+ * 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.
+ * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public synchronized void setMinimumGroupingDigits(int number) {
+ properties.setMinimumGroupingDigits(number);
+ refreshFormatter();
+ }
+
+ /**
+ * Returns whether the decimal separator is shown on integers.
+ *
+ * @see #setDecimalSeparatorAlwaysShown
+ */
+ public synchronized boolean isDecimalSeparatorAlwaysShown() {
+ return exportedProperties.getDecimalSeparatorAlwaysShown();
+ }
+
+ /**
+ * <strong>Separators:</strong> Sets whether the decimal separator (a period in <em>en-US</em>) is
+ * shown on integers. For example, if this setting is turned on, formatting 123 will result in
+ * "123." with the decimal separator.
+ *
+ * <p>This setting can be specified in the pattern for integer formats: "#,##0." is an example.
+ *
+ * @param value true to always show the decimal separator; false to show it only when there is a
+ * fraction part of the number.
+ */
+ public synchronized void setDecimalSeparatorAlwaysShown(boolean value) {
+ properties.setDecimalSeparatorAlwaysShown(value);
+ refreshFormatter();
+ }
+
+ /**
+ * Returns the user-specified currency. May be null.
+ *
+ * @see #setCurrency
+ * @see DecimalFormatSymbols#getCurrency
+ */
+ @Override
+ public synchronized Currency getCurrency() {
+ return properties.getCurrency();
+ }
+
+ /**
+ * Sets the currency to be used when formatting numbers. The effect is twofold:
+ *
+ * <ol>
+ * <li>Substitutions for currency symbols in the pattern string will use this currency
+ * <li>The rounding mode will obey the rules for this currency (see {@link #setCurrencyUsage})
+ * </ol>
+ *
+ * <strong>Important:</strong> Displaying the currency in the output requires that the patter
+ * associated with this formatter contains a currency symbol '¤'. This will be the case if the
+ * instance was created via {@link #getCurrencyInstance} or one of its friends.
+ *
+ * @param currency The currency to use.
+ */
+ @Override
+ public synchronized void setCurrency(Currency currency) {
+ properties.setCurrency(currency);
+ // Backwards compatibility: also set the currency in the DecimalFormatSymbols
+ if (currency != null) {
+ symbols.setCurrency(currency);
+ String symbol = currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
+ symbols.setCurrencySymbol(symbol);
+ }
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns the strategy for rounding currency amounts.
+ *
+ * @see #setCurrencyUsage
+ */
+ public synchronized CurrencyUsage getCurrencyUsage() {
+ // CurrencyUsage is not exported, so we have to get it from the input property bag.
+ // TODO: Should we export CurrencyUsage instead?
+ CurrencyUsage usage = properties.getCurrencyUsage();
+ if (usage == null) {
+ usage = CurrencyUsage.STANDARD;
+ }
+ return usage;
+ }
+
+ /**
+ * <strong>[icu]</strong> Sets the currency-dependent strategy to use when rounding numbers. There are two
+ * strategies:
+ *
+ * <ul>
+ * <li>STANDARD: When the amount displayed is intended for banking statements or electronic
+ * transfer.
+ * <li>CASH: When the amount displayed is intended to be representable in physical currency,
+ * like at a cash register.
+ * </ul>
+ *
+ * CASH mode is relevant in currencies that do not have tender down to the penny. For more
+ * information on the two rounding strategies, see <a
+ * href="http://unicode.org/reports/tr35/tr35-numbers.html#Supplemental_Currency_Data">UTS
+ * #35</a>. If omitted, the strategy defaults to STANDARD. To override currency rounding
+ * altogether, use {@link #setMinimumFractionDigits} and {@link #setMaximumFractionDigits} or
+ * {@link #setRoundingIncrement}.
+ *
+ * @param usage The strategy to use when rounding in the current currency.
+ */
+ public synchronized void setCurrencyUsage(CurrencyUsage usage) {
+ properties.setCurrencyUsage(usage);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns the current instance of CurrencyPluralInfo.
+ *
+ * @see #setCurrencyPluralInfo
+ */
+ public synchronized CurrencyPluralInfo getCurrencyPluralInfo() {
+ // CurrencyPluralInfo also is not exported.
+ return properties.getCurrencyPluralInfo();
+ }
+
+ /**
+ * <strong>[icu]</strong> Sets a custom instance of CurrencyPluralInfo. CurrencyPluralInfo generates pattern
+ * strings for printing currency long names.
+ *
+ * <p><strong>Most users should not call this method directly.</strong> You should instead create
+ * your formatter via <code>NumberFormat.getInstance(NumberFormat.PLURALCURRENCYSTYLE)</code>.
+ *
+ * @param newInfo The CurrencyPluralInfo to use when printing currency long names.
+ */
+ public synchronized void setCurrencyPluralInfo(CurrencyPluralInfo newInfo) {
+ properties.setCurrencyPluralInfo(newInfo);
+ refreshFormatter();
+ }
+
+ /**
+ * Returns whether {@link #parse} will always return a BigDecimal.
+ *
+ * @see #setParseBigDecimal
+ */
+ public synchronized boolean isParseBigDecimal() {
+ return properties.getParseToBigDecimal();
+ }
+
+ /**
+ * Whether to make {@link #parse} prefer returning a {@link android.icu.math.BigDecimal} when
+ * possible. For strings corresponding to return values of Infinity, -Infinity, NaN, and -0.0, a
+ * Double will be returned even if ParseBigDecimal is enabled.
+ *
+ * @param value true to cause {@link #parse} to prefer BigDecimal; false to let {@link #parse}
+ * return additional data types like Long or BigInteger.
+ */
+ public synchronized void setParseBigDecimal(boolean value) {
+ properties.setParseToBigDecimal(value);
+ // refreshFormatter() not needed
+ }
+
+ /**
+ * Always returns 1000, the default prior to ICU 59.
+ *
+ * @deprecated Setting max parse digits has no effect since ICU4J 59.
+ */
+ @Deprecated
+ public int getParseMaxDigits() {
+ return 1000;
+ }
+
+ /**
+ * @param maxDigits Prior to ICU 59, the maximum number of digits in the output number after
+ * exponential notation is applied.
+ * @deprecated Setting max parse digits has no effect since ICU4J 59.
+ */
+ @Deprecated
+ public void setParseMaxDigits(int maxDigits) {}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public synchronized boolean isParseStrict() {
+ return properties.getParseMode() == Parse.ParseMode.STRICT;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public synchronized void setParseStrict(boolean parseStrict) {
+ Parse.ParseMode mode = parseStrict ? Parse.ParseMode.STRICT : Parse.ParseMode.LENIENT;
+ properties.setParseMode(mode);
+ // refreshFormatter() not needed
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see #setParseIntegerOnly
+ */
+ @Override
+ public synchronized boolean isParseIntegerOnly() {
+ return properties.getParseIntegerOnly();
+ }
+
+ /**
+ * <strong>Parsing:</strong> {@inheritDoc}
+ *
+ * <p>This is functionally equivalent to calling {@link #setDecimalPatternMatchRequired} and a
+ * pattern without a decimal point.
+ *
+ * @param parseIntegerOnly true to ignore fractional parts of numbers when parsing; false to
+ * consume fractional parts.
+ */
+ @Override
+ public synchronized void setParseIntegerOnly(boolean parseIntegerOnly) {
+ properties.setParseIntegerOnly(parseIntegerOnly);
+ // refreshFormatter() not needed
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns whether the presence of a decimal point must match the pattern.
+ *
+ * @see #setDecimalPatternMatchRequired
+ */
+ public synchronized boolean isDecimalPatternMatchRequired() {
+ return properties.getDecimalPatternMatchRequired();
+ }
+
+ /**
+ * <strong>[icu]</strong> <strong>Parsing:</strong> This method is used to either <em>require</em> or
+ * <em>forbid</em> the presence of a decimal point in the string being parsed (disabled by
+ * default). This feature was designed to be an extra layer of strictness on top of strict
+ * parsing, although it can be used in either lenient mode or strict mode.
+ *
+ * <p>To <em>require</em> a decimal point, call this method in combination with either a pattern
+ * containing a decimal point or with {@link #setDecimalSeparatorAlwaysShown}.
+ *
+ * <pre>
+ * // Require a decimal point in the string being parsed:
+ * df.applyPattern("#.");
+ * df.setDecimalPatternMatchRequired(true);
+ *
+ * // Alternatively:
+ * df.setDecimalSeparatorAlwaysShown(true);
+ * df.setDecimalPatternMatchRequired(true);
+ * </pre>
+ *
+ * To <em>forbid</em> a decimal point, call this method in combination with a pattern containing
+ * no decimal point. Alternatively, use {@link #setParseIntegerOnly} for the same behavior without
+ * depending on the contents of the pattern string.
+ *
+ * <pre>
+ * // Forbid a decimal point in the string being parsed:
+ * df.applyPattern("#");
+ * df.setDecimalPatternMatchRequired(true);
+ * </pre>
+ *
+ * @param value true to either require or forbid the decimal point according to the pattern; false
+ * to disable this feature.
+ * @see #setParseIntegerOnly
+ */
+ public synchronized void setDecimalPatternMatchRequired(boolean value) {
+ properties.setDecimalPatternMatchRequired(value);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns whether to ignore exponents when parsing.
+ *
+ * @see #setParseNoExponent
+ * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public synchronized boolean getParseNoExponent() {
+ return properties.getParseNoExponent();
+ }
+
+ /**
+ * <strong>[icu]</strong> Specifies whether to stop parsing when an exponent separator is encountered. For
+ * example, parses "123E4" to 123 (with parse position 3) instead of 1230000 (with parse position
+ * 5).
+ *
+ * @param value true to prevent exponents from being parsed; false to allow them to be parsed.
+ * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public synchronized void setParseNoExponent(boolean value) {
+ properties.setParseNoExponent(value);
+ refreshFormatter();
+ }
+
+ /**
+ * <strong>[icu]</strong> Returns whether to force case (uppercase/lowercase) to match when parsing.
+ *
+ * @see #setParseNoExponent
+ * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public synchronized boolean getParseCaseSensitive() {
+ return properties.getParseCaseSensitive();
+ }
+
+ /**
+ * <strong>[icu]</strong> Specifies whether parsing should require cases to match in affixes, exponent separators,
+ * and currency codes. Case mapping is performed for each code point using {@link
+ * UCharacter#foldCase}.
+ *
+ * @param value true to force case (uppercase/lowercase) to match when parsing; false to ignore
+ * case and perform case folding.
+ * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public synchronized void setParseCaseSensitive(boolean value) {
+ properties.setParseCaseSensitive(value);
+ refreshFormatter();
+ }
+
+ // TODO(sffc): Uncomment for ICU 60 API proposal.
+ //
+ // /**
+ // * {@icu} Returns the strategy used for choosing between grouping and decimal separators when
+ // * parsing.
+ // *
+ // * @see #setParseGroupingMode
+ // * @category Parsing
+ // */
+ // public synchronized GroupingMode getParseGroupingMode() {
+ // return properties.getParseGroupingMode();
+ // }
+ //
+ // /**
+ // * {@icu} Sets the strategy used during parsing when a code point needs to be interpreted as
+ // * either a decimal separator or a grouping separator.
+ // *
+ // * <p>The comma, period, space, and apostrophe have different meanings in different locales. For
+ // * example, in <em>en-US</em> and most American locales, the period is used as a decimal
+ // * separator, but in <em>es-PY</em> and most European locales, it is used as a grouping separator.
+ // *
+ // * Suppose you are in <em>fr-FR</em> the parser encounters the string "1.234". In <em>fr-FR</em>,
+ // * the grouping is a space and the decimal is a comma. The <em>grouping mode</em> is a mechanism
+ // * to let you specify whether to accept the string as 1234 (GroupingMode.DEFAULT) or whether to reject it since the separators
+ // * don't match (GroupingMode.RESTRICTED).
+ // *
+ // * When resolving grouping separators, it is the <em>equivalence class</em> of separators that is considered.
+ // * For example, a period is seen as equal to a fixed set of other period-like characters.
+ // *
+ // * @param groupingMode The strategy to use; either DEFAULT or RESTRICTED.
+ // * @category Parsing
+ // */
+ // public synchronized void setParseGroupingMode(GroupingMode groupingMode) {
+ // properties.setParseGroupingMode(groupingMode);
+ // refreshFormatter();
+ // }
+
+ //=====================================================================================//
+ // UTILITIES //
+ //=====================================================================================//
+
+ /**
+ * Tests for equality between this formatter and another formatter.
+ *
+ * <p>If two DecimalFormat instances are equal, then they will always produce the same output.
+ * However, the reverse is not necessarily true: if two DecimalFormat instances always produce the
+ * same output, they are not necessarily equal.
+ */
+ @Override
+ public synchronized boolean equals(Object obj) {
+ if (obj == null) return false;
+ if (obj == this) return true;
+ if (!(obj instanceof DecimalFormat)) return false;
+ DecimalFormat other = (DecimalFormat) obj;
+ return properties.equals(other.properties) && symbols.equals(other.symbols);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public synchronized int hashCode() {
+ return properties.hashCode() ^ symbols.hashCode();
+ }
+
+ /**
+ * Returns the default value of toString() with extra DecimalFormat-specific information appended
+ * to the end of the string. This extra information is intended for debugging purposes, and the
+ * format is not guaranteed to be stable.
+ */
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append(getClass().getName());
+ result.append("@");
+ result.append(Integer.toHexString(hashCode()));
+ result.append(" { symbols@");
+ result.append(Integer.toHexString(symbols.hashCode()));
+ synchronized (this) {
+ properties.toStringBare(result);
+ }
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Serializes this formatter object to a decimal format pattern string. The result of this method
+ * is guaranteed to be <em>functionally</em> equivalent to the pattern string used to create this
+ * instance after incorporating values from the setter methods.
+ *
+ * <p>For more information on decimal format pattern strings, see <a
+ * href="http://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns">UTS #35</a>.
+ *
+ * <p><strong>Important:</strong> Not all properties are capable of being encoded in a pattern
+ * string. See a list of properties in {@link #applyPattern}.
+ *
+ * @return A decimal format pattern string.
+ */
+ public synchronized String toPattern() {
+ // Pull some properties from exportedProperties and others from properties
+ // to keep affix patterns intact. In particular, pull rounding properties
+ // so that CurrencyUsage is reflected properly.
+ // TODO: Consider putting this logic in PatternString.java instead.
+ Properties tprops = threadLocalProperties.get().copyFrom(properties);
+ if (android.icu.impl.number.formatters.CurrencyFormat.useCurrency(properties)) {
+ tprops.setMinimumFractionDigits(exportedProperties.getMinimumFractionDigits());
+ tprops.setMaximumFractionDigits(exportedProperties.getMaximumFractionDigits());
+ tprops.setRoundingIncrement(exportedProperties.getRoundingIncrement());
+ }
+ return PatternString.propertiesToString(tprops);
+ }
+
+ /**
+ * Calls {@link #toPattern} and converts the string to localized notation. For more information on
+ * localized notation, see {@link #applyLocalizedPattern}. This method is provided for backwards
+ * compatibility and should not be used in new projects.
+ *
+ * @return A decimal format pattern string in localized notation.
+ */
+ public synchronized String toLocalizedPattern() {
+ String pattern = toPattern();
+ return PatternString.convertLocalized(pattern, symbols, true);
+ }
+
+ /**
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public IFixedDecimal getFixedDecimal(double number) {
+ FormatQuantity4 fq = new FormatQuantity4(number);
+ formatter.format(fq);
+ return fq;
+ }
+
+ private static final ThreadLocal<Properties> threadLocalProperties =
+ new ThreadLocal<Properties>() {
+ @Override
+ protected Properties initialValue() {
+ return new Properties();
+ }
+ };
+
+ /** Rebuilds the formatter object from the property bag. */
+ void refreshFormatter() {
+ if (exportedProperties == null) {
+ // exportedProperties is null only when the formatter is not ready yet.
+ // The only time when this happens is during legacy deserialization.
+ return;
+ }
+ formatter = Endpoint.fromBTA(properties, symbols);
+ exportedProperties.clear();
+ formatter.export(exportedProperties);
+ }
+
+ /**
+ * Converts a java.math.BigDecimal to a android.icu.math.BigDecimal with fallback for numbers
+ * outside of the range supported by android.icu.math.BigDecimal.
+ *
+ * @param number
+ * @return
+ */
+ private Number safeConvertBigDecimal(java.math.BigDecimal number) {
+ try {
+ return new android.icu.math.BigDecimal(number);
+ } catch (NumberFormatException e) {
+ if (number.signum() > 0 && number.scale() < 0) {
+ return Double.POSITIVE_INFINITY;
+ } else if (number.scale() < 0) {
+ return Double.NEGATIVE_INFINITY;
+ } else if (number.signum() < 0) {
+ return -0.0;
+ } else {
+ return 0.0;
+ }
+ }
+ }
+
+ /**
+ * Updates the property bag with settings from the given pattern.
+ *
+ * @param pattern The pattern string to parse.
+ * @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
+ * increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
+ * as CurrencyUsage, is to be used instead. One of {@link
+ * PatternString#IGNORE_ROUNDING_ALWAYS}, {@link PatternString#IGNORE_ROUNDING_IF_CURRENCY},
+ * or {@link PatternString#IGNORE_ROUNDING_NEVER}.
+ * @see PatternString#parseToExistingProperties
+ */
+ void setPropertiesFromPattern(String pattern, int ignoreRounding) {
+ if (pattern == null) {
+ throw new NullPointerException();
+ }
+ PatternString.parseToExistingProperties(pattern, properties, ignoreRounding);
+ }
+
+ /**
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public synchronized void setProperties(PropertySetter func) {
+ func.set(properties);
+ refreshFormatter();
+ }
+
+ /**
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public static interface PropertySetter {
/**
- * This is a special function used by the CompactDecimalFormat subclass.
- * It completes only the rounding portion of the formatting and returns
- * the resulting double. CompactDecimalFormat uses the result to compute
- * the plural form to use.
- *
- * @param number The number to format.
- * @return The number rounded to the correct number of significant digits
- * with negative sign stripped off.
* @deprecated This API is ICU internal only.
* @hide draft / provisional / internal are hidden on Android
*/
@Deprecated
- double adjustNumberAsInFormatting(double number) {
- if (Double.isNaN(number)) {
- return number;
- }
- number = round(multiply(number));
- if (Double.isInfinite(number)) {
- return number;
- }
- return toDigitList(number).getDouble();
- }
-
- @Deprecated
- DigitList toDigitList(double number) {
- DigitList result = new DigitList();
- result.set(number, precision(false), false);
- return result;
- }
-
- /**
- * This is a special function used by the CompactDecimalFormat subclass
- * to determine if the number to be formatted is negative.
- *
- * @param number The number to format.
- * @return True if number is negative.
- * @deprecated This API is ICU internal only.
+ public void set(Properties props);
+ }
+
+ /**
+ * An enum containing the choices for significant digits modes.
+ *
+ * @see #setSignificantDigitsMode
+ * @deprecated ICU 59: This API is technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public static enum SignificantDigitsMode {
+ /**
+ * Respect significant digits counts, ignoring the fraction length.
+ *
+ * @see DecimalFormat#setSignificantDigitsMode
+ * @deprecated ICU 59: This API is technical preview. It may change in an upcoming release.
* @hide draft / provisional / internal are hidden on Android
- */
- @Deprecated
- boolean isNumberNegative(double number) {
- if (Double.isNaN(number)) {
- return false;
- }
- return isNegative(multiply(number));
- }
-
- /**
- * Round a double value to the nearest multiple of the given rounding increment,
- * according to the given mode. This is equivalent to rounding value/roundingInc to
- * the nearest integer, according to the given mode, and returning that integer *
- * roundingInc. Note this is changed from the version in 2.4, since division of
- * doubles have inaccuracies. jitterbug 1871.
- *
- * @param number
- * the absolute value of the number to be rounded
- * @param roundingInc
- * the rounding increment
- * @param roundingIncReciprocal
- * if non-zero, is the reciprocal of rounding inc.
- * @param mode
- * a BigDecimal rounding mode
- * @param isNegative
- * true if the number to be rounded is negative
- * @return the absolute value of the rounded result
- */
- private static double round(double number, double roundingInc, double roundingIncReciprocal,
- int mode, boolean isNegative) {
-
- double div = roundingIncReciprocal == 0.0 ? number / roundingInc : number *
- roundingIncReciprocal;
-
- // do the absolute cases first
-
- switch (mode) {
- case BigDecimal.ROUND_CEILING:
- div = (isNegative ? Math.floor(div + epsilon) : Math.ceil(div - epsilon));
- break;
- case BigDecimal.ROUND_FLOOR:
- div = (isNegative ? Math.ceil(div - epsilon) : Math.floor(div + epsilon));
- break;
- case BigDecimal.ROUND_DOWN:
- div = (Math.floor(div + epsilon));
- break;
- case BigDecimal.ROUND_UP:
- div = (Math.ceil(div - epsilon));
- break;
- case BigDecimal.ROUND_UNNECESSARY:
- if (div != Math.floor(div)) {
- throw new ArithmeticException("Rounding necessary");
- }
- return number;
- default:
-
- // Handle complex cases, where the choice depends on the closer value.
-
- // We figure out the distances to the two possible values, ceiling and floor.
- // We then go for the diff that is smaller. Only if they are equal does the
- // mode matter.
-
- double ceil = Math.ceil(div);
- double ceildiff = ceil - div; // (ceil * roundingInc) - number;
- double floor = Math.floor(div);
- double floordiff = div - floor; // number - (floor * roundingInc);
-
- // Note that the diff values were those mapped back to the "normal" space by
- // using the roundingInc. I don't have access to the original author of the
- // code but suspect that that was to produce better result in edge cases
- // because of machine precision, rather than simply using the difference
- // between, say, ceil and div. However, it didn't work in all cases. Am
- // trying instead using an epsilon value.
-
- switch (mode) {
- case BigDecimal.ROUND_HALF_EVEN:
- // We should be able to just return Math.rint(a), but this
- // doesn't work in some VMs.
- // if one is smaller than the other, take the corresponding side
- if (floordiff + epsilon < ceildiff) {
- div = floor;
- } else if (ceildiff + epsilon < floordiff) {
- div = ceil;
- } else { // they are equal, so we want to round to whichever is even
- double testFloor = floor / 2;
- div = (testFloor == Math.floor(testFloor)) ? floor : ceil;
- }
- break;
- case BigDecimal.ROUND_HALF_DOWN:
- div = ((floordiff <= ceildiff + epsilon) ? floor : ceil);
- break;
- case BigDecimal.ROUND_HALF_UP:
- div = ((ceildiff <= floordiff + epsilon) ? ceil : floor);
- break;
- default:
- throw new IllegalArgumentException("Invalid rounding mode: " + mode);
- }
- }
- number = roundingIncReciprocal == 0.0 ? div * roundingInc : div / roundingIncReciprocal;
- return number;
- }
-
- private static double epsilon = 0.00000000001;
-
- /**
- */
- // [Spark/CDL] Delegate to format_long_StringBuffer_FieldPosition_boolean
- @Override
- public StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition) {
- return format(number, result, fieldPosition, false);
- }
-
- private StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition,
- boolean parseAttr) {
- fieldPosition.setBeginIndex(0);
- fieldPosition.setEndIndex(0);
-
- // If we are to do rounding, we need to move into the BigDecimal
- // domain in order to do divide/multiply correctly.
- if (actualRoundingIncrementICU != null) {
- return format(BigDecimal.valueOf(number), result, fieldPosition);
- }
-
- boolean isNegative = (number < 0);
- if (isNegative)
- number = -number;
-
- // In general, long values always represent real finite numbers, so we don't have
- // to check for +/- Infinity or NaN. However, there is one case we have to be
- // careful of: The multiplier can push a number near MIN_VALUE or MAX_VALUE
- // outside the legal range. We check for this before multiplying, and if it
- // happens we use BigInteger instead.
- if (multiplier != 1) {
- boolean tooBig = false;
- if (number < 0) { // This can only happen if number == Long.MIN_VALUE
- long cutoff = Long.MIN_VALUE / multiplier;
- tooBig = (number <= cutoff); // number == cutoff can only happen if multiplier == -1
- } else {
- long cutoff = Long.MAX_VALUE / multiplier;
- tooBig = (number > cutoff);
- }
- if (tooBig) {
- // [Spark/CDL] Use
- // format_BigInteger_StringBuffer_FieldPosition_boolean instead
- // parseAttr is used to judge whether to synthesize attributes.
- return format(BigInteger.valueOf(isNegative ? -number : number), result,
- fieldPosition, parseAttr);
- }
- }
-
- number *= multiplier;
- synchronized (digitList) {
- digitList.set(number, precision(true));
- // Issue 11808
- if (digitList.wasRounded() && roundingMode == BigDecimal.ROUND_UNNECESSARY) {
- throw new ArithmeticException("Rounding necessary");
- }
- return subformat(number, result, fieldPosition, isNegative, true, parseAttr);
- }
- }
-
- /**
- * Formats a BigInteger number.
- */
- @Override
- public StringBuffer format(BigInteger number, StringBuffer result,
- FieldPosition fieldPosition) {
- return format(number, result, fieldPosition, false);
- }
-
- private StringBuffer format(BigInteger number, StringBuffer result, FieldPosition fieldPosition,
- boolean parseAttr) {
- // If we are to do rounding, we need to move into the BigDecimal
- // domain in order to do divide/multiply correctly.
- if (actualRoundingIncrementICU != null) {
- return format(new BigDecimal(number), result, fieldPosition);
- }
-
- if (multiplier != 1) {
- number = number.multiply(BigInteger.valueOf(multiplier));
- }
-
- // At this point we are guaranteed a nonnegative finite
- // number.
- synchronized (digitList) {
- digitList.set(number, precision(true));
- // For issue 11808.
- if (digitList.wasRounded() && roundingMode == BigDecimal.ROUND_UNNECESSARY) {
- throw new ArithmeticException("Rounding necessary");
- }
- return subformat(number.intValue(), result, fieldPosition, number.signum() < 0, true,
- parseAttr);
- }
- }
-
- /**
- * Formats a BigDecimal number.
- */
- @Override
- public StringBuffer format(java.math.BigDecimal number, StringBuffer result,
- FieldPosition fieldPosition) {
- return format(number, result, fieldPosition, false);
- }
-
- private StringBuffer format(java.math.BigDecimal number, StringBuffer result,
- FieldPosition fieldPosition,
- boolean parseAttr) {
- if (multiplier != 1) {
- number = number.multiply(java.math.BigDecimal.valueOf(multiplier));
- }
-
- if (actualRoundingIncrement != null) {
- number = number.divide(actualRoundingIncrement, 0, roundingMode).multiply(actualRoundingIncrement);
- }
-
- synchronized (digitList) {
- digitList.set(number, precision(false), !useExponentialNotation &&
- !areSignificantDigitsUsed());
- // For issue 11808.
- if (digitList.wasRounded() && roundingMode == BigDecimal.ROUND_UNNECESSARY) {
- throw new ArithmeticException("Rounding necessary");
- }
- return subformat(number.doubleValue(), result, fieldPosition, number.signum() < 0,
- false, parseAttr);
- }
- }
-
- /**
- * Formats a BigDecimal number.
- */
- @Override
- public StringBuffer format(BigDecimal number, StringBuffer result,
- FieldPosition fieldPosition) {
- // This method is just a copy of the corresponding java.math.BigDecimal method
- // for now. It isn't very efficient since it must create a conversion object to
- // do math on the rounding increment. In the future we may try to clean this up,
- // or even better, limit our support to just one flavor of BigDecimal.
- if (multiplier != 1) {
- number = number.multiply(BigDecimal.valueOf(multiplier), mathContext);
- }
-
- if (actualRoundingIncrementICU != null) {
- number = number.divide(actualRoundingIncrementICU, 0, roundingMode)
- .multiply(actualRoundingIncrementICU, mathContext);
- }
-
- synchronized (digitList) {
- digitList.set(number, precision(false), !useExponentialNotation &&
- !areSignificantDigitsUsed());
- // For issue 11808.
- if (digitList.wasRounded() && roundingMode == BigDecimal.ROUND_UNNECESSARY) {
- throw new ArithmeticException("Rounding necessary");
- }
- return subformat(number.doubleValue(), result, fieldPosition, number.signum() < 0,
- false, false);
- }
- }
-
- /**
- * Returns true if a grouping separator belongs at the given position, based on whether
- * grouping is in use and the values of the primary and secondary grouping interval.
- *
- * @param pos the number of integer digits to the right of the current position. Zero
- * indicates the position after the rightmost integer digit.
- * @return true if a grouping character belongs at the current position.
- */
- private boolean isGroupingPosition(int pos) {
- boolean result = false;
- if (isGroupingUsed() && (pos > 0) && (groupingSize > 0)) {
- if ((groupingSize2 > 0) && (pos > groupingSize)) {
- result = ((pos - groupingSize) % groupingSize2) == 0;
- } else {
- result = pos % groupingSize == 0;
- }
- }
- return result;
- }
-
- /**
- * Return the number of fraction digits to display, or the total
- * number of digits for significant digit formats and exponential
- * formats.
- */
- private int precision(boolean isIntegral) {
- if (areSignificantDigitsUsed()) {
- return getMaximumSignificantDigits();
- } else if (useExponentialNotation) {
- return getMinimumIntegerDigits() + getMaximumFractionDigits();
- } else {
- return isIntegral ? 0 : getMaximumFractionDigits();
- }
- }
-
- private StringBuffer subformat(int number, StringBuffer result, FieldPosition fieldPosition,
- boolean isNegative, boolean isInteger, boolean parseAttr) {
- if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
- // compute the plural category from the digitList plus other settings
- return subformat(currencyPluralInfo.select(getFixedDecimal(number)),
- result, fieldPosition, isNegative,
- isInteger, parseAttr);
- } else {
- return subformat(result, fieldPosition, isNegative, isInteger, parseAttr);
- }
- }
-
- /**
- * This is ugly, but don't see a better way to do it without major restructuring of the code.
- */
- /*package*/ FixedDecimal getFixedDecimal(double number) {
- // get the visible fractions and the number of fraction digits.
- return getFixedDecimal(number, digitList);
- }
-
- FixedDecimal getFixedDecimal(double number, DigitList dl) {
- int fractionalDigitsInDigitList = dl.count - dl.decimalAt;
- int v;
- long f;
- int maxFractionalDigits;
- int minFractionalDigits;
- if (useSignificantDigits) {
- maxFractionalDigits = maxSignificantDigits - dl.decimalAt;
- minFractionalDigits = minSignificantDigits - dl.decimalAt;
- if (minFractionalDigits < 0) {
- minFractionalDigits = 0;
- }
- if (maxFractionalDigits < 0) {
- maxFractionalDigits = 0;
- }
- } else {
- maxFractionalDigits = getMaximumFractionDigits();
- minFractionalDigits = getMinimumFractionDigits();
- }
- v = fractionalDigitsInDigitList;
- if (v < minFractionalDigits) {
- v = minFractionalDigits;
- } else if (v > maxFractionalDigits) {
- v = maxFractionalDigits;
- }
- f = 0;
- if (v > 0) {
- for (int i = Math.max(0, dl.decimalAt); i < dl.count; ++i) {
- f *= 10;
- f += (dl.digits[i] - '0');
- }
- for (int i = v; i < fractionalDigitsInDigitList; ++i) {
- f *= 10;
- }
- }
- return new FixedDecimal(number, v, f);
- }
-
- private StringBuffer subformat(double number, StringBuffer result, FieldPosition fieldPosition,
- boolean isNegative,
- boolean isInteger, boolean parseAttr) {
- if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
- // compute the plural category from the digitList plus other settings
- return subformat(currencyPluralInfo.select(getFixedDecimal(number)),
- result, fieldPosition, isNegative,
- isInteger, parseAttr);
- } else {
- return subformat(result, fieldPosition, isNegative, isInteger, parseAttr);
- }
- }
-
- private StringBuffer subformat(String pluralCount, StringBuffer result, FieldPosition fieldPosition,
- boolean isNegative, boolean isInteger, boolean parseAttr) {
- // There are 2 ways to activate currency plural format: by applying a pattern with
- // 3 currency sign directly, or by instantiate a decimal formatter using
- // PLURALCURRENCYSTYLE. For both cases, the number of currency sign in the
- // pattern is 3. Even if the number of currency sign in the pattern is 3, it does
- // not mean we need to reset the pattern. For 1st case, we do not need to reset
- // pattern. For 2nd case, we might need to reset pattern, if the default pattern
- // (corresponding to plural count 'other') we use is different from the pattern
- // based on 'pluralCount'.
- //
- // style is only valid when decimal formatter is constructed through
- // DecimalFormat(pattern, symbol, style)
- if (style == NumberFormat.PLURALCURRENCYSTYLE) {
- // May need to reset pattern if the style is PLURALCURRENCYSTYLE.
- String currencyPluralPattern = currencyPluralInfo.getCurrencyPluralPattern(pluralCount);
- if (formatPattern.equals(currencyPluralPattern) == false) {
- applyPatternWithoutExpandAffix(currencyPluralPattern, false);
- }
- }
- // Expand the affix to the right name according to the plural rule. This is only
- // used for currency plural formatting. Currency plural name is not a fixed
- // static one, it is a dynamic name based on the currency plural count. So, the
- // affixes need to be expanded here. For other cases, the affix is a static one
- // based on pattern alone, and it is already expanded during applying pattern, or
- // setDecimalFormatSymbols, or setCurrency.
- expandAffixAdjustWidth(pluralCount);
- return subformat(result, fieldPosition, isNegative, isInteger, parseAttr);
- }
-
- /**
- * Complete the formatting of a finite number. On entry, the
- * digitList must be filled in with the correct digits.
- */
- private StringBuffer subformat(StringBuffer result, FieldPosition fieldPosition,
- boolean isNegative, boolean isInteger, boolean parseAttr) {
- // NOTE: This isn't required anymore because DigitList takes care of this.
- //
- // // The negative of the exponent represents the number of leading // zeros
- // between the decimal and the first non-zero digit, for // a value < 0.1 (e.g.,
- // for 0.00123, -fExponent == 2). If this // is more than the maximum fraction
- // digits, then we have an underflow // for the printed representation. We
- // recognize this here and set // the DigitList representation to zero in this
- // situation.
- //
- // if (-digitList.decimalAt >= getMaximumFractionDigits())
- // {
- // digitList.count = 0;
- // }
-
-
-
- // Per bug 4147706, DecimalFormat must respect the sign of numbers which format as
- // zero. This allows sensible computations and preserves relations such as
- // signum(1/x) = signum(x), where x is +Infinity or -Infinity. Prior to this fix,
- // we always formatted zero values as if they were positive. Liu 7/6/98.
- if (digitList.isZero()) {
- digitList.decimalAt = 0; // Normalize
- }
-
- int prefixLen = appendAffix(result, isNegative, true, fieldPosition, parseAttr);
-
- if (useExponentialNotation) {
- subformatExponential(result, fieldPosition, parseAttr);
- } else {
- subformatFixed(result, fieldPosition, isInteger, parseAttr);
- }
-
- int suffixLen = appendAffix(result, isNegative, false, fieldPosition, parseAttr);
- addPadding(result, fieldPosition, prefixLen, suffixLen);
- return result;
- }
-
- private void subformatFixed(StringBuffer result,
- FieldPosition fieldPosition,
- boolean isInteger,
- boolean parseAttr) {
- String[] digits = symbols.getDigitStrings();
-
- String grouping = currencySignCount == CURRENCY_SIGN_COUNT_ZERO ?
- symbols.getGroupingSeparatorString(): symbols.getMonetaryGroupingSeparatorString();
- String decimal = currencySignCount == CURRENCY_SIGN_COUNT_ZERO ?
- symbols.getDecimalSeparatorString() : symbols.getMonetaryDecimalSeparatorString();
- boolean useSigDig = areSignificantDigitsUsed();
- int maxIntDig = getMaximumIntegerDigits();
- int minIntDig = getMinimumIntegerDigits();
- int i;
- // [Spark/CDL] Record the integer start index.
- int intBegin = result.length();
- // Record field information for caller.
- if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD ||
- fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
- fieldPosition.setBeginIndex(intBegin);
- }
- long fractionalDigits = 0;
- int fractionalDigitsCount = 0;
- boolean recordFractionDigits = false;
-
- int sigCount = 0;
- int minSigDig = getMinimumSignificantDigits();
- int maxSigDig = getMaximumSignificantDigits();
- if (!useSigDig) {
- minSigDig = 0;
- maxSigDig = Integer.MAX_VALUE;
- }
-
- // Output the integer portion. Here 'count' is the total number of integer
- // digits we will display, including both leading zeros required to satisfy
- // getMinimumIntegerDigits, and actual digits present in the number.
- int count = useSigDig ? Math.max(1, digitList.decimalAt) : minIntDig;
- if (digitList.decimalAt > 0 && count < digitList.decimalAt) {
- count = digitList.decimalAt;
- }
-
- // Handle the case where getMaximumIntegerDigits() is smaller than the real
- // number of integer digits. If this is so, we output the least significant
- // max integer digits. For example, the value 1997 printed with 2 max integer
- // digits is just "97".
-
- int digitIndex = 0; // Index into digitList.fDigits[]
- if (count > maxIntDig && maxIntDig >= 0) {
- count = maxIntDig;
- digitIndex = digitList.decimalAt - count;
- }
-
- int sizeBeforeIntegerPart = result.length();
- for (i = count - 1; i >= 0; --i) {
- if (i < digitList.decimalAt && digitIndex < digitList.count
- && sigCount < maxSigDig) {
- // Output a real digit
- result.append(digits[digitList.getDigitValue(digitIndex++)]);
- ++sigCount;
- } else {
- // Output a zero (leading or trailing)
- result.append(digits[0]);
- if (sigCount > 0) {
- ++sigCount;
- }
- }
-
- // Output grouping separator if necessary.
- if (isGroupingPosition(i)) {
- result.append(grouping);
- // [Spark/CDL] Add grouping separator attribute here.
- // Set only for the first instance.
- // Length of grouping separator is 1.
- if (fieldPosition.getFieldAttribute() == Field.GROUPING_SEPARATOR &&
- fieldPosition.getBeginIndex() == 0 && fieldPosition.getEndIndex() == 0) {
- fieldPosition.setBeginIndex(result.length()-1);
- fieldPosition.setEndIndex(result.length());
- }
- if (parseAttr) {
- addAttribute(Field.GROUPING_SEPARATOR, result.length() - 1, result.length());
- }
- }
- }
-
- // Record field information for caller.
- if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD ||
- fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
- fieldPosition.setEndIndex(result.length());
- }
-
- // This handles the special case of formatting 0. For zero only, we count the
- // zero to the left of the decimal point as one signficant digit. Ordinarily we
- // do not count any leading 0's as significant. If the number we are formatting
- // is not zero, then either sigCount or digits.getCount() will be non-zero.
- if (sigCount == 0 && digitList.count == 0) {
- sigCount = 1;
- }
-
- // Determine whether or not there are any printable fractional digits. If
- // we've used up the digits we know there aren't.
- boolean fractionPresent = (!isInteger && digitIndex < digitList.count)
- || (useSigDig ? (sigCount < minSigDig) : (getMinimumFractionDigits() > 0));
-
- // If there is no fraction present, and we haven't printed any integer digits,
- // then print a zero. Otherwise we won't print _any_ digits, and we won't be
- // able to parse this string.
- if (!fractionPresent && result.length() == sizeBeforeIntegerPart)
- result.append(digits[0]);
- // [Spark/CDL] Add attribute for integer part.
- if (parseAttr) {
- addAttribute(Field.INTEGER, intBegin, result.length());
- }
- // Output the decimal separator if we always do so.
- if (decimalSeparatorAlwaysShown || fractionPresent) {
- if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
- fieldPosition.setBeginIndex(result.length());
- }
- result.append(decimal);
- if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
- fieldPosition.setEndIndex(result.length());
- }
- // [Spark/CDL] Add attribute for decimal separator
- if (parseAttr) {
- addAttribute(Field.DECIMAL_SEPARATOR, result.length() - 1, result.length());
- }
- }
-
- // Record field information for caller.
- if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
- fieldPosition.setBeginIndex(result.length());
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
- fieldPosition.setBeginIndex(result.length());
- }
-
- // [Spark/CDL] Record the begin index of fraction part.
- int fracBegin = result.length();
- recordFractionDigits = fieldPosition instanceof UFieldPosition;
-
- count = useSigDig ? Integer.MAX_VALUE : getMaximumFractionDigits();
- if (useSigDig && (sigCount == maxSigDig ||
- (sigCount >= minSigDig && digitIndex == digitList.count))) {
- count = 0;
- }
- for (i = 0; i < count; ++i) {
- // Here is where we escape from the loop. We escape if we've output the
- // maximum fraction digits (specified in the for expression above). We
- // also stop when we've output the minimum digits and either: we have an
- // integer, so there is no fractional stuff to display, or we're out of
- // significant digits.
- if (!useSigDig && i >= getMinimumFractionDigits() &&
- (isInteger || digitIndex >= digitList.count)) {
- break;
- }
-
- // Output leading fractional zeros. These are zeros that come after the
- // decimal but before any significant digits. These are only output if
- // abs(number being formatted) < 1.0.
- if (-1 - i > (digitList.decimalAt - 1)) {
- result.append(digits[0]);
- if (recordFractionDigits) {
- ++fractionalDigitsCount;
- fractionalDigits *= 10;
- }
- continue;
- }
-
- // Output a digit, if we have any precision left, or a zero if we
- // don't. We don't want to output noise digits.
- if (!isInteger && digitIndex < digitList.count) {
- byte digit = digitList.getDigitValue(digitIndex++);
- result.append(digits[digit]);
- if (recordFractionDigits) {
- ++fractionalDigitsCount;
- fractionalDigits *= 10;
- fractionalDigits += digit;
- }
- } else {
- result.append(digits[0]);
- if (recordFractionDigits) {
- ++fractionalDigitsCount;
- fractionalDigits *= 10;
- }
- }
-
- // If we reach the maximum number of significant digits, or if we output
- // all the real digits and reach the minimum, then we are done.
- ++sigCount;
- if (useSigDig && (sigCount == maxSigDig ||
- (digitIndex == digitList.count && sigCount >= minSigDig))) {
- break;
- }
- }
-
- // Record field information for caller.
- if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
- fieldPosition.setEndIndex(result.length());
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
- fieldPosition.setEndIndex(result.length());
- }
- if (recordFractionDigits) {
- ((UFieldPosition) fieldPosition).setFractionDigits(fractionalDigitsCount, fractionalDigits);
- }
-
- // [Spark/CDL] Add attribute information if necessary.
- if (parseAttr && (decimalSeparatorAlwaysShown || fractionPresent)) {
- addAttribute(Field.FRACTION, fracBegin, result.length());
- }
- }
-
- private void subformatExponential(StringBuffer result,
- FieldPosition fieldPosition,
- boolean parseAttr) {
- String[] digits = symbols.getDigitStringsLocal();
- String decimal = currencySignCount == CURRENCY_SIGN_COUNT_ZERO ?
- symbols.getDecimalSeparatorString() : symbols.getMonetaryDecimalSeparatorString();
- boolean useSigDig = areSignificantDigitsUsed();
- int maxIntDig = getMaximumIntegerDigits();
- int minIntDig = getMinimumIntegerDigits();
- int i;
- // Record field information for caller.
- if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
- fieldPosition.setBeginIndex(result.length());
- fieldPosition.setEndIndex(-1);
- } else if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
- fieldPosition.setBeginIndex(-1);
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
- fieldPosition.setBeginIndex(result.length());
- fieldPosition.setEndIndex(-1);
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
- fieldPosition.setBeginIndex(-1);
- }
-
- // [Spark/CDL]
- // the begin index of integer part
- // the end index of integer part
- // the begin index of fractional part
- int intBegin = result.length();
- int intEnd = -1;
- int fracBegin = -1;
- int minFracDig = 0;
- if (useSigDig) {
- maxIntDig = minIntDig = 1;
- minFracDig = getMinimumSignificantDigits() - 1;
- } else {
- minFracDig = getMinimumFractionDigits();
- if (maxIntDig > MAX_SCIENTIFIC_INTEGER_DIGITS) {
- maxIntDig = 1;
- if (maxIntDig < minIntDig) {
- maxIntDig = minIntDig;
- }
- }
- if (maxIntDig > minIntDig) {
- minIntDig = 1;
- }
- }
- long fractionalDigits = 0;
- int fractionalDigitsCount = 0;
- boolean recordFractionDigits = false;
-
- // Minimum integer digits are handled in exponential format by adjusting the
- // exponent. For example, 0.01234 with 3 minimum integer digits is "123.4E-4".
-
- // Maximum integer digits are interpreted as indicating the repeating
- // range. This is useful for engineering notation, in which the exponent is
- // restricted to a multiple of 3. For example, 0.01234 with 3 maximum integer
- // digits is "12.34e-3". If maximum integer digits are defined and are larger
- // than minimum integer digits, then minimum integer digits are ignored.
-
- int exponent = digitList.decimalAt;
- if (maxIntDig > 1 && maxIntDig != minIntDig) {
- // A exponent increment is defined; adjust to it.
- exponent = (exponent > 0) ? (exponent - 1) / maxIntDig : (exponent / maxIntDig) - 1;
- exponent *= maxIntDig;
- } else {
- // No exponent increment is defined; use minimum integer digits.
- // If none is specified, as in "#E0", generate 1 integer digit.
- exponent -= (minIntDig > 0 || minFracDig > 0) ? minIntDig : 1;
- }
-
- // We now output a minimum number of digits, and more if there are more
- // digits, up to the maximum number of digits. We place the decimal point
- // after the "integer" digits, which are the first (decimalAt - exponent)
- // digits.
- int minimumDigits = minIntDig + minFracDig;
- // The number of integer digits is handled specially if the number
- // is zero, since then there may be no digits.
- int integerDigits = digitList.isZero() ? minIntDig : digitList.decimalAt - exponent;
- int totalDigits = digitList.count;
- if (minimumDigits > totalDigits)
- totalDigits = minimumDigits;
- if (integerDigits > totalDigits)
- totalDigits = integerDigits;
-
- for (i = 0; i < totalDigits; ++i) {
- if (i == integerDigits) {
- // Record field information for caller.
- if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
- fieldPosition.setEndIndex(result.length());
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
- fieldPosition.setEndIndex(result.length());
- }
-
- // [Spark/CDL] Add attribute for integer part
- if (parseAttr) {
- intEnd = result.length();
- addAttribute(Field.INTEGER, intBegin, result.length());
- }
- if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
- fieldPosition.setBeginIndex(result.length());
- }
- result.append(decimal);
- if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
- fieldPosition.setEndIndex(result.length());
- }
- // [Spark/CDL] Add attribute for decimal separator
- fracBegin = result.length();
- if (parseAttr) {
- // Length of decimal separator is 1.
- int decimalSeparatorBegin = result.length() - 1;
- addAttribute(Field.DECIMAL_SEPARATOR, decimalSeparatorBegin,
- result.length());
- }
- // Record field information for caller.
- if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
- fieldPosition.setBeginIndex(result.length());
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
- fieldPosition.setBeginIndex(result.length());
- }
- recordFractionDigits = fieldPosition instanceof UFieldPosition;
-
- }
- byte digit = (i < digitList.count) ? digitList.getDigitValue(i) : (byte)0;
- result.append(digits[digit]);
- if (recordFractionDigits) {
- ++fractionalDigitsCount;
- fractionalDigits *= 10;
- fractionalDigits += digit;
- }
- }
-
- // For ICU compatibility and format 0 to 0E0 with pattern "#E0" [Richard/GCL]
- if (digitList.isZero() && (totalDigits == 0)) {
- result.append(digits[0]);
- }
-
- // add the decimal separator if it is to be always shown AND there are no decimal digits
- if ((fracBegin == -1) && this.decimalSeparatorAlwaysShown) {
- if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
- fieldPosition.setBeginIndex(result.length());
- }
- result.append(decimal);
- if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
- fieldPosition.setEndIndex(result.length());
- }
- if (parseAttr) {
- // Length of decimal separator is 1.
- int decimalSeparatorBegin = result.length() - 1;
- addAttribute(Field.DECIMAL_SEPARATOR, decimalSeparatorBegin, result.length());
- }
- }
-
- // Record field information
- if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
- if (fieldPosition.getEndIndex() < 0) {
- fieldPosition.setEndIndex(result.length());
- }
- } else if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
- if (fieldPosition.getBeginIndex() < 0) {
- fieldPosition.setBeginIndex(result.length());
- }
- fieldPosition.setEndIndex(result.length());
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
- if (fieldPosition.getEndIndex() < 0) {
- fieldPosition.setEndIndex(result.length());
- }
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
- if (fieldPosition.getBeginIndex() < 0) {
- fieldPosition.setBeginIndex(result.length());
- }
- fieldPosition.setEndIndex(result.length());
- }
- if (recordFractionDigits) {
- ((UFieldPosition) fieldPosition).setFractionDigits(fractionalDigitsCount, fractionalDigits);
- }
-
- // [Spark/CDL] Calculate the end index of integer part and fractional
- // part if they are not properly processed yet.
- if (parseAttr) {
- if (intEnd < 0) {
- addAttribute(Field.INTEGER, intBegin, result.length());
- }
- if (fracBegin > 0) {
- addAttribute(Field.FRACTION, fracBegin, result.length());
- }
- }
-
- // The exponent is output using the pattern-specified minimum exponent
- // digits. There is no maximum limit to the exponent digits, since truncating
- // the exponent would result in an unacceptable inaccuracy.
- if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SYMBOL) {
- fieldPosition.setBeginIndex(result.length());
- }
-
- result.append(symbols.getExponentSeparator());
- if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SYMBOL) {
- fieldPosition.setEndIndex(result.length());
- }
- // [Spark/CDL] For exponent symbol, add an attribute.
- if (parseAttr) {
- addAttribute(Field.EXPONENT_SYMBOL, result.length() -
- symbols.getExponentSeparator().length(), result.length());
- }
- // For zero values, we force the exponent to zero. We must do this here, and
- // not earlier, because the value is used to determine integer digit count
- // above.
- if (digitList.isZero())
- exponent = 0;
-
- boolean negativeExponent = exponent < 0;
- if (negativeExponent) {
- exponent = -exponent;
- if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SIGN) {
- fieldPosition.setBeginIndex(result.length());
- }
- result.append(symbols.getMinusSignString());
- if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SIGN) {
- fieldPosition.setEndIndex(result.length());
- }
- // [Spark/CDL] If exponent has sign, then add an exponent sign
- // attribute.
- if (parseAttr) {
- // Length of exponent sign is 1.
- addAttribute(Field.EXPONENT_SIGN, result.length() - 1, result.length());
- }
- } else if (exponentSignAlwaysShown) {
- if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SIGN) {
- fieldPosition.setBeginIndex(result.length());
- }
- result.append(symbols.getPlusSignString());
- if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SIGN) {
- fieldPosition.setEndIndex(result.length());
- }
- // [Spark/CDL] Add an plus sign attribute.
- if (parseAttr) {
- // Length of exponent sign is 1.
- int expSignBegin = result.length() - 1;
- addAttribute(Field.EXPONENT_SIGN, expSignBegin, result.length());
- }
- }
- int expBegin = result.length();
- digitList.set(exponent);
- {
- int expDig = minExponentDigits;
- if (useExponentialNotation && expDig < 1) {
- expDig = 1;
- }
- for (i = digitList.decimalAt; i < expDig; ++i)
- result.append(digits[0]);
- }
- for (i = 0; i < digitList.decimalAt; ++i) {
- result.append((i < digitList.count) ? digits[digitList.getDigitValue(i)]
- : digits[0]);
- }
- // [Spark/CDL] Add attribute for exponent part.
- if (fieldPosition.getFieldAttribute() == Field.EXPONENT) {
- fieldPosition.setBeginIndex(expBegin);
- fieldPosition.setEndIndex(result.length());
- }
- if (parseAttr) {
- addAttribute(Field.EXPONENT, expBegin, result.length());
- }
- }
-
- private final void addPadding(StringBuffer result, FieldPosition fieldPosition, int prefixLen,
- int suffixLen) {
- if (formatWidth > 0) {
- int len = formatWidth - result.length();
- if (len > 0) {
- char[] padding = new char[len];
- for (int i = 0; i < len; ++i) {
- padding[i] = pad;
- }
- switch (padPosition) {
- case PAD_AFTER_PREFIX:
- result.insert(prefixLen, padding);
- break;
- case PAD_BEFORE_PREFIX:
- result.insert(0, padding);
- break;
- case PAD_BEFORE_SUFFIX:
- result.insert(result.length() - suffixLen, padding);
- break;
- case PAD_AFTER_SUFFIX:
- result.append(padding);
- break;
- }
- if (padPosition == PAD_BEFORE_PREFIX || padPosition == PAD_AFTER_PREFIX) {
- fieldPosition.setBeginIndex(fieldPosition.getBeginIndex() + len);
- fieldPosition.setEndIndex(fieldPosition.getEndIndex() + len);
- }
- }
- }
- }
-
- /**
- * Parses the given string, returning a <code>Number</code> object to represent the
- * parsed value. <code>Double</code> objects are returned to represent non-integral
- * values which cannot be stored in a <code>BigDecimal</code>. These are
- * <code>NaN</code>, infinity, -infinity, and -0.0. If {@link #isParseBigDecimal()} is
- * false (the default), all other values are returned as <code>Long</code>,
- * <code>BigInteger</code>, or <code>BigDecimal</code> values, in that order of
- * preference. If {@link #isParseBigDecimal()} is true, all other values are returned
- * as <code>BigDecimal</code> valuse. If the parse fails, null is returned.
- *
- * @param text the string to be parsed
- * @param parsePosition defines the position where parsing is to begin, and upon
- * return, the position where parsing left off. If the position has not changed upon
- * return, then parsing failed.
- * @return a <code>Number</code> object with the parsed value or
- * <code>null</code> if the parse failed
- */
- @Override
- public Number parse(String text, ParsePosition parsePosition) {
- return (Number) parse(text, parsePosition, null);
- }
-
- /**
- * Parses text from the given string as a CurrencyAmount. Unlike the parse() method,
- * this method will attempt to parse a generic currency name, searching for a match of
- * this object's locale's currency display names, or for a 3-letter ISO currency
- * code. This method will fail if this format is not a currency format, that is, if it
- * does not contain the currency pattern symbol (U+00A4) in its prefix or suffix.
- *
- * @param text the text to parse
- * @param pos input-output position; on input, the position within text to match; must
- * have 0 &lt;= pos.getIndex() &lt; text.length(); on output, the position after the last
- * matched character. If the parse fails, the position in unchanged upon output.
- * @return a CurrencyAmount, or null upon failure
- */
- @Override
- public CurrencyAmount parseCurrency(CharSequence text, ParsePosition pos) {
- Currency[] currency = new Currency[1];
- return (CurrencyAmount) parse(text.toString(), pos, currency);
- }
-
- /**
- * Parses the given text as either a Number or a CurrencyAmount.
- *
- * @param text the string to parse
- * @param parsePosition input-output position; on input, the position within text to
- * match; must have 0 <= pos.getIndex() < text.length(); on output, the position after
- * the last matched character. If the parse fails, the position in unchanged upon
- * output.
- * @param currency if non-null, a CurrencyAmount is parsed and returned; otherwise a
- * Number is parsed and returned
- * @return a Number or CurrencyAmount or null
- */
- private Object parse(String text, ParsePosition parsePosition, Currency[] currency) {
- int backup;
- int i = backup = parsePosition.getIndex();
-
- // Handle NaN as a special case:
-
- // Skip padding characters, if around prefix
- if (formatWidth > 0 &&
- (padPosition == PAD_BEFORE_PREFIX || padPosition == PAD_AFTER_PREFIX)) {
- i = skipPadding(text, i);
- }
- if (text.regionMatches(i, symbols.getNaN(), 0, symbols.getNaN().length())) {
- i += symbols.getNaN().length();
- // Skip padding characters, if around suffix
- if (formatWidth > 0 && (padPosition == PAD_BEFORE_SUFFIX ||
- padPosition == PAD_AFTER_SUFFIX)) {
- i = skipPadding(text, i);
- }
- parsePosition.setIndex(i);
- return new Double(Double.NaN);
- }
-
- // NaN parse failed; start over
- i = backup;
-
- boolean[] status = new boolean[STATUS_LENGTH];
- if (currencySignCount != CURRENCY_SIGN_COUNT_ZERO) {
- if (!parseForCurrency(text, parsePosition, currency, status)) {
- return null;
- }
- } else if (currency != null) {
- return null;
- } else {
- if (!subparse(text, parsePosition, digitList, status, currency, negPrefixPattern,
- negSuffixPattern, posPrefixPattern, posSuffixPattern,
- false, Currency.SYMBOL_NAME)) {
- parsePosition.setIndex(backup);
- return null;
- }
- }
-
- Number n = null;
-
- // Handle infinity
- if (status[STATUS_INFINITE]) {
- n = new Double(status[STATUS_POSITIVE] ? Double.POSITIVE_INFINITY :
- Double.NEGATIVE_INFINITY);
- }
-
- // Handle underflow
- else if (status[STATUS_UNDERFLOW]) {
- n = status[STATUS_POSITIVE] ? new Double("0.0") : new Double("-0.0");
- }
-
- // Handle -0.0
- else if (!status[STATUS_POSITIVE] && digitList.isZero()) {
- n = new Double("-0.0");
- }
-
- else {
- // Do as much of the multiplier conversion as possible without
- // losing accuracy.
- int mult = multiplier; // Don't modify this.multiplier
- while (mult % 10 == 0) {
- --digitList.decimalAt;
- mult /= 10;
- }
-
- // Handle integral values
- if (!parseBigDecimal && mult == 1 && digitList.isIntegral()) {
- // hack quick long
- if (digitList.decimalAt < 12) { // quick check for long
- long l = 0;
- if (digitList.count > 0) {
- int nx = 0;
- while (nx < digitList.count) {
- l = l * 10 + (char) digitList.digits[nx++] - '0';
- }
- while (nx++ < digitList.decimalAt) {
- l *= 10;
- }
- if (!status[STATUS_POSITIVE]) {
- l = -l;
- }
- }
- n = Long.valueOf(l);
- } else {
- BigInteger big = digitList.getBigInteger(status[STATUS_POSITIVE]);
- n = (big.bitLength() < 64) ? (Number) Long.valueOf(big.longValue()) : (Number) big;
- }
- }
- // Handle non-integral values or the case where parseBigDecimal is set
- else {
- BigDecimal big = digitList.getBigDecimalICU(status[STATUS_POSITIVE]);
- n = big;
- if (mult != 1) {
- n = big.divide(BigDecimal.valueOf(mult), mathContext);
- }
- }
- }
-
- // Assemble into CurrencyAmount if necessary
- return (currency != null) ? (Object) new CurrencyAmount(n, currency[0]) : (Object) n;
- }
-
- private boolean parseForCurrency(String text, ParsePosition parsePosition,
- Currency[] currency, boolean[] status) {
- int origPos = parsePosition.getIndex();
- if (!isReadyForParsing) {
- int savedCurrencySignCount = currencySignCount;
- setupCurrencyAffixForAllPatterns();
- // reset pattern back
- if (savedCurrencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
- applyPatternWithoutExpandAffix(formatPattern, false);
- } else {
- applyPattern(formatPattern, false);
- }
- isReadyForParsing = true;
- }
- int maxPosIndex = origPos;
- int maxErrorPos = -1;
- boolean[] savedStatus = null;
- // First, parse against current pattern.
- // Since current pattern could be set by applyPattern(),
- // it could be an arbitrary pattern, and it may not be the one
- // defined in current locale.
- boolean[] tmpStatus = new boolean[STATUS_LENGTH];
- ParsePosition tmpPos = new ParsePosition(origPos);
- DigitList tmpDigitList = new DigitList();
- boolean found;
- if (style == NumberFormat.PLURALCURRENCYSTYLE) {
- found = subparse(text, tmpPos, tmpDigitList, tmpStatus, currency,
- negPrefixPattern, negSuffixPattern, posPrefixPattern, posSuffixPattern,
- true, Currency.LONG_NAME);
- } else {
- found = subparse(text, tmpPos, tmpDigitList, tmpStatus, currency,
- negPrefixPattern, negSuffixPattern, posPrefixPattern, posSuffixPattern,
- true, Currency.SYMBOL_NAME);
- }
- if (found) {
- if (tmpPos.getIndex() > maxPosIndex) {
- maxPosIndex = tmpPos.getIndex();
- savedStatus = tmpStatus;
- digitList = tmpDigitList;
- }
- } else {
- maxErrorPos = tmpPos.getErrorIndex();
- }
- // Then, parse against affix patterns. Those are currency patterns and currency
- // plural patterns defined in the locale.
- for (AffixForCurrency affix : affixPatternsForCurrency) {
- tmpStatus = new boolean[STATUS_LENGTH];
- tmpPos = new ParsePosition(origPos);
- tmpDigitList = new DigitList();
- boolean result = subparse(text, tmpPos, tmpDigitList, tmpStatus, currency,
- affix.getNegPrefix(), affix.getNegSuffix(),
- affix.getPosPrefix(), affix.getPosSuffix(),
- true, affix.getPatternType());
- if (result) {
- found = true;
- if (tmpPos.getIndex() > maxPosIndex) {
- maxPosIndex = tmpPos.getIndex();
- savedStatus = tmpStatus;
- digitList = tmpDigitList;
- }
- } else {
- maxErrorPos = (tmpPos.getErrorIndex() > maxErrorPos) ? tmpPos.getErrorIndex()
- : maxErrorPos;
- }
- }
- // Finally, parse against simple affix to find the match. For example, in
- // TestMonster suite, if the to-be-parsed text is "-\u00A40,00".
- // complexAffixCompare will not find match, since there is no ISO code matches
- // "\u00A4", and the parse stops at "\u00A4". We will just use simple affix
- // comparison (look for exact match) to pass it.
- //
- // TODO: We should parse against simple affix first when
- // output currency is not requested. After the complex currency
- // parsing implementation was introduced, the default currency
- // instance parsing slowed down because of the new code flow.
- // I filed #10312 - Yoshito
- tmpStatus = new boolean[STATUS_LENGTH];
- tmpPos = new ParsePosition(origPos);
- tmpDigitList = new DigitList();
-
- // Disable complex currency parsing and try it again.
- boolean result = subparse(text, tmpPos, tmpDigitList, tmpStatus, currency,
- negativePrefix, negativeSuffix, positivePrefix, positiveSuffix,
- false /* disable complex currency parsing */, Currency.SYMBOL_NAME);
- if (result) {
- if (tmpPos.getIndex() > maxPosIndex) {
- maxPosIndex = tmpPos.getIndex();
- savedStatus = tmpStatus;
- digitList = tmpDigitList;
- }
- found = true;
- } else {
- maxErrorPos = (tmpPos.getErrorIndex() > maxErrorPos) ? tmpPos.getErrorIndex() :
- maxErrorPos;
- }
-
- if (!found) {
- // parsePosition.setIndex(origPos);
- parsePosition.setErrorIndex(maxErrorPos);
- } else {
- parsePosition.setIndex(maxPosIndex);
- parsePosition.setErrorIndex(-1);
- for (int index = 0; index < STATUS_LENGTH; ++index) {
- status[index] = savedStatus[index];
- }
- }
- return found;
- }
-
- // Get affix patterns used in locale's currency pattern (NumberPatterns[1]) and
- // currency plural pattern (CurrencyUnitPatterns).
- private void setupCurrencyAffixForAllPatterns() {
- if (currencyPluralInfo == null) {
- currencyPluralInfo = new CurrencyPluralInfo(symbols.getULocale());
- }
- affixPatternsForCurrency = new HashSet<AffixForCurrency>();
-
- // save the current pattern, since it will be changed by
- // applyPatternWithoutExpandAffix
- String savedFormatPattern = formatPattern;
-
- // CURRENCYSTYLE and ISOCURRENCYSTYLE should have the same prefix and suffix, so,
- // only need to save one of them. Here, chose onlyApplyPatternWithoutExpandAffix
- // without saving the actualy pattern in 'pattern' data member. TODO: is it uloc?
- applyPatternWithoutExpandAffix(getPattern(symbols.getULocale(), NumberFormat.CURRENCYSTYLE),
- false);
- AffixForCurrency affixes = new AffixForCurrency(
- negPrefixPattern, negSuffixPattern, posPrefixPattern, posSuffixPattern,
- Currency.SYMBOL_NAME);
- affixPatternsForCurrency.add(affixes);
-
- // add plural pattern
- Iterator<String> iter = currencyPluralInfo.pluralPatternIterator();
- Set<String> currencyUnitPatternSet = new HashSet<String>();
- while (iter.hasNext()) {
- String pluralCount = iter.next();
- String currencyPattern = currencyPluralInfo.getCurrencyPluralPattern(pluralCount);
- if (currencyPattern != null &&
- currencyUnitPatternSet.contains(currencyPattern) == false) {
- currencyUnitPatternSet.add(currencyPattern);
- applyPatternWithoutExpandAffix(currencyPattern, false);
- affixes = new AffixForCurrency(negPrefixPattern, negSuffixPattern, posPrefixPattern,
- posSuffixPattern, Currency.LONG_NAME);
- affixPatternsForCurrency.add(affixes);
- }
- }
- // reset pattern back
- formatPattern = savedFormatPattern;
- }
-
- // currency formatting style options
- private static final int CURRENCY_SIGN_COUNT_ZERO = 0;
- private static final int CURRENCY_SIGN_COUNT_IN_SYMBOL_FORMAT = 1;
- private static final int CURRENCY_SIGN_COUNT_IN_ISO_FORMAT = 2;
- private static final int CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT = 3;
-
- private static final int STATUS_INFINITE = 0;
- private static final int STATUS_POSITIVE = 1;
- private static final int STATUS_UNDERFLOW = 2;
- private static final int STATUS_LENGTH = 3;
-
- private static final UnicodeSet dotEquivalents = new UnicodeSet(
- //"[.\u2024\u3002\uFE12\uFE52\uFF0E\uFF61]"
- 0x002E, 0x002E,
- 0x2024, 0x2024,
- 0x3002, 0x3002,
- 0xFE12, 0xFE12,
- 0xFE52, 0xFE52,
- 0xFF0E, 0xFF0E,
- 0xFF61, 0xFF61).freeze();
-
- private static final UnicodeSet commaEquivalents = new UnicodeSet(
- //"[,\u060C\u066B\u3001\uFE10\uFE11\uFE50\uFE51\uFF0C\uFF64]"
- 0x002C, 0x002C,
- 0x060C, 0x060C,
- 0x066B, 0x066B,
- 0x3001, 0x3001,
- 0xFE10, 0xFE11,
- 0xFE50, 0xFE51,
- 0xFF0C, 0xFF0C,
- 0xFF64, 0xFF64).freeze();
-
-// private static final UnicodeSet otherGroupingSeparators = new UnicodeSet(
-// //"[\\ '\u00A0\u066C\u2000-\u200A\u2018\u2019\u202F\u205F\u3000\uFF07]"
-// 0x0020, 0x0020,
-// 0x0027, 0x0027,
-// 0x00A0, 0x00A0,
-// 0x066C, 0x066C,
-// 0x2000, 0x200A,
-// 0x2018, 0x2019,
-// 0x202F, 0x202F,
-// 0x205F, 0x205F,
-// 0x3000, 0x3000,
-// 0xFF07, 0xFF07).freeze();
-
- private static final UnicodeSet strictDotEquivalents = new UnicodeSet(
- //"[.\u2024\uFE52\uFF0E\uFF61]"
- 0x002E, 0x002E,
- 0x2024, 0x2024,
- 0xFE52, 0xFE52,
- 0xFF0E, 0xFF0E,
- 0xFF61, 0xFF61).freeze();
-
- private static final UnicodeSet strictCommaEquivalents = new UnicodeSet(
- //"[,\u066B\uFE10\uFE50\uFF0C]"
- 0x002C, 0x002C,
- 0x066B, 0x066B,
- 0xFE10, 0xFE10,
- 0xFE50, 0xFE50,
- 0xFF0C, 0xFF0C).freeze();
-
-// private static final UnicodeSet strictOtherGroupingSeparators = new UnicodeSet(
-// //"[\\ '\u00A0\u066C\u2000-\u200A\u2018\u2019\u202F\u205F\u3000\uFF07]"
-// 0x0020, 0x0020,
-// 0x0027, 0x0027,
-// 0x00A0, 0x00A0,
-// 0x066C, 0x066C,
-// 0x2000, 0x200A,
-// 0x2018, 0x2019,
-// 0x202F, 0x202F,
-// 0x205F, 0x205F,
-// 0x3000, 0x3000,
-// 0xFF07, 0xFF07).freeze();
-
- private static final UnicodeSet defaultGroupingSeparators =
- // new UnicodeSet(dotEquivalents).addAll(commaEquivalents)
- // .addAll(otherGroupingSeparators).freeze();
- new UnicodeSet(
- 0x0020, 0x0020,
- 0x0027, 0x0027,
- 0x002C, 0x002C,
- 0x002E, 0x002E,
- 0x00A0, 0x00A0,
- 0x060C, 0x060C,
- 0x066B, 0x066C,
- 0x2000, 0x200A,
- 0x2018, 0x2019,
- 0x2024, 0x2024,
- 0x202F, 0x202F,
- 0x205F, 0x205F,
- 0x3000, 0x3002,
- 0xFE10, 0xFE12,
- 0xFE50, 0xFE52,
- 0xFF07, 0xFF07,
- 0xFF0C, 0xFF0C,
- 0xFF0E, 0xFF0E,
- 0xFF61, 0xFF61,
- 0xFF64, 0xFF64).freeze();
-
- private static final UnicodeSet strictDefaultGroupingSeparators =
- // new UnicodeSet(strictDotEquivalents).addAll(strictCommaEquivalents)
- // .addAll(strictOtherGroupingSeparators).freeze();
- new UnicodeSet(
- 0x0020, 0x0020,
- 0x0027, 0x0027,
- 0x002C, 0x002C,
- 0x002E, 0x002E,
- 0x00A0, 0x00A0,
- 0x066B, 0x066C,
- 0x2000, 0x200A,
- 0x2018, 0x2019,
- 0x2024, 0x2024,
- 0x202F, 0x202F,
- 0x205F, 0x205F,
- 0x3000, 0x3000,
- 0xFE10, 0xFE10,
- 0xFE50, 0xFE50,
- 0xFE52, 0xFE52,
- 0xFF07, 0xFF07,
- 0xFF0C, 0xFF0C,
- 0xFF0E, 0xFF0E,
- 0xFF61, 0xFF61).freeze();
-
- static final UnicodeSet minusSigns =
- new UnicodeSet(
- 0x002D, 0x002D,
- 0x207B, 0x207B,
- 0x208B, 0x208B,
- 0x2212, 0x2212,
- 0x2796, 0x2796,
- 0xFE63, 0xFE63,
- 0xFF0D, 0xFF0D).freeze();
-
- static final UnicodeSet plusSigns =
- new UnicodeSet(
- 0x002B, 0x002B,
- 0x207A, 0x207A,
- 0x208A, 0x208A,
- 0x2795, 0x2795,
- 0xFB29, 0xFB29,
- 0xFE62, 0xFE62,
- 0xFF0B, 0xFF0B).freeze();
-
- // equivalent grouping and decimal support
- static final boolean skipExtendedSeparatorParsing = ICUConfig.get(
- "android.icu.text.DecimalFormat.SkipExtendedSeparatorParsing", "false")
- .equals("true");
-
- // allow control of requiring a matching decimal point when parsing
- boolean parseRequireDecimalPoint = false;
-
- // When parsing a number with big exponential value, it requires to transform the
- // value into a string representation to construct BigInteger instance. We want to
- // set the maximum size because it can easily trigger OutOfMemoryException.
- // PARSE_MAX_EXPONENT is currently set to 1000 (See getParseMaxDigits()),
- // which is much bigger than MAX_VALUE of Double ( See the problem reported by ticket#5698
- private int PARSE_MAX_EXPONENT = 1000;
-
- /**
- * Parses the given text into a number. The text is parsed beginning at parsePosition,
- * until an unparseable character is seen.
- *
- * @param text the string to parse.
- * @param parsePosition the position at which to being parsing. Upon return, the first
- * unparseable character.
- * @param digits the DigitList to set to the parsed value.
- * @param status Upon return contains boolean status flags indicating whether the
- * value was infinite and whether it was positive.
- * @param currency return value for parsed currency, for generic currency parsing
- * mode, or null for normal parsing. In generic currency parsing mode, any currency is
- * parsed, not just the currency that this formatter is set to.
- * @param negPrefix negative prefix pattern
- * @param negSuffix negative suffix pattern
- * @param posPrefix positive prefix pattern
- * @param negSuffix negative suffix pattern
- * @param parseComplexCurrency whether it is complex currency parsing or not.
- * @param type type of currency to parse against, LONG_NAME only or not.
- */
- private final boolean subparse(
- String text, ParsePosition parsePosition, DigitList digits,
- boolean status[], Currency currency[], String negPrefix, String negSuffix, String posPrefix,
- String posSuffix, boolean parseComplexCurrency, int type) {
-
- int position = parsePosition.getIndex();
- int oldStart = parsePosition.getIndex();
-
- // Match padding before prefix
- if (formatWidth > 0 && padPosition == PAD_BEFORE_PREFIX) {
- position = skipPadding(text, position);
- }
-
- // Match positive and negative prefixes; prefer longest match.
- int posMatch = compareAffix(text, position, false, true, posPrefix, parseComplexCurrency, type, currency);
- int negMatch = compareAffix(text, position, true, true, negPrefix, parseComplexCurrency, type, currency);
- if (posMatch >= 0 && negMatch >= 0) {
- if (posMatch > negMatch) {
- negMatch = -1;
- } else if (negMatch > posMatch) {
- posMatch = -1;
- }
- }
- if (posMatch >= 0) {
- position += posMatch;
- } else if (negMatch >= 0) {
- position += negMatch;
- } else {
- parsePosition.setErrorIndex(position);
- return false;
- }
-
- // Match padding after prefix
- if (formatWidth > 0 && padPosition == PAD_AFTER_PREFIX) {
- position = skipPadding(text, position);
- }
-
- // process digits or Inf, find decimal position
- status[STATUS_INFINITE] = false;
- if (text.regionMatches(position, symbols.getInfinity(), 0,
- symbols.getInfinity().length())) {
- position += symbols.getInfinity().length();
- status[STATUS_INFINITE] = true;
- } else {
- // We now have a string of digits, possibly with grouping symbols, and decimal
- // points. We want to process these into a DigitList. We don't want to put a
- // bunch of leading zeros into the DigitList though, so we keep track of the
- // location of the decimal point, put only significant digits into the
- // DigitList, and adjust the exponent as needed.
-
- digits.decimalAt = digits.count = 0;
- String decimal = (currencySignCount == CURRENCY_SIGN_COUNT_ZERO) ?
- symbols.getDecimalSeparatorString() : symbols.getMonetaryDecimalSeparatorString();
- String grouping = (currencySignCount == CURRENCY_SIGN_COUNT_ZERO) ?
- symbols.getGroupingSeparatorString() : symbols.getMonetaryGroupingSeparatorString();
-
- String exponentSep = symbols.getExponentSeparator();
- boolean sawDecimal = false;
- boolean sawGrouping = false;
- boolean sawDigit = false;
- long exponent = 0; // Set to the exponent value, if any
-
- // strict parsing
- boolean strictParse = isParseStrict();
- boolean strictFail = false; // did we exit with a strict parse failure?
- int lastGroup = -1; // where did we last see a grouping separator?
- int groupedDigitCount = 0; // tracking count of digits delimited by grouping separator
- int gs2 = groupingSize2 == 0 ? groupingSize : groupingSize2;
-
- UnicodeSet decimalEquiv = skipExtendedSeparatorParsing ? UnicodeSet.EMPTY :
- getEquivalentDecimals(decimal, strictParse);
- UnicodeSet groupEquiv = skipExtendedSeparatorParsing ? UnicodeSet.EMPTY :
- (strictParse ? strictDefaultGroupingSeparators : defaultGroupingSeparators);
-
- // We have to track digitCount ourselves, because digits.count will pin when
- // the maximum allowable digits is reached.
- int digitCount = 0;
-
- int backup = -1; // used for preserving the last confirmed position
- int[] parsedDigit = {-1}; // allocates int[1] for parsing a single digit
-
- while (position < text.length()) {
- // Check if the sequence at the current position matches a decimal digit
- int matchLen = matchesDigit(text, position, parsedDigit);
- if (matchLen > 0) {
- // matched a digit
- // Cancel out backup setting (see grouping handler below)
- if (backup != -1) {
- if (strictParse) {
- // comma followed by digit, so group before comma is a secondary
- // group. If there was a group separator before that, the group
- // must == the secondary group length, else it can be <= the the
- // secondary group length.
- if ((lastGroup != -1 && groupedDigitCount != gs2)
- || (lastGroup == -1 && groupedDigitCount > gs2)) {
- strictFail = true;
- break;
- }
- }
- lastGroup = backup;
- groupedDigitCount = 0;
- }
-
- groupedDigitCount++;
- position += matchLen;
- backup = -1;
- sawDigit = true;
- if (parsedDigit[0] == 0 && digits.count == 0) {
- // Handle leading zeros
- if (!sawDecimal) {
- // Ignore leading zeros in integer part of number.
- continue;
- }
- // If we have seen the decimal, but no significant digits yet,
- // then we account for leading zeros by decrementing the
- // digits.decimalAt into negative values.
- --digits.decimalAt;
- } else {
- ++digitCount;
- digits.append((char) (parsedDigit[0] + '0'));
- }
- continue;
- }
-
- // Check if the sequence at the current position matches locale's decimal separator
- int decimalStrLen = decimal.length();
- if (text.regionMatches(position, decimal, 0, decimalStrLen)) {
- // matched a decimal separator
- if (strictParse) {
- if (backup != -1 ||
- (lastGroup != -1 && groupedDigitCount != groupingSize)) {
- strictFail = true;
- break;
- }
- }
-
- // If we're only parsing integers, or if we ALREADY saw the decimal,
- // then don't parse this one.
- if (isParseIntegerOnly() || sawDecimal) {
- break;
- }
-
- digits.decimalAt = digitCount; // Not digits.count!
- sawDecimal = true;
- position += decimalStrLen;
- continue;
- }
-
- if (isGroupingUsed()) {
- // Check if the sequence at the current position matches locale's grouping separator
- int groupingStrLen = grouping.length();
- if (text.regionMatches(position, grouping, 0, groupingStrLen)) {
- if (sawDecimal) {
- break;
- }
-
- if (strictParse) {
- if ((!sawDigit || backup != -1)) {
- // leading group, or two group separators in a row
- strictFail = true;
- break;
- }
- }
-
- // Ignore grouping characters, if we are using them, but require that
- // they be followed by a digit. Otherwise we backup and reprocess
- // them.
- backup = position;
- position += groupingStrLen;
- sawGrouping = true;
- continue;
- }
- }
-
- // Check if the code point at the current position matches one of decimal/grouping equivalent group chars
- int cp = text.codePointAt(position);
- if (!sawDecimal && decimalEquiv.contains(cp)) {
- // matched a decimal separator
- if (strictParse) {
- if (backup != -1 ||
- (lastGroup != -1 && groupedDigitCount != groupingSize)) {
- strictFail = true;
- break;
- }
- }
-
- // If we're only parsing integers, or if we ALREADY saw the decimal,
- // then don't parse this one.
- if (isParseIntegerOnly()) {
- break;
- }
-
- digits.decimalAt = digitCount; // Not digits.count!
-
- // Once we see a decimal separator character, we only accept that
- // decimal separator character from then on.
- decimal = String.valueOf(Character.toChars(cp));
-
- sawDecimal = true;
- position += Character.charCount(cp);
- continue;
- }
-
- if (isGroupingUsed() && !sawGrouping && groupEquiv.contains(cp)) {
- // matched a grouping separator
- if (sawDecimal) {
- break;
- }
-
- if (strictParse) {
- if ((!sawDigit || backup != -1)) {
- // leading group, or two group separators in a row
- strictFail = true;
- break;
- }
- }
-
- // Once we see a grouping character, we only accept that grouping
- // character from then on.
- grouping = String.valueOf(Character.toChars(cp));
-
- // Ignore grouping characters, if we are using them, but require that
- // they be followed by a digit. Otherwise we backup and reprocess
- // them.
- backup = position;
- position += Character.charCount(cp);
- sawGrouping = true;
- continue;
- }
-
- // Check if the sequence at the current position matches locale's exponent separator
- int exponentSepStrLen = exponentSep.length();
- if (text.regionMatches(true, position, exponentSep, 0, exponentSepStrLen)) {
- // parse sign, if present
- boolean negExp = false;
- int pos = position + exponentSep.length();
- if (pos < text.length()) {
- String plusSign = symbols.getPlusSignString();
- String minusSign = symbols.getMinusSignString();
- if (text.regionMatches(pos, plusSign, 0, plusSign.length())) {
- pos += plusSign.length();
- } else if (text.regionMatches(pos, minusSign, 0, minusSign.length())) {
- pos += minusSign.length();
- negExp = true;
- }
- }
-
- DigitList exponentDigits = new DigitList();
- exponentDigits.count = 0;
- while (pos < text.length()) {
- int digitMatchLen = matchesDigit(text, pos, parsedDigit);
- if (digitMatchLen > 0) {
- exponentDigits.append((char) (parsedDigit[0] + '0'));
- pos += digitMatchLen;
- } else {
- break;
- }
- }
-
- if (exponentDigits.count > 0) {
- // defer strict parse until we know we have a bona-fide exponent
- if (strictParse && sawGrouping) {
- strictFail = true;
- break;
- }
-
- // Quick overflow check for exponential part. Actual limit check
- // will be done later in this code.
- if (exponentDigits.count > 10 /* maximum decimal digits for int */) {
- if (negExp) {
- // set underflow flag
- status[STATUS_UNDERFLOW] = true;
- } else {
- // set infinite flag
- status[STATUS_INFINITE] = true;
- }
- } else {
- exponentDigits.decimalAt = exponentDigits.count;
- exponent = exponentDigits.getLong();
- if (negExp) {
- exponent = -exponent;
- }
- }
- position = pos; // Advance past the exponent
- }
-
- break; // Whether we fail or succeed, we exit this loop
- }
-
- // All other cases, stop parsing
- break;
- }
-
- if (digits.decimalAt == 0 && isDecimalPatternMatchRequired()) {
- if (this.formatPattern.indexOf(decimal) != -1) {
- parsePosition.setIndex(oldStart);
- parsePosition.setErrorIndex(position);
- return false;
- }
- }
-
- if (backup != -1)
- position = backup;
-
- // If there was no decimal point we have an integer
- if (!sawDecimal) {
- digits.decimalAt = digitCount; // Not digits.count!
- }
-
- // check for strict parse errors
- if (strictParse && !sawDecimal) {
- if (lastGroup != -1 && groupedDigitCount != groupingSize) {
- strictFail = true;
- }
- }
- if (strictFail) {
- // only set with strictParse and a leading zero error leading zeros are an
- // error with strict parsing except immediately before nondigit (except
- // group separator followed by digit), or end of text.
-
- parsePosition.setIndex(oldStart);
- parsePosition.setErrorIndex(position);
- return false;
- }
-
- // Adjust for exponent, if any
- exponent += digits.decimalAt;
- if (exponent < -getParseMaxDigits()) {
- status[STATUS_UNDERFLOW] = true;
- } else if (exponent > getParseMaxDigits()) {
- status[STATUS_INFINITE] = true;
- } else {
- digits.decimalAt = (int) exponent;
- }
-
- // If none of the text string was recognized. For example, parse "x" with
- // pattern "#0.00" (return index and error index both 0) parse "$" with
- // pattern "$#0.00". (return index 0 and error index 1).
- if (!sawDigit && digitCount == 0) {
- parsePosition.setIndex(oldStart);
- parsePosition.setErrorIndex(oldStart);
- return false;
- }
- }
-
- // Match padding before suffix
- if (formatWidth > 0 && padPosition == PAD_BEFORE_SUFFIX) {
- position = skipPadding(text, position);
- }
-
- // Match positive and negative suffixes; prefer longest match.
- if (posMatch >= 0) {
- posMatch = compareAffix(text, position, false, false, posSuffix, parseComplexCurrency, type, currency);
- }
- if (negMatch >= 0) {
- negMatch = compareAffix(text, position, true, false, negSuffix, parseComplexCurrency, type, currency);
- }
- if (posMatch >= 0 && negMatch >= 0) {
- if (posMatch > negMatch) {
- negMatch = -1;
- } else if (negMatch > posMatch) {
- posMatch = -1;
- }
- }
-
- // Fail if neither or both
- if ((posMatch >= 0) == (negMatch >= 0)) {
- parsePosition.setErrorIndex(position);
- return false;
- }
-
- position += (posMatch >= 0 ? posMatch : negMatch);
-
- // Match padding after suffix
- if (formatWidth > 0 && padPosition == PAD_AFTER_SUFFIX) {
- position = skipPadding(text, position);
- }
-
- parsePosition.setIndex(position);
-
- status[STATUS_POSITIVE] = (posMatch >= 0);
-
- if (parsePosition.getIndex() == oldStart) {
- parsePosition.setErrorIndex(position);
- return false;
- }
- return true;
- }
-
- /**
- * Check if the substring at the specified position matches a decimal digit.
- * If matched, this method sets the decimal value to <code>decVal</code> and
- * returns matched length.
- *
- * @param str The input string
- * @param start The start index
- * @param decVal Receives decimal value
- * @return Length of match, or 0 if the sequence at the position is not
- * a decimal digit.
- */
- private int matchesDigit(String str, int start, int[] decVal) {
- String[] localeDigits = symbols.getDigitStringsLocal();
-
- // Check if the sequence at the current position matches locale digits.
- for (int i = 0; i < 10; i++) {
- int digitStrLen = localeDigits[i].length();
- if (str.regionMatches(start, localeDigits[i], 0, digitStrLen)) {
- decVal[0] = i;
- return digitStrLen;
- }
- }
-
- // If no locale digit match, then check if this is a Unicode digit
- int cp = str.codePointAt(start);
- decVal[0] = UCharacter.digit(cp, 10);
- if (decVal[0] >= 0) {
- return Character.charCount(cp);
- }
-
- return 0;
- }
-
- /**
- * Returns a set of characters equivalent to the given desimal separator used for
- * parsing number. This method may return an empty set.
- */
- private UnicodeSet getEquivalentDecimals(String decimal, boolean strictParse) {
- UnicodeSet equivSet = UnicodeSet.EMPTY;
- if (strictParse) {
- if (strictDotEquivalents.contains(decimal)) {
- equivSet = strictDotEquivalents;
- } else if (strictCommaEquivalents.contains(decimal)) {
- equivSet = strictCommaEquivalents;
- }
- } else {
- if (dotEquivalents.contains(decimal)) {
- equivSet = dotEquivalents;
- } else if (commaEquivalents.contains(decimal)) {
- equivSet = commaEquivalents;
- }
- }
- return equivSet;
- }
-
- /**
- * Starting at position, advance past a run of pad characters, if any. Return the
- * index of the first character after position that is not a pad character. Result is
- * >= position.
- */
- private final int skipPadding(String text, int position) {
- while (position < text.length() && text.charAt(position) == pad) {
- ++position;
- }
- return position;
- }
-
- /**
- * Returns the length matched by the given affix, or -1 if none. Runs of white space
- * in the affix, match runs of white space in the input. Pattern white space and input
- * white space are determined differently; see code.
- *
- * @param text input text
- * @param pos offset into input at which to begin matching
- * @param isNegative
- * @param isPrefix
- * @param affixPat affix pattern used for currency affix comparison
- * @param complexCurrencyParsing whether it is currency parsing or not
- * @param type compare against currency type, LONG_NAME only or not.
- * @param currency return value for parsed currency, for generic currency parsing
- * mode, or null for normal parsing. In generic currency parsing mode, any currency
- * is parsed, not just the currency that this formatter is set to.
- * @return length of input that matches, or -1 if match failure
- */
- private int compareAffix(String text, int pos, boolean isNegative, boolean isPrefix,
- String affixPat, boolean complexCurrencyParsing, int type, Currency[] currency) {
- if (currency != null || currencyChoice != null || (currencySignCount != CURRENCY_SIGN_COUNT_ZERO && complexCurrencyParsing)) {
- return compareComplexAffix(affixPat, text, pos, type, currency);
- }
- if (isPrefix) {
- return compareSimpleAffix(isNegative ? negativePrefix : positivePrefix, text, pos);
- } else {
- return compareSimpleAffix(isNegative ? negativeSuffix : positiveSuffix, text, pos);
- }
-
- }
-
- /**
- * Check for bidi marks: LRM, RLM, ALM
- */
- private static boolean isBidiMark(int c) {
- return (c==0x200E || c==0x200F || c==0x061C);
- }
-
- /**
- * Remove bidi marks from affix
- */
- private static String trimMarksFromAffix(String affix) {
- boolean hasBidiMark = false;
- int idx = 0;
- for (; idx < affix.length(); idx++) {
- if (isBidiMark(affix.charAt(idx))) {
- hasBidiMark = true;
- break;
- }
- }
- if (!hasBidiMark) {
- return affix;
- }
-
- StringBuilder buf = new StringBuilder();
- buf.append(affix, 0, idx);
- idx++; // skip the first Bidi mark
- for (; idx < affix.length(); idx++) {
- char c = affix.charAt(idx);
- if (!isBidiMark(c)) {
- buf.append(c);
- }
- }
-
- return buf.toString();
- }
-
- /**
- * Return the length matched by the given affix, or -1 if none. Runs of white space in
- * the affix, match runs of white space in the input. Pattern white space and input
- * white space are determined differently; see code.
- *
- * @param affix pattern string, taken as a literal
- * @param input input text
- * @param pos offset into input at which to begin matching
- * @return length of input that matches, or -1 if match failure
- */
- private static int compareSimpleAffix(String affix, String input, int pos) {
- int start = pos;
- // Affixes here might consist of sign, currency symbol and related spacing, etc.
- // For more efficiency we should keep lazily-created trimmed affixes around in
- // instance variables instead of trimming each time they are used (the next step).
- String trimmedAffix = (affix.length() > 1)? trimMarksFromAffix(affix): affix;
- for (int i = 0; i < trimmedAffix.length();) {
- int c = UTF16.charAt(trimmedAffix, i);
- int len = UTF16.getCharCount(c);
- if (PatternProps.isWhiteSpace(c)) {
- // We may have a pattern like: \u200F and input text like: \u200F Note
- // that U+200F and U+0020 are Pattern_White_Space but only U+0020 is
- // UWhiteSpace. So we have to first do a direct match of the run of RULE
- // whitespace in the pattern, then match any extra characters.
- boolean literalMatch = false;
- while (pos < input.length()) {
- int ic = UTF16.charAt(input, pos);
- if (ic == c) {
- literalMatch = true;
- i += len;
- pos += len;
- if (i == trimmedAffix.length()) {
- break;
- }
- c = UTF16.charAt(trimmedAffix, i);
- len = UTF16.getCharCount(c);
- if (!PatternProps.isWhiteSpace(c)) {
- break;
- }
- } else if (isBidiMark(ic)) {
- pos++; // just skip over this input text
- } else {
- break;
- }
- }
-
- // Advance over run in trimmedAffix
- i = skipPatternWhiteSpace(trimmedAffix, i);
-
- // Advance over run in input text. Must see at least one white space char
- // in input, unless we've already matched some characters literally.
- int s = pos;
- pos = skipUWhiteSpace(input, pos);
- if (pos == s && !literalMatch) {
- return -1;
- }
- // If we skip UWhiteSpace in the input text, we need to skip it in the
- // pattern. Otherwise, the previous lines may have skipped over text
- // (such as U+00A0) that is also in the trimmedAffix.
- i = skipUWhiteSpace(trimmedAffix, i);
- } else {
- boolean match = false;
- while (pos < input.length()) {
- int ic = UTF16.charAt(input, pos);
- if (!match && equalWithSignCompatibility(ic, c)) {
- i += len;
- pos += len;
- match = true;
- } else if (isBidiMark(ic)) {
- pos++; // just skip over this input text
- } else {
- break;
- }
- }
- if (!match) {
- return -1;
- }
- }
- }
- return pos - start;
- }
-
- private static boolean equalWithSignCompatibility(int lhs, int rhs) {
- return lhs == rhs
- || (minusSigns.contains(lhs) && minusSigns.contains(rhs))
- || (plusSigns.contains(lhs) && plusSigns.contains(rhs));
- }
-
- /**
- * Skips over a run of zero or more Pattern_White_Space characters at pos in text.
- */
- private static int skipPatternWhiteSpace(String text, int pos) {
- while (pos < text.length()) {
- int c = UTF16.charAt(text, pos);
- if (!PatternProps.isWhiteSpace(c)) {
- break;
- }
- pos += UTF16.getCharCount(c);
- }
- return pos;
- }
-
- /**
- * Skips over a run of zero or more isUWhiteSpace() characters at pos in text.
- */
- private static int skipUWhiteSpace(String text, int pos) {
- while (pos < text.length()) {
- int c = UTF16.charAt(text, pos);
- if (!UCharacter.isUWhiteSpace(c)) {
- break;
- }
- pos += UTF16.getCharCount(c);
- }
- return pos;
- }
-
- /**
- * Skips over a run of zero or more bidi marks at pos in text.
- */
- private static int skipBidiMarks(String text, int pos) {
- while (pos < text.length()) {
- int c = UTF16.charAt(text, pos);
- if (!isBidiMark(c)) {
- break;
- }
- pos += UTF16.getCharCount(c);
- }
- return pos;
- }
-
- /**
- * Returns the length matched by the given affix, or -1 if none.
- *
- * @param affixPat pattern string
- * @param text input text
- * @param pos offset into input at which to begin matching
- * @param type parse against currency type, LONG_NAME only or not.
- * @param currency return value for parsed currency, for generic
- * currency parsing mode, or null for normal parsing. In generic
- * currency parsing mode, any currency is parsed, not just the
- * currency that this formatter is set to.
- * @return position after the matched text, or -1 if match failure
- */
- private int compareComplexAffix(String affixPat, String text, int pos, int type,
- Currency[] currency) {
- int start = pos;
- for (int i = 0; i < affixPat.length() && pos >= 0;) {
- char c = affixPat.charAt(i++);
- if (c == QUOTE) {
- for (;;) {
- int j = affixPat.indexOf(QUOTE, i);
- if (j == i) {
- pos = match(text, pos, QUOTE);
- i = j + 1;
- break;
- } else if (j > i) {
- pos = match(text, pos, affixPat.substring(i, j));
- i = j + 1;
- if (i < affixPat.length() && affixPat.charAt(i) == QUOTE) {
- pos = match(text, pos, QUOTE);
- ++i;
- // loop again
- } else {
- break;
- }
- } else {
- // Unterminated quote; should be caught by apply
- // pattern.
- throw new RuntimeException();
- }
- }
- continue;
- }
-
- String affix = null;
-
- switch (c) {
- case CURRENCY_SIGN:
- // since the currency names in choice format is saved the same way as
- // other currency names, do not need to do currency choice parsing here.
- // the general currency parsing parse against all names, including names
- // in choice format. assert(currency != null || (getCurrency() != null &&
- // currencyChoice != null));
- boolean intl = i < affixPat.length() && affixPat.charAt(i) == CURRENCY_SIGN;
- if (intl) {
- ++i;
- }
- boolean plural = i < affixPat.length() && affixPat.charAt(i) == CURRENCY_SIGN;
- if (plural) {
- ++i;
- intl = false;
- }
- // Parse generic currency -- anything for which we have a display name, or
- // any 3-letter ISO code. Try to parse display name for our locale; first
- // determine our locale. TODO: use locale in CurrencyPluralInfo
- ULocale uloc = getLocale(ULocale.VALID_LOCALE);
- if (uloc == null) {
- // applyPattern has been called; use the symbols
- uloc = symbols.getLocale(ULocale.VALID_LOCALE);
- }
- // Delegate parse of display name => ISO code to Currency
- ParsePosition ppos = new ParsePosition(pos);
- // using Currency.parse to handle mixed style parsing.
- String iso = Currency.parse(uloc, text, type, ppos);
-
- // If parse succeeds, populate currency[0]
- if (iso != null) {
- if (currency != null) {
- currency[0] = Currency.getInstance(iso);
- } else {
- // The formatter is currency-style but the client has not requested
- // the value of the parsed currency. In this case, if that value does
- // not match the formatter's current value, then the parse fails.
- Currency effectiveCurr = getEffectiveCurrency();
- if (iso.compareTo(effectiveCurr.getCurrencyCode()) != 0) {
- pos = -1;
- continue;
- }
- }
- pos = ppos.getIndex();
- } else {
- pos = -1;
- }
- continue;
- case PATTERN_PERCENT:
- affix = symbols.getPercentString();
- break;
- case PATTERN_PER_MILLE:
- affix = symbols.getPerMillString();
- break;
- case PATTERN_PLUS_SIGN:
- affix = symbols.getPlusSignString();
- break;
- case PATTERN_MINUS_SIGN:
- affix = symbols.getMinusSignString();
- break;
- default:
- // fall through to affix != null test, which will fail
- break;
- }
-
- if (affix != null) {
- pos = match(text, pos, affix);
- continue;
- }
-
- pos = match(text, pos, c);
- if (PatternProps.isWhiteSpace(c)) {
- i = skipPatternWhiteSpace(affixPat, i);
- }
- }
-
- return pos - start;
- }
-
- /**
- * Matches a single character at text[pos] and return the index of the next character
- * upon success. Return -1 on failure. If ch is a Pattern_White_Space then match a run of
- * white space in text.
- */
- static final int match(String text, int pos, int ch) {
- if (pos < 0 || pos >= text.length()) {
- return -1;
- }
- pos = skipBidiMarks(text, pos);
- if (PatternProps.isWhiteSpace(ch)) {
- // Advance over run of white space in input text
- // Must see at least one white space char in input
- int s = pos;
- pos = skipPatternWhiteSpace(text, pos);
- if (pos == s) {
- return -1;
- }
- return pos;
- }
- if (pos >= text.length() || UTF16.charAt(text, pos) != ch) {
- return -1;
- }
- pos = skipBidiMarks(text, pos + UTF16.getCharCount(ch));
- return pos;
- }
-
- /**
- * Matches a string at text[pos] and return the index of the next character upon
- * success. Return -1 on failure. Match a run of white space in str with a run of
- * white space in text.
- */
- static final int match(String text, int pos, String str) {
- for (int i = 0; i < str.length() && pos >= 0;) {
- int ch = UTF16.charAt(str, i);
- i += UTF16.getCharCount(ch);
- if (isBidiMark(ch)) {
- continue;
- }
- pos = match(text, pos, ch);
- if (PatternProps.isWhiteSpace(ch)) {
- i = skipPatternWhiteSpace(str, i);
- }
- }
- return pos;
- }
-
- /**
- * Returns a copy of the decimal format symbols used by this format.
- *
- * @return desired DecimalFormatSymbols
- * @see DecimalFormatSymbols
- */
- public DecimalFormatSymbols getDecimalFormatSymbols() {
- try {
- // don't allow multiple references
- return (DecimalFormatSymbols) symbols.clone();
- } catch (Exception foo) {
- return null; // should never happen
- }
- }
-
- /**
- * Sets the decimal format symbols used by this format. The format uses a copy of the
- * provided symbols.
- *
- * @param newSymbols desired DecimalFormatSymbols
- * @see DecimalFormatSymbols
- */
- public void setDecimalFormatSymbols(DecimalFormatSymbols newSymbols) {
- symbols = (DecimalFormatSymbols) newSymbols.clone();
- setCurrencyForSymbols();
- expandAffixes(null);
- }
-
- /**
- * Update the currency object to match the symbols. This method is used only when the
- * caller has passed in a symbols object that may not be the default object for its
- * locale.
- */
- private void setCurrencyForSymbols() {
-
- // Bug 4212072 Update the affix strings according to symbols in order to keep the
- // affix strings up to date. [Richard/GCL]
-
- // With the introduction of the Currency object, the currency symbols in the DFS
- // object are ignored. For backward compatibility, we check any explicitly set DFS
- // object. If it is a default symbols object for its locale, we change the
- // currency object to one for that locale. If it is custom, we set the currency to
- // null.
- DecimalFormatSymbols def = new DecimalFormatSymbols(symbols.getULocale());
-
- if (symbols.getCurrencySymbol().equals(def.getCurrencySymbol())
- && symbols.getInternationalCurrencySymbol()
- .equals(def.getInternationalCurrencySymbol())) {
- setCurrency(Currency.getInstance(symbols.getULocale()));
- } else {
- setCurrency(null);
- }
- }
-
- /**
- * Returns the positive prefix.
- *
- * <p>Examples: +123, $123, sFr123
- * @return the prefix
- */
- public String getPositivePrefix() {
- return positivePrefix;
- }
-
- /**
- * Sets the positive prefix.
- *
- * <p>Examples: +123, $123, sFr123
- * @param newValue the prefix
- */
- public void setPositivePrefix(String newValue) {
- positivePrefix = newValue;
- posPrefixPattern = null;
- }
-
- /**
- * Returns the negative prefix.
- *
- * <p>Examples: -123, ($123) (with negative suffix), sFr-123
- *
- * @return the prefix
- */
- public String getNegativePrefix() {
- return negativePrefix;
- }
-
- /**
- * Sets the negative prefix.
- *
- * <p>Examples: -123, ($123) (with negative suffix), sFr-123
- * @param newValue the prefix
- */
- public void setNegativePrefix(String newValue) {
- negativePrefix = newValue;
- negPrefixPattern = null;
- }
-
- /**
- * Returns the positive suffix.
- *
- * <p>Example: 123%
- *
- * @return the suffix
- */
- public String getPositiveSuffix() {
- return positiveSuffix;
- }
-
- /**
- * Sets the positive suffix.
- *
- * <p>Example: 123%
- * @param newValue the suffix
- */
- public void setPositiveSuffix(String newValue) {
- positiveSuffix = newValue;
- posSuffixPattern = null;
- }
-
- /**
- * Returns the negative suffix.
- *
- * <p>Examples: -123%, ($123) (with positive suffixes)
- *
- * @return the suffix
- */
- public String getNegativeSuffix() {
- return negativeSuffix;
- }
-
- /**
- * Sets the positive suffix.
- *
- * <p>Examples: 123%
- * @param newValue the suffix
- */
- public void setNegativeSuffix(String newValue) {
- negativeSuffix = newValue;
- negSuffixPattern = null;
- }
-
- /**
- * Returns the multiplier for use in percent, permill, etc. For a percentage, set the
- * suffixes to have "%" and the multiplier to be 100. (For Arabic, use arabic percent
- * symbol). For a permill, set the suffixes to have "\u2031" and the multiplier to be
- * 1000.
- *
- * <p>Examples: with 100, 1.23 -&gt; "123", and "123" -&gt; 1.23
- *
- * @return the multiplier
- */
- public int getMultiplier() {
- return multiplier;
- }
-
- /**
- * Sets the multiplier for use in percent, permill, etc. For a percentage, set the
- * suffixes to have "%" and the multiplier to be 100. (For Arabic, use arabic percent
- * symbol). For a permill, set the suffixes to have "\u2031" and the multiplier to be
- * 1000.
- *
- * <p>Examples: with 100, 1.23 -&gt; "123", and "123" -&gt; 1.23
- *
- * @param newValue the multiplier
- */
- public void setMultiplier(int newValue) {
- if (newValue == 0) {
- throw new IllegalArgumentException("Bad multiplier: " + newValue);
- }
- multiplier = newValue;
- }
-
- /**
- * <strong>[icu]</strong> Returns the rounding increment.
- *
- * @return A positive rounding increment, or <code>null</code> if a custom rounding
- * increment is not in effect.
- * @see #setRoundingIncrement
- * @see #getRoundingMode
- * @see #setRoundingMode
- */
- public java.math.BigDecimal getRoundingIncrement() {
- if (roundingIncrementICU == null)
- return null;
- return roundingIncrementICU.toBigDecimal();
- }
-
- /**
- * <strong>[icu]</strong> Sets the rounding increment. In the absence of a rounding increment, numbers
- * will be rounded to the number of digits displayed.
- *
- * @param newValue A positive rounding increment, or <code>null</code> or
- * <code>BigDecimal(0.0)</code> to use the default rounding increment.
- * @throws IllegalArgumentException if <code>newValue</code> is &lt; 0.0
- * @see #getRoundingIncrement
- * @see #getRoundingMode
- * @see #setRoundingMode
- */
- public void setRoundingIncrement(java.math.BigDecimal newValue) {
- if (newValue == null) {
- setRoundingIncrement((BigDecimal) null);
- } else {
- setRoundingIncrement(new BigDecimal(newValue));
- }
- }
-
- /**
- * <strong>[icu]</strong> Sets the rounding increment. In the absence of a rounding increment, numbers
- * will be rounded to the number of digits displayed.
- *
- * @param newValue A positive rounding increment, or <code>null</code> or
- * <code>BigDecimal(0.0)</code> to use the default rounding increment.
- * @throws IllegalArgumentException if <code>newValue</code> is &lt; 0.0
- * @see #getRoundingIncrement
- * @see #getRoundingMode
- * @see #setRoundingMode
- */
- public void setRoundingIncrement(BigDecimal newValue) {
- int i = newValue == null ? 0 : newValue.compareTo(BigDecimal.ZERO);
- if (i < 0) {
- throw new IllegalArgumentException("Illegal rounding increment");
- }
- if (i == 0) {
- setInternalRoundingIncrement(null);
- } else {
- setInternalRoundingIncrement(newValue);
- }
- resetActualRounding();
- }
-
- /**
- * <strong>[icu]</strong> Sets the rounding increment. In the absence of a rounding increment, numbers
- * will be rounded to the number of digits displayed.
- *
- * @param newValue A positive rounding increment, or 0.0 to use the default
- * rounding increment.
- * @throws IllegalArgumentException if <code>newValue</code> is &lt; 0.0
- * @see #getRoundingIncrement
- * @see #getRoundingMode
- * @see #setRoundingMode
- */
- public void setRoundingIncrement(double newValue) {
- if (newValue < 0.0) {
- throw new IllegalArgumentException("Illegal rounding increment");
- }
- if (newValue == 0.0d) {
- setInternalRoundingIncrement((BigDecimal) null);
- } else {
- // Should use BigDecimal#valueOf(double) instead of constructor
- // to avoid the double precision problem.
- setInternalRoundingIncrement(BigDecimal.valueOf(newValue));
- }
- resetActualRounding();
- }
-
- /**
- * Returns the rounding mode.
- *
- * @return A rounding mode, between <code>BigDecimal.ROUND_UP</code> and
- * <code>BigDecimal.ROUND_UNNECESSARY</code>.
- * @see #setRoundingIncrement
- * @see #getRoundingIncrement
- * @see #setRoundingMode
- * @see java.math.BigDecimal
- */
- @Override
- public int getRoundingMode() {
- return roundingMode;
- }
-
- /**
- * Sets the rounding mode. This has no effect unless the rounding increment is greater
- * than zero.
- *
- * @param roundingMode A rounding mode, between <code>BigDecimal.ROUND_UP</code> and
- * <code>BigDecimal.ROUND_UNNECESSARY</code>.
- * @exception IllegalArgumentException if <code>roundingMode</code> is unrecognized.
- * @see #setRoundingIncrement
- * @see #getRoundingIncrement
- * @see #getRoundingMode
- * @see java.math.BigDecimal
- */
- @Override
- public void setRoundingMode(int roundingMode) {
- if (roundingMode < BigDecimal.ROUND_UP || roundingMode > BigDecimal.ROUND_UNNECESSARY) {
- throw new IllegalArgumentException("Invalid rounding mode: " + roundingMode);
- }
-
- this.roundingMode = roundingMode;
- resetActualRounding();
- }
-
- /**
- * Returns the width to which the output of <code>format()</code> is padded. The width is
- * counted in 16-bit code units.
- *
- * @return the format width, or zero if no padding is in effect
- * @see #setFormatWidth
- * @see #getPadCharacter
- * @see #setPadCharacter
- * @see #getPadPosition
- * @see #setPadPosition
- */
- public int getFormatWidth() {
- return formatWidth;
- }
-
- /**
- * Sets the width to which the output of <code>format()</code> is
- * padded. The width is counted in 16-bit code units. This method
- * also controls whether padding is enabled.
- *
- * @param width the width to which to pad the result of
- * <code>format()</code>, or zero to disable padding
- * @exception IllegalArgumentException if <code>width</code> is &lt; 0
- * @see #getFormatWidth
- * @see #getPadCharacter
- * @see #setPadCharacter
- * @see #getPadPosition
- * @see #setPadPosition
*/
- public void setFormatWidth(int width) {
- if (width < 0) {
- throw new IllegalArgumentException("Illegal format width");
- }
- formatWidth = width;
- }
-
- /**
- * <strong>[icu]</strong> Returns the character used to pad to the format width. The default is ' '.
- *
- * @return the pad character
- * @see #setFormatWidth
- * @see #getFormatWidth
- * @see #setPadCharacter
- * @see #getPadPosition
- * @see #setPadPosition
- */
- public char getPadCharacter() {
- return pad;
- }
-
- /**
- * <strong>[icu]</strong> Sets the character used to pad to the format width. If padding is not
- * enabled, then this will take effect if padding is later enabled.
- *
- * @param padChar the pad character
- * @see #setFormatWidth
- * @see #getFormatWidth
- * @see #getPadCharacter
- * @see #getPadPosition
- * @see #setPadPosition
- */
- public void setPadCharacter(char padChar) {
- pad = padChar;
- }
-
- /**
- * <strong>[icu]</strong> Returns the position at which padding will take place. This is the location at
- * which padding will be inserted if the result of <code>format()</code> is shorter
- * than the format width.
- *
- * @return the pad position, one of <code>PAD_BEFORE_PREFIX</code>,
- * <code>PAD_AFTER_PREFIX</code>, <code>PAD_BEFORE_SUFFIX</code>, or
- * <code>PAD_AFTER_SUFFIX</code>.
- * @see #setFormatWidth
- * @see #getFormatWidth
- * @see #setPadCharacter
- * @see #getPadCharacter
- * @see #setPadPosition
- * @see #PAD_BEFORE_PREFIX
- * @see #PAD_AFTER_PREFIX
- * @see #PAD_BEFORE_SUFFIX
- * @see #PAD_AFTER_SUFFIX
- */
- public int getPadPosition() {
- return padPosition;
- }
-
- /**
- * <strong>[icu]</strong> Sets the position at which padding will take place. This is the location at
- * which padding will be inserted if the result of <code>format()</code> is shorter
- * than the format width. This has no effect unless padding is enabled.
- *
- * @param padPos the pad position, one of <code>PAD_BEFORE_PREFIX</code>,
- * <code>PAD_AFTER_PREFIX</code>, <code>PAD_BEFORE_SUFFIX</code>, or
- * <code>PAD_AFTER_SUFFIX</code>.
- * @exception IllegalArgumentException if the pad position in unrecognized
- * @see #setFormatWidth
- * @see #getFormatWidth
- * @see #setPadCharacter
- * @see #getPadCharacter
- * @see #getPadPosition
- * @see #PAD_BEFORE_PREFIX
- * @see #PAD_AFTER_PREFIX
- * @see #PAD_BEFORE_SUFFIX
- * @see #PAD_AFTER_SUFFIX
- */
- public void setPadPosition(int padPos) {
- if (padPos < PAD_BEFORE_PREFIX || padPos > PAD_AFTER_SUFFIX) {
- throw new IllegalArgumentException("Illegal pad position");
- }
- padPosition = padPos;
- }
-
- /**
- * <strong>[icu]</strong> Returns whether or not scientific notation is used.
- *
- * @return true if this object formats and parses scientific notation
- * @see #setScientificNotation
- * @see #getMinimumExponentDigits
- * @see #setMinimumExponentDigits
- * @see #isExponentSignAlwaysShown
- * @see #setExponentSignAlwaysShown
- */
- public boolean isScientificNotation() {
- return useExponentialNotation;
- }
-
- /**
- * <strong>[icu]</strong> Sets whether or not scientific notation is used. When scientific notation is
- * used, the effective maximum number of integer digits is &lt;= 8. If the maximum number
- * of integer digits is set to more than 8, the effective maximum will be 1. This
- * allows this call to generate a 'default' scientific number format without
- * additional changes.
- *
- * @param useScientific true if this object formats and parses scientific notation
- * @see #isScientificNotation
- * @see #getMinimumExponentDigits
- * @see #setMinimumExponentDigits
- * @see #isExponentSignAlwaysShown
- * @see #setExponentSignAlwaysShown
- */
- public void setScientificNotation(boolean useScientific) {
- useExponentialNotation = useScientific;
- }
-
- /**
- * <strong>[icu]</strong> Returns the minimum exponent digits that will be shown.
- *
- * @return the minimum exponent digits that will be shown
- * @see #setScientificNotation
- * @see #isScientificNotation
- * @see #setMinimumExponentDigits
- * @see #isExponentSignAlwaysShown
- * @see #setExponentSignAlwaysShown
- */
- public byte getMinimumExponentDigits() {
- return minExponentDigits;
- }
-
- /**
- * <strong>[icu]</strong> Sets the minimum exponent digits that will be shown. This has no effect
- * unless scientific notation is in use.
- *
- * @param minExpDig a value &gt;= 1 indicating the fewest exponent
- * digits that will be shown
- * @exception IllegalArgumentException if <code>minExpDig</code> &lt; 1
- * @see #setScientificNotation
- * @see #isScientificNotation
- * @see #getMinimumExponentDigits
- * @see #isExponentSignAlwaysShown
- * @see #setExponentSignAlwaysShown
- */
- public void setMinimumExponentDigits(byte minExpDig) {
- if (minExpDig < 1) {
- throw new IllegalArgumentException("Exponent digits must be >= 1");
- }
- minExponentDigits = minExpDig;
- }
-
- /**
- * <strong>[icu]</strong> Returns whether the exponent sign is always shown.
- *
- * @return true if the exponent is always prefixed with either the localized minus
- * sign or the localized plus sign, false if only negative exponents are prefixed with
- * the localized minus sign.
- * @see #setScientificNotation
- * @see #isScientificNotation
- * @see #setMinimumExponentDigits
- * @see #getMinimumExponentDigits
- * @see #setExponentSignAlwaysShown
- */
- public boolean isExponentSignAlwaysShown() {
- return exponentSignAlwaysShown;
- }
-
- /**
- * <strong>[icu]</strong> Sets whether the exponent sign is always shown. This has no effect unless
- * scientific notation is in use.
- *
- * @param expSignAlways true if the exponent is always prefixed with either the
- * localized minus sign or the localized plus sign, false if only negative exponents
- * are prefixed with the localized minus sign.
- * @see #setScientificNotation
- * @see #isScientificNotation
- * @see #setMinimumExponentDigits
- * @see #getMinimumExponentDigits
- * @see #isExponentSignAlwaysShown
- */
- public void setExponentSignAlwaysShown(boolean expSignAlways) {
- exponentSignAlwaysShown = expSignAlways;
- }
-
- /**
- * Returns the grouping size. Grouping size is the number of digits between grouping
- * separators in the integer portion of a number. For example, in the number
- * "123,456.78", the grouping size is 3.
- *
- * @see #setGroupingSize
- * @see NumberFormat#isGroupingUsed
- * @see DecimalFormatSymbols#getGroupingSeparator
- */
- public int getGroupingSize() {
- return groupingSize;
- }
-
- /**
- * Sets the grouping size. Grouping size is the number of digits between grouping
- * separators in the integer portion of a number. For example, in the number
- * "123,456.78", the grouping size is 3.
- *
- * @see #getGroupingSize
- * @see NumberFormat#setGroupingUsed
- * @see DecimalFormatSymbols#setGroupingSeparator
- */
- public void setGroupingSize(int newValue) {
- groupingSize = (byte) newValue;
- }
-
- /**
- * <strong>[icu]</strong> Returns the secondary grouping size. In some locales one grouping interval
- * is used for the least significant integer digits (the primary grouping size), and
- * another is used for all others (the secondary grouping size). A formatter
- * supporting a secondary grouping size will return a positive integer unequal to the
- * primary grouping size returned by <code>getGroupingSize()</code>. For example, if
- * the primary grouping size is 4, and the secondary grouping size is 2, then the
- * number 123456789 formats as "1,23,45,6789", and the pattern appears as "#,##,###0".
- *
- * @return the secondary grouping size, or a value less than one if there is none
- * @see #setSecondaryGroupingSize
- * @see NumberFormat#isGroupingUsed
- * @see DecimalFormatSymbols#getGroupingSeparator
- */
- public int getSecondaryGroupingSize() {
- return groupingSize2;
- }
-
- /**
- * <strong>[icu]</strong> Sets the secondary grouping size. If set to a value less than 1, then
- * secondary grouping is turned off, and the primary grouping size is used for all
- * intervals, not just the least significant.
- *
- * @see #getSecondaryGroupingSize
- * @see NumberFormat#setGroupingUsed
- * @see DecimalFormatSymbols#setGroupingSeparator
- */
- public void setSecondaryGroupingSize(int newValue) {
- groupingSize2 = (byte) newValue;
- }
-
- /**
- * <strong>[icu]</strong> Returns the MathContext used by this format.
- *
- * @return desired MathContext
- * @see #getMathContext
- */
- public MathContext getMathContextICU() {
- return mathContext;
- }
-
- /**
- * <strong>[icu]</strong> Returns the MathContext used by this format.
- *
- * @return desired MathContext
- * @see #getMathContext
- */
- public java.math.MathContext getMathContext() {
- try {
- // don't allow multiple references
- return mathContext == null ? null : new java.math.MathContext(mathContext.getDigits(),
- java.math.RoundingMode.valueOf(mathContext.getRoundingMode()));
- } catch (Exception foo) {
- return null; // should never happen
- }
- }
-
- /**
- * <strong>[icu]</strong> Sets the MathContext used by this format.
- *
- * @param newValue desired MathContext
- * @see #getMathContext
- */
- public void setMathContextICU(MathContext newValue) {
- mathContext = newValue;
- }
-
- /**
- * <strong>[icu]</strong> Sets the MathContext used by this format.
- *
- * @param newValue desired MathContext
- * @see #getMathContext
- */
- public void setMathContext(java.math.MathContext newValue) {
- mathContext = new MathContext(newValue.getPrecision(), MathContext.SCIENTIFIC, false,
- (newValue.getRoundingMode()).ordinal());
- }
-
- /**
- * Returns the behavior of the decimal separator with integers. (The decimal
- * separator will always appear with decimals.) <p> Example: Decimal ON: 12345 -&gt;
- * 12345.; OFF: 12345 -&gt; 12345
- */
- public boolean isDecimalSeparatorAlwaysShown() {
- return decimalSeparatorAlwaysShown;
- }
-
- /**
- * When decimal match is not required, the input does not have to
- * contain a decimal mark when there is a decimal mark specified in the
- * pattern.
- * @param value true if input must contain a match to decimal mark in pattern
- * Default is false.
- */
- public void setDecimalPatternMatchRequired(boolean value) {
- parseRequireDecimalPoint = value;
- }
-
- /**
- * <strong>[icu]</strong> Returns whether the input to parsing must contain a decimal mark if there
- * is a decimal mark in the pattern.
- * @return true if input must contain a match to decimal mark in pattern
- */
- public boolean isDecimalPatternMatchRequired() {
- return parseRequireDecimalPoint;
- }
-
-
- /**
- * Sets the behavior of the decimal separator with integers. (The decimal separator
- * will always appear with decimals.)
- *
- * <p>This only affects formatting, and only where there might be no digits after the
- * decimal point, e.g., if true, 3456.00 -&gt; "3,456." if false, 3456.00 -&gt; "3456" This
- * is independent of parsing. If you want parsing to stop at the decimal point, use
- * setParseIntegerOnly.
- *
- * <p>
- * Example: Decimal ON: 12345 -&gt; 12345.; OFF: 12345 -&gt; 12345
- */
- public void setDecimalSeparatorAlwaysShown(boolean newValue) {
- decimalSeparatorAlwaysShown = newValue;
- }
-
- /**
- * <strong>[icu]</strong> Returns a copy of the CurrencyPluralInfo used by this format. It might
- * return null if the decimal format is not a plural type currency decimal
- * format. Plural type currency decimal format means either the pattern in the decimal
- * format contains 3 currency signs, or the decimal format is initialized with
- * PLURALCURRENCYSTYLE.
- *
- * @return desired CurrencyPluralInfo
- * @see CurrencyPluralInfo
- */
- public CurrencyPluralInfo getCurrencyPluralInfo() {
- try {
- // don't allow multiple references
- return currencyPluralInfo == null ? null :
- (CurrencyPluralInfo) currencyPluralInfo.clone();
- } catch (Exception foo) {
- return null; // should never happen
- }
- }
-
- /**
- * <strong>[icu]</strong> Sets the CurrencyPluralInfo used by this format. The format uses a copy of
- * the provided information.
- *
- * @param newInfo desired CurrencyPluralInfo
- * @see CurrencyPluralInfo
- */
- public void setCurrencyPluralInfo(CurrencyPluralInfo newInfo) {
- currencyPluralInfo = (CurrencyPluralInfo) newInfo.clone();
- isReadyForParsing = false;
- }
-
- /**
- * Overrides clone.
- */
- @Override
- public Object clone() {
- try {
- DecimalFormat other = (DecimalFormat) super.clone();
- other.symbols = (DecimalFormatSymbols) symbols.clone();
- other.digitList = new DigitList(); // fix for JB#5358
- if (currencyPluralInfo != null) {
- other.currencyPluralInfo = (CurrencyPluralInfo) currencyPluralInfo.clone();
- }
- other.attributes = new ArrayList<FieldPosition>(); // #9240
- other.currencyUsage = currencyUsage;
-
- // TODO: We need to figure out whether we share a single copy of DigitList by
- // multiple cloned copies. format/subformat are designed to use a single
- // instance, but parse/subparse implementation is not.
- return other;
- } catch (Exception e) {
- throw new IllegalStateException();
- }
- }
-
- /**
- * Overrides equals.
- */
- @Override
- public boolean equals(Object obj) {
- if (obj == null)
- return false;
- if (!super.equals(obj))
- return false; // super does class check
-
- DecimalFormat other = (DecimalFormat) obj;
- // Add the comparison of the four new added fields ,they are posPrefixPattern,
- // posSuffixPattern, negPrefixPattern, negSuffixPattern. [Richard/GCL]
- // following are added to accomodate changes for currency plural format.
- return currencySignCount == other.currencySignCount
- && (style != NumberFormat.PLURALCURRENCYSTYLE ||
- equals(posPrefixPattern, other.posPrefixPattern)
- && equals(posSuffixPattern, other.posSuffixPattern)
- && equals(negPrefixPattern, other.negPrefixPattern)
- && equals(negSuffixPattern, other.negSuffixPattern))
- && multiplier == other.multiplier
- && groupingSize == other.groupingSize
- && groupingSize2 == other.groupingSize2
- && decimalSeparatorAlwaysShown == other.decimalSeparatorAlwaysShown
- && useExponentialNotation == other.useExponentialNotation
- && (!useExponentialNotation || minExponentDigits == other.minExponentDigits)
- && useSignificantDigits == other.useSignificantDigits
- && (!useSignificantDigits || minSignificantDigits == other.minSignificantDigits
- && maxSignificantDigits == other.maxSignificantDigits)
- && symbols.equals(other.symbols)
- && Utility.objectEquals(currencyPluralInfo, other.currencyPluralInfo)
- && currencyUsage.equals(other.currencyUsage);
- }
-
- // method to unquote the strings and compare
- private boolean equals(String pat1, String pat2) {
- if (pat1 == null || pat2 == null) {
- return (pat1 == null && pat2 == null);
- }
- // fast path
- if (pat1.equals(pat2)) {
- return true;
- }
- return unquote(pat1).equals(unquote(pat2));
- }
-
- private String unquote(String pat) {
- StringBuilder buf = new StringBuilder(pat.length());
- int i = 0;
- while (i < pat.length()) {
- char ch = pat.charAt(i++);
- if (ch != QUOTE) {
- buf.append(ch);
- }
- }
- return buf.toString();
- }
-
- // protected void handleToString(StringBuffer buf) {
- // buf.append("\nposPrefixPattern: '" + posPrefixPattern + "'\n");
- // buf.append("positivePrefix: '" + positivePrefix + "'\n");
- // buf.append("posSuffixPattern: '" + posSuffixPattern + "'\n");
- // buf.append("positiveSuffix: '" + positiveSuffix + "'\n");
- // buf.append("negPrefixPattern: '" +
- // android.icu.impl.Utility.format1ForSource(negPrefixPattern) + "'\n");
- // buf.append("negativePrefix: '" +
- // android.icu.impl.Utility.format1ForSource(negativePrefix) + "'\n");
- // buf.append("negSuffixPattern: '" + negSuffixPattern + "'\n");
- // buf.append("negativeSuffix: '" + negativeSuffix + "'\n");
- // buf.append("multiplier: '" + multiplier + "'\n");
- // buf.append("groupingSize: '" + groupingSize + "'\n");
- // buf.append("groupingSize2: '" + groupingSize2 + "'\n");
- // buf.append("decimalSeparatorAlwaysShown: '" + decimalSeparatorAlwaysShown + "'\n");
- // buf.append("useExponentialNotation: '" + useExponentialNotation + "'\n");
- // buf.append("minExponentDigits: '" + minExponentDigits + "'\n");
- // buf.append("useSignificantDigits: '" + useSignificantDigits + "'\n");
- // buf.append("minSignificantDigits: '" + minSignificantDigits + "'\n");
- // buf.append("maxSignificantDigits: '" + maxSignificantDigits + "'\n");
- // buf.append("symbols: '" + symbols + "'");
- // }
-
- /**
- * Overrides hashCode.
- */
- @Override
- public int hashCode() {
- return super.hashCode() * 37 + positivePrefix.hashCode();
- // just enough fields for a reasonable distribution
- }
-
- /**
- * Synthesizes a pattern string that represents the current state of this Format
- * object.
- *
- * @see #applyPattern
- */
- public String toPattern() {
- if (style == NumberFormat.PLURALCURRENCYSTYLE) {
- // the prefix or suffix pattern might not be defined yet, so they can not be
- // synthesized, instead, get them directly. but it might not be the actual
- // pattern used in formatting. the actual pattern used in formatting depends
- // on the formatted number's plural count.
- return formatPattern;
- }
- return toPattern(false);
- }
-
- /**
- * Synthesizes a localized pattern string that represents the current state of this
- * Format object.
- *
- * @see #applyPattern
- */
- public String toLocalizedPattern() {
- if (style == NumberFormat.PLURALCURRENCYSTYLE) {
- return formatPattern;
- }
- return toPattern(true);
- }
-
- /**
- * Expands the affix pattern strings into the expanded affix strings. If any affix
- * pattern string is null, do not expand it. This method should be called any time the
- * symbols or the affix patterns change in order to keep the expanded affix strings up
- * to date. This method also will be called before formatting if format currency
- * plural names, since the plural name is not a static one, it is based on the
- * currency plural count, the affix will be known only after the currency plural count
- * is know. In which case, the parameter 'pluralCount' will be a non-null currency
- * plural count. In all other cases, the 'pluralCount' is null, which means it is not
- * needed.
- */
- // Bug 4212072 [Richard/GCL]
- private void expandAffixes(String pluralCount) {
- // expandAffix() will set currencyChoice to a non-null value if
- // appropriate AND if it is null.
- currencyChoice = null;
-
- // Reuse one StringBuffer for better performance
- StringBuffer buffer = new StringBuffer();
- if (posPrefixPattern != null) {
- expandAffix(posPrefixPattern, pluralCount, buffer);
- positivePrefix = buffer.toString();
- }
- if (posSuffixPattern != null) {
- expandAffix(posSuffixPattern, pluralCount, buffer);
- positiveSuffix = buffer.toString();
- }
- if (negPrefixPattern != null) {
- expandAffix(negPrefixPattern, pluralCount, buffer);
- negativePrefix = buffer.toString();
- }
- if (negSuffixPattern != null) {
- expandAffix(negSuffixPattern, pluralCount, buffer);
- negativeSuffix = buffer.toString();
- }
- }
-
- /**
- * Expands an affix pattern into an affix string. All characters in the pattern are
- * literal unless bracketed by QUOTEs. The following characters outside QUOTE are
- * recognized: PATTERN_PERCENT, PATTERN_PER_MILLE, PATTERN_MINUS, and
- * CURRENCY_SIGN. If CURRENCY_SIGN is doubled, it is interpreted as an international
- * currency sign. If CURRENCY_SIGN is tripled, it is interpreted as currency plural
- * long names, such as "US Dollars". Any other character outside QUOTE represents
- * itself. Quoted text must be well-formed.
- *
- * This method is used in two distinct ways. First, it is used to expand the stored
- * affix patterns into actual affixes. For this usage, doFormat must be false. Second,
- * it is used to expand the stored affix patterns given a specific number (doFormat ==
- * true), for those rare cases in which a currency format references a ChoiceFormat
- * (e.g., en_IN display name for INR). The number itself is taken from digitList.
- * TODO: There are no currency ChoiceFormat patterns, figure out what is still relevant here.
- *
- * When used in the first way, this method has a side effect: It sets currencyChoice
- * to a ChoiceFormat object, if the currency's display name in this locale is a
- * ChoiceFormat pattern (very rare). It only does this if currencyChoice is null to
- * start with.
- *
- * @param pattern the non-null, possibly empty pattern
- * @param pluralCount the plural count. It is only used for currency plural format. In
- * which case, it is the plural count of the currency amount. For example, in en_US,
- * it is the singular "one", or the plural "other". For all other cases, it is null,
- * and is not being used.
- * @param buffer a scratch StringBuffer; its contents will be lost
- */
- // Bug 4212072 [Richard/GCL]
- private void expandAffix(String pattern, String pluralCount, StringBuffer buffer) {
- buffer.setLength(0);
- for (int i = 0; i < pattern.length();) {
- char c = pattern.charAt(i++);
- if (c == QUOTE) {
- for (;;) {
- int j = pattern.indexOf(QUOTE, i);
- if (j == i) {
- buffer.append(QUOTE);
- i = j + 1;
- break;
- } else if (j > i) {
- buffer.append(pattern.substring(i, j));
- i = j + 1;
- if (i < pattern.length() && pattern.charAt(i) == QUOTE) {
- buffer.append(QUOTE);
- ++i;
- // loop again
- } else {
- break;
- }
- } else {
- // Unterminated quote; should be caught by apply
- // pattern.
- throw new RuntimeException();
- }
- }
- continue;
- }
-
- switch (c) {
- case CURRENCY_SIGN:
- // As of ICU 2.2 we use the currency object, and ignore the currency
- // symbols in the DFS, unless we have a null currency object. This occurs
- // if resurrecting a pre-2.2 object or if the user sets a custom DFS.
- boolean intl = i < pattern.length() && pattern.charAt(i) == CURRENCY_SIGN;
- boolean plural = false;
- if (intl) {
- ++i;
- if (i < pattern.length() && pattern.charAt(i) == CURRENCY_SIGN) {
- plural = true;
- intl = false;
- ++i;
- }
- }
- String s = null;
- Currency currency = getCurrency();
- if (currency != null) {
- // plural name is only needed when pluralCount != null, which means
- // when formatting currency plural names. For other cases,
- // pluralCount == null, and plural names are not needed.
- if (plural && pluralCount != null) {
- s = currency.getName(symbols.getULocale(), Currency.PLURAL_LONG_NAME,
- pluralCount, null);
- } else if (!intl) {
- s = currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
- } else {
- s = currency.getCurrencyCode();
- }
- } else {
- s = intl ? symbols.getInternationalCurrencySymbol() :
- symbols.getCurrencySymbol();
- }
- // Here is where FieldPosition could be set for CURRENCY PLURAL.
- buffer.append(s);
- break;
- case PATTERN_PERCENT:
- buffer.append(symbols.getPercentString());
- break;
- case PATTERN_PER_MILLE:
- buffer.append(symbols.getPerMillString());
- break;
- case PATTERN_MINUS_SIGN:
- buffer.append(symbols.getMinusSignString());
- break;
- default:
- buffer.append(c);
- break;
- }
- }
- }
-
- /**
- * Append an affix to the given StringBuffer.
- *
- * @param buf
- * buffer to append to
- * @param isNegative
- * @param isPrefix
- * @param fieldPosition
- * @param parseAttr
- */
- private int appendAffix(StringBuffer buf, boolean isNegative, boolean isPrefix,
- FieldPosition fieldPosition,
- boolean parseAttr) {
- if (currencyChoice != null) {
- String affixPat = null;
- if (isPrefix) {
- affixPat = isNegative ? negPrefixPattern : posPrefixPattern;
- } else {
- affixPat = isNegative ? negSuffixPattern : posSuffixPattern;
- }
- StringBuffer affixBuf = new StringBuffer();
- expandAffix(affixPat, null, affixBuf);
- buf.append(affixBuf);
- return affixBuf.length();
- }
-
- String affix = null;
- String pattern;
- if (isPrefix) {
- affix = isNegative ? negativePrefix : positivePrefix;
- pattern = isNegative ? negPrefixPattern : posPrefixPattern;
- } else {
- affix = isNegative ? negativeSuffix : positiveSuffix;
- pattern = isNegative ? negSuffixPattern : posSuffixPattern;
- }
- // [Spark/CDL] Invoke formatAffix2Attribute to add attributes for affix
- if (parseAttr) {
- // Updates for Ticket 11805.
- int offset = affix.indexOf(symbols.getCurrencySymbol());
- if (offset > -1) {
- formatAffix2Attribute(isPrefix, Field.CURRENCY, buf, offset,
- symbols.getCurrencySymbol().length());
- }
- offset = affix.indexOf(symbols.getMinusSignString());
- if (offset > -1) {
- formatAffix2Attribute(isPrefix, Field.SIGN, buf, offset,
- symbols.getMinusSignString().length());
- }
- offset = affix.indexOf(symbols.getPercentString());
- if (offset > -1) {
- formatAffix2Attribute(isPrefix, Field.PERCENT, buf, offset,
- symbols.getPercentString().length());
- }
- offset = affix.indexOf(symbols.getPerMillString());
- if (offset > -1) {
- formatAffix2Attribute(isPrefix, Field.PERMILLE, buf, offset,
- symbols.getPerMillString().length());
- }
- offset = pattern.indexOf("¤¤¤");
- if (offset > -1) {
- formatAffix2Attribute(isPrefix, Field.CURRENCY, buf, offset,
- affix.length() - offset);
- }
- }
-
- // Look for SIGN, PERCENT, PERMILLE in the formatted affix.
- if (fieldPosition.getFieldAttribute() == NumberFormat.Field.SIGN) {
- String sign = isNegative ? symbols.getMinusSignString() : symbols.getPlusSignString();
- int firstPos = affix.indexOf(sign);
- if (firstPos > -1) {
- int startPos = buf.length() + firstPos;
- fieldPosition.setBeginIndex(startPos);
- fieldPosition.setEndIndex(startPos + sign.length());
- }
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.PERCENT) {
- int firstPos = affix.indexOf(symbols.getPercentString());
- if (firstPos > -1) {
- int startPos = buf.length() + firstPos;
- fieldPosition.setBeginIndex(startPos);
- fieldPosition.setEndIndex(startPos + symbols.getPercentString().length());
- }
- } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.PERMILLE) {
- int firstPos = affix.indexOf(symbols.getPerMillString());
- if (firstPos > -1) {
- int startPos = buf.length() + firstPos;
- fieldPosition.setBeginIndex(startPos);
- fieldPosition.setEndIndex(startPos + symbols.getPerMillString().length());
- }
- } else
- // If CurrencySymbol or InternationalCurrencySymbol is in the affix, check for currency symbol.
- // Get spelled out name if "¤¤¤" is in the pattern.
- if (fieldPosition.getFieldAttribute() == NumberFormat.Field.CURRENCY) {
- if (affix.indexOf(symbols.getCurrencySymbol()) > -1) {
- String aff = symbols.getCurrencySymbol();
- int firstPos = affix.indexOf(aff);
- int start = buf.length() + firstPos;
- int end = start + aff.length();
- fieldPosition.setBeginIndex(start);
- fieldPosition.setEndIndex(end);
- } else if (affix.indexOf(symbols.getInternationalCurrencySymbol()) > -1) {
- String aff = symbols.getInternationalCurrencySymbol();
- int firstPos = affix.indexOf(aff);
- int start = buf.length() + firstPos;
- int end = start + aff.length();
- fieldPosition.setBeginIndex(start);
- fieldPosition.setEndIndex(end);
- } else if (pattern.indexOf("¤¤¤") > -1) {
- // It's a plural, and we know where it is in the pattern.
- int firstPos = pattern.indexOf("¤¤¤");
- int start = buf.length() + firstPos;
- int end = buf.length() + affix.length(); // This seems clunky and wrong.
- fieldPosition.setBeginIndex(start);
- fieldPosition.setEndIndex(end);
- }
- }
-
- buf.append(affix);
- return affix.length();
- }
-
- // Fix for prefix and suffix in Ticket 11805.
- private void formatAffix2Attribute(boolean isPrefix, Field fieldType,
- StringBuffer buf, int offset, int symbolSize) {
- int begin;
- begin = offset;
- if (!isPrefix) {
- begin += buf.length();
- }
-
- addAttribute(fieldType, begin, begin + symbolSize);
- }
-
- /**
- * [Spark/CDL] Use this method to add attribute.
- */
- private void addAttribute(Field field, int begin, int end) {
- FieldPosition pos = new FieldPosition(field);
- pos.setBeginIndex(begin);
- pos.setEndIndex(end);
- attributes.add(pos);
- }
-
- /**
- * Formats the object to an attributed string, and return the corresponding iterator.
- */
- @Override
- public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
- return formatToCharacterIterator(obj, NULL_UNIT);
- }
-
- AttributedCharacterIterator formatToCharacterIterator(Object obj, Unit unit) {
- if (!(obj instanceof Number))
- throw new IllegalArgumentException();
- Number number = (Number) obj;
- StringBuffer text = new StringBuffer();
- unit.writePrefix(text);
- attributes.clear();
- if (obj instanceof BigInteger) {
- format((BigInteger) number, text, new FieldPosition(0), true);
- } else if (obj instanceof java.math.BigDecimal) {
- format((java.math.BigDecimal) number, text, new FieldPosition(0)
- , true);
- } else if (obj instanceof Double) {
- format(number.doubleValue(), text, new FieldPosition(0), true);
- } else if (obj instanceof Integer || obj instanceof Long) {
- format(number.longValue(), text, new FieldPosition(0), true);
- } else {
- throw new IllegalArgumentException();
- }
- unit.writeSuffix(text);
- AttributedString as = new AttributedString(text.toString());
-
- // add NumberFormat field attributes to the AttributedString
- for (int i = 0; i < attributes.size(); i++) {
- FieldPosition pos = attributes.get(i);
- Format.Field attribute = pos.getFieldAttribute();
- as.addAttribute(attribute, attribute, pos.getBeginIndex(), pos.getEndIndex());
- }
-
- // return the CharacterIterator from AttributedString
- return as.getIterator();
- }
-
- /**
- * Appends an affix pattern to the given StringBuffer. Localize unquoted specials.
- * <p>
- * <b>Note:</b> This implementation does not support new String localized symbols.
- */
- private void appendAffixPattern(StringBuffer buffer, boolean isNegative, boolean isPrefix,
- boolean localized) {
- String affixPat = null;
- if (isPrefix) {
- affixPat = isNegative ? negPrefixPattern : posPrefixPattern;
- } else {
- affixPat = isNegative ? negSuffixPattern : posSuffixPattern;
- }
-
- // When there is a null affix pattern, we use the affix itself.
- if (affixPat == null) {
- String affix = null;
- if (isPrefix) {
- affix = isNegative ? negativePrefix : positivePrefix;
- } else {
- affix = isNegative ? negativeSuffix : positiveSuffix;
- }
- // Do this crudely for now: Wrap everything in quotes.
- buffer.append(QUOTE);
- for (int i = 0; i < affix.length(); ++i) {
- char ch = affix.charAt(i);
- if (ch == QUOTE) {
- buffer.append(ch);
- }
- buffer.append(ch);
- }
- buffer.append(QUOTE);
- return;
- }
-
- if (!localized) {
- buffer.append(affixPat);
- } else {
- int i, j;
- for (i = 0; i < affixPat.length(); ++i) {
- char ch = affixPat.charAt(i);
- switch (ch) {
- case QUOTE:
- j = affixPat.indexOf(QUOTE, i + 1);
- if (j < 0) {
- throw new IllegalArgumentException("Malformed affix pattern: " + affixPat);
- }
- buffer.append(affixPat.substring(i, j + 1));
- i = j;
- continue;
- case PATTERN_PER_MILLE:
- ch = symbols.getPerMill();
- break;
- case PATTERN_PERCENT:
- ch = symbols.getPercent();
- break;
- case PATTERN_MINUS_SIGN:
- ch = symbols.getMinusSign();
- break;
- }
- // check if char is same as any other symbol
- if (ch == symbols.getDecimalSeparator() || ch == symbols.getGroupingSeparator()) {
- buffer.append(QUOTE);
- buffer.append(ch);
- buffer.append(QUOTE);
- } else {
- buffer.append(ch);
- }
- }
- }
- }
-
- /**
- * Does the real work of generating a pattern.
- * <p>
- * <b>Note:</b> This implementation does not support new String localized symbols.
- */
- private String toPattern(boolean localized) {
- StringBuffer result = new StringBuffer();
- char zero = localized ? symbols.getZeroDigit() : PATTERN_ZERO_DIGIT;
- char digit = localized ? symbols.getDigit() : PATTERN_DIGIT;
- char sigDigit = 0;
- boolean useSigDig = areSignificantDigitsUsed();
- if (useSigDig) {
- sigDigit = localized ? symbols.getSignificantDigit() : PATTERN_SIGNIFICANT_DIGIT;
- }
- char group = localized ? symbols.getGroupingSeparator() : PATTERN_GROUPING_SEPARATOR;
- int i;
- int roundingDecimalPos = 0; // Pos of decimal in roundingDigits
- String roundingDigits = null;
- int padPos = (formatWidth > 0) ? padPosition : -1;
- String padSpec = (formatWidth > 0)
- ? new StringBuffer(2).append(localized
- ? symbols.getPadEscape()
- : PATTERN_PAD_ESCAPE).append(pad).toString()
- : null;
- if (roundingIncrementICU != null) {
- i = roundingIncrementICU.scale();
- roundingDigits = roundingIncrementICU.movePointRight(i).toString();
- roundingDecimalPos = roundingDigits.length() - i;
- }
- for (int part = 0; part < 2; ++part) {
- // variable not used int partStart = result.length();
- if (padPos == PAD_BEFORE_PREFIX) {
- result.append(padSpec);
- }
-
- // Use original symbols read from resources in pattern eg. use "\u00A4"
- // instead of "$" in Locale.US [Richard/GCL]
- appendAffixPattern(result, part != 0, true, localized);
- if (padPos == PAD_AFTER_PREFIX) {
- result.append(padSpec);
- }
- int sub0Start = result.length();
- int g = isGroupingUsed() ? Math.max(0, groupingSize) : 0;
- if (g > 0 && groupingSize2 > 0 && groupingSize2 != groupingSize) {
- g += groupingSize2;
- }
- int maxDig = 0, minDig = 0, maxSigDig = 0;
- if (useSigDig) {
- minDig = getMinimumSignificantDigits();
- maxDig = maxSigDig = getMaximumSignificantDigits();
- } else {
- minDig = getMinimumIntegerDigits();
- maxDig = getMaximumIntegerDigits();
- }
- if (useExponentialNotation) {
- if (maxDig > MAX_SCIENTIFIC_INTEGER_DIGITS) {
- maxDig = 1;
- }
- } else if (useSigDig) {
- maxDig = Math.max(maxDig, g + 1);
- } else {
- maxDig = Math.max(Math.max(g, getMinimumIntegerDigits()), roundingDecimalPos) + 1;
- }
- for (i = maxDig; i > 0; --i) {
- if (!useExponentialNotation && i < maxDig && isGroupingPosition(i)) {
- result.append(group);
- }
- if (useSigDig) {
- // #@,@### (maxSigDig == 5, minSigDig == 2) 65 4321 (1-based pos,
- // count from the right) Use # if pos > maxSigDig or 1 <= pos <=
- // (maxSigDig - minSigDig) Use @ if (maxSigDig - minSigDig) < pos <=
- // maxSigDig
- result.append((maxSigDig >= i && i > (maxSigDig - minDig)) ? sigDigit : digit);
- } else {
- if (roundingDigits != null) {
- int pos = roundingDecimalPos - i;
- if (pos >= 0 && pos < roundingDigits.length()) {
- result.append((char) (roundingDigits.charAt(pos) - '0' + zero));
- continue;
- }
- }
- result.append(i <= minDig ? zero : digit);
- }
- }
- if (!useSigDig) {
- if (getMaximumFractionDigits() > 0 || decimalSeparatorAlwaysShown) {
- result.append(localized ? symbols.getDecimalSeparator() :
- PATTERN_DECIMAL_SEPARATOR);
- }
- int pos = roundingDecimalPos;
- for (i = 0; i < getMaximumFractionDigits(); ++i) {
- if (roundingDigits != null && pos < roundingDigits.length()) {
- result.append(pos < 0 ? zero :
- (char) (roundingDigits.charAt(pos) - '0' + zero));
- ++pos;
- continue;
- }
- result.append(i < getMinimumFractionDigits() ? zero : digit);
- }
- }
- if (useExponentialNotation) {
- if (localized) {
- result.append(symbols.getExponentSeparator());
- } else {
- result.append(PATTERN_EXPONENT);
- }
- if (exponentSignAlwaysShown) {
- result.append(localized ? symbols.getPlusSign() : PATTERN_PLUS_SIGN);
- }
- for (i = 0; i < minExponentDigits; ++i) {
- result.append(zero);
- }
- }
- if (padSpec != null && !useExponentialNotation) {
- int add = formatWidth
- - result.length()
- + sub0Start
- - ((part == 0)
- ? positivePrefix.length() + positiveSuffix.length()
- : negativePrefix.length() + negativeSuffix.length());
- while (add > 0) {
- result.insert(sub0Start, digit);
- ++maxDig;
- --add;
- // Only add a grouping separator if we have at least 2 additional
- // characters to be added, so we don't end up with ",###".
- if (add > 1 && isGroupingPosition(maxDig)) {
- result.insert(sub0Start, group);
- --add;
- }
- }
- }
- if (padPos == PAD_BEFORE_SUFFIX) {
- result.append(padSpec);
- }
- // Use original symbols read from resources in pattern eg. use "\u00A4"
- // instead of "$" in Locale.US [Richard/GCL]
- appendAffixPattern(result, part != 0, false, localized);
- if (padPos == PAD_AFTER_SUFFIX) {
- result.append(padSpec);
- }
- if (part == 0) {
- if (negativeSuffix.equals(positiveSuffix) &&
- negativePrefix.equals(PATTERN_MINUS_SIGN + positivePrefix)) {
- break;
- } else {
- result.append(localized ? symbols.getPatternSeparator() : PATTERN_SEPARATOR);
- }
- }
- }
- return result.toString();
- }
-
- /**
- * Applies the given pattern to this Format object. A pattern is a short-hand
- * specification for the various formatting properties. These properties can also be
- * changed individually through the various setter methods.
- *
- * <p>There is no limit to integer digits are set by this routine, since that is the
- * typical end-user desire; use setMaximumInteger if you want to set a real value. For
- * negative numbers, use a second pattern, separated by a semicolon
- *
- * <p>Example "#,#00.0#" -&gt; 1,234.56
- *
- * <p>This means a minimum of 2 integer digits, 1 fraction digit, and a maximum of 2
- * fraction digits.
- *
- * <p>Example: "#,#00.0#;(#,#00.0#)" for negatives in parentheses.
- *
- * <p>In negative patterns, the minimum and maximum counts are ignored; these are
- * presumed to be set in the positive pattern.
- */
- public void applyPattern(String pattern) {
- applyPattern(pattern, false);
- }
-
- /**
- * Applies the given pattern to this Format object. The pattern is assumed to be in a
- * localized notation. A pattern is a short-hand specification for the various
- * formatting properties. These properties can also be changed individually through
- * the various setter methods.
- *
- * <p>There is no limit to integer digits are set by this routine, since that is the
- * typical end-user desire; use setMaximumInteger if you want to set a real value. For
- * negative numbers, use a second pattern, separated by a semicolon
- *
- * <p>Example "#,#00.0#" -&gt; 1,234.56
- *
- * <p>This means a minimum of 2 integer digits, 1 fraction digit, and a maximum of 2
- * fraction digits.
- *
- * <p>Example: "#,#00.0#;(#,#00.0#)" for negatives in parantheses.
- *
- * <p>In negative patterns, the minimum and maximum counts are ignored; these are
- * presumed to be set in the positive pattern.
- */
- public void applyLocalizedPattern(String pattern) {
- applyPattern(pattern, true);
- }
-
- /**
- * Does the real work of applying a pattern.
- */
- private void applyPattern(String pattern, boolean localized) {
- applyPatternWithoutExpandAffix(pattern, localized);
- expandAffixAdjustWidth(null);
- }
-
- private void expandAffixAdjustWidth(String pluralCount) {
- // Bug 4212072 Update the affix strings according to symbols in order to keep the
- // affix strings up to date. [Richard/GCL]
- expandAffixes(pluralCount);
-
- // Now that we have the actual prefix and suffix, fix up formatWidth
- if (formatWidth > 0) {
- formatWidth += positivePrefix.length() + positiveSuffix.length();
- }
- }
-
- private void applyPatternWithoutExpandAffix(String pattern, boolean localized) {
- char zeroDigit = PATTERN_ZERO_DIGIT; // '0'
- char sigDigit = PATTERN_SIGNIFICANT_DIGIT; // '@'
- char groupingSeparator = PATTERN_GROUPING_SEPARATOR;
- char decimalSeparator = PATTERN_DECIMAL_SEPARATOR;
- char percent = PATTERN_PERCENT;
- char perMill = PATTERN_PER_MILLE;
- char digit = PATTERN_DIGIT; // '#'
- char separator = PATTERN_SEPARATOR;
- String exponent = String.valueOf(PATTERN_EXPONENT);
- char plus = PATTERN_PLUS_SIGN;
- char padEscape = PATTERN_PAD_ESCAPE;
- char minus = PATTERN_MINUS_SIGN; // Bug 4212072 [Richard/GCL]
- if (localized) {
- zeroDigit = symbols.getZeroDigit();
- sigDigit = symbols.getSignificantDigit();
- groupingSeparator = symbols.getGroupingSeparator();
- decimalSeparator = symbols.getDecimalSeparator();
- percent = symbols.getPercent();
- perMill = symbols.getPerMill();
- digit = symbols.getDigit();
- separator = symbols.getPatternSeparator();
- exponent = symbols.getExponentSeparator();
- plus = symbols.getPlusSign();
- padEscape = symbols.getPadEscape();
- minus = symbols.getMinusSign(); // Bug 4212072 [Richard/GCL]
- }
- char nineDigit = (char) (zeroDigit + 9);
-
- boolean gotNegative = false;
-
- int pos = 0;
- // Part 0 is the positive pattern. Part 1, if present, is the negative
- // pattern.
- for (int part = 0; part < 2 && pos < pattern.length(); ++part) {
- // The subpart ranges from 0 to 4: 0=pattern proper, 1=prefix, 2=suffix,
- // 3=prefix in quote, 4=suffix in quote. Subpart 0 is between the prefix and
- // suffix, and consists of pattern characters. In the prefix and suffix,
- // percent, permille, and currency symbols are recognized and translated.
- int subpart = 1, sub0Start = 0, sub0Limit = 0, sub2Limit = 0;
-
- // It's important that we don't change any fields of this object
- // prematurely. We set the following variables for the multiplier, grouping,
- // etc., and then only change the actual object fields if everything parses
- // correctly. This also lets us register the data from part 0 and ignore the
- // part 1, except for the prefix and suffix.
- StringBuilder prefix = new StringBuilder();
- StringBuilder suffix = new StringBuilder();
- int decimalPos = -1;
- int multpl = 1;
- int digitLeftCount = 0, zeroDigitCount = 0, digitRightCount = 0, sigDigitCount = 0;
- byte groupingCount = -1;
- byte groupingCount2 = -1;
- int padPos = -1;
- char padChar = 0;
- int incrementPos = -1;
- long incrementVal = 0;
- byte expDigits = -1;
- boolean expSignAlways = false;
- int currencySignCnt = 0;
-
- // The affix is either the prefix or the suffix.
- StringBuilder affix = prefix;
-
- int start = pos;
-
- PARTLOOP: for (; pos < pattern.length(); ++pos) {
- char ch = pattern.charAt(pos);
- switch (subpart) {
- case 0: // Pattern proper subpart (between prefix & suffix)
- // Process the digits, decimal, and grouping characters. We record
- // five pieces of information. We expect the digits to occur in the
- // pattern ####00.00####, and we record the number of left digits,
- // zero (central) digits, and right digits. The position of the last
- // grouping character is recorded (should be somewhere within the
- // first two blocks of characters), as is the position of the decimal
- // point, if any (should be in the zero digits). If there is no
- // decimal point, then there should be no right digits.
- if (ch == digit) {
- if (zeroDigitCount > 0 || sigDigitCount > 0) {
- ++digitRightCount;
- } else {
- ++digitLeftCount;
- }
- if (groupingCount >= 0 && decimalPos < 0) {
- ++groupingCount;
- }
- } else if ((ch >= zeroDigit && ch <= nineDigit) || ch == sigDigit) {
- if (digitRightCount > 0) {
- patternError("Unexpected '" + ch + '\'', pattern);
- }
- if (ch == sigDigit) {
- ++sigDigitCount;
- } else {
- ++zeroDigitCount;
- if (ch != zeroDigit) {
- int p = digitLeftCount + zeroDigitCount + digitRightCount;
- if (incrementPos >= 0) {
- while (incrementPos < p) {
- incrementVal *= 10;
- ++incrementPos;
- }
- } else {
- incrementPos = p;
- }
- incrementVal += ch - zeroDigit;
- }
- }
- if (groupingCount >= 0 && decimalPos < 0) {
- ++groupingCount;
- }
- } else if (ch == groupingSeparator) {
- // Bug 4212072 process the Localized pattern like
- // "'Fr. '#'##0.05;'Fr.-'#'##0.05" (Locale="CH", groupingSeparator
- // == QUOTE) [Richard/GCL]
- if (ch == QUOTE && (pos + 1) < pattern.length()) {
- char after = pattern.charAt(pos + 1);
- if (!(after == digit || (after >= zeroDigit && after <= nineDigit))) {
- // A quote outside quotes indicates either the opening
- // quote or two quotes, which is a quote literal. That is,
- // we have the first quote in 'do' or o''clock.
- if (after == QUOTE) {
- ++pos;
- // Fall through to append(ch)
- } else {
- if (groupingCount < 0) {
- subpart = 3; // quoted prefix subpart
- } else {
- // Transition to suffix subpart
- subpart = 2; // suffix subpart
- affix = suffix;
- sub0Limit = pos--;
- }
- continue;
- }
- }
- }
-
- if (decimalPos >= 0) {
- patternError("Grouping separator after decimal", pattern);
- }
- groupingCount2 = groupingCount;
- groupingCount = 0;
- } else if (ch == decimalSeparator) {
- if (decimalPos >= 0) {
- patternError("Multiple decimal separators", pattern);
- }
- // Intentionally incorporate the digitRightCount, even though it
- // is illegal for this to be > 0 at this point. We check pattern
- // syntax below.
- decimalPos = digitLeftCount + zeroDigitCount + digitRightCount;
- } else {
- if (pattern.regionMatches(pos, exponent, 0, exponent.length())) {
- if (expDigits >= 0) {
- patternError("Multiple exponential symbols", pattern);
- }
- if (groupingCount >= 0) {
- patternError("Grouping separator in exponential", pattern);
- }
- pos += exponent.length();
- // Check for positive prefix
- if (pos < pattern.length() && pattern.charAt(pos) == plus) {
- expSignAlways = true;
- ++pos;
- }
- // Use lookahead to parse out the exponential part of the
- // pattern, then jump into suffix subpart.
- expDigits = 0;
- while (pos < pattern.length() && pattern.charAt(pos) == zeroDigit) {
- ++expDigits;
- ++pos;
- }
-
- // 1. Require at least one mantissa pattern digit
- // 2. Disallow "#+ @" in mantissa
- // 3. Require at least one exponent pattern digit
- if (((digitLeftCount + zeroDigitCount) < 1 &&
- (sigDigitCount + digitRightCount) < 1)
- || (sigDigitCount > 0 && digitLeftCount > 0) || expDigits < 1) {
- patternError("Malformed exponential", pattern);
- }
- }
- // Transition to suffix subpart
- subpart = 2; // suffix subpart
- affix = suffix;
- sub0Limit = pos--; // backup: for() will increment
- continue;
- }
- break;
- case 1: // Prefix subpart
- case 2: // Suffix subpart
- // Process the prefix / suffix characters Process unquoted characters
- // seen in prefix or suffix subpart.
-
- // Several syntax characters implicitly begins the next subpart if we
- // are in the prefix; otherwise they are illegal if unquoted.
- if (ch == digit || ch == groupingSeparator || ch == decimalSeparator
- || (ch >= zeroDigit && ch <= nineDigit) || ch == sigDigit) {
- // Any of these characters implicitly begins the
- // next subpart if we are in the prefix
- if (subpart == 1) { // prefix subpart
- subpart = 0; // pattern proper subpart
- sub0Start = pos--; // Reprocess this character
- continue;
- } else if (ch == QUOTE) {
- // Bug 4212072 process the Localized pattern like
- // "'Fr. '#'##0.05;'Fr.-'#'##0.05" (Locale="CH",
- // groupingSeparator == QUOTE) [Richard/GCL]
-
- // A quote outside quotes indicates either the opening quote
- // or two quotes, which is a quote literal. That is, we have
- // the first quote in 'do' or o''clock.
- if ((pos + 1) < pattern.length() && pattern.charAt(pos + 1) == QUOTE) {
- ++pos;
- affix.append(ch);
- } else {
- subpart += 2; // open quote
- }
- continue;
- }
- patternError("Unquoted special character '" + ch + '\'', pattern);
- } else if (ch == CURRENCY_SIGN) {
- // Use lookahead to determine if the currency sign is
- // doubled or not.
- boolean doubled = (pos + 1) < pattern.length() &&
- pattern.charAt(pos + 1) == CURRENCY_SIGN;
-
- // Bug 4212072 To meet the need of expandAffix(String,
- // StirngBuffer) [Richard/GCL]
- if (doubled) {
- ++pos; // Skip over the doubled character
- affix.append(ch); // append two: one here, one below
- if ((pos + 1) < pattern.length() &&
- pattern.charAt(pos + 1) == CURRENCY_SIGN) {
- ++pos; // Skip over the tripled character
- affix.append(ch); // append again
- currencySignCnt = CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT;
- } else {
- currencySignCnt = CURRENCY_SIGN_COUNT_IN_ISO_FORMAT;
- }
- } else {
- currencySignCnt = CURRENCY_SIGN_COUNT_IN_SYMBOL_FORMAT;
- }
- // Fall through to append(ch)
- } else if (ch == QUOTE) {
- // A quote outside quotes indicates either the opening quote or
- // two quotes, which is a quote literal. That is, we have the
- // first quote in 'do' or o''clock.
- if ((pos + 1) < pattern.length() && pattern.charAt(pos + 1) == QUOTE) {
- ++pos;
- affix.append(ch); // append two: one here, one below
- } else {
- subpart += 2; // open quote
- }
- // Fall through to append(ch)
- } else if (ch == separator) {
- // Don't allow separators in the prefix, and don't allow
- // separators in the second pattern (part == 1).
- if (subpart == 1 || part == 1) {
- patternError("Unquoted special character '" + ch + '\'', pattern);
- }
- sub2Limit = pos++;
- break PARTLOOP; // Go to next part
- } else if (ch == percent || ch == perMill) {
- // Next handle characters which are appended directly.
- if (multpl != 1) {
- patternError("Too many percent/permille characters", pattern);
- }
- multpl = (ch == percent) ? 100 : 1000;
- // Convert to non-localized pattern
- ch = (ch == percent) ? PATTERN_PERCENT : PATTERN_PER_MILLE;
- // Fall through to append(ch)
- } else if (ch == minus) {
- // Convert to non-localized pattern
- ch = PATTERN_MINUS_SIGN;
- // Fall through to append(ch)
- } else if (ch == padEscape) {
- if (padPos >= 0) {
- patternError("Multiple pad specifiers", pattern);
- }
- if ((pos + 1) == pattern.length()) {
- patternError("Invalid pad specifier", pattern);
- }
- padPos = pos++; // Advance past pad char
- padChar = pattern.charAt(pos);
- continue;
- }
- affix.append(ch);
- break;
- case 3: // Prefix subpart, in quote
- case 4: // Suffix subpart, in quote
- // A quote within quotes indicates either the closing quote or two
- // quotes, which is a quote literal. That is, we have the second quote
- // in 'do' or 'don''t'.
- if (ch == QUOTE) {
- if ((pos + 1) < pattern.length() && pattern.charAt(pos + 1) == QUOTE) {
- ++pos;
- affix.append(ch);
- } else {
- subpart -= 2; // close quote
- }
- // Fall through to append(ch)
- }
- // NOTE: In ICU 2.2 there was code here to parse quoted percent and
- // permille characters _within quotes_ and give them special
- // meaning. This is incorrect, since quoted characters are literals
- // without special meaning.
- affix.append(ch);
- break;
- }
- }
-
- if (subpart == 3 || subpart == 4) {
- patternError("Unterminated quote", pattern);
- }
-
- if (sub0Limit == 0) {
- sub0Limit = pattern.length();
- }
-
- if (sub2Limit == 0) {
- sub2Limit = pattern.length();
- }
-
- // Handle patterns with no '0' pattern character. These patterns are legal,
- // but must be recodified to make sense. "##.###" -> "#0.###". ".###" ->
- // ".0##".
- //
- // We allow patterns of the form "####" to produce a zeroDigitCount of zero
- // (got that?); although this seems like it might make it possible for
- // format() to produce empty strings, format() checks for this condition and
- // outputs a zero digit in this situation. Having a zeroDigitCount of zero
- // yields a minimum integer digits of zero, which allows proper round-trip
- // patterns. We don't want "#" to become "#0" when toPattern() is called (even
- // though that's what it really is, semantically).
- if (zeroDigitCount == 0 && sigDigitCount == 0 &&
- digitLeftCount > 0 && decimalPos >= 0) {
- // Handle "###.###" and "###." and ".###"
- int n = decimalPos;
- if (n == 0)
- ++n; // Handle ".###"
- digitRightCount = digitLeftCount - n;
- digitLeftCount = n - 1;
- zeroDigitCount = 1;
- }
-
- // Do syntax checking on the digits, decimal points, and quotes.
- if ((decimalPos < 0 && digitRightCount > 0 && sigDigitCount == 0)
- || (decimalPos >= 0
- && (sigDigitCount > 0
- || decimalPos < digitLeftCount
- || decimalPos > (digitLeftCount + zeroDigitCount)))
- || groupingCount == 0
- || groupingCount2 == 0
- || (sigDigitCount > 0 && zeroDigitCount > 0)
- || subpart > 2) { // subpart > 2 == unmatched quote
- patternError("Malformed pattern", pattern);
- }
-
- // Make sure pad is at legal position before or after affix.
- if (padPos >= 0) {
- if (padPos == start) {
- padPos = PAD_BEFORE_PREFIX;
- } else if (padPos + 2 == sub0Start) {
- padPos = PAD_AFTER_PREFIX;
- } else if (padPos == sub0Limit) {
- padPos = PAD_BEFORE_SUFFIX;
- } else if (padPos + 2 == sub2Limit) {
- padPos = PAD_AFTER_SUFFIX;
- } else {
- patternError("Illegal pad position", pattern);
- }
- }
-
- if (part == 0) {
- // Set negative affixes temporarily to match the positive
- // affixes. Fix this up later after processing both parts.
-
- // Bug 4212072 To meet the need of expandAffix(String, StirngBuffer)
- // [Richard/GCL]
- posPrefixPattern = negPrefixPattern = prefix.toString();
- posSuffixPattern = negSuffixPattern = suffix.toString();
-
- useExponentialNotation = (expDigits >= 0);
- if (useExponentialNotation) {
- minExponentDigits = expDigits;
- exponentSignAlwaysShown = expSignAlways;
- }
- int digitTotalCount = digitLeftCount + zeroDigitCount + digitRightCount;
- // The effectiveDecimalPos is the position the decimal is at or would be
- // at if there is no decimal. Note that if decimalPos<0, then
- // digitTotalCount == digitLeftCount + zeroDigitCount.
- int effectiveDecimalPos = decimalPos >= 0 ? decimalPos : digitTotalCount;
- boolean useSigDig = (sigDigitCount > 0);
- setSignificantDigitsUsed(useSigDig);
- if (useSigDig) {
- setMinimumSignificantDigits(sigDigitCount);
- setMaximumSignificantDigits(sigDigitCount + digitRightCount);
- } else {
- int minInt = effectiveDecimalPos - digitLeftCount;
- setMinimumIntegerDigits(minInt);
-
- // Upper limit on integer and fraction digits for a Java double
- // [Richard/GCL]
- setMaximumIntegerDigits(useExponentialNotation ? digitLeftCount + minInt :
- DOUBLE_INTEGER_DIGITS);
- _setMaximumFractionDigits(decimalPos >= 0 ?
- (digitTotalCount - decimalPos) : 0);
- setMinimumFractionDigits(decimalPos >= 0 ?
- (digitLeftCount + zeroDigitCount - decimalPos) : 0);
- }
- setGroupingUsed(groupingCount > 0);
- this.groupingSize = (groupingCount > 0) ? groupingCount : 0;
- this.groupingSize2 = (groupingCount2 > 0 && groupingCount2 != groupingCount)
- ? groupingCount2 : 0;
- this.multiplier = multpl;
- setDecimalSeparatorAlwaysShown(decimalPos == 0 || decimalPos == digitTotalCount);
- if (padPos >= 0) {
- padPosition = padPos;
- formatWidth = sub0Limit - sub0Start; // to be fixed up below
- pad = padChar;
- } else {
- formatWidth = 0;
- }
- if (incrementVal != 0) {
- // BigDecimal scale cannot be negative (even though this makes perfect
- // sense), so we need to handle this.
- int scale = incrementPos - effectiveDecimalPos;
- roundingIncrementICU = BigDecimal.valueOf(incrementVal, scale > 0 ? scale : 0);
- if (scale < 0) {
- roundingIncrementICU = roundingIncrementICU.movePointRight(-scale);
- }
- roundingMode = BigDecimal.ROUND_HALF_EVEN;
- } else {
- setRoundingIncrement((BigDecimal) null);
- }
-
- // Update currency sign count for the new pattern
- currencySignCount = currencySignCnt;
- } else {
- // Bug 4212072 To meet the need of expandAffix(String, StirngBuffer)
- // [Richard/GCL]
- negPrefixPattern = prefix.toString();
- negSuffixPattern = suffix.toString();
- gotNegative = true;
- }
- }
-
-
- // Bug 4140009 Process the empty pattern [Richard/GCL]
- if (pattern.length() == 0) {
- posPrefixPattern = posSuffixPattern = "";
- setMinimumIntegerDigits(0);
- setMaximumIntegerDigits(DOUBLE_INTEGER_DIGITS);
- setMinimumFractionDigits(0);
- _setMaximumFractionDigits(DOUBLE_FRACTION_DIGITS);
- }
-
- // If there was no negative pattern, or if the negative pattern is identical to
- // the positive pattern, then prepend the minus sign to the positive pattern to
- // form the negative pattern.
-
- // Bug 4212072 To meet the need of expandAffix(String, StirngBuffer) [Richard/GCL]
-
- if (!gotNegative ||
- (negPrefixPattern.equals(posPrefixPattern)
- && negSuffixPattern.equals(posSuffixPattern))) {
- negSuffixPattern = posSuffixPattern;
- negPrefixPattern = PATTERN_MINUS_SIGN + posPrefixPattern;
- }
- setLocale(null, null);
- // save the pattern
- formatPattern = pattern;
-
- // special handlings for currency instance
- if (currencySignCount != CURRENCY_SIGN_COUNT_ZERO) {
- // reset rounding increment and max/min fractional digits
- // by the currency
- Currency theCurrency = getCurrency();
- if (theCurrency != null) {
- setRoundingIncrement(theCurrency.getRoundingIncrement(currencyUsage));
- int d = theCurrency.getDefaultFractionDigits(currencyUsage);
- setMinimumFractionDigits(d);
- _setMaximumFractionDigits(d);
- }
-
- // initialize currencyPluralInfo if needed
- if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT
- && currencyPluralInfo == null) {
- currencyPluralInfo = new CurrencyPluralInfo(symbols.getULocale());
- }
- }
- resetActualRounding();
- }
-
-
- private void patternError(String msg, String pattern) {
- throw new IllegalArgumentException(msg + " in pattern \"" + pattern + '"');
- }
-
-
- // Rewrite the following 4 "set" methods Upper limit on integer and fraction digits
- // for a Java double [Richard/GCL]
-
- /**
- * Sets the maximum number of digits allowed in the integer portion of a number. This
- * override limits the integer digit count to 2,000,000,000 to match ICU4C.
- *
- * @see NumberFormat#setMaximumIntegerDigits
- */
- @Override
- public void setMaximumIntegerDigits(int newValue) {
- // Android changed: Allow 2 billion integer digits.
- super.setMaximumIntegerDigits(Math.min(newValue, MAX_INTEGER_DIGITS));
- }
-
- /**
- * Sets the minimum number of digits allowed in the integer portion of a number. This
- * override limits the integer digit count to 309.
- *
- * @see NumberFormat#setMinimumIntegerDigits
- */
- @Override
- public void setMinimumIntegerDigits(int newValue) {
- super.setMinimumIntegerDigits(Math.min(newValue, DOUBLE_INTEGER_DIGITS));
- }
-
- /**
- * <strong>[icu]</strong> Returns the minimum number of significant digits that will be
- * displayed. This value has no effect unless {@link #areSignificantDigitsUsed()}
- * returns true.
- *
- * @return the fewest significant digits that will be shown
- */
- public int getMinimumSignificantDigits() {
- return minSignificantDigits;
- }
-
- /**
- * <strong>[icu]</strong> Returns the maximum number of significant digits that will be
- * displayed. This value has no effect unless {@link #areSignificantDigitsUsed()}
- * returns true.
- *
- * @return the most significant digits that will be shown
- */
- public int getMaximumSignificantDigits() {
- return maxSignificantDigits;
- }
-
- /**
- * <strong>[icu]</strong> Sets the minimum number of significant digits that will be displayed. If
- * <code>min</code> is less than one then it is set to one. If the maximum significant
- * digits count is less than <code>min</code>, then it is set to <code>min</code>.
- * This function also enables the use of significant digits by this formatter -
- * {@link #areSignificantDigitsUsed()} will return true.
- *
- * @param min the fewest significant digits to be shown
- */
- public void setMinimumSignificantDigits(int min) {
- if (min < 1) {
- min = 1;
- }
- // pin max sig dig to >= min
- int max = Math.max(maxSignificantDigits, min);
- minSignificantDigits = min;
- maxSignificantDigits = max;
- setSignificantDigitsUsed(true);
- }
-
- /**
- * <strong>[icu]</strong> Sets the maximum number of significant digits that will be displayed. If
- * <code>max</code> is less than one then it is set to one. If the minimum significant
- * digits count is greater than <code>max</code>, then it is set to <code>max</code>.
- * This function also enables the use of significant digits by this formatter -
- * {@link #areSignificantDigitsUsed()} will return true.
- *
- * @param max the most significant digits to be shown
- */
- public void setMaximumSignificantDigits(int max) {
- if (max < 1) {
- max = 1;
- }
- // pin min sig dig to 1..max
- int min = Math.min(minSignificantDigits, max);
- minSignificantDigits = min;
- maxSignificantDigits = max;
- setSignificantDigitsUsed(true);
- }
-
- /**
- * <strong>[icu]</strong> Returns true if significant digits are in use or false if integer and
- * fraction digit counts are in use.
- *
- * @return true if significant digits are in use
- */
- public boolean areSignificantDigitsUsed() {
- return useSignificantDigits;
- }
-
- /**
- * <strong>[icu]</strong> Sets whether significant digits are in use, or integer and fraction digit
- * counts are in use.
- *
- * @param useSignificantDigits true to use significant digits, or false to use integer
- * and fraction digit counts
- */
- public void setSignificantDigitsUsed(boolean useSignificantDigits) {
- this.useSignificantDigits = useSignificantDigits;
- }
-
- /**
- * Sets the <tt>Currency</tt> object used to display currency amounts. This takes
- * effect immediately, if this format is a currency format. If this format is not a
- * currency format, then the currency object is used if and when this object becomes a
- * currency format through the application of a new pattern.
- *
- * @param theCurrency new currency object to use. Must not be null.
- */
- @Override
- public void setCurrency(Currency theCurrency) {
- // If we are a currency format, then modify our affixes to
- // encode the currency symbol for the given currency in our
- // locale, and adjust the decimal digits and rounding for the
- // given currency.
-
- super.setCurrency(theCurrency);
- if (theCurrency != null) {
- String s = theCurrency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
- symbols.setCurrency(theCurrency);
- symbols.setCurrencySymbol(s);
- }
-
- if (currencySignCount != CURRENCY_SIGN_COUNT_ZERO) {
- if (theCurrency != null) {
- setRoundingIncrement(theCurrency.getRoundingIncrement(currencyUsage));
- int d = theCurrency.getDefaultFractionDigits(currencyUsage);
- setMinimumFractionDigits(d);
- setMaximumFractionDigits(d);
- }
- if (currencySignCount != CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
- // This is not necessary for plural format type
- // because affixes will be resolved in subformat
- expandAffixes(null);
- }
- }
- }
-
- /**
- * Sets the <tt>Currency Usage</tt> object used to display currency.
- * This takes effect immediately, if this format is a
- * currency format.
- * @param newUsage new currency context object to use.
- */
- public void setCurrencyUsage(CurrencyUsage newUsage) {
- if (newUsage == null) {
- throw new NullPointerException("return value is null at method AAA");
- }
- currencyUsage = newUsage;
- Currency theCurrency = this.getCurrency();
-
- // We set rounding/digit based on currency context
- if (theCurrency != null) {
- setRoundingIncrement(theCurrency.getRoundingIncrement(currencyUsage));
- int d = theCurrency.getDefaultFractionDigits(currencyUsage);
- setMinimumFractionDigits(d);
- _setMaximumFractionDigits(d);
- }
- }
-
- /**
- * Returns the <tt>Currency Usage</tt> object used to display currency
- */
- public CurrencyUsage getCurrencyUsage() {
- return currencyUsage;
- }
+ @Deprecated
+ OVERRIDE_MAXIMUM_FRACTION,
/**
- * Returns the currency in effect for this formatter. Subclasses should override this
- * method as needed. Unlike getCurrency(), this method should never return null.
+ * Respect the fraction length, overriding significant digits counts if necessary.
*
- * @deprecated This API is ICU internal only.
- * @hide original deprecated declaration
+ * @see DecimalFormat#setSignificantDigitsMode
+ * @deprecated ICU 59: This API is technical preview. It may change in an upcoming release.
* @hide draft / provisional / internal are hidden on Android
*/
@Deprecated
- @Override
- protected Currency getEffectiveCurrency() {
- Currency c = getCurrency();
- if (c == null) {
- c = Currency.getInstance(symbols.getInternationalCurrencySymbol());
- }
- return c;
- }
+ RESPECT_MAXIMUM_FRACTION,
/**
- * Sets the maximum number of digits allowed in the fraction portion of a number. This
- * override limits the fraction digit count to 340.
+ * Respect minimum significant digits, overriding fraction length if necessary.
*
- * @see NumberFormat#setMaximumFractionDigits
- */
- @Override
- public void setMaximumFractionDigits(int newValue) {
- _setMaximumFractionDigits(newValue);
- resetActualRounding();
- }
-
- /*
- * Internal method for DecimalFormat, setting maximum fractional digits
- * without triggering actual rounding recalculated.
- */
- private void _setMaximumFractionDigits(int newValue) {
- super.setMaximumFractionDigits(Math.min(newValue, DOUBLE_FRACTION_DIGITS));
- }
-
- /**
- * Sets the minimum number of digits allowed in the fraction portion of a number. This
- * override limits the fraction digit count to 340.
- *
- * @see NumberFormat#setMinimumFractionDigits
- */
- @Override
- public void setMinimumFractionDigits(int newValue) {
- super.setMinimumFractionDigits(Math.min(newValue, DOUBLE_FRACTION_DIGITS));
- }
-
- /**
- * Sets whether {@link #parse(String, ParsePosition)} returns BigDecimal. The
- * default value is false.
- *
- * @param value true if {@link #parse(String, ParsePosition)}
- * returns BigDecimal.
- */
- public void setParseBigDecimal(boolean value) {
- parseBigDecimal = value;
- }
-
- /**
- * Returns whether {@link #parse(String, ParsePosition)} returns BigDecimal.
- *
- * @return true if {@link #parse(String, ParsePosition)} returns BigDecimal.
- */
- public boolean isParseBigDecimal() {
- return parseBigDecimal;
- }
-
- /**
- * Set the maximum number of exponent digits when parsing a number.
- * If the limit is set too high, an OutOfMemoryException may be triggered.
- * The default value is 1000.
- * @param newValue the new limit
- */
- public void setParseMaxDigits(int newValue) {
- if (newValue > 0) {
- PARSE_MAX_EXPONENT = newValue;
- }
- }
-
- /**
- * Get the current maximum number of exponent digits when parsing a
- * number.
- * @return the maximum number of exponent digits for parsing
- */
- public int getParseMaxDigits() {
- return PARSE_MAX_EXPONENT;
- }
-
- private void writeObject(ObjectOutputStream stream) throws IOException {
- // Ticket#6449 Format.Field instances are not serializable. When
- // formatToCharacterIterator is called, attributes (ArrayList) stores
- // FieldPosition instances with NumberFormat.Field. Because NumberFormat.Field is
- // not serializable, we need to clear the contents of the list when writeObject is
- // called. We could remove the field or make it transient, but it will break
- // serialization compatibility.
- attributes.clear();
-
- stream.defaultWriteObject();
- }
-
- /**
- * First, read the default serializable fields from the stream. Then if
- * <code>serialVersionOnStream</code> is less than 1, indicating that the stream was
- * written by JDK 1.1, initialize <code>useExponentialNotation</code> to false, since
- * it was not present in JDK 1.1. Finally, set serialVersionOnStream back to the
- * maximum allowed value so that default serialization will work properly if this
- * object is streamed out again.
- */
- private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
- stream.defaultReadObject();
-
- // Bug 4185761 validate fields [Richard/GCL]
-
- // We only need to check the maximum counts because NumberFormat .readObject has
- // already ensured that the maximum is greater than the minimum count.
-
- // Commented for compatibility with previous version, and reserved for further use
- // if (getMaximumIntegerDigits() > DOUBLE_INTEGER_DIGITS ||
- // getMaximumFractionDigits() > DOUBLE_FRACTION_DIGITS) { throw new
- // InvalidObjectException("Digit count out of range"); }
-
-
- // Android changed: Allow 2 billion integer digits.
- // Truncate the maximumIntegerDigits to MAX_INTEGER_DIGITS and
- // maximumFractionDigits to DOUBLE_FRACTION_DIGITS
-
- if (getMaximumIntegerDigits() > MAX_INTEGER_DIGITS) {
- setMaximumIntegerDigits(MAX_INTEGER_DIGITS);
- }
- if (getMaximumFractionDigits() > DOUBLE_FRACTION_DIGITS) {
- _setMaximumFractionDigits(DOUBLE_FRACTION_DIGITS);
- }
- if (serialVersionOnStream < 2) {
- exponentSignAlwaysShown = false;
- setInternalRoundingIncrement(null);
- roundingMode = BigDecimal.ROUND_HALF_EVEN;
- formatWidth = 0;
- pad = ' ';
- padPosition = PAD_BEFORE_PREFIX;
- if (serialVersionOnStream < 1) {
- // Didn't have exponential fields
- useExponentialNotation = false;
- }
- }
- if (serialVersionOnStream < 3) {
- // Versions prior to 3 do not store a currency object. Create one to match
- // the DecimalFormatSymbols object.
- setCurrencyForSymbols();
- }
- if (serialVersionOnStream < 4) {
- currencyUsage = CurrencyUsage.STANDARD;
- }
- serialVersionOnStream = currentSerialVersion;
- digitList = new DigitList();
-
- if (roundingIncrement != null) {
- setInternalRoundingIncrement(new BigDecimal(roundingIncrement));
- }
- resetActualRounding();
- }
-
- private void setInternalRoundingIncrement(BigDecimal value) {
- roundingIncrementICU = value;
- roundingIncrement = value == null ? null : value.toBigDecimal();
- }
-
- // ----------------------------------------------------------------------
- // INSTANCE VARIABLES
- // ----------------------------------------------------------------------
-
- private transient DigitList digitList = new DigitList();
-
- /**
- * The symbol used as a prefix when formatting positive numbers, e.g. "+".
- *
- * @serial
- * @see #getPositivePrefix
- */
- private String positivePrefix = "";
-
- /**
- * The symbol used as a suffix when formatting positive numbers. This is often an
- * empty string.
- *
- * @serial
- * @see #getPositiveSuffix
- */
- private String positiveSuffix = "";
-
- /**
- * The symbol used as a prefix when formatting negative numbers, e.g. "-".
- *
- * @serial
- * @see #getNegativePrefix
- */
- private String negativePrefix = "-";
-
- /**
- * The symbol used as a suffix when formatting negative numbers. This is often an
- * empty string.
- *
- * @serial
- * @see #getNegativeSuffix
- */
- private String negativeSuffix = "";
-
- /**
- * The prefix pattern for non-negative numbers. This variable corresponds to
- * <code>positivePrefix</code>.
- *
- * <p>This pattern is expanded by the method <code>expandAffix()</code> to
- * <code>positivePrefix</code> to update the latter to reflect changes in
- * <code>symbols</code>. If this variable is <code>null</code> then
- * <code>positivePrefix</code> is taken as a literal value that does not change when
- * <code>symbols</code> changes. This variable is always <code>null</code> for
- * <code>DecimalFormat</code> objects older than stream version 2 restored from
- * stream.
- *
- * @serial
- */
- // [Richard/GCL]
- private String posPrefixPattern;
-
- /**
- * The suffix pattern for non-negative numbers. This variable corresponds to
- * <code>positiveSuffix</code>. This variable is analogous to
- * <code>posPrefixPattern</code>; see that variable for further documentation.
- *
- * @serial
- */
- // [Richard/GCL]
- private String posSuffixPattern;
-
- /**
- * The prefix pattern for negative numbers. This variable corresponds to
- * <code>negativePrefix</code>. This variable is analogous to
- * <code>posPrefixPattern</code>; see that variable for further documentation.
- *
- * @serial
- */
- // [Richard/GCL]
- private String negPrefixPattern;
-
- /**
- * The suffix pattern for negative numbers. This variable corresponds to
- * <code>negativeSuffix</code>. This variable is analogous to
- * <code>posPrefixPattern</code>; see that variable for further documentation.
- *
- * @serial
- */
- // [Richard/GCL]
- private String negSuffixPattern;
-
- /**
- * Formatter for ChoiceFormat-based currency names. If this field is not null, then
- * delegate to it to format currency symbols.
- * TODO: This is obsolete: Remove, and design extensible serialization. ICU ticket #12090.
- */
- private ChoiceFormat currencyChoice;
-
- /**
- * The multiplier for use in percent, permill, etc.
- *
- * @serial
- * @see #getMultiplier
- */
- private int multiplier = 1;
-
- /**
- * The number of digits between grouping separators in the integer portion of a
- * number. Must be greater than 0 if <code>NumberFormat.groupingUsed</code> is true.
- *
- * @serial
- * @see #getGroupingSize
- * @see NumberFormat#isGroupingUsed
- */
- private byte groupingSize = 3; // invariant, > 0 if useThousands
-
- /**
- * The secondary grouping size. This is only used for Hindi numerals, which use a
- * primary grouping of 3 and a secondary grouping of 2, e.g., "12,34,567". If this
- * value is less than 1, then secondary grouping is equal to the primary grouping.
- *
- */
- private byte groupingSize2 = 0;
-
- /**
- * If true, forces the decimal separator to always appear in a formatted number, even
- * if the fractional part of the number is zero.
- *
- * @serial
- * @see #isDecimalSeparatorAlwaysShown
- */
- private boolean decimalSeparatorAlwaysShown = false;
-
- /**
- * The <code>DecimalFormatSymbols</code> object used by this format. It contains the
- * symbols used to format numbers, e.g. the grouping separator, decimal separator, and
- * so on.
- *
- * @serial
- * @see #setDecimalFormatSymbols
- * @see DecimalFormatSymbols
- */
- private DecimalFormatSymbols symbols = null; // LIU new DecimalFormatSymbols();
-
- /**
- * True to use significant digits rather than integer and fraction digit counts.
- *
- * @serial
- */
- private boolean useSignificantDigits = false;
-
- /**
- * The minimum number of significant digits to show. Must be &gt;= 1 and &lt;=
- * maxSignificantDigits. Ignored unless useSignificantDigits == true.
- *
- * @serial
- */
- private int minSignificantDigits = 1;
-
- /**
- * The maximum number of significant digits to show. Must be &gt;=
- * minSignficantDigits. Ignored unless useSignificantDigits == true.
- *
- * @serial
- */
- private int maxSignificantDigits = 6;
-
- /**
- * True to force the use of exponential (i.e. scientific) notation
- * when formatting numbers.
- *
- *<p> Note that the JDK 1.2 public API provides no way to set this
- * field, even though it is supported by the implementation and
- * the stream format. The intent is that this will be added to the
- * API in the future.
- *
- * @serial
- */
- private boolean useExponentialNotation; // Newly persistent in JDK 1.2
-
- /**
- * The minimum number of digits used to display the exponent when a number is
- * formatted in exponential notation. This field is ignored if
- * <code>useExponentialNotation</code> is not true.
- *
- * <p>Note that the JDK 1.2 public API provides no way to set this field, even though
- * it is supported by the implementation and the stream format. The intent is that
- * this will be added to the API in the future.
- *
- * @serial
- */
- private byte minExponentDigits; // Newly persistent in JDK 1.2
-
- /**
- * If true, the exponent is always prefixed with either the plus sign or the minus
- * sign. Otherwise, only negative exponents are prefixed with the minus sign. This has
- * no effect unless <code>useExponentialNotation</code> is true.
- *
- * @serial
- */
- private boolean exponentSignAlwaysShown = false;
-
- /**
- * The value to which numbers are rounded during formatting. For example, if the
- * rounding increment is 0.05, then 13.371 would be formatted as 13.350, assuming 3
- * fraction digits. Has the value <code>null</code> if rounding is not in effect, or a
- * positive value if rounding is in effect. Default value <code>null</code>.
- *
- * @serial
- */
- // Note: this is kept in sync with roundingIncrementICU.
- // it is only kept around to avoid a conversion when formatting a java.math.BigDecimal
- private java.math.BigDecimal roundingIncrement = null;
-
- /**
- * The value to which numbers are rounded during formatting. For example, if the
- * rounding increment is 0.05, then 13.371 would be formatted as 13.350, assuming 3
- * fraction digits. Has the value <code>null</code> if rounding is not in effect, or a
- * positive value if rounding is in effect. Default value <code>null</code>. WARNING:
- * the roundingIncrement value is the one serialized.
- *
- * @serial
- */
- private transient BigDecimal roundingIncrementICU = null;
-
- /**
- * The rounding mode. This value controls any rounding operations which occur when
- * applying a rounding increment or when reducing the number of fraction digits to
- * satisfy a maximum fraction digits limit. The value may assume any of the
- * <code>BigDecimal</code> rounding mode values. Default value
- * <code>BigDecimal.ROUND_HALF_EVEN</code>.
- *
- * @serial
- */
- private int roundingMode = BigDecimal.ROUND_HALF_EVEN;
-
- /**
- * Operations on <code>BigDecimal</code> numbers are controlled by a {@link
- * MathContext} object, which provides the context (precision and other information)
- * for the operation. The default <code>MathContext</code> settings are
- * <code>digits=0, form=PLAIN, lostDigits=false, roundingMode=ROUND_HALF_UP</code>;
- * these settings perform fixed point arithmetic with unlimited precision, as defined
- * for the original BigDecimal class in Java 1.1 and Java 1.2
- */
- // context for plain unlimited math
- private MathContext mathContext = new MathContext(0, MathContext.PLAIN);
-
- /**
- * The padded format width, or zero if there is no padding. Must be &gt;= 0. Default
- * value zero.
- *
- * @serial
- */
- private int formatWidth = 0;
-
- /**
- * The character used to pad the result of format to <code>formatWidth</code>, if
- * padding is in effect. Default value ' '.
- *
- * @serial
- */
- private char pad = ' ';
-
- /**
- * The position in the string at which the <code>pad</code> character will be
- * inserted, if padding is in effect. Must have a value from
- * <code>PAD_BEFORE_PREFIX</code> to <code>PAD_AFTER_SUFFIX</code>. Default value
- * <code>PAD_BEFORE_PREFIX</code>.
- *
- * @serial
- */
- private int padPosition = PAD_BEFORE_PREFIX;
-
- /**
- * True if {@link #parse(String, ParsePosition)} to return BigDecimal rather than
- * Long, Double or BigDecimal except special values. This property is introduced for
- * J2SE 5 compatibility support.
- *
- * @serial
- * @see #setParseBigDecimal(boolean)
- * @see #isParseBigDecimal()
- */
- private boolean parseBigDecimal = false;
-
- /**
- * The currency usage for the NumberFormat(standard or cash usage).
- * It is used as STANDARD by default
- */
- private CurrencyUsage currencyUsage = CurrencyUsage.STANDARD;
-
- // ----------------------------------------------------------------------
-
- static final int currentSerialVersion = 4;
-
- /**
- * The internal serial version which says which version was written Possible values
- * are:
- *
- * <ul>
- *
- * <li><b>0</b> (default): versions before JDK 1.2
- *
- * <li><b>1</b>: version from JDK 1.2 and later, which includes the two new fields
- * <code>useExponentialNotation</code> and <code>minExponentDigits</code>.
- *
- * <li><b>2</b>: version on AlphaWorks, which adds roundingMode, formatWidth, pad,
- * padPosition, exponentSignAlwaysShown, roundingIncrement.
- *
- * <li><b>3</b>: ICU 2.2. Adds currency object.
- *
- * <li><b>4</b>: ICU 54. Adds currency usage(standard vs cash)
- *
- * </ul>
- *
- * @serial
- */
- private int serialVersionOnStream = currentSerialVersion;
-
- // ----------------------------------------------------------------------
- // CONSTANTS
- // ----------------------------------------------------------------------
-
- /**
- * <strong>[icu]</strong> Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to
- * specify pad characters inserted before the prefix.
- *
- * @see #setPadPosition
- * @see #getPadPosition
- * @see #PAD_AFTER_PREFIX
- * @see #PAD_BEFORE_SUFFIX
- * @see #PAD_AFTER_SUFFIX
- */
- public static final int PAD_BEFORE_PREFIX = 0;
-
- /**
- * <strong>[icu]</strong> Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to
- * specify pad characters inserted after the prefix.
- *
- * @see #setPadPosition
- * @see #getPadPosition
- * @see #PAD_BEFORE_PREFIX
- * @see #PAD_BEFORE_SUFFIX
- * @see #PAD_AFTER_SUFFIX
- */
- public static final int PAD_AFTER_PREFIX = 1;
-
- /**
- * <strong>[icu]</strong> Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to
- * specify pad characters inserted before the suffix.
- *
- * @see #setPadPosition
- * @see #getPadPosition
- * @see #PAD_BEFORE_PREFIX
- * @see #PAD_AFTER_PREFIX
- * @see #PAD_AFTER_SUFFIX
- */
- public static final int PAD_BEFORE_SUFFIX = 2;
-
- /**
- * <strong>[icu]</strong> Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to
- * specify pad characters inserted after the suffix.
- *
- * @see #setPadPosition
- * @see #getPadPosition
- * @see #PAD_BEFORE_PREFIX
- * @see #PAD_AFTER_PREFIX
- * @see #PAD_BEFORE_SUFFIX
- */
- public static final int PAD_AFTER_SUFFIX = 3;
-
- // Constants for characters used in programmatic (unlocalized) patterns.
- static final char PATTERN_ZERO_DIGIT = '0';
- static final char PATTERN_ONE_DIGIT = '1';
- static final char PATTERN_TWO_DIGIT = '2';
- static final char PATTERN_THREE_DIGIT = '3';
- static final char PATTERN_FOUR_DIGIT = '4';
- static final char PATTERN_FIVE_DIGIT = '5';
- static final char PATTERN_SIX_DIGIT = '6';
- static final char PATTERN_SEVEN_DIGIT = '7';
- static final char PATTERN_EIGHT_DIGIT = '8';
- static final char PATTERN_NINE_DIGIT = '9';
- static final char PATTERN_GROUPING_SEPARATOR = ',';
- static final char PATTERN_DECIMAL_SEPARATOR = '.';
- static final char PATTERN_DIGIT = '#';
- static final char PATTERN_SIGNIFICANT_DIGIT = '@';
- static final char PATTERN_EXPONENT = 'E';
- static final char PATTERN_PLUS_SIGN = '+';
- static final char PATTERN_MINUS_SIGN = '-';
-
- // Affix
- private static final char PATTERN_PER_MILLE = '\u2030';
- private static final char PATTERN_PERCENT = '%';
- static final char PATTERN_PAD_ESCAPE = '*';
-
- // Other
- private static final char PATTERN_SEPARATOR = ';';
-
- // Pad escape is package private to allow access by DecimalFormatSymbols.
- // Also plus sign. Also exponent.
-
- /**
- * The CURRENCY_SIGN is the standard Unicode symbol for currency. It is used in
- * patterns and substitued with either the currency symbol, or if it is doubled, with
- * the international currency symbol. If the CURRENCY_SIGN is seen in a pattern, then
- * the decimal separator is replaced with the monetary decimal separator.
- *
- * The CURRENCY_SIGN is not localized.
- */
- private static final char CURRENCY_SIGN = '\u00A4';
-
- private static final char QUOTE = '\'';
-
- /**
- * Upper limit on integer and fraction digits for a Java double [Richard/GCL]
- */
- static final int DOUBLE_INTEGER_DIGITS = 309;
- // Android changed: Allow 2 billion integer digits.
- // This change is necessary to stay feature-compatible in java.text.DecimalFormat which
- // used to be implemented using ICU4C (which has a 2 billion integer digits limit) and
- // is now implemented based on this class.
- static final int MAX_INTEGER_DIGITS = 2000000000;
- static final int DOUBLE_FRACTION_DIGITS = 340;
-
- /**
- * When someone turns on scientific mode, we assume that more than this number of
- * digits is due to flipping from some other mode that didn't restrict the maximum,
- * and so we force 1 integer digit. We don't bother to track and see if someone is
- * using exponential notation with more than this number, it wouldn't make sense
- * anyway, and this is just to make sure that someone turning on scientific mode with
- * default settings doesn't end up with lots of zeroes.
- */
- static final int MAX_SCIENTIFIC_INTEGER_DIGITS = 8;
-
- // Proclaim JDK 1.1 serial compatibility.
- private static final long serialVersionUID = 864413376551465018L;
-
- private ArrayList<FieldPosition> attributes = new ArrayList<FieldPosition>();
-
- // The following are used in currency format
-
- // -- triple currency sign char array
- // private static final char[] tripleCurrencySign = {0xA4, 0xA4, 0xA4};
- // -- triple currency sign string
- // private static final String tripleCurrencyStr = new String(tripleCurrencySign);
- //
- // -- default currency plural pattern char array
- // private static final char[] defaultCurrencyPluralPatternChar =
- // {0, '.', '#', '#', ' ', 0xA4, 0xA4, 0xA4};
- // -- default currency plural pattern string
- // private static final String defaultCurrencyPluralPattern =
- // new String(defaultCurrencyPluralPatternChar);
-
- // pattern used in this formatter
- private String formatPattern = "";
- // style is only valid when decimal formatter is constructed by
- // DecimalFormat(pattern, decimalFormatSymbol, style)
- private int style = NumberFormat.NUMBERSTYLE;
- /**
- * Represents whether this is a currency format, and which currency format style. 0:
- * not currency format type; 1: currency style -- symbol name, such as "$" for US
- * dollar. 2: currency style -- ISO name, such as USD for US dollar. 3: currency style
- * -- plural long name, such as "US Dollar" for "1.00 US Dollar", or "US Dollars" for
- * "3.00 US Dollars".
- */
- private int currencySignCount = CURRENCY_SIGN_COUNT_ZERO;
-
- /**
- * For parsing purposes, we need to remember all prefix patterns and suffix patterns
- * of every currency format pattern, including the pattern of the default currency
- * style, ISO currency style, and plural currency style. The patterns are set through
- * applyPattern. The following are used to represent the affix patterns in currency
- * plural formats.
- */
- private static final class AffixForCurrency {
- // negative prefix pattern
- private String negPrefixPatternForCurrency = null;
- // negative suffix pattern
- private String negSuffixPatternForCurrency = null;
- // positive prefix pattern
- private String posPrefixPatternForCurrency = null;
- // positive suffix pattern
- private String posSuffixPatternForCurrency = null;
- private final int patternType;
-
- public AffixForCurrency(String negPrefix, String negSuffix, String posPrefix,
- String posSuffix, int type) {
- negPrefixPatternForCurrency = negPrefix;
- negSuffixPatternForCurrency = negSuffix;
- posPrefixPatternForCurrency = posPrefix;
- posSuffixPatternForCurrency = posSuffix;
- patternType = type;
- }
-
- public String getNegPrefix() {
- return negPrefixPatternForCurrency;
- }
-
- public String getNegSuffix() {
- return negSuffixPatternForCurrency;
- }
-
- public String getPosPrefix() {
- return posPrefixPatternForCurrency;
- }
-
- public String getPosSuffix() {
- return posSuffixPatternForCurrency;
- }
-
- public int getPatternType() {
- return patternType;
- }
- }
-
- // Affix pattern set for currency. It is a set of AffixForCurrency, each element of
- // the set saves the negative prefix, negative suffix, positive prefix, and positive
- // suffix of a pattern.
- private transient Set<AffixForCurrency> affixPatternsForCurrency = null;
-
- // For currency parsing. Since currency parsing needs to parse against all currency
- // patterns, before the parsing, we need to set up the affix patterns for all currencies.
- private transient boolean isReadyForParsing = false;
-
- // Information needed for DecimalFormat to format/parse currency plural.
- private CurrencyPluralInfo currencyPluralInfo = null;
-
- /**
- * Unit is an immutable class for the textual representation of a unit, in
- * particular its prefix and suffix.
- *
- * @author rocketman
- *
- */
- static class Unit {
- private final String prefix;
- private final String suffix;
-
- public Unit(String prefix, String suffix) {
- this.prefix = prefix;
- this.suffix = suffix;
- }
-
- public void writeSuffix(StringBuffer toAppendTo) {
- toAppendTo.append(suffix);
- }
-
- public void writePrefix(StringBuffer toAppendTo) {
- toAppendTo.append(prefix);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (!(obj instanceof Unit)) {
- return false;
- }
- Unit other = (Unit) obj;
- return prefix.equals(other.prefix) && suffix.equals(other.suffix);
- }
- @Override
- public String toString() {
- return prefix + "/" + suffix;
- }
- }
-
- static final Unit NULL_UNIT = new Unit("", "");
-
- // Note about rounding implementation
- //
- // The original design intended to skip rounding operation when roundingIncrement is not
- // set. However, rounding may need to occur when fractional digits exceed the width of
- // fractional part of pattern.
- //
- // DigitList class has built-in rounding mechanism, using ROUND_HALF_EVEN. This implementation
- // forces non-null roundingIncrement if the setting is other than ROUND_HALF_EVEN, otherwise,
- // when rounding occurs in DigitList by pattern's fractional digits' width, the result
- // does not match the rounding mode.
- //
- // Ideally, all rounding operation should be done in one place like ICU4C trunk does
- // (ICU4C rounding implementation was rewritten recently). This is intrim implemetation
- // to fix various issues. In the future, we should entire implementation of rounding
- // in this class, like ICU4C did.
- //
- // Once we fully implement rounding logic in DigitList, then following fields and methods
- // should be gone.
-
- private transient BigDecimal actualRoundingIncrementICU = null;
- private transient java.math.BigDecimal actualRoundingIncrement = null;
-
- /*
- * The actual rounding increment as a double.
- */
- private transient double roundingDouble = 0.0;
-
- /*
- * If the roundingDouble is the reciprocal of an integer (the most common case!), this
- * is set to be that integer. Otherwise it is 0.0.
- */
- private transient double roundingDoubleReciprocal = 0.0;
-
- /*
- * Set roundingDouble, roundingDoubleReciprocal and actualRoundingIncrement
- * based on rounding mode and width of fractional digits. Whenever setting affecting
- * rounding mode, rounding increment and maximum width of fractional digits, then
- * this method must be called.
- *
- * roundingIncrementICU is the field storing the custom rounding increment value,
- * while actual rounding increment could be larger.
+ * @see DecimalFormat#setSignificantDigitsMode
+ * @deprecated ICU 59: This API is technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
*/
- private void resetActualRounding() {
- if (roundingIncrementICU != null) {
- BigDecimal byWidth = getMaximumFractionDigits() > 0 ?
- BigDecimal.ONE.movePointLeft(getMaximumFractionDigits()) : BigDecimal.ONE;
- if (roundingIncrementICU.compareTo(byWidth) >= 0) {
- actualRoundingIncrementICU = roundingIncrementICU;
- } else {
- actualRoundingIncrementICU = byWidth.equals(BigDecimal.ONE) ? null : byWidth;
- }
- } else {
- if (roundingMode == BigDecimal.ROUND_HALF_EVEN || isScientificNotation()) {
- // This rounding fix is irrelevant if mode is ROUND_HALF_EVEN as DigitList
- // does ROUND_HALF_EVEN for us. This rounding fix won't work at all for
- // scientific notation.
- actualRoundingIncrementICU = null;
- } else {
- if (getMaximumFractionDigits() > 0) {
- actualRoundingIncrementICU = BigDecimal.ONE.movePointLeft(getMaximumFractionDigits());
- } else {
- actualRoundingIncrementICU = BigDecimal.ONE;
- }
- }
- }
-
- if (actualRoundingIncrementICU == null) {
- setRoundingDouble(0.0d);
- actualRoundingIncrement = null;
- } else {
- setRoundingDouble(actualRoundingIncrementICU.doubleValue());
- actualRoundingIncrement = actualRoundingIncrementICU.toBigDecimal();
- }
- }
-
- static final double roundingIncrementEpsilon = 0.000000001;
-
- private void setRoundingDouble(double newValue) {
- roundingDouble = newValue;
- if (roundingDouble > 0.0d) {
- double rawRoundedReciprocal = 1.0d / roundingDouble;
- roundingDoubleReciprocal = Math.rint(rawRoundedReciprocal);
- if (Math.abs(rawRoundedReciprocal - roundingDoubleReciprocal) > roundingIncrementEpsilon) {
- roundingDoubleReciprocal = 0.0d;
- }
- } else {
- roundingDoubleReciprocal = 0.0d;
- }
- }
+ @Deprecated
+ ENSURE_MINIMUM_SIGNIFICANT
+ }
+
+ /**
+ * <strong>[icu]</strong> Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to specify pad
+ * characters inserted before the prefix.
+ *
+ * @see #setPadPosition
+ * @see #getPadPosition
+ * @see #PAD_AFTER_PREFIX
+ * @see #PAD_BEFORE_SUFFIX
+ * @see #PAD_AFTER_SUFFIX
+ */
+ public static final int PAD_BEFORE_PREFIX = 0;
+
+ /**
+ * <strong>[icu]</strong> Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to specify pad
+ * characters inserted after the prefix.
+ *
+ * @see #setPadPosition
+ * @see #getPadPosition
+ * @see #PAD_BEFORE_PREFIX
+ * @see #PAD_BEFORE_SUFFIX
+ * @see #PAD_AFTER_SUFFIX
+ */
+ public static final int PAD_AFTER_PREFIX = 1;
+
+ /**
+ * <strong>[icu]</strong> Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to specify pad
+ * characters inserted before the suffix.
+ *
+ * @see #setPadPosition
+ * @see #getPadPosition
+ * @see #PAD_BEFORE_PREFIX
+ * @see #PAD_AFTER_PREFIX
+ * @see #PAD_AFTER_SUFFIX
+ */
+ public static final int PAD_BEFORE_SUFFIX = 2;
+
+ /**
+ * <strong>[icu]</strong> Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to specify pad
+ * characters inserted after the suffix.
+ *
+ * @see #setPadPosition
+ * @see #getPadPosition
+ * @see #PAD_BEFORE_PREFIX
+ * @see #PAD_AFTER_PREFIX
+ * @see #PAD_BEFORE_SUFFIX
+ */
+ public static final int PAD_AFTER_SUFFIX = 3;
}
-
-// eof
diff --git a/android_icu4j/src/main/java/android/icu/text/DecimalFormatSymbols.java b/android_icu4j/src/main/java/android/icu/text/DecimalFormatSymbols.java
index 5870cc7f1..2d7fab96a 100644
--- a/android_icu4j/src/main/java/android/icu/text/DecimalFormatSymbols.java
+++ b/android_icu4j/src/main/java/android/icu/text/DecimalFormatSymbols.java
@@ -213,8 +213,11 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
* Returns the array of strings used as digits, in order from 0 through 9
* Package private method - doesn't create a defensively copy.
* @return the array of digit strings
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
*/
- String[] getDigitStringsLocal() {
+ @Deprecated
+ public String[] getDigitStringsLocal() {
return digitStrings;
}
@@ -1224,9 +1227,9 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
setMonetaryGroupingSeparatorString(numberElements[11]);
setExponentMultiplicationSign(numberElements[12]);
- digit = DecimalFormat.PATTERN_DIGIT; // Localized pattern character no longer in CLDR
- padEscape = DecimalFormat.PATTERN_PAD_ESCAPE;
- sigDigit = DecimalFormat.PATTERN_SIGNIFICANT_DIGIT;
+ digit = '#'; // Localized pattern character no longer in CLDR
+ padEscape = '*';
+ sigDigit = '@';
CurrencyDisplayInfo info = CurrencyData.provider.getInstance(locale, true);
@@ -1354,8 +1357,8 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
exponential = 'E';
}
if (serialVersionOnStream < 2) {
- padEscape = DecimalFormat.PATTERN_PAD_ESCAPE;
- plusSign = DecimalFormat.PATTERN_PLUS_SIGN;
+ padEscape = '*';
+ plusSign = '+';
exponentSeparator = String.valueOf(exponential);
// Although we read the exponential field on stream to create the
// exponentSeparator, we don't do the reverse, since scientific
@@ -1433,7 +1436,7 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
groupingSeparatorString = String.valueOf(groupingSeparator);
}
if (percentString == null) {
- percentString = String.valueOf(percentString);
+ percentString = String.valueOf(percent);
}
if (perMillString == null) {
perMillString = String.valueOf(perMill);
diff --git a/android_icu4j/src/main/java/android/icu/text/DigitList.java b/android_icu4j/src/main/java/android/icu/text/DigitList.java
deleted file mode 100644
index 78f9eeb7f..000000000
--- a/android_icu4j/src/main/java/android/icu/text/DigitList.java
+++ /dev/null
@@ -1,838 +0,0 @@
-/* GENERATED SOURCE. DO NOT MODIFY. */
-// © 2016 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html#License
-/*
- *******************************************************************************
- * Copyright (C) 1996-2015, International Business Machines Corporation and *
- * others. All Rights Reserved. *
- *******************************************************************************
- */
-package android.icu.text;
-
-import java.math.BigInteger;
-
-/**
- * <code>DigitList</code> handles the transcoding between numeric values and
- * strings of characters. It only represents non-negative numbers. The
- * division of labor between <code>DigitList</code> and
- * <code>DecimalFormat</code> is that <code>DigitList</code> handles the radix
- * 10 representation issues and numeric conversion, including rounding;
- * <code>DecimalFormat</code> handles the locale-specific issues such as
- * positive and negative representation, digit grouping, decimal point,
- * currency, and so on.
- *
- * <p>A <code>DigitList</code> is a representation of a finite numeric value.
- * <code>DigitList</code> objects do not represent <code>NaN</code> or infinite
- * values. A <code>DigitList</code> value can be converted to a
- * <code>BigDecimal</code> without loss of precision. Conversion to other
- * numeric formats may involve loss of precision, depending on the specific
- * value.
- *
- * <p>The <code>DigitList</code> representation consists of a string of
- * characters, which are the digits radix 10, from '0' to '9'. It also has a
- * base 10 exponent associated with it. The value represented by a
- * <code>DigitList</code> object can be computed by mulitplying the fraction
- * <em>f</em>, where 0 <= <em>f</em> < 1, derived by placing all the digits of
- * the list to the right of the decimal point, by 10^exponent.
- *
- * @see java.util.Locale
- * @see java.text.Format
- * @see NumberFormat
- * @see DecimalFormat
- * @see java.text.ChoiceFormat
- * @see java.text.MessageFormat
- * @version 1.18 08/12/98
- * @author Mark Davis, Alan Liu
- * @hide Made public for testing
- * */
-public final class DigitList {
- /**
- * The maximum number of significant digits in an IEEE 754 double, that
- * is, in a Java double. This must not be increased, or garbage digits
- * will be generated, and should not be decreased, or accuracy will be lost.
- */
- public static final int MAX_LONG_DIGITS = 19; // == Long.toString(Long.MAX_VALUE).length()
- public static final int DBL_DIG = 17;
-
- /**
- * These data members are intentionally public and can be set directly.
- *
- * The value represented is given by placing the decimal point before
- * digits[decimalAt]. If decimalAt is < 0, then leading zeros between
- * the decimal point and the first nonzero digit are implied. If decimalAt
- * is > count, then trailing zeros between the digits[count-1] and the
- * decimal point are implied.
- *
- * Equivalently, the represented value is given by f * 10^decimalAt. Here
- * f is a value 0.1 <= f < 1 arrived at by placing the digits in Digits to
- * the right of the decimal.
- *
- * DigitList is normalized, so if it is non-zero, figits[0] is non-zero. We
- * don't allow denormalized numbers because our exponent is effectively of
- * unlimited magnitude. The count value contains the number of significant
- * digits present in digits[].
- *
- * Zero is represented by any DigitList with count == 0 or with each digits[i]
- * for all i <= count == '0'.
- */
- public int decimalAt = 0;
- public int count = 0;
- public byte[] digits = new byte[MAX_LONG_DIGITS];
-
- private final void ensureCapacity(int digitCapacity, int digitsToCopy) {
- if (digitCapacity > digits.length) {
- byte[] newDigits = new byte[digitCapacity * 2];
- System.arraycopy(digits, 0, newDigits, 0, digitsToCopy);
- digits = newDigits;
- }
- }
-
- /**
- * Return true if the represented number is zero.
- */
- boolean isZero()
- {
- for (int i=0; i<count; ++i) if (digits[i] != '0') return false;
- return true;
- }
-
-// Unused as of ICU 2.6 - alan
-// /**
-// * Clears out the digits.
-// * Use before appending them.
-// * Typically, you set a series of digits with append, then at the point
-// * you hit the decimal point, you set myDigitList.decimalAt = myDigitList.count;
-// * then go on appending digits.
-// */
-// public void clear () {
-// decimalAt = 0;
-// count = 0;
-// }
-
- /**
- * Appends digits to the list.
- */
- public void append (int digit) {
- ensureCapacity(count+1, count);
- digits[count++] = (byte) digit;
- }
-
- public byte getDigitValue(int i) {
- return (byte) (digits[i] - '0');
- }
-
- /**
- * Utility routine to get the value of the digit list
- * If (count == 0) this throws a NumberFormatException, which
- * mimics Long.parseLong().
- */
- public final double getDouble() {
- if (count == 0) return 0.0;
- StringBuilder temp = new StringBuilder(count);
- temp.append('.');
- for (int i = 0; i < count; ++i) temp.append((char)(digits[i]));
- temp.append('E');
- temp.append(Integer.toString(decimalAt));
- return Double.valueOf(temp.toString()).doubleValue();
- // long value = Long.parseLong(temp.toString());
- // return (value * Math.pow(10, decimalAt - count));
- }
-
- /**
- * Utility routine to get the value of the digit list.
- * If (count == 0) this returns 0, unlike Long.parseLong().
- */
- public final long getLong() {
- // for now, simple implementation; later, do proper IEEE native stuff
-
- if (count == 0) return 0;
-
- // We have to check for this, because this is the one NEGATIVE value
- // we represent. If we tried to just pass the digits off to parseLong,
- // we'd get a parse failure.
- if (isLongMIN_VALUE()) return Long.MIN_VALUE;
-
- StringBuilder temp = new StringBuilder(count);
- for (int i = 0; i < decimalAt; ++i)
- {
- temp.append((i < count) ? (char)(digits[i]) : '0');
- }
- return Long.parseLong(temp.toString());
- }
-
- /**
- * Return a <code>BigInteger</code> representing the value stored in this
- * <code>DigitList</code>. This method assumes that this object contains
- * an integral value; if not, it will return an incorrect value.
- * [bnf]
- * @param isPositive determines the sign of the returned result
- * @return the value of this object as a <code>BigInteger</code>
- */
- public BigInteger getBigInteger(boolean isPositive) {
- if (isZero()) return BigInteger.valueOf(0);
- //Eclipse stated the following is "dead code"
- /*if (false) {
- StringBuilder stringRep = new StringBuilder(count);
- if (!isPositive) {
- stringRep.append('-');
- }
- for (int i=0; i<count; ++i) {
- stringRep.append((char) digits[i]);
- }
- int d = decimalAt;
- while (d-- > count) {
- stringRep.append('0');
- }
- return new BigInteger(stringRep.toString());
- } else*/ {
- int len = decimalAt > count ? decimalAt : count;
- if (!isPositive) {
- len += 1;
- }
- char[] text = new char[len];
- int n = 0;
- if (!isPositive) {
- text[0] = '-';
- for (int i = 0; i < count; ++i) {
- text[i+1] = (char)digits[i];
- }
- n = count+1;
- } else {
- for (int i = 0; i < count; ++i) {
- text[i] = (char)digits[i];
- }
- n = count;
- }
- for (int i = n; i < text.length; ++i) {
- text[i] = '0';
- }
- return new BigInteger(new String(text));
- }
- }
-
- private String getStringRep(boolean isPositive) {
- if (isZero()) return "0";
- StringBuilder stringRep = new StringBuilder(count+1);
- if (!isPositive) {
- stringRep.append('-');
- }
- int d = decimalAt;
- if (d < 0) {
- stringRep.append('.');
- while (d < 0) {
- stringRep.append('0');
- ++d;
- }
- d = -1;
- }
- for (int i=0; i<count; ++i) {
- if (d == i) {
- stringRep.append('.');
- }
- stringRep.append((char) digits[i]);
- }
- while (d-- > count) {
- stringRep.append('0');
- }
- return stringRep.toString();
- }
-
- /**
- * Return an <code>ICU BigDecimal</code> representing the value stored in this
- * <code>DigitList</code>.
- * [bnf]
- * @param isPositive determines the sign of the returned result
- * @return the value of this object as a <code>BigDecimal</code>
- */
- public android.icu.math.BigDecimal getBigDecimalICU(boolean isPositive) {
- if (isZero()) {
- return android.icu.math.BigDecimal.valueOf(0);
- }
- // if exponential notion is negative,
- // we prefer to use BigDecimal constructor with scale,
- // because it works better when extremely small value
- // is used. See #5698.
- long scale = (long)count - (long)decimalAt;
- if (scale > 0) {
- int numDigits = count;
- if (scale > (long)Integer.MAX_VALUE) {
- // try to reduce the scale
- long numShift = scale - (long)Integer.MAX_VALUE;
- if (numShift < count) {
- numDigits -= numShift;
- } else {
- // fallback to 0
- return new android.icu.math.BigDecimal(0);
- }
- }
- StringBuilder significantDigits = new StringBuilder(numDigits + 1);
- if (!isPositive) {
- significantDigits.append('-');
- }
- for (int i = 0; i < numDigits; i++) {
- significantDigits.append((char)digits[i]);
- }
- BigInteger unscaledVal = new BigInteger(significantDigits.toString());
- return new android.icu.math.BigDecimal(unscaledVal, (int)scale);
- } else {
- return new android.icu.math.BigDecimal(getStringRep(isPositive));
- }
- }
-
- /**
- * Return whether or not this objects represented value is an integer.
- * [bnf]
- * @return true if the represented value of this object is an integer
- */
- boolean isIntegral() {
- // Trim trailing zeros. This does not change the represented value.
- while (count > 0 && digits[count - 1] == (byte)'0') --count;
- return count == 0 || decimalAt >= count;
- }
-
-// Unused as of ICU 2.6 - alan
-// /**
-// * Return true if the number represented by this object can fit into
-// * a long.
-// */
-// boolean fitsIntoLong(boolean isPositive)
-// {
-// // Figure out if the result will fit in a long. We have to
-// // first look for nonzero digits after the decimal point;
-// // then check the size. If the digit count is 18 or less, then
-// // the value can definitely be represented as a long. If it is 19
-// // then it may be too large.
-//
-// // Trim trailing zeros. This does not change the represented value.
-// while (count > 0 && digits[count - 1] == (byte)'0') --count;
-//
-// if (count == 0) {
-// // Positive zero fits into a long, but negative zero can only
-// // be represented as a double. - bug 4162852
-// return isPositive;
-// }
-//
-// if (decimalAt < count || decimalAt > MAX_LONG_DIGITS) return false;
-//
-// if (decimalAt < MAX_LONG_DIGITS) return true;
-//
-// // At this point we have decimalAt == count, and count == MAX_LONG_DIGITS.
-// // The number will overflow if it is larger than 9223372036854775807
-// // or smaller than -9223372036854775808.
-// for (int i=0; i<count; ++i)
-// {
-// byte dig = digits[i], max = LONG_MIN_REP[i];
-// if (dig > max) return false;
-// if (dig < max) return true;
-// }
-//
-// // At this point the first count digits match. If decimalAt is less
-// // than count, then the remaining digits are zero, and we return true.
-// if (count < decimalAt) return true;
-//
-// // Now we have a representation of Long.MIN_VALUE, without the leading
-// // negative sign. If this represents a positive value, then it does
-// // not fit; otherwise it fits.
-// return !isPositive;
-// }
-
-// Unused as of ICU 2.6 - alan
-// /**
-// * Set the digit list to a representation of the given double value.
-// * This method supports fixed-point notation.
-// * @param source Value to be converted; must not be Inf, -Inf, Nan,
-// * or a value <= 0.
-// * @param maximumFractionDigits The most fractional digits which should
-// * be converted.
-// */
-// public final void set(double source, int maximumFractionDigits)
-// {
-// set(source, maximumFractionDigits, true);
-// }
-
- /**
- * Set the digit list to a representation of the given double value.
- * This method supports both fixed-point and exponential notation.
- * @param source Value to be converted; must not be Inf, -Inf, Nan,
- * or a value <= 0.
- * @param maximumDigits The most fractional or total digits which should
- * be converted.
- * @param fixedPoint If true, then maximumDigits is the maximum
- * fractional digits to be converted. If false, total digits.
- */
- final void set(double source, int maximumDigits, boolean fixedPoint)
- {
- if (source == 0) source = 0;
- // Generate a representation of the form DDDDD, DDDDD.DDDDD, or
- // DDDDDE+/-DDDDD.
- String rep = Double.toString(source);
-
- didRound = false;
-
- set(rep, MAX_LONG_DIGITS);
-
- if (fixedPoint) {
- // The negative of the exponent represents the number of leading
- // zeros between the decimal and the first non-zero digit, for
- // a value < 0.1 (e.g., for 0.00123, -decimalAt == 2). If this
- // is more than the maximum fraction digits, then we have an underflow
- // for the printed representation.
- if (-decimalAt > maximumDigits) {
- count = 0;
- return;
- } else if (-decimalAt == maximumDigits) {
- if (shouldRoundUp(0)) {
- count = 1;
- ++decimalAt;
- digits[0] = (byte)'1';
- } else {
- count = 0;
- }
- return;
- }
- // else fall through
- }
-
- // Eliminate trailing zeros.
- while (count > 1 && digits[count - 1] == '0')
- --count;
-
- // Eliminate digits beyond maximum digits to be displayed.
- // Round up if appropriate.
- round(fixedPoint ? (maximumDigits + decimalAt) : maximumDigits == 0 ? -1 : maximumDigits);
- }
-
- /**
- * Given a string representation of the form DDDDD, DDDDD.DDDDD,
- * or DDDDDE+/-DDDDD, set this object's value to it. Ignore
- * any leading '-'.
- */
- private void set(String rep, int maxCount) {
- decimalAt = -1;
- count = 0;
- int exponent = 0;
- // Number of zeros between decimal point and first non-zero digit after
- // decimal point, for numbers < 1.
- int leadingZerosAfterDecimal = 0;
- boolean nonZeroDigitSeen = false;
- // Skip over leading '-'
- int i=0;
- if (rep.charAt(i) == '-') {
- ++i;
- }
- for (; i < rep.length(); ++i) {
- char c = rep.charAt(i);
- if (c == '.') {
- decimalAt = count;
- } else if (c == 'e' || c == 'E') {
- ++i;
- // Integer.parseInt doesn't handle leading '+' signs
- if (rep.charAt(i) == '+') {
- ++i;
- }
- exponent = Integer.valueOf(rep.substring(i)).intValue();
- break;
- } else if (count < maxCount) {
- if (!nonZeroDigitSeen) {
- nonZeroDigitSeen = (c != '0');
- if (!nonZeroDigitSeen && decimalAt != -1) {
- ++leadingZerosAfterDecimal;
- }
- }
-
- if (nonZeroDigitSeen) {
- ensureCapacity(count+1, count);
- digits[count++] = (byte)c;
- }
- }
- }
- if (decimalAt == -1) {
- decimalAt = count;
- }
- decimalAt += exponent - leadingZerosAfterDecimal;
- }
-
- /**
- * Return true if truncating the representation to the given number
- * of digits will result in an increment to the last digit. This
- * method implements half-even rounding, the default rounding mode.
- * [bnf]
- * @param maximumDigits the number of digits to keep, from 0 to
- * <code>count-1</code>. If 0, then all digits are rounded away, and
- * this method returns true if a one should be generated (e.g., formatting
- * 0.09 with "#.#").
- * @return true if digit <code>maximumDigits-1</code> should be
- * incremented
- */
- private boolean shouldRoundUp(int maximumDigits) {
- // variable not used boolean increment = false;
- // Implement IEEE half-even rounding
- /*Bug 4243108
- format(0.0) gives "0.1" if preceded by parse("99.99") [Richard/GCL]
- */
- if (maximumDigits < count) {
- if (digits[maximumDigits] > '5') {
- return true;
- } else if (digits[maximumDigits] == '5' ) {
- for (int i=maximumDigits+1; i<count; ++i) {
- if (digits[i] != '0') {
- return true;
- }
- }
- return maximumDigits > 0 && (digits[maximumDigits-1] % 2 != 0);
- }
- }
- return false;
- }
-
- /**
- * Round the representation to the given number of digits.
- * @param maximumDigits The maximum number of digits to be shown.
- * Upon return, count will be less than or equal to maximumDigits.
- * This now performs rounding when maximumDigits is 0, formerly it did not.
- */
- public final void round(int maximumDigits) {
- // Eliminate digits beyond maximum digits to be displayed.
- // Round up if appropriate.
- // [bnf] rewritten to fix 4179818
- if (maximumDigits >= 0 && maximumDigits < count) {
- if (shouldRoundUp(maximumDigits)) {
- // Rounding up involves incrementing digits from LSD to MSD.
- // In most cases this is simple, but in a worst case situation
- // (9999..99) we have to adjust the decimalAt value.
- for (;;)
- {
- --maximumDigits;
- if (maximumDigits < 0)
- {
- // We have all 9's, so we increment to a single digit
- // of one and adjust the exponent.
- digits[0] = (byte) '1';
- ++decimalAt;
- maximumDigits = 0; // Adjust the count
- didRound = true;
- break;
- }
-
- ++digits[maximumDigits];
- didRound = true;
- if (digits[maximumDigits] <= '9') break;
- // digits[maximumDigits] = '0'; // Unnecessary since we'll truncate this
- }
- ++maximumDigits; // Increment for use as count
- }
- count = maximumDigits;
- }
- // Bug 4217661 DecimalFormat formats 1.001 to "1.00" instead of "1"
- // Eliminate trailing zeros. [Richard/GCL]
- // [dlf] moved outside if block, see ticket #6408
- while (count > 1 && digits[count-1] == '0') {
- --count;
- }
- }
-
- // Value to indicate that rounding was done.
- private boolean didRound = false;
-
- /**
- * Indicates if last digit set was rounded or not.
- * true indicates it was rounded.
- * false indicates rounding has not been done.
- */
- public boolean wasRounded() {
- return didRound;
- }
-
- /**
- * Utility routine to set the value of the digit list from a long
- */
- public final void set(long source)
- {
- set(source, 0);
- }
-
- /**
- * Set the digit list to a representation of the given long value.
- * @param source Value to be converted; must be >= 0 or ==
- * Long.MIN_VALUE.
- * @param maximumDigits The most digits which should be converted.
- * If maximumDigits is lower than the number of significant digits
- * in source, the representation will be rounded. Ignored if <= 0.
- */
- public final void set(long source, int maximumDigits)
- {
- // This method does not expect a negative number. However,
- // "source" can be a Long.MIN_VALUE (-9223372036854775808),
- // if the number being formatted is a Long.MIN_VALUE. In that
- // case, it will be formatted as -Long.MIN_VALUE, a number
- // which is outside the legal range of a long, but which can
- // be represented by DigitList.
- // [NEW] Faster implementation
- didRound = false;
-
- if (source <= 0) {
- if (source == Long.MIN_VALUE) {
- decimalAt = count = MAX_LONG_DIGITS;
- System.arraycopy(LONG_MIN_REP, 0, digits, 0, count);
- } else {
- count = 0;
- decimalAt = 0;
- }
- } else {
- int left = MAX_LONG_DIGITS;
- int right;
- while (source > 0) {
- digits[--left] = (byte) (((long) '0') + (source % 10));
- source /= 10;
- }
- decimalAt = MAX_LONG_DIGITS-left;
- // Don't copy trailing zeros
- // we are guaranteed that there is at least one non-zero digit,
- // so we don't have to check lower bounds
- for (right = MAX_LONG_DIGITS - 1; digits[right] == (byte) '0'; --right) {}
- count = right - left + 1;
- System.arraycopy(digits, left, digits, 0, count);
- }
- if (maximumDigits > 0) round(maximumDigits);
- }
-
- /**
- * Set the digit list to a representation of the given BigInteger value.
- * [bnf]
- * @param source Value to be converted
- * @param maximumDigits The most digits which should be converted.
- * If maximumDigits is lower than the number of significant digits
- * in source, the representation will be rounded. Ignored if <= 0.
- */
- public final void set(BigInteger source, int maximumDigits) {
- String stringDigits = source.toString();
-
- count = decimalAt = stringDigits.length();
- didRound = false;
-
- // Don't copy trailing zeros
- while (count > 1 && stringDigits.charAt(count - 1) == '0') --count;
-
- int offset = 0;
- if (stringDigits.charAt(0) == '-') {
- ++offset;
- --count;
- --decimalAt;
- }
-
- ensureCapacity(count, 0);
- for (int i = 0; i < count; ++i) {
- digits[i] = (byte) stringDigits.charAt(i + offset);
- }
-
- if (maximumDigits > 0) round(maximumDigits);
- }
-
- /**
- * Internal method that sets this digit list to represent the
- * given value. The value is given as a String of the format
- * returned by BigDecimal.
- * @param stringDigits value to be represented with the following
- * syntax, expressed as a regular expression: -?\d*.?\d*
- * Must not be an empty string.
- * @param maximumDigits The most digits which should be converted.
- * If maximumDigits is lower than the number of significant digits
- * in source, the representation will be rounded. Ignored if <= 0.
- * @param fixedPoint If true, then maximumDigits is the maximum
- * fractional digits to be converted. If false, total digits.
- */
- private void setBigDecimalDigits(String stringDigits,
- int maximumDigits, boolean fixedPoint) {
-//| // Find the first non-zero digit, the decimal, and the last non-zero digit.
-//| int first=-1, last=stringDigits.length()-1, decimal=-1;
-//| for (int i=0; (first<0 || decimal<0) && i<=last; ++i) {
-//| char c = stringDigits.charAt(i);
-//| if (c == '.') {
-//| decimal = i;
-//| } else if (first < 0 && (c >= '1' && c <= '9')) {
-//| first = i;
-//| }
-//| }
-//|
-//| if (first < 0) {
-//| clear();
-//| return;
-//| }
-//|
-//| // At this point we know there is at least one non-zero digit, so the
-//| // following loop is safe.
-//| for (;;) {
-//| char c = stringDigits.charAt(last);
-//| if (c != '0' && c != '.') {
-//| break;
-//| }
-//| --last;
-//| }
-//|
-//| if (decimal < 0) {
-//| decimal = stringDigits.length();
-//| }
-//|
-//| count = last - first;
-//| if (decimal < first || decimal > last) {
-//| ++count;
-//| }
-//| decimalAt = decimal - first;
-//| if (decimalAt < 0) {
-//| ++decimalAt;
-//| }
-//|
-//| ensureCapacity(count, 0);
-//| for (int i = 0; i < count; ++i) {
-//| digits[i] = (byte) stringDigits.charAt(first++);
-//| if (first == decimal) {
-//| ++first;
-//| }
-//| }
-
- didRound = false;
-
- // The maxDigits here could also be Integer.MAX_VALUE
- set(stringDigits, stringDigits.length());
-
- // Eliminate digits beyond maximum digits to be displayed.
- // Round up if appropriate.
- // {dlf} Some callers depend on passing '0' to round to mean 'don't round', but
- // rather than pass that information explicitly, we rely on some magic with maximumDigits
- // and decimalAt. Unfortunately, this is no good, because there are cases where maximumDigits
- // is zero and we do want to round, e.g. BigDecimal values -1 < x < 1. So since round
- // changed to perform rounding when the argument is 0, we now force the argument
- // to -1 in the situations where it matters.
- round(fixedPoint ? (maximumDigits + decimalAt) : maximumDigits == 0 ? -1 : maximumDigits);
- }
-
- /**
- * Set the digit list to a representation of the given BigDecimal value.
- * [bnf]
- * @param source Value to be converted
- * @param maximumDigits The most digits which should be converted.
- * If maximumDigits is lower than the number of significant digits
- * in source, the representation will be rounded. Ignored if <= 0.
- * @param fixedPoint If true, then maximumDigits is the maximum
- * fractional digits to be converted. If false, total digits.
- */
- public final void set(java.math.BigDecimal source,
- int maximumDigits, boolean fixedPoint) {
- setBigDecimalDigits(source.toString(), maximumDigits, fixedPoint);
- }
-
- /*
- * Set the digit list to a representation of the given BigDecimal value.
- * [bnf]
- * @param source Value to be converted
- * @param maximumDigits The most digits which should be converted.
- * If maximumDigits is lower than the number of significant digits
- * in source, the representation will be rounded. Ignored if <= 0.
- * @param fixedPoint If true, then maximumDigits is the maximum
- * fractional digits to be converted. If false, total digits.
- */
- public final void set(android.icu.math.BigDecimal source,
- int maximumDigits, boolean fixedPoint) {
- setBigDecimalDigits(source.toString(), maximumDigits, fixedPoint);
- }
-
- /**
- * Returns true if this DigitList represents Long.MIN_VALUE;
- * false, otherwise. This is required so that getLong() works.
- */
- private boolean isLongMIN_VALUE()
- {
- if (decimalAt != count || count != MAX_LONG_DIGITS)
- return false;
-
- for (int i = 0; i < count; ++i)
- {
- if (digits[i] != LONG_MIN_REP[i]) return false;
- }
-
- return true;
- }
-
- private static byte[] LONG_MIN_REP;
-
- static
- {
- // Store the representation of LONG_MIN without the leading '-'
- String s = Long.toString(Long.MIN_VALUE);
- LONG_MIN_REP = new byte[MAX_LONG_DIGITS];
- for (int i=0; i < MAX_LONG_DIGITS; ++i)
- {
- LONG_MIN_REP[i] = (byte)s.charAt(i + 1);
- }
- }
-
-// Unused -- Alan 2003-05
-// /**
-// * Return the floor of the log base 10 of a given double.
-// * This method compensates for inaccuracies which arise naturally when
-// * computing logs, and always give the correct value. The parameter
-// * must be positive and finite.
-// */
-// private static final int log10(double d)
-// {
-// // The reason this routine is needed is that simply taking the
-// // log and dividing by log10 yields a result which may be off
-// // by 1 due to rounding errors. For example, the naive log10
-// // of 1.0e300 taken this way is 299, rather than 300.
-// double log10 = Math.log(d) / LOG10;
-// int ilog10 = (int)Math.floor(log10);
-// // Positive logs could be too small, e.g. 0.99 instead of 1.0
-// if (log10 > 0 && d >= Math.pow(10, ilog10 + 1))
-// {
-// ++ilog10;
-// }
-// // Negative logs could be too big, e.g. -0.99 instead of -1.0
-// else if (log10 < 0 && d < Math.pow(10, ilog10))
-// {
-// --ilog10;
-// }
-// return ilog10;
-// }
-//
-// private static final double LOG10 = Math.log(10.0);
-
- /**
- * equality test between two digit lists.
- */
- public boolean equals(Object obj) {
- if (this == obj) // quick check
- return true;
- if (!(obj instanceof DigitList)) // (1) same object?
- return false;
- DigitList other = (DigitList) obj;
- if (count != other.count ||
- decimalAt != other.decimalAt)
- return false;
- for (int i = 0; i < count; i++)
- if (digits[i] != other.digits[i])
- return false;
- return true;
- }
-
- /**
- * Generates the hash code for the digit list.
- */
- public int hashCode() {
- int hashcode = decimalAt;
-
- for (int i = 0; i < count; i++)
- hashcode = hashcode * 37 + digits[i];
-
- return hashcode;
- }
-
- public String toString()
- {
- if (isZero()) return "0";
- StringBuilder buf = new StringBuilder("0.");
- for (int i=0; i<count; ++i) buf.append((char)digits[i]);
- buf.append("x10^");
- buf.append(decimalAt);
- return buf.toString();
- }
-}
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 4d4616bfb..7899d7f50 100644
--- a/android_icu4j/src/main/java/android/icu/text/MeasureFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/MeasureFormat.java
@@ -57,13 +57,13 @@ import android.icu.util.UResourceBundle;
* <p>To format a Measure object, first create a formatter
* object using a MeasureFormat factory method. Then use that
* object's format or formatMeasures methods.
- *
+ *
* Here is sample code:
* <pre>
* MeasureFormat fmtFr = MeasureFormat.getInstance(
* ULocale.FRENCH, FormatWidth.SHORT);
* Measure measure = new Measure(23, MeasureUnit.CELSIUS);
- *
+ *
* // Output: 23 °C
* System.out.println(fmtFr.format(measure));
*
@@ -71,29 +71,29 @@ import android.icu.util.UResourceBundle;
*
* // Output: 70 °F
* System.out.println(fmtFr.format(measureF));
- *
+ *
* MeasureFormat fmtFrFull = MeasureFormat.getInstance(
* ULocale.FRENCH, FormatWidth.WIDE);
* // Output: 70 pieds et 5,3 pouces
* System.out.println(fmtFrFull.formatMeasures(
* new Measure(70, MeasureUnit.FOOT),
* new Measure(5.3, MeasureUnit.INCH)));
- *
+ *
* // Output: 1 pied et 1 pouce
* System.out.println(fmtFrFull.formatMeasures(
* new Measure(1, MeasureUnit.FOOT),
* new Measure(1, MeasureUnit.INCH)));
- *
+ *
* MeasureFormat fmtFrNarrow = MeasureFormat.getInstance(
ULocale.FRENCH, FormatWidth.NARROW);
* // Output: 1′ 1″
* System.out.println(fmtFrNarrow.formatMeasures(
* new Measure(1, MeasureUnit.FOOT),
* new Measure(1, MeasureUnit.INCH)));
- *
- *
+ *
+ *
* MeasureFormat fmtEn = MeasureFormat.getInstance(ULocale.ENGLISH, FormatWidth.WIDE);
- *
+ *
* // Output: 1 inch, 2 feet
* fmtEn.formatMeasures(
* new Measure(1, MeasureUnit.INCH),
@@ -106,7 +106,7 @@ import android.icu.util.UResourceBundle;
* This class is immutable and thread-safe so long as its deprecated subclass,
* TimeUnitFormat, is never used. TimeUnitFormat is not thread-safe, and is
* mutable. Although this class has existing subclasses, this class does not support new
- * sub-classes.
+ * sub-classes.
*
* @see android.icu.text.UFormat
* @author Alan Liu
@@ -162,12 +162,12 @@ public class MeasureFormat extends UFormat {
/**
* Spell out everything.
*/
- WIDE(ListFormatter.Style.DURATION, NumberFormat.PLURALCURRENCYSTYLE),
+ WIDE(ListFormatter.Style.DURATION, NumberFormat.PLURALCURRENCYSTYLE),
/**
* Abbreviate when possible.
*/
- SHORT(ListFormatter.Style.DURATION_SHORT, NumberFormat.ISOCURRENCYSTYLE),
+ SHORT(ListFormatter.Style.DURATION_SHORT, NumberFormat.ISOCURRENCYSTYLE),
/**
* Brief. Use only a symbol for the unit when possible.
@@ -280,11 +280,11 @@ public class MeasureFormat extends UFormat {
* If the pos argument identifies a NumberFormat field,
* then its indices are set to the beginning and end of the first such field
* encountered. MeasureFormat itself does not supply any fields.
- *
+ *
* Calling a
* <code>formatMeasures</code> method is preferred over calling
* this method as they give better performance.
- *
+ *
* @param obj must be a Collection&lt;? extends Measure&gt;, Measure[], or Measure object.
* @param toAppendTo Formatted string appended here.
* @param pos Identifies a field in the formatted text.
@@ -311,7 +311,7 @@ public class MeasureFormat extends UFormat {
} else if (obj instanceof Measure){
toAppendTo.append(formatMeasure((Measure) obj, numberFormat, new StringBuilder(), fpos));
} else {
- throw new IllegalArgumentException(obj.toString());
+ throw new IllegalArgumentException(obj.toString());
}
if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) {
pos.setBeginIndex(fpos.getBeginIndex() + prevLength);
@@ -339,7 +339,7 @@ public class MeasureFormat extends UFormat {
* and using the appropriate Number values. Typically the units should be
* in descending order, with all but the last Measure having integer values
* (eg, not “3.2 feet, 2 inches”).
- *
+ *
* @param measures a sequence of one or more measures.
* @return the formatted string.
*/
@@ -357,7 +357,7 @@ public class MeasureFormat extends UFormat {
* <br>Note: If the format doesn’t have enough decimals, or lowValue ≥ highValue,
* the result will be a degenerate range, like “5-5 meters”.
* <br>Currency Units are not yet supported.
- *
+ *
* @param lowValue low value in range
* @param highValue high value in range
* @return the formatted string.
@@ -399,11 +399,11 @@ public class MeasureFormat extends UFormat {
}
final double lowDouble = lowNumber.doubleValue();
- String keywordLow = rules.select(new PluralRules.FixedDecimal(lowDouble,
+ String keywordLow = rules.select(new PluralRules.FixedDecimal(lowDouble,
lowFpos.getCountVisibleFractionDigits(), lowFpos.getFractionDigits()));
final double highDouble = highNumber.doubleValue();
- String keywordHigh = rules.select(new PluralRules.FixedDecimal(highDouble,
+ String keywordHigh = rules.select(new PluralRules.FixedDecimal(highDouble,
highFpos.getCountVisibleFractionDigits(), highFpos.getFractionDigits()));
final PluralRanges pluralRanges = Factory.getDefaultFactory().getPluralRanges(getLocale());
@@ -465,10 +465,10 @@ public class MeasureFormat extends UFormat {
result.append(affix.substring(pos+replacement.length()));
}
}
-
+
/**
- * Formats a single measure per unit.
- *
+ * Formats a single measure per unit.
+ *
* An example of such a formatted string is "3.5 meters per second."
*
* @param measure the measure object. In above example, 3.5 meters.
@@ -503,11 +503,11 @@ public class MeasureFormat extends UFormat {
/**
* Formats a sequence of measures.
- *
+ *
* If the fieldPosition argument identifies a NumberFormat field,
* then its indices are set to the beginning and end of the first such field
* encountered. MeasureFormat itself does not supply any fields.
- *
+ *
* @param appendTo the formatted string appended here.
* @param fieldPosition Identifies a field in the formatted text.
* @param measures the measures to format.
@@ -591,8 +591,8 @@ public class MeasureFormat extends UFormat {
}
MeasureFormat rhs = (MeasureFormat) other;
// A very slow but safe implementation.
- return getWidth() == rhs.getWidth()
- && getLocale().equals(rhs.getLocale())
+ return getWidth() == rhs.getWidth()
+ && getLocale().equals(rhs.getLocale())
&& getNumberFormat().equals(rhs.getNumberFormat());
}
@@ -602,7 +602,7 @@ public class MeasureFormat extends UFormat {
@Override
public final int hashCode() {
// A very slow but safe implementation.
- return (getLocale().hashCode() * 31
+ return (getLocale().hashCode() * 31
+ getNumberFormat().hashCode()) * 31 + getWidth().hashCode();
}
@@ -806,10 +806,8 @@ public class MeasureFormat extends UFormat {
// Trigger a fresh lookup of the patterns for this unit+width.
patterns = null;
- if (value.getType() == ICUResourceBundle.STRING) {
- // Units like "coordinate" that don't have plural variants
- setFormatterIfAbsent(StandardPlural.OTHER.ordinal(), value, 0);
- } else if (value.getType() == ICUResourceBundle.TABLE) {
+ // We no longer handle units like "coordinate" here (which do not have plural variants)
+ if (value.getType() == ICUResourceBundle.TABLE) {
// Units that have plural variants
UResource.Table patternTableTable = value.getTable();
for (int i = 0; patternTableTable.getKeyAndValue(i, key, value); i++) {
@@ -847,6 +845,8 @@ public class MeasureFormat extends UFormat {
consumeCompoundPattern(key, value);
}
}
+ } else if (key.contentEquals("coordinate")) {
+ // special handling but we need to determine what that is
} else {
type = key.toString();
UResource.Table subtypeTable = value.getTable();
@@ -992,7 +992,12 @@ public class MeasureFormat extends UFormat {
return pattern;
}
- private String getPluralFormatter(MeasureUnit unit, FormatWidth width, int index) {
+ /**
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public String getPluralFormatter(MeasureUnit unit, FormatWidth width, int index) {
if (index != StandardPlural.OTHER_INDEX) {
String pattern = getFormatterOrNull(unit, width, index);
if (pattern != null) {
@@ -1143,6 +1148,7 @@ public class MeasureFormat extends UFormat {
suffix = pattern.substring(pos+3);
}
}
+ @Override
public String toString() {
return prefix + "; " + suffix;
}
@@ -1174,7 +1180,7 @@ public class MeasureFormat extends UFormat {
if (fieldPositionFoundIndex == -1) {
results[i] = formatMeasure(measures[i], nf, new StringBuilder(), fpos).toString();
if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) {
- fieldPositionFoundIndex = i;
+ fieldPositionFoundIndex = i;
}
} else {
results[i] = formatMeasure(measures[i], nf);
@@ -1255,7 +1261,7 @@ public class MeasureFormat extends UFormat {
// if hour-minute-second
if (startIndex == 0 && endIndex == 2) {
return formatNumeric(
- d,
+ d,
numericFormatters.getHourMinuteSecond(),
DateFormat.Field.SECOND,
hms[endIndex],
@@ -1264,7 +1270,7 @@ public class MeasureFormat extends UFormat {
// if minute-second
if (startIndex == 1 && endIndex == 2) {
return formatNumeric(
- d,
+ d,
numericFormatters.getMinuteSecond(),
DateFormat.Field.SECOND,
hms[endIndex],
@@ -1273,7 +1279,7 @@ public class MeasureFormat extends UFormat {
// if hour-minute
if (startIndex == 0 && endIndex == 1) {
return formatNumeric(
- d,
+ d,
numericFormatters.getHourMinute(),
DateFormat.Field.MINUTE,
hms[endIndex],
@@ -1376,6 +1382,7 @@ public class MeasureFormat extends UFormat {
public MeasureProxy() {
}
+ @Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeByte(0); // version
out.writeUTF(locale.toLanguageTag());
@@ -1385,6 +1392,7 @@ public class MeasureFormat extends UFormat {
out.writeObject(keyValues);
}
+ @Override
@SuppressWarnings("unchecked")
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
in.readByte(); // version.
diff --git a/android_icu4j/src/main/java/android/icu/text/MessageFormat.java b/android_icu4j/src/main/java/android/icu/text/MessageFormat.java
index de9f01922..cd4dbc1a0 100644
--- a/android_icu4j/src/main/java/android/icu/text/MessageFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/MessageFormat.java
@@ -39,7 +39,7 @@ import android.icu.impl.PatternProps;
import android.icu.impl.Utility;
import android.icu.text.MessagePattern.ArgType;
import android.icu.text.MessagePattern.Part;
-import android.icu.text.PluralRules.FixedDecimal;
+import android.icu.text.PluralRules.IFixedDecimal;
import android.icu.text.PluralRules.PluralType;
import android.icu.util.ICUUncheckedIOException;
import android.icu.util.ULocale;
@@ -140,8 +140,8 @@ import android.icu.util.ULocale.Category;
* and unquoted {curly braces} must occur in matched pairs.
* </ul>
*
- * <p>Recommendation: Use the real apostrophe (single quote) character \u2019 for
- * human-readable text, and use the ASCII apostrophe (\u0027 ' )
+ * <p>Recommendation: Use the real apostrophe (single quote) character \\u2019 for
+ * human-readable text, and use the ASCII apostrophe (\\u0027 ' )
* only in program syntax, like quoting in MessageFormat.
* See the annotations for U+0027 Apostrophe in The Unicode Standard.
*
@@ -2064,7 +2064,7 @@ public class MessageFormat extends UFormat {
assert context.number.doubleValue() == number; // argument number minus the offset
context.numberString = context.formatter.format(context.number);
if(context.formatter instanceof DecimalFormat) {
- FixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number);
+ IFixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number);
return rules.select(dec);
} else {
return rules.select(number);
diff --git a/android_icu4j/src/main/java/android/icu/text/NFSubstitution.java b/android_icu4j/src/main/java/android/icu/text/NFSubstitution.java
index 1065ba707..4aee5b928 100644
--- a/android_icu4j/src/main/java/android/icu/text/NFSubstitution.java
+++ b/android_icu4j/src/main/java/android/icu/text/NFSubstitution.java
@@ -11,6 +11,8 @@ package android.icu.text;
import java.text.ParsePosition;
+import android.icu.impl.number.FormatQuantity4;
+
//===================================================================
// NFSubstitution (abstract base class)
//===================================================================
@@ -233,7 +235,8 @@ abstract class NFSubstitution {
* @param that The substitution to compare this one to
* @return true if the two substitutions are functionally equivalent
*/
- public boolean equals(Object that) {
+ @Override
+ public boolean equals(Object that) {
// compare class and all of the fields all substitutions have
// in common
if (that == null) {
@@ -251,8 +254,9 @@ abstract class NFSubstitution {
}
return false;
}
-
- public int hashCode() {
+
+ @Override
+ public int hashCode() {
assert false : "hashCode not designed";
return 42;
}
@@ -263,7 +267,8 @@ abstract class NFSubstitution {
* not be identical to the description it was created from, but
* it'll produce the same result.
*/
- public String toString() {
+ @Override
+ public String toString() {
// use tokenChar() to get the character at the beginning and
// end of the substitution token. In between them will go
// either the name of the rule set it uses, or the pattern of
@@ -279,6 +284,8 @@ abstract class NFSubstitution {
// formatting
//-----------------------------------------------------------------------
+ private static final long MAX_INT64_IN_DOUBLE = 0x1FFFFFFFFFFFFFL;
+
/**
* Performs a mathematical operation on the number, formats it using
* either ruleSet or decimalFormat, and inserts the result into
@@ -290,15 +297,37 @@ abstract class NFSubstitution {
* position to determine exactly where to insert the new text)
*/
public void doSubstitution(long number, StringBuilder toInsertInto, int position, int recursionCount) {
- // perform a transformation on the number that is dependent
- // on the type of substitution this is, then just call its
- // rule set's format() method to format the result
- long numberToFormat = transformNumber(number);
-
if (ruleSet != null) {
+ // Perform a transformation on the number that is dependent
+ // on the type of substitution this is, then just call its
+ // rule set's format() method to format the result
+ long numberToFormat = transformNumber(number);
+
ruleSet.format(numberToFormat, toInsertInto, position + pos, recursionCount);
} else {
- toInsertInto.insert(position + pos, numberFormat.format(numberToFormat));
+ if (number <= MAX_INT64_IN_DOUBLE) {
+ // or perform the transformation on the number (preserving
+ // the result's fractional part if the formatter it set
+ // to show it), then use that formatter's format() method
+ // to format the result
+ double numberToFormat = transformNumber((double) number);
+ if (numberFormat.getMaximumFractionDigits() == 0) {
+ numberToFormat = Math.floor(numberToFormat);
+ }
+
+ toInsertInto.insert(position + pos, numberFormat.format(numberToFormat));
+ }
+ else {
+ // We have gone beyond double precision. Something has to give.
+ // We're favoring accuracy of the large number over potential rules
+ // that round like a CompactDecimalFormat, which is not a common use case.
+ //
+ // Perform a transformation on the number that is dependent
+ // on the type of substitution this is, then just call its
+ // rule set's format() method to format the result
+ long numberToFormat = transformNumber(number);
+ toInsertInto.insert(position + pos, numberFormat.format(numberToFormat));
+ }
}
}
@@ -563,7 +592,8 @@ class SameValueSubstitution extends NFSubstitution {
* Returns "number" unchanged.
* @return "number"
*/
- public long transformNumber(long number) {
+ @Override
+ public long transformNumber(long number) {
return number;
}
@@ -571,7 +601,8 @@ class SameValueSubstitution extends NFSubstitution {
* Returns "number" unchanged.
* @return "number"
*/
- public double transformNumber(double number) {
+ @Override
+ public double transformNumber(double number) {
return number;
}
@@ -588,7 +619,8 @@ class SameValueSubstitution extends NFSubstitution {
* substitution.
* @return newRuleValue
*/
- public double composeRuleValue(double newRuleValue, double oldRuleValue) {
+ @Override
+ public double composeRuleValue(double newRuleValue, double oldRuleValue) {
return newRuleValue;
}
@@ -597,7 +629,8 @@ class SameValueSubstitution extends NFSubstitution {
* @param oldUpperBound The current upper bound.
* @return oldUpperBound
*/
- public double calcUpperBound(double oldUpperBound) {
+ @Override
+ public double calcUpperBound(double oldUpperBound) {
return oldUpperBound;
}
@@ -609,7 +642,8 @@ class SameValueSubstitution extends NFSubstitution {
* The token character for a SameValueSubstitution is =.
* @return '='
*/
- char tokenChar() {
+ @Override
+ char tokenChar() {
return '=';
}
}
@@ -668,7 +702,8 @@ class MultiplierSubstitution extends NFSubstitution {
* @param radix The radix of the divisor.
* @param exponent The exponent of the divisor.
*/
- public void setDivisor(int radix, short exponent) {
+ @Override
+ public void setDivisor(int radix, short exponent) {
divisor = NFRule.power(radix, exponent);
if (divisor == 0) {
@@ -685,10 +720,11 @@ class MultiplierSubstitution extends NFSubstitution {
* @param that The other substitution
* @return true if the two substitutions are functionally equal
*/
- public boolean equals(Object that) {
+ @Override
+ public boolean equals(Object that) {
return super.equals(that) && divisor == ((MultiplierSubstitution) that).divisor;
}
-
+
//-----------------------------------------------------------------------
// formatting
//-----------------------------------------------------------------------
@@ -698,7 +734,8 @@ class MultiplierSubstitution extends NFSubstitution {
* @param number The number being formatted.
* @return "number" divided by the rule's divisor
*/
- public long transformNumber(long number) {
+ @Override
+ public long transformNumber(long number) {
return (long)Math.floor(number / divisor);
}
@@ -711,7 +748,8 @@ class MultiplierSubstitution extends NFSubstitution {
* @param number The number being formatted
* @return "number" divided by the rule's divisor
*/
- public double transformNumber(double number) {
+ @Override
+ public double transformNumber(double number) {
if (ruleSet == null) {
return number / divisor;
} else {
@@ -732,7 +770,8 @@ class MultiplierSubstitution extends NFSubstitution {
* substitution
* @return newRuleValue * divisor
*/
- public double composeRuleValue(double newRuleValue, double oldRuleValue) {
+ @Override
+ public double composeRuleValue(double newRuleValue, double oldRuleValue) {
return newRuleValue * divisor;
}
@@ -741,7 +780,8 @@ class MultiplierSubstitution extends NFSubstitution {
* @param oldUpperBound Ignored.
* @return The rule's divisor.
*/
- public double calcUpperBound(double oldUpperBound) {
+ @Override
+ public double calcUpperBound(double oldUpperBound) {
return divisor;
}
@@ -753,7 +793,8 @@ class MultiplierSubstitution extends NFSubstitution {
* The token character for a multiplier substitution is &lt;.
* @return '&lt;'
*/
- char tokenChar() {
+ @Override
+ char tokenChar() {
return '<';
}
}
@@ -835,7 +876,8 @@ class ModulusSubstitution extends NFSubstitution {
* @param radix The radix of the divisor.
* @param exponent The exponent of the divisor.
*/
- public void setDivisor(int radix, short exponent) {
+ @Override
+ public void setDivisor(int radix, short exponent) {
divisor = NFRule.power(radix, exponent);
if (divisor == 0) { // this will cause recursion
@@ -853,7 +895,8 @@ class ModulusSubstitution extends NFSubstitution {
* @param that The other substitution
* @return true if the two substitutions are functionally equivalent
*/
- public boolean equals(Object that) {
+ @Override
+ public boolean equals(Object that) {
if (super.equals(that)) {
ModulusSubstitution that2 = (ModulusSubstitution)that;
@@ -862,7 +905,7 @@ class ModulusSubstitution extends NFSubstitution {
return false;
}
}
-
+
//-----------------------------------------------------------------------
// formatting
//-----------------------------------------------------------------------
@@ -875,7 +918,8 @@ class ModulusSubstitution extends NFSubstitution {
* into
* @param position The position of the rule text in toInsertInto
*/
- public void doSubstitution(long number, StringBuilder toInsertInto, int position, int recursionCount) {
+ @Override
+ public void doSubstitution(long number, StringBuilder toInsertInto, int position, int recursionCount) {
// if this isn't a >>> substitution, just use the inherited version
// of this function (which uses either a rule set or a DecimalFormat
// to format its substitution value)
@@ -898,7 +942,8 @@ class ModulusSubstitution extends NFSubstitution {
* into
* @param position The position of the rule text in toInsertInto
*/
- public void doSubstitution(double number, StringBuilder toInsertInto, int position, int recursionCount) {
+ @Override
+ public void doSubstitution(double number, StringBuilder toInsertInto, int position, int recursionCount) {
// if this isn't a >>> substitution, just use the inherited version
// of this function (which uses either a rule set or a DecimalFormat
// to format its substitution value)
@@ -920,7 +965,8 @@ class ModulusSubstitution extends NFSubstitution {
* @param number The number being formatted
* @return "number" mod divisor
*/
- public long transformNumber(long number) {
+ @Override
+ public long transformNumber(long number) {
return number % divisor;
}
@@ -930,7 +976,8 @@ class ModulusSubstitution extends NFSubstitution {
* @param number The number being formatted
* @return "number" mod divisor
*/
- public double transformNumber(double number) {
+ @Override
+ public double transformNumber(double number) {
return Math.floor(number % divisor);
}
@@ -947,7 +994,8 @@ class ModulusSubstitution extends NFSubstitution {
* @param baseValue The partial parse result prior to calling this
* routine.
*/
- public Number doParse(String text, ParsePosition parsePosition, double baseValue,
+ @Override
+ public Number doParse(String text, ParsePosition parsePosition, double baseValue,
double upperBound, boolean lenientParse) {
// if this isn't a >>> substitution, we can just use the
// inherited parse() routine to do the parsing
@@ -987,7 +1035,8 @@ class ModulusSubstitution extends NFSubstitution {
* @param oldRuleValue The base value of the rule containing the
* substitution
*/
- public double composeRuleValue(double newRuleValue, double oldRuleValue) {
+ @Override
+ public double composeRuleValue(double newRuleValue, double oldRuleValue) {
return (oldRuleValue - (oldRuleValue % divisor)) + newRuleValue;
}
@@ -996,7 +1045,8 @@ class ModulusSubstitution extends NFSubstitution {
* @param oldUpperBound Ignored
* @return The owning rule's divisor
*/
- public double calcUpperBound(double oldUpperBound) {
+ @Override
+ public double calcUpperBound(double oldUpperBound) {
return divisor;
}
@@ -1008,7 +1058,8 @@ class ModulusSubstitution extends NFSubstitution {
* Returns true. This _is_ a ModulusSubstitution.
* @return true
*/
- public boolean isModulusSubstitution() {
+ @Override
+ public boolean isModulusSubstitution() {
return true;
}
@@ -1016,7 +1067,8 @@ class ModulusSubstitution extends NFSubstitution {
* The token character of a ModulusSubstitution is &gt;.
* @return '&gt;'
*/
- char tokenChar() {
+ @Override
+ char tokenChar() {
return '>';
}
}
@@ -1054,7 +1106,8 @@ class IntegralPartSubstitution extends NFSubstitution {
* @param number The number being formatted
* @return "number" unchanged
*/
- public long transformNumber(long number) {
+ @Override
+ public long transformNumber(long number) {
return number;
}
@@ -1063,7 +1116,8 @@ class IntegralPartSubstitution extends NFSubstitution {
* @param number The integral part of the number being formatted
* @return floor(number)
*/
- public double transformNumber(double number) {
+ @Override
+ public double transformNumber(double number) {
return Math.floor(number);
}
@@ -1081,7 +1135,8 @@ class IntegralPartSubstitution extends NFSubstitution {
* calling this function
* @return oldRuleValue + newRuleValue
*/
- public double composeRuleValue(double newRuleValue, double oldRuleValue) {
+ @Override
+ public double composeRuleValue(double newRuleValue, double oldRuleValue) {
return newRuleValue + oldRuleValue;
}
@@ -1091,7 +1146,8 @@ class IntegralPartSubstitution extends NFSubstitution {
* @param oldUpperBound Ignored
* @return Double.MAX_VALUE
*/
- public double calcUpperBound(double oldUpperBound) {
+ @Override
+ public double calcUpperBound(double oldUpperBound) {
return Double.MAX_VALUE;
}
@@ -1103,7 +1159,8 @@ class IntegralPartSubstitution extends NFSubstitution {
* An IntegralPartSubstitution's token character is &lt;
* @return '&lt;'
*/
- char tokenChar() {
+ @Override
+ char tokenChar() {
return '<';
}
}
@@ -1170,7 +1227,8 @@ class FractionalPartSubstitution extends NFSubstitution {
* @param position The position of the owning rule's rule text in
* toInsertInto
*/
- public void doSubstitution(double number, StringBuilder toInsertInto, int position, int recursionCount) {
+ @Override
+ public void doSubstitution(double number, StringBuilder toInsertInto, int position, int recursionCount) {
if (!byDigits) {
// if we're not in "byDigits" mode, just use the inherited
// doSubstitution() routine
@@ -1184,27 +1242,18 @@ class FractionalPartSubstitution extends NFSubstitution {
// (this is slower, but more accurate, than doing it from the
// other end)
- // just print to string and then use that
- DigitList dl = new DigitList();
- dl.set(number, 20, true);
+ FormatQuantity4 fq = new FormatQuantity4(number);
+ fq.roundToInfinity(); // ensure doubles are resolved using slow path
boolean pad = false;
- while (dl.count > Math.max(0, dl.decimalAt)) {
- if (pad && useSpaces) {
- toInsertInto.insert(position + pos, ' ');
- } else {
- pad = true;
- }
- ruleSet.format(dl.digits[--dl.count] - '0', toInsertInto, position + pos, recursionCount);
- }
- while (dl.decimalAt < 0) {
+ int mag = fq.getLowerDisplayMagnitude();
+ while (mag < 0) {
if (pad && useSpaces) {
toInsertInto.insert(position + pos, ' ');
} else {
pad = true;
}
- ruleSet.format(0, toInsertInto, position + pos, recursionCount);
- ++dl.decimalAt;
+ ruleSet.format(fq.getDigit(mag++), toInsertInto, position + pos, recursionCount);
}
}
}
@@ -1215,7 +1264,8 @@ class FractionalPartSubstitution extends NFSubstitution {
* @param number The number being formatted
* @return 0
*/
- public long transformNumber(long number) {
+ @Override
+ public long transformNumber(long number) {
return 0;
}
@@ -1224,7 +1274,8 @@ class FractionalPartSubstitution extends NFSubstitution {
* @param number The number being formatted.
* @return number - floor(number)
*/
- public double transformNumber(double number) {
+ @Override
+ public double transformNumber(double number) {
return number - Math.floor(number);
}
@@ -1248,7 +1299,8 @@ class FractionalPartSubstitution extends NFSubstitution {
* result; otherwise new Long(0). The result is either a Long or
* a Double.
*/
- public Number doParse(String text, ParsePosition parsePosition, double baseValue,
+ @Override
+ public Number doParse(String text, ParsePosition parsePosition, double baseValue,
double upperBound, boolean lenientParse) {
// if we're not in byDigits mode, we can just use the inherited
// doParse()
@@ -1265,7 +1317,8 @@ class FractionalPartSubstitution extends NFSubstitution {
double result;
int digit;
- DigitList dl = new DigitList();
+ FormatQuantity4 fq = new FormatQuantity4();
+ int leadingZeros = 0;
while (workText.length() > 0 && workPos.getIndex() != 0) {
workPos.setIndex(0);
digit = ruleSet.parse(workText, workPos, 10).intValue();
@@ -1277,7 +1330,12 @@ class FractionalPartSubstitution extends NFSubstitution {
}
if (workPos.getIndex() != 0) {
- dl.append('0'+digit);
+ if (digit == 0) {
+ leadingZeros++;
+ } else {
+ fq.appendDigit((byte) digit, leadingZeros, false);
+ leadingZeros = 0;
+ }
parsePosition.setIndex(parsePosition.getIndex() + workPos.getIndex());
workText = workText.substring(workPos.getIndex());
@@ -1287,7 +1345,7 @@ class FractionalPartSubstitution extends NFSubstitution {
}
}
}
- result = dl.count == 0 ? 0 : dl.getDouble();
+ result = fq.toDouble();
result = composeRuleValue(result, baseValue);
return new Double(result);
@@ -1301,14 +1359,16 @@ class FractionalPartSubstitution extends NFSubstitution {
* this function
* @return newRuleValue + oldRuleValue
*/
- public double composeRuleValue(double newRuleValue, double oldRuleValue) {
+ @Override
+ public double composeRuleValue(double newRuleValue, double oldRuleValue) {
return newRuleValue + oldRuleValue;
}
/**
* Not used.
*/
- public double calcUpperBound(double oldUpperBound) {
+ @Override
+ public double calcUpperBound(double oldUpperBound) {
return 0; // this value is ignored
}
@@ -1320,7 +1380,8 @@ class FractionalPartSubstitution extends NFSubstitution {
* The token character for a FractionalPartSubstitution is &gt;.
* @return '&gt;'
*/
- char tokenChar() {
+ @Override
+ char tokenChar() {
return '>';
}
}
@@ -1357,7 +1418,8 @@ class AbsoluteValueSubstitution extends NFSubstitution {
* @param number The number being formatted.
* @return abs(number)
*/
- public long transformNumber(long number) {
+ @Override
+ public long transformNumber(long number) {
return Math.abs(number);
}
@@ -1366,7 +1428,8 @@ class AbsoluteValueSubstitution extends NFSubstitution {
* @param number The number being formatted.
* @return abs(number)
*/
- public double transformNumber(double number) {
+ @Override
+ public double transformNumber(double number) {
return Math.abs(number);
}
@@ -1382,7 +1445,8 @@ class AbsoluteValueSubstitution extends NFSubstitution {
* this function
* @return -newRuleValue
*/
- public double composeRuleValue(double newRuleValue, double oldRuleValue) {
+ @Override
+ public double composeRuleValue(double newRuleValue, double oldRuleValue) {
return -newRuleValue;
}
@@ -1391,7 +1455,8 @@ class AbsoluteValueSubstitution extends NFSubstitution {
* @param oldUpperBound Ignored.
* @return Double.MAX_VALUE
*/
- public double calcUpperBound(double oldUpperBound) {
+ @Override
+ public double calcUpperBound(double oldUpperBound) {
return Double.MAX_VALUE;
}
@@ -1403,7 +1468,8 @@ class AbsoluteValueSubstitution extends NFSubstitution {
* The token character for an AbsoluteValueSubstitution is &gt;
* @return '&gt;'
*/
- char tokenChar() {
+ @Override
+ char tokenChar() {
return '>';
}
}
@@ -1453,13 +1519,13 @@ class NumeratorSubstitution extends NFSubstitution {
// Rather than keeping a backpointer to the rule, we copy its
// base value here
this.denominator = denominator;
-
+
this.withZeros = description.endsWith("<<");
}
static String fixdesc(String description) {
- return description.endsWith("<<")
- ? description.substring(0,description.length()-1)
+ return description.endsWith("<<")
+ ? description.substring(0,description.length()-1)
: description;
}
@@ -1472,7 +1538,8 @@ class NumeratorSubstitution extends NFSubstitution {
* @param that The other NumeratorSubstitution
* @return true if the two objects are functionally equivalent
*/
- public boolean equals(Object that) {
+ @Override
+ public boolean equals(Object that) {
if (super.equals(that)) {
NumeratorSubstitution that2 = (NumeratorSubstitution)that;
return denominator == that2.denominator && withZeros == that2.withZeros;
@@ -1480,7 +1547,7 @@ class NumeratorSubstitution extends NFSubstitution {
return false;
}
}
-
+
//-----------------------------------------------------------------------
// formatting
//-----------------------------------------------------------------------
@@ -1495,7 +1562,8 @@ class NumeratorSubstitution extends NFSubstitution {
* rule text begins (this value is added to this substitution's
* position to determine exactly where to insert the new text)
*/
- public void doSubstitution(double number, StringBuilder toInsertInto, int position, int recursionCount) {
+ @Override
+ public void doSubstitution(double number, StringBuilder toInsertInto, int position, int recursionCount) {
// perform a transformation on the number being formatted that
// is dependent on the type of substitution this is
//String s = toInsertInto.toString();
@@ -1534,7 +1602,8 @@ class NumeratorSubstitution extends NFSubstitution {
* @param number The number being formatted
* @return number * denominator
*/
- public long transformNumber(long number) {
+ @Override
+ public long transformNumber(long number) {
return Math.round(number * denominator);
}
@@ -1543,7 +1612,8 @@ class NumeratorSubstitution extends NFSubstitution {
* @param number The number being formatted
* @return number * denominator
*/
- public double transformNumber(double number) {
+ @Override
+ public double transformNumber(double number) {
return Math.round(number * denominator);
}
@@ -1555,7 +1625,8 @@ class NumeratorSubstitution extends NFSubstitution {
* Dispatches to the inherited version of this function, but makes
* sure that lenientParse is off.
*/
- public Number doParse(String text, ParsePosition parsePosition, double baseValue,
+ @Override
+ public Number doParse(String text, ParsePosition parsePosition, double baseValue,
double upperBound, boolean lenientParse) {
// we don't have to do anything special to do the parsing here,
// but we have to turn lenient parsing off-- if we leave it on,
@@ -1597,7 +1668,7 @@ class NumeratorSubstitution extends NFSubstitution {
if (withZeros) {
// any base value will do in this case. is there a way to
// force this to not bother trying all the base values?
-
+
// compute the 'effective' base and prescale the value down
long n = result.longValue();
long d = 1;
@@ -1623,7 +1694,8 @@ class NumeratorSubstitution extends NFSubstitution {
* @param oldRuleValue The owning rule's base value
* @return newRuleValue / oldRuleValue
*/
- public double composeRuleValue(double newRuleValue, double oldRuleValue) {
+ @Override
+ public double composeRuleValue(double newRuleValue, double oldRuleValue) {
return newRuleValue / oldRuleValue;
}
@@ -1632,7 +1704,8 @@ class NumeratorSubstitution extends NFSubstitution {
* @param oldUpperBound Ignored
* @return The base value of the rule owning this substitution
*/
- public double calcUpperBound(double oldUpperBound) {
+ @Override
+ public double calcUpperBound(double oldUpperBound) {
return denominator;
}
@@ -1644,7 +1717,8 @@ class NumeratorSubstitution extends NFSubstitution {
* The token character for a NumeratorSubstitution is &lt;
* @return '&lt;'
*/
- char tokenChar() {
+ @Override
+ char tokenChar() {
return '<';
}
}
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 fec326fb8..6a931c431 100644
--- a/android_icu4j/src/main/java/android/icu/text/NumberFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/NumberFormat.java
@@ -387,13 +387,19 @@ public abstract class NumberFormat extends UFormat {
/**
* Returns a Long if possible (e.g., within the range [Long.MIN_VALUE,
- * Long.MAX_VALUE] and with no decimals), otherwise a Double.
- * If IntegerOnly is set, will stop at a decimal
+ * Long.MAX_VALUE] and with no decimals); otherwise, returns another type,
+ * such as a BigDecimal, BigInteger, or Double. The return type is not
+ * guaranteed other than for the Long case.
+ *
+ * <p>If IntegerOnly is set, will stop at a decimal
* point (or equivalent; e.g., for rational numbers "1 2/3", will stop
* after the 1).
- * Does not throw an exception; if no object can be parsed, index is
+ *
+ * <p>Does not throw an exception; if no object can be parsed, index is
* unchanged!
+ *
* @see #isParseIntegerOnly
+ * @see DecimalFormat#setParseBigDecimal
* @see java.text.Format#parseObject(String, ParsePosition)
*/
public abstract Number parse(String text, ParsePosition parsePosition);
@@ -450,6 +456,7 @@ public abstract class NumberFormat extends UFormat {
* would stop at the "." character. The decimal separator accepted
* by the parse operation is locale-dependent and determined by the
* subclass.
+ *
* @return true if this will parse integers only
*/
public boolean isParseIntegerOnly() {
@@ -457,7 +464,13 @@ public abstract class NumberFormat extends UFormat {
}
/**
- * Sets whether or not numbers should be parsed as integers only.
+ * Sets whether to ignore the fraction part of a number when parsing
+ * (defaults to false). If a string contains a decimal point, parsing will stop before the decimal
+ * point. Note that determining whether a character is a decimal point depends on the locale.
+ *
+ * <p>For example, in <em>en-US</em>, parsing the string "123.45" will return the number 123 and
+ * parse position 3.
+ *
* @param value true if this should parse integers only
* @see #isParseIntegerOnly
*/
@@ -466,8 +479,13 @@ public abstract class NumberFormat extends UFormat {
}
/**
- * <strong>[icu]</strong> Sets whether strict parsing is in effect. When this is true, the
- * following conditions cause a parse failure (examples use the pattern "#,##0.#"):<ul>
+ * <strong>[icu]</strong> Sets whether strict parsing is in effect. When this is true, the string
+ * is required to be a stronger match to the pattern than when lenient parsing is in
+ * effect. More specifically, the following conditions cause a parse failure relative
+ * to lenient mode (examples use the pattern "#,##0.#"):<ul>
+ * <li>The presence and position of special symbols, including currency, must match the
+ * pattern.<br>
+ * '+123' fails (there is no plus sign in the pattern)</li>
* <li>Leading or doubled grouping separators<br>
* ',123' and '1,,234" fail</li>
* <li>Groups of incorrect length when grouping is used<br>
@@ -966,7 +984,7 @@ public abstract class NumberFormat extends UFormat {
// ===== End of factory stuff =====
/**
- * Overrides hashCode.
+ * {@inheritDoc}
*/
@Override
public int hashCode() {
@@ -1035,8 +1053,13 @@ public abstract class NumberFormat extends UFormat {
/**
* Returns the maximum number of digits allowed in the integer portion of a
* number. The default value is 40, which subclasses can override.
- * When formatting, the exact behavior when this value is exceeded is
- * subclass-specific. When parsing, this has no effect.
+ *
+ * When formatting, if the number of digits exceeds this value, the highest-
+ * significance digits are truncated until the limit is reached, in accordance
+ * with UTS#35.
+ *
+ * This setting has no effect on parsing.
+ *
* @return the maximum number of integer digits
* @see #setMaximumIntegerDigits
*/
@@ -1325,10 +1348,12 @@ public abstract class NumberFormat extends UFormat {
f.setDecimalSeparatorAlwaysShown(false);
f.setParseIntegerOnly(true);
}
-
if (choice == CASHCURRENCYSTYLE) {
f.setCurrencyUsage(CurrencyUsage.CASH);
}
+ if (choice == PLURALCURRENCYSTYLE) {
+ f.setCurrencyPluralInfo(CurrencyPluralInfo.getInstance(desiredLocale));
+ }
format = f;
}
// TODO: the actual locale of the *pattern* may differ from that
@@ -1361,6 +1386,19 @@ public abstract class NumberFormat extends UFormat {
* @return the pattern
*/
protected static String getPattern(ULocale forLocale, int choice) {
+ return getPatternForStyle(forLocale, choice);
+ }
+
+ /**
+ * Returns the pattern for the provided locale and choice.
+ * @param forLocale the locale of the data.
+ * @param choice the pattern format.
+ * @return the pattern
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public static String getPatternForStyle(ULocale forLocale, int choice) {
/* for ISOCURRENCYSTYLE and PLURALCURRENCYSTYLE,
* the pattern is the same as the pattern of CURRENCYSTYLE
* but by replacing the single currency sign with
@@ -1370,6 +1408,7 @@ public abstract class NumberFormat extends UFormat {
switch (choice) {
case NUMBERSTYLE:
case INTEGERSTYLE:
+ case PLURALCURRENCYSTYLE:
patternKey = "decimalFormat";
break;
case CURRENCYSTYLE:
@@ -1379,7 +1418,6 @@ public abstract class NumberFormat extends UFormat {
break;
case CASHCURRENCYSTYLE:
case ISOCURRENCYSTYLE:
- case PLURALCURRENCYSTYLE:
case STANDARDCURRENCYSTYLE:
patternKey = "currencyFormat";
break;
diff --git a/android_icu4j/src/main/java/android/icu/text/PluralFormat.java b/android_icu4j/src/main/java/android/icu/text/PluralFormat.java
index 1ba1a4e33..68bea7f89 100644
--- a/android_icu4j/src/main/java/android/icu/text/PluralFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/PluralFormat.java
@@ -19,6 +19,8 @@ import java.util.Map;
import android.icu.impl.Utility;
import android.icu.text.PluralRules.FixedDecimal;
+import android.icu.text.PluralRules.IFixedDecimal;
+import android.icu.text.PluralRules.Operand;
import android.icu.text.PluralRules.PluralType;
import android.icu.util.ULocale;
import android.icu.util.ULocale.Category;
@@ -539,8 +541,8 @@ public class PluralFormat extends UFormat {
private final class PluralSelectorAdapter implements PluralSelector {
@Override
public String select(Object context, double number) {
- FixedDecimal dec = (FixedDecimal) context;
- assert dec.source == (dec.isNegative ? -number : number);
+ IFixedDecimal dec = (IFixedDecimal) context;
+ assert dec.getPluralOperand(Operand.n) == Math.abs(number);
return pluralRules.select(dec);
}
}
@@ -601,7 +603,7 @@ public class PluralFormat extends UFormat {
} else {
numberString = numberFormat.format(numberMinusOffset);
}
- FixedDecimal dec;
+ IFixedDecimal dec;
if(numberFormat instanceof DecimalFormat) {
dec = ((DecimalFormat) numberFormat).getFixedDecimal(numberMinusOffset);
} else {
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 c6855bc2a..2f9f1bedc 100644
--- a/android_icu4j/src/main/java/android/icu/text/PluralRules.java
+++ b/android_icu4j/src/main/java/android/icu/text/PluralRules.java
@@ -355,7 +355,7 @@ public class PluralRules implements Serializable {
private static final long serialVersionUID = 9163464945387899416L;
@Override
- public boolean isFulfilled(FixedDecimal n) {
+ public boolean isFulfilled(IFixedDecimal n) {
return true;
}
@@ -408,24 +408,120 @@ public class PluralRules implements Serializable {
*/
public static final PluralRules DEFAULT = new PluralRules(new RuleList().addRule(DEFAULT_RULE));
- private enum Operand {
+ /**
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public static enum Operand {
+ /**
+ * The double value of the entire number.
+ *
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
n,
+
+ /**
+ * The integer value, with the fraction digits truncated off.
+ *
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
i,
+
+ /**
+ * All visible fraction digits as an integer, including trailing zeros.
+ *
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
f,
+
+ /**
+ * Visible fraction digits as an integer, not including trailing zeros.
+ *
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
t,
+
+ /**
+ * Number of visible fraction digits.
+ *
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
v,
+
+ /**
+ * Number of visible fraction digits, not including trailing zeros.
+ *
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
w,
- /* deprecated */
+
+ /**
+ * 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.
+ * That is, using "j" instead of "i" is like implicitly adding "v is 0".
+ *
+ * <p>For example, "j is 3" is equivalent to "i is 3 and v is 0": it matches
+ * "3" but not "3.1" or "3.0".
+ *
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
j;
}
/**
* @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public static interface IFixedDecimal {
+ /**
+ * Returns the value corresponding to the specified operand (n, i, f, t, v, or w).
+ * If the operand is 'n', returns a double; otherwise, returns an integer.
+ *
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public double getPluralOperand(Operand operand);
+
+ /**
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public boolean isNaN();
+
+ /**
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public boolean isInfinite();
+ }
+
+ /**
+ * @deprecated This API is ICU internal only.
* @hide original deprecated declaration
* @hide draft / provisional / internal are hidden on Android
*/
@Deprecated
- public static class FixedDecimal extends Number implements Comparable<FixedDecimal> {
+ public static class FixedDecimal extends Number implements Comparable<FixedDecimal>, IFixedDecimal {
private static final long serialVersionUID = -4756200506571685661L;
/**
* @deprecated This API is ICU internal only.
@@ -743,19 +839,22 @@ public class PluralRules implements Serializable {
}
/**
+ * {@inheritDoc}
+ *
* @deprecated This API is ICU internal only.
- * @hide original deprecated declaration
* @hide draft / provisional / internal are hidden on Android
*/
+ @Override
@Deprecated
- public double get(Operand operand) {
+ public double getPluralOperand(Operand operand) {
switch(operand) {
- default: return source;
+ case n: return source;
case i: return integerValue;
case f: return decimalDigits;
case t: return decimalDigitsWithoutTrailingZeros;
case v: return visibleDecimalDigitCount;
case w: return visibleDecimalDigitCountWithoutTrailingZeros;
+ default: return source;
}
}
@@ -913,6 +1012,30 @@ public class PluralRules implements Serializable {
) throws IOException, ClassNotFoundException {
throw new NotSerializableException();
}
+
+ /**
+ * {@inheritDoc}
+ *
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ @Override
+ public boolean isNaN() {
+ return Double.isNaN(source);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ @Override
+ public boolean isInfinite() {
+ return Double.isInfinite(source);
+ }
}
/**
@@ -1152,7 +1275,7 @@ public class PluralRules implements Serializable {
* Returns true if the number fulfills the constraint.
* @param n the number to test, >= 0.
*/
- boolean isFulfilled(FixedDecimal n);
+ boolean isFulfilled(IFixedDecimal n);
/*
* Returns false if an unlimited number of values fulfills the
@@ -1509,10 +1632,10 @@ public class PluralRules implements Serializable {
}
@Override
- public boolean isFulfilled(FixedDecimal number) {
- double n = number.get(operand);
+ public boolean isFulfilled(IFixedDecimal number) {
+ double n = number.getPluralOperand(operand);
if ((integersOnly && (n - (long)n) != 0.0
- || operand == Operand.j && number.visibleDecimalDigitCount != 0)) {
+ || operand == Operand.j && number.getPluralOperand(Operand.v) != 0)) {
return !inRange;
}
if (mod != 0) {
@@ -1612,7 +1735,7 @@ public class PluralRules implements Serializable {
}
@Override
- public boolean isFulfilled(FixedDecimal n) {
+ public boolean isFulfilled(IFixedDecimal n) {
return a.isFulfilled(n)
&& b.isFulfilled(n);
}
@@ -1640,7 +1763,7 @@ public class PluralRules implements Serializable {
}
@Override
- public boolean isFulfilled(FixedDecimal n) {
+ public boolean isFulfilled(IFixedDecimal n) {
return a.isFulfilled(n)
|| b.isFulfilled(n);
}
@@ -1691,7 +1814,7 @@ public class PluralRules implements Serializable {
return keyword;
}
- public boolean appliesTo(FixedDecimal n) {
+ public boolean appliesTo(IFixedDecimal n) {
return constraint.isFulfilled(n);
}
@@ -1754,7 +1877,7 @@ public class PluralRules implements Serializable {
return this;
}
- private Rule selectRule(FixedDecimal n) {
+ private Rule selectRule(IFixedDecimal n) {
for (Rule rule : rules) {
if (rule.appliesTo(n)) {
return rule;
@@ -1763,8 +1886,8 @@ public class PluralRules implements Serializable {
return null;
}
- public String select(FixedDecimal n) {
- if (Double.isInfinite(n.source) || Double.isNaN(n.source)) {
+ public String select(IFixedDecimal n) {
+ if (n.isInfinite() || n.isNaN()) {
return KEYWORD_OTHER;
}
Rule r = selectRule(n);
@@ -1826,7 +1949,7 @@ public class PluralRules implements Serializable {
return null;
}
- public boolean select(FixedDecimal sample, String keyword) {
+ public boolean select(IFixedDecimal sample, String keyword) {
for (Rule rule : rules) {
if (rule.getKeyword().equals(keyword) && rule.appliesTo(sample)) {
return true;
@@ -1846,9 +1969,9 @@ public class PluralRules implements Serializable {
}
@SuppressWarnings("unused")
- private boolean addConditional(Set<FixedDecimal> toAddTo, Set<FixedDecimal> others, double trial) {
+ private boolean addConditional(Set<IFixedDecimal> toAddTo, Set<IFixedDecimal> others, double trial) {
boolean added;
- FixedDecimal toAdd = new FixedDecimal(trial);
+ IFixedDecimal toAdd = new FixedDecimal(trial);
if (!toAddTo.contains(toAdd) && !others.contains(toAdd)) {
others.add(toAdd);
added = true;
@@ -2009,11 +2132,10 @@ public class PluralRules implements Serializable {
* @param number The number information for which the rule has to be determined.
* @return The keyword of the selected rule.
* @deprecated This API is ICU internal only.
- * @hide original deprecated declaration
* @hide draft / provisional / internal are hidden on Android
*/
@Deprecated
- public String select(FixedDecimal number) {
+ public String select(IFixedDecimal number) {
return rules.select(number);
}
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 ad61cc040..8ac76e86d 100644
--- a/android_icu4j/src/main/java/android/icu/text/RelativeDateTimeFormatter.java
+++ b/android_icu4j/src/main/java/android/icu/text/RelativeDateTimeFormatter.java
@@ -259,97 +259,81 @@ public final class RelativeDateTimeFormatter {
/**
* Represents the unit for formatting a relative date. e.g "in 5 days"
* or "next year"
- * @hide draft / provisional / internal are hidden on Android
*/
public static enum RelativeDateTimeUnit {
/**
* Specifies that relative unit is year, e.g. "last year",
* "in 5 years".
- * @hide draft / provisional / internal are hidden on Android
*/
YEAR,
/**
* Specifies that relative unit is quarter, e.g. "last quarter",
* "in 5 quarters".
- * @hide draft / provisional / internal are hidden on Android
*/
QUARTER,
/**
* Specifies that relative unit is month, e.g. "last month",
* "in 5 months".
- * @hide draft / provisional / internal are hidden on Android
*/
MONTH,
/**
* Specifies that relative unit is week, e.g. "last week",
* "in 5 weeks".
- * @hide draft / provisional / internal are hidden on Android
*/
WEEK,
/**
* Specifies that relative unit is day, e.g. "yesterday",
* "in 5 days".
- * @hide draft / provisional / internal are hidden on Android
*/
DAY,
/**
* Specifies that relative unit is hour, e.g. "1 hour ago",
* "in 5 hours".
- * @hide draft / provisional / internal are hidden on Android
*/
HOUR,
/**
* Specifies that relative unit is minute, e.g. "1 minute ago",
* "in 5 minutes".
- * @hide draft / provisional / internal are hidden on Android
*/
MINUTE,
/**
* Specifies that relative unit is second, e.g. "1 second ago",
* "in 5 seconds".
- * @hide draft / provisional / internal are hidden on Android
*/
SECOND,
/**
* Specifies that relative unit is Sunday, e.g. "last Sunday",
* "this Sunday", "next Sunday", "in 5 Sundays".
- * @hide draft / provisional / internal are hidden on Android
*/
SUNDAY,
/**
* Specifies that relative unit is Monday, e.g. "last Monday",
* "this Monday", "next Monday", "in 5 Mondays".
- * @hide draft / provisional / internal are hidden on Android
*/
MONDAY,
/**
* Specifies that relative unit is Tuesday, e.g. "last Tuesday",
* "this Tuesday", "next Tuesday", "in 5 Tuesdays".
- * @hide draft / provisional / internal are hidden on Android
*/
TUESDAY,
/**
* Specifies that relative unit is Wednesday, e.g. "last Wednesday",
* "this Wednesday", "next Wednesday", "in 5 Wednesdays".
- * @hide draft / provisional / internal are hidden on Android
*/
WEDNESDAY,
/**
* Specifies that relative unit is Thursday, e.g. "last Thursday",
* "this Thursday", "next Thursday", "in 5 Thursdays".
- * @hide draft / provisional / internal are hidden on Android
*/
THURSDAY,
/**
* Specifies that relative unit is Friday, e.g. "last Friday",
* "this Friday", "next Friday", "in 5 Fridays".
- * @hide draft / provisional / internal are hidden on Android
*/
FRIDAY,
/**
* Specifies that relative unit is Saturday, e.g. "last Saturday",
* "this Saturday", "next Saturday", "in 5 Saturdays".
- * @hide draft / provisional / internal are hidden on Android
*/
SATURDAY,
}
@@ -488,7 +472,6 @@ 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
*/
public String formatNumeric(double offset, RelativeDateTimeUnit unit) {
// TODO:
@@ -567,7 +550,6 @@ 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
*/
public String format(double offset, RelativeDateTimeUnit unit) {
// TODO:
diff --git a/android_icu4j/src/main/java/android/icu/text/Replaceable.java b/android_icu4j/src/main/java/android/icu/text/Replaceable.java
index 04c95aa65..fe82c7779 100644
--- a/android_icu4j/src/main/java/android/icu/text/Replaceable.java
+++ b/android_icu4j/src/main/java/android/icu/text/Replaceable.java
@@ -49,8 +49,6 @@ package android.icu.text;
* </li>
* </ul>
* If this is not the behavior, the subclass should document any differences.
- *
- * <p>Copyright &copy; IBM Corporation 1999. All rights reserved.
*
* @author Alan Liu
*/
@@ -58,7 +56,7 @@ public interface Replaceable {
/**
* Returns the number of 16-bit code units in the text.
* @return number of 16-bit code units in text
- */
+ */
int length();
/**
@@ -157,7 +155,7 @@ public interface Replaceable {
* <pre> char[] text = new char[limit - start];
* getChars(start, limit, text, 0);
* replace(dest, dest, text, 0, limit - start);</pre>
- *
+ *
* @param start the beginning index, inclusive; <code>0 &lt;= start &lt;=
* limit</code>.
* @param limit the ending index, exclusive; <code>start &lt;= limit &lt;=
@@ -168,7 +166,7 @@ public interface Replaceable {
* dest &gt;= limit</code>.
*/
void copy(int start, int limit, int dest);
-
+
/**R
* Returns true if this object contains metadata. If a
* Replaceable object has metadata, calls to the Replaceable API
diff --git a/android_icu4j/src/main/java/android/icu/text/ReplaceableString.java b/android_icu4j/src/main/java/android/icu/text/ReplaceableString.java
index c4067e207..dbf56148c 100644
--- a/android_icu4j/src/main/java/android/icu/text/ReplaceableString.java
+++ b/android_icu4j/src/main/java/android/icu/text/ReplaceableString.java
@@ -17,8 +17,6 @@ package android.icu.text;
* intended for general use. Most clients will need to implement
* {@link Replaceable} in their text representation class.
*
- * <p>Copyright &copy; IBM Corporation 1999. All rights reserved.
- *
* @see Replaceable
* @author Alan Liu
* @hide Only a subset of ICU is exposed in Android
@@ -57,6 +55,7 @@ public class ReplaceableString implements Replaceable {
* Return the contents of this object as a <code>String</code>.
* @return string contents of this object
*/
+ @Override
public String toString() {
return buf.toString();
}
@@ -71,7 +70,8 @@ public class ReplaceableString implements Replaceable {
/**
* Return the number of characters contained in this object.
* <code>Replaceable</code> API.
- */
+ */
+ @Override
public int length() {
return buf.length();
}
@@ -82,6 +82,7 @@ public class ReplaceableString implements Replaceable {
* @param offset offset into the contents, from 0 to
* <code>length()</code> - 1
*/
+ @Override
public char charAt(int offset) {
return buf.charAt(offset);
}
@@ -96,6 +97,7 @@ public class ReplaceableString implements Replaceable {
* inclusive
* @return 32-bit code point of text at given offset
*/
+ @Override
public int char32At(int offset) {
return UTF16.charAt(buf, offset);
}
@@ -117,6 +119,7 @@ public class ReplaceableString implements Replaceable {
* @param dst the destination array.
* @param dstStart the start offset in the destination array.
*/
+ @Override
public void getChars(int srcStart, int srcLimit, char dst[], int dstStart) {
if (srcStart != srcLimit) {
buf.getChars(srcStart, srcLimit, dst, dstStart);
@@ -133,6 +136,7 @@ public class ReplaceableString implements Replaceable {
* @param text new text to replace characters <code>start</code> to
* <code>limit - 1</code>
*/
+ @Override
public void replace(int start, int limit, String text) {
buf.replace(start, limit, text);
}
@@ -149,6 +153,7 @@ public class ReplaceableString implements Replaceable {
* inclusive; <code>0 &lt;= start &lt;= limit</code>.
* @param charsLen the number of characters of <code>chars</code>.
*/
+ @Override
public void replace(int start, int limit, char[] chars,
int charsStart, int charsLen) {
buf.delete(start, limit);
@@ -159,7 +164,7 @@ public class ReplaceableString implements Replaceable {
* Copy a substring of this object, retaining attribute (out-of-band)
* information. This method is used to duplicate or reorder substrings.
* The destination index must not overlap the source range.
- *
+ *
* @param start the beginning index, inclusive; <code>0 &lt;= start &lt;=
* limit</code>.
* @param limit the ending index, exclusive; <code>start &lt;= limit &lt;=
@@ -169,6 +174,7 @@ public class ReplaceableString implements Replaceable {
* Implementations of this method may assume that <code>dest &lt;= start ||
* dest &gt;= limit</code>.
*/
+ @Override
public void copy(int start, int limit, int dest) {
if (start == limit && start >= 0 && start <= buf.length()) {
return;
@@ -177,10 +183,11 @@ public class ReplaceableString implements Replaceable {
getChars(start, limit, text, 0);
replace(dest, dest, text, 0, limit - start);
}
-
+
/**
* Implements Replaceable
*/
+ @Override
public boolean hasMetaData() {
return false;
}
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 d333e476a..9dfe219b9 100644
--- a/android_icu4j/src/main/java/android/icu/text/RuleBasedBreakIterator.java
+++ b/android_icu4j/src/main/java/android/icu/text/RuleBasedBreakIterator.java
@@ -21,7 +21,8 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.text.CharacterIterator;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.ArrayList;
+import java.util.List;
import android.icu.impl.Assert;
import android.icu.impl.CharTrie;
@@ -49,7 +50,9 @@ public class RuleBasedBreakIterator extends BreakIterator {
private RuleBasedBreakIterator() {
fLastStatusIndexValid = true;
fDictionaryCharCount = 0;
- fBreakEngines.put(-1, fUnhandledBreakEngine);
+ synchronized(gAllBreakEngines) {
+ fBreakEngines = new ArrayList<LanguageBreakEngine>(gAllBreakEngines);
+ }
}
/**
@@ -132,6 +135,13 @@ public class RuleBasedBreakIterator extends BreakIterator {
if (fText != null) {
result.fText = (CharacterIterator)(fText.clone());
}
+ synchronized (gAllBreakEngines) {
+ result.fBreakEngines = new ArrayList<LanguageBreakEngine>(gAllBreakEngines);
+ }
+ result.fLookAheadMatches = new LookAheadResults();
+ if (fCachedBreakPositions != null) {
+ result.fCachedBreakPositions = fCachedBreakPositions.clone();
+ }
return result;
}
@@ -255,10 +265,34 @@ public class RuleBasedBreakIterator extends BreakIterator {
* The "default" break engine - just skips over ranges of dictionary words,
* producing no breaks. Should only be used if characters need to be handled
* by a dictionary but we have no dictionary implementation for them.
+ *
+ * Only one instance; shared by all break iterators.
*/
- private final UnhandledBreakEngine fUnhandledBreakEngine = new UnhandledBreakEngine();
+ private static final UnhandledBreakEngine gUnhandledBreakEngine;
+
+ /**
+ * List of all known break engines, common for all break iterators.
+ * Lazily updated as break engines are needed, because instantiation of
+ * break engines is expensive.
+ *
+ * Because gAllBreakEngines can be referenced concurrently from different
+ * BreakIterator instances, all access is synchronized.
+ */
+ private static final List<LanguageBreakEngine> gAllBreakEngines;
+
+ static {
+ gUnhandledBreakEngine = new UnhandledBreakEngine();
+ gAllBreakEngines = new ArrayList<LanguageBreakEngine>();
+ gAllBreakEngines.add(gUnhandledBreakEngine);
+ }
/**
+ * List of all known break engines. Similar to gAllBreakEngines, but local to a
+ * break iterator, allowing it to be used without synchronization.
+ */
+ private List<LanguageBreakEngine> fBreakEngines;
+
+ /**
* when a range of characters is divided up using the dictionary, the break
* positions that are discovered are stored here, preventing us from having
* to use either the dictionary or the state table again until the iterator
@@ -272,9 +306,6 @@ public class RuleBasedBreakIterator extends BreakIterator {
*/
private int fPositionInCache;
-
- private final ConcurrentHashMap<Integer, LanguageBreakEngine> fBreakEngines =
- new ConcurrentHashMap<Integer, LanguageBreakEngine>();
/**
* Dumps caches and performs other actions associated with a complete change
* in text or iteration position.
@@ -1087,26 +1118,32 @@ public class RuleBasedBreakIterator extends BreakIterator {
// We have a dictionary character.
// Does an already instantiated break engine handle it?
- for (LanguageBreakEngine candidate : fBreakEngines.values()) {
+ for (LanguageBreakEngine candidate : fBreakEngines) {
if (candidate.handles(c, fBreakType)) {
return candidate;
}
}
- // if we don't have an existing engine, build one.
- int script = UCharacter.getIntPropertyValue(c, UProperty.SCRIPT);
- if (script == UScript.KATAKANA || script == UScript.HIRAGANA) {
- // Katakana, Hiragana and Han are handled by the same dictionary engine.
- // Fold them together for mapping from script -> engine.
- script = UScript.HAN;
- }
+ synchronized (gAllBreakEngines) {
+ // This break iterator's list of break engines didn't handle the character.
+ // Check the global list, another break iterator may have instantiated the
+ // desired engine.
+ for (LanguageBreakEngine candidate : gAllBreakEngines) {
+ if (candidate.handles(c, fBreakType)) {
+ fBreakEngines.add(candidate);
+ return candidate;
+ }
+ }
+
+ // The global list doesn't have an existing engine, build one.
+ int script = UCharacter.getIntPropertyValue(c, UProperty.SCRIPT);
+ if (script == UScript.KATAKANA || script == UScript.HIRAGANA) {
+ // Katakana, Hiragana and Han are handled by the same dictionary engine.
+ // Fold them together for mapping from script -> engine.
+ script = UScript.HAN;
+ }
- LanguageBreakEngine eng = fBreakEngines.get(script);
- /*
- if (eng != null && !eng.handles(c, fBreakType)) {
- fUnhandledBreakEngine.handleChar(c, getBreakType());
- eng = fUnhandledBreakEngine;
- } else */ {
+ LanguageBreakEngine eng;
try {
switch (script) {
case UScript.THAI:
@@ -1126,38 +1163,33 @@ public class RuleBasedBreakIterator extends BreakIterator {
eng = new CjkBreakEngine(false);
}
else {
- fUnhandledBreakEngine.handleChar(c, getBreakType());
- eng = fUnhandledBreakEngine;
+ gUnhandledBreakEngine.handleChar(c, getBreakType());
+ eng = gUnhandledBreakEngine;
}
break;
case UScript.HANGUL:
if (getBreakType() == KIND_WORD) {
eng = new CjkBreakEngine(true);
} else {
- fUnhandledBreakEngine.handleChar(c, getBreakType());
- eng = fUnhandledBreakEngine;
+ gUnhandledBreakEngine.handleChar(c, getBreakType());
+ eng = gUnhandledBreakEngine;
}
break;
default:
- fUnhandledBreakEngine.handleChar(c, getBreakType());
- eng = fUnhandledBreakEngine;
+ gUnhandledBreakEngine.handleChar(c, getBreakType());
+ eng = gUnhandledBreakEngine;
break;
}
} catch (IOException e) {
eng = null;
}
- }
- if (eng != null && eng != fUnhandledBreakEngine) {
- LanguageBreakEngine existingEngine = fBreakEngines.putIfAbsent(script, eng);
- if (existingEngine != null) {
- // There was a race & another thread was first to register an engine for this script.
- // Use theirs and discard the one we just created.
- eng = existingEngine;
+ if (eng != null && eng != gUnhandledBreakEngine) {
+ gAllBreakEngines.add(eng);
+ fBreakEngines.add(eng);
}
- // assert eng.handles(c, fBreakType);
- }
- return eng;
+ return eng;
+ } // end synchronized(gAllBreakEngines)
}
private static final int kMaxLookaheads = 8;
diff --git a/android_icu4j/src/main/java/android/icu/text/RuleBasedNumberFormat.java b/android_icu4j/src/main/java/android/icu/text/RuleBasedNumberFormat.java
index bb3109611..cde9ff39f 100644
--- a/android_icu4j/src/main/java/android/icu/text/RuleBasedNumberFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/RuleBasedNumberFormat.java
@@ -1240,7 +1240,7 @@ public class RuleBasedNumberFormat extends NumberFormat {
public StringBuffer format(android.icu.math.BigDecimal number,
StringBuffer toAppendTo,
FieldPosition pos) {
- if (MIN_VALUE.compareTo(number) >= 0 || MAX_VALUE.compareTo(number) <= 0) {
+ if (MIN_VALUE.compareTo(number) > 0 || MAX_VALUE.compareTo(number) < 0) {
// We're outside of our normal range that this framework can handle.
// The DecimalFormat will provide more accurate results.
return getDecimalFormat().format(number, toAppendTo, pos);
@@ -1988,8 +1988,10 @@ public class RuleBasedNumberFormat extends NumberFormat {
* Adjust capitalization of formatted result for display context
*/
private String adjustForContext(String result) {
- if (result != null && result.length() > 0 && UCharacter.isLowerCase(result.codePointAt(0))) {
- DisplayContext capitalization = getContext(DisplayContext.Type.CAPITALIZATION);
+ DisplayContext capitalization = getContext(DisplayContext.Type.CAPITALIZATION);
+ if (capitalization != DisplayContext.CAPITALIZATION_NONE && result != null && result.length() > 0
+ && UCharacter.isLowerCase(result.codePointAt(0)))
+ {
if ( capitalization==DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE ||
(capitalization == DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU && capitalizationForListOrMenu) ||
(capitalization == DisplayContext.CAPITALIZATION_FOR_STANDALONE && capitalizationForStandAlone) ) {
diff --git a/android_icu4j/src/main/java/android/icu/text/RuleBasedTransliterator.java b/android_icu4j/src/main/java/android/icu/text/RuleBasedTransliterator.java
index 405178989..f08ea972d 100644
--- a/android_icu4j/src/main/java/android/icu/text/RuleBasedTransliterator.java
+++ b/android_icu4j/src/main/java/android/icu/text/RuleBasedTransliterator.java
@@ -268,8 +268,6 @@ import java.util.Map;
* always matches anything it matches. In other words, the first
* rule <em>masks</em> the second rule.
*
- * <p>Copyright (c) IBM Corporation 1999-2000. All rights reserved.
- *
* @author Alan Liu
* @deprecated This API is ICU internal only.
* @hide Only a subset of ICU is exposed in Android
diff --git a/android_icu4j/src/main/java/android/icu/text/ScientificNumberFormatter.java b/android_icu4j/src/main/java/android/icu/text/ScientificNumberFormatter.java
index 601640da6..12d506eea 100644
--- a/android_icu4j/src/main/java/android/icu/text/ScientificNumberFormatter.java
+++ b/android_icu4j/src/main/java/android/icu/text/ScientificNumberFormatter.java
@@ -14,6 +14,7 @@ import java.text.AttributedCharacterIterator.Attribute;
import java.text.CharacterIterator;
import java.util.Map;
+import android.icu.impl.number.Parse;
import android.icu.lang.UCharacter;
import android.icu.util.ULocale;
@@ -218,14 +219,14 @@ public final class ScientificNumberFormatter {
int start = iterator.getRunStart(NumberFormat.Field.EXPONENT_SIGN);
int limit = iterator.getRunLimit(NumberFormat.Field.EXPONENT_SIGN);
int aChar = char32AtAndAdvance(iterator);
- if (DecimalFormat.minusSigns.contains(aChar)) {
+ if (Parse.UNISET_MINUS.contains(aChar)) {
append(
iterator,
copyFromOffset,
start,
result);
result.append(SUPERSCRIPT_MINUS_SIGN);
- } else if (DecimalFormat.plusSigns.contains(aChar)) {
+ } else if (Parse.UNISET_PLUS.contains(aChar)) {
append(
iterator,
copyFromOffset,
diff --git a/android_icu4j/src/main/java/android/icu/text/SimpleFormatter.java b/android_icu4j/src/main/java/android/icu/text/SimpleFormatter.java
index f4d36a8fd..70555f44e 100644
--- a/android_icu4j/src/main/java/android/icu/text/SimpleFormatter.java
+++ b/android_icu4j/src/main/java/android/icu/text/SimpleFormatter.java
@@ -34,7 +34,6 @@ import android.icu.impl.SimpleFormatterImpl;
* @see MessageFormat
* @see MessagePattern.ApostropheMode
* @hide Only a subset of ICU is exposed in Android
- * @hide draft / provisional / internal are hidden on Android
*/
public final class SimpleFormatter {
// For internal use in Java, use SimpleFormatterImpl directly instead:
@@ -58,7 +57,6 @@ public final class SimpleFormatter {
* @param pattern The pattern string.
* @return The new SimpleFormatter object.
* @throws IllegalArgumentException for bad argument syntax.
- * @hide draft / provisional / internal are hidden on Android
*/
public static SimpleFormatter compile(CharSequence pattern) {
return compileMinMaxArguments(pattern, 0, Integer.MAX_VALUE);
@@ -74,7 +72,6 @@ public final class SimpleFormatter {
* @param max The pattern must have at most this many arguments.
* @return The new SimpleFormatter object.
* @throws IllegalArgumentException for bad argument syntax and too few or too many arguments.
- * @hide draft / provisional / internal are hidden on Android
*/
public static SimpleFormatter compileMinMaxArguments(CharSequence pattern, int min, int max) {
StringBuilder sb = new StringBuilder();
@@ -84,7 +81,6 @@ public final class SimpleFormatter {
/**
* @return The max argument number + 1.
- * @hide draft / provisional / internal are hidden on Android
*/
public int getArgumentLimit() {
return SimpleFormatterImpl.getArgumentLimit(compiledPattern);
@@ -92,7 +88,6 @@ public final class SimpleFormatter {
/**
* Formats the given values.
- * @hide draft / provisional / internal are hidden on Android
*/
public String format(CharSequence... values) {
return SimpleFormatterImpl.formatCompiledPattern(compiledPattern, values);
@@ -111,7 +106,6 @@ public final class SimpleFormatter {
* values.length must be at least getArgumentLimit().
* Can be null if getArgumentLimit()==0.
* @return appendTo
- * @hide draft / provisional / internal are hidden on Android
*/
public StringBuilder formatAndAppend(
StringBuilder appendTo, int[] offsets, CharSequence... values) {
@@ -132,7 +126,6 @@ public final class SimpleFormatter {
* An argument value may be the same object as result.
* values.length must be at least getArgumentLimit().
* @return result
- * @hide draft / provisional / internal are hidden on Android
*/
public StringBuilder formatAndReplace(
StringBuilder result, int[] offsets, CharSequence... values) {
@@ -141,8 +134,6 @@ public final class SimpleFormatter {
/**
* Returns a string similar to the original pattern, only for debugging.
- *
- * @hide draft / provisional / internal are hidden on Android
*/
@Override
public String toString() {
@@ -156,8 +147,6 @@ public final class SimpleFormatter {
/**
* Returns the pattern text with none of the arguments.
* Like formatting with all-empty string values.
- *
- * @hide draft / provisional / internal are hidden on Android
*/
public String getTextWithNoArguments() {
return SimpleFormatterImpl.getTextWithNoArguments(compiledPattern);
diff --git a/android_icu4j/src/main/java/android/icu/text/StringSearch.java b/android_icu4j/src/main/java/android/icu/text/StringSearch.java
index a21917d40..1e7fe6b34 100644
--- a/android_icu4j/src/main/java/android/icu/text/StringSearch.java
+++ b/android_icu4j/src/main/java/android/icu/text/StringSearch.java
@@ -33,7 +33,7 @@ import android.icu.util.ULocale;
// is a leftover from already-disabled Boyer-Moore search code. This Java implementation
// preserves the code, but we should clean this up later.
-/**
+/**
*
* <tt>StringSearch</tt> is a {@link SearchIterator} that provides
* language-sensitive text searching based on the comparison rules defined
@@ -50,7 +50,7 @@ import android.icu.util.ULocale;
* <br>
* A pattern string P matches a text string S at the offsets [start, end]
* if
- * <pre>
+ * <pre>
* option 1. Some canonical equivalent of P matches some canonical equivalent
* of S'
* option 2. P matches S' and if P starts or ends with a combining mark,
@@ -59,10 +59,10 @@ import android.icu.util.ULocale;
* </pre>
* Option 2. is the default.
* <p>
- * This search has APIs similar to that of other text iteration mechanisms
- * such as the break iterators in {@link BreakIterator}. Using these
- * APIs, it is easy to scan through text looking for all occurrences of
- * a given pattern. This search iterator allows changing of direction by
+ * This search has APIs similar to that of other text iteration mechanisms
+ * such as the break iterators in {@link BreakIterator}. Using these
+ * APIs, it is easy to scan through text looking for all occurrences of
+ * a given pattern. This search iterator allows changing of direction by
* calling a {@link #reset} followed by a {@link #next} or {@link #previous}.
* Though a direction change can occur without calling {@link #reset} first,
* this operation comes with some speed penalty.
@@ -110,7 +110,7 @@ import android.icu.util.ULocale;
* from {@link #getCollator} and using the APIs in {@link RuleBasedCollator}.
* Lastly to update <tt>StringSearch</tt> to the new collator attributes,
* {@link #reset} has to be called.
- * <p>
+ * <p>
* Restriction: <br>
* Currently there are no composite characters that consists of a
* character with combining class &gt; 0 before a character with combining
@@ -127,7 +127,7 @@ import android.icu.util.ULocale;
* @see RuleBasedCollator
* @author Laura Werner, synwee
*/
-// internal notes: all methods do not guarantee the correct status of the
+// internal notes: all methods do not guarantee the correct status of the
// characteriterator. the caller has to maintain the original index position
// if necessary. methods could change the index position as it deems fit
public final class StringSearch extends SearchIterator {
@@ -157,15 +157,15 @@ public final class StringSearch extends SearchIterator {
// private char[] canonicalSuffixAccents_;
/**
- * Initializes the iterator to use the language-specific rules defined in
- * the argument collator to search for argument pattern in the argument
+ * Initializes the iterator to use the language-specific rules defined in
+ * the argument collator to search for argument pattern in the argument
* target text. The argument <code>breakiter</code> is used to define logical matches.
- * See super class documentation for more details on the use of the target
+ * See super class documentation for more details on the use of the target
* text and {@link BreakIterator}.
* @param pattern text to look for.
- * @param target target text to search for pattern.
+ * @param target target text to search for pattern.
* @param collator {@link RuleBasedCollator} that defines the language rules
- * @param breakiter A {@link BreakIterator} that is used to determine the
+ * @param breakiter A {@link BreakIterator} that is used to determine the
* boundaries of a logical match. This argument can be null.
* @throws IllegalArgumentException thrown when argument target is null,
* or of length 0
@@ -218,11 +218,11 @@ public final class StringSearch extends SearchIterator {
}
/**
- * Initializes the iterator to use the language-specific rules defined in
- * the argument collator to search for argument pattern in the argument
+ * Initializes the iterator to use the language-specific rules defined in
+ * the argument collator to search for argument pattern in the argument
* target text. No {@link BreakIterator}s are set to test for logical matches.
* @param pattern text to look for.
- * @param target target text to search for pattern.
+ * @param target target text to search for pattern.
* @param collator {@link RuleBasedCollator} that defines the language rules
* @throws IllegalArgumentException thrown when argument target is null,
* or of length 0
@@ -233,14 +233,14 @@ public final class StringSearch extends SearchIterator {
}
/**
- * Initializes the iterator to use the language-specific rules and
- * break iterator rules defined in the argument locale to search for
- * argument pattern in the argument target text.
+ * Initializes the iterator to use the language-specific rules and
+ * break iterator rules defined in the argument locale to search for
+ * argument pattern in the argument target text.
* @param pattern text to look for.
- * @param target target text to search for pattern.
+ * @param target target text to search for pattern.
* @param locale locale to use for language and break iterator rules
* @throws IllegalArgumentException thrown when argument target is null,
- * or of length 0. ClassCastException thrown if the collator for
+ * or of length 0. ClassCastException thrown if the collator for
* the specified locale is not a RuleBasedCollator.
*/
public StringSearch(String pattern, CharacterIterator target, Locale locale) {
@@ -248,16 +248,16 @@ public final class StringSearch extends SearchIterator {
}
/**
- * Initializes the iterator to use the language-specific rules and
- * break iterator rules defined in the argument locale to search for
- * argument pattern in the argument target text.
- * See super class documentation for more details on the use of the target
+ * Initializes the iterator to use the language-specific rules and
+ * break iterator rules defined in the argument locale to search for
+ * argument pattern in the argument target text.
+ * See super class documentation for more details on the use of the target
* text and {@link BreakIterator}.
* @param pattern text to look for.
- * @param target target text to search for pattern.
+ * @param target target text to search for pattern.
* @param locale locale to use for language and break iterator rules
* @throws IllegalArgumentException thrown when argument target is null,
- * or of length 0. ClassCastException thrown if the collator for
+ * or of length 0. ClassCastException thrown if the collator for
* the specified locale is not a RuleBasedCollator.
* @see BreakIterator
* @see RuleBasedCollator
@@ -268,13 +268,13 @@ public final class StringSearch extends SearchIterator {
}
/**
- * Initializes the iterator to use the language-specific rules and
- * break iterator rules defined in the default locale to search for
+ * Initializes the iterator to use the language-specific rules and
+ * break iterator rules defined in the default locale to search for
* argument pattern in the argument target text.
* @param pattern text to look for.
- * @param target target text to search for pattern.
+ * @param target target text to search for pattern.
* @throws IllegalArgumentException thrown when argument target is null,
- * or of length 0. ClassCastException thrown if the collator for
+ * or of length 0. ClassCastException thrown if the collator for
* the default locale is not a RuleBasedCollator.
*/
public StringSearch(String pattern, String target) {
@@ -285,9 +285,9 @@ public final class StringSearch extends SearchIterator {
/**
* Gets the {@link RuleBasedCollator} used for the language rules.
* <p>
- * Since <tt>StringSearch</tt> depends on the returned {@link RuleBasedCollator}, any
- * changes to the {@link RuleBasedCollator} result should follow with a call to
- * either {@link #reset()} or {@link #setCollator(RuleBasedCollator)} to ensure the correct
+ * Since <tt>StringSearch</tt> depends on the returned {@link RuleBasedCollator}, any
+ * changes to the {@link RuleBasedCollator} result should follow with a call to
+ * either {@link #reset()} or {@link #setCollator(RuleBasedCollator)} to ensure the correct
* search behavior.
* </p>
* @return {@link RuleBasedCollator} used by this <tt>StringSearch</tt>
@@ -335,7 +335,7 @@ public final class StringSearch extends SearchIterator {
}
/**
- * Set the pattern to search for.
+ * Set the pattern to search for.
* The iterator's position will not be changed by this method.
* @param pattern for searching
* @see #getPattern
@@ -352,7 +352,7 @@ public final class StringSearch extends SearchIterator {
}
/**
- * Determines whether canonical matches (option 1, as described in the
+ * Determines whether canonical matches (option 1, as described in the
* class documentation) is set.
* See setCanonical(boolean) for more information.
* @see #setCanonical
@@ -408,7 +408,7 @@ public final class StringSearch extends SearchIterator {
textIter_.setOffset(position);
}
- /**
+ /**
* {@inheritDoc}
*/
@Override
@@ -598,7 +598,7 @@ public final class StringSearch extends SearchIterator {
/**
* Getting the modified collation elements taking into account the collation
* attributes.
- *
+ *
* @param sourcece
* @return the modified collation element
*/
@@ -629,20 +629,19 @@ public final class StringSearch extends SearchIterator {
}
/**
- * Direct port of ICU4C static int32_t * addTouint32_tArray(...) in usearch.cpp.
+ * Direct port of ICU4C static int32_t * addTouint32_tArray(...) in usearch.cpp
+ * (except not taking destination buffer size and status param).
* This is used for appending a PCE to Pattern.PCE_ buffer. We probably should
* implement this in Pattern class.
- *
+ *
* @param destination target array
* @param offset destination offset to add value
- * @param destinationlength target array size
* @param value to be added
* @param increments incremental size expected
* @return new destination array, destination if there was no new allocation
*/
- private static int[] addToIntArray(int[] destination, int offset, int destinationlength,
- int value, int increments) {
- int newlength = destinationlength;
+ private static int[] addToIntArray(int[] destination, int offset, int value, int increments) {
+ int newlength = destination.length;
if (offset + 1 == newlength) {
newlength += increments;
int temp[] = new int[newlength];
@@ -657,7 +656,7 @@ public final class StringSearch extends SearchIterator {
* Direct port of ICU4C static int64_t * addTouint64_tArray(...) in usearch.cpp.
* This is used for appending a PCE to Pattern.PCE_ buffer. We probably should
* implement this in Pattern class.
- *
+ *
* @param destination target array
* @param offset destination offset to add value
* @param destinationlength target array size
@@ -689,7 +688,6 @@ public final class StringSearch extends SearchIterator {
// TODO: We probably do not need Pattern CE table.
private int initializePatternCETable() {
int[] cetable = new int[INITIAL_ARRAY_SIZE_];
- int cetablesize = cetable.length;
int patternlength = pattern_.text_.length();
CollationElementIterator coleiter = utilIter_;
@@ -707,7 +705,7 @@ public final class StringSearch extends SearchIterator {
while ((ce = coleiter.next()) != CollationElementIterator.NULLORDER) {
int newce = getCE(ce);
if (newce != CollationElementIterator.IGNORABLE /* 0 */) {
- int[] temp = addToIntArray(cetable, offset, cetablesize, newce,
+ int[] temp = addToIntArray(cetable, offset, newce,
patternlength - coleiter.getOffset() + 1);
offset++;
cetable = temp;
@@ -789,9 +787,9 @@ public final class StringSearch extends SearchIterator {
// *** Boyer-Moore ***
/*
- private final void setShiftTable(char shift[],
- char backshift[],
- int cetable[], int cesize,
+ private final void setShiftTable(char shift[],
+ char backshift[],
+ int cetable[], int cesize,
int expansionsize,
int defaultforward,
int defaultbackward) {
@@ -823,6 +821,7 @@ public final class StringSearch extends SearchIterator {
* @hide original deprecated declaration
* @hide draft / provisional / internal are hidden on Android
*/
+ @Override
@Deprecated
protected void setMatchNotFound() {
super.setMatchNotFound();
@@ -1494,7 +1493,7 @@ public final class StringSearch extends SearchIterator {
//
// ICU4C usearch_handleNextExact() is identical to usearch_handleNextCanonical()
// for the linear search implementation. The differences are addressed in search().
- //
+ //
private boolean handleNextExact() {
return handleNextCommonImpl();
}
@@ -1569,9 +1568,9 @@ public final class StringSearch extends SearchIterator {
/**
* Gets a substring out of a CharacterIterator
- *
+ *
* Java porting note: Not available in ICU4C
- *
+ *
* @param text CharacterIterator
* @param start start offset
* @param length of substring
@@ -1706,12 +1705,12 @@ public final class StringSearch extends SearchIterator {
/**
* Get the processed ordering priority of the next collation element in the text.
* A single character may contain more than one collation element.
- *
+ *
* Note: This is equivalent to
* UCollationPCE::nextProcessed(int32_t *ixLow, int32_t *ixHigh, UErrorCode *status);
*
* @param range receiving the iterator index before/after fetching the CE.
- * @return The next collation elements ordering, otherwise returns PROCESSED_NULLORDER
+ * @return The next collation elements ordering, otherwise returns PROCESSED_NULLORDER
* if an error has occurred or if the end of string has been reached
*/
public long nextProcessed(Range range) {
@@ -1749,7 +1748,7 @@ public final class StringSearch extends SearchIterator {
* UCollationPCE::previousProcessed(int32_t *ixLow, int32_t *ixHigh, UErrorCode *status);
*
* @param range receiving the iterator index before/after fetching the CE.
- * @return The previous collation elements ordering, otherwise returns
+ * @return The previous collation elements ordering, otherwise returns
* PROCESSED_NULLORDER if an error has occurred or if the start of
* string has been reached.
*/
@@ -1910,7 +1909,7 @@ public final class StringSearch extends SearchIterator {
/**
* Java port of ICU4C CEI (usearch.cpp)
- *
+ *
* CEI Collation Element + source text index.
* These structs are kept in the circular buffer.
*/
diff --git a/android_icu4j/src/main/java/android/icu/text/Transliterator.java b/android_icu4j/src/main/java/android/icu/text/Transliterator.java
index 6ea97683e..716247287 100644
--- a/android_icu4j/src/main/java/android/icu/text/Transliterator.java
+++ b/android_icu4j/src/main/java/android/icu/text/Transliterator.java
@@ -222,9 +222,6 @@ import android.icu.util.UResourceBundle;
* <code>transliterate()</code> method taking a <code>String</code> and <code>StringBuffer</code> if the performance of
* these methods can be improved over the performance obtained by the default implementations in this class.
*
- * <p>
- * Copyright &copy; IBM Corporation 1999. All rights reserved.
- *
* @author Alan Liu
* @hide Only a subset of ICU is exposed in Android
*/
diff --git a/android_icu4j/src/main/java/android/icu/text/UnhandledBreakEngine.java b/android_icu4j/src/main/java/android/icu/text/UnhandledBreakEngine.java
index 706cb014d..2bfc1e000 100644
--- a/android_icu4j/src/main/java/android/icu/text/UnhandledBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/text/UnhandledBreakEngine.java
@@ -12,6 +12,7 @@ package android.icu.text;
import static android.icu.impl.CharacterIteration.DONE32;
import java.text.CharacterIterator;
+import java.util.concurrent.atomic.AtomicReferenceArray;
import android.icu.impl.CharacterIteration;
import android.icu.lang.UCharacter;
@@ -20,42 +21,70 @@ import android.icu.lang.UProperty;
final class UnhandledBreakEngine implements LanguageBreakEngine {
// TODO: Use two arrays of UnicodeSet, one with all frozen sets, one with unfrozen.
// in handleChar(), update the unfrozen version, clone, freeze, replace the frozen one.
- private final UnicodeSet[] fHandled = new UnicodeSet[BreakIterator.KIND_TITLE + 1];
+
+ // Note on concurrency: A single instance of UnhandledBreakEngine is shared across all
+ // RuleBasedBreakIterators in a process. They may make arbitrary concurrent calls.
+ // If handleChar() is updating the set of unhandled characters at the same time
+ // findBreaks() or handles() is referencing it, the referencing functions must see
+ // a consistent set. It doesn't matter whether they see it before or after the update,
+ // but they should not see an inconsistent, changing set.
+ //
+ // To do this, an update is made by cloning the old set, updating the clone, then
+ // replacing the old with the new. Once made visible, each set remains constant.
+
+ // TODO: it's odd that findBreaks() can produce different results, depending
+ // on which scripts have been previously seen by handleChar(). (This is not a
+ // threading specific issue). Possibly stop on script boundaries?
+
+ final AtomicReferenceArray<UnicodeSet> fHandled = new AtomicReferenceArray<UnicodeSet>(BreakIterator.KIND_TITLE + 1);
public UnhandledBreakEngine() {
- for (int i = 0; i < fHandled.length; i++) {
- fHandled[i] = new UnicodeSet();
+ for (int i = 0; i < fHandled.length(); i++) {
+ fHandled.set(i, new UnicodeSet());
}
}
-
+
+ @Override
public boolean handles(int c, int breakType) {
- return (breakType >= 0 && breakType < fHandled.length) &&
- (fHandled[breakType].contains(c));
+ return (breakType >= 0 && breakType < fHandled.length()) &&
+ (fHandled.get(breakType).contains(c));
}
+ @Override
public int findBreaks(CharacterIterator text, int startPos, int endPos,
boolean reverse, int breakType, DictionaryBreakEngine.DequeI foundBreaks) {
- if (breakType >= 0 && breakType < fHandled.length) {
- int c = CharacterIteration.current32(text);
- if (reverse) {
- while (text.getIndex() > startPos && fHandled[breakType].contains(c)) {
- CharacterIteration.previous32(text);
- c = CharacterIteration.current32(text);
- }
- } else {
- while (text.getIndex() < endPos && fHandled[breakType].contains(c)) {
- CharacterIteration.next32(text);
- c = CharacterIteration.current32(text);
- }
- }
- }
+ if (breakType >= 0 && breakType < fHandled.length()) {
+ UnicodeSet uniset = fHandled.get(breakType);
+ int c = CharacterIteration.current32(text);
+ if (reverse) {
+ while (text.getIndex() > startPos && uniset.contains(c)) {
+ CharacterIteration.previous32(text);
+ c = CharacterIteration.current32(text);
+ }
+ } else {
+ while (text.getIndex() < endPos && uniset.contains(c)) {
+ CharacterIteration.next32(text);
+ c = CharacterIteration.current32(text);
+ }
+ }
+ }
return 0;
}
- public synchronized void handleChar(int c, int breakType) {
- if (breakType >= 0 && breakType < fHandled.length && c != DONE32) {
- if (!fHandled[breakType].contains(c)) {
+ /**
+ * Update the set of unhandled characters for the specified breakType to include
+ * all that have the same script as c.
+ * May be called concurrently with handles() or findBreaks().
+ * Must not be called concurrently with itself.
+ */
+ public void handleChar(int c, int breakType) {
+ if (breakType >= 0 && breakType < fHandled.length() && c != DONE32) {
+ UnicodeSet originalSet = fHandled.get(breakType);
+ if (!originalSet.contains(c)) {
int script = UCharacter.getIntPropertyValue(c, UProperty.SCRIPT);
- fHandled[breakType].applyIntPropertyValue(UProperty.SCRIPT, script);
+ UnicodeSet newSet = new UnicodeSet();
+ newSet.applyIntPropertyValue(UProperty.SCRIPT, script);
+ newSet.addAll(originalSet);
+ fHandled.set(breakType, newSet);
}
}
}
diff --git a/android_icu4j/src/main/java/android/icu/text/UnicodeSet.java b/android_icu4j/src/main/java/android/icu/text/UnicodeSet.java
index df7cd2da0..f1126b445 100644
--- a/android_icu4j/src/main/java/android/icu/text/UnicodeSet.java
+++ b/android_icu4j/src/main/java/android/icu/text/UnicodeSet.java
@@ -3334,7 +3334,7 @@ public class UnicodeSet extends UnicodeFilter implements Iterable<String>, Compa
* property alias, or a special ID. Special IDs are matched
* loosely and correspond to the following sets:
*
- * "ANY" = [\\u0000-\\u0010FFFF],
+ * "ANY" = [\\u0000-\\U0010FFFF],
* "ASCII" = [\\u0000-\\u007F].
*
* @param valueAlias a value alias, either short or long. The
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 a0f12630e..fce31c708 100644
--- a/android_icu4j/src/main/java/android/icu/util/Currency.java
+++ b/android_icu4j/src/main/java/android/icu/util/Currency.java
@@ -648,19 +648,7 @@ public class Currency extends MeasureUnit {
*/
@Deprecated
public static String parse(ULocale locale, String text, int type, ParsePosition pos) {
- List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = CURRENCY_NAME_CACHE.get(locale);
- if (currencyTrieVec == null) {
- TextTrieMap<CurrencyStringInfo> currencyNameTrie =
- new TextTrieMap<CurrencyStringInfo>(true);
- TextTrieMap<CurrencyStringInfo> currencySymbolTrie =
- new TextTrieMap<CurrencyStringInfo>(false);
- currencyTrieVec = new ArrayList<TextTrieMap<CurrencyStringInfo>>();
- currencyTrieVec.add(currencySymbolTrie);
- currencyTrieVec.add(currencyNameTrie);
- setupCurrencyTrieVec(locale, currencyTrieVec);
- CURRENCY_NAME_CACHE.put(locale, currencyTrieVec);
- }
-
+ List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = getCurrencyTrieVec(locale);
int maxLength = 0;
String isoResult = null;
@@ -685,6 +673,37 @@ public class Currency extends MeasureUnit {
return isoResult;
}
+ /**
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public static TextTrieMap<CurrencyStringInfo>.ParseState openParseState(
+ ULocale locale, int startingCp, int type) {
+ List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = getCurrencyTrieVec(locale);
+ if (type == Currency.LONG_NAME) {
+ return currencyTrieVec.get(0).openParseState(startingCp);
+ } else {
+ return currencyTrieVec.get(1).openParseState(startingCp);
+ }
+ }
+
+ private static List<TextTrieMap<CurrencyStringInfo>> getCurrencyTrieVec(ULocale locale) {
+ List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = CURRENCY_NAME_CACHE.get(locale);
+ if (currencyTrieVec == null) {
+ TextTrieMap<CurrencyStringInfo> currencyNameTrie =
+ new TextTrieMap<CurrencyStringInfo>(true);
+ TextTrieMap<CurrencyStringInfo> currencySymbolTrie =
+ new TextTrieMap<CurrencyStringInfo>(false);
+ currencyTrieVec = new ArrayList<TextTrieMap<CurrencyStringInfo>>();
+ currencyTrieVec.add(currencySymbolTrie);
+ currencyTrieVec.add(currencyNameTrie);
+ setupCurrencyTrieVec(locale, currencyTrieVec);
+ CURRENCY_NAME_CACHE.put(locale, currencyTrieVec);
+ }
+ return currencyTrieVec;
+ }
+
private static void setupCurrencyTrieVec(ULocale locale,
List<TextTrieMap<CurrencyStringInfo>> trieVec) {
@@ -708,19 +727,39 @@ public class Currency extends MeasureUnit {
}
}
- private static final class CurrencyStringInfo {
+ /**
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public static final class CurrencyStringInfo {
private String isoCode;
private String currencyString;
+ /**
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
public CurrencyStringInfo(String isoCode, String currencyString) {
this.isoCode = isoCode;
this.currencyString = currencyString;
}
+ /**
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
public String getISOCode() {
return isoCode;
}
+ /**
+ * @deprecated This API is ICU internal only.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
@SuppressWarnings("unused")
public String getCurrencyString() {
return currencyString;
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 05bdb0ce9..e98bde946 100644
--- a/android_icu4j/src/main/java/android/icu/util/LocaleMatcher.java
+++ b/android_icu4j/src/main/java/android/icu/util/LocaleMatcher.java
@@ -26,19 +26,22 @@ import android.icu.impl.Relation;
import android.icu.impl.Row;
import android.icu.impl.Row.R3;
import android.icu.impl.Utility;
+import android.icu.impl.locale.XLocaleDistance.DistanceOption;
+import android.icu.impl.locale.XLocaleMatcher;
+import android.icu.impl.locale.XLocaleMatcher.Builder;
/**
* Provides a way to match the languages (locales) supported by a product to the
* languages (locales) acceptable to a user, and get the best match. For
* example:
- *
+ *
* <pre>
* LocaleMatcher matcher = new LocaleMatcher("fr, en-GB, en");
- *
+ *
* // afterwards:
* matcher.getBestMatch("en-US").toLanguageTag() =&gt; "en"
* </pre>
- *
+ *
* It takes into account when languages are close to one another, such as fil
* and tl, and when language regional variants are close, like en-GB and en-AU.
* It also handles scripts, like zh-Hant vs zh-TW. For examples, see the test
@@ -47,7 +50,7 @@ import android.icu.impl.Utility;
* product will just need one static instance, built with the languages
* that it supports. However, it may want multiple instances with different
* default languages based on additional information, such as the domain.
- *
+ *
* @author markdavis@google.com
* @hide Only a subset of ICU is exposed in Android
*/
@@ -84,7 +87,7 @@ public class LocaleMatcher {
* threshold, that default language is chosen. Typically the default is English,
* but it could be different based on additional information, such as the domain
* of the page.
- *
+ *
* @param languagePriorityList weighted list
*/
public LocaleMatcher(LocalePriorityList languagePriorityList) {
@@ -94,7 +97,7 @@ public class LocaleMatcher {
/**
* Create a new language matcher from a String form. The highest-weighted
* language is the default.
- *
+ *
* @param languagePriorityListString String form of LanguagePriorityList
*/
public LocaleMatcher(String languagePriorityListString) {
@@ -123,6 +126,7 @@ public class LocaleMatcher {
@Deprecated
public LocaleMatcher(LocalePriorityList languagePriorityList, LanguageMatcherData matcherData, double threshold) {
this.matcherData = matcherData == null ? defaultWritten : matcherData.freeze();
+ this.languagePriorityList = languagePriorityList;
for (final ULocale language : languagePriorityList) {
add(language, languagePriorityList.getWeight(language));
}
@@ -176,7 +180,7 @@ public class LocaleMatcher {
/**
* Get the best match for a LanguagePriorityList
- *
+ *
* @param languageList list to match
* @return best matching language code
*/
@@ -202,7 +206,7 @@ public class LocaleMatcher {
/**
* Convenience method: Get the best match for a LanguagePriorityList
- *
+ *
* @param languageList String form of language priority list
* @return best matching language code
*/
@@ -212,7 +216,7 @@ public class LocaleMatcher {
/**
* Get the best match for an individual language code.
- *
+ *
* @param ulocale locale/language code to match
* @return best matching language code
*/
@@ -234,14 +238,14 @@ public class LocaleMatcher {
*/
@Override
public String toString() {
- return "{" + defaultLanguage + ", "
+ return "{" + defaultLanguage + ", "
+ localeToMaxLocaleAndWeight + "}";
}
// ================= Privates =====================
/**
* Get the best match for an individual language code.
- *
+ *
* @param languageCode
* @return best matching language code and weight (as per
* {@link #match(ULocale, ULocale)})
@@ -284,7 +288,7 @@ public class LocaleMatcher {
}
return bestTableMatch;
}
-
+
/**
* @deprecated This API is ICU internal only.
* @hide draft / provisional / internal are hidden on Android
@@ -302,7 +306,7 @@ public class LocaleMatcher {
}
/**
- * We preprocess the data to get just the possible matches for each desired base language.
+ * We preprocess the data to get just the possible matches for each desired base language.
*/
private void processMapping() {
for (Entry<String, Set<String>> desiredToMatchingLanguages : matcherData.matchingLanguages().keyValuesSet()) {
@@ -336,7 +340,7 @@ public class LocaleMatcher {
}
Set<Row.R3<ULocale, ULocale, Double>> localeToMaxLocaleAndWeight = new LinkedHashSet<Row.R3<ULocale, ULocale, Double>>();
- Map<String,Set<Row.R3<ULocale, ULocale, Double>>> desiredLanguageToPossibleLocalesToMaxLocaleToData
+ Map<String,Set<Row.R3<ULocale, ULocale, Double>>> desiredLanguageToPossibleLocalesToMaxLocaleToData
= new LinkedHashMap<String,Set<Row.R3<ULocale, ULocale, Double>>>();
// =============== Special Mapping Information ==============
@@ -437,6 +441,7 @@ public class LocaleMatcher {
return (region == null ? "*" : region);
}
+ @Override
public String toString() {
String result = getLanguage();
if (level != Level.language) {
@@ -480,7 +485,7 @@ public class LocaleMatcher {
enum Level {
language(0.99),
- script(0.2),
+ script(0.2),
region(0.04);
final double worst;
@@ -520,7 +525,7 @@ public class LocaleMatcher {
}
}
- double getScore(ULocale dMax, String desiredRaw, String desiredMax,
+ double getScore(ULocale dMax, String desiredRaw, String desiredMax,
ULocale sMax, String supportedRaw, String supportedMax) {
double distance = 0;
if (!desiredMax.equals(supportedMax)) {
@@ -536,7 +541,7 @@ public class LocaleMatcher {
System.out.println("\t\t\t" + level + " Raw Score:\t" + desiredLocale + ";\t" + supportedLocale);
}
for (R3<LocalePatternMatcher,LocalePatternMatcher,Double> datum : scores) { // : result
- if (datum.get0().matches(desiredLocale)
+ if (datum.get0().matches(desiredLocale)
&& datum.get1().matches(supportedLocale)) {
if (DEBUG) {
System.out.println("\t\t\t\tFOUND\t" + datum);
@@ -550,6 +555,7 @@ public class LocaleMatcher {
return level.worst;
}
+ @Override
public String toString() {
StringBuilder result = new StringBuilder().append(level);
for (R3<LocalePatternMatcher, LocalePatternMatcher, Double> score : scores) {
@@ -559,6 +565,7 @@ public class LocaleMatcher {
}
+ @Override
@SuppressWarnings("unchecked")
public ScoreData cloneAsThawed() {
try {
@@ -574,10 +581,12 @@ public class LocaleMatcher {
private volatile boolean frozen = false;
+ @Override
public ScoreData freeze() {
return this;
}
+ @Override
public boolean isFrozen() {
return frozen;
}
@@ -631,6 +640,7 @@ public class LocaleMatcher {
* @deprecated This API is ICU internal only.
* @hide draft / provisional / internal are hidden on Android
*/
+ @Override
@Deprecated
public String toString() {
return languageScores + "\n\t" + scriptScores + "\n\t" + regionScores;
@@ -739,11 +749,12 @@ public class LocaleMatcher {
return this;
}
- /**
+ /**
* {@inheritDoc}
* @deprecated This API is ICU internal only.
* @hide draft / provisional / internal are hidden on Android
*/
+ @Override
@Deprecated
public LanguageMatcherData cloneAsThawed() {
LanguageMatcherData result;
@@ -759,11 +770,12 @@ public class LocaleMatcher {
}
}
- /**
+ /**
* {@inheritDoc}
* @deprecated This API is ICU internal only.
* @hide draft / provisional / internal are hidden on Android
*/
+ @Override
@Deprecated
public LanguageMatcherData freeze() {
languageScores.freeze();
@@ -774,11 +786,12 @@ public class LocaleMatcher {
return this;
}
- /**
+ /**
* {@inheritDoc}
* @deprecated This API is ICU internal only.
* @hide draft / provisional / internal are hidden on Android
*/
+ @Override
@Deprecated
public boolean isFrozen() {
return frozen;
@@ -786,6 +799,7 @@ public class LocaleMatcher {
}
LanguageMatcherData matcherData;
+ LocalePriorityList languagePriorityList;
private static final LanguageMatcherData defaultWritten;
@@ -838,4 +852,84 @@ public class LocaleMatcher {
final LocaleMatcher matcher = new LocaleMatcher("");
return matcher.match(a, matcher.addLikelySubtags(a), b, matcher.addLikelySubtags(b));
}
+
+ transient XLocaleMatcher xLocaleMatcher = null;
+ transient ULocale xDefaultLanguage = null;
+ transient boolean xFavorScript = false;
+
+ /**
+ * Returns the distance between the two languages, using the new CLDR syntax (see getBestMatch).
+ * The values are not necessarily symmetric.
+ * @param desired A locale desired by the user
+ * @param supported A locale supported by a program.
+ * @return A return of 0 is a complete match, and 100 is a complete mismatch (above the thresholdDistance).
+ * A language is first maximized with add likely subtags, then compared.
+ * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public int distance(ULocale desired, ULocale supported) {
+ return getLocaleMatcher().distance(desired, supported);
+ }
+
+ private synchronized XLocaleMatcher getLocaleMatcher() {
+ if (xLocaleMatcher == null) {
+ Builder builder = XLocaleMatcher.builder();
+ builder.setSupportedLocales(languagePriorityList);
+ if (xDefaultLanguage != null) {
+ builder.setDefaultLanguage(xDefaultLanguage);
+ }
+ if (xFavorScript) {
+ builder.setDistanceOption(DistanceOption.SCRIPT_FIRST);
+ }
+ xLocaleMatcher = builder.build();
+ }
+ return xLocaleMatcher;
+ }
+
+ /**
+ * Get the best match between the desired languages and supported languages
+ * This supports the new CLDR syntax to provide for better matches within
+ * regional clusters (such as maghreb Arabic vs non-maghreb Arabic, or regions that use en-GB vs en-US)
+ * and also matching between regions and macroregions, such as comparing es-419 to es-AR).
+ * @param desiredLanguages Typically the supplied user's languages, in order of preference, with best first.
+ * @param outputBestDesired The one of the desired languages that matched best.
+ * Set to null if the best match was not below the threshold distance.
+ * @return best-match supported language
+ * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public ULocale getBestMatch(LinkedHashSet<ULocale> desiredLanguages, Output<ULocale> outputBestDesired) {
+ return getLocaleMatcher().getBestMatch(desiredLanguages, outputBestDesired);
+ }
+
+ /**
+ * Set the default language, with null = default = first supported language
+ * @param defaultLanguage Language to use in case the threshold for distance is exceeded.
+ * @return this, for chaining
+ * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public synchronized LocaleMatcher setDefaultLanguage(ULocale defaultLanguage) {
+ this.xDefaultLanguage = defaultLanguage;
+ xLocaleMatcher = null;
+ return this;
+ }
+
+ /**
+ * If true, then the language differences are smaller than than script differences.
+ * This is used in situations (such as maps) where it is better to fall back to the same script than a similar language.
+ * @param favorScript Set to true to treat script as most important.
+ * @return this, for chaining.
+ * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release.
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ @Deprecated
+ public synchronized LocaleMatcher setFavorScript(boolean favorScript) {
+ this.xFavorScript = favorScript;
+ xLocaleMatcher = null;
+ return this;
+ }
}
diff --git a/android_icu4j/src/main/java/android/icu/util/Measure.java b/android_icu4j/src/main/java/android/icu/util/Measure.java
index faae47477..8b87c4d92 100644
--- a/android_icu4j/src/main/java/android/icu/util/Measure.java
+++ b/android_icu4j/src/main/java/android/icu/util/Measure.java
@@ -42,7 +42,7 @@ public class Measure {
*/
public Measure(Number number, MeasureUnit unit) {
if (number == null || unit == null) {
- throw new NullPointerException();
+ throw new NullPointerException("Number and MeasureUnit must not be null");
}
this.number = number;
this.unit = unit;
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 90fe3e662..a599bdb2b 100644
--- a/android_icu4j/src/main/java/android/icu/util/MeasureUnit.java
+++ b/android_icu4j/src/main/java/android/icu/util/MeasureUnit.java
@@ -249,8 +249,8 @@ public class MeasureUnit implements Serializable {
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
UResource.Table unitTypesTable = value.getTable();
for (int i2 = 0; unitTypesTable.getKeyAndValue(i2, key, value); ++i2) {
- // Skip "compound" since it is treated differently from the other units
- if (key.contentEquals("compound")) {
+ // Skip "compound" and "coordinate" since they are treated differently from the other units
+ if (key.contentEquals("compound") || key.contentEquals("coordinate")) {
continue;
}
@@ -442,7 +442,6 @@ public class MeasureUnit implements Serializable {
/**
* Constant for unit of concentr: milligram-per-deciliter
- * @hide draft / provisional / internal are hidden on Android
*/
public static final MeasureUnit MILLIGRAM_PER_DECILITER = MeasureUnit.internalGetInstance("concentr", "milligram-per-deciliter");
@@ -475,33 +474,16 @@ public class MeasureUnit implements Serializable {
/**
* Constant for unit of consumption: mile-per-gallon-imperial
- * @hide draft / provisional / internal are hidden on Android
*/
public static final MeasureUnit MILE_PER_GALLON_IMPERIAL = MeasureUnit.internalGetInstance("consumption", "mile-per-gallon-imperial");
/**
- * Constant for unit of coordinate: east
- * @hide draft / provisional / internal are hidden on Android
- */
- public static final MeasureUnit EAST = MeasureUnit.internalGetInstance("coordinate", "east");
-
- /**
- * Constant for unit of coordinate: north
- * @hide draft / provisional / internal are hidden on Android
- */
- public static final MeasureUnit NORTH = MeasureUnit.internalGetInstance("coordinate", "north");
-
- /**
- * Constant for unit of coordinate: south
- * @hide draft / provisional / internal are hidden on Android
- */
- public static final MeasureUnit SOUTH = MeasureUnit.internalGetInstance("coordinate", "south");
-
- /**
- * Constant for unit of coordinate: west
- * @hide draft / provisional / internal are hidden on Android
+ * @draft ICU 58, withdrawn
+ * public static final MeasureUnit EAST = MeasureUnit.internalGetInstance("coordinate", "east");
+ * public static final MeasureUnit NORTH = MeasureUnit.internalGetInstance("coordinate", "north");
+ * public static final MeasureUnit SOUTH = MeasureUnit.internalGetInstance("coordinate", "south");
+ * public static final MeasureUnit WEST = MeasureUnit.internalGetInstance("coordinate", "west");
*/
- public static final MeasureUnit WEST = MeasureUnit.internalGetInstance("coordinate", "west");
/**
* Constant for unit of digital: bit
@@ -769,6 +751,12 @@ public class MeasureUnit implements Serializable {
public static final MeasureUnit PICOMETER = MeasureUnit.internalGetInstance("length", "picometer");
/**
+ * Constant for unit of length: point
+ * @hide draft / provisional / internal are hidden on Android
+ */
+ public static final MeasureUnit POINT = MeasureUnit.internalGetInstance("length", "point");
+
+ /**
* Constant for unit of length: yard
*/
public static final MeasureUnit YARD = MeasureUnit.internalGetInstance("length", "yard");
@@ -1005,7 +993,6 @@ public class MeasureUnit implements Serializable {
/**
* Constant for unit of volume: gallon-imperial
- * @hide draft / provisional / internal are hidden on Android
*/
public static final MeasureUnit GALLON_IMPERIAL = MeasureUnit.internalGetInstance("volume", "gallon-imperial");
diff --git a/android_icu4j/src/main/java/android/icu/util/TimeZone.java b/android_icu4j/src/main/java/android/icu/util/TimeZone.java
index 55305dd8a..74e633d12 100644
--- a/android_icu4j/src/main/java/android/icu/util/TimeZone.java
+++ b/android_icu4j/src/main/java/android/icu/util/TimeZone.java
@@ -846,35 +846,28 @@ abstract public class TimeZone implements Serializable, Cloneable, Freezable<Tim
* @return a default <code>TimeZone</code>.
*/
public static TimeZone getDefault() {
- // Android patch (http://b/30979219) start.
- // Avoid race condition by copying defaultZone to a local variable.
- TimeZone result = defaultZone;
- if (result == null) {
- // Android patch (http://b/30937209) start.
- // Avoid a deadlock by always acquiring monitors in order (1) java.util.TimeZone.class
- // then (2) icu.util.TimeZone.class and not (2) then (1).
- // Without the synchronized here there is a possible deadlock between threads calling
- // this method and other threads calling methods on java.util.TimeZone. e.g.
- // java.util.TimeZone.setDefault() calls back into
- // icu.util.TimeZone.clearCachedDefault() so always acquires them in order (1) then (2).
+ // Copy the reference to the current defaultZone,
+ // so it won't be affected by setDefault().
+ TimeZone tmpDefaultZone = defaultZone;
+
+ if (tmpDefaultZone == null) {
synchronized (java.util.TimeZone.class) {
- synchronized (TimeZone.class) {
- result = defaultZone;
- if (result == null) {
+ synchronized(TimeZone.class) {
+ tmpDefaultZone = defaultZone;
+ if (tmpDefaultZone == null) {
if (TZ_IMPL == TIMEZONE_JDK) {
- result = new JavaTimeZone();
+ tmpDefaultZone = new JavaTimeZone();
} else {
java.util.TimeZone temp = java.util.TimeZone.getDefault();
- result = getFrozenTimeZone(temp.getID());
+ tmpDefaultZone = getFrozenTimeZone(temp.getID());
}
- defaultZone = result;
+ defaultZone = tmpDefaultZone;
}
}
}
- // Android patch (http://b/30937209) end.
}
- return result.cloneAsThawed();
- // Android patch (http://b/30979219) end.
+
+ return tmpDefaultZone.cloneAsThawed();
}
// Android patch (http://b/28949992) start.
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 2ebc1476b..68d36815b 100644
--- a/android_icu4j/src/main/java/android/icu/util/ULocale.java
+++ b/android_icu4j/src/main/java/android/icu/util/ULocale.java
@@ -298,96 +298,75 @@ public final class ULocale implements Serializable, Comparable<ULocale> {
private transient volatile BaseLocale baseLocale;
private transient volatile LocaleExtensions extensions;
+ /**
+ * This table lists pairs of locale ids for canonicalization. The
+ * The 1st item is the normalized id. The 2nd item is the
+ * canonicalized id. The 3rd is the keyword. The 4th is the keyword value.
+ */
+ private static String[][] CANONICALIZE_MAP = {
+ { "C", "en_US_POSIX", null, null }, /* POSIX name */
+ { "art_LOJBAN", "jbo", null, null }, /* registered name */
+ { "az_AZ_CYRL", "az_Cyrl_AZ", null, null }, /* .NET name */
+ { "az_AZ_LATN", "az_Latn_AZ", null, null }, /* .NET name */
+ { "ca_ES_PREEURO", "ca_ES", "currency", "ESP" },
+ { "cel_GAULISH", "cel__GAULISH", null, null }, /* registered name */
+ { "de_1901", "de__1901", null, null }, /* registered name */
+ { "de_1906", "de__1906", null, null }, /* registered name */
+ { "de__PHONEBOOK", "de", "collation", "phonebook" }, /* Old ICU name */
+ { "de_AT_PREEURO", "de_AT", "currency", "ATS" },
+ { "de_DE_PREEURO", "de_DE", "currency", "DEM" },
+ { "de_LU_PREEURO", "de_LU", "currency", "EUR" },
+ { "el_GR_PREEURO", "el_GR", "currency", "GRD" },
+ { "en_BOONT", "en__BOONT", null, null }, /* registered name */
+ { "en_SCOUSE", "en__SCOUSE", null, null }, /* registered name */
+ { "en_BE_PREEURO", "en_BE", "currency", "BEF" },
+ { "en_IE_PREEURO", "en_IE", "currency", "IEP" },
+ { "es__TRADITIONAL", "es", "collation", "traditional" }, /* Old ICU name */
+ { "es_ES_PREEURO", "es_ES", "currency", "ESP" },
+ { "eu_ES_PREEURO", "eu_ES", "currency", "ESP" },
+ { "fi_FI_PREEURO", "fi_FI", "currency", "FIM" },
+ { "fr_BE_PREEURO", "fr_BE", "currency", "BEF" },
+ { "fr_FR_PREEURO", "fr_FR", "currency", "FRF" },
+ { "fr_LU_PREEURO", "fr_LU", "currency", "LUF" },
+ { "ga_IE_PREEURO", "ga_IE", "currency", "IEP" },
+ { "gl_ES_PREEURO", "gl_ES", "currency", "ESP" },
+ { "hi__DIRECT", "hi", "collation", "direct" }, /* Old ICU name */
+ { "it_IT_PREEURO", "it_IT", "currency", "ITL" },
+ { "ja_JP_TRADITIONAL", "ja_JP", "calendar", "japanese" },
+ //{ "nb_NO_NY", "nn_NO", null, null },
+ { "nl_BE_PREEURO", "nl_BE", "currency", "BEF" },
+ { "nl_NL_PREEURO", "nl_NL", "currency", "NLG" },
+ { "pt_PT_PREEURO", "pt_PT", "currency", "PTE" },
+ { "sl_ROZAJ", "sl__ROZAJ", null, null }, /* registered name */
+ { "sr_SP_CYRL", "sr_Cyrl_RS", null, null }, /* .NET name */
+ { "sr_SP_LATN", "sr_Latn_RS", null, null }, /* .NET name */
+ { "sr_YU_CYRILLIC", "sr_Cyrl_RS", null, null }, /* Linux name */
+ { "th_TH_TRADITIONAL", "th_TH", "calendar", "buddhist" }, /* Old ICU name */
+ { "uz_UZ_CYRILLIC", "uz_Cyrl_UZ", null, null }, /* Linux name */
+ { "uz_UZ_CYRL", "uz_Cyrl_UZ", null, null }, /* .NET name */
+ { "uz_UZ_LATN", "uz_Latn_UZ", null, null }, /* .NET name */
+ { "zh_CHS", "zh_Hans", null, null }, /* .NET name */
+ { "zh_CHT", "zh_Hant", null, null }, /* .NET name */
+ { "zh_GAN", "zh__GAN", null, null }, /* registered name */
+ { "zh_GUOYU", "zh", null, null }, /* registered name */
+ { "zh_HAKKA", "zh__HAKKA", null, null }, /* registered name */
+ { "zh_MIN", "zh__MIN", null, null }, /* registered name */
+ { "zh_MIN_NAN", "zh__MINNAN", null, null }, /* registered name */
+ { "zh_WUU", "zh__WUU", null, null }, /* registered name */
+ { "zh_XIANG", "zh__XIANG", null, null }, /* registered name */
+ { "zh_YUE", "zh__YUE", null, null } /* registered name */
+ };
- private static String[][] CANONICALIZE_MAP;
- private static String[][] variantsToKeywords;
+ /**
+ * This table lists pairs of locale ids for canonicalization.
+ * The first item is the normalized variant id.
+ */
+ private static String[][] variantsToKeywords = {
+ { "EURO", "currency", "EUR" },
+ { "PINYIN", "collation", "pinyin" }, /* Solaris variant */
+ { "STROKE", "collation", "stroke" } /* Solaris variant */
+ };
- private static void initCANONICALIZE_MAP() {
- if (CANONICALIZE_MAP == null) {
- /**
- * This table lists pairs of locale ids for canonicalization. The
- * The 1st item is the normalized id. The 2nd item is the
- * canonicalized id. The 3rd is the keyword. The 4th is the keyword value.
- */
- String[][] tempCANONICALIZE_MAP = {
- // { EMPTY_STRING, "en_US_POSIX", null, null }, /* .NET name */
- { "C", "en_US_POSIX", null, null }, /* POSIX name */
- { "art_LOJBAN", "jbo", null, null }, /* registered name */
- { "az_AZ_CYRL", "az_Cyrl_AZ", null, null }, /* .NET name */
- { "az_AZ_LATN", "az_Latn_AZ", null, null }, /* .NET name */
- { "ca_ES_PREEURO", "ca_ES", "currency", "ESP" },
- { "cel_GAULISH", "cel__GAULISH", null, null }, /* registered name */
- { "de_1901", "de__1901", null, null }, /* registered name */
- { "de_1906", "de__1906", null, null }, /* registered name */
- { "de__PHONEBOOK", "de", "collation", "phonebook" }, /* Old ICU name */
- { "de_AT_PREEURO", "de_AT", "currency", "ATS" },
- { "de_DE_PREEURO", "de_DE", "currency", "DEM" },
- { "de_LU_PREEURO", "de_LU", "currency", "EUR" },
- { "el_GR_PREEURO", "el_GR", "currency", "GRD" },
- { "en_BOONT", "en__BOONT", null, null }, /* registered name */
- { "en_SCOUSE", "en__SCOUSE", null, null }, /* registered name */
- { "en_BE_PREEURO", "en_BE", "currency", "BEF" },
- { "en_IE_PREEURO", "en_IE", "currency", "IEP" },
- { "es__TRADITIONAL", "es", "collation", "traditional" }, /* Old ICU name */
- { "es_ES_PREEURO", "es_ES", "currency", "ESP" },
- { "eu_ES_PREEURO", "eu_ES", "currency", "ESP" },
- { "fi_FI_PREEURO", "fi_FI", "currency", "FIM" },
- { "fr_BE_PREEURO", "fr_BE", "currency", "BEF" },
- { "fr_FR_PREEURO", "fr_FR", "currency", "FRF" },
- { "fr_LU_PREEURO", "fr_LU", "currency", "LUF" },
- { "ga_IE_PREEURO", "ga_IE", "currency", "IEP" },
- { "gl_ES_PREEURO", "gl_ES", "currency", "ESP" },
- { "hi__DIRECT", "hi", "collation", "direct" }, /* Old ICU name */
- { "it_IT_PREEURO", "it_IT", "currency", "ITL" },
- { "ja_JP_TRADITIONAL", "ja_JP", "calendar", "japanese" },
- // { "nb_NO_NY", "nn_NO", null, null },
- { "nl_BE_PREEURO", "nl_BE", "currency", "BEF" },
- { "nl_NL_PREEURO", "nl_NL", "currency", "NLG" },
- { "pt_PT_PREEURO", "pt_PT", "currency", "PTE" },
- { "sl_ROZAJ", "sl__ROZAJ", null, null }, /* registered name */
- { "sr_SP_CYRL", "sr_Cyrl_RS", null, null }, /* .NET name */
- { "sr_SP_LATN", "sr_Latn_RS", null, null }, /* .NET name */
- { "sr_YU_CYRILLIC", "sr_Cyrl_RS", null, null }, /* Linux name */
- { "th_TH_TRADITIONAL", "th_TH", "calendar", "buddhist" }, /* Old ICU name */
- { "uz_UZ_CYRILLIC", "uz_Cyrl_UZ", null, null }, /* Linux name */
- { "uz_UZ_CYRL", "uz_Cyrl_UZ", null, null }, /* .NET name */
- { "uz_UZ_LATN", "uz_Latn_UZ", null, null }, /* .NET name */
- { "zh_CHS", "zh_Hans", null, null }, /* .NET name */
- { "zh_CHT", "zh_Hant", null, null }, /* .NET name */
- { "zh_GAN", "zh__GAN", null, null }, /* registered name */
- { "zh_GUOYU", "zh", null, null }, /* registered name */
- { "zh_HAKKA", "zh__HAKKA", null, null }, /* registered name */
- { "zh_MIN", "zh__MIN", null, null }, /* registered name */
- { "zh_MIN_NAN", "zh__MINNAN", null, null }, /* registered name */
- { "zh_WUU", "zh__WUU", null, null }, /* registered name */
- { "zh_XIANG", "zh__XIANG", null, null }, /* registered name */
- { "zh_YUE", "zh__YUE", null, null } /* registered name */
- };
-
- synchronized (ULocale.class) {
- if (CANONICALIZE_MAP == null) {
- CANONICALIZE_MAP = tempCANONICALIZE_MAP;
- }
- }
- }
- if (variantsToKeywords == null) {
- /**
- * This table lists pairs of locale ids for canonicalization. The
- * The first item is the normalized variant id.
- */
- String[][] tempVariantsToKeywords = {
- { "EURO", "currency", "EUR" },
- { "PINYIN", "collation", "pinyin" }, /* Solaris variant */
- { "STROKE", "collation", "stroke" } /* Solaris variant */
- };
-
- synchronized (ULocale.class) {
- if (variantsToKeywords == null) {
- variantsToKeywords = tempVariantsToKeywords;
- }
- }
- }
- }
/**
* Private constructor used by static initializers.
@@ -1165,8 +1144,6 @@ public final class ULocale implements Serializable, Comparable<ULocale> {
// we have an ID in the form xx_Yyyy_ZZ_KKKKK
- initCANONICALIZE_MAP();
-
/* convert the variants to appropriate ID */
for (int i = 0; i < variantsToKeywords.length; i++) {
String[] vals = variantsToKeywords[i];
diff --git a/android_icu4j/src/main/java/android/icu/util/UniversalTimeScale.java b/android_icu4j/src/main/java/android/icu/util/UniversalTimeScale.java
index c538aba01..81d2cf0db 100644
--- a/android_icu4j/src/main/java/android/icu/util/UniversalTimeScale.java
+++ b/android_icu4j/src/main/java/android/icu/util/UniversalTimeScale.java
@@ -116,7 +116,10 @@ public final class UniversalTimeScale
/**
* This is the first unused time scale value.
+ *
+ * @deprecated ICU 59
*/
+ @Deprecated
public static final int MAX_SCALE = 10;
/**
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 7c771a952..e7319629d 100644
--- a/android_icu4j/src/main/java/android/icu/util/VersionInfo.java
+++ b/android_icu4j/src/main/java/android/icu/util/VersionInfo.java
@@ -155,7 +155,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 = "58b";
+ public static final String ICU_DATA_VERSION_PATH = "59b";
/**
* Data version in ICU4J.
@@ -521,8 +521,8 @@ public final class VersionInfo implements Comparable<VersionInfo>
UNICODE_8_0 = getInstance(8, 0, 0, 0);
UNICODE_9_0 = getInstance(9, 0, 0, 0);
- ICU_VERSION = getInstance(58, 2, 0, 0);
- ICU_DATA_VERSION = getInstance(58, 2, 0, 0);
+ ICU_VERSION = getInstance(59, 1, 0, 0);
+ ICU_DATA_VERSION = getInstance(59, 1, 0, 0);
UNICODE_VERSION = UNICODE_9_0;
UCOL_RUNTIME_VERSION = getInstance(9);