--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+
+import newapi.impl.AffixPatternProvider;
+
+/** Implements a recursive descent parser for decimal format patterns. */
+public class LdmlPatternInfo {
+
+ public static PatternParseResult parse(String patternString) {
+ ParserState state = new ParserState(patternString);
+ PatternParseResult result = new PatternParseResult(patternString);
+ consumePattern(state, result);
+ return result;
+ }
+
+ /**
+ * An internal, intermediate data structure used for storing parse results before they are
+ * finalized into a DecimalFormatPattern.Builder.
+ */
+ public static class PatternParseResult implements AffixPatternProvider {
+ public String pattern;
+ public LdmlPatternInfo.SubpatternParseResult positive;
+ public LdmlPatternInfo.SubpatternParseResult negative;
+
+ private PatternParseResult(String pattern) {
+ this.pattern = pattern;
+ }
+
+ @Override
+ public char charAt(int flags, int index) {
+ long endpoints = getEndpoints(flags);
+ int left = (int) (endpoints & 0xffffffff);
+ int right = (int) (endpoints >>> 32);
+ if (index < 0 || index >= right - left) {
+ throw new IndexOutOfBoundsException();
+ }
+ return pattern.charAt(left + index);
+ }
+
+ @Override
+ public int length(int flags) {
+ return getLengthFromEndpoints(getEndpoints(flags));
+ }
+
+ public static int getLengthFromEndpoints(long endpoints) {
+ int left = (int) (endpoints & 0xffffffff);
+ int right = (int) (endpoints >>> 32);
+ return right - left;
+ }
+
+ public String getString(int flags) {
+ long endpoints = getEndpoints(flags);
+ int left = (int) (endpoints & 0xffffffff);
+ int right = (int) (endpoints >>> 32);
+ if (left == right) {
+ return "";
+ }
+ return pattern.substring(left, right);
+ }
+
+ private long getEndpoints(int flags) {
+ boolean prefix = (flags & Flags.PREFIX) != 0;
+ boolean isNegative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
+ boolean padding = (flags & Flags.PADDING) != 0;
+ if (isNegative && padding) {
+ return negative.paddingEndpoints;
+ } else if (padding) {
+ return positive.paddingEndpoints;
+ } else if (prefix && isNegative) {
+ return negative.prefixEndpoints;
+ } else if (prefix) {
+ return positive.prefixEndpoints;
+ } else if (isNegative) {
+ return negative.suffixEndpoints;
+ } else {
+ return positive.suffixEndpoints;
+ }
+ }
+
+ @Override
+ public boolean positiveHasPlusSign() {
+ return positive.hasPlusSign;
+ }
+
+ @Override
+ public boolean hasNegativeSubpattern() {
+ return negative != null;
+ }
+
+ @Override
+ public boolean negativeHasMinusSign() {
+ return negative.hasMinusSign;
+ }
+
+ @Override
+ public boolean hasCurrencySign() {
+ return positive.hasCurrencySign || (negative != null && negative.hasCurrencySign);
+ }
+
+ @Override
+ public boolean containsSymbolType(int type) {
+ return AffixPatternUtils.containsType(pattern, type);
+ }
+ }
+
+ public static class SubpatternParseResult {
+ public long groupingSizes = 0x0000ffffffff0000L;
+ public int minimumIntegerDigits = 0;
+ public int totalIntegerDigits = 0;
+ public int minimumFractionDigits = 0;
+ public int maximumFractionDigits = 0;
+ public int minimumSignificantDigits = 0;
+ public int maximumSignificantDigits = 0;
+ public boolean hasDecimal = false;
+ public int paddingWidth = 0;
+ public PadPosition paddingLocation = null;
+ public FormatQuantity4 rounding = null;
+ public boolean exponentShowPlusSign = false;
+ public int exponentDigits = 0;
+ public boolean hasPercentSign = false;
+ public boolean hasPerMilleSign = false;
+ public boolean hasCurrencySign = false;
+ public boolean hasMinusSign = false;
+ public boolean hasPlusSign = false;
+
+ public long prefixEndpoints = 0;
+ public long suffixEndpoints = 0;
+ public long paddingEndpoints = 0;
+ }
+
+ /** 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());
+ }
+ }
+
+ private static void consumePattern(
+ LdmlPatternInfo.ParserState state, LdmlPatternInfo.PatternParseResult result) {
+ // pattern := subpattern (';' subpattern)?
+ result.positive = new SubpatternParseResult();
+ consumeSubpattern(state, result.positive);
+ if (state.peek() == ';') {
+ state.next(); // consume the ';'
+ // Don't consume the negative subpattern if it is empty (trailing ';')
+ if (state.peek() != -1) {
+ result.negative = new SubpatternParseResult();
+ consumeSubpattern(state, result.negative);
+ }
+ }
+ if (state.peek() != -1) {
+ throw state.toParseException("Found unquoted special character");
+ }
+ }
+
+ private static void consumeSubpattern(
+ LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
+ // subpattern := literals? number exponent? literals?
+ consumePadding(state, result, PadPosition.BEFORE_PREFIX);
+ result.prefixEndpoints = consumeAffix(state, result);
+ consumePadding(state, result, PadPosition.AFTER_PREFIX);
+ consumeFormat(state, result);
+ consumeExponent(state, result);
+ consumePadding(state, result, PadPosition.BEFORE_SUFFIX);
+ result.suffixEndpoints = consumeAffix(state, result);
+ consumePadding(state, result, PadPosition.AFTER_SUFFIX);
+ }
+
+ private static void consumePadding(
+ LdmlPatternInfo.ParserState state,
+ LdmlPatternInfo.SubpatternParseResult result,
+ PadPosition paddingLocation) {
+ if (state.peek() != '*') {
+ return;
+ }
+ result.paddingLocation = paddingLocation;
+ state.next(); // consume the '*'
+ result.paddingEndpoints |= state.offset;
+ consumeLiteral(state);
+ result.paddingEndpoints |= ((long) state.offset) << 32;
+ }
+
+ private static long consumeAffix(
+ LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
+ // literals := { literal }
+ long endpoints = state.offset;
+ outer:
+ 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
+ break outer;
+
+ case '%':
+ result.hasPercentSign = true;
+ break;
+
+ case '‰':
+ result.hasPerMilleSign = true;
+ break;
+
+ case '¤':
+ result.hasCurrencySign = true;
+ break;
+
+ case '-':
+ result.hasMinusSign = true;
+ break;
+
+ case '+':
+ result.hasPlusSign = true;
+ break;
+ }
+ consumeLiteral(state);
+ }
+ endpoints |= ((long) state.offset) << 32;
+ return endpoints;
+ }
+
+ private static void consumeLiteral(LdmlPatternInfo.ParserState state) {
+ if (state.peek() == -1) {
+ throw state.toParseException("Expected unquoted literal but found EOL");
+ } else if (state.peek() == '\'') {
+ state.next(); // consume the starting quote
+ while (state.peek() != '\'') {
+ if (state.peek() == -1) {
+ throw state.toParseException("Expected quoted literal but found EOL");
+ } else {
+ state.next(); // consume a quoted character
+ }
+ }
+ state.next(); // consume the ending quote
+ } else {
+ // consume a non-quoted literal character
+ state.next();
+ }
+ }
+
+ private static void consumeFormat(
+ LdmlPatternInfo.ParserState state, LdmlPatternInfo.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(
+ LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
+ boolean seenSignificantDigitMarker = false;
+ boolean seenDigit = false;
+
+ outer:
+ while (true) {
+ switch (state.peek()) {
+ case ',':
+ result.paddingWidth += 1;
+ result.groupingSizes <<= 16;
+ break;
+
+ case '#':
+ if (seenDigit) throw state.toParseException("# cannot follow 0 before decimal point");
+ result.paddingWidth += 1;
+ result.groupingSizes += 1;
+ result.totalIntegerDigits += (seenSignificantDigitMarker ? 0 : 1);
+ // no change to result.minimumIntegerDigits
+ // no change to result.minimumSignificantDigits
+ result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0);
+ if (result.rounding != null) {
+ 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 += 1;
+ result.totalIntegerDigits += 1;
+ // no change to result.minimumIntegerDigits
+ result.minimumSignificantDigits += 1;
+ result.maximumSignificantDigits += 1;
+ if (result.rounding != null) {
+ 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 += 1;
+ result.totalIntegerDigits += 1;
+ result.minimumIntegerDigits += 1;
+ // no change to result.minimumSignificantDigits
+ // no change to result.maximumSignificantDigits
+ if (state.peek() != '0' && result.rounding == null) {
+ result.rounding = new FormatQuantity4();
+ }
+ if (result.rounding != null) {
+ result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true);
+ }
+ break;
+
+ default:
+ break outer;
+ }
+ state.next(); // consume the symbol
+ }
+
+ // Disallow patterns with a trailing ',' or with two ',' next to each other
+ short grouping1 = (short) (result.groupingSizes & 0xffff);
+ short grouping2 = (short) ((result.groupingSizes >>> 16) & 0xffff);
+ short grouping3 = (short) ((result.groupingSizes >>> 32) & 0xffff);
+ if (grouping1 == 0 && grouping2 != -1) {
+ throw state.toParseException("Trailing grouping separator is invalid");
+ }
+ if (grouping2 == 0 && grouping3 != -1) {
+ throw state.toParseException("Grouping width of zero is invalid");
+ }
+ }
+
+ private static void consumeFractionFormat(
+ LdmlPatternInfo.ParserState state, LdmlPatternInfo.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 {
+ if (result.rounding == null) {
+ result.rounding = new FormatQuantity4();
+ }
+ result.rounding.appendDigit((byte) (state.peek() - '0'), zeroCounter, false);
+ zeroCounter = 0;
+ }
+ break;
+
+ default:
+ return;
+ }
+ state.next(); // consume the symbol
+ }
+ }
+
+ private static void consumeExponent(
+ LdmlPatternInfo.ParserState state, LdmlPatternInfo.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++;
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.text.AttributedCharacterIterator;
+import java.text.FieldPosition;
+import java.util.Arrays;
+import java.util.Locale;
+
+import com.ibm.icu.impl.number.FormatQuantityBCD;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.text.NumberingSystem;
+import com.ibm.icu.text.PluralRules.IFixedDecimal;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.CurrencyAmount;
+import com.ibm.icu.util.ICUUncheckedIOException;
+import com.ibm.icu.util.Measure;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+import newapi.impl.GroupingImpl;
+import newapi.impl.IntegerWidthImpl;
+import newapi.impl.MicroProps;
+import newapi.impl.NotationImpl.NotationCompactImpl;
+import newapi.impl.NotationImpl.NotationScientificImpl;
+import newapi.impl.NumberFormatterImpl;
+import newapi.impl.PaddingImpl;
+import newapi.impl.RoundingImpl.RoundingImplCurrency;
+import newapi.impl.RoundingImpl.RoundingImplFraction;
+import newapi.impl.RoundingImpl.RoundingImplIncrement;
+import newapi.impl.RoundingImpl.RoundingImplInfinity;
+import newapi.impl.RoundingImpl.RoundingImplSignificant;
+
+public final class NumberFormatter {
+
+ public interface IRounding {
+ public BigDecimal round(BigDecimal input);
+ }
+
+ public interface IGrouping {
+ public boolean groupAtPosition(int position, BigDecimal input);
+ }
+
+ // This could possibly be combined into MeasureFormat.FormatWidth
+ public static enum CurrencyDisplay {
+ SYMBOL, // ¤
+ ISO_4217, // ¤¤
+ DISPLAY_NAME, // ¤¤¤
+ SYMBOL_NARROW, // ¤¤¤¤
+ HIDDEN, // uses currency rounding and formatting but omits the currency symbol
+ // TODO: For hidden, what to do if currency symbol appears in the middle, as in Portugal ?
+ }
+
+ public static enum DecimalMarkDisplay {
+ AUTO,
+ ALWAYS_SHOWN,
+ }
+
+ public static enum SignDisplay {
+ AUTO,
+ ALWAYS_SHOWN,
+ NEVER_SHOWN,
+ }
+
+ public static class UnlocalizedNumberFormatter {
+
+ public UnlocalizedNumberFormatter notation(Notation notation) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public UnlocalizedNumberFormatter unit(MeasureUnit unit) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public UnlocalizedNumberFormatter rounding(IRounding rounding) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public UnlocalizedNumberFormatter grouping(IGrouping grouping) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public UnlocalizedNumberFormatter padding(Padding padding) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public UnlocalizedNumberFormatter integerWidth(IntegerWidth style) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public UnlocalizedNumberFormatter symbols(DecimalFormatSymbols symbols) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public UnlocalizedNumberFormatter symbols(NumberingSystem ns) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public UnlocalizedNumberFormatter unitWidth(FormatWidth style) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public UnlocalizedNumberFormatter sign(SignDisplay style) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public UnlocalizedNumberFormatter decimal(DecimalMarkDisplay style) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public LocalizedNumberFormatter locale(Locale locale) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public LocalizedNumberFormatter locale(ULocale locale) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public String toSkeleton() {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ // Prevent external subclassing with private constructor
+ private UnlocalizedNumberFormatter() {}
+ }
+
+ public static class LocalizedNumberFormatter extends UnlocalizedNumberFormatter {
+
+ @Override
+ public UnlocalizedNumberFormatter notation(Notation notation) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ @Override
+ public LocalizedNumberFormatter unit(MeasureUnit unit) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ @Override
+ public LocalizedNumberFormatter rounding(IRounding rounding) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ @Override
+ public LocalizedNumberFormatter grouping(IGrouping grouping) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ @Override
+ public LocalizedNumberFormatter padding(Padding padding) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ @Override
+ public LocalizedNumberFormatter integerWidth(IntegerWidth style) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ @Override
+ public LocalizedNumberFormatter symbols(DecimalFormatSymbols symbols) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ @Override
+ public LocalizedNumberFormatter symbols(NumberingSystem ns) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ @Override
+ public LocalizedNumberFormatter unitWidth(FormatWidth style) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ @Override
+ public LocalizedNumberFormatter sign(SignDisplay style) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ @Override
+ public LocalizedNumberFormatter decimal(DecimalMarkDisplay style) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public NumberFormatterResult format(long input) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public NumberFormatterResult format(double input) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public NumberFormatterResult format(Number input) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ public NumberFormatterResult format(Measure input) {
+ throw new AssertionError("See NumberFormatterImpl");
+ }
+
+ // Prevent external subclassing with private constructor
+ private LocalizedNumberFormatter() {}
+
+ /**
+ * @internal
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Internal extends LocalizedNumberFormatter {}
+ }
+
+ public static UnlocalizedNumberFormatter fromSkeleton(String skeleton) {
+ // FIXME
+ throw new UnsupportedOperationException();
+ }
+
+ public static UnlocalizedNumberFormatter with() {
+ return NumberFormatterImpl.with();
+ }
+
+ public static LocalizedNumberFormatter withLocale(Locale locale) {
+ return NumberFormatterImpl.with().locale(locale);
+ }
+
+ public static LocalizedNumberFormatter withLocale(ULocale locale) {
+ return NumberFormatterImpl.with().locale(locale);
+ }
+
+ public static class NumberFormatterResult {
+ NumberStringBuilder nsb;
+ FormatQuantityBCD fq;
+ MicroProps micros;
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ public NumberFormatterResult(NumberStringBuilder nsb, FormatQuantityBCD fq, MicroProps micros) {
+ this.nsb = nsb;
+ this.fq = fq;
+ this.micros = micros;
+ }
+
+ @Override
+ public String toString() {
+ return nsb.toString();
+ }
+
+ public <A extends Appendable> A appendTo(A appendable) {
+ try {
+ appendable.append(nsb);
+ } catch (IOException e) {
+ // Throw as an unchecked exception to avoid users needing try/catch
+ throw new ICUUncheckedIOException(e);
+ }
+ return appendable;
+ }
+
+ public AttributedCharacterIterator toAttributedCharacterIterator() {
+ return nsb.getIterator();
+ }
+
+ /**
+ * @internal
+ * @deprecated This API a technology preview. It is not stable and may change or go away in an
+ * upcoming release.
+ */
+ @Deprecated
+ public void populateFieldPosition(FieldPosition fieldPosition, int offset) {
+ nsb.populateFieldPosition(fieldPosition, offset);
+ fq.populateUFieldPosition(fieldPosition);
+ }
+
+ /**
+ * @internal
+ * @deprecated This API a technology preview. It is not stable and may change or go away in an
+ * upcoming release.
+ */
+ @Deprecated
+ public String getPrefix() {
+ return micros.modOuter.getPrefix()
+ + micros.modMiddle.getPrefix()
+ + micros.modInner.getPrefix();
+ }
+
+ /**
+ * @internal
+ * @deprecated This API a technology preview. It is not stable and may change or go away in an
+ * upcoming release.
+ */
+ @Deprecated
+ public String getSuffix() {
+ return micros.modInner.getSuffix()
+ + micros.modMiddle.getSuffix()
+ + micros.modOuter.getSuffix();
+ }
+
+ /**
+ * @internal
+ * @deprecated This API a technology preview. It is not stable and may change or go away in an
+ * upcoming release.
+ */
+ @Deprecated
+ public IFixedDecimal getFixedDecimal() {
+ return fq;
+ }
+
+ public BigDecimal toBigDecimal() {
+ return fq.toBigDecimal();
+ }
+
+ @Override
+ public int hashCode() {
+ // NumberStringBuilder and BigDecimal are mutable, so we can't call
+ // #equals() or #hashCode() on them directly.
+ return Arrays.hashCode(nsb.toCharArray())
+ ^ Arrays.hashCode(nsb.toFieldArray())
+ ^ fq.toBigDecimal().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (other == null) return false;
+ if (!(other instanceof NumberFormatterResult)) return false;
+ // NumberStringBuilder and BigDecimal are mutable, so we can't call
+ // #equals() or #hashCode() on them directly.
+ NumberFormatterResult _other = (NumberFormatterResult) other;
+ return Arrays.equals(nsb.toCharArray(), _other.nsb.toCharArray())
+ ^ Arrays.equals(nsb.toFieldArray(), _other.nsb.toFieldArray())
+ ^ fq.toBigDecimal().equals(_other.fq.toBigDecimal());
+ }
+ }
+
+ public static class Notation {
+
+ // FIXME: Support engineering intervals other than 3?
+ public static final NotationScientific SCIENTIFIC = new NotationScientificImpl(1);
+ public static final NotationScientific ENGINEERING = new NotationScientificImpl(3);
+ public static final NotationCompact COMPACT_SHORT = new NotationCompactImpl(CompactStyle.SHORT);
+ public static final NotationCompact COMPACT_LONG = new NotationCompactImpl(CompactStyle.LONG);
+ public static final NotationSimple SIMPLE = new NotationSimple();
+
+ // Prevent subclassing
+ private Notation() {}
+ }
+
+ @SuppressWarnings("unused")
+ public static class NotationScientific extends Notation {
+
+ public NotationScientific withMinExponentDigits(int minExponentDigits) {
+ // Overridden in NotationImpl
+ throw new AssertionError();
+ }
+
+ public NotationScientific withExponentSignDisplay(SignDisplay exponentSignDisplay) {
+ // Overridden in NotationImpl
+ throw new AssertionError();
+ }
+
+ // Prevent subclassing
+ private NotationScientific() {}
+
+ /**
+ * @internal
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Internal extends NotationScientific {}
+ }
+
+ public static class NotationCompact extends Notation {
+
+ // Prevent subclassing
+ private NotationCompact() {}
+
+ /**
+ * @internal
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Internal extends NotationCompact {}
+ }
+
+ public static class NotationSimple extends Notation {
+ // Prevent subclassing
+ private NotationSimple() {}
+ }
+
+ public static class Rounding implements IRounding {
+
+ protected static final int MAX_VALUE = 100;
+
+ public static final Rounding NONE = new RoundingImplInfinity();
+ public static final Rounding INTEGER = new RoundingImplFraction();
+
+ public static FractionRounding fixedFraction(int minMaxFrac) {
+ if (minMaxFrac >= 0 && minMaxFrac <= MAX_VALUE) {
+ return RoundingImplFraction.getInstance(minMaxFrac, minMaxFrac);
+ } else {
+ throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
+ }
+ }
+
+ public static FractionRounding minFraction(int minFrac) {
+ if (minFrac >= 0 && minFrac < MAX_VALUE) {
+ return RoundingImplFraction.getInstance(minFrac, Integer.MAX_VALUE);
+ } else {
+ throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
+ }
+ }
+
+ public static FractionRounding maxFraction(int maxFrac) {
+ if (maxFrac >= 0 && maxFrac < MAX_VALUE) {
+ return RoundingImplFraction.getInstance(0, maxFrac);
+ } else {
+ throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
+ }
+ }
+
+ public static FractionRounding minMaxFraction(int minFrac, int maxFrac) {
+ if (minFrac >= 0 && maxFrac <= MAX_VALUE && minFrac <= maxFrac) {
+ return RoundingImplFraction.getInstance(minFrac, maxFrac);
+ } else {
+ throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
+ }
+ }
+
+ public static Rounding fixedFigures(int minMaxSig) {
+ if (minMaxSig > 0 && minMaxSig <= MAX_VALUE) {
+ return RoundingImplSignificant.getInstance(minMaxSig, minMaxSig);
+ } else {
+ throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
+ }
+ }
+
+ public static Rounding minFigures(int minSig) {
+ if (minSig > 0 && minSig <= MAX_VALUE) {
+ return RoundingImplSignificant.getInstance(minSig, Integer.MAX_VALUE);
+ } else {
+ throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
+ }
+ }
+
+ public static Rounding maxFigures(int maxSig) {
+ if (maxSig > 0 && maxSig <= MAX_VALUE) {
+ return RoundingImplSignificant.getInstance(0, maxSig);
+ } else {
+ throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
+ }
+ }
+
+ public static Rounding minMaxFigures(int minSig, int maxSig) {
+ if (minSig > 0 && maxSig <= MAX_VALUE && minSig <= maxSig) {
+ return RoundingImplSignificant.getInstance(minSig, maxSig);
+ } else {
+ throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
+ }
+ }
+
+ public static Rounding increment(BigDecimal roundingIncrement) {
+ if (roundingIncrement == null) {
+ throw new IllegalArgumentException("Rounding increment must be non-null");
+ } else if (roundingIncrement.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("Rounding increment must be positive");
+ } else {
+ return RoundingImplIncrement.getInstance(roundingIncrement);
+ }
+ }
+
+ public static CurrencyRounding currency(CurrencyUsage currencyUsage) {
+ if (currencyUsage != CurrencyUsage.STANDARD && currencyUsage != CurrencyUsage.CASH) {
+ throw new IllegalArgumentException("Unknown CurrencyUsage: " + currencyUsage);
+ } else {
+ return RoundingImplCurrency.getInstance(currencyUsage);
+ }
+ }
+
+ /**
+ * Sets the {@link java.math.RoundingMode} to use when picking the direction to round (up or
+ * down).
+ *
+ * <p>Common values include {@link RoundingMode#HALF_EVEN}, {@link RoundingMode#HALF_UP}, and
+ * {@link RoundingMode#CEILING}. The default is HALF_EVEN.
+ *
+ * @param roundingMode The RoundingMode to use.
+ * @return An immutable object for chaining.
+ */
+ public Rounding withMode(RoundingMode roundingMode) {
+ // Overridden in RoundingImpl
+ throw new AssertionError();
+ }
+
+ /**
+ * Sets a MathContext directly instead of RoundingMode.
+ *
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ public Rounding withMode(MathContext mathContext) {
+ // Overridden in RoundingImpl
+ throw new AssertionError();
+ }
+
+ @Override
+ public BigDecimal round(BigDecimal input) {
+ // Overridden in RoundingImpl
+ throw new AssertionError();
+ }
+
+ // Prevent subclassing
+ private Rounding() {}
+
+ /**
+ * @internal
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Internal extends Rounding {}
+ }
+
+ /**
+ * A rounding strategy based on a minimum and/or maximum number of fraction digits. Allows for a
+ * minimum or maximum number of significant digits to be specified.
+ */
+ public static class FractionRounding extends Rounding {
+ /**
+ * Ensures that no less than this number of significant figures are retained when rounding
+ * according to fraction rules.
+ *
+ * <p>For example, with integer rounding, the number 3.141 becomes "3". However, with minimum
+ * figures set to 2, 3.141 becomes "3.1" instead.
+ *
+ * <p>This setting does not affect the number of trailing zeros. For example, 3.01 would print
+ * as "3", not "3.0".
+ *
+ * @param minFigures The number of significant figures to guarantee.
+ * @return An immutable object for chaining.
+ */
+ public Rounding withMinFigures(int minFigures) {
+ // Overridden in RoundingImpl
+ throw new AssertionError();
+ }
+
+ /**
+ * Ensures that no more than this number of significant figures are retained when rounding
+ * according to fraction rules.
+ *
+ * <p>For example, with integer rounding, the number 123.4 becomes "123". However, with maximum
+ * figures set to 2, 123.4 becomes "120" instead.
+ *
+ * <p>This setting does not affect the number of trailing zeros. For example, with fixed
+ * fraction of 2, 123.4 would become "120.00".
+ *
+ * @param maxFigures
+ * @return An immutable object for chaining.
+ */
+ public Rounding withMaxFigures(int maxFigures) {
+ // Overridden in RoundingImpl
+ throw new AssertionError();
+ }
+
+ // Prevent subclassing
+ private FractionRounding() {}
+
+ /**
+ * @internal
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Internal extends FractionRounding {}
+ }
+
+ /** A rounding strategy parameterized by a currency. */
+ public static class CurrencyRounding extends Rounding {
+ /**
+ * Associates a {@link com.ibm.icu.util.Currency} with this rounding strategy. Only applies to
+ * rounding strategies returned from {@link #currency(CurrencyUsage)}.
+ *
+ * <p><strong>Calling this method is <em>not required</em></strong>, because the currency
+ * specified in {@link NumberFormatter#unit(MeasureUnit)} or via a {@link CurrencyAmount} passed
+ * into {@link LocalizedNumberFormatter#format(Measure)} is automatically applied to currency
+ * rounding strategies. However, this method enables you to override that automatic association.
+ *
+ * <p>This method also enables numbers to be formatted using currency rounding rules without
+ * explicitly using a currency format.
+ *
+ * @param currency The currency to associate with this rounding strategy.
+ * @return An immutable object for chaining.
+ */
+ public Rounding withCurrency(Currency currency) {
+ // Overridden in RoundingImpl
+ throw new AssertionError();
+ }
+
+ // Prevent subclassing
+ private CurrencyRounding() {}
+
+ /**
+ * @internal
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Internal extends CurrencyRounding {}
+ }
+
+ public static class Grouping implements IGrouping {
+
+ public static final Grouping DEFAULT = new GroupingImpl(GroupingImpl.TYPE_PLACEHOLDER);
+ public static final Grouping DEFAULT_MIN_2_DIGITS = new GroupingImpl(GroupingImpl.TYPE_MIN2);
+ public static final Grouping NONE = new GroupingImpl(GroupingImpl.TYPE_NONE);
+
+ @Override
+ public boolean groupAtPosition(int position, BigDecimal input) {
+ throw new UnsupportedOperationException(
+ "This grouping strategy cannot be used outside of number formatting.");
+ }
+
+ // Prevent subclassing
+ private Grouping() {}
+
+ /**
+ * @internal
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Internal extends Grouping {}
+ }
+
+ public static class Padding {
+
+ public static final Padding NONE = new PaddingImpl();
+
+ public static Padding codePoints(int cp, int targetWidth, PadPosition position) {
+ String paddingString = String.valueOf(Character.toChars(cp));
+ return PaddingImpl.getInstance(paddingString, targetWidth, position);
+ }
+
+ // Prevent subclassing
+ private Padding() {}
+
+ /**
+ * @internal
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Internal extends Padding {}
+ }
+
+ @SuppressWarnings("unused")
+ public static class IntegerWidth {
+
+ public static final IntegerWidth DEFAULT = new IntegerWidthImpl();
+
+ public static IntegerWidth zeroFillTo(int minInt) {
+ return new IntegerWidthImpl(minInt, Integer.MAX_VALUE);
+ }
+
+ public IntegerWidth truncateAt(int maxInt) {
+ // Implemented in IntegerWidthImpl
+ throw new AssertionError();
+ }
+
+ // Prevent subclassing
+ private IntegerWidth() {}
+
+ /**
+ * @internal
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Internal extends IntegerWidth {}
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi;
+
+import java.math.RoundingMode;
+
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.text.NumberingSystem;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.Dimensionless;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.Grouping;
+import newapi.NumberFormatter.Notation;
+import newapi.NumberFormatter.Rounding;
+import newapi.NumberFormatter.SignDisplay;
+import newapi.NumberFormatter.UnlocalizedNumberFormatter;
+
+public class demo {
+ public static void main(String[] args) {
+ System.out.println(NumberingSystem.LATIN.getDescription());
+ UnlocalizedNumberFormatter formatter =
+ NumberFormatter.with()
+ .notation(Notation.COMPACT_SHORT)
+ .notation(Notation.SCIENTIFIC.withExponentSignDisplay(SignDisplay.ALWAYS_SHOWN))
+ .notation(Notation.ENGINEERING.withMinExponentDigits(2))
+ .notation(Notation.SIMPLE)
+ .unit(Currency.getInstance("GBP"))
+ .unit(Dimensionless.PERCENT)
+ .unit(MeasureUnit.CUBIC_METER)
+ .unitWidth(FormatWidth.SHORT)
+ // .rounding(Rounding.fixedSignificantDigits(3))
+// .rounding(
+// (BigDecimal input) -> {
+// return input.divide(new BigDecimal("0.02"), 0).multiply(new BigDecimal("0.02"));
+// })
+ .rounding(Rounding.fixedFraction(2).withMode(RoundingMode.HALF_UP))
+ .rounding(Rounding.INTEGER.withMode(RoundingMode.CEILING))
+ .rounding(Rounding.currency(CurrencyUsage.STANDARD))
+// .grouping(
+// (int position, BigDecimal number) -> {
+// return (position % 3) == 0;
+// })
+ .grouping(Grouping.DEFAULT)
+ .grouping(Grouping.NONE)
+ .grouping(Grouping.DEFAULT_MIN_2_DIGITS)
+ // .padding(Padding.codePoints(' ', 8, PadPosition.AFTER_PREFIX))
+ .sign(SignDisplay.ALWAYS_SHOWN)
+ .decimal(DecimalMarkDisplay.ALWAYS_SHOWN)
+ .symbols(DecimalFormatSymbols.getInstance(new ULocale("fr@digits=ascii")))
+ .symbols(NumberingSystem.getInstanceByName("arab"))
+ .symbols(NumberingSystem.LATIN);
+ System.out.println(formatter.toSkeleton());
+ System.out.println(formatter.locale(ULocale.ENGLISH).format(0.98381).toString());
+ // .locale(Locale.ENGLISH)
+ // .format(123.45)
+ // .toString();
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+public interface AffixPatternProvider {
+ public static final class Flags {
+ public static final int PLURAL_MASK = 0xff;
+ public static final int PREFIX = 0x100;
+ public static final int NEGATIVE_SUBPATTERN = 0x200;
+ public static final int PADDING = 0x400;
+ }
+
+ public char charAt(int flags, int i);
+
+ public int length(int flags);
+
+ public boolean hasCurrencySign();
+
+ public boolean positiveHasPlusSign();
+
+ public boolean hasNegativeSubpattern();
+
+ public boolean negativeHasMinusSign();
+
+ public boolean containsSymbolType(int type);
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.MissingResourceException;
+import java.util.Set;
+
+import com.ibm.icu.impl.ICUData;
+import com.ibm.icu.impl.ICUResourceBundle;
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.UResource;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.CompactDecimalFormat.CompactType;
+import com.ibm.icu.text.NumberingSystem;
+import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.UResourceBundle;
+
+class CompactData implements RoundingImpl.MultiplierProducer {
+
+ public static CompactData getInstance(
+ ULocale locale, CompactType compactType, CompactStyle compactStyle) {
+ // TODO: Add a data cache? It would be keyed by locale, compact type, and compact style.
+ CompactData data = new CompactData();
+ CompactDataSink sink = new CompactDataSink(data, compactType, compactStyle);
+ String nsName = NumberingSystem.getInstance(locale).getName();
+ ICUResourceBundle rb =
+ (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
+ CompactData.internalPopulateData(nsName, rb, sink, data);
+ if (data.isEmpty() && compactStyle == CompactStyle.LONG) {
+ // No long data is available; load short data instead
+ sink.compactStyle = CompactStyle.SHORT;
+ CompactData.internalPopulateData(nsName, rb, sink, data);
+ }
+ return data;
+ }
+
+ public static CompactData getInstance(
+ Map<String, Map<String, String>> powersToPluralsToPatterns) {
+ CompactData data = new CompactData();
+ 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();
+ data.setPattern(patternString, magnitude, plural);
+ int numZeros = countZeros(patternString);
+ if (numZeros > 0) { // numZeros==0 in certain cases, like Somali "Kun"
+ data.setMultiplier(magnitude, (byte) (numZeros - magnitude - 1));
+ }
+ }
+ }
+ return data;
+ }
+
+ private static void internalPopulateData(
+ String nsName, ICUResourceBundle rb, CompactDataSink sink, CompactData 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;
+ }
+ }
+
+ // A dummy object used when a "0" compact decimal entry is encountered. This is necessary
+ // in order to prevent falling back to root. Object equality ("==") is intended.
+ private static final String USE_FALLBACK = "<USE FALLBACK>";
+
+ private final String[] patterns;
+ private final byte[] multipliers;
+ private boolean isEmpty;
+ private int largestMagnitude;
+
+ private static final int MAX_DIGITS = 15;
+
+ private CompactData() {
+ patterns = new String[(CompactData.MAX_DIGITS + 1) * StandardPlural.COUNT];
+ multipliers = new byte[CompactData.MAX_DIGITS + 1];
+ isEmpty = true;
+ largestMagnitude = 0;
+ }
+
+ public boolean isEmpty() {
+ return isEmpty;
+ }
+
+ @Override
+ public int getMultiplier(int magnitude) {
+ if (magnitude < 0) {
+ return 0;
+ }
+ if (magnitude > largestMagnitude) {
+ magnitude = largestMagnitude;
+ }
+ return multipliers[magnitude];
+ }
+
+ /** Returns the multiplier from the array directly without bounds checking. */
+ public int getMultiplierDirect(int magnitude) {
+ return multipliers[magnitude];
+ }
+
+ private void setMultiplier(int magnitude, byte multiplier) {
+ if (multipliers[magnitude] != 0) {
+ assert multipliers[magnitude] == multiplier;
+ return;
+ }
+ multipliers[magnitude] = multiplier;
+ isEmpty = false;
+ if (magnitude > largestMagnitude) largestMagnitude = magnitude;
+ }
+
+ public String getPattern(int magnitude, StandardPlural plural) {
+ if (magnitude < 0) {
+ return null;
+ }
+ if (magnitude > largestMagnitude) {
+ magnitude = largestMagnitude;
+ }
+ String patternString = patterns[getIndex(magnitude, plural)];
+ if (patternString == null && plural != StandardPlural.OTHER) {
+ // Fall back to "other" plural variant
+ patternString = patterns[getIndex(magnitude, StandardPlural.OTHER)];
+ }
+ if (patternString == USE_FALLBACK) {
+ // Return null if USE_FALLBACK is present
+ patternString = null;
+ }
+ return patternString;
+ }
+
+ public Set<String> getAllPatterns() {
+ Set<String> result = new HashSet<String>();
+ result.addAll(Arrays.asList(patterns));
+ result.remove(USE_FALLBACK);
+ result.remove(null);
+ return result;
+ }
+
+ private boolean has(int magnitude, StandardPlural plural) {
+ // Return true if USE_FALLBACK is present
+ return patterns[getIndex(magnitude, plural)] != null;
+ }
+
+ private void setPattern(String patternString, int magnitude, StandardPlural plural) {
+ patterns[getIndex(magnitude, plural)] = patternString;
+ isEmpty = false;
+ if (magnitude > largestMagnitude) largestMagnitude = magnitude;
+ }
+
+ private void setNoFallback(int magnitude, StandardPlural plural) {
+ setPattern(USE_FALLBACK, magnitude, plural);
+ }
+
+ private static final int getIndex(int magnitude, StandardPlural plural) {
+ return magnitude * StandardPlural.COUNT + plural.ordinal();
+ }
+
+ private static final class CompactDataSink extends UResource.Sink {
+
+ CompactData data;
+ CompactStyle compactStyle;
+ CompactType compactType;
+ 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 CompactDataSink(CompactData data, CompactType compactType, CompactStyle compactStyle) {
+ this.data = data;
+ this.compactType = compactType;
+ this.compactStyle = compactStyle;
+ }
+
+ @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) {
+
+ // 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);
+ byte multiplier = (byte) data.getMultiplierDirect(magnitude);
+
+ // Silently ignore divisors that are too big.
+ if (magnitude >= CompactData.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;
+ }
+
+ // Save the pattern string. We will parse it lazily.
+ data.setPattern(patternString, magnitude, plural);
+
+ // If necessary, compute the multiplier: the difference between the magnitude
+ // and the number of zeros in the pattern.
+ if (multiplier == 0) {
+ int numZeros = countZeros(patternString);
+ if (numZeros > 0) { // numZeros==0 in certain cases, like Somali "Kun"
+ multiplier = (byte) (numZeros - magnitude - 1);
+ }
+ }
+ }
+
+ data.setMultiplier(magnitude, multiplier);
+ }
+
+ // 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;
+ }
+ }
+ }
+ }
+
+ private static final int countZeros(String patternString) {
+ // NOTE: This strategy for computing the number of zeros is a hack for efficiency.
+ // It could break if there are any 0s that aren't part of the main pattern.
+ int numZeros = 0;
+ for (int i = 0; i < patternString.length(); i++) {
+ if (patternString.charAt(i) == '0') {
+ numZeros++;
+ } else if (numZeros > 0) {
+ break; // zeros should always be contiguous
+ }
+ }
+ return numZeros;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.LdmlPatternInfo;
+import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.CompactDecimalFormat.CompactType;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.util.ULocale;
+
+import newapi.impl.MurkyModifier.ImmutableMurkyModifier;
+import newapi.impl.RoundingImpl.RoundingImplDummy;
+
+public class CompactImpl implements QuantityChain {
+
+ final PluralRules rules;
+ final CompactData data;
+ /* final */ Map<String, CompactModInfo> precomputedMods;
+ /* final */ QuantityChain parent;
+
+ public static CompactImpl getInstance(
+ ULocale dataLocale, CompactType compactType, CompactStyle compactStyle, PluralRules rules) {
+ CompactData data = CompactData.getInstance(dataLocale, compactType, compactStyle);
+ return new CompactImpl(data, rules);
+ }
+
+ public static CompactImpl getInstance(
+ Map<String, Map<String, String>> compactCustomData, PluralRules rules) {
+ CompactData data = CompactData.getInstance(compactCustomData);
+ return new CompactImpl(data, rules);
+ }
+
+ private CompactImpl(CompactData data, PluralRules rules) {
+ this.data = data;
+ this.rules = rules;
+ }
+
+ /** To be used by the building code path */
+ public void precomputeAllModifiers(MurkyModifier reference) {
+ precomputedMods = new HashMap<String, CompactModInfo>();
+ Set<String> allPatterns = data.getAllPatterns();
+ for (String patternString : allPatterns) {
+ CompactModInfo info = new CompactModInfo();
+ PatternParseResult patternInfo = LdmlPatternInfo.parse(patternString);
+ reference.setPatternInfo(patternInfo);
+ info.mod = reference.createImmutable();
+ info.numDigits = patternInfo.positive.totalIntegerDigits;
+ precomputedMods.put(patternString, info);
+ }
+ }
+
+ private static class CompactModInfo {
+ public ImmutableMurkyModifier mod;
+ public int numDigits;
+ }
+
+ @Override
+ public QuantityChain chain(QuantityChain parent) {
+ this.parent = parent;
+ return this;
+ }
+
+ @Override
+ public MicroProps withQuantity(FormatQuantity input) {
+ MicroProps micros = parent.withQuantity(input);
+ assert micros.rounding != null;
+
+ // Treat zero as if it had magnitude 0
+ int magnitude;
+ if (input.isZero()) {
+ magnitude = 0;
+ micros.rounding.apply(input);
+ } else {
+ // TODO: Revisit chooseMultiplierAndApply
+ int multiplier = micros.rounding.chooseMultiplierAndApply(input, data);
+ magnitude = input.isZero() ? 0 : input.getMagnitude();
+ magnitude -= multiplier;
+ }
+
+ StandardPlural plural = input.getStandardPlural(rules);
+ String patternString = data.getPattern(magnitude, plural);
+ int numDigits = -1;
+ if (patternString == null) {
+ // Use the default (non-compact) modifier.
+ // No need to take any action.
+ } else if (precomputedMods != null) {
+ // Build code path.
+ CompactModInfo info = precomputedMods.get(patternString);
+ info.mod.applyToMicros(micros, input);
+ numDigits = info.numDigits;
+ } else {
+ // Non-build code path.
+ // Overwrite the PatternInfo in the existing modMiddle
+ assert micros.modMiddle instanceof MurkyModifier;
+ PatternParseResult patternInfo = LdmlPatternInfo.parse(patternString);
+ ((MurkyModifier) micros.modMiddle).setPatternInfo(patternInfo);
+ numDigits = patternInfo.positive.totalIntegerDigits;
+ }
+
+ // FIXME: Deal with numDigits == 0 (Awaiting a test case)
+
+ // We already performed rounding. Do not perform it again.
+ micros.rounding = RoundingImplDummy.INSTANCE;
+
+ return micros;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.text.UnicodeSet;
+
+/** Identical to {@link ConstantMultiFieldModifier}, but supports currency spacing. */
+public class CurrencySpacingEnabledModifier extends ConstantMultiFieldModifier {
+
+ // These are the default currency spacing UnicodeSets in CLDR.
+ // Pre-compute them for performance.
+ // TODO: Is there a way to write a unit test to make sure these hard-coded values
+ // stay consistent with CLDR?
+ private static final UnicodeSet UNISET_DIGIT = new UnicodeSet("[:digit:]").freeze();
+ private static final UnicodeSet UNISET_NOTS = new UnicodeSet("[:^S:]").freeze();
+
+ // Constants for better readability. Types are for compiler checking.
+ static final byte PREFIX = 0;
+ static final byte SUFFIX = 1;
+ static final short IN_CURRENCY = 0;
+ static final short IN_NUMBER = 1;
+
+ private final UnicodeSet afterPrefixUnicodeSet;
+ private final String afterPrefixInsert;
+ private final UnicodeSet beforeSuffixUnicodeSet;
+ private final String beforeSuffixInsert;
+
+ /** Build code path */
+ public CurrencySpacingEnabledModifier(
+ NumberStringBuilder prefix,
+ NumberStringBuilder suffix,
+ boolean strong,
+ DecimalFormatSymbols symbols) {
+ super(prefix, suffix, strong);
+
+ // Check for currency spacing. Do not build the UnicodeSets unless there is
+ // a currency code point at a boundary.
+ if (prefixFields.length > 0
+ && prefixFields[prefixFields.length - 1] == NumberFormat.Field.CURRENCY) {
+ int prefixCp = Character.codePointBefore(prefixChars, prefixChars.length);
+ UnicodeSet prefixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, PREFIX);
+ if (prefixUnicodeSet.contains(prefixCp)) {
+ afterPrefixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, PREFIX);
+ afterPrefixInsert = getInsertString(symbols, PREFIX);
+ } else {
+ afterPrefixUnicodeSet = null;
+ afterPrefixInsert = null;
+ }
+ } else {
+ afterPrefixUnicodeSet = null;
+ afterPrefixInsert = null;
+ }
+ if (suffixFields.length > 0 && suffixFields[0] == NumberFormat.Field.CURRENCY) {
+ int suffixCp = Character.codePointAt(suffixChars, 0);
+ UnicodeSet suffixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, SUFFIX);
+ if (suffixUnicodeSet.contains(suffixCp)) {
+ beforeSuffixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, SUFFIX);
+ beforeSuffixInsert = getInsertString(symbols, SUFFIX);
+ } else {
+ beforeSuffixUnicodeSet = null;
+ beforeSuffixInsert = null;
+ }
+ } else {
+ beforeSuffixUnicodeSet = null;
+ beforeSuffixInsert = null;
+ }
+ }
+
+ /** Non-build code path */
+ public static int applyCurrencySpacing(
+ NumberStringBuilder output,
+ int prefixStart,
+ int prefixLen,
+ int suffixStart,
+ int suffixLen,
+ DecimalFormatSymbols symbols) {
+ int length = 0;
+ boolean hasPrefix = (prefixLen > 0);
+ boolean hasSuffix = (suffixLen > 0);
+ boolean hasNumber = (suffixStart - prefixStart - prefixLen > 0); // could be empty string
+ if (hasPrefix && hasNumber) {
+ length += applyCurrencySpacingAffix(output, prefixStart + prefixLen, PREFIX, symbols);
+ }
+ if (hasSuffix && hasNumber) {
+ length += applyCurrencySpacingAffix(output, suffixStart + length, SUFFIX, symbols);
+ }
+ return length;
+ }
+
+ private static int applyCurrencySpacingAffix(
+ NumberStringBuilder output, int index, byte affix, DecimalFormatSymbols symbols) {
+ // NOTE: For prefix, output.fieldAt(index-1) gets the last field type in the prefix.
+ // This works even if the last code point in the prefix is 2 code units because the
+ // field value gets populated to both indices in the field array.
+ NumberFormat.Field affixField =
+ (affix == PREFIX) ? output.fieldAt(index - 1) : output.fieldAt(index);
+ if (affixField != NumberFormat.Field.CURRENCY) {
+ return 0;
+ }
+ int affixCp =
+ (affix == PREFIX)
+ ? Character.codePointBefore(output, index)
+ : Character.codePointAt(output, index);
+ UnicodeSet affixUniset = getUnicodeSet(symbols, IN_CURRENCY, affix);
+ if (!affixUniset.contains(affixCp)) {
+ return 0;
+ }
+ int numberCp =
+ (affix == PREFIX)
+ ? Character.codePointAt(output, index)
+ : Character.codePointBefore(output, index);
+ UnicodeSet numberUniset = getUnicodeSet(symbols, IN_NUMBER, affix);
+ if (!numberUniset.contains(numberCp)) {
+ return 0;
+ }
+ String spacingString = getInsertString(symbols, affix);
+
+ // NOTE: This next line *inserts* the spacing string, triggering an arraycopy.
+ // It would be more efficient if this could be done before affixes were attached,
+ // so that it could be prepended/appended instead of inserted.
+ // However, the build code path is more efficient, and this is the most natural
+ // place to put currency spacing in the non-build code path.
+ // TODO: Should we use the CURRENCY field here?
+ return output.insert(index, spacingString, null);
+ }
+
+ private static UnicodeSet getUnicodeSet(DecimalFormatSymbols symbols, short position, byte affix) {
+ String pattern =
+ symbols.getPatternForCurrencySpacing(
+ position == IN_CURRENCY
+ ? DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH
+ : DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH,
+ affix == SUFFIX);
+ if (pattern.equals("[:digit:]")) {
+ return UNISET_DIGIT;
+ } else if (pattern.equals("[:^S:]")) {
+ return UNISET_NOTS;
+ } else {
+ return new UnicodeSet(pattern);
+ }
+ }
+
+ private static String getInsertString(DecimalFormatSymbols symbols, byte affix) {
+ return symbols.getPatternForCurrencySpacing(
+ DecimalFormatSymbols.CURRENCY_SPC_INSERT, affix == SUFFIX);
+ }
+
+ @Override
+ public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+ // Currency spacing logic
+ int length = 0;
+ if (rightIndex - leftIndex > 0
+ && afterPrefixUnicodeSet != null
+ && afterPrefixUnicodeSet.contains(Character.codePointAt(output, leftIndex))) {
+ // TODO: Should we use the CURRENCY field here?
+ length += output.insert(leftIndex, afterPrefixInsert, null);
+ }
+ if (rightIndex - leftIndex > 0
+ && beforeSuffixUnicodeSet != null
+ && beforeSuffixUnicodeSet.contains(Character.codePointBefore(output, rightIndex))) {
+ // TODO: Should we use the CURRENCY field here?
+ length += output.insert(rightIndex + length, beforeSuffixInsert, null);
+ }
+
+ // Call super for the remaining logic
+ length += super.apply(output, leftIndex, rightIndex + length);
+ return length;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.ULocale;
+
+public class CustomSymbolCurrency extends Currency {
+ private String symbol1;
+ private String symbol2;
+
+ public static Currency resolve(Currency currency, ULocale locale, DecimalFormatSymbols symbols) {
+ if (currency == null) {
+ currency = symbols.getCurrency();
+ }
+ String currency1Sym = symbols.getCurrencySymbol();
+ String currency2Sym = symbols.getInternationalCurrencySymbol();
+ if (currency == null) {
+ return new CustomSymbolCurrency("XXX", currency1Sym, currency2Sym);
+ }
+ if (!currency.equals(symbols.getCurrency())) {
+ return currency;
+ }
+ String currency1 = currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
+ String currency2 = currency.getCurrencyCode();
+ if (!currency1.equals(currency1Sym) || !currency2.equals(currency2Sym)) {
+ return new CustomSymbolCurrency(currency2, currency1Sym, currency2Sym);
+ }
+ return currency;
+ }
+
+ public CustomSymbolCurrency(String isoCode, String currency1Sym, String currency2Sym) {
+ super(isoCode);
+ this.symbol1 = currency1Sym;
+ this.symbol2 = currency2Sym;
+ }
+
+ @Override
+ public String getName(ULocale locale, int nameStyle, boolean[] isChoiceFormat) {
+ if (nameStyle == SYMBOL_NAME) {
+ return symbol1;
+ }
+ return super.getName(locale, nameStyle, isChoiceFormat);
+ }
+
+ @Override
+ public String getName(
+ ULocale locale, int nameStyle, String pluralCount, boolean[] isChoiceFormat) {
+ if (nameStyle == PLURAL_LONG_NAME && subType.equals("XXX")) {
+ // Plural in absence of a currency should return the symbol
+ return symbol1;
+ }
+ return super.getName(locale, nameStyle, pluralCount, isChoiceFormat);
+ }
+
+ @Override
+ public String getCurrencyCode() {
+ return symbol2;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.EnumMap;
+import java.util.Map;
+
+import com.ibm.icu.impl.CurrencyData;
+import com.ibm.icu.impl.SimpleFormatterImpl;
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.modifiers.SimpleModifier;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.text.NumberFormat.Field;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+public class DataUtils {
+
+ public static Map<StandardPlural, Modifier> getCurrencyLongNameModifiers(
+ ULocale loc, Currency currency) {
+ Map<String, String> data = CurrencyData.provider.getInstance(loc, true).getUnitPatterns();
+ Map<StandardPlural, Modifier> result =
+ new EnumMap<StandardPlural, Modifier>(StandardPlural.class);
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry<String, String> e : data.entrySet()) {
+ String pluralKeyword = e.getKey();
+ StandardPlural plural = StandardPlural.fromString(e.getKey());
+ String longName = currency.getName(loc, Currency.PLURAL_LONG_NAME, pluralKeyword, null);
+ String simpleFormat = e.getValue(); // e.g., "{0} {1}"
+ simpleFormat = simpleFormat.replace("{1}", longName);
+ String compiled = SimpleFormatterImpl.compileToStringMinMaxArguments(simpleFormat, sb, 1, 1);
+ Modifier mod = new SimpleModifier(compiled, Field.CURRENCY, false);
+ result.put(plural, mod);
+ }
+ return result;
+ }
+
+ public static Map<StandardPlural, Modifier> getMeasureUnitModifiers(
+ ULocale loc, MeasureUnit unit, FormatWidth width) {
+ Map<StandardPlural, String> simpleFormats = MeasureData.getMeasureData(loc, unit, width);
+ Map<StandardPlural, Modifier> result =
+ new EnumMap<StandardPlural, Modifier>(StandardPlural.class);
+ StringBuilder sb = new StringBuilder();
+ for (StandardPlural plural : StandardPlural.VALUES) {
+ if (simpleFormats.get(plural) == null) {
+ plural = StandardPlural.OTHER;
+ }
+ String simpleFormat = simpleFormats.get(plural);
+ String compiled = SimpleFormatterImpl.compileToStringMinMaxArguments(simpleFormat, sb, 1, 1);
+ Modifier mod = new SimpleModifier(compiled, Field.CURRENCY, false);
+ result.put(plural, mod);
+ }
+ return result;
+ // Map<StandardPlural, Modifier> result =
+ // new EnumMap<StandardPlural, Modifier>(StandardPlural.class);
+ // // TODO: Get the data directly instead of taking the detour through MeasureFormat.
+ // MeasureFormat mf = MeasureFormat.getInstance(loc, width);
+ // for (StandardPlural plural : StandardPlural.VALUES) {
+ // String compiled = mf.getPluralFormatter(unit, width, plural.ordinal());
+ // Modifier mod = new SimpleModifier(compiled, null, false);
+ // result.put(plural, mod);
+ // }
+ // return result;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.math.BigDecimal;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
+
+import newapi.NumberFormatter.Grouping;
+import newapi.NumberFormatter.IGrouping;
+
+public class GroupingImpl extends Grouping.Internal {
+
+ // Conveniences for Java handling of shorts
+ private static final short S2 = 2;
+ private static final short S3 = 3;
+
+ // For the "placeholder constructor"
+ public static final char TYPE_PLACEHOLDER = 0;
+ public static final char TYPE_MIN2 = 1;
+ public static final char TYPE_NONE = 2;
+
+ // Statically initialized objects (cannot be used statically by other ICU classes)
+ static final GroupingImpl NONE = new GroupingImpl(TYPE_NONE);
+ static final GroupingImpl GROUPING_3 = new GroupingImpl(S3, S3, false);
+ static final GroupingImpl GROUPING_3_2 = new GroupingImpl(S3, S2, false);
+ static final GroupingImpl GROUPING_3_MIN2 = new GroupingImpl(S3, S3, true);
+ static final GroupingImpl GROUPING_3_2_MIN2 = new GroupingImpl(S3, S2, true);
+
+ static GroupingImpl getInstance(short grouping1, short grouping2, boolean min2) {
+ if (grouping1 == -1) {
+ return NONE;
+ } else if (!min2 && grouping1 == 3 && grouping2 == 3) {
+ return GROUPING_3;
+ } else if (!min2 && grouping1 == 3 && grouping2 == 2) {
+ return GROUPING_3_2;
+ } else if (min2 && grouping1 == 3 && grouping2 == 3) {
+ return GROUPING_3_MIN2;
+ } else if (min2 && grouping1 == 3 && grouping2 == 2) {
+ return GROUPING_3_2_MIN2;
+ } else {
+ return new GroupingImpl(grouping1, grouping2, min2);
+ }
+ }
+
+ public static GroupingImpl normalizeType(IGrouping grouping, PatternParseResult patternInfo) {
+ assert grouping != null;
+ if (grouping instanceof GroupingImpl) {
+ return ((GroupingImpl) grouping).withLocaleData(patternInfo);
+ } else {
+ return new GroupingImpl(grouping);
+ }
+ }
+
+ final IGrouping lambda;
+ final short grouping1; // -2 means "needs locale data"; -1 means "no grouping"
+ final short grouping2;
+ final boolean min2;
+
+ /** The "placeholder constructor". Pass in one of the GroupingImpl.TYPE_* variables. */
+ public GroupingImpl(char type) {
+ lambda = null;
+ switch (type) {
+ case TYPE_PLACEHOLDER:
+ grouping1 = -2;
+ grouping2 = -2;
+ min2 = false;
+ break;
+ case TYPE_MIN2:
+ grouping1 = -2;
+ grouping2 = -2;
+ min2 = true;
+ break;
+ case TYPE_NONE:
+ grouping1 = -1;
+ grouping2 = -1;
+ min2 = false;
+ break;
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ private GroupingImpl(short grouping1, short grouping2, boolean min2) {
+ this.lambda = null;
+ this.grouping1 = grouping1;
+ this.grouping2 = grouping2;
+ this.min2 = min2;
+ }
+
+ private GroupingImpl(IGrouping lambda) {
+ this.lambda = lambda;
+ this.grouping1 = -3;
+ this.grouping2 = -3;
+ this.min2 = false;
+ }
+
+ GroupingImpl withLocaleData(PatternParseResult patternInfo) {
+ if (grouping1 != -2) {
+ return this;
+ }
+ assert lambda == null;
+ short grouping1 = (short) (patternInfo.positive.groupingSizes & 0xffff);
+ short grouping2 = (short) ((patternInfo.positive.groupingSizes >>> 16) & 0xffff);
+ short grouping3 = (short) ((patternInfo.positive.groupingSizes >>> 32) & 0xffff);
+ if (grouping2 == -1) {
+ grouping1 = -1;
+ }
+ if (grouping3 == -1) {
+ grouping2 = grouping1;
+ }
+ return getInstance(grouping1, grouping2, min2);
+ }
+
+ boolean groupAtPosition(int position, FormatQuantity value) {
+ // Check for lambda function
+ if (lambda != null) {
+ // TODO: Cache the BigDecimal
+ BigDecimal temp = value.toBigDecimal();
+ return lambda.groupAtPosition(position, temp);
+ }
+
+ assert grouping1 != -2;
+ if (grouping1 == -1 || grouping1 == 0) {
+ // Either -1 or 0 means "no grouping"
+ return false;
+ }
+ position -= grouping1;
+ return position >= 0
+ && (position % grouping2) == 0
+ && value.getUpperDisplayMagnitude() - grouping1 + 1 >= (min2 ? 2 : 1);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import newapi.NumberFormatter.IntegerWidth;
+
+public final class IntegerWidthImpl extends IntegerWidth.Internal {
+ public final int minInt;
+ public final int maxInt;
+
+ public static final IntegerWidthImpl DEFAULT = new IntegerWidthImpl();
+
+ /** Default constructor */
+ public IntegerWidthImpl() {
+ this(1, Integer.MAX_VALUE);
+ }
+
+ public IntegerWidthImpl(int minInt, int maxInt) {
+ this.minInt = minInt;
+ this.maxInt = maxInt;
+ }
+
+ @Override
+public IntegerWidthImpl truncateAt(int maxInt) {
+ return new IntegerWidthImpl(minInt, maxInt);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.Objects;
+
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.IGrouping;
+import newapi.NumberFormatter.IRounding;
+import newapi.NumberFormatter.IntegerWidth;
+import newapi.NumberFormatter.Notation;
+import newapi.NumberFormatter.Padding;
+import newapi.NumberFormatter.SignDisplay;
+
+public class MacroProps implements Cloneable {
+ public Notation notation;
+ public MeasureUnit unit;
+ public IRounding rounding;
+ public IGrouping grouping;
+ public Padding padding;
+ public IntegerWidth integerWidth;
+ public Object symbols;
+ public FormatWidth unitWidth;
+ public SignDisplay sign;
+ public DecimalMarkDisplay decimal;
+ public AffixPatternProvider affixProvider; // not in API; for JDK compatibility mode only
+ public MultiplierImpl multiplier; // not in API; for JDK compatibility mode only
+ public PluralRules rules; // not in API; could be made public in the future
+ public ULocale loc;
+
+ /**
+ * Copies values from fallback into this instance if they are null in this instance.
+ *
+ * @param fallback The instance to copy from; not modified by this operation.
+ */
+ public void fallback(MacroProps fallback) {
+ if (notation == null) notation = fallback.notation;
+ if (unit == null) unit = fallback.unit;
+ if (rounding == null) rounding = fallback.rounding;
+ if (grouping == null) grouping = fallback.grouping;
+ if (padding == null) padding = fallback.padding;
+ if (integerWidth == null) integerWidth = fallback.integerWidth;
+ if (symbols == null) symbols = fallback.symbols;
+ if (unitWidth == null) unitWidth = fallback.unitWidth;
+ if (sign == null) sign = fallback.sign;
+ if (decimal == null) decimal = fallback.decimal;
+ if (affixProvider == null) affixProvider = fallback.affixProvider;
+ if (multiplier == null) multiplier = fallback.multiplier;
+ if (rules == null) rules = fallback.rules;
+ if (loc == null) loc = fallback.loc;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ notation,
+ unit,
+ rounding,
+ grouping,
+ padding,
+ integerWidth,
+ symbols,
+ unitWidth,
+ sign,
+ decimal,
+ affixProvider,
+ multiplier,
+ rules,
+ loc);
+ }
+
+ @Override
+ public boolean equals(Object _other) {
+ MacroProps other = (MacroProps) _other;
+ return Objects.equals(notation, other.notation)
+ && Objects.equals(unit, other.unit)
+ && Objects.equals(rounding, other.rounding)
+ && Objects.equals(grouping, other.grouping)
+ && Objects.equals(padding, other.padding)
+ && Objects.equals(integerWidth, other.integerWidth)
+ && Objects.equals(symbols, other.symbols)
+ && Objects.equals(unitWidth, other.unitWidth)
+ && Objects.equals(sign, other.sign)
+ && Objects.equals(decimal, other.decimal)
+ && Objects.equals(affixProvider, other.affixProvider)
+ && Objects.equals(multiplier, other.multiplier)
+ && Objects.equals(rules, other.rules)
+ && Objects.equals(loc, other.loc);
+ }
+
+ @Override
+ public Object clone() {
+ // TODO: Remove this method?
+ try {
+ return super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.EnumMap;
+import java.util.Map;
+
+import com.ibm.icu.impl.ICUData;
+import com.ibm.icu.impl.ICUResourceBundle;
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.UResource;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.UResourceBundle;
+
+public class MeasureData {
+
+ private static final class ShanesMeasureUnitSink extends UResource.Sink {
+
+ Map<StandardPlural, String> output;
+
+ public ShanesMeasureUnitSink(Map<StandardPlural, String> output) {
+ this.output = output;
+ }
+
+ @Override
+ public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
+ UResource.Table pluralsTable = value.getTable();
+ for (int i1 = 0; pluralsTable.getKeyAndValue(i1, key, value); ++i1) {
+ if (key.contentEquals("dnam") || key.contentEquals("per")) {
+ continue;
+ }
+ StandardPlural plural = StandardPlural.fromString(key);
+ if (output.containsKey(plural)) {
+ continue;
+ }
+ String formatString = value.getString();
+ output.put(plural, formatString);
+ }
+ }
+ }
+
+ public static Map<StandardPlural, String> getMeasureData(
+ ULocale locale, MeasureUnit unit, FormatWidth width) {
+ ICUResourceBundle resource =
+ (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale);
+ StringBuilder key = new StringBuilder();
+ key.append("units");
+ if (width == FormatWidth.NARROW) {
+ key.append("Narrow");
+ } else if (width == FormatWidth.SHORT) {
+ key.append("Short");
+ }
+ key.append("/");
+ key.append(unit.getType());
+ key.append("/");
+ key.append(unit.getSubtype());
+ Map<StandardPlural, String> output = new EnumMap<StandardPlural, String>(StandardPlural.class);
+ ShanesMeasureUnitSink sink = new ShanesMeasureUnitSink(output);
+ resource.getAllItemsWithFallback(key.toString(), sink);
+ return output;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.text.DecimalFormatSymbols;
+
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.SignDisplay;
+
+public class MicroProps implements Cloneable, QuantityChain {
+ // Populated globally:
+ public SignDisplay sign;
+ public DecimalFormatSymbols symbols;
+ public PaddingImpl padding;
+ public DecimalMarkDisplay decimal;
+ public IntegerWidthImpl integerWidth;
+
+ // Populated by notation/unit:
+ public Modifier modOuter;
+ public Modifier modMiddle;
+ public Modifier modInner;
+ public RoundingImpl rounding;
+ public GroupingImpl grouping;
+ public int multiplier;
+ public boolean useCurrency;
+
+ private boolean frozen = false;
+
+ public void enableCloneInChain() {
+ frozen = true;
+ }
+
+ @Override
+ public QuantityChain chain(QuantityChain parent) {
+ // The MicroProps instance should always be at the top of the chain!
+ throw new AssertionError();
+ }
+
+ @Override
+ public MicroProps withQuantity(FormatQuantity quantity) {
+ if (frozen) {
+ return (MicroProps) this.clone();
+ } else {
+ return this;
+ }
+ }
+
+ @Override
+ public Object clone() {
+ try {
+ return super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.math.BigDecimal;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+
+public class MultiplierImpl implements QuantityChain {
+ final int magnitudeMultiplier;
+ final BigDecimal bigDecimalMultiplier;
+ /* final */ QuantityChain parent;
+
+ public MultiplierImpl(int magnitudeMultiplier) {
+ this.magnitudeMultiplier = magnitudeMultiplier;
+ this.bigDecimalMultiplier = null;
+ }
+
+ public MultiplierImpl(BigDecimal bigDecimalMultiplier) {
+ this.magnitudeMultiplier = 0;
+ this.bigDecimalMultiplier = bigDecimalMultiplier;
+ }
+
+ @Override
+ public QuantityChain chain(QuantityChain parent) {
+ this.parent = parent;
+ return this;
+ }
+
+ @Override
+ public MicroProps withQuantity(FormatQuantity quantity) {
+ MicroProps micros = parent.withQuantity(quantity);
+ quantity.adjustMagnitude(magnitudeMultiplier);
+ if (bigDecimalMultiplier != null) {
+ quantity.multiplyBy(bigDecimalMultiplier);
+ }
+ return micros;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.AffixPatternUtils;
+import com.ibm.icu.impl.number.AffixPatternUtils.SymbolProvider;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.util.Currency;
+
+import newapi.NumberFormatter.SignDisplay;
+
+/**
+ * This is a MUTABLE, NON-THREAD-SAFE class designed for performance. Do NOT save references to this
+ * or attempt to use it from multiple threads!!!
+ *
+ * <p>This class takes a parsed pattern and returns a Modifier, without creating any objects. When
+ * the Modifier methods are called, symbols are substituted directly into the output
+ * NumberStringBuilder, without creating any intermediate Strings.
+ */
+public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, QuantityChain {
+
+ // Modifier details
+ final boolean isStrong;
+
+ // Pattern details
+ AffixPatternProvider patternInfo;
+ SignDisplay signDisplay;
+ boolean perMilleReplacesPercent;
+
+ // Symbol details
+ DecimalFormatSymbols symbols;
+ FormatWidth unitWidth;
+ String currency1;
+ String currency2;
+ String[] currency3;
+ PluralRules rules;
+
+ // Number details
+ boolean isNegative;
+ StandardPlural plural;
+
+ // QuantityChain details
+ QuantityChain parent;
+
+ // Transient CharSequence fields
+ boolean inCharSequenceMode;
+ int flags;
+ int length;
+ boolean prependSign;
+ boolean plusReplacesMinusSign;
+
+ public MurkyModifier(boolean isStrong) {
+ this.isStrong = isStrong;
+ }
+
+ public void setPatternInfo(AffixPatternProvider patternInfo) {
+ this.patternInfo = patternInfo;
+ }
+
+ public void setPatternAttributes(SignDisplay signDisplay, boolean perMille) {
+ this.signDisplay = signDisplay;
+ this.perMilleReplacesPercent = perMille;
+ }
+
+ public void setSymbols(
+ DecimalFormatSymbols symbols, Currency currency, FormatWidth unitWidth, PluralRules rules) {
+ assert (rules != null) == needsPlurals();
+ this.symbols = symbols;
+ this.unitWidth = unitWidth;
+ this.rules = rules;
+
+ currency1 = currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
+ currency2 = currency.getCurrencyCode();
+
+ if (rules != null) {
+ currency3 = new String[StandardPlural.COUNT];
+ for (StandardPlural plural : StandardPlural.VALUES) {
+ currency3[plural.ordinal()] =
+ currency.getName(
+ symbols.getULocale(), Currency.PLURAL_LONG_NAME, plural.getKeyword(), null);
+ }
+ }
+ }
+
+ public void setNumberProperties(boolean isNegative, StandardPlural plural) {
+ assert (plural != null) == needsPlurals();
+ this.isNegative = isNegative;
+ this.plural = plural;
+ }
+
+ /**
+ * Returns true if the pattern represented by this MurkyModifier requires a plural keyword in
+ * order to localize. This is currently true only if there is a currency long name placeholder in
+ * the pattern.
+ */
+ public boolean needsPlurals() {
+ return patternInfo.containsSymbolType(AffixPatternUtils.TYPE_CURRENCY_TRIPLE);
+ }
+
+ @Override
+ public QuantityChain chain(QuantityChain parent) {
+ this.parent = parent;
+ return this;
+ }
+
+ @Override
+ public MicroProps withQuantity(FormatQuantity fq) {
+ MicroProps micros = parent.withQuantity(fq);
+ if (needsPlurals()) {
+ // TODO: Fix this. Avoid the copy.
+ FormatQuantity copy = fq.createCopy();
+ micros.rounding.apply(copy);
+ setNumberProperties(fq.isNegative(), copy.getStandardPlural(rules));
+ } else {
+ setNumberProperties(fq.isNegative(), null);
+ }
+ micros.modMiddle = this;
+ return micros;
+ }
+
+ /**
+ * Creates a new quantity-dependent Modifier that behaves the same as the current instance, but
+ * which is immutable and can be saved for future use. The current instance is not changed by
+ * calling this method except for the number properties.
+ *
+ * @return An immutable that supports both positive and negative numbers.
+ */
+ public ImmutableMurkyModifier createImmutable() {
+ NumberStringBuilder a = new NumberStringBuilder();
+ NumberStringBuilder b = new NumberStringBuilder();
+ if (needsPlurals()) {
+ // Slower path when we require the plural keyword.
+ Modifier[] mods = new Modifier[ImmutableMurkyModifierWithPlurals.getModsLength()];
+ for (StandardPlural plural : StandardPlural.VALUES) {
+ setNumberProperties(false, plural);
+ Modifier positive = createConstantModifier(a, b);
+ setNumberProperties(true, plural);
+ Modifier negative = createConstantModifier(a, b);
+ mods[ImmutableMurkyModifierWithPlurals.getModIndex(false, plural)] = positive;
+ mods[ImmutableMurkyModifierWithPlurals.getModIndex(true, plural)] = negative;
+ }
+ return new ImmutableMurkyModifierWithPlurals(mods, rules);
+ } else {
+ // Faster path when plural keyword is not needed.
+ setNumberProperties(false, null);
+ Modifier positive = createConstantModifier(a, b);
+ setNumberProperties(true, null);
+ Modifier negative = createConstantModifier(a, b);
+ return new ImmutableMurkyModifierWithoutPlurals(positive, negative);
+ }
+ }
+
+ private Modifier createConstantModifier(NumberStringBuilder a, NumberStringBuilder b) {
+ insertPrefix(a.clear(), 0);
+ insertSuffix(b.clear(), 0);
+ if (patternInfo.hasCurrencySign()) {
+ return new CurrencySpacingEnabledModifier(a, b, isStrong, symbols);
+ } else {
+ return new ConstantMultiFieldModifier(a, b, isStrong);
+ }
+ }
+
+ public static interface ImmutableMurkyModifier extends QuantityChain {
+ public void applyToMicros(MicroProps micros, FormatQuantity quantity);
+ }
+
+ public static class ImmutableMurkyModifierWithoutPlurals implements ImmutableMurkyModifier {
+ final Modifier positive;
+ final Modifier negative;
+ /* final */ QuantityChain parent;
+
+ public ImmutableMurkyModifierWithoutPlurals(Modifier positive, Modifier negative) {
+ this.positive = positive;
+ this.negative = negative;
+ }
+
+ @Override
+ public QuantityChain chain(QuantityChain parent) {
+ this.parent = parent;
+ return this;
+ }
+
+ @Override
+ public MicroProps withQuantity(FormatQuantity quantity) {
+ MicroProps micros = parent.withQuantity(quantity);
+ applyToMicros(micros, quantity);
+ return micros;
+ }
+
+ @Override
+ public void applyToMicros(MicroProps micros, FormatQuantity quantity) {
+ if (quantity.isNegative()) {
+ micros.modMiddle = negative;
+ } else {
+ micros.modMiddle = positive;
+ }
+ }
+ }
+
+ public static class ImmutableMurkyModifierWithPlurals implements ImmutableMurkyModifier {
+ final Modifier[] mods;
+ final PluralRules rules;
+ /* final */ QuantityChain parent;
+
+ public ImmutableMurkyModifierWithPlurals(Modifier[] mods, PluralRules rules) {
+ assert mods.length == getModsLength();
+ assert rules != null;
+ this.mods = mods;
+ this.rules = rules;
+ }
+
+ public static int getModsLength() {
+ return 2 * StandardPlural.COUNT;
+ }
+
+ public static int getModIndex(boolean isNegative, StandardPlural plural) {
+ return plural.ordinal() * 2 + (isNegative ? 1 : 0);
+ }
+
+ @Override
+ public QuantityChain chain(QuantityChain parent) {
+ this.parent = parent;
+ return this;
+ }
+
+ @Override
+ public MicroProps withQuantity(FormatQuantity quantity) {
+ MicroProps micros = parent.withQuantity(quantity);
+ applyToMicros(micros, quantity);
+ return micros;
+ }
+
+ @Override
+ public void applyToMicros(MicroProps micros, FormatQuantity quantity) {
+ // TODO: Fix this. Avoid the copy.
+ FormatQuantity copy = quantity.createCopy();
+ copy.roundToInfinity();
+ StandardPlural plural = copy.getStandardPlural(rules);
+ Modifier mod = mods[getModIndex(quantity.isNegative(), plural)];
+ micros.modMiddle = mod;
+ }
+ }
+
+ @Override
+ public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+ int prefixLen = insertPrefix(output, leftIndex);
+ int suffixLen = insertSuffix(output, rightIndex + prefixLen);
+ CurrencySpacingEnabledModifier.applyCurrencySpacing(
+ output, leftIndex, prefixLen, rightIndex + prefixLen, suffixLen, symbols);
+ return prefixLen + suffixLen;
+ }
+
+ @Override
+ public boolean isStrong() {
+ return isStrong;
+ }
+
+ @Override
+ public String getPrefix() {
+ NumberStringBuilder sb = new NumberStringBuilder(10);
+ insertPrefix(sb, 0);
+ return sb.toString();
+ }
+
+ @Override
+ public String getSuffix() {
+ NumberStringBuilder sb = new NumberStringBuilder(10);
+ insertSuffix(sb, 0);
+ return sb.toString();
+ }
+
+ private int insertPrefix(NumberStringBuilder sb, int position) {
+ enterCharSequenceMode(true);
+ int length = AffixPatternUtils.unescape(this, sb, position, this);
+ exitCharSequenceMode();
+ return length;
+ }
+
+ private int insertSuffix(NumberStringBuilder sb, int position) {
+ enterCharSequenceMode(false);
+ int length = AffixPatternUtils.unescape(this, sb, position, this);
+ exitCharSequenceMode();
+ return length;
+ }
+
+ @Override
+ public CharSequence getSymbol(int type) {
+ switch (type) {
+ case AffixPatternUtils.TYPE_MINUS_SIGN:
+ return symbols.getMinusSignString();
+ case AffixPatternUtils.TYPE_PLUS_SIGN:
+ return symbols.getPlusSignString();
+ case AffixPatternUtils.TYPE_PERCENT:
+ return symbols.getPercentString();
+ case AffixPatternUtils.TYPE_PERMILLE:
+ return symbols.getPerMillString();
+ case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
+ // FormatWidth ISO overrides the singular currency symbol
+ if (unitWidth == FormatWidth.SHORT) {
+ return currency2;
+ } else {
+ return currency1;
+ }
+ case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
+ return currency2;
+ case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
+ // NOTE: This is the code path only for patterns containing "".
+ // Most plural currencies are formatted in DataUtils.
+ assert plural != null;
+ if (currency3 == null) {
+ return currency2;
+ } else {
+ return currency3[plural.ordinal()];
+ }
+ case AffixPatternUtils.TYPE_CURRENCY_QUAD:
+ return "\uFFFD";
+ case AffixPatternUtils.TYPE_CURRENCY_QUINT:
+ return "\uFFFD";
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ /** This method contains the heart of the logic for rendering LDML affix strings. */
+ private void enterCharSequenceMode(boolean isPrefix) {
+ assert !inCharSequenceMode;
+ inCharSequenceMode = true;
+
+ // Should the output render '+' where '-' would normally appear in the pattern?
+ plusReplacesMinusSign =
+ !isNegative
+ && signDisplay == SignDisplay.ALWAYS_SHOWN
+ && patternInfo.positiveHasPlusSign() == false;
+
+ // Should we use the negative affix pattern? (If not, we will use the positive one)
+ boolean useNegativeAffixPattern =
+ patternInfo.hasNegativeSubpattern()
+ && (isNegative || (patternInfo.negativeHasMinusSign() && plusReplacesMinusSign));
+
+ // Resolve the flags for the affix pattern.
+ flags = 0;
+ if (useNegativeAffixPattern) {
+ flags |= AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN;
+ }
+ if (isPrefix) {
+ flags |= AffixPatternProvider.Flags.PREFIX;
+ }
+ if (plural != null) {
+ assert plural.ordinal() == (AffixPatternProvider.Flags.PLURAL_MASK & plural.ordinal());
+ flags |= plural.ordinal();
+ }
+
+ // Should we prepend a sign to the pattern?
+ if (!isPrefix || useNegativeAffixPattern) {
+ prependSign = false;
+ } else if (isNegative) {
+ prependSign = signDisplay != SignDisplay.NEVER_SHOWN;
+ } else {
+ prependSign = plusReplacesMinusSign;
+ }
+
+ // Finally, compute the length of the affix pattern.
+ length = patternInfo.length(flags) + (prependSign ? 1 : 0);
+ }
+
+ private void exitCharSequenceMode() {
+ assert inCharSequenceMode;
+ inCharSequenceMode = false;
+ }
+
+ @Override
+ public int length() {
+ if (inCharSequenceMode) {
+ return length;
+ } else {
+ NumberStringBuilder sb = new NumberStringBuilder(20);
+ apply(sb, 0, 0);
+ return sb.length();
+ }
+ }
+
+ @Override
+ public char charAt(int index) {
+ assert inCharSequenceMode;
+ char candidate;
+ if (prependSign && index == 0) {
+ candidate = '-';
+ } else if (prependSign) {
+ candidate = patternInfo.charAt(flags, index - 1);
+ } else {
+ candidate = patternInfo.charAt(flags, index);
+ }
+ if (plusReplacesMinusSign && candidate == '-') {
+ return '+';
+ }
+ if (perMilleReplacesPercent && candidate == '%') {
+ return '‰';
+ }
+ return candidate;
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ // Should never be called in normal circumstances
+ throw new AssertionError();
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.Map;
+
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+
+import newapi.NumberFormatter.NotationCompact;
+import newapi.NumberFormatter.NotationScientific;
+import newapi.NumberFormatter.SignDisplay;
+
+@SuppressWarnings("deprecation")
+public class NotationImpl {
+
+ public static class NotationScientificImpl extends NotationScientific.Internal
+ implements Cloneable {
+
+ int engineeringInterval;
+ boolean requireMinInt;
+ int minExponentDigits;
+ SignDisplay exponentSignDisplay;
+
+ public NotationScientificImpl(int engineeringInterval) {
+ this.engineeringInterval = engineeringInterval;
+ requireMinInt = false;
+ minExponentDigits = 1;
+ exponentSignDisplay = SignDisplay.AUTO;
+ }
+
+ public NotationScientificImpl(
+ int engineeringInterval,
+ boolean requireMinInt,
+ int minExponentDigits,
+ SignDisplay exponentSignDisplay) {
+ this.engineeringInterval = engineeringInterval;
+ this.requireMinInt = requireMinInt;
+ this.minExponentDigits = minExponentDigits;
+ this.exponentSignDisplay = exponentSignDisplay;
+ }
+
+ @Override
+ public NotationScientific withMinExponentDigits(int minExponentDigits) {
+ NotationScientificImpl other = (NotationScientificImpl) this.clone();
+ other.minExponentDigits = minExponentDigits;
+ return other;
+ }
+
+ @Override
+ public NotationScientific withExponentSignDisplay(SignDisplay exponentSignDisplay) {
+ NotationScientificImpl other = (NotationScientificImpl) this.clone();
+ other.exponentSignDisplay = exponentSignDisplay;
+ return other;
+ }
+
+ @Override
+ public Object clone() {
+ try {
+ return super.clone();
+ } catch (CloneNotSupportedException e) {
+ // Should not happen since parent is Object
+ throw new AssertionError(e);
+ }
+ }
+ }
+
+ public static class NotationCompactImpl extends NotationCompact.Internal {
+ final CompactStyle compactStyle;
+ final Map<String, Map<String, String>> compactCustomData;
+
+ public NotationCompactImpl(CompactStyle compactStyle) {
+ compactCustomData = null;
+ this.compactStyle = compactStyle;
+ }
+
+ public NotationCompactImpl(Map<String, Map<String, String>> compactCustomData) {
+ compactStyle = null;
+ this.compactCustomData = compactCustomData;
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.Locale;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.ibm.icu.impl.number.FormatQuantity4;
+import com.ibm.icu.impl.number.FormatQuantityBCD;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.text.NumberingSystem;
+import com.ibm.icu.util.Measure;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+import newapi.NumberFormatter;
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.IGrouping;
+import newapi.NumberFormatter.IRounding;
+import newapi.NumberFormatter.IntegerWidth;
+import newapi.NumberFormatter.Notation;
+import newapi.NumberFormatter.NumberFormatterResult;
+import newapi.NumberFormatter.Padding;
+import newapi.NumberFormatter.SignDisplay;
+import newapi.NumberFormatter.UnlocalizedNumberFormatter;
+
+/** @author sffc */
+public class NumberFormatterImpl extends NumberFormatter.LocalizedNumberFormatter.Internal {
+
+ private static final NumberFormatterImpl BASE = new NumberFormatterImpl();
+
+ // TODO: Set a good value here.
+ static final int DEFAULT_THRESHOLD = 3;
+
+ static final int KEY_MACROS = 0;
+ static final int KEY_LOCALE = 1;
+ static final int KEY_NOTATION = 2;
+ static final int KEY_UNIT = 3;
+ static final int KEY_ROUNDING = 4;
+ static final int KEY_GROUPING = 5;
+ static final int KEY_PADDING = 6;
+ static final int KEY_INTEGER = 7;
+ static final int KEY_SYMBOLS = 8;
+ static final int KEY_UNIT_WIDTH = 9;
+ static final int KEY_SIGN = 10;
+ static final int KEY_DECIMAL = 11;
+ static final int KEY_MAX = 12;
+
+ public static NumberFormatterImpl with() {
+ return BASE;
+ }
+
+ /** Internal method to set a starting macros. */
+ public static NumberFormatterImpl fromMacros(MacroProps macros) {
+ return new NumberFormatterImpl(BASE, KEY_MACROS, macros);
+ }
+
+ /**
+ * Internal method to construct a chain from a pattern using {@link NumberPropertyMapper}. Could
+ * be added to the public API if the feature is requested. In that case, a more efficient
+ * implementation may be desired.
+ */
+ public static UnlocalizedNumberFormatter fromPattern(
+ String string, DecimalFormatSymbols symbols) {
+ Properties props = PatternString.parseToProperties(string);
+ MacroProps macros = NumberPropertyMapper.oldToNew(props, symbols, null);
+ return fromMacros(macros);
+ }
+
+ // TODO: Reduce the number of fields.
+ final NumberFormatterImpl parent;
+ final int key;
+ final Object value;
+ volatile MacroProps resolvedMacros;
+ volatile AtomicInteger callCount;
+ volatile NumberFormatterImpl savedWithUnit;
+ volatile Worker1 compiled;
+
+ /** Base constructor; called during startup only */
+ private NumberFormatterImpl() {
+ parent = null;
+ key = -1;
+ value = null;
+ }
+
+ /** Primary constructor */
+ private NumberFormatterImpl(NumberFormatterImpl parent, int key, Object value) {
+ this.parent = parent;
+ this.key = key;
+ this.value = value;
+ }
+
+ @Override
+ public NumberFormatterImpl notation(Notation notation) {
+ return new NumberFormatterImpl(this, KEY_NOTATION, notation);
+ }
+
+ @Override
+ public NumberFormatterImpl unit(MeasureUnit unit) {
+ return new NumberFormatterImpl(this, KEY_UNIT, unit);
+ }
+
+ @Override
+ public NumberFormatterImpl rounding(IRounding rounding) {
+ return new NumberFormatterImpl(this, KEY_ROUNDING, rounding);
+ }
+
+ @Override
+ public NumberFormatterImpl grouping(IGrouping grouping) {
+ return new NumberFormatterImpl(this, KEY_GROUPING, grouping);
+ }
+
+ @Override
+ public NumberFormatterImpl padding(Padding padding) {
+ return new NumberFormatterImpl(this, KEY_PADDING, padding);
+ }
+
+ @Override
+ public NumberFormatterImpl integerWidth(IntegerWidth style) {
+ return new NumberFormatterImpl(this, KEY_INTEGER, style);
+ }
+
+ @Override
+ public NumberFormatterImpl symbols(DecimalFormatSymbols symbols) {
+ return new NumberFormatterImpl(this, KEY_SYMBOLS, symbols);
+ }
+
+ @Override
+ public NumberFormatterImpl symbols(NumberingSystem ns) {
+ return new NumberFormatterImpl(this, KEY_SYMBOLS, ns);
+ }
+
+ @Override
+ public NumberFormatterImpl unitWidth(FormatWidth style) {
+ return new NumberFormatterImpl(this, KEY_UNIT_WIDTH, style);
+ }
+
+ @Override
+ public NumberFormatterImpl sign(SignDisplay style) {
+ return new NumberFormatterImpl(this, KEY_SIGN, style);
+ }
+
+ @Override
+ public NumberFormatterImpl decimal(DecimalMarkDisplay style) {
+ return new NumberFormatterImpl(this, KEY_DECIMAL, style);
+ }
+
+ @Override
+ public NumberFormatterImpl locale(Locale locale) {
+ return new NumberFormatterImpl(this, KEY_LOCALE, ULocale.forLocale(locale));
+ }
+
+ @Override
+ public NumberFormatterImpl locale(ULocale locale) {
+ return new NumberFormatterImpl(this, KEY_LOCALE, locale);
+ }
+
+ @Override
+ public String toSkeleton() {
+ return SkeletonBuilder.macrosToSkeleton(resolve());
+ }
+
+ @Override
+ public NumberFormatterResult format(long input) {
+ return format(new FormatQuantity4(input), DEFAULT_THRESHOLD);
+ }
+
+ @Override
+ public NumberFormatterResult format(double input) {
+ return format(new FormatQuantity4(input), DEFAULT_THRESHOLD);
+ }
+
+ @Override
+ public NumberFormatterResult format(Number input) {
+ return format(new FormatQuantity4(input), DEFAULT_THRESHOLD);
+ }
+
+ @Override
+ public NumberFormatterResult format(Measure input) {
+ return formatWithThreshold(input, DEFAULT_THRESHOLD);
+ }
+
+ /**
+ * Internal version of format with support for a custom regulation threshold. A threshold of 1
+ * causes the data structures to be built right away. A threshold of 0 prevents the data
+ * structures from being built.
+ */
+ public NumberFormatterResult formatWithThreshold(Number number, int threshold) {
+ return format(new FormatQuantity4(number), threshold);
+ }
+
+ /**
+ * Internal version of format with support for a custom regulation threshold. A threshold of 1
+ * causes the data structures to be built right away. A threshold of 0 prevents the data
+ * structures from being built.
+ */
+ public NumberFormatterResult formatWithThreshold(Measure input, int threshold) {
+ MeasureUnit unit = input.getUnit();
+ Number number = input.getNumber();
+ // Use this formatter if possible
+ if (Objects.equals(resolve().unit, unit)) {
+ return formatWithThreshold(number, threshold);
+ }
+ // This mechanism saves the previously used unit, so if the user calls this method with the
+ // same unit multiple times in a row, they get a more efficient code path.
+ NumberFormatterImpl withUnit = savedWithUnit;
+ if (withUnit == null || !Objects.equals(withUnit.resolve().unit, unit)) {
+ withUnit = new NumberFormatterImpl(this, KEY_UNIT, unit);
+ savedWithUnit = withUnit;
+ }
+ return withUnit.formatWithThreshold(number, threshold);
+ }
+
+ private NumberFormatterResult format(FormatQuantityBCD fq, int threshold) {
+ NumberStringBuilder string = new NumberStringBuilder();
+ // Lazily create the AtomicInteger
+ if (callCount == null) {
+ callCount = new AtomicInteger();
+ }
+ int currentCount = callCount.incrementAndGet();
+ MicroProps micros;
+ if (currentCount == threshold) {
+ compiled = Worker1.fromMacros(resolve());
+ micros = compiled.apply(fq, string);
+ } else if (compiled != null) {
+ micros = compiled.apply(fq, string);
+ } else {
+ micros = Worker1.applyStatic(resolve(), fq, string);
+ }
+ return new NumberFormatterResult(string, fq, micros);
+ }
+
+ @Override
+ public int hashCode() {
+ return resolve().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (other == null) return false;
+ if (!(other instanceof NumberFormatterImpl)) return false;
+ return resolve().equals(((NumberFormatterImpl) other).resolve());
+ }
+
+ private MacroProps resolve() {
+ if (resolvedMacros != null) {
+ return resolvedMacros;
+ }
+ // Although the linked-list fluent storage approach requires this method,
+ // my benchmarks show that linked-list is still faster than a full clone
+ // of a MacroProps object at each step.
+ MacroProps macros = new MacroProps();
+ NumberFormatterImpl current = this;
+ while (current != BASE) {
+ switch (current.key) {
+ case KEY_MACROS:
+ macros.fallback((MacroProps) current.value);
+ break;
+ case KEY_LOCALE:
+ if (macros.loc == null) {
+ macros.loc = (ULocale) current.value;
+ }
+ break;
+ case KEY_NOTATION:
+ if (macros.notation == null) {
+ macros.notation = (Notation) current.value;
+ }
+ break;
+ case KEY_UNIT:
+ if (macros.unit == null) {
+ macros.unit = (MeasureUnit) current.value;
+ }
+ break;
+ case KEY_ROUNDING:
+ if (macros.rounding == null) {
+ macros.rounding = (IRounding) current.value;
+ }
+ break;
+ case KEY_GROUPING:
+ if (macros.grouping == null) {
+ macros.grouping = (IGrouping) current.value;
+ }
+ break;
+ case KEY_PADDING:
+ if (macros.padding == null) {
+ macros.padding = (Padding) current.value;
+ }
+ break;
+ case KEY_INTEGER:
+ if (macros.integerWidth == null) {
+ macros.integerWidth = (IntegerWidth) current.value;
+ }
+ break;
+ case KEY_SYMBOLS:
+ if (macros.symbols == null) {
+ macros.symbols = /*(Object)*/ current.value;
+ }
+ break;
+ case KEY_UNIT_WIDTH:
+ if (macros.unitWidth == null) {
+ macros.unitWidth = (FormatWidth) current.value;
+ }
+ break;
+ case KEY_SIGN:
+ if (macros.sign == null) {
+ macros.sign = (SignDisplay) current.value;
+ }
+ break;
+ case KEY_DECIMAL:
+ if (macros.decimal == null) {
+ macros.decimal = (DecimalMarkDisplay) current.value;
+ }
+ break;
+ default:
+ throw new AssertionError();
+ }
+ current = current.parent;
+ }
+ resolvedMacros = macros;
+ return macros;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.AffixPatternUtils;
+import com.ibm.icu.impl.number.LdmlPatternInfo;
+import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.RoundingUtils;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.CurrencyPluralInfo;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.ULocale;
+
+import newapi.NumberFormatter.CurrencyRounding;
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.IntegerWidth;
+import newapi.NumberFormatter.Notation;
+import newapi.NumberFormatter.Rounding;
+import newapi.NumberFormatter.SignDisplay;
+import newapi.impl.NotationImpl.NotationScientificImpl;
+import newapi.impl.RoundingImpl.RoundingImplCurrency;
+import newapi.impl.RoundingImpl.RoundingImplFraction;
+import newapi.impl.RoundingImpl.RoundingImplIncrement;
+import newapi.impl.RoundingImpl.RoundingImplSignificant;
+
+/** @author sffc */
+public final class NumberPropertyMapper {
+
+ /**
+ * Creates a new {@link MacroProps} object based on the content of a {@link Properties} object. In
+ * other words, maps Properties to MacroProps. This function is used by the JDK-compatibility API
+ * to call into the ICU 60 fluent number formatting pipeline.
+ *
+ * @param properties The property bag to be mapped.
+ * @param symbols The symbols associated with the property bag.
+ * @param exportedProperties A property bag in which to store validated properties.
+ * @return A new MacroProps containing all of the information in the Properties.
+ */
+ public static MacroProps oldToNew(
+ Properties properties, DecimalFormatSymbols symbols, Properties exportedProperties) {
+ MacroProps macros = new MacroProps();
+ ULocale locale = symbols.getULocale();
+
+ /////////////
+ // SYMBOLS //
+ /////////////
+
+ macros.symbols = symbols;
+
+ //////////////////
+ // PLURAL RULES //
+ //////////////////
+
+ macros.rules = properties.getPluralRules();
+
+ /////////////
+ // AFFIXES //
+ /////////////
+
+ AffixPatternProvider affixProvider;
+ if (properties.getCurrencyPluralInfo() == null) {
+ affixProvider =
+ new PropertiesAffixPatternProvider(
+ properties.getPositivePrefix() != null
+ ? AffixPatternUtils.escape(properties.getPositivePrefix())
+ : properties.getPositivePrefixPattern(),
+ properties.getPositiveSuffix() != null
+ ? AffixPatternUtils.escape(properties.getPositiveSuffix())
+ : properties.getPositiveSuffixPattern(),
+ properties.getNegativePrefix() != null
+ ? AffixPatternUtils.escape(properties.getNegativePrefix())
+ : properties.getNegativePrefixPattern(),
+ properties.getNegativeSuffix() != null
+ ? AffixPatternUtils.escape(properties.getNegativeSuffix())
+ : properties.getNegativeSuffixPattern());
+ } else {
+ affixProvider = new CurrencyPluralInfoAffixProvider(properties.getCurrencyPluralInfo());
+ }
+ macros.affixProvider = affixProvider;
+
+ ///////////
+ // UNITS //
+ ///////////
+
+ boolean useCurrency =
+ ((properties.getCurrency() != null)
+ || properties.getCurrencyPluralInfo() != null
+ || properties.getCurrencyUsage() != null
+ || affixProvider.hasCurrencySign());
+ Currency currency = CustomSymbolCurrency.resolve(properties.getCurrency(), locale, symbols);
+ CurrencyUsage currencyUsage = properties.getCurrencyUsage();
+ boolean explicitCurrencyUsage = currencyUsage != Properties.DEFAULT_CURRENCY_USAGE;
+ if (!explicitCurrencyUsage) {
+ currencyUsage = CurrencyUsage.STANDARD;
+ }
+ if (useCurrency) {
+ macros.unit = currency;
+ }
+
+ ///////////////////////
+ // ROUNDING STRATEGY //
+ ///////////////////////
+
+ int maxInt = properties.getMaximumIntegerDigits();
+ int minInt = properties.getMinimumIntegerDigits();
+ int maxFrac = properties.getMaximumFractionDigits();
+ int minFrac = properties.getMinimumFractionDigits();
+ int minSig = properties.getMinimumSignificantDigits();
+ int maxSig = properties.getMaximumSignificantDigits();
+ BigDecimal roundingIncrement = properties.getRoundingIncrement();
+ MathContext mathContext = RoundingUtils.getMathContextOrUnlimited(properties);
+ boolean explicitMinMaxFrac =
+ minFrac != Properties.DEFAULT_MINIMUM_FRACTION_DIGITS
+ || maxFrac != Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS;
+ boolean explicitMinMaxSig =
+ minSig != Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS
+ || maxSig != Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS;
+ // 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;
+ }
+ Rounding rounding = null;
+ if (explicitCurrencyUsage) {
+ rounding = RoundingImplCurrency.getInstance(currencyUsage).withCurrency(currency);
+ } else if (roundingIncrement != null) {
+ rounding = RoundingImplIncrement.getInstance(roundingIncrement);
+ } else if (explicitMinMaxSig) {
+ minSig = minSig < 1 ? 1 : minSig > 1000 ? 1000 : minSig;
+ maxSig = maxSig < 0 ? 1000 : maxSig < minSig ? minSig : maxSig > 1000 ? 1000 : maxSig;
+ rounding = RoundingImplSignificant.getInstance(minSig, maxSig);
+ } else if (explicitMinMaxFrac) {
+ rounding = RoundingImplFraction.getInstance(minFrac, maxFrac);
+ } else if (useCurrency) {
+ rounding = RoundingImplCurrency.getInstance(currencyUsage);
+ }
+ if (rounding != null) {
+ rounding = rounding.withMode(mathContext);
+ macros.rounding = rounding;
+ }
+
+ ///////////////////
+ // INTEGER WIDTH //
+ ///////////////////
+
+ macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt);
+
+ ///////////////////////
+ // GROUPING STRATEGY //
+ ///////////////////////
+
+ int grouping1 = properties.getGroupingSize();
+ int grouping2 = properties.getSecondaryGroupingSize();
+ int minGrouping = properties.getMinimumGroupingDigits();
+ assert grouping1 >= -2; // value of -2 means to forward no grouping information
+ grouping1 = grouping1 > 0 ? grouping1 : grouping2 > 0 ? grouping2 : grouping1;
+ grouping2 = grouping2 > 0 ? grouping2 : grouping1;
+ // TODO: Is it important to handle minGrouping > 2?
+ macros.grouping =
+ GroupingImpl.getInstance((short) grouping1, (short) grouping2, minGrouping == 2);
+
+ /////////////
+ // PADDING //
+ /////////////
+
+ if (properties.getFormatWidth() != Properties.DEFAULT_FORMAT_WIDTH) {
+ macros.padding =
+ PaddingImpl.getInstance(
+ properties.getPadString(), properties.getFormatWidth(), properties.getPadPosition());
+ }
+
+ ///////////////////////////////
+ // DECIMAL MARK ALWAYS SHOWN //
+ ///////////////////////////////
+
+ macros.decimal =
+ properties.getDecimalSeparatorAlwaysShown()
+ ? DecimalMarkDisplay.ALWAYS_SHOWN
+ : DecimalMarkDisplay.AUTO;
+
+ ///////////////////////
+ // SIGN ALWAYS SHOWN //
+ ///////////////////////
+
+ macros.sign = properties.getSignAlwaysShown() ? SignDisplay.ALWAYS_SHOWN : SignDisplay.AUTO;
+
+ /////////////////////////
+ // SCIENTIFIC NOTATION //
+ /////////////////////////
+
+ if (properties.getMinimumExponentDigits() != Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS) {
+ // Scientific notation is required.
+ // The mapping from property bag to scientific notation is nontrivial due to LDML rules.
+ // The maximum of 8 engineering digits has unknown origins and is not in the spec.
+ int engineering =
+ (maxInt != Integer.MAX_VALUE) ? maxInt : properties.getMaximumIntegerDigits();
+ engineering = (engineering < 0) ? 0 : (engineering > 8) ? minInt : engineering;
+ macros.notation =
+ new NotationScientificImpl(
+ // Engineering interval:
+ engineering,
+ // Enforce minimum integer digits (for patterns like "000.00E0"):
+ (engineering == minInt),
+ // Minimum exponent digits:
+ properties.getMinimumExponentDigits(),
+ // Exponent sign always shown:
+ properties.getExponentSignAlwaysShown()
+ ? SignDisplay.ALWAYS_SHOWN
+ : SignDisplay.AUTO);
+ // Scientific notation also involves overriding the rounding mode.
+ if (macros.rounding instanceof RoundingImplFraction) {
+ int minInt_ = properties.getMinimumIntegerDigits();
+ int minFrac_ = properties.getMinimumFractionDigits();
+ int maxFrac_ = properties.getMaximumFractionDigits();
+ if (minInt_ == 0 && maxFrac_ == 0) {
+ // Patterns like "#E0" and "##E0", which mean no rounding!
+ macros.rounding = Rounding.NONE.withMode(mathContext);
+ } else if (minInt_ == 0 && minFrac_ == 0) {
+ // Patterns like "#.##E0" (no zeros in the mantissa), which mean round to maxFrac+1
+ macros.rounding = new RoundingImplSignificant(1, maxFrac_ + 1).withMode(mathContext);
+ } else {
+ // All other scientific patterns, which mean round to minInt+maxFrac
+ macros.rounding =
+ new RoundingImplSignificant(minInt_ + minFrac_, minInt_ + maxFrac_)
+ .withMode(mathContext);
+ }
+ }
+ }
+
+ //////////////////////
+ // COMPACT NOTATION //
+ //////////////////////
+
+ if (properties.getCompactStyle() != Properties.DEFAULT_COMPACT_STYLE) {
+ if (properties.getCompactCustomData() != null) {
+ macros.notation = new NotationImpl.NotationCompactImpl(properties.getCompactCustomData());
+ } else if (properties.getCompactStyle() == CompactStyle.LONG) {
+ macros.notation = Notation.COMPACT_LONG;
+ } else {
+ macros.notation = Notation.COMPACT_SHORT;
+ }
+ // Do not forward the affix provider.
+ macros.affixProvider = null;
+ }
+
+ /////////////////
+ // MULTIPLIERS //
+ /////////////////
+
+ if (properties.getMagnitudeMultiplier() != Properties.DEFAULT_MAGNITUDE_MULTIPLIER) {
+ macros.multiplier = new MultiplierImpl(properties.getMagnitudeMultiplier());
+ } else if (properties.getMultiplier() != Properties.DEFAULT_MULTIPLIER) {
+ macros.multiplier = new MultiplierImpl(properties.getMultiplier());
+ }
+
+ //////////////////////
+ // PROPERTY EXPORTS //
+ //////////////////////
+
+ if (exportedProperties != null) {
+
+ exportedProperties.setMathContext(mathContext);
+ exportedProperties.setRoundingMode(mathContext.getRoundingMode());
+ exportedProperties.setMinimumIntegerDigits(minInt);
+ exportedProperties.setMaximumIntegerDigits(maxInt);
+
+ Rounding rounding_;
+ if (rounding instanceof CurrencyRounding) {
+ rounding_ = ((CurrencyRounding) rounding).withCurrency(currency);
+ } else {
+ rounding_ = rounding;
+ }
+ int minFrac_ = minFrac;
+ int maxFrac_ = maxFrac;
+ int minSig_ = minSig;
+ int maxSig_ = maxSig;
+ BigDecimal increment_ = null;
+ if (rounding_ instanceof RoundingImplFraction) {
+ minFrac_ = ((RoundingImplFraction) rounding_).minFrac;
+ maxFrac_ = ((RoundingImplFraction) rounding_).maxFrac;
+ } else if (rounding_ instanceof RoundingImplIncrement) {
+ increment_ = ((RoundingImplIncrement) rounding_).increment;
+ minFrac_ = increment_.scale();
+ maxFrac_ = increment_.scale();
+ } else if (rounding_ instanceof RoundingImplSignificant) {
+ minSig_ = ((RoundingImplSignificant) rounding_).minSig;
+ maxSig_ = ((RoundingImplSignificant) rounding_).maxSig;
+ }
+
+ exportedProperties.setMinimumFractionDigits(minFrac_);
+ exportedProperties.setMaximumFractionDigits(maxFrac_);
+ exportedProperties.setMinimumSignificantDigits(minSig_);
+ exportedProperties.setMaximumSignificantDigits(maxSig_);
+ exportedProperties.setRoundingIncrement(increment_);
+ }
+
+ return macros;
+ }
+
+ private static class PropertiesAffixPatternProvider implements AffixPatternProvider {
+ private final String posPrefixPattern;
+ private final String posSuffixPattern;
+ private final String negPrefixPattern;
+ private final String negSuffixPattern;
+
+ public PropertiesAffixPatternProvider(String ppp, String psp, String npp, String nsp) {
+ if (ppp == null) ppp = "";
+ if (psp == null) psp = "";
+ if (npp == null && nsp != null) npp = "-"; // TODO: This is a hack.
+ if (nsp == null && npp != null) nsp = "";
+ posPrefixPattern = ppp;
+ posSuffixPattern = psp;
+ negPrefixPattern = npp;
+ negSuffixPattern = nsp;
+ }
+
+ @Override
+ public char charAt(int flags, int i) {
+ boolean prefix = (flags & Flags.PREFIX) != 0;
+ boolean negative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
+ if (prefix && negative) {
+ return negPrefixPattern.charAt(i);
+ } else if (prefix) {
+ return posPrefixPattern.charAt(i);
+ } else if (negative) {
+ return negSuffixPattern.charAt(i);
+ } else {
+ return posSuffixPattern.charAt(i);
+ }
+ }
+
+ @Override
+ public int length(int flags) {
+ boolean prefix = (flags & Flags.PREFIX) != 0;
+ boolean negative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
+ if (prefix && negative) {
+ return negPrefixPattern.length();
+ } else if (prefix) {
+ return posPrefixPattern.length();
+ } else if (negative) {
+ return negSuffixPattern.length();
+ } else {
+ return posSuffixPattern.length();
+ }
+ }
+
+ @Override
+ public boolean positiveHasPlusSign() {
+ return AffixPatternUtils.containsType(posPrefixPattern, AffixPatternUtils.TYPE_PLUS_SIGN)
+ || AffixPatternUtils.containsType(posSuffixPattern, AffixPatternUtils.TYPE_PLUS_SIGN);
+ }
+
+ @Override
+ public boolean hasNegativeSubpattern() {
+ return negPrefixPattern != null;
+ }
+
+ @Override
+ public boolean negativeHasMinusSign() {
+ return AffixPatternUtils.containsType(negPrefixPattern, AffixPatternUtils.TYPE_MINUS_SIGN)
+ || AffixPatternUtils.containsType(negSuffixPattern, AffixPatternUtils.TYPE_MINUS_SIGN);
+ }
+
+ @Override
+ public boolean hasCurrencySign() {
+ return AffixPatternUtils.hasCurrencySymbols(posPrefixPattern)
+ || AffixPatternUtils.hasCurrencySymbols(posSuffixPattern)
+ || AffixPatternUtils.hasCurrencySymbols(negPrefixPattern)
+ || AffixPatternUtils.hasCurrencySymbols(negSuffixPattern);
+ }
+
+ @Override
+ public boolean containsSymbolType(int type) {
+ return AffixPatternUtils.containsType(posPrefixPattern, type)
+ || AffixPatternUtils.containsType(posSuffixPattern, type)
+ || AffixPatternUtils.containsType(negPrefixPattern, type)
+ || AffixPatternUtils.containsType(negSuffixPattern, type);
+ }
+ }
+
+ private static class CurrencyPluralInfoAffixProvider implements AffixPatternProvider {
+ private final AffixPatternProvider[] affixesByPlural;
+
+ public CurrencyPluralInfoAffixProvider(CurrencyPluralInfo cpi) {
+ affixesByPlural = new PatternParseResult[StandardPlural.COUNT];
+ for (StandardPlural plural : StandardPlural.VALUES) {
+ affixesByPlural[plural.ordinal()] =
+ LdmlPatternInfo.parse(cpi.getCurrencyPluralPattern(plural.getKeyword()));
+ }
+ }
+
+ @Override
+ public char charAt(int flags, int i) {
+ int pluralOrdinal = (flags & Flags.PLURAL_MASK);
+ return affixesByPlural[pluralOrdinal].charAt(flags, i);
+ }
+
+ @Override
+ public int length(int flags) {
+ int pluralOrdinal = (flags & Flags.PLURAL_MASK);
+ return affixesByPlural[pluralOrdinal].length(flags);
+ }
+
+ @Override
+ public boolean positiveHasPlusSign() {
+ return affixesByPlural[StandardPlural.OTHER.ordinal()].positiveHasPlusSign();
+ }
+
+ @Override
+ public boolean hasNegativeSubpattern() {
+ return affixesByPlural[StandardPlural.OTHER.ordinal()].hasNegativeSubpattern();
+ }
+
+ @Override
+ public boolean negativeHasMinusSign() {
+ return affixesByPlural[StandardPlural.OTHER.ordinal()].negativeHasMinusSign();
+ }
+
+ @Override
+ public boolean hasCurrencySign() {
+ return affixesByPlural[StandardPlural.OTHER.ordinal()].hasCurrencySign();
+ }
+
+ @Override
+ public boolean containsSymbolType(int type) {
+ return affixesByPlural[StandardPlural.OTHER.ordinal()].containsSymbolType(type);
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+
+import newapi.NumberFormatter.Padding;
+
+public class PaddingImpl extends Padding.Internal {
+
+ String paddingString;
+ int targetWidth;
+ PadPosition position;
+
+ public static final PaddingImpl NONE = new PaddingImpl();
+
+ public static PaddingImpl getInstance(
+ String paddingString, int targetWidth, PadPosition position) {
+ // TODO: Add a few default implementations
+ return new PaddingImpl(paddingString, targetWidth, position);
+ }
+
+ /** Default constructor, producing an empty instance */
+ public PaddingImpl() {
+ paddingString = null;
+ targetWidth = -1;
+ position = null;
+ }
+
+ private PaddingImpl(String paddingString, int targetWidth, PadPosition position) {
+ this.paddingString = (paddingString == null) ? " " : paddingString;
+ this.targetWidth = targetWidth;
+ this.position = (position == null) ? PadPosition.BEFORE_PREFIX : position;
+ }
+
+ public int applyModsAndMaybePad(
+ MicroProps micros, NumberStringBuilder string, int leftIndex, int rightIndex) {
+ // Apply modInner (scientific notation) before padding
+ int innerLength = micros.modInner.apply(string, leftIndex, rightIndex);
+
+ // No padding; apply the mods and leave.
+ if (targetWidth < 0) {
+ return applyMicroMods(micros, string, leftIndex, rightIndex + innerLength);
+ }
+
+ // Estimate the padding width needed.
+ // TODO: Make this more efficient (less copying)
+ // TODO: How to handle when padding is inserted between a currency sign and the number
+ // when currency spacing is in play?
+ NumberStringBuilder backup = new NumberStringBuilder(string);
+ int length = innerLength + applyMicroMods(micros, string, leftIndex, rightIndex + innerLength);
+ int requiredPadding = targetWidth - string.codePointCount();
+
+ if (requiredPadding <= 0) {
+ // Padding is not required.
+ return length;
+ }
+
+ length = innerLength;
+ string.copyFrom(backup);
+ if (position == PadPosition.AFTER_PREFIX) {
+ length += addPaddingHelper(paddingString, requiredPadding, string, leftIndex);
+ } else if (position == PadPosition.BEFORE_SUFFIX) {
+ length += addPaddingHelper(paddingString, requiredPadding, string, rightIndex + length);
+ }
+ length += applyMicroMods(micros, string, leftIndex, rightIndex + length);
+ if (position == PadPosition.BEFORE_PREFIX) {
+ length = addPaddingHelper(paddingString, requiredPadding, string, leftIndex);
+ } else if (position == PadPosition.AFTER_SUFFIX) {
+ length = addPaddingHelper(paddingString, requiredPadding, string, rightIndex + length);
+ }
+
+ // The length might not be exactly right due to currency spacing.
+ // Make an adjustment if needed.
+ while (string.codePointCount() < targetWidth) {
+ int insertIndex;
+ switch (position) {
+ case AFTER_PREFIX:
+ insertIndex = leftIndex + length;
+ break;
+ case BEFORE_SUFFIX:
+ insertIndex = rightIndex + length;
+ break;
+ default:
+ // Should not happen since currency spacing is always on the inside.
+ throw new AssertionError();
+ }
+ length += string.insert(insertIndex, paddingString, null);
+ }
+
+ return length;
+ }
+
+ private static int applyMicroMods(
+ MicroProps micros, NumberStringBuilder string, int leftIndex, int rightIndex) {
+ int length = micros.modMiddle.apply(string, leftIndex, rightIndex);
+ length += micros.modOuter.apply(string, leftIndex, rightIndex + length);
+ return length;
+ }
+
+ private static int addPaddingHelper(
+ String paddingString, int requiredPadding, NumberStringBuilder string, int index) {
+ for (int i = 0; i < requiredPadding; i++) {
+ string.insert(index, paddingString, null);
+ }
+ return paddingString.length() * requiredPadding;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+// License & terms of use: http://www.unicode.org/copyright.html#License
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.NumberFormat;
+
+import newapi.NumberFormatter.DecimalMarkDisplay;
+
+public class PositiveDecimalImpl implements Format.TargetFormat {
+
+ @Override
+ public int target(FormatQuantity input, NumberStringBuilder string, int startIndex) {
+ // FIXME
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * @param micros
+ * @param fq
+ * @param output
+ * @return
+ */
+ public static int apply(MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
+ int length = 0;
+ if (input.isInfinite()) {
+ length += string.insert(length, micros.symbols.getInfinity(), NumberFormat.Field.INTEGER);
+
+ } else if (input.isNaN()) {
+ length += string.insert(length, micros.symbols.getNaN(), NumberFormat.Field.INTEGER);
+
+ } else {
+ // Add the integer digits
+ length += addIntegerDigits(micros, input, string);
+
+ // Add the decimal point
+ if (input.getLowerDisplayMagnitude() < 0
+ || micros.decimal == DecimalMarkDisplay.ALWAYS_SHOWN) {
+ length +=
+ string.insert(
+ length,
+ micros.useCurrency
+ ? micros.symbols.getMonetaryDecimalSeparatorString()
+ : micros.symbols.getDecimalSeparatorString(),
+ NumberFormat.Field.DECIMAL_SEPARATOR);
+ }
+
+ // Add the fraction digits
+ length += addFractionDigits(micros, input, string);
+ }
+
+ return length;
+ }
+
+ private static int addIntegerDigits(
+ MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
+ int length = 0;
+ int integerCount = input.getUpperDisplayMagnitude() + 1;
+ for (int i = 0; i < integerCount; i++) {
+ // Add grouping separator
+ if (micros.grouping.groupAtPosition(i, input)) {
+ length +=
+ string.insert(
+ 0,
+ micros.useCurrency
+ ? micros.symbols.getMonetaryGroupingSeparatorString()
+ : micros.symbols.getGroupingSeparatorString(),
+ NumberFormat.Field.GROUPING_SEPARATOR);
+ }
+
+ // Get and append the next digit value
+ byte nextDigit = input.getDigit(i);
+ if (micros.symbols.getCodePointZero() != -1) {
+ length +=
+ string.insertCodePoint(
+ 0, micros.symbols.getCodePointZero() + nextDigit, NumberFormat.Field.INTEGER);
+ } else {
+ length +=
+ string.insert(
+ 0, micros.symbols.getDigitStringsLocal()[nextDigit], NumberFormat.Field.INTEGER);
+ }
+ }
+ return length;
+ }
+
+ private static int addFractionDigits(
+ MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
+ 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);
+ if (micros.symbols.getCodePointZero() != -1) {
+ length +=
+ string.appendCodePoint(
+ micros.symbols.getCodePointZero() + nextDigit, NumberFormat.Field.FRACTION);
+ } else {
+ length +=
+ string.append(
+ micros.symbols.getDigitStringsLocal()[nextDigit], NumberFormat.Field.FRACTION);
+ }
+ }
+ return length;
+ }
+
+ @Override
+ public void export(Properties properties) {
+ throw new UnsupportedOperationException();
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+
+public interface QuantityChain {
+ QuantityChain chain(QuantityChain parent);
+ MicroProps withQuantity(FormatQuantity quantity);
+}
\ No newline at end of file
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.Map;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.text.PluralRules;
+
+public class QuantityDependentModOuter implements QuantityChain {
+ final Map<StandardPlural, Modifier> data;
+ final PluralRules rules;
+ /* final */ QuantityChain parent;
+
+ public QuantityDependentModOuter(Map<StandardPlural, Modifier> data, PluralRules rules) {
+ this.data = data;
+ this.rules = rules;
+ }
+
+ @Override
+ public QuantityChain chain(QuantityChain parent) {
+ this.parent = parent;
+ return this;
+ }
+
+ @Override
+ public MicroProps withQuantity(FormatQuantity quantity) {
+ MicroProps micros = parent.withQuantity(quantity);
+ // TODO: Avoid the copy here?
+ FormatQuantity copy = quantity.createCopy();
+ micros.rounding.apply(copy);
+ micros.modOuter = data.get(copy.getStandardPlural(rules));
+ return micros;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.FormatQuantity4;
+import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
+import com.ibm.icu.impl.number.RoundingUtils;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+
+import newapi.NumberFormatter.CurrencyRounding;
+import newapi.NumberFormatter.FractionRounding;
+import newapi.NumberFormatter.IRounding;
+import newapi.NumberFormatter.Rounding;
+
+/**
+ * The internal version of {@link Rounding} with additional methods.
+ *
+ * <p>Although it seems as though RoundingImpl should extend Rounding, it actually extends
+ * FractionRounding. This is because instances of FractionRounding are self-contained rounding
+ * instances themselves, and they need to implement RoundingImpl. When ICU adopts Java 8, there will
+ * be more options for the polymorphism, such as multiple inheritance with interfaces having default
+ * methods and static factory methods on interfaces.
+ */
+@SuppressWarnings("deprecation")
+public abstract class RoundingImpl extends FractionRounding.Internal implements Cloneable {
+
+ public static RoundingImpl forPattern(PatternParseResult patternInfo) {
+ if (patternInfo.positive.rounding != null) {
+ return RoundingImplIncrement.getInstance(patternInfo.positive.rounding.toBigDecimal());
+ } else if (patternInfo.positive.minimumSignificantDigits > 0) {
+ return RoundingImplSignificant.getInstance(
+ patternInfo.positive.minimumSignificantDigits,
+ patternInfo.positive.maximumSignificantDigits);
+ } else if (patternInfo.positive.exponentDigits > 0) {
+ // FIXME
+ throw new UnsupportedOperationException();
+ } else {
+ return RoundingImplFraction.getInstance(
+ patternInfo.positive.minimumFractionDigits, patternInfo.positive.maximumFractionDigits);
+ }
+ }
+
+ /**
+ * Returns a RoundingImpl no matter what is the type of the provided argument. If the argument is
+ * already a RoundingImpl, this method just returns the same object. Otherwise, it does some
+ * processing to build a RoundingImpl.
+ *
+ * @param rounding The input object, which might or might not be a RoundingImpl.
+ * @param currency A currency object to use in case the input object needs it.
+ * @return A RoundingImpl object.
+ */
+ public static RoundingImpl normalizeType(IRounding rounding, Currency currency) {
+ if (rounding instanceof RoundingImpl) {
+ return (RoundingImpl) rounding;
+ } else if (rounding instanceof RoundingImplCurrency) {
+ return ((RoundingImplCurrency) rounding).withCurrency(currency);
+ } else {
+ return RoundingImplLambda.getInstance(rounding);
+ }
+ }
+
+ private static final MathContext DEFAULT_MATH_CONTEXT =
+ RoundingUtils.mathContextUnlimited(RoundingMode.HALF_EVEN);
+
+ public MathContext mathContext;
+
+ public RoundingImpl() {
+ this.mathContext = DEFAULT_MATH_CONTEXT;
+ // TODO: This is ugly, but necessary if a RoundingImpl is created
+ // before this class has been initialized.
+ if (this.mathContext == null) {
+ this.mathContext = RoundingUtils.mathContextUnlimited(RoundingMode.HALF_EVEN);
+ }
+ }
+
+ @Override
+ public Rounding withMode(RoundingMode roundingMode) {
+ return withMode(RoundingUtils.mathContextUnlimited(roundingMode));
+ }
+
+ @Override
+ public Rounding withMode(MathContext mathContext) {
+ if (this.mathContext.equals(mathContext)) {
+ return this;
+ }
+ RoundingImpl other = (RoundingImpl) this.clone();
+ other.mathContext = mathContext;
+ return other;
+ }
+
+ abstract void apply(FormatQuantity value);
+
+ @Override
+ public BigDecimal round(BigDecimal input) {
+ // Provided for API compatibility.
+ FormatQuantity fq = new FormatQuantity4(input);
+ this.apply(fq);
+ return fq.toBigDecimal();
+ }
+
+ static interface MultiplierProducer {
+ int getMultiplier(int magnitude);
+ }
+
+ int chooseMultiplierAndApply(FormatQuantity input, MultiplierProducer producer) {
+ // TODO: Make a better and more efficient implementation.
+ // TODO: Avoid the object creation here.
+ FormatQuantity copy = input.createCopy();
+
+ assert !input.isZero();
+ int magnitude = input.getMagnitude();
+ int multiplier = producer.getMultiplier(magnitude);
+ input.adjustMagnitude(multiplier);
+ apply(input);
+
+ // If the number turned to zero when rounding, do not re-attempt the rounding.
+ if (!input.isZero() && input.getMagnitude() == magnitude + multiplier + 1) {
+ magnitude += 1;
+ input.copyFrom(copy);
+ multiplier = producer.getMultiplier(magnitude);
+ input.adjustMagnitude(multiplier);
+ assert input.getMagnitude() == magnitude + multiplier - 1;
+ apply(input);
+ assert input.getMagnitude() == magnitude + multiplier;
+ }
+
+ return multiplier;
+ }
+
+ @Override
+ public Object clone() {
+ try {
+ return super.clone();
+ } catch (CloneNotSupportedException e) {
+ // Should not happen since parent is Object
+ throw new AssertionError(e);
+ }
+ }
+
+ /** A dummy class used when the number has already been rounded elsewhere. */
+ public static class RoundingImplDummy extends RoundingImpl {
+ public static final RoundingImplDummy INSTANCE = new RoundingImplDummy();
+
+ private RoundingImplDummy() {}
+
+ @Override
+ void apply(FormatQuantity value) {}
+ }
+
+ public static class RoundingImplInfinity extends RoundingImpl {
+ @Override
+ void apply(FormatQuantity value) {
+ value.roundToInfinity();
+ value.setFractionLength(0, Integer.MAX_VALUE);
+ }
+ }
+
+ public static class RoundingImplFraction extends RoundingImpl {
+ int minFrac;
+ int maxFrac;
+
+ private static final RoundingImplFraction FIXED_0 = new RoundingImplFraction(0, 0);
+ private static final RoundingImplFraction FIXED_2 = new RoundingImplFraction(2, 2);
+
+ /** Assumes that minFrac <= maxFrac. */
+ public static RoundingImplFraction getInstance(int minFrac, int maxFrac) {
+ assert minFrac >= 0 && minFrac <= maxFrac;
+ if (minFrac == 0 && maxFrac == 0) {
+ return FIXED_0;
+ } else if (minFrac == 2 && maxFrac == 2) {
+ return FIXED_2;
+ } else {
+ return new RoundingImplFraction(minFrac, maxFrac);
+ }
+ }
+
+ /** Hook for public static final; uses integer rounding */
+ public RoundingImplFraction() {
+ this(0, 0);
+ }
+
+ private RoundingImplFraction(int minFrac, int maxFrac) {
+ this.minFrac = minFrac;
+ this.maxFrac = maxFrac;
+ }
+
+ @Override
+ void apply(FormatQuantity value) {
+ value.roundToMagnitude(getRoundingMagnitude(maxFrac), mathContext);
+ value.setFractionLength(Math.max(0, -getDisplayMagnitude(minFrac)), Integer.MAX_VALUE);
+ }
+
+ static int getRoundingMagnitude(int maxFrac) {
+ if (maxFrac == Integer.MAX_VALUE) {
+ return Integer.MIN_VALUE;
+ }
+ return -maxFrac;
+ }
+
+ static int getDisplayMagnitude(int minFrac) {
+ if (minFrac == 0) {
+ return Integer.MAX_VALUE;
+ }
+ return -minFrac;
+ }
+
+ @Override
+ public Rounding withMinFigures(int minFigures) {
+ if (minFigures > 0 && minFigures <= MAX_VALUE) {
+ return RoundingImplFractionSignificant.getInstance(this, minFigures, -1);
+ } else {
+ throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
+ }
+ }
+
+ @Override
+ public Rounding withMaxFigures(int maxFigures) {
+ if (maxFigures > 0 && maxFigures <= MAX_VALUE) {
+ return RoundingImplFractionSignificant.getInstance(this, -1, maxFigures);
+ } else {
+ throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
+ }
+ }
+ }
+
+ public static class RoundingImplSignificant extends RoundingImpl {
+ int minSig;
+ int maxSig;
+
+ private static final RoundingImplSignificant FIXED_2 = new RoundingImplSignificant(2, 2);
+ private static final RoundingImplSignificant FIXED_3 = new RoundingImplSignificant(3, 3);
+ private static final RoundingImplSignificant RANGE_2_3 = new RoundingImplSignificant(2, 3);
+
+ /** Assumes that minSig <= maxSig. */
+ public static RoundingImplSignificant getInstance(int minSig, int maxSig) {
+ assert minSig >= 0 && minSig <= maxSig;
+ if (minSig == 2 && maxSig == 2) {
+ return FIXED_2;
+ } else if (minSig == 3 && maxSig == 3) {
+ return FIXED_3;
+ } else if (minSig == 2 && maxSig == 3) {
+ return RANGE_2_3;
+ } else {
+ return new RoundingImplSignificant(minSig, maxSig);
+ }
+ }
+
+ RoundingImplSignificant(int minSig, int maxSig) {
+ this.minSig = minSig;
+ this.maxSig = maxSig;
+ }
+
+ @Override
+ void apply(FormatQuantity value) {
+ value.roundToMagnitude(getRoundingMagnitude(value, maxSig), mathContext);
+ value.setFractionLength(Math.max(0, -getDisplayMagnitude(value, minSig)), Integer.MAX_VALUE);
+ }
+
+ /** Version of {@link #apply} that obeys minInt constraints. */
+ public void apply(FormatQuantity quantity, int minInt) {
+ assert quantity.isZero();
+ quantity.setFractionLength(minSig - minInt, Integer.MAX_VALUE);
+ }
+
+ static int getRoundingMagnitude(FormatQuantity value, int maxSig) {
+ if (maxSig == Integer.MAX_VALUE) {
+ return Integer.MIN_VALUE;
+ }
+ int magnitude = value.isZero() ? 0 : value.getMagnitude();
+ return magnitude - maxSig + 1;
+ }
+
+ static int getDisplayMagnitude(FormatQuantity value, int minSig) {
+ int magnitude = value.isZero() ? 0 : value.getMagnitude();
+ return magnitude - minSig + 1;
+ }
+ }
+
+ public static class RoundingImplFractionSignificant extends RoundingImpl {
+ int minFrac;
+ int maxFrac;
+ int minSig;
+ int maxSig;
+
+ // Package-private
+ static final RoundingImplFractionSignificant COMPACT_STRATEGY =
+ new RoundingImplFractionSignificant(0, 0, 2, -1);
+
+ public static Rounding getInstance(FractionRounding _base, int minSig, int maxSig) {
+ assert _base instanceof RoundingImplFraction;
+ RoundingImplFraction base = (RoundingImplFraction) _base;
+ if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 2 /* && maxSig == -1 */) {
+ return COMPACT_STRATEGY;
+ } else {
+ return new RoundingImplFractionSignificant(base.minFrac, base.maxFrac, minSig, maxSig);
+ }
+ }
+
+ /** Assumes that minFrac <= maxFrac and minSig <= maxSig except for -1. */
+ private RoundingImplFractionSignificant(int minFrac, int maxFrac, int minSig, int maxSig) {
+ // Exactly one of the arguments should be -1, either minSig or maxSig.
+ assert (minFrac != -1 && maxFrac != -1 && minSig == -1 && maxSig != -1 && minFrac <= maxFrac)
+ || (minFrac != -1 && maxFrac != -1 && minSig != -1 && maxSig == -1 && minFrac <= maxFrac);
+ this.minFrac = minFrac;
+ this.maxFrac = maxFrac;
+ this.minSig = minSig;
+ this.maxSig = maxSig;
+ }
+
+ @Override
+ void apply(FormatQuantity value) {
+ int displayMag = RoundingImplFraction.getDisplayMagnitude(minFrac);
+ int roundingMag = RoundingImplFraction.getRoundingMagnitude(maxFrac);
+ if (minSig == -1) {
+ // Max Sig override
+ int candidate = RoundingImplSignificant.getRoundingMagnitude(value, maxSig);
+ roundingMag = Math.max(roundingMag, candidate);
+ } else {
+ // Min Sig override
+ int candidate = RoundingImplSignificant.getDisplayMagnitude(value, minSig);
+ roundingMag = Math.min(roundingMag, candidate);
+ }
+ value.roundToMagnitude(roundingMag, mathContext);
+ value.setFractionLength(Math.max(0, -displayMag), Integer.MAX_VALUE);
+ }
+ }
+
+ public static class RoundingImplIncrement extends RoundingImpl {
+ BigDecimal increment;
+
+ private static final RoundingImplIncrement NICKEL =
+ new RoundingImplIncrement(BigDecimal.valueOf(0.5));
+
+ public static RoundingImplIncrement getInstance(BigDecimal increment) {
+ assert increment != null;
+ if (increment.compareTo(NICKEL.increment) == 0) {
+ return NICKEL;
+ } else {
+ return new RoundingImplIncrement(increment);
+ }
+ }
+
+ private RoundingImplIncrement(BigDecimal increment) {
+ this.increment = increment;
+ }
+
+ @Override
+ void apply(FormatQuantity value) {
+ value.roundToIncrement(increment, mathContext);
+ value.setFractionLength(increment.scale(), increment.scale());
+ }
+ }
+
+ public static class RoundingImplLambda extends RoundingImpl {
+ IRounding lambda;
+
+ public static RoundingImplLambda getInstance(IRounding lambda) {
+ assert !(lambda instanceof Rounding);
+ return new RoundingImplLambda(lambda);
+ }
+
+ private RoundingImplLambda(IRounding lambda) {
+ this.lambda = lambda;
+ }
+
+ @Override
+ void apply(FormatQuantity value) {
+ // TODO: Cache the BigDecimal between calls?
+ BigDecimal temp = value.toBigDecimal();
+ temp = lambda.round(temp);
+ value.setToBigDecimal(temp);
+ value.setFractionLength(temp.scale(), Integer.MAX_VALUE);
+ }
+ }
+
+ /**
+ * NOTE: This is unlike the other classes here. It is NOT a standalone rounder and it does NOT
+ * extend RoundingImpl.
+ */
+ public static class RoundingImplCurrency extends CurrencyRounding.Internal {
+ final CurrencyUsage usage;
+ final MathContext mc;
+
+ private static final RoundingImplCurrency MONETARY_STANDARD =
+ new RoundingImplCurrency(CurrencyUsage.STANDARD, DEFAULT_MATH_CONTEXT);
+
+ private static final RoundingImplCurrency MONETARY_CASH =
+ new RoundingImplCurrency(CurrencyUsage.CASH, DEFAULT_MATH_CONTEXT);
+
+ public static RoundingImplCurrency getInstance(CurrencyUsage usage) {
+ if (usage == CurrencyUsage.STANDARD) {
+ return MONETARY_STANDARD;
+ } else if (usage == CurrencyUsage.CASH) {
+ return MONETARY_CASH;
+ } else {
+ throw new AssertionError();
+ }
+ }
+
+ private RoundingImplCurrency(CurrencyUsage usage, MathContext mc) {
+ this.usage = usage;
+ this.mc = mc;
+ }
+
+ @Override
+ public RoundingImpl withCurrency(Currency currency) {
+ assert currency != null;
+ double incrementDouble = currency.getRoundingIncrement(usage);
+ if (incrementDouble != 0.0) {
+ BigDecimal increment = BigDecimal.valueOf(incrementDouble);
+ return RoundingImplIncrement.getInstance(increment);
+ } else {
+ int minMaxFrac = currency.getDefaultFractionDigits(usage);
+ return RoundingImplFraction.getInstance(minMaxFrac, minMaxFrac);
+ }
+ }
+
+ @Override
+ public RoundingImplCurrency withMode(RoundingMode roundingMode) {
+ // This is similar to RoundingImpl#withMode().
+ return withMode(RoundingUtils.mathContextUnlimited(roundingMode));
+ }
+
+ @Override
+ public RoundingImplCurrency withMode(MathContext mathContext) {
+ // This is similar to RoundingImpl#withMode().
+ if (mc.equals(mathContext)) {
+ return this;
+ }
+ return new RoundingImplCurrency(usage, mathContext);
+ }
+
+ @Override
+ public BigDecimal round(BigDecimal input) {
+ throw new UnsupportedOperationException(
+ "A currency must be specified before calling this method.");
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat;
+
+import newapi.NumberFormatter.SignDisplay;
+import newapi.impl.RoundingImpl.RoundingImplDummy;
+import newapi.impl.RoundingImpl.RoundingImplSignificant;
+
+public class ScientificImpl implements QuantityChain, RoundingImpl.MultiplierProducer {
+
+ final NotationImpl.NotationScientificImpl notation;
+ final DecimalFormatSymbols symbols;
+ final ScientificModifier[] precomputedMods;
+ /* final */ QuantityChain parent;
+
+ public static ScientificImpl getInstance(
+ NotationImpl.NotationScientificImpl notation, DecimalFormatSymbols symbols, boolean build) {
+ return new ScientificImpl(notation, symbols, build);
+ }
+
+ private ScientificImpl(
+ NotationImpl.NotationScientificImpl notation, DecimalFormatSymbols symbols, boolean build) {
+ this.notation = notation;
+ this.symbols = symbols;
+
+ if (build) {
+ // Pre-build the modifiers for exponents -12 through 12
+ precomputedMods = new ScientificModifier[25];
+ for (int i = -12; i <= 12; i++) {
+ precomputedMods[i + 12] = new ScientificModifier(i);
+ }
+ } else {
+ precomputedMods = null;
+ }
+ }
+
+ @Override
+ public QuantityChain chain(QuantityChain parent) {
+ this.parent = parent;
+ return this;
+ }
+
+ @Override
+ public MicroProps withQuantity(FormatQuantity quantity) {
+ MicroProps micros = parent.withQuantity(quantity);
+ assert micros.rounding != null;
+
+ // Treat zero as if it had magnitude 0
+ int exponent;
+ if (quantity.isZero()) {
+ if (notation.requireMinInt && micros.rounding instanceof RoundingImplSignificant) {
+ // Shown "00.000E0" on pattern "00.000E0"
+ ((RoundingImplSignificant) micros.rounding).apply(quantity, notation.engineeringInterval);
+ exponent = 0;
+ } else {
+ micros.rounding.apply(quantity);
+ exponent = 0;
+ }
+ } else {
+ exponent = -micros.rounding.chooseMultiplierAndApply(quantity, this);
+ }
+
+ // Add the Modifier for the scientific format.
+ if (precomputedMods != null && exponent >= -12 && exponent <= 12) {
+ micros.modInner = precomputedMods[exponent + 12];
+ } else {
+ micros.modInner = new ScientificModifier(exponent);
+ }
+
+ // We already performed rounding. Do not perform it again.
+ micros.rounding = RoundingImplDummy.INSTANCE;
+
+ return micros;
+ }
+
+ private class ScientificModifier implements Modifier {
+ final int exponent;
+
+ ScientificModifier(int exponent) {
+ this.exponent = exponent;
+ }
+
+ @Override
+ public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+ // FIXME: Localized exponent separator location.
+ int i = rightIndex;
+ // Append the exponent separator and sign
+ i += output.insert(i, symbols.getExponentSeparator(), NumberFormat.Field.EXPONENT_SYMBOL);
+ if (exponent < 0 && notation.exponentSignDisplay != SignDisplay.NEVER_SHOWN) {
+ i += output.insert(i, symbols.getMinusSignString(), NumberFormat.Field.EXPONENT_SIGN);
+ } else if (notation.exponentSignDisplay == SignDisplay.ALWAYS_SHOWN) {
+ i += output.insert(i, symbols.getPlusSignString(), NumberFormat.Field.EXPONENT_SIGN);
+ }
+ // Append the exponent digits (using a simple inline algorithm)
+ int disp = Math.abs(exponent);
+ for (int j = 0; j < notation.minExponentDigits || disp > 0; j++, disp /= 10) {
+ int d = disp % 10;
+ String digitString = symbols.getDigitStringsLocal()[d];
+ i += output.insert(i - j, digitString, NumberFormat.Field.EXPONENT);
+ }
+ return i - rightIndex;
+ }
+
+ @Override
+ public boolean isStrong() {
+ return true;
+ }
+
+ @Override
+ public String getPrefix() {
+ // Should never get called
+ throw new AssertionError();
+ }
+
+ @Override
+ public String getSuffix() {
+ // Should never get called
+ throw new AssertionError();
+ }
+ }
+
+ @Override
+ public int getMultiplier(int magnitude) {
+ int interval = notation.engineeringInterval;
+ int digitsShown;
+ if (notation.requireMinInt) {
+ // For patterns like "000.00E0" and ".00E0"
+ digitsShown = interval;
+ } else if (interval <= 1) {
+ // For patterns like "0.00E0" and "@@@E0"
+ digitsShown = 1;
+ } else {
+ // For patterns like "##0.00"
+ digitsShown = ((magnitude % interval + interval) % interval) + 1;
+ }
+ return digitsShown - magnitude - 1;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.text.NumberingSystem;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.Dimensionless;
+import com.ibm.icu.util.MeasureUnit;
+
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.FractionRounding;
+import newapi.NumberFormatter.Grouping;
+import newapi.NumberFormatter.IGrouping;
+import newapi.NumberFormatter.IRounding;
+import newapi.NumberFormatter.IntegerWidth;
+import newapi.NumberFormatter.Notation;
+import newapi.NumberFormatter.NotationCompact;
+import newapi.NumberFormatter.NotationScientific;
+import newapi.NumberFormatter.NotationSimple;
+import newapi.NumberFormatter.Padding;
+import newapi.NumberFormatter.Rounding;
+import newapi.NumberFormatter.SignDisplay;
+import newapi.impl.RoundingImpl.RoundingImplCurrency;
+import newapi.impl.RoundingImpl.RoundingImplFraction;
+import newapi.impl.RoundingImpl.RoundingImplFractionSignificant;
+import newapi.impl.RoundingImpl.RoundingImplIncrement;
+import newapi.impl.RoundingImpl.RoundingImplInfinity;
+import newapi.impl.RoundingImpl.RoundingImplSignificant;
+
+public final class SkeletonBuilder {
+
+ public static String macrosToSkeleton(MacroProps macros) {
+ // Print out the values in their canonical order.
+ StringBuilder sb = new StringBuilder();
+ if (macros.notation != null) {
+ // sb.append("notation=");
+ notationToSkeleton(macros.notation, sb);
+ sb.append(' ');
+ }
+ if (macros.unit != null) {
+ // sb.append("unit=");
+ unitToSkeleton(macros.unit, sb);
+ sb.append(' ');
+ }
+ if (macros.rounding != null) {
+ // sb.append("rounding=");
+ roundingToSkeleton(macros.rounding, sb);
+ sb.append(' ');
+ }
+ if (macros.grouping != null) {
+ sb.append("grouping=");
+ groupingToSkeleton(macros.grouping, sb);
+ sb.append(' ');
+ }
+ if (macros.padding != null) {
+ sb.append("padding=");
+ paddingToSkeleton(macros.padding, sb);
+ sb.append(' ');
+ }
+ if (macros.integerWidth != null) {
+ sb.append("integer-width=");
+ integerWidthToSkeleton(macros.integerWidth, sb);
+ sb.append(' ');
+ }
+ if (macros.symbols != null) {
+ sb.append("symbols=");
+ symbolsToSkeleton(macros.symbols, sb);
+ sb.append(' ');
+ }
+ if (macros.unitWidth != null) {
+ sb.append("unit-width=");
+ unitWidthToSkeleton(macros.unitWidth, sb);
+ sb.append(' ');
+ }
+ if (macros.sign != null) {
+ sb.append("sign=");
+ signToSkeleton(macros.sign, sb);
+ sb.append(' ');
+ }
+ if (macros.decimal != null) {
+ sb.append("decimal=");
+ decimalToSkeleton(macros.decimal, sb);
+ sb.append(' ');
+ }
+ if (sb.length() > 0) {
+ // Remove the trailing space
+ sb.setLength(sb.length() - 1);
+ }
+ return sb.toString();
+ }
+
+ public static MacroProps skeletonToMacros(String skeleton) {
+ MacroProps macros = new MacroProps();
+ for (int offset = 0; offset < skeleton.length(); ) {
+ char c = skeleton.charAt(offset);
+ switch (c) {
+ case ' ':
+ offset++;
+ break;
+ case 'E':
+ case 'C':
+ case 'I':
+ offset += skeletonToNotation(skeleton, offset, macros);
+ break;
+ case '%':
+ case 'B':
+ case '$':
+ case 'U':
+ offset += skeletonToUnit(skeleton, offset, macros);
+ break;
+ case 'F':
+ case 'S':
+ case 'M':
+ case 'G':
+ case 'Y':
+ offset += skeletonToRounding(skeleton, offset, macros);
+ break;
+ default:
+ if (skeleton.regionMatches(offset, "notation=", 0, 9)) {
+ offset += 9;
+ offset += skeletonToNotation(skeleton, offset, macros);
+ } else if (skeleton.regionMatches(offset, "unit=", 0, 5)) {
+ offset += 5;
+ offset += skeletonToUnit(skeleton, offset, macros);
+ } else if (skeleton.regionMatches(offset, "rounding=", 0, 9)) {
+ offset += 9;
+ offset += skeletonToRounding(skeleton, offset, macros);
+ } else if (skeleton.regionMatches(offset, "grouping=", 0, 9)) {
+ offset += 9;
+ offset += skeletonToGrouping(skeleton, offset, macros);
+ } else if (skeleton.regionMatches(offset, "padding=", 0, 9)) {
+ offset += 8;
+ offset += skeletonToPadding(skeleton, offset, macros);
+ } else if (skeleton.regionMatches(offset, "integer-width=", 0, 9)) {
+ offset += 14;
+ offset += skeletonToIntegerWidth(skeleton, offset, macros);
+ } else if (skeleton.regionMatches(offset, "symbols=", 0, 9)) {
+ offset += 8;
+ offset += skeletonToSymbols(skeleton, offset, macros);
+ } else if (skeleton.regionMatches(offset, "unit-width=", 0, 9)) {
+ offset += 11;
+ offset += skeletonToUnitWidth(skeleton, offset, macros);
+ } else if (skeleton.regionMatches(offset, "sign=", 0, 9)) {
+ offset += 5;
+ offset += skeletonToSign(skeleton, offset, macros);
+ } else if (skeleton.regionMatches(offset, "decimal=", 0, 9)) {
+ offset += 8;
+ offset += skeletonToDecimal(skeleton, offset, macros);
+ } else {
+ throw new IllegalArgumentException(
+ "Unexpected token at offset " + offset + " in skeleton string: " + c);
+ }
+ }
+ }
+ return macros;
+ }
+
+ private static void notationToSkeleton(Notation value, StringBuilder sb) {
+ if (value instanceof NotationScientific) {
+ NotationImpl.NotationScientificImpl notation = (NotationImpl.NotationScientificImpl) value;
+ sb.append('E');
+ if (notation.engineeringInterval != 1) {
+ sb.append(notation.engineeringInterval);
+ }
+ if (notation.exponentSignDisplay == SignDisplay.ALWAYS_SHOWN) {
+ sb.append('+');
+ } else if (notation.exponentSignDisplay == SignDisplay.NEVER_SHOWN) {
+ sb.append('!');
+ } else {
+ assert notation.exponentSignDisplay == SignDisplay.AUTO;
+ }
+ if (notation.minExponentDigits != 1) {
+ for (int i = 0; i < notation.minExponentDigits; i++) {
+ sb.append('0');
+ }
+ }
+ } else if (value instanceof NotationCompact) {
+ NotationImpl.NotationCompactImpl notation = (NotationImpl.NotationCompactImpl) value;
+ if (notation.compactStyle == CompactStyle.SHORT) {
+ sb.append('C');
+ } else {
+ // FIXME: CCC or CCCC instead?
+ sb.append("CC");
+ }
+ } else {
+ assert value instanceof NotationSimple;
+ sb.append('I');
+ }
+ }
+
+ private static int skeletonToNotation(String skeleton, int offset, MacroProps output) {
+ int originalOffset = offset;
+ char c0 = skeleton.charAt(offset++);
+ Notation result = null;
+ if (c0 == 'E') {
+ int engineering = 1;
+ SignDisplay sign = SignDisplay.AUTO;
+ int minExponentDigits = 0;
+ char c = safeCharAt(skeleton, offset++);
+ if (c >= '1' && c <= '9') {
+ engineering = c - '0';
+ c = safeCharAt(skeleton, offset++);
+ }
+ if (c == '+') {
+ sign = SignDisplay.ALWAYS_SHOWN;
+ c = safeCharAt(skeleton, offset++);
+ }
+ if (c == '!') {
+ sign = SignDisplay.NEVER_SHOWN;
+ c = safeCharAt(skeleton, offset++);
+ }
+ while (c == '0') {
+ minExponentDigits++;
+ c = safeCharAt(skeleton, offset++);
+ }
+ minExponentDigits = Math.max(1, minExponentDigits);
+ result = new NotationImpl.NotationScientificImpl(engineering, false, minExponentDigits, sign);
+ } else if (c0 == 'C') {
+ char c = safeCharAt(skeleton, offset++);
+ if (c == 'C') {
+ result = Notation.COMPACT_LONG;
+ } else {
+ result = Notation.COMPACT_SHORT;
+ }
+ } else if (c0 == 'I') {
+ result = Notation.SIMPLE;
+ }
+ output.notation = result;
+ return offset - originalOffset;
+ }
+
+ private static void unitToSkeleton(MeasureUnit value, StringBuilder sb) {
+ if (value.getType().equals("dimensionless")) {
+ if (value.getSubtype().equals("percent")) {
+ sb.append('%');
+ } else if (value.getSubtype().equals("permille")) {
+ sb.append("%%");
+ } else {
+ assert value.getSubtype().equals("base");
+ sb.append('B');
+ }
+ } else if (value.getType().equals("currency")) {
+ sb.append('$');
+ sb.append(value.getSubtype());
+ } else {
+ sb.append('U');
+ sb.append(value.getType());
+ sb.append(':');
+ sb.append(value.getSubtype());
+ }
+ }
+
+ private static int skeletonToUnit(String skeleton, int offset, MacroProps output) {
+ int originalOffset = offset;
+ char c0 = skeleton.charAt(offset++);
+ MeasureUnit result = null;
+ if (c0 == '%') {
+ char c = safeCharAt(skeleton, offset++);
+ if (c == '%') {
+ result = Dimensionless.PERCENT;
+ } else {
+ result = Dimensionless.PERMILLE;
+ }
+ } else if (c0 == 'B') {
+ result = Dimensionless.BASE;
+ } else if (c0 == '$') {
+ String currencyCode = skeleton.substring(offset, offset + 3);
+ offset += 3;
+ result = Currency.getInstance(currencyCode);
+ } else if (c0 == 'U') {
+ StringBuilder sb = new StringBuilder();
+ offset += consumeUntil(skeleton, offset, ':', sb);
+ String type = sb.toString();
+ sb.setLength(0);
+ offset += consumeUntil(skeleton, offset, ' ', sb);
+ String subtype = sb.toString();
+ for (MeasureUnit candidate : MeasureUnit.getAvailable(type)) {
+ if (candidate.getSubtype().equals(subtype)) {
+ result = candidate;
+ break;
+ }
+ }
+ }
+ output.unit = result;
+ return offset - originalOffset;
+ }
+
+ private static void roundingToSkeleton(IRounding value, StringBuilder sb) {
+ if (!(value instanceof Rounding)) {
+ // FIXME: Throw an exception here instead?
+ return;
+ }
+ MathContext mathContext;
+ if (value instanceof RoundingImplFraction) {
+ RoundingImplFraction rounding = (RoundingImplFraction) value;
+ sb.append('F');
+ minMaxToSkeletonHelper(rounding.minFrac, rounding.maxFrac, sb);
+ mathContext = rounding.mathContext;
+ } else if (value instanceof RoundingImplSignificant) {
+ RoundingImplSignificant rounding = (RoundingImplSignificant) value;
+ sb.append('S');
+ minMaxToSkeletonHelper(rounding.minSig, rounding.maxSig, sb);
+ mathContext = rounding.mathContext;
+ } else if (value instanceof RoundingImplFractionSignificant) {
+ RoundingImplFractionSignificant rounding = (RoundingImplFractionSignificant) value;
+ sb.append('F');
+ minMaxToSkeletonHelper(rounding.minFrac, rounding.maxFrac, sb);
+ if (rounding.minSig != -1) {
+ sb.append('>');
+ sb.append(rounding.minSig);
+ } else {
+ sb.append('<');
+ sb.append(rounding.maxSig);
+ }
+ mathContext = rounding.mathContext;
+ } else if (value instanceof RoundingImplIncrement) {
+ RoundingImplIncrement rounding = (RoundingImplIncrement) value;
+ sb.append('M');
+ sb.append(rounding.increment.toString());
+ mathContext = rounding.mathContext;
+ } else if (value instanceof RoundingImplCurrency) {
+ RoundingImplCurrency rounding = (RoundingImplCurrency) value;
+ sb.append('G');
+ sb.append(rounding.usage.name());
+ mathContext = rounding.mc;
+ } else {
+ RoundingImplInfinity rounding = (RoundingImplInfinity) value;
+ sb.append('Y');
+ mathContext = rounding.mathContext;
+ }
+ // RoundingMode
+ RoundingMode roundingMode = mathContext.getRoundingMode();
+ if (roundingMode != RoundingMode.HALF_EVEN) {
+ sb.append(';');
+ sb.append(roundingMode.name());
+ }
+ }
+
+ private static void minMaxToSkeletonHelper(int minFrac, int maxFrac, StringBuilder sb) {
+ if (minFrac == maxFrac) {
+ sb.append(minFrac);
+ } else {
+ boolean showMaxFrac = (maxFrac >= 0 && maxFrac < Integer.MAX_VALUE);
+ if (minFrac > 0 || !showMaxFrac) {
+ sb.append(minFrac);
+ }
+ sb.append('-');
+ if (showMaxFrac) {
+ sb.append(maxFrac);
+ }
+ }
+ }
+
+ private static int skeletonToRounding(String skeleton, int offset, MacroProps output) {
+ int originalOffset = offset;
+ char c0 = skeleton.charAt(offset++);
+ Rounding result = null;
+ if (c0 == 'F') {
+ int[] minMax = new int[2];
+ offset += skeletonToMinMaxHelper(skeleton, offset, minMax);
+ FractionRounding temp = RoundingImplFraction.getInstance(minMax[0], minMax[1]);
+ char c1 = skeleton.charAt(offset++);
+ if (c1 == '<') {
+ char c2 = skeleton.charAt(offset++);
+ result = temp.withMaxFigures(c2 - '0');
+ } else if (c1 == '>') {
+ char c2 = skeleton.charAt(offset++);
+ result = temp.withMinFigures(c2 - '0');
+ } else {
+ result = temp;
+ }
+ } else if (c0 == 'S') {
+ int[] minMax = new int[2];
+ offset += skeletonToMinMaxHelper(skeleton, offset, minMax);
+ result = RoundingImplSignificant.getInstance(minMax[0], minMax[1]);
+ } else if (c0 == 'M') {
+ StringBuilder sb = new StringBuilder();
+ offset += consumeUntil(skeleton, offset, ' ', sb);
+ BigDecimal increment = new BigDecimal(sb.toString());
+ result = RoundingImplIncrement.getInstance(increment);
+ } else if (c0 == 'G') {
+ StringBuilder sb = new StringBuilder();
+ offset += consumeUntil(skeleton, offset, ' ', sb);
+ CurrencyUsage usage = Enum.valueOf(CurrencyUsage.class, sb.toString());
+ result = Rounding.currency(usage);
+ } else if (c0 == 'Y') {
+ result = Rounding.NONE;
+ }
+ output.rounding = result;
+ return offset - originalOffset;
+ }
+
+ private static int skeletonToMinMaxHelper(String skeleton, int offset, int[] output) {
+ int originalOffset = offset;
+ char c0 = safeCharAt(skeleton, offset++);
+ char c1 = safeCharAt(skeleton, offset++);
+ // TODO: This algorithm breaks if the number is more than 1 char wide.
+ if (c1 == '-') {
+ output[0] = c0 - '0';
+ char c2 = safeCharAt(skeleton, offset++);
+ if (c2 == ' ') {
+ output[1] = Integer.MAX_VALUE;
+ } else {
+ output[1] = c2 - '0';
+ }
+ } else if ('0' <= c1 && c1 <= '9') {
+ output[0] = 0;
+ output[1] = c1 - '0';
+ } else {
+ offset--;
+ output[0] = c0 - '0';
+ output[1] = c0 - '0';
+ }
+ return offset - originalOffset;
+ }
+
+ private static void groupingToSkeleton(IGrouping value, StringBuilder sb) {
+ if (!(value instanceof Grouping)) {
+ // FIXME: Throw an exception here instead?
+ sb.append("custom");
+ return;
+ }
+ if (value.equals(Grouping.DEFAULT)) {
+ sb.append("DEFAULT");
+ } else if (value.equals(Grouping.DEFAULT_MIN_2_DIGITS)) {
+ sb.append("DEFAULT_MIN_2_DIGITS");
+ } else if (value.equals(Grouping.NONE)) {
+ sb.append("NONE");
+ } else {
+ GroupingImpl grouping = (GroupingImpl) value;
+ if (grouping.grouping2 == -1 || grouping.grouping2 == 0) {
+ sb.append("NONE");
+ } else {
+ sb.append(grouping.grouping1);
+ if (grouping.grouping2 != grouping.grouping1) {
+ sb.append(',');
+ sb.append(grouping.grouping2);
+ }
+ if (grouping.min2) {
+ sb.append('&');
+ }
+ }
+ }
+ }
+
+ private static int skeletonToGrouping(String skeleton, int offset, MacroProps output) {
+ int originalOffset = offset;
+ char c0 = skeleton.charAt(offset++);
+ Grouping result = null;
+ if ('0' <= c0 && c0 <= '9') {
+ char c1 = safeCharAt(skeleton, offset++);
+ if (c1 == ',') {
+ char c2 = safeCharAt(skeleton, offset++);
+ char c3 = safeCharAt(skeleton, offset++);
+ result = GroupingImpl.getInstance((short) (c0 - '0'), (short) (c2 - '0'), c3 == '&');
+ } else {
+ result = GroupingImpl.getInstance((short) (c0 - '0'), (short) (c0 - '0'), c1 == '&');
+ }
+ } else {
+ StringBuilder sb = new StringBuilder();
+ offset += consumeUntil(skeleton, --offset, ' ', sb);
+ String name = sb.toString();
+ if (name.equals("DEFAULT")) {
+ result = Grouping.DEFAULT;
+ } else if (name.equals("DEFAULT_MIN_2_DIGITS")) {
+ result = Grouping.DEFAULT_MIN_2_DIGITS;
+ } else if (name.equals("NONE")) {
+ result = Grouping.NONE;
+ }
+ }
+ output.grouping = result;
+ return offset - originalOffset;
+ }
+
+ private static void paddingToSkeleton(Padding value, StringBuilder sb) {
+ PaddingImpl padding = (PaddingImpl) value;
+ if (padding == Padding.NONE) {
+ sb.append("NONE");
+ return;
+ }
+ sb.append("CP:");
+ // TODO: Handle padding strings that contain ':'
+ sb.append(padding.paddingString);
+ sb.append(':');
+ sb.append(padding.targetWidth);
+ sb.append(':');
+ sb.append(padding.position.name());
+ }
+
+ private static void integerWidthToSkeleton(IntegerWidth value, StringBuilder sb) {
+ IntegerWidthImpl impl = (IntegerWidthImpl) value;
+ sb.append(impl.minInt);
+ if (impl.maxInt != impl.minInt) {
+ sb.append('-');
+ if (impl.maxInt < Integer.MAX_VALUE) {
+ sb.append(impl.maxInt);
+ }
+ }
+ }
+
+ private static void symbolsToSkeleton(Object value, StringBuilder sb) {
+ if (value instanceof DecimalFormatSymbols) {
+ // TODO: Check to see if any of the symbols are not default?
+ sb.append("loc:");
+ sb.append(((DecimalFormatSymbols) value).getULocale());
+ } else {
+ sb.append("ns:");
+ sb.append(((NumberingSystem) value).getName());
+ }
+ }
+
+ private static void unitWidthToSkeleton(FormatWidth value, StringBuilder sb) {
+ sb.append(value.name());
+ }
+
+ private static void signToSkeleton(SignDisplay value, StringBuilder sb) {
+ sb.append(value.name());
+ }
+
+ private static void decimalToSkeleton(DecimalMarkDisplay value, StringBuilder sb) {
+ sb.append(value.name());
+ }
+
+ private static char safeCharAt(String str, int offset) {
+ if (offset < str.length()) {
+ return str.charAt(offset);
+ } else {
+ return ' ';
+ }
+ }
+
+ private static int consumeUntil(String skeleton, int offset, char brk, StringBuilder sb) {
+ int originalOffset = offset;
+ char c = safeCharAt(skeleton, offset++);
+ while (c != brk) {
+ sb.append(c);
+ c = safeCharAt(skeleton, offset++);
+ }
+ return offset - originalOffset;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.Map;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.LdmlPatternInfo;
+import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.CompactDecimalFormat.CompactType;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.text.NumberingSystem;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.Dimensionless;
+import com.ibm.icu.util.ULocale;
+
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.Grouping;
+import newapi.NumberFormatter.NotationCompact;
+import newapi.NumberFormatter.NotationScientific;
+import newapi.NumberFormatter.Rounding;
+import newapi.NumberFormatter.SignDisplay;
+
+public class Worker1 {
+
+ public static Worker1 fromMacros(MacroProps macros) {
+ return new Worker1(make(macros, true));
+ }
+
+ public static MicroProps applyStatic(
+ MacroProps macros, FormatQuantity inValue, NumberStringBuilder outString) {
+ MicroProps micros = make(macros, false).withQuantity(inValue);
+ applyStatic(micros, inValue, outString);
+ return micros;
+ }
+
+ private static final Currency DEFAULT_CURRENCY = Currency.getInstance("XXX");
+
+ final QuantityChain microsGenerator;
+
+ private Worker1(QuantityChain microsGenerator) {
+ this.microsGenerator = microsGenerator;
+ }
+
+ public MicroProps apply(FormatQuantity inValue, NumberStringBuilder outString) {
+ MicroProps micros = microsGenerator.withQuantity(inValue);
+ applyStatic(micros, inValue, outString);
+ return micros;
+ }
+
+ //////////
+
+ private static QuantityChain make(MacroProps input, boolean build) {
+
+ String innerPattern = null;
+ Map<StandardPlural, Modifier> outerMods = null;
+ Rounding defaultRounding = Rounding.NONE;
+ Currency currency = DEFAULT_CURRENCY;
+ FormatWidth unitWidth = null;
+ boolean perMille = false;
+ PluralRules rules = input.rules;
+
+ MicroProps micros = new MicroProps();
+ QuantityChain chain = micros;
+
+ // Copy over the simple settings
+ micros.sign = input.sign == null ? SignDisplay.AUTO : input.sign;
+ micros.decimal = input.decimal == null ? DecimalMarkDisplay.AUTO : input.decimal;
+ micros.multiplier = 0;
+ micros.integerWidth =
+ input.integerWidth == null
+ ? IntegerWidthImpl.DEFAULT
+ : (IntegerWidthImpl) input.integerWidth;
+
+ if (input.unit == null || input.unit == Dimensionless.BASE) {
+ // No units; default format
+ innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE);
+ } else if (input.unit == Dimensionless.PERCENT) {
+ // Percent
+ innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.PERCENTSTYLE);
+ micros.multiplier += 2;
+ } else if (input.unit == Dimensionless.PERMILLE) {
+ // Permille
+ innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.PERCENTSTYLE);
+ micros.multiplier += 3;
+ perMille = true;
+ } else if (input.unit instanceof Currency && input.unitWidth != FormatWidth.WIDE) {
+ // Narrow, short, or ISO currency.
+ // TODO: Accounting style?
+ innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.CURRENCYSTYLE);
+ defaultRounding = Rounding.currency(CurrencyUsage.STANDARD);
+ currency = (Currency) input.unit;
+ micros.useCurrency = true;
+ unitWidth = (input.unitWidth == null) ? FormatWidth.NARROW : input.unitWidth;
+ } else if (input.unit instanceof Currency) {
+ // Currency long name
+ innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE);
+ outerMods = DataUtils.getCurrencyLongNameModifiers(input.loc, (Currency) input.unit);
+ defaultRounding = Rounding.currency(CurrencyUsage.STANDARD);
+ currency = (Currency) input.unit;
+ micros.useCurrency = true;
+ unitWidth = input.unitWidth = FormatWidth.WIDE;
+ } else {
+ // MeasureUnit
+ innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE);
+ unitWidth = (input.unitWidth == null) ? FormatWidth.SHORT : input.unitWidth;
+ outerMods = DataUtils.getMeasureUnitModifiers(input.loc, input.unit, unitWidth);
+ }
+
+ // Parse the pattern, which is used for grouping and affixes only.
+ PatternParseResult patternInfo = LdmlPatternInfo.parse(innerPattern);
+
+ // Symbols
+ if (input.symbols == null) {
+ micros.symbols = DecimalFormatSymbols.getInstance(input.loc);
+ } else if (input.symbols instanceof DecimalFormatSymbols) {
+ micros.symbols = (DecimalFormatSymbols) input.symbols;
+ } else if (input.symbols instanceof NumberingSystem) {
+ // TODO: Do this more efficiently. Will require modifying DecimalFormatSymbols.
+ NumberingSystem ns = (NumberingSystem) input.symbols;
+ ULocale temp = input.loc.setKeywordValue("numbers", ns.getName());
+ micros.symbols = DecimalFormatSymbols.getInstance(temp);
+ } else {
+ throw new AssertionError();
+ }
+
+ // TODO: Normalize the currency (accept symbols from DecimalFormatSymbols)?
+ // currency = CustomSymbolCurrency.resolve(currency, input.loc, micros.symbols);
+
+ // Multiplier (compatibility mode value).
+ // An int magnitude multiplier is used when not in compatibility mode to
+ // reduce object creations.
+ if (input.multiplier != null) {
+ // TODO: Make sure this is thread safe.
+ chain = input.multiplier.chain(chain);
+ }
+
+ // Rounding strategy
+ if (input.rounding != null) {
+ micros.rounding = RoundingImpl.normalizeType(input.rounding, currency);
+ } else if (input.notation instanceof NotationCompact) {
+ micros.rounding = RoundingImpl.RoundingImplFractionSignificant.COMPACT_STRATEGY;
+ } else {
+ micros.rounding = RoundingImpl.normalizeType(defaultRounding, currency);
+ }
+
+ // Grouping strategy
+ if (input.grouping != null) {
+ micros.grouping = GroupingImpl.normalizeType(input.grouping, patternInfo);
+ } else if (input.notation instanceof NotationCompact) {
+ // Compact notation uses minGrouping by default since ICU 59
+ micros.grouping = GroupingImpl.normalizeType(Grouping.DEFAULT_MIN_2_DIGITS, patternInfo);
+ } else {
+ micros.grouping = GroupingImpl.normalizeType(Grouping.DEFAULT, patternInfo);
+ }
+
+ // Inner modifier (scientific notation)
+ if (input.notation instanceof NotationScientific) {
+ assert input.notation instanceof NotationImpl.NotationScientificImpl;
+ chain =
+ ScientificImpl.getInstance(
+ (NotationImpl.NotationScientificImpl) input.notation, micros.symbols, build)
+ .chain(chain);
+ } else {
+ // No inner modifier required
+ micros.modInner = ConstantAffixModifier.EMPTY;
+ }
+
+ // Middle modifier (patterns, positive/negative, currency symbols, percent)
+ MurkyModifier murkyMod = new MurkyModifier(false);
+ murkyMod.setPatternInfo((input.affixProvider != null) ? input.affixProvider : patternInfo);
+ murkyMod.setPatternAttributes(micros.sign, perMille);
+ if (murkyMod.needsPlurals()) {
+ if (rules == null) {
+ // Lazily create PluralRules
+ rules = PluralRules.forLocale(input.loc);
+ }
+ murkyMod.setSymbols(micros.symbols, currency, unitWidth, rules);
+ } else {
+ murkyMod.setSymbols(micros.symbols, currency, unitWidth, null);
+ }
+ if (build) {
+ chain = murkyMod.createImmutable().chain(chain);
+ } else {
+ chain = murkyMod.chain(chain);
+ }
+
+ // Outer modifier (CLDR units and currency long names)
+ if (outerMods != null) {
+ if (rules == null) {
+ // Lazily create PluralRules
+ rules = PluralRules.forLocale(input.loc);
+ }
+ chain = new QuantityDependentModOuter(outerMods, rules).chain(chain);
+ } else {
+ // No outer modifier required
+ micros.modOuter = ConstantAffixModifier.EMPTY;
+ }
+
+ // Padding strategy
+ if (input.padding != null) {
+ micros.padding = (PaddingImpl) input.padding;
+ } else {
+ micros.padding = PaddingImpl.NONE;
+ }
+
+ // Compact notation
+ // NOTE: Compact notation can (but might not) override the middle modifier and rounding.
+ // It therefore needs to go at the end of the chain.
+ if (input.notation instanceof NotationCompact) {
+ assert input.notation instanceof NotationImpl.NotationCompactImpl;
+ if (rules == null) {
+ // Lazily create PluralRules
+ rules = PluralRules.forLocale(input.loc);
+ }
+ CompactStyle compactStyle = ((NotationImpl.NotationCompactImpl) input.notation).compactStyle;
+ CompactImpl worker;
+ if (compactStyle == null) {
+ // Use compact custom data
+ worker =
+ CompactImpl.getInstance(
+ ((NotationImpl.NotationCompactImpl) input.notation).compactCustomData, rules);
+ } else {
+ CompactType compactType =
+ (input.unit instanceof Currency) ? CompactType.CURRENCY : CompactType.DECIMAL;
+ worker = CompactImpl.getInstance(input.loc, compactType, compactStyle, rules);
+ }
+ if (build) {
+ worker.precomputeAllModifiers(murkyMod);
+ }
+ chain = worker.chain(chain);
+ }
+
+ if (build) {
+ micros.enableCloneInChain();
+ }
+
+ return chain;
+ }
+
+ //////////
+
+ private static int applyStatic(
+ MicroProps micros, FormatQuantity inValue, NumberStringBuilder outString) {
+ inValue.adjustMagnitude(micros.multiplier);
+ micros.rounding.apply(inValue);
+ inValue.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt);
+ int length = PositiveDecimalImpl.apply(micros, inValue, outString);
+ // NOTE: When range formatting is added, these modifiers can bubble up.
+ // For now, apply them all here at once.
+ length += micros.padding.applyModsAndMaybePad(micros, outString, 0, length);
+ return length;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.number;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import com.ibm.icu.impl.number.LdmlPatternInfo;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.ULocale;
+
+import newapi.NumberFormatter.SignDisplay;
+import newapi.impl.MurkyModifier;
+
+public class MurkyModifierTest {
+
+ @Test
+ public void basic() {
+ MurkyModifier murky = new MurkyModifier(false);
+ murky.setPatternInfo(LdmlPatternInfo.parse("a0b"));
+ murky.setPatternAttributes(SignDisplay.AUTO, false);
+ murky.setSymbols(
+ DecimalFormatSymbols.getInstance(ULocale.ENGLISH),
+ Currency.getInstance("USD"),
+ FormatWidth.SHORT,
+ null);
+ murky.setNumberProperties(false, null);
+ assertEquals("a", murky.getPrefix());
+ assertEquals("b", murky.getSuffix());
+ murky.setPatternAttributes(SignDisplay.ALWAYS_SHOWN, false);
+ assertEquals("+a", murky.getPrefix());
+ assertEquals("b", murky.getSuffix());
+ murky.setNumberProperties(true, null);
+ assertEquals("-a", murky.getPrefix());
+ assertEquals("b", murky.getSuffix());
+ murky.setPatternAttributes(SignDisplay.NEVER_SHOWN, false);
+ assertEquals("a", murky.getPrefix());
+ assertEquals("b", murky.getSuffix());
+
+ murky.setPatternInfo(LdmlPatternInfo.parse("a0b;c-0d"));
+ murky.setPatternAttributes(SignDisplay.AUTO, false);
+ murky.setNumberProperties(false, null);
+ assertEquals("a", murky.getPrefix());
+ assertEquals("b", murky.getSuffix());
+ murky.setPatternAttributes(SignDisplay.ALWAYS_SHOWN, false);
+ assertEquals("c+", murky.getPrefix());
+ assertEquals("d", murky.getSuffix());
+ murky.setNumberProperties(true, null);
+ assertEquals("c-", murky.getPrefix());
+ assertEquals("d", murky.getSuffix());
+ murky.setPatternAttributes(SignDisplay.NEVER_SHOWN, false);
+ assertEquals("c-", murky.getPrefix()); // TODO: What should this behavior be?
+ assertEquals("d", murky.getSuffix());
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.number;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Locale;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.text.NumberingSystem;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.CurrencyAmount;
+import com.ibm.icu.util.Dimensionless;
+import com.ibm.icu.util.Measure;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+import newapi.NumberFormatter;
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.Grouping;
+import newapi.NumberFormatter.IRounding;
+import newapi.NumberFormatter.IntegerWidth;
+import newapi.NumberFormatter.LocalizedNumberFormatter;
+import newapi.NumberFormatter.Notation;
+import newapi.NumberFormatter.NumberFormatterResult;
+import newapi.NumberFormatter.Padding;
+import newapi.NumberFormatter.Rounding;
+import newapi.NumberFormatter.SignDisplay;
+import newapi.NumberFormatter.UnlocalizedNumberFormatter;
+import newapi.impl.NumberFormatterImpl;
+
+public class NumberFormatterTest {
+
+ private static final Currency USD = Currency.getInstance("USD");
+ private static final Currency GBP = Currency.getInstance("GBP");
+ private static final Currency CZK = Currency.getInstance("CZK");
+
+ @Test
+ public void notationSimple() {
+ assertFormatDescending(
+ "Basic",
+ "",
+ NumberFormatter.with(),
+ ULocale.ENGLISH,
+ "87,650",
+ "8,765",
+ "876.5",
+ "87.65",
+ "8.765",
+ "0.8765",
+ "0.08765",
+ "0.008765",
+ "0");
+
+ assertFormatSingle(
+ "Basic with Negative Sign",
+ "",
+ NumberFormatter.with(),
+ ULocale.ENGLISH,
+ -9876543.21,
+ "-9,876,543.21");
+ }
+
+ @Test
+ public void notationScientific() {
+ assertFormatDescending(
+ "Scientific",
+ "E",
+ NumberFormatter.with().notation(Notation.SCIENTIFIC),
+ ULocale.ENGLISH,
+ "8.765E4",
+ "8.765E3",
+ "8.765E2",
+ "8.765E1",
+ "8.765E0",
+ "8.765E-1",
+ "8.765E-2",
+ "8.765E-3",
+ "0E0");
+
+ assertFormatDescending(
+ "Engineering",
+ "E3",
+ NumberFormatter.with().notation(Notation.ENGINEERING),
+ ULocale.ENGLISH,
+ "87.65E3",
+ "8.765E3",
+ "876.5E0",
+ "87.65E0",
+ "8.765E0",
+ "876.5E-3",
+ "87.65E-3",
+ "8.765E-3",
+ "0E0");
+
+ assertFormatDescending(
+ "Scientific sign always shown",
+ "E+",
+ NumberFormatter.with()
+ .notation(Notation.SCIENTIFIC.withExponentSignDisplay(SignDisplay.ALWAYS_SHOWN)),
+ ULocale.ENGLISH,
+ "8.765E+4",
+ "8.765E+3",
+ "8.765E+2",
+ "8.765E+1",
+ "8.765E+0",
+ "8.765E-1",
+ "8.765E-2",
+ "8.765E-3",
+ "0E+0");
+
+ assertFormatDescending(
+ "Scientific min exponent digits",
+ "E00",
+ NumberFormatter.with().notation(Notation.SCIENTIFIC.withMinExponentDigits(2)),
+ ULocale.ENGLISH,
+ "8.765E04",
+ "8.765E03",
+ "8.765E02",
+ "8.765E01",
+ "8.765E00",
+ "8.765E-01",
+ "8.765E-02",
+ "8.765E-03",
+ "0E00");
+
+ assertFormatSingle(
+ "Scientific Negative",
+ "E",
+ NumberFormatter.with().notation(Notation.SCIENTIFIC),
+ ULocale.ENGLISH,
+ -1000000,
+ "-1E6");
+ }
+
+ @Test
+ public void notationCompact() {
+ assertFormatDescending(
+ "Compact Short",
+ "C",
+ NumberFormatter.with().notation(Notation.COMPACT_SHORT),
+ ULocale.ENGLISH,
+ "88K",
+ "8.8K",
+ "876",
+ "88",
+ "8.8",
+ "0.88",
+ "0.088",
+ "0.0088",
+ "0");
+
+ assertFormatDescending(
+ "Compact Long",
+ "CC",
+ NumberFormatter.with().notation(Notation.COMPACT_LONG),
+ ULocale.ENGLISH,
+ "88 thousand",
+ "8.8 thousand",
+ "876",
+ "88",
+ "8.8",
+ "0.88",
+ "0.088",
+ "0.0088",
+ "0");
+
+ assertFormatDescending(
+ "Compact Short Currency",
+ "C $USD",
+ NumberFormatter.with().notation(Notation.COMPACT_SHORT).unit(USD),
+ ULocale.ENGLISH,
+ "$88K",
+ "$8.8K",
+ "$876",
+ "$88",
+ "$8.8",
+ "$0.88",
+ "$0.088",
+ "$0.0088",
+ "$0");
+
+ // Note: Most locales don't have compact long currency, so this currently falls back to short.
+ assertFormatDescending(
+ "Compact Long Currency",
+ "CC $USD",
+ NumberFormatter.with().notation(Notation.COMPACT_LONG).unit(USD),
+ ULocale.ENGLISH,
+ "$88K",
+ "$8.8K",
+ "$876",
+ "$88",
+ "$8.8",
+ "$0.88",
+ "$0.088",
+ "$0.0088",
+ "$0");
+
+ assertFormatSingle(
+ "Compact Plural One",
+ "CC",
+ NumberFormatter.with().notation(Notation.COMPACT_LONG),
+ ULocale.forLanguageTag("es"),
+ 1000000,
+ "1 millón");
+
+ assertFormatSingle(
+ "Compact Plural Other",
+ "CC",
+ NumberFormatter.with().notation(Notation.COMPACT_LONG),
+ ULocale.forLanguageTag("es"),
+ 2000000,
+ "2 millones");
+
+ assertFormatSingle(
+ "Compact with Negative Sign",
+ "C",
+ NumberFormatter.with().notation(Notation.COMPACT_SHORT),
+ ULocale.ENGLISH,
+ -9876543.21,
+ "-9.9M");
+ }
+
+ @Test
+ public void unitMeasure() {
+ assertFormatDescending(
+ "Meters Short",
+ "Ulength:meter",
+ NumberFormatter.with().unit(MeasureUnit.METER),
+ ULocale.ENGLISH,
+ "87,650 m",
+ "8,765 m",
+ "876.5 m",
+ "87.65 m",
+ "8.765 m",
+ "0.8765 m",
+ "0.08765 m",
+ "0.008765 m",
+ "0 m");
+
+ assertFormatDescending(
+ "Meters Long",
+ "Ulength:meter unit-width=WIDE",
+ NumberFormatter.with().unit(MeasureUnit.METER).unitWidth(FormatWidth.WIDE),
+ ULocale.ENGLISH,
+ "87,650 meters",
+ "8,765 meters",
+ "876.5 meters",
+ "87.65 meters",
+ "8.765 meters",
+ "0.8765 meters",
+ "0.08765 meters",
+ "0.008765 meters",
+ "0 meters");
+
+ assertFormatDescending(
+ "Compact Meters Long",
+ "CC Ulength:meter unit-width=WIDE",
+ NumberFormatter.with()
+ .notation(Notation.COMPACT_LONG)
+ .unit(MeasureUnit.METER)
+ .unitWidth(FormatWidth.WIDE),
+ ULocale.ENGLISH,
+ "88 thousand meters",
+ "8.8 thousand meters",
+ "876 meters",
+ "88 meters",
+ "8.8 meters",
+ "0.88 meters",
+ "0.088 meters",
+ "0.0088 meters",
+ "0 meters");
+
+ assertFormatSingleMeasure(
+ "Meters with Measure Input",
+ "unit-width=WIDE",
+ NumberFormatter.with().unitWidth(FormatWidth.WIDE),
+ ULocale.ENGLISH,
+ new Measure(5.43, MeasureUnit.METER),
+ "5.43 meters");
+
+ assertFormatSingle(
+ "Meters with Negative Sign",
+ "Ulength:meter",
+ NumberFormatter.with().unit(MeasureUnit.METER),
+ ULocale.ENGLISH,
+ -9876543.21,
+ "-9,876,543.21 m");
+ }
+
+ @Test
+ public void unitCurrency() {
+ assertFormatDescending(
+ "Currency",
+ "$GBP",
+ NumberFormatter.with().unit(GBP),
+ ULocale.ENGLISH,
+ "£87,650.00",
+ "£8,765.00",
+ "£876.50",
+ "£87.65",
+ "£8.76",
+ "£0.88",
+ "£0.09",
+ "£0.01",
+ "£0.00");
+
+ assertFormatDescending(
+ "Currency ISO",
+ "$GBP unit-width=SHORT",
+ NumberFormatter.with().unit(GBP).unitWidth(FormatWidth.SHORT),
+ ULocale.ENGLISH,
+ "GBP 87,650.00",
+ "GBP 8,765.00",
+ "GBP 876.50",
+ "GBP 87.65",
+ "GBP 8.76",
+ "GBP 0.88",
+ "GBP 0.09",
+ "GBP 0.01",
+ "GBP 0.00");
+
+ assertFormatDescending(
+ "Currency Long Name",
+ "$GBP unit-width=WIDE",
+ NumberFormatter.with().unit(GBP).unitWidth(FormatWidth.WIDE),
+ ULocale.ENGLISH,
+ "87,650.00 British pounds",
+ "8,765.00 British pounds",
+ "876.50 British pounds",
+ "87.65 British pounds",
+ "8.76 British pounds",
+ "0.88 British pounds",
+ "0.09 British pounds",
+ "0.01 British pounds",
+ "0.00 British pounds");
+
+ assertFormatSingleMeasure(
+ "Currency with CurrencyAmount Input",
+ "",
+ NumberFormatter.with(),
+ ULocale.ENGLISH,
+ new CurrencyAmount(5.43, GBP),
+ "£5.43");
+
+ assertFormatSingle(
+ "Currency Long Name from Pattern Syntax",
+ "$GBP F0 grouping=NONE integer-width=1- symbols=loc:en_US sign=AUTO decimal=AUTO",
+ NumberFormatterImpl.fromPattern("0 ¤¤¤", DecimalFormatSymbols.getInstance()).unit(GBP),
+ ULocale.ENGLISH,
+ 1234567.89,
+ "1234568 British pounds");
+
+ assertFormatSingle(
+ "Currency with Negative Sign",
+ "$GBP",
+ NumberFormatter.with().unit(GBP),
+ ULocale.ENGLISH,
+ -9876543.21,
+ "-£9,876,543.21");
+ }
+
+ @Test
+ public void unitPercent() {
+ assertFormatDescending(
+ "Percent",
+ "%",
+ NumberFormatter.with().unit(Dimensionless.PERCENT),
+ ULocale.ENGLISH,
+ "8,765,000%",
+ "876,500%",
+ "87,650%",
+ "8,765%",
+ "876.5%",
+ "87.65%",
+ "8.765%",
+ "0.8765%",
+ "0%");
+
+ assertFormatDescending(
+ "Permille",
+ "%%",
+ NumberFormatter.with().unit(Dimensionless.PERMILLE),
+ ULocale.ENGLISH,
+ "87,650,000‰",
+ "8,765,000‰",
+ "876,500‰",
+ "87,650‰",
+ "8,765‰",
+ "876.5‰",
+ "87.65‰",
+ "8.765‰",
+ "0‰");
+
+ assertFormatSingle(
+ "Percent with Negative Sign",
+ "%",
+ NumberFormatter.with().unit(Dimensionless.PERCENT),
+ ULocale.ENGLISH,
+ -0.987654321,
+ "-98.7654321%");
+ }
+
+ @Test
+ public void roundingFraction() {
+ assertFormatDescending(
+ "Integer",
+ "F0",
+ NumberFormatter.with().rounding(Rounding.INTEGER),
+ ULocale.ENGLISH,
+ "87,650",
+ "8,765",
+ "876",
+ "88",
+ "9",
+ "1",
+ "0",
+ "0",
+ "0");
+
+ assertFormatDescending(
+ "Fixed Fraction",
+ "F3",
+ NumberFormatter.with().rounding(Rounding.fixedFraction(3)),
+ ULocale.ENGLISH,
+ "87,650.000",
+ "8,765.000",
+ "876.500",
+ "87.650",
+ "8.765",
+ "0.876",
+ "0.088",
+ "0.009",
+ "0.000");
+
+ assertFormatDescending(
+ "Min Fraction",
+ "F1-",
+ NumberFormatter.with().rounding(Rounding.minFraction(1)),
+ ULocale.ENGLISH,
+ "87,650.0",
+ "8,765.0",
+ "876.5",
+ "87.65",
+ "8.765",
+ "0.8765",
+ "0.08765",
+ "0.008765",
+ "0.0");
+
+ assertFormatDescending(
+ "Max Fraction",
+ "F-1",
+ NumberFormatter.with().rounding(Rounding.maxFraction(1)),
+ ULocale.ENGLISH,
+ "87,650",
+ "8,765",
+ "876.5",
+ "87.6",
+ "8.8",
+ "0.9",
+ "0.1",
+ "0",
+ "0");
+
+ assertFormatDescending(
+ "Min/Max Fraction",
+ "F1-3",
+ NumberFormatter.with().rounding(Rounding.minMaxFraction(1, 3)),
+ ULocale.ENGLISH,
+ "87,650.0",
+ "8,765.0",
+ "876.5",
+ "87.65",
+ "8.765",
+ "0.876",
+ "0.088",
+ "0.009",
+ "0.0");
+ }
+
+ @Test
+ public void roundingFigures() {
+ assertFormatSingle(
+ "Fixed Significant",
+ "S3",
+ NumberFormatter.with().rounding(Rounding.fixedFigures(3)),
+ ULocale.ENGLISH,
+ -98,
+ "-98.0");
+
+ assertFormatSingle(
+ "Fixed Significant Rounding",
+ "S3",
+ NumberFormatter.with().rounding(Rounding.fixedFigures(3)),
+ ULocale.ENGLISH,
+ -98.7654321,
+ "-98.8");
+
+ assertFormatSingle(
+ "Fixed Significant Zero",
+ "S3",
+ NumberFormatter.with().rounding(Rounding.fixedFigures(3)),
+ ULocale.ENGLISH,
+ 0,
+ "0.00");
+
+ assertFormatSingle(
+ "Min Significant",
+ "S2-",
+ NumberFormatter.with().rounding(Rounding.minFigures(2)),
+ ULocale.ENGLISH,
+ -9,
+ "-9.0");
+
+ assertFormatSingle(
+ "Max Significant",
+ "S-4",
+ NumberFormatter.with().rounding(Rounding.maxFigures(4)),
+ ULocale.ENGLISH,
+ 98.7654321,
+ "98.77");
+
+ assertFormatSingle(
+ "Min/Max Significant",
+ "S3-4",
+ NumberFormatter.with().rounding(Rounding.minMaxFigures(3, 4)),
+ ULocale.ENGLISH,
+ 9.99999,
+ "10.0");
+ }
+
+ @Test
+ public void roundingFractionFigures() {
+ assertFormatDescending(
+ "Basic Significant", // for comparison
+ "S-2",
+ NumberFormatter.with().rounding(Rounding.maxFigures(2)),
+ ULocale.ENGLISH,
+ "88,000",
+ "8,800",
+ "880",
+ "88",
+ "8.8",
+ "0.88",
+ "0.088",
+ "0.0088",
+ "0");
+
+ assertFormatDescending(
+ "FracSig minMaxFrac minSig",
+ "F1-2>3",
+ NumberFormatter.with().rounding(Rounding.minMaxFraction(1, 2).withMinFigures(3)),
+ ULocale.ENGLISH,
+ "87,650.0",
+ "8,765.0",
+ "876.5",
+ "87.65",
+ "8.76",
+ "0.876", // minSig beats maxFrac
+ "0.0876", // minSig beats maxFrac
+ "0.00876", // minSig beats maxFrac
+ "0.0");
+
+ assertFormatDescending(
+ "FracSig minMaxFrac maxSig A",
+ "F1-3<2",
+ NumberFormatter.with().rounding(Rounding.minMaxFraction(1, 3).withMaxFigures(2)),
+ ULocale.ENGLISH,
+ "88,000.0", // maxSig beats maxFrac
+ "8,800.0", // maxSig beats maxFrac
+ "880.0", // maxSig beats maxFrac
+ "88.0", // maxSig beats maxFrac
+ "8.8", // maxSig beats maxFrac
+ "0.88", // maxSig beats maxFrac
+ "0.088",
+ "0.009",
+ "0.0");
+
+ assertFormatDescending(
+ "FracSig minMaxFrac maxSig B",
+ "F2<2",
+ NumberFormatter.with().rounding(Rounding.fixedFraction(2).withMaxFigures(2)),
+ ULocale.ENGLISH,
+ "88,000.00", // maxSig beats maxFrac
+ "8,800.00", // maxSig beats maxFrac
+ "880.00", // maxSig beats maxFrac
+ "88.00", // maxSig beats maxFrac
+ "8.80", // maxSig beats maxFrac
+ "0.88",
+ "0.09",
+ "0.01",
+ "0.00");
+ }
+
+ @Test
+ public void roundingOther() {
+ assertFormatDescending(
+ "Rounding None",
+ "Y",
+ NumberFormatter.with().rounding(Rounding.NONE),
+ ULocale.ENGLISH,
+ "87,650",
+ "8,765",
+ "876.5",
+ "87.65",
+ "8.765",
+ "0.8765",
+ "0.08765",
+ "0.008765",
+ "0");
+
+ assertFormatDescending(
+ "Increment",
+ "M0.5",
+ NumberFormatter.with().rounding(Rounding.increment(BigDecimal.valueOf(0.5))),
+ ULocale.ENGLISH,
+ "87,650.0",
+ "8,765.0",
+ "876.5",
+ "87.5",
+ "9.0",
+ "1.0",
+ "0.0",
+ "0.0",
+ "0.0");
+
+ assertFormatDescending(
+ "Currency Standard",
+ "$CZK GSTANDARD",
+ NumberFormatter.with().rounding(Rounding.currency(CurrencyUsage.STANDARD)).unit(CZK),
+ ULocale.ENGLISH,
+ "CZK 87,650.00",
+ "CZK 8,765.00",
+ "CZK 876.50",
+ "CZK 87.65",
+ "CZK 8.76",
+ "CZK 0.88",
+ "CZK 0.09",
+ "CZK 0.01",
+ "CZK 0.00");
+
+ assertFormatDescending(
+ "Currency Cash",
+ "$CZK GCASH",
+ NumberFormatter.with().rounding(Rounding.currency(CurrencyUsage.CASH)).unit(CZK),
+ ULocale.ENGLISH,
+ "CZK 87,650",
+ "CZK 8,765",
+ "CZK 876",
+ "CZK 88",
+ "CZK 9",
+ "CZK 1",
+ "CZK 0",
+ "CZK 0",
+ "CZK 0");
+
+ assertFormatDescending(
+ "Currency not in top-level fluent chain",
+ "F0",
+ NumberFormatter.with().rounding(Rounding.currency(CurrencyUsage.CASH).withCurrency(CZK)),
+ ULocale.ENGLISH,
+ "87,650",
+ "8,765",
+ "876",
+ "88",
+ "9",
+ "1",
+ "0",
+ "0",
+ "0");
+
+ assertFormatDescending(
+ "Lambda Function",
+ "",
+ NumberFormatter.with()
+ .rounding(
+ new IRounding() {
+ @Override
+ public BigDecimal round(BigDecimal input) {
+ return input.setScale(2, RoundingMode.FLOOR);
+ }
+ }),
+ ULocale.ENGLISH,
+ "87,650.00",
+ "8,765.00",
+ "876.50",
+ "87.65",
+ "8.76",
+ "0.87",
+ "0.08",
+ "0.00",
+ "0.00");
+ }
+
+ @Test
+ public void grouping() {
+ // Dimensionless.PERMILLE multiplies all the number by 10^3 (good for testing grouping).
+ // Note that en-US is already performed in the unitPercent() function.
+ assertFormatDescending(
+ "Indic Grouping",
+ "%% grouping=DEFAULT",
+ NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouping.DEFAULT),
+ new ULocale("en-IN"),
+ "8,76,50,000‰",
+ "87,65,000‰",
+ "8,76,500‰",
+ "87,650‰",
+ "8,765‰",
+ "876.5‰",
+ "87.65‰",
+ "8.765‰",
+ "0‰");
+
+ assertFormatDescending(
+ "Western Grouping, Min 2",
+ "%% grouping=DEFAULT_MIN_2_DIGITS",
+ NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouping.DEFAULT_MIN_2_DIGITS),
+ ULocale.ENGLISH,
+ "87,650,000‰",
+ "8,765,000‰",
+ "876,500‰",
+ "87,650‰",
+ "8765‰",
+ "876.5‰",
+ "87.65‰",
+ "8.765‰",
+ "0‰");
+
+ assertFormatDescending(
+ "Indic Grouping, Min 2",
+ "%% grouping=DEFAULT_MIN_2_DIGITS",
+ NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouping.DEFAULT_MIN_2_DIGITS),
+ new ULocale("en-IN"),
+ "8,76,50,000‰",
+ "87,65,000‰",
+ "8,76,500‰",
+ "87,650‰",
+ "8765‰",
+ "876.5‰",
+ "87.65‰",
+ "8.765‰",
+ "0‰");
+
+ assertFormatDescending(
+ "No Grouping",
+ "%% grouping=NONE",
+ NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouping.NONE),
+ new ULocale("en-IN"),
+ "87650000‰",
+ "8765000‰",
+ "876500‰",
+ "87650‰",
+ "8765‰",
+ "876.5‰",
+ "87.65‰",
+ "8.765‰",
+ "0‰");
+ }
+
+ @Test
+ public void padding() {
+ assertFormatDescending(
+ "Padding",
+ "padding=NONE",
+ NumberFormatter.with().padding(Padding.NONE),
+ ULocale.ENGLISH,
+ "87,650",
+ "8,765",
+ "876.5",
+ "87.65",
+ "8.765",
+ "0.8765",
+ "0.08765",
+ "0.008765",
+ "0");
+
+ assertFormatDescending(
+ "Padding",
+ "padding=CP:*:8:AFTER_PREFIX",
+ NumberFormatter.with().padding(Padding.codePoints('*', 8, PadPosition.AFTER_PREFIX)),
+ ULocale.ENGLISH,
+ "**87,650",
+ "***8,765",
+ "***876.5",
+ "***87.65",
+ "***8.765",
+ "**0.8765",
+ "*0.08765",
+ "0.008765",
+ "*******0");
+
+ assertFormatDescending(
+ "Padding with code points",
+ "padding=CP:𐇤:8:AFTER_PREFIX",
+ NumberFormatter.with().padding(Padding.codePoints(0x101E4, 8, PadPosition.AFTER_PREFIX)),
+ ULocale.ENGLISH,
+ "𐇤𐇤87,650",
+ "𐇤𐇤𐇤8,765",
+ "𐇤𐇤𐇤876.5",
+ "𐇤𐇤𐇤87.65",
+ "𐇤𐇤𐇤8.765",
+ "𐇤𐇤0.8765",
+ "𐇤0.08765",
+ "0.008765",
+ "𐇤𐇤𐇤𐇤𐇤𐇤𐇤0");
+
+ assertFormatDescending(
+ "Padding with wide digits",
+ "padding=CP:*:8:AFTER_PREFIX symbols=ns:mathsanb",
+ NumberFormatter.with()
+ .padding(Padding.codePoints('*', 8, PadPosition.AFTER_PREFIX))
+ .symbols(NumberingSystem.getInstanceByName("mathsanb")),
+ ULocale.ENGLISH,
+ "**𝟴𝟳,𝟲𝟱𝟬",
+ "***𝟴,𝟳𝟲𝟱",
+ "***𝟴𝟳𝟲.𝟱",
+ "***𝟴𝟳.𝟲𝟱",
+ "***𝟴.𝟳𝟲𝟱",
+ "**𝟬.𝟴𝟳𝟲𝟱",
+ "*𝟬.𝟬𝟴𝟳𝟲𝟱",
+ "𝟬.𝟬𝟬𝟴𝟳𝟲𝟱",
+ "*******𝟬");
+
+ assertFormatDescending(
+ "Padding with currency spacing",
+ "$GBP padding=CP:*:10:AFTER_PREFIX unit-width=SHORT",
+ NumberFormatter.with()
+ .padding(Padding.codePoints('*', 10, PadPosition.AFTER_PREFIX))
+ .unit(GBP)
+ .unitWidth(FormatWidth.SHORT),
+ ULocale.ENGLISH,
+ "GBP 87,650.00",
+ "GBP 8,765.00",
+ "GBP 876.50",
+ "GBP**87.65",
+ "GBP***8.76",
+ "GBP***0.88",
+ "GBP***0.09",
+ "GBP***0.01",
+ "GBP***0.00");
+
+ assertFormatSingle(
+ "Pad Before Prefix",
+ "padding=CP:*:8:BEFORE_PREFIX",
+ NumberFormatter.with().padding(Padding.codePoints('*', 8, PadPosition.BEFORE_PREFIX)),
+ ULocale.ENGLISH,
+ -88.88,
+ "**-88.88");
+
+ assertFormatSingle(
+ "Pad After Prefix",
+ "padding=CP:*:8:AFTER_PREFIX",
+ NumberFormatter.with().padding(Padding.codePoints('*', 8, PadPosition.AFTER_PREFIX)),
+ ULocale.ENGLISH,
+ -88.88,
+ "-**88.88");
+
+ assertFormatSingle(
+ "Pad Before Suffix",
+ "% padding=CP:*:8:BEFORE_SUFFIX",
+ NumberFormatter.with()
+ .padding(Padding.codePoints('*', 8, PadPosition.BEFORE_SUFFIX))
+ .unit(Dimensionless.PERCENT),
+ ULocale.ENGLISH,
+ 0.8888,
+ "88.88**%");
+
+ assertFormatSingle(
+ "Pad After Suffix",
+ "% padding=CP:*:8:AFTER_SUFFIX",
+ NumberFormatter.with()
+ .padding(Padding.codePoints('*', 8, PadPosition.AFTER_SUFFIX))
+ .unit(Dimensionless.PERCENT),
+ ULocale.ENGLISH,
+ 0.8888,
+ "88.88%**");
+ }
+
+ @Test
+ public void integerWidth() {
+ assertFormatDescending(
+ "Integer Width Default",
+ "integer-width=1-",
+ NumberFormatter.with().integerWidth(IntegerWidth.DEFAULT),
+ ULocale.ENGLISH,
+ "87,650",
+ "8,765",
+ "876.5",
+ "87.65",
+ "8.765",
+ "0.8765",
+ "0.08765",
+ "0.008765",
+ "0");
+
+ assertFormatDescending(
+ "Integer Width Zero Fill 0",
+ "integer-width=0-",
+ NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(0)),
+ ULocale.ENGLISH,
+ "87,650",
+ "8,765",
+ "876.5",
+ "87.65",
+ "8.765",
+ ".8765",
+ ".08765",
+ ".008765",
+ ""); // FIXME: Avoid the empty string here?
+
+ assertFormatDescending(
+ "Integer Width Zero Fill 3",
+ "integer-width=3-",
+ NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(3)),
+ ULocale.ENGLISH,
+ "87,650",
+ "8,765",
+ "876.5",
+ "087.65",
+ "008.765",
+ "000.8765",
+ "000.08765",
+ "000.008765",
+ "000");
+
+ assertFormatDescending(
+ "Integer Width Max 3",
+ "integer-width=1-3",
+ NumberFormatter.with().integerWidth(IntegerWidth.DEFAULT.truncateAt(3)),
+ ULocale.ENGLISH,
+ "650",
+ "765",
+ "876.5",
+ "87.65",
+ "8.765",
+ "0.8765",
+ "0.08765",
+ "0.008765",
+ "0");
+
+ assertFormatDescending(
+ "Integer Width Fixed 2",
+ "integer-width=2",
+ NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2).truncateAt(2)),
+ ULocale.ENGLISH,
+ "50",
+ "65",
+ "76.5",
+ "87.65",
+ "08.765",
+ "00.8765",
+ "00.08765",
+ "00.008765",
+ "00");
+ }
+
+ @Test
+ public void symbols() {
+ assertFormatDescending(
+ "French Symbols with Japanese Data 1",
+ "symbols=loc:fr",
+ NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)),
+ ULocale.JAPAN,
+ "87 650",
+ "8 765",
+ "876,5",
+ "87,65",
+ "8,765",
+ "0,8765",
+ "0,08765",
+ "0,008765",
+ "0");
+
+ assertFormatSingle(
+ "French Symbols with Japanese Data 2",
+ "C symbols=loc:fr",
+ NumberFormatter.with()
+ .notation(Notation.COMPACT_SHORT)
+ .symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)),
+ ULocale.JAPAN,
+ 12345,
+ "1,2\u4E07");
+
+ assertFormatDescending(
+ "Latin Numbering System with Arabic Data",
+ "$USD symbols=ns:latn",
+ NumberFormatter.with().symbols(NumberingSystem.LATIN).unit(USD),
+ new ULocale("ar"),
+ "87,650.00 US$",
+ "8,765.00 US$",
+ "876.50 US$",
+ "87.65 US$",
+ "8.76 US$",
+ "0.88 US$",
+ "0.09 US$",
+ "0.01 US$",
+ "0.00 US$");
+
+ assertFormatDescending(
+ "Math Numbering System with French Data",
+ "symbols=ns:mathsanb",
+ NumberFormatter.with().symbols(NumberingSystem.getInstanceByName("mathsanb")),
+ ULocale.FRENCH,
+ "𝟴𝟳 𝟲𝟱𝟬",
+ "𝟴 𝟳𝟲𝟱",
+ "𝟴𝟳𝟲,𝟱",
+ "𝟴𝟳,𝟲𝟱",
+ "𝟴,𝟳𝟲𝟱",
+ "𝟬,𝟴𝟳𝟲𝟱",
+ "𝟬,𝟬𝟴𝟳𝟲𝟱",
+ "𝟬,𝟬𝟬𝟴𝟳𝟲𝟱",
+ "𝟬");
+ }
+
+ @Test
+ @Ignore("This feature is not currently available.")
+ public void symbolsOverride() {
+ DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(ULocale.ENGLISH);
+ dfs.setCurrencySymbol("@");
+ dfs.setInternationalCurrencySymbol("foo");
+ assertFormatSingle(
+ "Custom Short Currency Symbol",
+ "$XXX",
+ NumberFormatter.with().unit(Currency.getInstance("XXX")).symbols(dfs),
+ ULocale.ENGLISH,
+ 12.3,
+ "@ 12.30");
+ }
+
+ @Test
+ public void sign() {
+ assertFormatSingle(
+ "Sign Auto Positive",
+ "sign=AUTO",
+ NumberFormatter.with().sign(SignDisplay.AUTO),
+ ULocale.ENGLISH,
+ 444444,
+ "444,444");
+
+ assertFormatSingle(
+ "Sign Auto Negative",
+ "sign=AUTO",
+ NumberFormatter.with().sign(SignDisplay.AUTO),
+ ULocale.ENGLISH,
+ -444444,
+ "-444,444");
+
+ assertFormatSingle(
+ "Sign Always Positive",
+ "sign=ALWAYS_SHOWN",
+ NumberFormatter.with().sign(SignDisplay.ALWAYS_SHOWN),
+ ULocale.ENGLISH,
+ 444444,
+ "+444,444");
+
+ assertFormatSingle(
+ "Sign Always Negative",
+ "sign=ALWAYS_SHOWN",
+ NumberFormatter.with().sign(SignDisplay.ALWAYS_SHOWN),
+ ULocale.ENGLISH,
+ -444444,
+ "-444,444");
+
+ assertFormatSingle(
+ "Sign Never Positive",
+ "sign=NEVER_SHOWN",
+ NumberFormatter.with().sign(SignDisplay.NEVER_SHOWN),
+ ULocale.ENGLISH,
+ 444444,
+ "444,444");
+
+ assertFormatSingle(
+ "Sign Never Negative",
+ "sign=NEVER_SHOWN",
+ NumberFormatter.with().sign(SignDisplay.NEVER_SHOWN),
+ ULocale.ENGLISH,
+ -444444,
+ "444,444");
+ }
+
+ @Test
+ public void decimal() {
+ assertFormatDescending(
+ "Decimal Default",
+ "decimal=AUTO",
+ NumberFormatter.with().decimal(DecimalMarkDisplay.AUTO),
+ ULocale.ENGLISH,
+ "87,650",
+ "8,765",
+ "876.5",
+ "87.65",
+ "8.765",
+ "0.8765",
+ "0.08765",
+ "0.008765",
+ "0");
+
+ assertFormatDescending(
+ "Decimal Always Shown",
+ "decimal=ALWAYS_SHOWN",
+ NumberFormatter.with().decimal(DecimalMarkDisplay.ALWAYS_SHOWN),
+ ULocale.ENGLISH,
+ "87,650.",
+ "8,765.",
+ "876.5",
+ "87.65",
+ "8.765",
+ "0.8765",
+ "0.08765",
+ "0.008765",
+ "0.");
+ }
+
+ @Test
+ public void locale() {
+ // Coverage for the locale setters.
+ assertEquals(
+ NumberFormatter.with().locale(ULocale.ENGLISH),
+ NumberFormatter.with().locale(Locale.ENGLISH));
+ assertNotEquals(
+ NumberFormatter.with().locale(ULocale.ENGLISH),
+ NumberFormatter.with().locale(Locale.FRENCH));
+ }
+
+ @Test
+ public void getPrefixSuffix() {
+ Object[][] cases = {
+ {
+ NumberFormatter.withLocale(ULocale.ENGLISH).unit(GBP).unitWidth(FormatWidth.SHORT),
+ "GBP",
+ "",
+ "-GBP",
+ ""
+ },
+ {
+ NumberFormatter.withLocale(ULocale.ENGLISH).unit(GBP).unitWidth(FormatWidth.WIDE),
+ "",
+ " British pounds",
+ "-",
+ " British pounds"
+ }
+ };
+
+ for (Object[] cas : cases) {
+ LocalizedNumberFormatter f = (LocalizedNumberFormatter) cas[0];
+ String posPrefix = (String) cas[1];
+ String posSuffix = (String) cas[2];
+ String negPrefix = (String) cas[3];
+ String negSuffix = (String) cas[4];
+ NumberFormatterResult positive = f.format(1);
+ NumberFormatterResult negative = f.format(-1);
+ assertEquals(posPrefix, positive.getPrefix());
+ assertEquals(posSuffix, positive.getSuffix());
+ assertEquals(negPrefix, negative.getPrefix());
+ assertEquals(negSuffix, negative.getSuffix());
+ }
+ }
+
+ @Test
+ public void plurals() {
+ // TODO: Expand this test.
+
+ assertFormatSingle(
+ "Plural 1",
+ "$USD F0 unit-width=WIDE",
+ NumberFormatter.with()
+ .unit(USD)
+ .unitWidth(FormatWidth.WIDE)
+ .rounding(Rounding.fixedFraction(0)),
+ ULocale.ENGLISH,
+ 1,
+ "1 US dollar");
+
+ assertFormatSingle(
+ "Plural 1.00",
+ "$USD F2 unit-width=WIDE",
+ NumberFormatter.with()
+ .unit(USD)
+ .unitWidth(FormatWidth.WIDE)
+ .rounding(Rounding.fixedFraction(2)),
+ ULocale.ENGLISH,
+ 1,
+ "1.00 US dollars");
+ }
+
+ private static void assertFormatDescending(
+ String message,
+ String skeleton,
+ UnlocalizedNumberFormatter f,
+ ULocale locale,
+ String... expected) {
+ assert expected.length == 9;
+ assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
+ final double[] inputs =
+ new double[] {87650, 8765, 876.5, 87.65, 8.765, 0.8765, 0.08765, 0.008765, 0};
+ NumberFormatterImpl l1 = (NumberFormatterImpl) f.locale(locale); // no self-regulation
+ NumberFormatterImpl l2 = (NumberFormatterImpl) f.locale(locale); // all self-regulation
+ for (int i = 0; i < 9; i++) {
+ double d = inputs[i];
+ String actual1 = l1.formatWithThreshold(d, 0).toString();
+ assertEquals(message + ": L1: " + d, expected[i], actual1);
+ String actual2 = l2.formatWithThreshold(d, 1).toString();
+ assertEquals(message + ": L2: " + d, expected[i], actual2);
+ }
+ }
+
+ private static void assertFormatSingle(
+ String message,
+ String skeleton,
+ UnlocalizedNumberFormatter f,
+ ULocale locale,
+ Number input,
+ String expected) {
+ assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
+ NumberFormatterImpl l1 = (NumberFormatterImpl) f.locale(locale); // no self-regulation
+ NumberFormatterImpl l2 = (NumberFormatterImpl) f.locale(locale); // all self-regulation
+ String actual1 = l1.formatWithThreshold(input, 0).toString();
+ assertEquals(message + ": L1: " + input, expected, actual1);
+ String actual2 = l2.formatWithThreshold(input, 1).toString();
+ assertEquals(message + ": L2: " + input, expected, actual2);
+ }
+
+ private static void assertFormatSingleMeasure(
+ String message,
+ String skeleton,
+ UnlocalizedNumberFormatter f,
+ ULocale locale,
+ Measure input,
+ String expected) {
+ assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
+ NumberFormatterImpl l1 = (NumberFormatterImpl) f.locale(locale); // no self-regulation
+ NumberFormatterImpl l2 = (NumberFormatterImpl) f.locale(locale); // all self-regulation
+ String actual1 = l1.formatWithThreshold(input, 0).toString();
+ assertEquals(message + ": L1: " + input, expected, actual1);
+ String actual2 = l2.formatWithThreshold(input, 1).toString();
+ assertEquals(message + ": L2: " + input, expected, actual2);
+ }
+}