diff options
author | Fredrik Roubert <roubert@google.com> | 2017-08-09 18:29:05 +0200 |
---|---|---|
committer | Fredrik Roubert <roubert@google.com> | 2017-08-09 18:29:05 +0200 |
commit | 495cb271e305cfb399d463f32210a371198f0abf (patch) | |
tree | 45ad7f39c66d45330a25d76d75d5211d484d3ee1 /android_icu4j/src/main/java/android | |
parent | eb03f4ed3153ac1c37b9a6b8d322238139f7c8a4 (diff) | |
download | android_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')
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 + * "<NumberStringBuilder [-123.45] [-iii.ff]>" + * + * @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 <Properties>. + */ + 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}. 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}. 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<3; ++j) { - * System.out.println("FORMAT"); - * for (int i = 0; i < 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() - * + " -> " + form.format(myNumber)); - * } catch (Exception e) {} - * try { - * System.out.println(" -> " + 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<=NumberFormat.PLURALCURRENCYSTYLE; ++j) { - * NumberFormat format = NumberFormat.getInstance(locale, j); - * try { - * // Assume format is a DecimalFormat - * System.out.print(": " + ((DecimalFormat) format).toPattern() - * + " -> " + form.format(myNumber)); - * } catch (Exception e) {} - * try { - * System.out.println(" -> " + 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>\u2030</code> - * <td>Prefix or suffix - * <td>Yes - * <td>Multiply by 1000 and show as per mille - * <tr style="vertical-align: top;"> - * <td><code>¤</code> (<code>\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 "#,##,###" formats the number 123456789 as - * "12,34,56,789". - * - * <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>\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>\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 + * "#,##,###" formats the number 123456789 as "12,34,56,789". + * <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 %" (with a non-breaking space), and "35 %" (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 <= pos.getIndex() < 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 -> "123", and "123" -> 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 -> "123", and "123" -> 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 < 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 < 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 < 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 < 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 <= 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 >= 1 indicating the fewest exponent - * digits that will be shown - * @exception IllegalArgumentException if <code>minExpDig</code> < 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 -> - * 12345.; OFF: 12345 -> 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 -> "3,456." if false, 3456.00 -> "3456" This - * is independent of parsing. If you want parsing to stop at the decimal point, use - * setParseIntegerOnly. - * - * <p> - * Example: Decimal ON: 12345 -> 12345.; OFF: 12345 -> 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#" -> 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#" -> 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 >= 1 and <= - * maxSignificantDigits. Ignored unless useSignificantDigits == true. - * - * @serial - */ - private int minSignificantDigits = 1; - - /** - * The maximum number of significant digits to show. Must be >= - * 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 >= 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<? extends Measure>, 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 <. * @return '<' */ - 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 >. * @return '>' */ - 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 < * @return '<' */ - 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 >. * @return '>' */ - 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 > * @return '>' */ - 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 < * @return '<' */ - 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 © 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 <= start <= * limit</code>. * @param limit the ending index, exclusive; <code>start <= limit <= @@ -168,7 +166,7 @@ public interface Replaceable { * dest >= 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 © 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 <= start <= 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 <= start <= * limit</code>. * @param limit the ending index, exclusive; <code>start <= limit <= @@ -169,6 +174,7 @@ public class ReplaceableString implements Replaceable { * Implementations of this method may assume that <code>dest <= start || * dest >= 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 > 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 © 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() => "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); |