diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LdmlPatternInfo.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LdmlPatternInfo.java new file mode 100644 index 0000000000..ef0781ab4b --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LdmlPatternInfo.java @@ -0,0 +1,447 @@ +// © 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++; + } + } +} diff --git a/icu4j/main/classes/core/src/newapi/NumberFormatter.java b/icu4j/main/classes/core/src/newapi/NumberFormatter.java new file mode 100644 index 0000000000..affce4410e --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/NumberFormatter.java @@ -0,0 +1,684 @@ +// © 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 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). + * + *

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. + * + *

For example, with integer rounding, the number 3.141 becomes "3". However, with minimum + * figures set to 2, 3.141 becomes "3.1" instead. + * + *

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. + * + *

For example, with integer rounding, the number 123.4 becomes "123". However, with maximum + * figures set to 2, 123.4 becomes "120" instead. + * + *

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)}. + * + *

Calling this method is not required, 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. + * + *

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 {} + } +} diff --git a/icu4j/main/classes/core/src/newapi/demo.java b/icu4j/main/classes/core/src/newapi/demo.java new file mode 100644 index 0000000000..6a4ae8eef7 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/demo.java @@ -0,0 +1,63 @@ +// © 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(); + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/AffixPatternProvider.java b/icu4j/main/classes/core/src/newapi/impl/AffixPatternProvider.java new file mode 100644 index 0000000000..87554b7f77 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/AffixPatternProvider.java @@ -0,0 +1,26 @@ +// © 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); +} diff --git a/icu4j/main/classes/core/src/newapi/impl/CompactData.java b/icu4j/main/classes/core/src/newapi/impl/CompactData.java new file mode 100644 index 0000000000..f18ce45007 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/CompactData.java @@ -0,0 +1,276 @@ +// © 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> powersToPluralsToPatterns) { + CompactData data = new CompactData(); + for (Map.Entry> magnitudeEntry : + powersToPluralsToPatterns.entrySet()) { + byte magnitude = (byte) (magnitudeEntry.getKey().length() - 1); + for (Map.Entry 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 = ""; + + 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 getAllPatterns() { + Set result = new HashSet(); + 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; + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/CompactImpl.java b/icu4j/main/classes/core/src/newapi/impl/CompactImpl.java new file mode 100644 index 0000000000..b68d6d6cc2 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/CompactImpl.java @@ -0,0 +1,114 @@ +// © 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 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> 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(); + Set 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; + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/CurrencySpacingEnabledModifier.java b/icu4j/main/classes/core/src/newapi/impl/CurrencySpacingEnabledModifier.java new file mode 100644 index 0000000000..eed7ed13fa --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/CurrencySpacingEnabledModifier.java @@ -0,0 +1,173 @@ +// © 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; + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/CustomSymbolCurrency.java b/icu4j/main/classes/core/src/newapi/impl/CustomSymbolCurrency.java new file mode 100644 index 0000000000..664694bd35 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/CustomSymbolCurrency.java @@ -0,0 +1,61 @@ +// © 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; + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/DataUtils.java b/icu4j/main/classes/core/src/newapi/impl/DataUtils.java new file mode 100644 index 0000000000..f505d01780 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/DataUtils.java @@ -0,0 +1,67 @@ +// © 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 getCurrencyLongNameModifiers( + ULocale loc, Currency currency) { + Map data = CurrencyData.provider.getInstance(loc, true).getUnitPatterns(); + Map result = + new EnumMap(StandardPlural.class); + StringBuilder sb = new StringBuilder(); + for (Map.Entry 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 getMeasureUnitModifiers( + ULocale loc, MeasureUnit unit, FormatWidth width) { + Map simpleFormats = MeasureData.getMeasureData(loc, unit, width); + Map result = + new EnumMap(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 result = + // new EnumMap(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; + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/GroupingImpl.java b/icu4j/main/classes/core/src/newapi/impl/GroupingImpl.java new file mode 100644 index 0000000000..9e9d2076a2 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/GroupingImpl.java @@ -0,0 +1,134 @@ +// © 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); + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/IntegerWidthImpl.java b/icu4j/main/classes/core/src/newapi/impl/IntegerWidthImpl.java new file mode 100644 index 0000000000..795652fa0b --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/IntegerWidthImpl.java @@ -0,0 +1,27 @@ +// © 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); + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/MacroProps.java b/icu4j/main/classes/core/src/newapi/impl/MacroProps.java new file mode 100644 index 0000000000..e9f8c3d620 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/MacroProps.java @@ -0,0 +1,105 @@ +// © 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(); + } + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/MeasureData.java b/icu4j/main/classes/core/src/newapi/impl/MeasureData.java new file mode 100644 index 0000000000..d48c129bde --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/MeasureData.java @@ -0,0 +1,64 @@ +// © 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 output; + + public ShanesMeasureUnitSink(Map 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 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 output = new EnumMap(StandardPlural.class); + ShanesMeasureUnitSink sink = new ShanesMeasureUnitSink(output); + resource.getAllItemsWithFallback(key.toString(), sink); + return output; + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/MicroProps.java b/icu4j/main/classes/core/src/newapi/impl/MicroProps.java new file mode 100644 index 0000000000..50c218fad2 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/MicroProps.java @@ -0,0 +1,58 @@ +// © 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(); + } + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/MultiplierImpl.java b/icu4j/main/classes/core/src/newapi/impl/MultiplierImpl.java new file mode 100644 index 0000000000..11448f4b08 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/MultiplierImpl.java @@ -0,0 +1,39 @@ +// © 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; + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/MurkyModifier.java b/icu4j/main/classes/core/src/newapi/impl/MurkyModifier.java new file mode 100644 index 0000000000..125bf19774 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/MurkyModifier.java @@ -0,0 +1,414 @@ +// © 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!!! + * + *

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(); + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/NotationImpl.java b/icu4j/main/classes/core/src/newapi/impl/NotationImpl.java new file mode 100644 index 0000000000..dbe5bc713f --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/NotationImpl.java @@ -0,0 +1,81 @@ +// © 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> compactCustomData; + + public NotationCompactImpl(CompactStyle compactStyle) { + compactCustomData = null; + this.compactStyle = compactStyle; + } + + public NotationCompactImpl(Map> compactCustomData) { + compactStyle = null; + this.compactCustomData = compactCustomData; + } + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/NumberFormatterImpl.java b/icu4j/main/classes/core/src/newapi/impl/NumberFormatterImpl.java new file mode 100644 index 0000000000..b7727eef03 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/NumberFormatterImpl.java @@ -0,0 +1,328 @@ +// © 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; + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/NumberPropertyMapper.java b/icu4j/main/classes/core/src/newapi/impl/NumberPropertyMapper.java new file mode 100644 index 0000000000..ae28af0d65 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/NumberPropertyMapper.java @@ -0,0 +1,447 @@ +// © 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); + } + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/PaddingImpl.java b/icu4j/main/classes/core/src/newapi/impl/PaddingImpl.java new file mode 100644 index 0000000000..f747ebb0e6 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/PaddingImpl.java @@ -0,0 +1,109 @@ +// © 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; + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/PositiveDecimalImpl.java b/icu4j/main/classes/core/src/newapi/impl/PositiveDecimalImpl.java new file mode 100644 index 0000000000..8c59a71802 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/PositiveDecimalImpl.java @@ -0,0 +1,114 @@ +// © 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(); + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/QuantityChain.java b/icu4j/main/classes/core/src/newapi/impl/QuantityChain.java new file mode 100644 index 0000000000..9efedec1b1 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/QuantityChain.java @@ -0,0 +1,10 @@ +// © 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 diff --git a/icu4j/main/classes/core/src/newapi/impl/QuantityDependentModOuter.java b/icu4j/main/classes/core/src/newapi/impl/QuantityDependentModOuter.java new file mode 100644 index 0000000000..45d1ad0661 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/QuantityDependentModOuter.java @@ -0,0 +1,37 @@ +// © 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 data; + final PluralRules rules; + /* final */ QuantityChain parent; + + public QuantityDependentModOuter(Map 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; + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/RoundingImpl.java b/icu4j/main/classes/core/src/newapi/impl/RoundingImpl.java new file mode 100644 index 0000000000..30f204cd48 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/RoundingImpl.java @@ -0,0 +1,445 @@ +// © 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. + * + *

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."); + } + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/ScientificImpl.java b/icu4j/main/classes/core/src/newapi/impl/ScientificImpl.java new file mode 100644 index 0000000000..8e40ce8168 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/ScientificImpl.java @@ -0,0 +1,144 @@ +// © 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; + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/SkeletonBuilder.java b/icu4j/main/classes/core/src/newapi/impl/SkeletonBuilder.java new file mode 100644 index 0000000000..ef1e0bace7 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/SkeletonBuilder.java @@ -0,0 +1,549 @@ +// © 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; + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/Worker1.java b/icu4j/main/classes/core/src/newapi/impl/Worker1.java new file mode 100644 index 0000000000..376f916145 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/Worker1.java @@ -0,0 +1,263 @@ +// © 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 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; + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MurkyModifierTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MurkyModifierTest.java new file mode 100644 index 0000000000..c4d9d5caf3 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MurkyModifierTest.java @@ -0,0 +1,58 @@ +// © 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()); + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java new file mode 100644 index 0000000000..697be1c3de --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java @@ -0,0 +1,1248 @@ +// © 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); + } +}