From b60d81e6688cbaf71f11c010201d1b4bda5deb19 Mon Sep 17 00:00:00 2001 From: Shane Carr Date: Sat, 26 Aug 2017 06:56:41 +0000 Subject: [PATCH] ICU-13177 Moving around a lot of code for better C++ portability. The only behavior change is that -1 is stored instead of Integer.MAX_VALUE internally in most places. X-SVN-Rev: 40357 --- .../CurrencySpacingEnabledModifier.java | 3 +- .../src/com/ibm/icu/text/DecimalFormat.java | 24 +- .../core/src/newapi/CompactNotation.java | 135 ++++ .../core/src/newapi/CurrencyRounder.java | 42 ++ .../core/src/newapi/FormattedNumber.java | 115 +++ .../core/src/newapi/FractionRounder.java | 60 ++ .../main/classes/core/src/newapi/Grouper.java | 98 +++ .../classes/core/src/newapi/IntegerWidth.java | 39 ++ .../src/newapi/LocalizedNumberFormatter.java | 108 +++ .../core/src/newapi/MurkyLongNameHandler.java | 111 +++ .../core/src/newapi/MurkyModifier.java | 473 +++++++++++++ .../classes/core/src/newapi/Notation.java | 40 ++ .../core/src/newapi/NumberFormatter.java | 653 +----------------- .../src/newapi/NumberFormatterSettings.java | 209 ++++++ .../core/src/newapi/NumberPropertyMapper.java | 454 ++++++++++++ .../main/classes/core/src/newapi/Rounder.java | 446 ++++++++++++ .../core/src/newapi/ScientificNotation.java | 218 ++++++ .../core/src/newapi/SimpleNotation.java | 8 + .../core/src/newapi/SkeletonBuilder.java | 586 ++++++++++++++++ .../newapi/UnlocalizedNumberFormatter.java | 32 + .../main/classes/core/src/newapi/Worker1.java | 299 ++++++++ .../core/src/newapi/impl/CompactData.java | 2 +- .../core/src/newapi/impl/CompactImpl.java | 125 ---- .../core/src/newapi/impl/DataUtils.java | 67 -- .../core/src/newapi/impl/GroupingImpl.java | 135 ---- .../src/newapi/impl/IntegerWidthImpl.java | 37 - .../core/src/newapi/impl/MacroProps.java | 34 +- .../core/src/newapi/impl/MicroProps.java | 12 +- .../src/newapi/impl/MultiplierProducer.java | 7 + .../core/src/newapi/impl/MurkyModifier.java | 465 ------------- .../core/src/newapi/impl/NotationImpl.java | 87 --- .../src/newapi/impl/NumberFormatterImpl.java | 330 --------- .../src/newapi/impl/NumberPropertyMapper.java | 453 ------------ .../classes/core/src/newapi/impl/Padder.java | 107 +++ .../core/src/newapi/impl/PaddingImpl.java | 109 --- .../src/newapi/impl/PositiveDecimalImpl.java | 95 --- .../impl/QuantityDependentModOuter.java | 32 - .../core/src/newapi/impl/RoundingImpl.java | 445 ------------ .../core/src/newapi/impl/ScientificImpl.java | 145 ---- .../core/src/newapi/impl/SkeletonBuilder.java | 624 ----------------- .../classes/core/src/newapi/impl/Worker1.java | 258 ------- .../core/src/newapi/{ => impl}/demo.java | 31 +- .../format/NumberFormatDataDrivenTest.java | 9 +- .../dev/test/number/FormatQuantityTest.java | 23 +- .../dev/test/number/MurkyModifierTest.java | 2 +- .../dev/test/number/NumberFormatterTest.java | 190 +++-- 46 files changed, 3746 insertions(+), 4231 deletions(-) rename icu4j/main/classes/core/src/{newapi/impl => com/ibm/icu/impl/number/modifiers}/CurrencySpacingEnabledModifier.java (98%) create mode 100644 icu4j/main/classes/core/src/newapi/CompactNotation.java create mode 100644 icu4j/main/classes/core/src/newapi/CurrencyRounder.java create mode 100644 icu4j/main/classes/core/src/newapi/FormattedNumber.java create mode 100644 icu4j/main/classes/core/src/newapi/FractionRounder.java create mode 100644 icu4j/main/classes/core/src/newapi/Grouper.java create mode 100644 icu4j/main/classes/core/src/newapi/IntegerWidth.java create mode 100644 icu4j/main/classes/core/src/newapi/LocalizedNumberFormatter.java create mode 100644 icu4j/main/classes/core/src/newapi/MurkyLongNameHandler.java create mode 100644 icu4j/main/classes/core/src/newapi/MurkyModifier.java create mode 100644 icu4j/main/classes/core/src/newapi/Notation.java create mode 100644 icu4j/main/classes/core/src/newapi/NumberFormatterSettings.java create mode 100644 icu4j/main/classes/core/src/newapi/NumberPropertyMapper.java create mode 100644 icu4j/main/classes/core/src/newapi/Rounder.java create mode 100644 icu4j/main/classes/core/src/newapi/ScientificNotation.java create mode 100644 icu4j/main/classes/core/src/newapi/SimpleNotation.java create mode 100644 icu4j/main/classes/core/src/newapi/SkeletonBuilder.java create mode 100644 icu4j/main/classes/core/src/newapi/UnlocalizedNumberFormatter.java create mode 100644 icu4j/main/classes/core/src/newapi/Worker1.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/CompactImpl.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/DataUtils.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/GroupingImpl.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/IntegerWidthImpl.java create mode 100644 icu4j/main/classes/core/src/newapi/impl/MultiplierProducer.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/MurkyModifier.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/NotationImpl.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/NumberFormatterImpl.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/NumberPropertyMapper.java create mode 100644 icu4j/main/classes/core/src/newapi/impl/Padder.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/PaddingImpl.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/PositiveDecimalImpl.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/QuantityDependentModOuter.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/RoundingImpl.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/ScientificImpl.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/SkeletonBuilder.java delete mode 100644 icu4j/main/classes/core/src/newapi/impl/Worker1.java rename icu4j/main/classes/core/src/newapi/{ => impl}/demo.java (71%) diff --git a/icu4j/main/classes/core/src/newapi/impl/CurrencySpacingEnabledModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/CurrencySpacingEnabledModifier.java similarity index 98% rename from icu4j/main/classes/core/src/newapi/impl/CurrencySpacingEnabledModifier.java rename to icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/CurrencySpacingEnabledModifier.java index eed7ed13fa6..66b7c2d6204 100644 --- a/icu4j/main/classes/core/src/newapi/impl/CurrencySpacingEnabledModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/CurrencySpacingEnabledModifier.java @@ -1,9 +1,8 @@ // © 2017 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html#License -package newapi.impl; +package com.ibm.icu.impl.number.modifiers; 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; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java index 3efa8fa15b8..d56f5d7e2f6 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java @@ -28,11 +28,11 @@ import com.ibm.icu.util.CurrencyAmount; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.ULocale.Category; -import newapi.NumberFormatter.LocalizedNumberFormatter; -import newapi.NumberFormatter.NumberFormatterResult; +import newapi.FormattedNumber; +import newapi.LocalizedNumberFormatter; +import newapi.NumberFormatter; +import newapi.NumberPropertyMapper; import newapi.impl.MacroProps; -import newapi.impl.NumberFormatterImpl; -import newapi.impl.NumberPropertyMapper; /** * {@icuenhanced java.text.DecimalFormat}.{@icu _usage_} DecimalFormat is the primary @@ -683,7 +683,7 @@ public class DecimalFormat extends NumberFormat { */ @Override public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) { - NumberFormatterResult output = formatter.format(number); + FormattedNumber output = formatter.format(number); output.populateFieldPosition(fieldPosition, result.length()); output.appendTo(result); return result; @@ -696,7 +696,7 @@ public class DecimalFormat extends NumberFormat { */ @Override public StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition) { - NumberFormatterResult output = formatter.format(number); + FormattedNumber output = formatter.format(number); output.populateFieldPosition(fieldPosition, result.length()); output.appendTo(result); return result; @@ -709,7 +709,7 @@ public class DecimalFormat extends NumberFormat { */ @Override public StringBuffer format(BigInteger number, StringBuffer result, FieldPosition fieldPosition) { - NumberFormatterResult output = formatter.format(number); + FormattedNumber output = formatter.format(number); output.populateFieldPosition(fieldPosition, result.length()); output.appendTo(result); return result; @@ -723,7 +723,7 @@ public class DecimalFormat extends NumberFormat { @Override public StringBuffer format( java.math.BigDecimal number, StringBuffer result, FieldPosition fieldPosition) { - NumberFormatterResult output = formatter.format(number); + FormattedNumber output = formatter.format(number); output.populateFieldPosition(fieldPosition, result.length()); output.appendTo(result); return result; @@ -736,7 +736,7 @@ public class DecimalFormat extends NumberFormat { */ @Override public StringBuffer format(BigDecimal number, StringBuffer result, FieldPosition fieldPosition) { - NumberFormatterResult output = formatter.format(number); + FormattedNumber output = formatter.format(number); output.populateFieldPosition(fieldPosition, result.length()); output.appendTo(result); return result; @@ -751,7 +751,7 @@ public class DecimalFormat extends NumberFormat { public AttributedCharacterIterator formatToCharacterIterator(Object obj) { if (!(obj instanceof Number)) throw new IllegalArgumentException(); Number number = (Number) obj; - NumberFormatterResult output = formatter.format(number); + FormattedNumber output = formatter.format(number); return output.toAttributedCharacterIterator(); } @@ -762,7 +762,7 @@ public class DecimalFormat extends NumberFormat { */ @Override public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) { - NumberFormatterResult output = formatter.format(currAmt); + FormattedNumber output = formatter.format(currAmt); output.populateFieldPosition(pos, toAppendTo.length()); output.appendTo(toAppendTo); return toAppendTo; @@ -2434,7 +2434,7 @@ public class DecimalFormat extends NumberFormat { locale = symbols.getULocale(); } assert locale != null; - formatter = NumberFormatterImpl.fromMacros(macros).locale(locale); + formatter = NumberFormatter.with().macros(macros).locale(locale); } /** diff --git a/icu4j/main/classes/core/src/newapi/CompactNotation.java b/icu4j/main/classes/core/src/newapi/CompactNotation.java new file mode 100644 index 00000000000..c45bb3778c9 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/CompactNotation.java @@ -0,0 +1,135 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +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.MurkyModifier.ImmutableMurkyModifier; +import newapi.impl.CompactData; +import newapi.impl.MicroProps; +import newapi.impl.QuantityChain; + +public class CompactNotation extends Notation { + + final CompactStyle compactStyle; + final Map> compactCustomData; + + public CompactNotation(CompactStyle compactStyle) { + compactCustomData = null; + this.compactStyle = compactStyle; + } + + public CompactNotation(Map> compactCustomData) { + compactStyle = null; + this.compactCustomData = compactCustomData; + } + + /* package-private */ QuantityChain withLocaleData(ULocale dataLocale, CompactType compactType, PluralRules rules, + MurkyModifier buildReference, QuantityChain parent) { + CompactData data; + if (compactStyle != null) { + data = CompactData.getInstance(dataLocale, compactType, compactStyle); + } else { + data = CompactData.getInstance(compactCustomData); + } + return new CompactImpl(data, rules, buildReference, parent); + } + + private static class CompactImpl implements QuantityChain { + + private static class CompactModInfo { + public ImmutableMurkyModifier mod; + public int numDigits; + } + + final PluralRules rules; + final CompactData data; + final Map precomputedMods; + final QuantityChain parent; + + private CompactImpl(CompactData data, PluralRules rules, MurkyModifier buildReference, QuantityChain parent) { + this.data = data; + this.rules = rules; + if (buildReference != null) { + // Safe code path + precomputedMods = precomputeAllModifiers(data, buildReference); + } else { + // Unsafe code path + precomputedMods = null; + } + this.parent = parent; + } + + /** Used by the safe code path */ + private static Map precomputeAllModifiers(CompactData data, + MurkyModifier buildReference) { + Map precomputedMods = new HashMap(); + Set allPatterns = data.getAllPatterns(); + for (String patternString : allPatterns) { + CompactModInfo info = new CompactModInfo(); + PatternParseResult patternInfo = LdmlPatternInfo.parse(patternString); + buildReference.setPatternInfo(patternInfo); + info.mod = buildReference.createImmutable(); + info.numDigits = patternInfo.positive.totalIntegerDigits; + precomputedMods.put(patternString, info); + } + return precomputedMods; + } + + @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 = Rounder.constructPassThrough(); + + return micros; + } + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/CurrencyRounder.java b/icu4j/main/classes/core/src/newapi/CurrencyRounder.java new file mode 100644 index 00000000000..631084e9a3d --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/CurrencyRounder.java @@ -0,0 +1,42 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +import com.ibm.icu.util.Currency; +import com.ibm.icu.util.Currency.CurrencyUsage; +import com.ibm.icu.util.CurrencyAmount; +import com.ibm.icu.util.Measure; +import com.ibm.icu.util.MeasureUnit; + +/** A rounding strategy parameterized by a currency. */ +public abstract class CurrencyRounder extends Rounder { + + /* package-private */ CurrencyRounder() { + } + + /** + * 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 Rounder withCurrency(Currency currency) { + if (currency != null) { + return constructFromCurrency(this, currency); + } else { + throw new IllegalArgumentException("Currency must not be null"); + } + }; +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/FormattedNumber.java b/icu4j/main/classes/core/src/newapi/FormattedNumber.java new file mode 100644 index 00000000000..3749eebc15e --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/FormattedNumber.java @@ -0,0 +1,115 @@ +// © 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.text.AttributedCharacterIterator; +import java.text.FieldPosition; +import java.util.Arrays; + +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.text.PluralRules.IFixedDecimal; +import com.ibm.icu.util.ICUUncheckedIOException; + +import newapi.impl.MicroProps; + +public class FormattedNumber { + NumberStringBuilder nsb; + FormatQuantity fq; + MicroProps micros; + + FormattedNumber(NumberStringBuilder nsb, FormatQuantity 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 void populateFieldPosition(FieldPosition fieldPosition) { + populateFieldPosition(fieldPosition, 0); + } + + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public void populateFieldPosition(FieldPosition fieldPosition, int offset) { + nsb.populateFieldPosition(fieldPosition, offset); + fq.populateUFieldPosition(fieldPosition); + } + + public AttributedCharacterIterator toAttributedCharacterIterator() { + return nsb.getIterator(); + } + + public BigDecimal toBigDecimal() { + return fq.toBigDecimal(); + } + + /** + * @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; + } + + @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 FormattedNumber)) + return false; + // NumberStringBuilder and BigDecimal are mutable, so we can't call + // #equals() or #hashCode() on them directly. + FormattedNumber _other = (FormattedNumber) other; + return Arrays.equals(nsb.toCharArray(), _other.nsb.toCharArray()) + ^ Arrays.equals(nsb.toFieldArray(), _other.nsb.toFieldArray()) + ^ fq.toBigDecimal().equals(_other.fq.toBigDecimal()); + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/FractionRounder.java b/icu4j/main/classes/core/src/newapi/FractionRounder.java new file mode 100644 index 00000000000..b6dd1f46600 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/FractionRounder.java @@ -0,0 +1,60 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +/** + * 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 abstract class FractionRounder extends Rounder { + + /* package-private */ FractionRounder() { + } + + /** + * 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 Rounder withMinFigures(int minFigures) { + if (minFigures > 0 && minFigures <= MAX_VALUE) { + return constructFractionSignificant(this, minFigures, -1); + } else { + throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE); + } + } + + /** + * 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 + * Round the number to no more than this number of significant figures. + * @return An immutable object for chaining. + */ + public Rounder withMaxFigures(int maxFigures) { + if (maxFigures > 0 && maxFigures <= MAX_VALUE) { + return constructFractionSignificant(this, -1, maxFigures); + } else { + throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE); + } + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/Grouper.java b/icu4j/main/classes/core/src/newapi/Grouper.java new file mode 100644 index 00000000000..0db2586769e --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/Grouper.java @@ -0,0 +1,98 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult; + +public class Grouper { + + // Conveniences for Java handling of bytes + private static final byte N2 = -2; + private static final byte N1 = -1; + private static final byte B2 = 2; + private static final byte B3 = 3; + + private static final Grouper DEFAULTS = new Grouper(N2, N2, false); + private static final Grouper MIN2 = new Grouper(N2, N2, true); + private static final Grouper NONE = new Grouper(N1, N1, false); + + private final byte grouping1; // -2 means "needs locale data"; -1 means "no grouping" + private final byte grouping2; + private final boolean min2; + + private Grouper(byte grouping1, byte grouping2, boolean min2) { + this.grouping1 = grouping1; + this.grouping2 = grouping2; + this.min2 = min2; + } + + public static Grouper defaults() { + return DEFAULTS; + } + + public static Grouper min2() { + return MIN2; + } + + public static Grouper none() { + return NONE; + } + + ////////////////////////// + // PACKAGE-PRIVATE APIS // + ////////////////////////// + + private static final Grouper GROUPING_3 = new Grouper(B3, B3, false); + private static final Grouper GROUPING_3_2 = new Grouper(B3, B2, false); + private static final Grouper GROUPING_3_MIN2 = new Grouper(B3, B3, true); + private static final Grouper GROUPING_3_2_MIN2 = new Grouper(B3, B2, true); + + static Grouper getInstance(byte grouping1, byte 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 Grouper(grouping1, grouping2, min2); + } + } + + static Grouper normalizeType(Grouper grouping, PatternParseResult patternInfo) { + return grouping.withLocaleData(patternInfo); + } + + Grouper withLocaleData(PatternParseResult patternInfo) { + if (grouping1 != -2) { + return this; + } + // TODO: short or byte? + byte grouping1 = (byte) (patternInfo.positive.groupingSizes & 0xffff); + byte grouping2 = (byte) ((patternInfo.positive.groupingSizes >>> 16) & 0xffff); + byte grouping3 = (byte) ((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) { + 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); + } + } \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/IntegerWidth.java b/icu4j/main/classes/core/src/newapi/IntegerWidth.java new file mode 100644 index 00000000000..e092ec08615 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/IntegerWidth.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; + +@SuppressWarnings("unused") +public class IntegerWidth { + + private static final IntegerWidth DEFAULT = new IntegerWidth(1, -1); + + final int minInt; + final int maxInt; + + private IntegerWidth(int minInt, int maxInt) { + this.minInt = minInt; + this.maxInt = maxInt; + } + + public static IntegerWidth zeroFillTo(int minInt) { + if (minInt == 1) { + return DEFAULT; + } else if (minInt >= 0 && minInt < Rounder.MAX_VALUE) { + return new IntegerWidth(minInt, -1); + } else { + throw new IllegalArgumentException("Integer digits must be between 0 and " + Rounder.MAX_VALUE); + } + } + + public IntegerWidth truncateAt(int maxInt) { + if (maxInt == this.maxInt) { + return this; + } else if (maxInt >= 0 && maxInt < Rounder.MAX_VALUE) { + return new IntegerWidth(minInt, maxInt); + } else if (maxInt == -1) { + return new IntegerWidth(minInt, maxInt); + } else { + throw new IllegalArgumentException("Integer digits must be between 0 and " + Rounder.MAX_VALUE); + } + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/LocalizedNumberFormatter.java b/icu4j/main/classes/core/src/newapi/LocalizedNumberFormatter.java new file mode 100644 index 00000000000..9152360fbff --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/LocalizedNumberFormatter.java @@ -0,0 +1,108 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.FormatQuantity4; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.util.Measure; +import com.ibm.icu.util.MeasureUnit; + +import newapi.impl.MacroProps; +import newapi.impl.MicroProps; + +public class LocalizedNumberFormatter extends NumberFormatterSettings { + + static final AtomicLongFieldUpdater callCount = AtomicLongFieldUpdater + .newUpdater(LocalizedNumberFormatter.class, "callCountInternal"); + + volatile long callCountInternal; // do not access directly; use callCount instead + volatile LocalizedNumberFormatter savedWithUnit; + volatile Worker1 compiled; + + LocalizedNumberFormatter(NumberFormatterSettings parent, int key, Object value) { + super(parent, key, value); + } + + public FormattedNumber format(long input) { + return format(new FormatQuantity4(input)); + } + + public FormattedNumber format(double input) { + return format(new FormatQuantity4(input)); + } + + public FormattedNumber format(Number input) { + return format(new FormatQuantity4(input)); + } + + public FormattedNumber format(Measure input) { + MeasureUnit unit = input.getUnit(); + Number number = input.getNumber(); + // Use this formatter if possible + if (Objects.equals(resolve().unit, unit)) { + return format(number); + } + // This mechanism saves the previously used unit, so if the user calls this method with the + // same unit multiple times in a row, they get a more efficient code path. + LocalizedNumberFormatter withUnit = savedWithUnit; + if (withUnit == null || !Objects.equals(withUnit.resolve().unit, unit)) { + withUnit = new LocalizedNumberFormatter(this, KEY_UNIT, unit); + savedWithUnit = withUnit; + } + return withUnit.format(number); + } + + /** + * This is the core entrypoint to the number formatting pipeline. It performs self-regulation: a static code path + * for the first few calls, and compiling a more efficient data structure if called repeatedly. + * + * @param fq + * The quantity to be formatted. + * @return The formatted number result. + * + * @internal + * @deprecated ICU 60 This API is ICU internal only. + */ + @Deprecated + public FormattedNumber format(FormatQuantity fq) { + MacroProps macros = resolve(); + NumberStringBuilder string = new NumberStringBuilder(); + long currentCount = callCount.incrementAndGet(this); + MicroProps micros; + if (currentCount == macros.threshold.longValue()) { + compiled = Worker1.fromMacros(macros); + micros = compiled.apply(fq, string); + } else if (compiled != null) { + micros = compiled.apply(fq, string); + } else { + micros = Worker1.applyStatic(macros, fq, string); + } + return new FormattedNumber(string, fq, micros); + } + + @Override + protected LocalizedNumberFormatter create(int key, Object value) { + return new LocalizedNumberFormatter(this, key, value); + } + + /** + * @internal + * @deprecated ICU 60 This API is ICU internal only. + */ + @Deprecated + public static class Internal extends LocalizedNumberFormatter { + + /** + * @internal + * @deprecated ICU 60 This API is ICU internal only. + */ + @Deprecated + public Internal(NumberFormatterSettings parent, int key, Object value) { + super(parent, key, value); + } + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/MurkyLongNameHandler.java b/icu4j/main/classes/core/src/newapi/MurkyLongNameHandler.java new file mode 100644 index 00000000000..8fee3a347e1 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/MurkyLongNameHandler.java @@ -0,0 +1,111 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +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.FormatQuantity; +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.text.PluralRules; +import com.ibm.icu.util.Currency; +import com.ibm.icu.util.MeasureUnit; +import com.ibm.icu.util.ULocale; + +import newapi.impl.MeasureData; +import newapi.impl.MicroProps; +import newapi.impl.QuantityChain; + +class MurkyLongNameHandler implements QuantityChain { + + private final Map data; + /* unsafe */ PluralRules rules; + /* unsafe */ QuantityChain parent; + + private MurkyLongNameHandler(Map data) { + this.data = data; + } + + public static MurkyLongNameHandler 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 new MurkyLongNameHandler(result); + } + + public static MurkyLongNameHandler 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 new MurkyLongNameHandler(result); + } + + public QuantityChain withLocaleData(PluralRules rules, boolean safe, QuantityChain parent) { + if (safe) { + // Safe code path: return a new object + return new ImmutableLongNameHandler(data, rules, parent); + } else { + // Unsafe code path: re-use this object! + this.rules = rules; + 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; + } + + public static class ImmutableLongNameHandler implements QuantityChain { + final Map data; + final PluralRules rules; + final QuantityChain parent; + + public ImmutableLongNameHandler(Map data, PluralRules rules, QuantityChain parent) { + this.data = data; + this.rules = rules; + this.parent = parent; + } + + @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/MurkyModifier.java b/icu4j/main/classes/core/src/newapi/MurkyModifier.java new file mode 100644 index 00000000000..2ef3c2f911d --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/MurkyModifier.java @@ -0,0 +1,473 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +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.LdmlPatternInfo; +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.impl.number.modifiers.CurrencySpacingEnabledModifier; +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; +import newapi.impl.AffixPatternProvider; +import newapi.impl.MicroProps; +import newapi.impl.QuantityChain; + +/** + * This class is a {@link Modifier} that wraps a decimal format pattern. It applies the pattern's affixes in + * {@link Modifier#apply}. + * + *

+ * In addition to being a Modifier, this class contains the business logic for substituting the correct locale symbols + * into the affixes of the decimal format pattern. + * + *

+ * In order to use this class, create a new instance and call the following four setters: {@link #setPatternInfo}, + * {@link #setPatternAttributes}, {@link #setSymbols}, and {@link #setNumberProperties}. After calling these four + * setters, the instance will be ready for use as a Modifier. + * + *

+ * 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! Instead, you can obtain a safe, immutable decimal format pattern modifier by calling + * {@link MurkyModifier#createImmutable}, in effect treating this instance as a builder for the immutable variant. + * + * FIXME: Make this package-private + */ +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; + + /** + * @param isStrong + * Whether the modifier should be considered strong. For more information, see + * {@link Modifier#isStrong()}. Most of the time, decimal format pattern modifiers should be considered + * as non-strong. + */ + public MurkyModifier(boolean isStrong) { + this.isStrong = isStrong; + } + + /** + * Sets a reference to the parsed decimal format pattern, usually obtained from + * {@link LdmlPatternInfo#parse(String)}, but any implementation of {@link AffixPatternProvider} is accepted. + */ + public void setPatternInfo(AffixPatternProvider patternInfo) { + this.patternInfo = patternInfo; + } + + /** + * Sets attributes that imply changes to the literal interpretation of the pattern string affixes. + * + * @param signDisplay + * Whether to force a plus sign on positive numbers. + * @param perMille + * Whether to substitute the percent sign in the pattern with a permille sign. + */ + public void setPatternAttributes(SignDisplay signDisplay, boolean perMille) { + this.signDisplay = signDisplay; + this.perMilleReplacesPercent = perMille; + } + + /** + * Sets locale-specific details that affect the symbols substituted into the pattern string affixes. + * + * @param symbols + * The desired instance of DecimalFormatSymbols. + * @param currency + * The currency to be used when substituting currency values into the affixes. Cannot be null, but a + * bogus currency like "XXX" can be used. + * @param unitWidth + * The width used to render currencies. + * @param rules + * Required if the triple currency sign, "¤¤¤", appears in the pattern, which can be determined from the + * convenience method {@link #needsPlurals()}. + */ + 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); + } + } + } + + /** + * Sets attributes of the current number being processed. + * + * @param isNegative + * Whether the number is negative. + * @param plural + * The plural form of the number, required only if the pattern contains the triple currency sign, "¤¤¤" + * (and as indicated by {@link #needsPlurals()}). + */ + public void setNumberProperties(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); + } + + /** + * Creates a new quantity-dependent Modifier that behaves the same as the current instance, but which is immutable + * and can be saved for future use. The number properties in the current instance are mutated; all other properties + * are left untouched. + * + *

+ * The resulting modifier cannot be used in a QuantityChain. + * + * @return An immutable that supports both positive and negative numbers. + */ + public ImmutableMurkyModifier createImmutable() { + return createImmutableAndChain(null); + } + + /** + * Creates a new quantity-dependent Modifier that behaves the same as the current instance, but which is immutable + * and can be saved for future use. The number properties in the current instance are mutated; all other properties + * are left untouched. + * + * @param parent + * The QuantityChain to which to chain this immutable. + * @return An immutable that supports both positive and negative numbers. + */ + public ImmutableMurkyModifier createImmutableAndChain(QuantityChain parent) { + 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, parent); + } 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, parent); + } + } + + 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, QuantityChain parent) { + this.positive = positive; + this.negative = negative; + this.parent = parent; + } + + @Override + public MicroProps withQuantity(FormatQuantity quantity) { + assert parent != null; + 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, QuantityChain parent) { + assert mods.length == getModsLength(); + assert rules != null; + this.mods = mods; + this.rules = rules; + this.parent = parent; + } + + 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 MicroProps withQuantity(FormatQuantity quantity) { + assert parent != null; + 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; + } + } + + public QuantityChain addToChain(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; + } + + @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 + && 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; + } 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/Notation.java b/icu4j/main/classes/core/src/newapi/Notation.java new file mode 100644 index 00000000000..0487baad666 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/Notation.java @@ -0,0 +1,40 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; + +import newapi.NumberFormatter.SignDisplay; + +public class Notation { + + // FIXME: Support engineering intervals other than 3? + private static final ScientificNotation SCIENTIFIC = new ScientificNotation(1, false, 1, SignDisplay.AUTO); + private static final ScientificNotation ENGINEERING = new ScientificNotation(3, false, 1, SignDisplay.AUTO); + private static final CompactNotation COMPACT_SHORT = new CompactNotation(CompactStyle.SHORT); + private static final CompactNotation COMPACT_LONG = new CompactNotation(CompactStyle.LONG); + private static final SimpleNotation SIMPLE = new SimpleNotation(); + + /* package-private */ Notation() { + } + + public static ScientificNotation scientific() { + return SCIENTIFIC; + } + + public static ScientificNotation engineering() { + return ENGINEERING; + } + + public static CompactNotation compactShort() { + return COMPACT_SHORT; + } + + public static CompactNotation compactLong() { + return COMPACT_LONG; + } + + public static SimpleNotation simple() { + return SIMPLE; + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/NumberFormatter.java b/icu4j/main/classes/core/src/newapi/NumberFormatter.java index 80f7138f54c..c3abf489e40 100644 --- a/icu4j/main/classes/core/src/newapi/NumberFormatter.java +++ b/icu4j/main/classes/core/src/newapi/NumberFormatter.java @@ -2,53 +2,14 @@ // 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.FormatQuantity; -import com.ibm.icu.impl.number.NumberStringBuilder; -import com.ibm.icu.impl.number.ThingsNeedingNewHome.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); - } + private static final UnlocalizedNumberFormatter BASE = new UnlocalizedNumberFormatter(); // This could possibly be combined into MeasureFormat.FormatWidth public static enum CurrencyDisplay { @@ -71,626 +32,20 @@ public final class NumberFormatter { NEVER, } - 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(); + return BASE; } public static LocalizedNumberFormatter withLocale(Locale locale) { - return NumberFormatterImpl.with().locale(locale); + return BASE.locale(locale); } public static LocalizedNumberFormatter withLocale(ULocale locale) { - return NumberFormatterImpl.with().locale(locale); - } - - public static class NumberFormatterResult { - NumberStringBuilder nsb; - FormatQuantity fq; - MicroProps micros; - - /** - * @internal - * @deprecated This API is ICU internal only. - */ - @Deprecated - public NumberFormatterResult(NumberStringBuilder nsb, FormatQuantity 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 { - - // FIXME - /** @internal */ - public 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 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) { - // TODO: Validate the code point - if (targetWidth >= 0) { - String paddingString = String.valueOf(Character.toChars(cp)); - return PaddingImpl.getInstance(paddingString, targetWidth, position); - } else { - throw new IllegalArgumentException("Padding width must not be negative"); - } - } - - // 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) { - if (minInt >= 0 && minInt < Rounding.MAX_VALUE) { - return new IntegerWidthImpl(minInt, Integer.MAX_VALUE); - } else { - throw new IllegalArgumentException( - "Integer digits must be between 0 and " + Rounding.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 {} + return BASE.locale(locale); } } diff --git a/icu4j/main/classes/core/src/newapi/NumberFormatterSettings.java b/icu4j/main/classes/core/src/newapi/NumberFormatterSettings.java new file mode 100644 index 00000000000..112a3092935 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/NumberFormatterSettings.java @@ -0,0 +1,209 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.text.MeasureFormat.FormatWidth; +import com.ibm.icu.text.NumberingSystem; +import com.ibm.icu.util.MeasureUnit; +import com.ibm.icu.util.ULocale; + +import newapi.NumberFormatter.DecimalMarkDisplay; +import newapi.NumberFormatter.SignDisplay; +import newapi.impl.MacroProps; +import newapi.impl.Padder; + +public abstract class NumberFormatterSettings> { + + 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_ROUNDER = 4; + static final int KEY_GROUPER = 5; + static final int KEY_PADDER = 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_THRESHOLD = 12; + static final int KEY_MAX = 13; + + final NumberFormatterSettings parent; + final int key; + final Object value; + volatile MacroProps resolvedMacros; + + NumberFormatterSettings(NumberFormatterSettings parent, int key, Object value) { + this.parent = parent; + this.key = key; + this.value = value; + } + + public T notation(Notation notation) { + return create(KEY_NOTATION, notation); + } + + public T unit(MeasureUnit unit) { + return create(KEY_UNIT, unit); + } + + public T rounding(Rounder rounder) { + return create(KEY_ROUNDER, rounder); + } + + public T grouping(Grouper grouper) { + return create(KEY_GROUPER, grouper); + } + + public T integerWidth(IntegerWidth style) { + return create(KEY_INTEGER, style); + } + + public T symbols(DecimalFormatSymbols symbols) { + return create(KEY_SYMBOLS, symbols); + } + + public T symbols(NumberingSystem ns) { + return create(KEY_SYMBOLS, ns); + } + + public T unitWidth(FormatWidth style) { + return create(KEY_UNIT_WIDTH, style); + } + + public T sign(SignDisplay style) { + return create(KEY_SIGN, style); + } + + public T decimal(DecimalMarkDisplay style) { + return create(KEY_DECIMAL, style); + } + + /** Internal method to set a starting macros. */ + public T macros(MacroProps macros) { + return create(KEY_MACROS, macros); + } + + /** Non-public method */ + public T padding(Padder padder) { + return create(KEY_PADDER, padder); + } + + /** + * Internal fluent setter to support 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 T threshold(Long threshold) { + return create(KEY_THRESHOLD, threshold); + } + + public String toSkeleton() { + return SkeletonBuilder.macrosToSkeleton(resolve()); + } + + abstract T create(int key, Object value); + + 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(); + NumberFormatterSettings current = this; + while (current != null) { + 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_ROUNDER: + if (macros.rounder == null) { + macros.rounder = (Rounder) current.value; + } + break; + case KEY_GROUPER: + if (macros.grouper == null) { + macros.grouper = (Grouper) current.value; + } + break; + case KEY_PADDER: + if (macros.padder == null) { + macros.padder = (Padder) 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; + case KEY_THRESHOLD: + if (macros.threshold == null) { + macros.threshold = (Long) current.value; + } + break; + default: + throw new AssertionError("Unknown key: " + current.key); + } + current = current.parent; + } + resolvedMacros = macros; + return macros; + } + + @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 NumberFormatterSettings)) { + return false; + } + return resolve().equals(((NumberFormatterSettings) other).resolve()); + } +} diff --git a/icu4j/main/classes/core/src/newapi/NumberPropertyMapper.java b/icu4j/main/classes/core/src/newapi/NumberPropertyMapper.java new file mode 100644 index 00000000000..274667ba3b7 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/NumberPropertyMapper.java @@ -0,0 +1,454 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +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.PatternString; +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.DecimalMarkDisplay; +import newapi.NumberFormatter.SignDisplay; +import newapi.Rounder.FractionRounderImpl; +import newapi.Rounder.IncrementRounderImpl; +import newapi.Rounder.SignificantRounderImpl; +import newapi.impl.AffixPatternProvider; +import newapi.impl.CustomSymbolCurrency; +import newapi.impl.MacroProps; +import newapi.impl.MultiplierImpl; +import newapi.impl.Padder; + +/** @author sffc */ +public final class NumberPropertyMapper { + + /** Convenience method to create a NumberFormatter directly from Properties. */ + public static UnlocalizedNumberFormatter create(Properties properties, DecimalFormatSymbols symbols) { + MacroProps macros = oldToNew(properties, symbols, null); + return NumberFormatter.with().macros(macros); + } + + /** + * Convenience method to create a NumberFormatter directly from a pattern string. Something like this could become + * public API if there is demand. + */ + public static UnlocalizedNumberFormatter create(String pattern, DecimalFormatSymbols symbols) { + Properties properties = PatternString.parseToProperties(pattern); + return create(properties, symbols); + } + + /** + * 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 != null; + 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 != -1 || maxFrac != -1; + boolean explicitMinMaxSig = minSig != -1 || maxSig != -1; + // 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 ? -1 : 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 ? -1 : maxInt < minInt ? minInt : maxInt; + } + Rounder rounding = null; + if (explicitCurrencyUsage) { + rounding = Rounder.constructCurrency(currencyUsage).withCurrency(currency); + } else if (roundingIncrement != null) { + rounding = Rounder.constructIncrement(roundingIncrement); + } else if (explicitMinMaxSig) { + minSig = minSig < 1 ? 1 : minSig > 1000 ? 1000 : minSig; + maxSig = maxSig < 0 ? 1000 : maxSig < minSig ? minSig : maxSig > 1000 ? 1000 : maxSig; + rounding = Rounder.constructSignificant(minSig, maxSig); + } else if (explicitMinMaxFrac) { + rounding = Rounder.constructFraction(minFrac, maxFrac); + } else if (useCurrency) { + rounding = Rounder.constructCurrency(currencyUsage); + } + if (rounding != null) { + rounding = rounding.withMode(mathContext); + macros.rounder = 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.grouper = Grouper.getInstance((byte) grouping1, (byte) grouping2, minGrouping == 2); + + ///////////// + // PADDING // + ///////////// + + if (properties.getFormatWidth() != -1) { + macros.padder = new Padder(properties.getPadString(), properties.getFormatWidth(), + properties.getPadPosition()); + } + + /////////////////////////////// + // DECIMAL MARK ALWAYS SHOWN // + /////////////////////////////// + + macros.decimal = properties.getDecimalSeparatorAlwaysShown() ? DecimalMarkDisplay.ALWAYS + : DecimalMarkDisplay.AUTO; + + /////////////////////// + // SIGN ALWAYS SHOWN // + /////////////////////// + + macros.sign = properties.getSignAlwaysShown() ? SignDisplay.ALWAYS : SignDisplay.AUTO; + + ///////////////////////// + // SCIENTIFIC NOTATION // + ///////////////////////// + + if (properties.getMinimumExponentDigits() != -1) { + // 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 != -1) ? maxInt : properties.getMaximumIntegerDigits(); + engineering = (engineering < 0) ? 0 : (engineering > 8) ? minInt : engineering; + // Bug #13289: if maxInt > minInt > 1, then minInt should be 1. + // Clear out IntegerWidth to prevent padding extra zeros. + if (maxInt > minInt && minInt > 1) { + macros.integerWidth = null; + } + macros.notation = new ScientificNotation( + // 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 : SignDisplay.AUTO); + // Scientific notation also involves overriding the rounding mode. + // TODO: Overriding here is a bit of a hack. Should this logic go earlier? + if (macros.rounder instanceof FractionRounder) { + 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.rounder = Rounder.constructInfinite().withMode(mathContext); + } else if (minInt_ == 0 && minFrac_ == 0) { + // Patterns like "#.##E0" (no zeros in the mantissa), which mean round to maxFrac+1 + macros.rounder = Rounder.constructSignificant(1, maxFrac_ + 1).withMode(mathContext); + } else { + // All other scientific patterns, which mean round to minInt+maxFrac + macros.rounder = Rounder.constructSignificant(minInt + minFrac, minInt + maxFrac) + .withMode(mathContext); + } + } + } + + ////////////////////// + // COMPACT NOTATION // + ////////////////////// + + if (properties.getCompactStyle() != null) { + if (properties.getCompactCustomData() != null) { + macros.notation = new CompactNotation(properties.getCompactCustomData()); + } else if (properties.getCompactStyle() == CompactStyle.LONG) { + macros.notation = Notation.compactLong(); + } else { + macros.notation = Notation.compactShort(); + } + // Do not forward the affix provider. + macros.affixProvider = null; + } + + ///////////////// + // MULTIPLIERS // + ///////////////// + + if (properties.getMagnitudeMultiplier() != 0) { + macros.multiplier = new MultiplierImpl(properties.getMagnitudeMultiplier()); + } else if (properties.getMultiplier() != null) { + macros.multiplier = new MultiplierImpl(properties.getMultiplier()); + } + + ////////////////////// + // PROPERTY EXPORTS // + ////////////////////// + + if (exportedProperties != null) { + + exportedProperties.setMathContext(mathContext); + exportedProperties.setRoundingMode(mathContext.getRoundingMode()); + exportedProperties.setMinimumIntegerDigits(minInt); + exportedProperties.setMaximumIntegerDigits(maxInt == -1 ? Integer.MAX_VALUE : maxInt); + + Rounder rounding_; + if (rounding instanceof CurrencyRounder) { + rounding_ = ((CurrencyRounder) rounding).withCurrency(currency); + } else { + rounding_ = rounding; + } + int minFrac_ = minFrac; + int maxFrac_ = maxFrac; + int minSig_ = minSig; + int maxSig_ = maxSig; + BigDecimal increment_ = null; + if (rounding_ instanceof FractionRounderImpl) { + minFrac_ = ((FractionRounderImpl) rounding_).minFrac; + maxFrac_ = ((FractionRounderImpl) rounding_).maxFrac; + } else if (rounding_ instanceof IncrementRounderImpl) { + increment_ = ((IncrementRounderImpl) rounding_).increment; + minFrac_ = increment_.scale(); + maxFrac_ = increment_.scale(); + } else if (rounding_ instanceof SignificantRounderImpl) { + minSig_ = ((SignificantRounderImpl) rounding_).minSig; + maxSig_ = ((SignificantRounderImpl) 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/Rounder.java b/icu4j/main/classes/core/src/newapi/Rounder.java new file mode 100644 index 00000000000..5a6e74d6b2f --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/Rounder.java @@ -0,0 +1,446 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +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.RoundingUtils; +import com.ibm.icu.util.Currency; +import com.ibm.icu.util.Currency.CurrencyUsage; + +import newapi.impl.MultiplierProducer; + +public abstract class Rounder implements Cloneable { + + // FIXME + /** @internal */ + public static final int MAX_VALUE = 100; + + /* package-private final */ MathContext mathContext; + + /* package-private */ Rounder() { + mathContext = RoundingUtils.mathContextUnlimited(RoundingMode.HALF_EVEN); + } + + public static Rounder none() { + return constructInfinite(); + } + + public static FractionRounder integer() { + return constructFraction(0, 0); + } + + public static FractionRounder fixedFraction(int minMaxFrac) { + if (minMaxFrac >= 0 && minMaxFrac <= MAX_VALUE) { + return constructFraction(minMaxFrac, minMaxFrac); + } else { + throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE); + } + } + + public static FractionRounder minFraction(int minFrac) { + if (minFrac >= 0 && minFrac < MAX_VALUE) { + return constructFraction(minFrac, -1); + } else { + throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE); + } + } + + public static FractionRounder maxFraction(int maxFrac) { + if (maxFrac >= 0 && maxFrac < MAX_VALUE) { + return constructFraction(0, maxFrac); + } else { + throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE); + } + } + + public static FractionRounder minMaxFraction(int minFrac, int maxFrac) { + if (minFrac >= 0 && maxFrac <= MAX_VALUE && minFrac <= maxFrac) { + return constructFraction(minFrac, maxFrac); + } else { + throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE); + } + } + + public static Rounder fixedFigures(int minMaxSig) { + if (minMaxSig > 0 && minMaxSig <= MAX_VALUE) { + return constructSignificant(minMaxSig, minMaxSig); + } else { + throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE); + } + } + + public static Rounder minFigures(int minSig) { + if (minSig > 0 && minSig <= MAX_VALUE) { + return constructSignificant(minSig, -1); + } else { + throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE); + } + } + + public static Rounder maxFigures(int maxSig) { + if (maxSig > 0 && maxSig <= MAX_VALUE) { + return constructSignificant(0, maxSig); + } else { + throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE); + } + } + + public static Rounder minMaxFigures(int minSig, int maxSig) { + if (minSig > 0 && maxSig <= MAX_VALUE && minSig <= maxSig) { + return constructSignificant(minSig, maxSig); + } else { + throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE); + } + } + + public static Rounder increment(BigDecimal roundingIncrement) { + if (roundingIncrement != null && roundingIncrement.compareTo(BigDecimal.ZERO) > 0) { + return constructIncrement(roundingIncrement); + } else { + throw new IllegalArgumentException("Rounding increment must be positive and non-null"); + } + } + + public static CurrencyRounder currency(CurrencyUsage currencyUsage) { + if (currencyUsage != null) { + return constructCurrency(currencyUsage); + } else { + throw new IllegalArgumentException("CurrencyUsage must be non-null"); + } + } + + /** + * 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 Rounder withMode(RoundingMode roundingMode) { + return withMode(RoundingUtils.mathContextUnlimited(roundingMode)); + } + + /** + * Sets a MathContext directly instead of RoundingMode. + * + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public Rounder withMode(MathContext mathContext) { + if (this.mathContext.equals(mathContext)) { + return this; + } + Rounder other = (Rounder) this.clone(); + other.mathContext = mathContext; + return other; + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + // Should not happen since parent is Object + throw new AssertionError(e); + } + } + + ////////////////////////// + // PACKAGE-PRIVATE APIS // + ////////////////////////// + + private static final InfiniteRounderImpl NONE = new InfiniteRounderImpl(); + + private static final FractionRounderImpl FIXED_FRAC_0 = new FractionRounderImpl(0, 0); + private static final FractionRounderImpl FIXED_FRAC_2 = new FractionRounderImpl(2, 2); + + private static final SignificantRounderImpl FIXED_SIG_2 = new SignificantRounderImpl(2, 2); + private static final SignificantRounderImpl FIXED_SIG_3 = new SignificantRounderImpl(3, 3); + private static final SignificantRounderImpl RANGE_SIG_2_3 = new SignificantRounderImpl(2, 3); + + /* package-private */ static final FracSigRounderImpl COMPACT_STRATEGY = new FracSigRounderImpl(0, 0, 2, -1); + + private static final IncrementRounderImpl NICKEL = new IncrementRounderImpl(BigDecimal.valueOf(0.5)); + + private static final CurrencyRounderImpl MONETARY_STANDARD = new CurrencyRounderImpl(CurrencyUsage.STANDARD); + private static final CurrencyRounderImpl MONETARY_CASH = new CurrencyRounderImpl(CurrencyUsage.CASH); + + private static final PassThroughRounderImpl PASS_THROUGH = new PassThroughRounderImpl(); + + static Rounder constructInfinite() { + return NONE; + } + + static FractionRounder constructFraction(int minFrac, int maxFrac) { + if (minFrac == 0 && maxFrac == 0) { + return FIXED_FRAC_0; + } else if (minFrac == 2 && maxFrac == 2) { + return FIXED_FRAC_2; + } else { + return new FractionRounderImpl(minFrac, maxFrac); + } + } + + /** Assumes that minSig <= maxSig. */ + static Rounder constructSignificant(int minSig, int maxSig) { + if (minSig == 2 && maxSig == 2) { + return FIXED_SIG_2; + } else if (minSig == 3 && maxSig == 3) { + return FIXED_SIG_3; + } else if (minSig == 2 && maxSig == 3) { + return RANGE_SIG_2_3; + } else { + return new SignificantRounderImpl(minSig, maxSig); + } + } + + static Rounder constructFractionSignificant(FractionRounder base_, int minSig, int maxSig) { + assert base_ instanceof FractionRounderImpl; + FractionRounderImpl base = (FractionRounderImpl) base_; + if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 2 /* && maxSig == -1 */) { + return COMPACT_STRATEGY; + } else { + return new FracSigRounderImpl(base.minFrac, base.maxFrac, minSig, maxSig); + } + } + + static Rounder constructIncrement(BigDecimal increment) { + if (increment.compareTo(NICKEL.increment) == 0) { + return NICKEL; + } else { + return new IncrementRounderImpl(increment); + } + } + + static CurrencyRounder constructCurrency(CurrencyUsage usage) { + if (usage == CurrencyUsage.STANDARD) { + return MONETARY_STANDARD; + } else if (usage == CurrencyUsage.CASH) { + return MONETARY_CASH; + } else { + throw new AssertionError(); + } + } + + static Rounder constructFromCurrency(CurrencyRounder base_, Currency currency) { + assert base_ instanceof CurrencyRounderImpl; + CurrencyRounderImpl base = (CurrencyRounderImpl) base_; + double incrementDouble = currency.getRoundingIncrement(base.usage); + if (incrementDouble != 0.0) { + BigDecimal increment = BigDecimal.valueOf(incrementDouble); + return constructIncrement(increment); + } else { + int minMaxFrac = currency.getDefaultFractionDigits(base.usage); + return constructFraction(minMaxFrac, minMaxFrac); + } + } + + static Rounder constructPassThrough() { + return PASS_THROUGH; + } + + /** + * Returns a valid working Rounder. If the Rounder is a CurrencyRounder, applies the given currency. Otherwise, + * simply passes through the argument. + * + * @param rounder + * The input object. + * @param currency + * A currency object to use in case the input object needs it. + * @return A Rounder object ready for use. + */ + static Rounder normalizeType(Rounder rounder, Currency currency) { + if (rounder instanceof CurrencyRounder) { + return ((CurrencyRounder) rounder).withCurrency(currency); + } else { + return rounder; + } + } + + abstract void apply(FormatQuantity value); + + 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; + } + + /////////////// + // INTERNALS // + /////////////// + + static class InfiniteRounderImpl extends Rounder { + + private InfiniteRounderImpl() { + } + + @Override + void apply(FormatQuantity value) { + value.roundToInfinity(); + value.setFractionLength(0, Integer.MAX_VALUE); + } + } + + static class FractionRounderImpl extends FractionRounder { + final int minFrac; + final int maxFrac; + + private FractionRounderImpl(int minFrac, int maxFrac) { + this.minFrac = minFrac; + this.maxFrac = maxFrac; + } + + @Override + void apply(FormatQuantity value) { + value.roundToMagnitude(getRoundingMagnitudeFraction(maxFrac), mathContext); + value.setFractionLength(Math.max(0, -getDisplayMagnitudeFraction(minFrac)), Integer.MAX_VALUE); + } + } + + static class SignificantRounderImpl extends Rounder { + final int minSig; + final int maxSig; + + private SignificantRounderImpl(int minSig, int maxSig) { + this.minSig = minSig; + this.maxSig = maxSig; + } + + @Override + void apply(FormatQuantity value) { + value.roundToMagnitude(getRoundingMagnitudeSignificant(value, maxSig), mathContext); + value.setFractionLength(Math.max(0, -getDisplayMagnitudeSignificant(value, minSig)), Integer.MAX_VALUE); + } + + /** Version of {@link #apply} that obeys minInt constraints. Used for scientific notation compatibility mode. */ + public void apply(FormatQuantity quantity, int minInt) { + assert quantity.isZero(); + quantity.setFractionLength(minSig - minInt, Integer.MAX_VALUE); + } + } + + static class FracSigRounderImpl extends Rounder { + final int minFrac; + final int maxFrac; + final int minSig; + final int maxSig; + + private FracSigRounderImpl(int minFrac, int maxFrac, int minSig, int maxSig) { + this.minFrac = minFrac; + this.maxFrac = maxFrac; + this.minSig = minSig; + this.maxSig = maxSig; + } + + @Override + void apply(FormatQuantity value) { + int displayMag = getDisplayMagnitudeFraction(minFrac); + int roundingMag = getRoundingMagnitudeFraction(maxFrac); + if (minSig == -1) { + // Max Sig override + int candidate = getRoundingMagnitudeSignificant(value, maxSig); + roundingMag = Math.max(roundingMag, candidate); + } else { + // Min Sig override + int candidate = getDisplayMagnitudeSignificant(value, minSig); + roundingMag = Math.min(roundingMag, candidate); + } + value.roundToMagnitude(roundingMag, mathContext); + value.setFractionLength(Math.max(0, -displayMag), Integer.MAX_VALUE); + } + } + + static class IncrementRounderImpl extends Rounder { + final BigDecimal increment; + + private IncrementRounderImpl(BigDecimal increment) { + this.increment = increment; + } + + @Override + void apply(FormatQuantity value) { + value.roundToIncrement(increment, mathContext); + value.setFractionLength(increment.scale(), increment.scale()); + } + } + + static class CurrencyRounderImpl extends CurrencyRounder { + final CurrencyUsage usage; + + private CurrencyRounderImpl(CurrencyUsage usage) { + this.usage = usage; + } + + @Override + void apply(FormatQuantity value) { + // Call .withCurrency() before .apply()! + throw new AssertionError(); + } + } + + static class PassThroughRounderImpl extends Rounder { + + private PassThroughRounderImpl() { + } + + @Override + void apply(FormatQuantity value) { + // TODO: Assert that value has already been rounded + } + } + + private static int getRoundingMagnitudeFraction(int maxFrac) { + if (maxFrac == -1) { + return Integer.MIN_VALUE; + } + return -maxFrac; + } + + private static int getRoundingMagnitudeSignificant(FormatQuantity value, int maxSig) { + if (maxSig == -1) { + return Integer.MIN_VALUE; + } + int magnitude = value.isZero() ? 0 : value.getMagnitude(); + return magnitude - maxSig + 1; + } + + private static int getDisplayMagnitudeFraction(int minFrac) { + if (minFrac == 0) { + return Integer.MAX_VALUE; + } + return -minFrac; + } + + private static int getDisplayMagnitudeSignificant(FormatQuantity value, int minSig) { + int magnitude = value.isZero() ? 0 : value.getMagnitude(); + return magnitude - minSig + 1; + } +} diff --git a/icu4j/main/classes/core/src/newapi/ScientificNotation.java b/icu4j/main/classes/core/src/newapi/ScientificNotation.java new file mode 100644 index 00000000000..d3a6ea1faf6 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/ScientificNotation.java @@ -0,0 +1,218 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +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.Rounder.SignificantRounderImpl; +import newapi.impl.MicroProps; +import newapi.impl.MultiplierProducer; +import newapi.impl.QuantityChain; + +@SuppressWarnings("unused") +public class ScientificNotation extends Notation implements Cloneable { + + int engineeringInterval; + boolean requireMinInt; + int minExponentDigits; + SignDisplay exponentSignDisplay; + + /* package-private */ ScientificNotation(int engineeringInterval, boolean requireMinInt, int minExponentDigits, + SignDisplay exponentSignDisplay) { + this.engineeringInterval = engineeringInterval; + this.requireMinInt = requireMinInt; + this.minExponentDigits = minExponentDigits; + this.exponentSignDisplay = exponentSignDisplay; + } + + public ScientificNotation withMinExponentDigits(int minExponentDigits) { + if (minExponentDigits >= 0 && minExponentDigits < Rounder.MAX_VALUE) { + ScientificNotation other = (ScientificNotation) this.clone(); + other.minExponentDigits = minExponentDigits; + return other; + } else { + throw new IllegalArgumentException("Integer digits must be between 0 and " + Rounder.MAX_VALUE); + } + } + + public ScientificNotation withExponentSignDisplay(SignDisplay exponentSignDisplay) { + ScientificNotation other = (ScientificNotation) 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); + } + } + + /* package-private */ QuantityChain withLocaleData(DecimalFormatSymbols symbols, boolean build, + QuantityChain parent) { + return new MurkyScientificHandler(symbols, build, parent); + } + + private class MurkyScientificHandler implements QuantityChain, MultiplierProducer, Modifier { + + final DecimalFormatSymbols symbols; + final ImmutableScientificModifier[] precomputedMods; + final QuantityChain parent; + /* unsafe */ int exponent; + + private MurkyScientificHandler(DecimalFormatSymbols symbols, boolean safe, QuantityChain parent) { + this.symbols = symbols; + this.parent = parent; + + if (safe) { + // Pre-build the modifiers for exponents -12 through 12 + precomputedMods = new ImmutableScientificModifier[25]; + for (int i = -12; i <= 12; i++) { + precomputedMods[i + 12] = new ImmutableScientificModifier(i); + } + } else { + precomputedMods = null; + } + } + + @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 (requireMinInt && micros.rounding instanceof SignificantRounderImpl) { + // Show "00.000E0" on pattern "00.000E0" + ((SignificantRounderImpl) micros.rounding).apply(quantity, 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) { + // Safe code path A + micros.modInner = precomputedMods[exponent + 12]; + } else if (precomputedMods != null) { + // Safe code path B + micros.modInner = new ImmutableScientificModifier(exponent); + } else { + // Unsafe code path: mutates the object and re-uses it as a Modifier! + this.exponent = exponent; + micros.modInner = this; + } + + // We already performed rounding. Do not perform it again. + micros.rounding = Rounder.constructPassThrough(); + + return micros; + } + + @Override + public int getMultiplier(int magnitude) { + int interval = engineeringInterval; + int digitsShown; + if (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; + } + + @Override + public boolean isStrong() { + return true; + } + + @Override + public String getPrefix() { + // FIXME: Localized exponent separator location. + return ""; + } + + @Override + public String getSuffix() { + // FIXME: Localized exponent separator location. + NumberStringBuilder temp = new NumberStringBuilder(); + doApply(exponent, temp, 0); + return temp.toString(); + } + + @Override + public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { + return doApply(exponent, output, rightIndex); + } + + private int doApply(int exponent, NumberStringBuilder output, 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 && exponentSignDisplay != SignDisplay.NEVER) { + i += output.insert(i, symbols.getMinusSignString(), NumberFormat.Field.EXPONENT_SIGN); + } else if (exponentSignDisplay == SignDisplay.ALWAYS) { + 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 < 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; + } + + private class ImmutableScientificModifier implements Modifier { + final int exponent; + + ImmutableScientificModifier(int exponent) { + this.exponent = exponent; + } + + @Override + public boolean isStrong() { + return true; + } + + @Override + public String getPrefix() { + // FIXME: Localized exponent separator location. + return ""; + } + + @Override + public String getSuffix() { + // FIXME: Localized exponent separator location. + NumberStringBuilder temp = new NumberStringBuilder(); + doApply(exponent, temp, 0); + return temp.toString(); + } + + @Override + public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { + return doApply(exponent, output, rightIndex); + } + } + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/SimpleNotation.java b/icu4j/main/classes/core/src/newapi/SimpleNotation.java new file mode 100644 index 00000000000..8c72cb427f1 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/SimpleNotation.java @@ -0,0 +1,8 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +public class SimpleNotation extends Notation { + /* package-private */ SimpleNotation() { + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/SkeletonBuilder.java b/icu4j/main/classes/core/src/newapi/SkeletonBuilder.java new file mode 100644 index 00000000000..bc77e212793 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/SkeletonBuilder.java @@ -0,0 +1,586 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +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.SignDisplay; +import newapi.Rounder.CurrencyRounderImpl; +import newapi.Rounder.FracSigRounderImpl; +import newapi.Rounder.FractionRounderImpl; +import newapi.Rounder.IncrementRounderImpl; +import newapi.Rounder.InfiniteRounderImpl; +import newapi.Rounder.SignificantRounderImpl; +import newapi.impl.MacroProps; + +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.rounder != null) { + // sb.append("rounding="); + rounderToSkeleton(macros.rounder, sb); + sb.append(' '); + } + if (macros.grouper != null) { + sb.append("grouping="); + grouperToSkeleton(macros.grouper, sb); + sb.append(' '); + } +// if (macros.padder != null) { +// sb.append("padding="); +// paddingToSkeleton(macros.padder, 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 ScientificNotation) { + ScientificNotation notation = (ScientificNotation) value; + sb.append('E'); + if (notation.engineeringInterval != 1) { + sb.append(notation.engineeringInterval); + } + if (notation.exponentSignDisplay == SignDisplay.ALWAYS) { + sb.append('+'); + } else if (notation.exponentSignDisplay == SignDisplay.NEVER) { + 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 CompactNotation) { + CompactNotation notation = (CompactNotation) value; + if (notation.compactStyle == CompactStyle.SHORT) { + sb.append('C'); + } else { + // FIXME: CCC or CCCC instead? + sb.append("CC"); + } + } else { + assert value instanceof SimpleNotation; + 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; + c = safeCharAt(skeleton, offset++); + } + if (c == '!') { + sign = SignDisplay.NEVER; + c = safeCharAt(skeleton, offset++); + } + while (c == '0') { + minExponentDigits++; + c = safeCharAt(skeleton, offset++); + } + minExponentDigits = Math.max(1, minExponentDigits); + result = new ScientificNotation(engineering, false, minExponentDigits, sign); + } else if (c0 == 'C') { + char c = safeCharAt(skeleton, offset++); + if (c == 'C') { + result = Notation.compactLong(); + } else { + result = Notation.compactShort(); + } + } 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 rounderToSkeleton(Rounder value, StringBuilder sb) { + if (!(value instanceof Rounder)) { + // FIXME: Throw an exception here instead? + return; + } + MathContext mathContext; + if (value instanceof FractionRounderImpl) { + FractionRounderImpl rounder = (FractionRounderImpl) value; + sb.append('F'); + minMaxToSkeletonHelper(rounder.minFrac, rounder.maxFrac, sb); + mathContext = rounder.mathContext; + } else if (value instanceof SignificantRounderImpl) { + SignificantRounderImpl rounder = (SignificantRounderImpl) value; + sb.append('S'); + minMaxToSkeletonHelper(rounder.minSig, rounder.maxSig, sb); + mathContext = rounder.mathContext; + } else if (value instanceof FracSigRounderImpl) { + FracSigRounderImpl rounder = (FracSigRounderImpl) value; + sb.append('F'); + minMaxToSkeletonHelper(rounder.minFrac, rounder.maxFrac, sb); + if (rounder.minSig != -1) { + sb.append('>'); + sb.append(rounder.minSig); + } else { + sb.append('<'); + sb.append(rounder.maxSig); + } + mathContext = rounder.mathContext; + } else if (value instanceof IncrementRounderImpl) { + IncrementRounderImpl rounder = (IncrementRounderImpl) value; + sb.append('M'); + sb.append(rounder.increment.toString()); + mathContext = rounder.mathContext; + } else if (value instanceof CurrencyRounderImpl) { + CurrencyRounderImpl rounder = (CurrencyRounderImpl) value; + sb.append('G'); + sb.append(rounder.usage.name()); + mathContext = rounder.mathContext; + } else { + InfiniteRounderImpl rounder = (InfiniteRounderImpl) value; + sb.append('Y'); + mathContext = rounder.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++); + Rounder result = null; + if (c0 == 'F') { + int[] minMax = new int[2]; + offset += skeletonToMinMaxHelper(skeleton, offset, minMax); + FractionRounder temp = Rounder.constructFraction(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 = Rounder.constructSignificant(minMax[0], minMax[1]); + } else if (c0 == 'M') { + StringBuilder sb = new StringBuilder(); + offset += consumeUntil(skeleton, offset, ' ', sb); + BigDecimal increment = new BigDecimal(sb.toString()); + result = Rounder.constructIncrement(increment); + } else if (c0 == 'G') { + StringBuilder sb = new StringBuilder(); + offset += consumeUntil(skeleton, offset, ' ', sb); + CurrencyUsage usage = Enum.valueOf(CurrencyUsage.class, sb.toString()); + result = Rounder.constructCurrency(usage); + } else if (c0 == 'Y') { + result = Rounder.constructInfinite(); + } + output.rounder = 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 grouperToSkeleton(Grouper value, StringBuilder sb) { + if (value.equals(Grouper.defaults())) { + sb.append("defaults"); + } else if (value.equals(Grouper.min2())) { + sb.append("min2"); + } else if (value.equals(Grouper.none())) { + sb.append("none"); + } else { + // Not supported in skeleton string + sb.append("defaults"); + } + } + + private static int skeletonToGrouping(String skeleton, int offset, MacroProps output) { + int originalOffset = offset; + char c0 = skeleton.charAt(offset++); + Grouper result = null; + StringBuilder sb = new StringBuilder(); + offset += consumeUntil(skeleton, --offset, ' ', sb); + String name = sb.toString(); + if (name.equals("defaults")) { + result = Grouper.defaults(); + } else if (name.equals("min2")) { + result = Grouper.min2(); + } else if (name.equals("none")) { + result = Grouper.none(); + } + output.grouper = result; + return offset - originalOffset; + } + +// private static void paddingToSkeleton(Padder value, StringBuilder sb) { +// PaddingImpl padding = (PaddingImpl) value; +// if (padding == Padder.NONE) { +// sb.append("NONE"); +// return; +// } +// sb.append(padding.targetWidth); +// sb.append(':'); +// sb.append(padding.position.name()); +// sb.append(':'); +// if (!padding.paddingString.equals(" ")) { +// sb.append(padding.paddingString); +// } +// } +// +// private static int skeletonToPadding(String skeleton, int offset, MacroProps output) { +// int originalOffset = offset; +// char c0 = skeleton.charAt(offset++); +// if (c0 == 'N') { +// offset += consumeUntil(skeleton, --offset, ' ', null); +// } else if ('0' <= c0 && c0 <= '9') { +// long intResult = consumeInt(skeleton, --offset); +// offset += intResult & 0xffffffff; +// int width = (int) (intResult >>> 32); +// char c1 = safeCharAt(skeleton, offset++); +// if (c1 != ':') { +// return offset - originalOffset - 1; +// } +// StringBuilder sb = new StringBuilder(); +// offset += consumeUntil(skeleton, offset, ':', sb); +// String padPositionString = sb.toString(); +// sb.setLength(0); +// offset += consumeUntil(skeleton, offset, ' ', sb); +// String string = (sb.length() == 0) ? " " : sb.toString(); +// PadPosition position = Enum.valueOf(PadPosition.class, padPositionString); +// output.padder = PaddingImpl.getInstance(string, width, position); +// } +// return offset - originalOffset; +// } + + private static void integerWidthToSkeleton(IntegerWidth value, StringBuilder sb) { + sb.append(value.minInt); + if (value.maxInt != value.minInt) { + sb.append('-'); + if (value.maxInt != -1) { + sb.append(value.maxInt); + } + } + } + + private static int skeletonToIntegerWidth(String skeleton, int offset, MacroProps output) { + int originalOffset = offset; + long intResult = consumeInt(skeleton, offset); + offset += intResult & 0xffffffff; + int minInt = (int) (intResult >>> 32); + char c1 = safeCharAt(skeleton, --offset); + int maxInt; + if (c1 == '-') { + intResult = consumeInt(skeleton, offset); + offset += intResult & 0xffffffff; + maxInt = (int) (intResult >>> 32); + } + } + + 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 int skeletonToUnitWidth(String skeleton, int offset, MacroProps output) { + int originalOffset = offset; + StringBuilder sb = new StringBuilder(); + offset += consumeUntil(skeleton, offset, ' ', sb); + output.unitWidth = Enum.valueOf(FormatWidth.class, sb.toString()); + return offset - originalOffset; + } + + private static void signToSkeleton(SignDisplay value, StringBuilder sb) { + sb.append(value.name()); + } + + private static int skeletonToSign(String skeleton, int offset, MacroProps output) { + int originalOffset = offset; + StringBuilder sb = new StringBuilder(); + offset += consumeUntil(skeleton, offset, ' ', sb); + output.sign = Enum.valueOf(SignDisplay.class, sb.toString()); + return offset - originalOffset; + } + + private static void decimalToSkeleton(DecimalMarkDisplay value, StringBuilder sb) { + sb.append(value.name()); + } + + private static int skeletonToDecimal(String skeleton, int offset, MacroProps output) { + int originalOffset = offset; + StringBuilder sb = new StringBuilder(); + offset += consumeUntil(skeleton, offset, ' ', sb); + output.decimal = Enum.valueOf(DecimalMarkDisplay.class, sb.toString()); + return offset - originalOffset; + } + + 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) { + if (sb != null) + sb.append(c); + c = safeCharAt(skeleton, offset++); + } + return offset - originalOffset; + } + + private static long consumeInt(String skeleton, int offset) { + int originalOffset = offset; + char c = safeCharAt(skeleton, offset++); + int result = 0; + while ('0' <= c && c <= '9') { + result = (result * 10) + (c - '0'); + c = safeCharAt(skeleton, offset++); + } + return (offset - originalOffset) | (((long) result) << 32); + } +} diff --git a/icu4j/main/classes/core/src/newapi/UnlocalizedNumberFormatter.java b/icu4j/main/classes/core/src/newapi/UnlocalizedNumberFormatter.java new file mode 100644 index 00000000000..fcfa7f99930 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/UnlocalizedNumberFormatter.java @@ -0,0 +1,32 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +import java.util.Locale; + +import com.ibm.icu.util.ULocale; + +public class UnlocalizedNumberFormatter extends NumberFormatterSettings { + + /** Base constructor; called during startup only. Sets the threshold to the default value of 3. */ + UnlocalizedNumberFormatter() { + super(null, KEY_THRESHOLD, new Long(3)); + } + + UnlocalizedNumberFormatter(NumberFormatterSettings parent, int key, Object value) { + super(parent, key, value); + } + + public LocalizedNumberFormatter locale(Locale locale) { + return new LocalizedNumberFormatter(this, KEY_LOCALE, ULocale.forLocale(locale)); + } + + public LocalizedNumberFormatter locale(ULocale locale) { + return new LocalizedNumberFormatter(this, KEY_LOCALE, locale); + } + + @Override + protected UnlocalizedNumberFormatter create(int key, Object value) { + return new UnlocalizedNumberFormatter(this, key, value); + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/Worker1.java b/icu4j/main/classes/core/src/newapi/Worker1.java new file mode 100644 index 00000000000..1453d7fc3d5 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/Worker1.java @@ -0,0 +1,299 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi; + +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.NumberStringBuilder; +import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier; +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.SignDisplay; +import newapi.impl.MacroProps; +import newapi.impl.MicroProps; +import newapi.impl.Padder; +import newapi.impl.QuantityChain; + +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; + MurkyLongNameHandler longNames = null; + Rounder defaultRounding = Rounder.none(); + Currency currency = DEFAULT_CURRENCY; + FormatWidth unitWidth = null; + boolean perMille = false; + PluralRules rules = input.rules; + + MicroProps micros = new MicroProps(build); + 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 ? IntegerWidth.zeroFillTo(1) : 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 = Rounder.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); + longNames = MurkyLongNameHandler.getCurrencyLongNameModifiers(input.loc, (Currency) input.unit); + defaultRounding = Rounder.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; + longNames = MurkyLongNameHandler.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) { + chain = input.multiplier.copyAndChain(chain); + } + + // Rounding strategy + if (input.rounder != null) { + micros.rounding = Rounder.normalizeType(input.rounder, currency); + } else if (input.notation instanceof CompactNotation) { + micros.rounding = Rounder.COMPACT_STRATEGY; + } else { + micros.rounding = Rounder.normalizeType(defaultRounding, currency); + } + + // Grouping strategy + if (input.grouper != null) { + micros.grouping = Grouper.normalizeType(input.grouper, patternInfo); + } else if (input.notation instanceof CompactNotation) { + // Compact notation uses minGrouping by default since ICU 59 + micros.grouping = Grouper.normalizeType(Grouper.min2(), patternInfo); + } else { + micros.grouping = Grouper.normalizeType(Grouper.defaults(), patternInfo); + } + + // Inner modifier (scientific notation) + if (input.notation instanceof ScientificNotation) { + chain = ((ScientificNotation) input.notation).withLocaleData(micros.symbols, build, chain); + } else { + // No inner modifier required + micros.modInner = ConstantAffixModifier.EMPTY; + } + + // Middle modifier (patterns, positive/negative, currency symbols, percent) + // The default middle modifier is weak (thus the false argument). + 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.createImmutableAndChain(chain); + } else { + chain = murkyMod.addToChain(chain); + } + + // Outer modifier (CLDR units and currency long names) + if (longNames != null) { + if (rules == null) { + // Lazily create PluralRules + rules = PluralRules.forLocale(input.loc); + } + chain = longNames.withLocaleData(rules, build, chain); + } else { + // No outer modifier required + micros.modOuter = ConstantAffixModifier.EMPTY; + } + + // Padding strategy + if (input.padder != null) { + micros.padding = input.padder; + } else { + micros.padding = Padder.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 CompactNotation) { + if (rules == null) { + // Lazily create PluralRules + rules = PluralRules.forLocale(input.loc); + } + CompactType compactType = (input.unit instanceof Currency) ? CompactType.CURRENCY : CompactType.DECIMAL; + chain = ((CompactNotation) input.notation).withLocaleData(input.loc, compactType, rules, + build ? murkyMod : null, chain); + } + + return chain; + } + + ////////// + + private static int applyStatic(MicroProps micros, FormatQuantity inValue, NumberStringBuilder outString) { + inValue.adjustMagnitude(micros.multiplier); + micros.rounding.apply(inValue); + if (micros.integerWidth.maxInt == -1) { + inValue.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE); + } else { + inValue.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt); + } + int length = writeNumber(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; + } + + private static int writeNumber(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 += writeIntegerDigits(micros, input, string); + + // Add the decimal point + if (input.getLowerDisplayMagnitude() < 0 || micros.decimal == DecimalMarkDisplay.ALWAYS) { + length += string.insert(length, micros.useCurrency ? micros.symbols.getMonetaryDecimalSeparatorString() + : micros.symbols.getDecimalSeparatorString(), NumberFormat.Field.DECIMAL_SEPARATOR); + } + + // Add the fraction digits + length += writeFractionDigits(micros, input, string); + } + + return length; + } + + private static int writeIntegerDigits(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 writeFractionDigits(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; + } +} diff --git a/icu4j/main/classes/core/src/newapi/impl/CompactData.java b/icu4j/main/classes/core/src/newapi/impl/CompactData.java index f18ce450072..f2fb71d92d8 100644 --- a/icu4j/main/classes/core/src/newapi/impl/CompactData.java +++ b/icu4j/main/classes/core/src/newapi/impl/CompactData.java @@ -18,7 +18,7 @@ import com.ibm.icu.text.NumberingSystem; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.UResourceBundle; -class CompactData implements RoundingImpl.MultiplierProducer { +public class CompactData implements MultiplierProducer { public static CompactData getInstance( ULocale locale, CompactType compactType, CompactStyle compactStyle) { diff --git a/icu4j/main/classes/core/src/newapi/impl/CompactImpl.java b/icu4j/main/classes/core/src/newapi/impl/CompactImpl.java deleted file mode 100644 index fe7de892d93..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/CompactImpl.java +++ /dev/null @@ -1,125 +0,0 @@ -// © 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, - MurkyModifier buildReference, - QuantityChain parent) { - CompactData data = CompactData.getInstance(dataLocale, compactType, compactStyle); - return new CompactImpl(data, rules, buildReference, parent); - } - - public static CompactImpl getInstance( - Map> compactCustomData, - PluralRules rules, - MurkyModifier buildReference, - QuantityChain parent) { - CompactData data = CompactData.getInstance(compactCustomData); - return new CompactImpl(data, rules, buildReference, parent); - } - - private CompactImpl( - CompactData data, PluralRules rules, MurkyModifier buildReference, QuantityChain parent) { - this.data = data; - this.rules = rules; - if (buildReference != null) { - precomputedMods = precomputeAllModifiers(data, buildReference); - } else { - precomputedMods = null; - } - this.parent = parent; - } - - /** To be used by the building code path */ - public static Map precomputeAllModifiers( - CompactData data, MurkyModifier buildReference) { - Map precomputedMods = new HashMap(); - Set allPatterns = data.getAllPatterns(); - for (String patternString : allPatterns) { - CompactModInfo info = new CompactModInfo(); - PatternParseResult patternInfo = LdmlPatternInfo.parse(patternString); - buildReference.setPatternInfo(patternInfo); - info.mod = buildReference.createImmutable(); - info.numDigits = patternInfo.positive.totalIntegerDigits; - precomputedMods.put(patternString, info); - } - return precomputedMods; - } - - private static class CompactModInfo { - public ImmutableMurkyModifier mod; - public int numDigits; - } - - @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/DataUtils.java b/icu4j/main/classes/core/src/newapi/impl/DataUtils.java deleted file mode 100644 index f505d01780a..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/DataUtils.java +++ /dev/null @@ -1,67 +0,0 @@ -// © 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 deleted file mode 100644 index 8ad25cc62d4..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/GroupingImpl.java +++ /dev/null @@ -1,135 +0,0 @@ -// © 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 byte B2 = 2; - private static final byte B3 = 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(B3, B3, false); - static final GroupingImpl GROUPING_3_2 = new GroupingImpl(B3, B2, false); - static final GroupingImpl GROUPING_3_MIN2 = new GroupingImpl(B3, B3, true); - static final GroupingImpl GROUPING_3_2_MIN2 = new GroupingImpl(B3, B2, true); - - static GroupingImpl getInstance(byte grouping1, byte 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 byte grouping1; // -2 means "needs locale data"; -1 means "no grouping" - final byte 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(byte grouping1, byte 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; - // TODO: short or byte? - byte grouping1 = (byte) (patternInfo.positive.groupingSizes & 0xffff); - byte grouping2 = (byte) ((patternInfo.positive.groupingSizes >>> 16) & 0xffff); - byte grouping3 = (byte) ((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 deleted file mode 100644 index 8c8695e1b17..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/IntegerWidthImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -// © 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; -import newapi.NumberFormatter.Rounding; - -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) { - if (maxInt == this.maxInt) { - return this; - } else if (maxInt >= 0 && maxInt < Rounding.MAX_VALUE) { - return new IntegerWidthImpl(minInt, maxInt); - } else if (maxInt == Integer.MAX_VALUE) { - return new IntegerWidthImpl(minInt, maxInt); - } else { - throw new IllegalArgumentException( - "Integer digits must be between 0 and " + Rounding.MAX_VALUE); - } - } -} diff --git a/icu4j/main/classes/core/src/newapi/impl/MacroProps.java b/icu4j/main/classes/core/src/newapi/impl/MacroProps.java index 16a11d709bc..e135116b0f7 100644 --- a/icu4j/main/classes/core/src/newapi/impl/MacroProps.java +++ b/icu4j/main/classes/core/src/newapi/impl/MacroProps.java @@ -9,20 +9,20 @@ import com.ibm.icu.text.PluralRules; import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.ULocale; +import newapi.Grouper; +import newapi.IntegerWidth; +import newapi.Notation; +import newapi.NumberFormatter; +import newapi.Rounder; 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 Rounder rounder; + public Grouper grouper; + public Padder padder; public IntegerWidth integerWidth; public Object symbols; public FormatWidth unitWidth; @@ -42,9 +42,9 @@ public class MacroProps implements Cloneable { 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 (rounder == null) rounder = fallback.rounder; + if (grouper == null) grouper = fallback.grouper; + if (padder == null) padder = fallback.padder; if (integerWidth == null) integerWidth = fallback.integerWidth; if (symbols == null) symbols = fallback.symbols; if (unitWidth == null) unitWidth = fallback.unitWidth; @@ -61,9 +61,9 @@ public class MacroProps implements Cloneable { return Objects.hash( notation, unit, - rounding, - grouping, - padding, + rounder, + grouper, + padder, integerWidth, symbols, unitWidth, @@ -80,9 +80,9 @@ public class MacroProps implements Cloneable { 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(rounder, other.rounder) + && Objects.equals(grouper, other.grouper) + && Objects.equals(padder, other.padder) && Objects.equals(integerWidth, other.integerWidth) && Objects.equals(symbols, other.symbols) && Objects.equals(unitWidth, other.unitWidth) diff --git a/icu4j/main/classes/core/src/newapi/impl/MicroProps.java b/icu4j/main/classes/core/src/newapi/impl/MicroProps.java index 6c57121f272..dc0aa88bdb2 100644 --- a/icu4j/main/classes/core/src/newapi/impl/MicroProps.java +++ b/icu4j/main/classes/core/src/newapi/impl/MicroProps.java @@ -6,6 +6,10 @@ import com.ibm.icu.impl.number.FormatQuantity; import com.ibm.icu.impl.number.Modifier; import com.ibm.icu.text.DecimalFormatSymbols; +import newapi.Grouper; +import newapi.IntegerWidth; +import newapi.NumberFormatter; +import newapi.Rounder; import newapi.NumberFormatter.DecimalMarkDisplay; import newapi.NumberFormatter.SignDisplay; @@ -13,16 +17,16 @@ public class MicroProps implements Cloneable, QuantityChain { // Populated globally: public SignDisplay sign; public DecimalFormatSymbols symbols; - public PaddingImpl padding; + public Padder padding; public DecimalMarkDisplay decimal; - public IntegerWidthImpl integerWidth; + public IntegerWidth integerWidth; // Populated by notation/unit: public Modifier modOuter; public Modifier modMiddle; public Modifier modInner; - public RoundingImpl rounding; - public GroupingImpl grouping; + public Rounder rounding; + public Grouper grouping; public int multiplier; public boolean useCurrency; diff --git a/icu4j/main/classes/core/src/newapi/impl/MultiplierProducer.java b/icu4j/main/classes/core/src/newapi/impl/MultiplierProducer.java new file mode 100644 index 00000000000..814ceaabf25 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/MultiplierProducer.java @@ -0,0 +1,7 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi.impl; + +public interface MultiplierProducer { + int getMultiplier(int magnitude); +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/impl/MurkyModifier.java b/icu4j/main/classes/core/src/newapi/impl/MurkyModifier.java deleted file mode 100644 index 27159e47e75..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/MurkyModifier.java +++ /dev/null @@ -1,465 +0,0 @@ -// © 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.LdmlPatternInfo; -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 class is a {@link Modifier} that wraps a decimal format pattern. It applies the pattern's - * affixes in {@link Modifier#apply}. - * - *

In addition to being a Modifier, this class contains the business logic for substituting the - * correct locale symbols into the affixes of the decimal format pattern. - * - *

In order to use this class, create a new instance and call the following four setters: {@link - * #setPatternInfo}, {@link #setPatternAttributes}, {@link #setSymbols}, and {@link - * #setNumberProperties}. After calling these four setters, the instance will be ready for use as a - * Modifier. - * - *

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! Instead, you can obtain a safe, immutable - * decimal format pattern modifier by calling {@link MurkyModifier#createImmutable}, in effect - * treating this instance as a builder for the immutable variant. - */ -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; - - /** - * @param isStrong Whether the modifier should be considered strong. For more information, see - * {@link Modifier#isStrong()}. Most of the time, decimal format pattern modifiers should be - * considered as non-strong. - */ - public MurkyModifier(boolean isStrong) { - this.isStrong = isStrong; - } - - /** - * Sets a reference to the parsed decimal format pattern, usually obtained from {@link - * LdmlPatternInfo#parse(String)}, but any implementation of {@link AffixPatternProvider} is - * accepted. - */ - public void setPatternInfo(AffixPatternProvider patternInfo) { - this.patternInfo = patternInfo; - } - - /** - * Sets attributes that imply changes to the literal interpretation of the pattern string affixes. - * - * @param signDisplay Whether to force a plus sign on positive numbers. - * @param perMille Whether to substitute the percent sign in the pattern with a permille sign. - */ - public void setPatternAttributes(SignDisplay signDisplay, boolean perMille) { - this.signDisplay = signDisplay; - this.perMilleReplacesPercent = perMille; - } - - /** - * Sets locale-specific details that affect the symbols substituted into the pattern string - * affixes. - * - * @param symbols The desired instance of DecimalFormatSymbols. - * @param currency The currency to be used when substituting currency values into the affixes. - * Cannot be null, but a bogus currency like "XXX" can be used. - * @param unitWidth The width used to render currencies. - * @param rules Required if the triple currency sign, "¤¤¤", appears in the pattern, which can be - * determined from the convenience method {@link #needsPlurals()}. - */ - 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); - } - } - } - - /** - * Sets attributes of the current number being processed. - * - * @param isNegative Whether the number is negative. - * @param plural The plural form of the number, required only if the pattern contains the triple - * currency sign, "¤¤¤" (and as indicated by {@link #needsPlurals()}). - */ - public void setNumberProperties(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); - } - - /** - * Creates a new quantity-dependent Modifier that behaves the same as the current instance, but - * which is immutable and can be saved for future use. The number properties in the current instance - * are mutated; all other properties are left untouched. - * - *

The resulting modifier cannot be used in a QuantityChain. - * - * @return An immutable that supports both positive and negative numbers. - */ - public ImmutableMurkyModifier createImmutable() { - return createImmutableAndChain(null); - } - - /** - * Creates a new quantity-dependent Modifier that behaves the same as the current instance, but - * which is immutable and can be saved for future use. The number properties in the current instance - * are mutated; all other properties are left untouched. - * - * @param parent The QuantityChain to which to chain this immutable. - * @return An immutable that supports both positive and negative numbers. - */ - public ImmutableMurkyModifier createImmutableAndChain(QuantityChain parent) { - 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, parent); - } 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, parent); - } - } - - 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, QuantityChain parent) { - this.positive = positive; - this.negative = negative; - this.parent = parent; - } - - @Override - public MicroProps withQuantity(FormatQuantity quantity) { - assert parent != null; - 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, QuantityChain parent) { - assert mods.length == getModsLength(); - assert rules != null; - this.mods = mods; - this.rules = rules; - this.parent = parent; - } - - 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 MicroProps withQuantity(FormatQuantity quantity) { - assert parent != null; - 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; - } - } - - public QuantityChain addToChain(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; - } - - @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 - && 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; - } 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 deleted file mode 100644 index 67ddc090dca..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/NotationImpl.java +++ /dev/null @@ -1,87 +0,0 @@ -// © 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.Rounding; -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) { - if (minExponentDigits >= 0 && minExponentDigits < Rounding.MAX_VALUE) { - NotationScientificImpl other = (NotationScientificImpl) this.clone(); - other.minExponentDigits = minExponentDigits; - return other; - } else { - throw new IllegalArgumentException( - "Integer digits must be between 0 and " + Rounding.MAX_VALUE); - } - } - - @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 deleted file mode 100644 index f772ed2492d..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/NumberFormatterImpl.java +++ /dev/null @@ -1,330 +0,0 @@ -// © 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.AtomicLongFieldUpdater; - -import com.ibm.icu.impl.number.FormatQuantity; -import com.ibm.icu.impl.number.FormatQuantity4; -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(); - - 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_THRESHOLD = 12; - static final int KEY_MAX = 13; - - 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); - } - - static final AtomicLongFieldUpdater callCount = - AtomicLongFieldUpdater.newUpdater(NumberFormatterImpl.class, "callCountInternal"); - - // TODO: Reduce the number of fields. - final NumberFormatterImpl parent; - final int key; - final Object value; - volatile MacroProps resolvedMacros; - volatile long callCountInternal; // do not access directly; use callCount instead - volatile NumberFormatterImpl savedWithUnit; - volatile Worker1 compiled; - - /** Base constructor; called during startup only. Sets the threshold to the default value of 3. */ - private NumberFormatterImpl() { - parent = null; - key = KEY_THRESHOLD; - value = new Long(3); - } - - /** 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); - } - - /** - * Internal fluent setter to support 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 NumberFormatterImpl threshold(Long threshold) { - return new NumberFormatterImpl(this, KEY_THRESHOLD, threshold); - } - - @Override - public String toSkeleton() { - return SkeletonBuilder.macrosToSkeleton(resolve()); - } - - @Override - public NumberFormatterResult format(long input) { - return format(new FormatQuantity4(input)); - } - - @Override - public NumberFormatterResult format(double input) { - return format(new FormatQuantity4(input)); - } - - @Override - public NumberFormatterResult format(Number input) { - return format(new FormatQuantity4(input)); - } - - @Override - public NumberFormatterResult format(Measure input) { - MeasureUnit unit = input.getUnit(); - Number number = input.getNumber(); - // Use this formatter if possible - if (Objects.equals(resolve().unit, unit)) { - return format(number); - } - // This mechanism saves the previously used unit, so if the user calls this method with the - // same unit multiple times in a row, they get a more efficient code path. - NumberFormatterImpl withUnit = savedWithUnit; - if (withUnit == null || !Objects.equals(withUnit.resolve().unit, unit)) { - withUnit = new NumberFormatterImpl(this, KEY_UNIT, unit); - savedWithUnit = withUnit; - } - return withUnit.format(number); - } - - /** - * This is the core entrypoint to the number formatting pipeline. It performs self-regulation: a - * static code path for the first few calls, and compiling a more efficient data structure if - * called repeatedly. - * - * @param fq The quantity to be formatted. - * @return The formatted number result. - */ - public NumberFormatterResult format(FormatQuantity fq) { - MacroProps macros = resolve(); - NumberStringBuilder string = new NumberStringBuilder(); - long currentCount = callCount.incrementAndGet(this); - MicroProps micros; - if (currentCount == macros.threshold.longValue()) { - compiled = Worker1.fromMacros(macros); - micros = compiled.apply(fq, string); - } else if (compiled != null) { - micros = compiled.apply(fq, string); - } else { - micros = Worker1.applyStatic(macros, 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 != null) { - 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; - case KEY_THRESHOLD: - if (macros.threshold == null) { - macros.threshold = (Long) current.value; - } - break; - default: - throw new AssertionError("Unknown key: " + current.key); - } - 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 deleted file mode 100644 index 81cf952960a..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/NumberPropertyMapper.java +++ /dev/null @@ -1,453 +0,0 @@ -// © 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 { - - /** Convenience method to create a NumberFormatterImpl directly. */ - public static NumberFormatterImpl create( - Properties properties, DecimalFormatSymbols symbols, ULocale uloc) { - MacroProps macros = oldToNew(properties, symbols, null); - return NumberFormatterImpl.fromMacros(macros).locale(uloc); - } - - /** - * 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 != null; - 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 != -1 || maxFrac != -1; - boolean explicitMinMaxSig = minSig != -1 || maxSig != -1; - // 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((byte) grouping1, (byte) grouping2, minGrouping == 2); - - ///////////// - // PADDING // - ///////////// - - if (properties.getFormatWidth() != -1) { - macros.padding = - PaddingImpl.getInstance( - properties.getPadString(), properties.getFormatWidth(), properties.getPadPosition()); - } - - /////////////////////////////// - // DECIMAL MARK ALWAYS SHOWN // - /////////////////////////////// - - macros.decimal = - properties.getDecimalSeparatorAlwaysShown() - ? DecimalMarkDisplay.ALWAYS - : DecimalMarkDisplay.AUTO; - - /////////////////////// - // SIGN ALWAYS SHOWN // - /////////////////////// - - macros.sign = properties.getSignAlwaysShown() ? SignDisplay.ALWAYS : SignDisplay.AUTO; - - ///////////////////////// - // SCIENTIFIC NOTATION // - ///////////////////////// - - if (properties.getMinimumExponentDigits() != -1) { - // 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; - // Bug #13289: if maxInt > minInt > 1, then minInt should be 1. - // Clear out IntegerWidth to prevent padding extra zeros. - if (maxInt > minInt && minInt > 1) { - macros.integerWidth = null; - } - 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 : 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() != null) { - 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() != 0) { - macros.multiplier = new MultiplierImpl(properties.getMagnitudeMultiplier()); - } else if (properties.getMultiplier() != null) { - 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/Padder.java b/icu4j/main/classes/core/src/newapi/impl/Padder.java new file mode 100644 index 00000000000..faacd505926 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/Padder.java @@ -0,0 +1,107 @@ +// © 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.ThingsNeedingNewHome.PadPosition; + +public class Padder { + + private static final Padder NONE = new Padder(null, -1, null); + + String paddingString; + int targetWidth; + PadPosition position; + + public Padder(String paddingString, int targetWidth, PadPosition position) { + // TODO: Add a few default instances + this.paddingString = (paddingString == null) ? " " : paddingString; + this.targetWidth = targetWidth; + this.position = (position == null) ? PadPosition.BEFORE_PREFIX : position; + } + + public static Padder none() { + return NONE; + } + + public static Padder codePoints(int cp, int targetWidth, PadPosition position) { + // TODO: Validate the code point + if (targetWidth >= 0) { + String paddingString = String.valueOf(Character.toChars(cp)); + return new Padder(paddingString, targetWidth, position); + } else { + throw new IllegalArgumentException("Padding width must not be negative"); + } + } + + 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; + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/impl/PaddingImpl.java b/icu4j/main/classes/core/src/newapi/impl/PaddingImpl.java deleted file mode 100644 index 6700d871820..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/PaddingImpl.java +++ /dev/null @@ -1,109 +0,0 @@ -// © 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.ThingsNeedingNewHome.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 deleted file mode 100644 index 01df495c477..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/PositiveDecimalImpl.java +++ /dev/null @@ -1,95 +0,0 @@ -// © 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.FormatQuantity; -import com.ibm.icu.impl.number.NumberStringBuilder; -import com.ibm.icu.text.NumberFormat; - -import newapi.NumberFormatter.DecimalMarkDisplay; - -public class PositiveDecimalImpl { - - 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) { - 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; - } -} diff --git a/icu4j/main/classes/core/src/newapi/impl/QuantityDependentModOuter.java b/icu4j/main/classes/core/src/newapi/impl/QuantityDependentModOuter.java deleted file mode 100644 index 1b544b90dfa..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/QuantityDependentModOuter.java +++ /dev/null @@ -1,32 +0,0 @@ -// © 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, QuantityChain parent) { - this.data = data; - this.rules = rules; - this.parent = parent; - } - - @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 deleted file mode 100644 index 30f204cd484..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/RoundingImpl.java +++ /dev/null @@ -1,445 +0,0 @@ -// © 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 deleted file mode 100644 index 526b50a4281..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/ScientificImpl.java +++ /dev/null @@ -1,145 +0,0 @@ -// © 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, - QuantityChain parent) { - return new ScientificImpl(notation, symbols, build, parent); - } - - private ScientificImpl( - NotationImpl.NotationScientificImpl notation, - DecimalFormatSymbols symbols, - boolean build, - QuantityChain parent) { - this.notation = notation; - this.symbols = symbols; - this.parent = parent; - - 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 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) { - i += output.insert(i, symbols.getMinusSignString(), NumberFormat.Field.EXPONENT_SIGN); - } else if (notation.exponentSignDisplay == SignDisplay.ALWAYS) { - 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 deleted file mode 100644 index 8663f715483..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/SkeletonBuilder.java +++ /dev/null @@ -1,624 +0,0 @@ -// © 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.ThingsNeedingNewHome.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.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) { - sb.append('+'); - } else if (notation.exponentSignDisplay == SignDisplay.NEVER) { - 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; - c = safeCharAt(skeleton, offset++); - } - if (c == '!') { - sign = SignDisplay.NEVER; - 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.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((byte) (c0 - '0'), (byte) (c2 - '0'), c3 == '&'); - } else { - result = GroupingImpl.getInstance((byte) (c0 - '0'), (byte) (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.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(padding.targetWidth); - sb.append(':'); - sb.append(padding.position.name()); - sb.append(':'); - if (!padding.paddingString.equals(" ")) { - sb.append(padding.paddingString); - } - } - - private static int skeletonToPadding(String skeleton, int offset, MacroProps output) { - int originalOffset = offset; - char c0 = skeleton.charAt(offset++); - if (c0 == 'N') { - offset += consumeUntil(skeleton, --offset, ' ', null); - } else if ('0' <= c0 && c0 <= '9') { - long intResult = consumeInt(skeleton, --offset); - offset += intResult & 0xffffffff; - int width = (int) (intResult >>> 32); - char c1 = safeCharAt(skeleton, offset++); - if (c1 != ':') { - return offset - originalOffset - 1; - } - StringBuilder sb = new StringBuilder(); - offset += consumeUntil(skeleton, offset, ':', sb); - String padPositionString = sb.toString(); - sb.setLength(0); - offset += consumeUntil(skeleton, offset, ' ', sb); - String string = (sb.length() == 0) ? " " : sb.toString(); - PadPosition position = Enum.valueOf(PadPosition.class, padPositionString); - output.padding = PaddingImpl.getInstance(string, width, position); - } - return offset - originalOffset; - } - - 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 int skeletonToIntegerWidth(String skeleton, int offset, MacroProps output) { - int originalOffset = offset; - long intResult = consumeInt(skeleton, offset); - offset += intResult & 0xffffffff; - int minInt = (int) (intResult >>> 32); - char c1 = safeCharAt(skeleton, --offset); - int maxInt; - if (c1 == '-') { - intResult = consumeInt(skeleton, offset); - offset += intResult & 0xffffffff; - maxInt = (int) (intResult >>> 32); - } - } - - 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 int skeletonToUnitWidth(String skeleton, int offset, MacroProps output) { - int originalOffset = offset; - StringBuilder sb = new StringBuilder(); - offset += consumeUntil(skeleton, offset, ' ', sb); - output.unitWidth = Enum.valueOf(FormatWidth.class, sb.toString()); - return offset - originalOffset; - } - - private static void signToSkeleton(SignDisplay value, StringBuilder sb) { - sb.append(value.name()); - } - - private static int skeletonToSign(String skeleton, int offset, MacroProps output) { - int originalOffset = offset; - StringBuilder sb = new StringBuilder(); - offset += consumeUntil(skeleton, offset, ' ', sb); - output.sign = Enum.valueOf(SignDisplay.class, sb.toString()); - return offset - originalOffset; - } - - private static void decimalToSkeleton(DecimalMarkDisplay value, StringBuilder sb) { - sb.append(value.name()); - } - - private static int skeletonToDecimal(String skeleton, int offset, MacroProps output) { - int originalOffset = offset; - StringBuilder sb = new StringBuilder(); - offset += consumeUntil(skeleton, offset, ' ', sb); - output.decimal = Enum.valueOf(DecimalMarkDisplay.class, sb.toString()); - return offset - originalOffset; - } - - 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) { - if (sb != null) sb.append(c); - c = safeCharAt(skeleton, offset++); - } - return offset - originalOffset; - } - - private static long consumeInt(String skeleton, int offset) { - int originalOffset = offset; - char c = safeCharAt(skeleton, offset++); - int result = 0; - while ('0' <= c && c <= '9') { - result = (result * 10) + (c - '0'); - c = safeCharAt(skeleton, offset++); - } - return (offset - originalOffset) | (((long) result) << 32); - } -} diff --git a/icu4j/main/classes/core/src/newapi/impl/Worker1.java b/icu4j/main/classes/core/src/newapi/impl/Worker1.java deleted file mode 100644 index 99cdf87d177..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/Worker1.java +++ /dev/null @@ -1,258 +0,0 @@ -// © 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(build); - 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) { - chain = input.multiplier.copyAndChain(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.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); - } else { - // No inner modifier required - micros.modInner = ConstantAffixModifier.EMPTY; - } - - // Middle modifier (patterns, positive/negative, currency symbols, percent) - // The default middle modifier is weak (thus the false argument). - 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.createImmutableAndChain(chain); - } else { - chain = murkyMod.addToChain(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); - } 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; - if (compactStyle == null) { - // Use compact custom data - chain = - CompactImpl.getInstance( - ((NotationImpl.NotationCompactImpl) input.notation).compactCustomData, - rules, - build ? murkyMod : null, - chain); - } else { - CompactType compactType = - (input.unit instanceof Currency) ? CompactType.CURRENCY : CompactType.DECIMAL; - chain = - CompactImpl.getInstance( - input.loc, compactType, compactStyle, rules, build ? murkyMod : null, chain); - } - } - - 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/classes/core/src/newapi/demo.java b/icu4j/main/classes/core/src/newapi/impl/demo.java similarity index 71% rename from icu4j/main/classes/core/src/newapi/demo.java rename to icu4j/main/classes/core/src/newapi/impl/demo.java index 6cc79fe1d0b..3f8dcea922a 100644 --- a/icu4j/main/classes/core/src/newapi/demo.java +++ b/icu4j/main/classes/core/src/newapi/impl/demo.java @@ -1,6 +1,6 @@ // © 2017 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html#License -package newapi; +package newapi.impl; import java.math.RoundingMode; @@ -13,22 +13,23 @@ import com.ibm.icu.util.Dimensionless; import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.ULocale; +import newapi.Grouper; +import newapi.Notation; +import newapi.NumberFormatter; +import newapi.Rounder; +import newapi.UnlocalizedNumberFormatter; 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)) - .notation(Notation.ENGINEERING.withMinExponentDigits(2)) - .notation(Notation.SIMPLE) + .notation(Notation.compactShort()) + .notation(Notation.scientific().withExponentSignDisplay(SignDisplay.ALWAYS)) + .notation(Notation.engineering().withMinExponentDigits(2)) + .notation(Notation.simple()) .unit(Currency.getInstance("GBP")) .unit(Dimensionless.PERCENT) .unit(MeasureUnit.CUBIC_METER) @@ -38,16 +39,16 @@ public class demo { // (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)) + .rounding(Rounder.fixedFraction(2).withMode(RoundingMode.HALF_UP)) + .rounding(Rounder.integer().withMode(RoundingMode.CEILING)) + .rounding(Rounder.currency(CurrencyUsage.STANDARD)) // .grouping( // (int position, BigDecimal number) -> { // return (position % 3) == 0; // }) - .grouping(Grouping.DEFAULT) - .grouping(Grouping.NONE) - .grouping(Grouping.MIN_2_DIGITS) + .grouping(Grouper.defaults()) + .grouping(Grouper.none()) + .grouping(Grouper.min2()) // .padding(Padding.codePoints(' ', 8, PadPosition.AFTER_PREFIX)) .sign(SignDisplay.ALWAYS) .decimal(DecimalMarkDisplay.ALWAYS) diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java index 1c8dd8277ed..3374fbf498e 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java @@ -18,10 +18,8 @@ import com.ibm.icu.text.DecimalFormat_ICU58; import com.ibm.icu.util.CurrencyAmount; import com.ibm.icu.util.ULocale; -import newapi.NumberFormatter.LocalizedNumberFormatter; -import newapi.impl.MacroProps; -import newapi.impl.NumberFormatterImpl; -import newapi.impl.NumberPropertyMapper; +import newapi.LocalizedNumberFormatter; +import newapi.NumberPropertyMapper; public class NumberFormatDataDrivenTest { @@ -557,8 +555,7 @@ public class NumberFormatDataDrivenTest { : PatternString.IGNORE_ROUNDING_NEVER); propertiesFromTuple(tuple, properties); DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale); - MacroProps macros = NumberPropertyMapper.oldToNew(properties, symbols, null); - LocalizedNumberFormatter fmt = NumberFormatterImpl.fromMacros(macros).locale(locale); + LocalizedNumberFormatter fmt = NumberPropertyMapper.create(properties, symbols).locale(locale); Number number = toNumber(tuple.format); String expected = tuple.output; String actual = fmt.format(number).toString(); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/FormatQuantityTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/FormatQuantityTest.java index c92905b261f..6d1b3617504 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/FormatQuantityTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/FormatQuantityTest.java @@ -20,10 +20,11 @@ import com.ibm.icu.impl.number.FormatQuantity3; import com.ibm.icu.impl.number.FormatQuantity4; import com.ibm.icu.impl.number.Properties; import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; +import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.util.ULocale; -import newapi.impl.NumberFormatterImpl; -import newapi.impl.NumberPropertyMapper; +import newapi.LocalizedNumberFormatter; +import newapi.NumberPropertyMapper; /** TODO: This is a temporary name for this class. Suggestions for a better name? */ public class FormatQuantityTest extends TestFmwk { @@ -32,27 +33,29 @@ public class FormatQuantityTest extends TestFmwk { public void testBehavior() throws ParseException { // Make a list of several formatters to test the behavior of FormatQuantity. - List formats = new ArrayList(); + List formats = new ArrayList(); + + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(ULocale.ENGLISH); Properties properties = new Properties(); - formats.add(NumberPropertyMapper.create(properties, null, ULocale.ENGLISH)); + formats.add(NumberPropertyMapper.create(properties, symbols).locale(ULocale.ENGLISH)); properties = new Properties() .setMinimumSignificantDigits(3) .setMaximumSignificantDigits(3) .setCompactStyle(CompactStyle.LONG); - formats.add(NumberPropertyMapper.create(properties, null, ULocale.ENGLISH)); + formats.add(NumberPropertyMapper.create(properties, symbols).locale(ULocale.ENGLISH)); properties = new Properties() .setMinimumExponentDigits(1) .setMaximumIntegerDigits(3) .setMaximumFractionDigits(1); - formats.add(NumberPropertyMapper.create(properties, null, ULocale.ENGLISH)); + formats.add(NumberPropertyMapper.create(properties, symbols).locale(ULocale.ENGLISH)); properties = new Properties().setRoundingIncrement(new BigDecimal("0.5")); - formats.add(NumberPropertyMapper.create(properties, null, ULocale.ENGLISH)); + formats.add(NumberPropertyMapper.create(properties, symbols).locale(ULocale.ENGLISH)); String[] cases = { "1.0", @@ -110,7 +113,7 @@ public class FormatQuantityTest extends TestFmwk { } } - static void testFormatQuantity(int t, String str, List formats, int mode) { + static void testFormatQuantity(int t, String str, List formats, int mode) { if (mode == 2) { assertEquals("Double is not valid", Double.toString(Double.parseDouble(str)), str); } @@ -228,8 +231,8 @@ public class FormatQuantityTest extends TestFmwk { } private static void testFormatQuantityWithFormats( - FormatQuantity rq0, FormatQuantity rq1, List formats) { - for (NumberFormatterImpl format : formats) { + FormatQuantity rq0, FormatQuantity rq1, List formats) { + for (LocalizedNumberFormatter format : formats) { FormatQuantity q0 = rq0.createCopy(); FormatQuantity q1 = rq1.createCopy(); String s1 = format.format(q0).toString(); 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 index d544e6fb0b1..f9cfe3f9076 100644 --- 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 @@ -12,8 +12,8 @@ import com.ibm.icu.text.MeasureFormat.FormatWidth; import com.ibm.icu.util.Currency; import com.ibm.icu.util.ULocale; +import newapi.MurkyModifier; import newapi.NumberFormatter.SignDisplay; -import newapi.impl.MurkyModifier; public class MurkyModifierTest { 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 index 89cb4ac6497..5ba0e95756d 100644 --- 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 @@ -6,7 +6,6 @@ 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; @@ -24,19 +23,18 @@ import com.ibm.icu.util.Measure; import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.ULocale; +import newapi.FormattedNumber; +import newapi.Grouper; +import newapi.IntegerWidth; +import newapi.LocalizedNumberFormatter; +import newapi.Notation; 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; +import newapi.impl.Padder; +import newapi.NumberPropertyMapper; +import newapi.Rounder; +import newapi.UnlocalizedNumberFormatter; public class NumberFormatterTest { @@ -75,7 +73,7 @@ public class NumberFormatterTest { assertFormatDescending( "Scientific", "E", - NumberFormatter.with().notation(Notation.SCIENTIFIC), + NumberFormatter.with().notation(Notation.scientific()), ULocale.ENGLISH, "8.765E4", "8.765E3", @@ -90,7 +88,7 @@ public class NumberFormatterTest { assertFormatDescending( "Engineering", "E3", - NumberFormatter.with().notation(Notation.ENGINEERING), + NumberFormatter.with().notation(Notation.engineering()), ULocale.ENGLISH, "87.65E3", "8.765E3", @@ -106,7 +104,7 @@ public class NumberFormatterTest { "Scientific sign always shown", "E+", NumberFormatter.with() - .notation(Notation.SCIENTIFIC.withExponentSignDisplay(SignDisplay.ALWAYS)), + .notation(Notation.scientific().withExponentSignDisplay(SignDisplay.ALWAYS)), ULocale.ENGLISH, "8.765E+4", "8.765E+3", @@ -121,7 +119,7 @@ public class NumberFormatterTest { assertFormatDescending( "Scientific min exponent digits", "E00", - NumberFormatter.with().notation(Notation.SCIENTIFIC.withMinExponentDigits(2)), + NumberFormatter.with().notation(Notation.scientific().withMinExponentDigits(2)), ULocale.ENGLISH, "8.765E04", "8.765E03", @@ -136,7 +134,7 @@ public class NumberFormatterTest { assertFormatSingle( "Scientific Negative", "E", - NumberFormatter.with().notation(Notation.SCIENTIFIC), + NumberFormatter.with().notation(Notation.scientific()), ULocale.ENGLISH, -1000000, "-1E6"); @@ -147,7 +145,7 @@ public class NumberFormatterTest { assertFormatDescending( "Compact Short", "C", - NumberFormatter.with().notation(Notation.COMPACT_SHORT), + NumberFormatter.with().notation(Notation.compactShort()), ULocale.ENGLISH, "88K", "8.8K", @@ -162,7 +160,7 @@ public class NumberFormatterTest { assertFormatDescending( "Compact Long", "CC", - NumberFormatter.with().notation(Notation.COMPACT_LONG), + NumberFormatter.with().notation(Notation.compactLong()), ULocale.ENGLISH, "88 thousand", "8.8 thousand", @@ -177,7 +175,7 @@ public class NumberFormatterTest { assertFormatDescending( "Compact Short Currency", "C $USD", - NumberFormatter.with().notation(Notation.COMPACT_SHORT).unit(USD), + NumberFormatter.with().notation(Notation.compactShort()).unit(USD), ULocale.ENGLISH, "$88K", "$8.8K", @@ -193,7 +191,7 @@ public class NumberFormatterTest { assertFormatDescending( "Compact Long Currency", "CC $USD", - NumberFormatter.with().notation(Notation.COMPACT_LONG).unit(USD), + NumberFormatter.with().notation(Notation.compactLong()).unit(USD), ULocale.ENGLISH, "$88K", "$8.8K", @@ -208,7 +206,7 @@ public class NumberFormatterTest { assertFormatSingle( "Compact Plural One", "CC", - NumberFormatter.with().notation(Notation.COMPACT_LONG), + NumberFormatter.with().notation(Notation.compactLong()), ULocale.forLanguageTag("es"), 1000000, "1 millón"); @@ -216,7 +214,7 @@ public class NumberFormatterTest { assertFormatSingle( "Compact Plural Other", "CC", - NumberFormatter.with().notation(Notation.COMPACT_LONG), + NumberFormatter.with().notation(Notation.compactLong()), ULocale.forLanguageTag("es"), 2000000, "2 millones"); @@ -224,7 +222,7 @@ public class NumberFormatterTest { assertFormatSingle( "Compact with Negative Sign", "C", - NumberFormatter.with().notation(Notation.COMPACT_SHORT), + NumberFormatter.with().notation(Notation.compactShort()), ULocale.ENGLISH, -9876543.21, "-9.9M"); @@ -266,7 +264,7 @@ public class NumberFormatterTest { "Compact Meters Long", "CC U:length:meter unit-width=WIDE", NumberFormatter.with() - .notation(Notation.COMPACT_LONG) + .notation(Notation.compactLong()) .unit(MeasureUnit.METER) .unitWidth(FormatWidth.WIDE), ULocale.ENGLISH, @@ -354,8 +352,8 @@ public class NumberFormatterTest { 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), + "$GBP F0 grouping=none integer-width=1- symbols=loc:en_US sign=AUTO decimal=AUTO", + NumberPropertyMapper.create("0 ¤¤¤", DecimalFormatSymbols.getInstance()).unit(GBP), ULocale.ENGLISH, 1234567.89, "1234568 British pounds"); @@ -415,7 +413,7 @@ public class NumberFormatterTest { assertFormatDescending( "Integer", "F0", - NumberFormatter.with().rounding(Rounding.INTEGER), + NumberFormatter.with().rounding(Rounder.integer()), ULocale.ENGLISH, "87,650", "8,765", @@ -430,7 +428,7 @@ public class NumberFormatterTest { assertFormatDescending( "Fixed Fraction", "F3", - NumberFormatter.with().rounding(Rounding.fixedFraction(3)), + NumberFormatter.with().rounding(Rounder.fixedFraction(3)), ULocale.ENGLISH, "87,650.000", "8,765.000", @@ -445,7 +443,7 @@ public class NumberFormatterTest { assertFormatDescending( "Min Fraction", "F1-", - NumberFormatter.with().rounding(Rounding.minFraction(1)), + NumberFormatter.with().rounding(Rounder.minFraction(1)), ULocale.ENGLISH, "87,650.0", "8,765.0", @@ -460,7 +458,7 @@ public class NumberFormatterTest { assertFormatDescending( "Max Fraction", "F-1", - NumberFormatter.with().rounding(Rounding.maxFraction(1)), + NumberFormatter.with().rounding(Rounder.maxFraction(1)), ULocale.ENGLISH, "87,650", "8,765", @@ -475,7 +473,7 @@ public class NumberFormatterTest { assertFormatDescending( "Min/Max Fraction", "F1-3", - NumberFormatter.with().rounding(Rounding.minMaxFraction(1, 3)), + NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 3)), ULocale.ENGLISH, "87,650.0", "8,765.0", @@ -493,7 +491,7 @@ public class NumberFormatterTest { assertFormatSingle( "Fixed Significant", "S3", - NumberFormatter.with().rounding(Rounding.fixedFigures(3)), + NumberFormatter.with().rounding(Rounder.fixedFigures(3)), ULocale.ENGLISH, -98, "-98.0"); @@ -501,7 +499,7 @@ public class NumberFormatterTest { assertFormatSingle( "Fixed Significant Rounding", "S3", - NumberFormatter.with().rounding(Rounding.fixedFigures(3)), + NumberFormatter.with().rounding(Rounder.fixedFigures(3)), ULocale.ENGLISH, -98.7654321, "-98.8"); @@ -509,7 +507,7 @@ public class NumberFormatterTest { assertFormatSingle( "Fixed Significant Zero", "S3", - NumberFormatter.with().rounding(Rounding.fixedFigures(3)), + NumberFormatter.with().rounding(Rounder.fixedFigures(3)), ULocale.ENGLISH, 0, "0.00"); @@ -517,7 +515,7 @@ public class NumberFormatterTest { assertFormatSingle( "Min Significant", "S2-", - NumberFormatter.with().rounding(Rounding.minFigures(2)), + NumberFormatter.with().rounding(Rounder.minFigures(2)), ULocale.ENGLISH, -9, "-9.0"); @@ -525,7 +523,7 @@ public class NumberFormatterTest { assertFormatSingle( "Max Significant", "S-4", - NumberFormatter.with().rounding(Rounding.maxFigures(4)), + NumberFormatter.with().rounding(Rounder.maxFigures(4)), ULocale.ENGLISH, 98.7654321, "98.77"); @@ -533,7 +531,7 @@ public class NumberFormatterTest { assertFormatSingle( "Min/Max Significant", "S3-4", - NumberFormatter.with().rounding(Rounding.minMaxFigures(3, 4)), + NumberFormatter.with().rounding(Rounder.minMaxFigures(3, 4)), ULocale.ENGLISH, 9.99999, "10.0"); @@ -544,7 +542,7 @@ public class NumberFormatterTest { assertFormatDescending( "Basic Significant", // for comparison "S-2", - NumberFormatter.with().rounding(Rounding.maxFigures(2)), + NumberFormatter.with().rounding(Rounder.maxFigures(2)), ULocale.ENGLISH, "88,000", "8,800", @@ -559,7 +557,7 @@ public class NumberFormatterTest { assertFormatDescending( "FracSig minMaxFrac minSig", "F1-2>3", - NumberFormatter.with().rounding(Rounding.minMaxFraction(1, 2).withMinFigures(3)), + NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 2).withMinFigures(3)), ULocale.ENGLISH, "87,650.0", "8,765.0", @@ -574,7 +572,7 @@ public class NumberFormatterTest { assertFormatDescending( "FracSig minMaxFrac maxSig A", "F1-3<2", - NumberFormatter.with().rounding(Rounding.minMaxFraction(1, 3).withMaxFigures(2)), + NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 3).withMaxFigures(2)), ULocale.ENGLISH, "88,000.0", // maxSig beats maxFrac "8,800.0", // maxSig beats maxFrac @@ -589,7 +587,7 @@ public class NumberFormatterTest { assertFormatDescending( "FracSig minMaxFrac maxSig B", "F2<2", - NumberFormatter.with().rounding(Rounding.fixedFraction(2).withMaxFigures(2)), + NumberFormatter.with().rounding(Rounder.fixedFraction(2).withMaxFigures(2)), ULocale.ENGLISH, "88,000.00", // maxSig beats maxFrac "8,800.00", // maxSig beats maxFrac @@ -607,7 +605,7 @@ public class NumberFormatterTest { assertFormatDescending( "Rounding None", "Y", - NumberFormatter.with().rounding(Rounding.NONE), + NumberFormatter.with().rounding(Rounder.none()), ULocale.ENGLISH, "87,650", "8,765", @@ -622,7 +620,7 @@ public class NumberFormatterTest { assertFormatDescending( "Increment", "M0.5", - NumberFormatter.with().rounding(Rounding.increment(BigDecimal.valueOf(0.5))), + NumberFormatter.with().rounding(Rounder.increment(BigDecimal.valueOf(0.5))), ULocale.ENGLISH, "87,650.0", "8,765.0", @@ -637,7 +635,7 @@ public class NumberFormatterTest { assertFormatDescending( "Currency Standard", "$CZK GSTANDARD", - NumberFormatter.with().rounding(Rounding.currency(CurrencyUsage.STANDARD)).unit(CZK), + NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.STANDARD)).unit(CZK), ULocale.ENGLISH, "CZK 87,650.00", "CZK 8,765.00", @@ -652,7 +650,7 @@ public class NumberFormatterTest { assertFormatDescending( "Currency Cash", "$CZK GCASH", - NumberFormatter.with().rounding(Rounding.currency(CurrencyUsage.CASH)).unit(CZK), + NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.CASH)).unit(CZK), ULocale.ENGLISH, "CZK 87,650", "CZK 8,765", @@ -667,7 +665,7 @@ public class NumberFormatterTest { assertFormatDescending( "Currency not in top-level fluent chain", "F0", - NumberFormatter.with().rounding(Rounding.currency(CurrencyUsage.CASH).withCurrency(CZK)), + NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.CASH).withCurrency(CZK)), ULocale.ENGLISH, "87,650", "8,765", @@ -678,28 +676,6 @@ public class NumberFormatterTest { "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 @@ -708,8 +684,8 @@ public class NumberFormatterTest { // Note that en-US is already performed in the unitPercent() function. assertFormatDescending( "Indic Grouping", - "%% grouping=DEFAULT", - NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouping.DEFAULT), + "%% grouping=defaults", + NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouper.defaults()), new ULocale("en-IN"), "8,76,50,000‰", "87,65,000‰", @@ -723,8 +699,8 @@ public class NumberFormatterTest { assertFormatDescending( "Western Grouping, Min 2", - "%% grouping=DEFAULT_MIN_2_DIGITS", - NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouping.MIN_2_DIGITS), + "%% grouping=min2", + NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouper.min2()), ULocale.ENGLISH, "87,650,000‰", "8,765,000‰", @@ -738,8 +714,8 @@ public class NumberFormatterTest { assertFormatDescending( "Indic Grouping, Min 2", - "%% grouping=DEFAULT_MIN_2_DIGITS", - NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouping.MIN_2_DIGITS), + "%% grouping=min2", + NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouper.min2()), new ULocale("en-IN"), "8,76,50,000‰", "87,65,000‰", @@ -753,8 +729,8 @@ public class NumberFormatterTest { assertFormatDescending( "No Grouping", - "%% grouping=NONE", - NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouping.NONE), + "%% grouping=none", + NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouper.none()), new ULocale("en-IN"), "87650000‰", "8765000‰", @@ -771,8 +747,8 @@ public class NumberFormatterTest { public void padding() { assertFormatDescending( "Padding", - "padding=NONE", - NumberFormatter.with().padding(Padding.NONE), + "", + NumberFormatter.with().padding(Padder.none()), ULocale.ENGLISH, "87,650", "8,765", @@ -786,8 +762,8 @@ public class NumberFormatterTest { assertFormatDescending( "Padding", - "padding=8:AFTER_PREFIX:*", - NumberFormatter.with().padding(Padding.codePoints('*', 8, PadPosition.AFTER_PREFIX)), + "", + NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)), ULocale.ENGLISH, "**87,650", "***8,765", @@ -801,8 +777,8 @@ public class NumberFormatterTest { assertFormatDescending( "Padding with code points", - "padding=8:AFTER_PREFIX:𐇤", - NumberFormatter.with().padding(Padding.codePoints(0x101E4, 8, PadPosition.AFTER_PREFIX)), + "", + NumberFormatter.with().padding(Padder.codePoints(0x101E4, 8, PadPosition.AFTER_PREFIX)), ULocale.ENGLISH, "𐇤𐇤87,650", "𐇤𐇤𐇤8,765", @@ -816,9 +792,9 @@ public class NumberFormatterTest { assertFormatDescending( "Padding with wide digits", - "padding=8:AFTER_PREFIX:* symbols=ns:mathsanb", + "symbols=ns:mathsanb", NumberFormatter.with() - .padding(Padding.codePoints('*', 8, PadPosition.AFTER_PREFIX)) + .padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)) .symbols(NumberingSystem.getInstanceByName("mathsanb")), ULocale.ENGLISH, "**𝟴𝟳,𝟲𝟱𝟬", @@ -833,9 +809,9 @@ public class NumberFormatterTest { assertFormatDescending( "Padding with currency spacing", - "$GBP padding=10:AFTER_PREFIX:* unit-width=SHORT", + "$GBP unit-width=SHORT", NumberFormatter.with() - .padding(Padding.codePoints('*', 10, PadPosition.AFTER_PREFIX)) + .padding(Padder.codePoints('*', 10, PadPosition.AFTER_PREFIX)) .unit(GBP) .unitWidth(FormatWidth.SHORT), ULocale.ENGLISH, @@ -851,25 +827,25 @@ public class NumberFormatterTest { assertFormatSingle( "Pad Before Prefix", - "padding=8:BEFORE_PREFIX:*", - NumberFormatter.with().padding(Padding.codePoints('*', 8, PadPosition.BEFORE_PREFIX)), + "", + NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.BEFORE_PREFIX)), ULocale.ENGLISH, -88.88, "**-88.88"); assertFormatSingle( "Pad After Prefix", - "padding=8:AFTER_PREFIX:*", - NumberFormatter.with().padding(Padding.codePoints('*', 8, PadPosition.AFTER_PREFIX)), + "", + NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)), ULocale.ENGLISH, -88.88, "-**88.88"); assertFormatSingle( "Pad Before Suffix", - "% padding=8:BEFORE_SUFFIX:*", + "%", NumberFormatter.with() - .padding(Padding.codePoints('*', 8, PadPosition.BEFORE_SUFFIX)) + .padding(Padder.codePoints('*', 8, PadPosition.BEFORE_SUFFIX)) .unit(Dimensionless.PERCENT), ULocale.ENGLISH, 0.8888, @@ -877,9 +853,9 @@ public class NumberFormatterTest { assertFormatSingle( "Pad After Suffix", - "% padding=8:AFTER_SUFFIX:*", + "%", NumberFormatter.with() - .padding(Padding.codePoints('*', 8, PadPosition.AFTER_SUFFIX)) + .padding(Padder.codePoints('*', 8, PadPosition.AFTER_SUFFIX)) .unit(Dimensionless.PERCENT), ULocale.ENGLISH, 0.8888, @@ -891,7 +867,7 @@ public class NumberFormatterTest { assertFormatDescending( "Integer Width Default", "integer-width=1-", - NumberFormatter.with().integerWidth(IntegerWidth.DEFAULT), + NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1)), ULocale.ENGLISH, "87,650", "8,765", @@ -936,7 +912,7 @@ public class NumberFormatterTest { assertFormatDescending( "Integer Width Max 3", "integer-width=1-3", - NumberFormatter.with().integerWidth(IntegerWidth.DEFAULT.truncateAt(3)), + NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1).truncateAt(3)), ULocale.ENGLISH, "650", "765", @@ -985,7 +961,7 @@ public class NumberFormatterTest { "French Symbols with Japanese Data 2", "C symbols=loc:fr", NumberFormatter.with() - .notation(Notation.COMPACT_SHORT) + .notation(Notation.compactShort()) .symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)), ULocale.JAPAN, 12345, @@ -1157,8 +1133,8 @@ public class NumberFormatterTest { 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); + FormattedNumber positive = f.format(1); + FormattedNumber negative = f.format(-1); assertEquals(posPrefix, positive.getPrefix()); assertEquals(posSuffix, positive.getSuffix()); assertEquals(negPrefix, negative.getPrefix()); @@ -1176,7 +1152,7 @@ public class NumberFormatterTest { NumberFormatter.with() .unit(USD) .unitWidth(FormatWidth.WIDE) - .rounding(Rounding.fixedFraction(0)), + .rounding(Rounder.fixedFraction(0)), ULocale.ENGLISH, 1, "1 US dollar"); @@ -1187,7 +1163,7 @@ public class NumberFormatterTest { NumberFormatter.with() .unit(USD) .unitWidth(FormatWidth.WIDE) - .rounding(Rounding.fixedFraction(2)), + .rounding(Rounder.fixedFraction(2)), ULocale.ENGLISH, 1, "1.00 US dollars"); @@ -1203,8 +1179,8 @@ public class NumberFormatterTest { 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).threshold(0L).locale(locale); // no self-regulation - NumberFormatterImpl l2 = ((NumberFormatterImpl) f).threshold(1L).locale(locale); // all self-regulation + LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation + LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation for (int i = 0; i < 9; i++) { double d = inputs[i]; String actual1 = l1.format(d).toString(); @@ -1222,8 +1198,8 @@ public class NumberFormatterTest { Number input, String expected) { assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton()); - NumberFormatterImpl l1 = ((NumberFormatterImpl) f).threshold(0L).locale(locale); // no self-regulation - NumberFormatterImpl l2 = ((NumberFormatterImpl) f).threshold(1L).locale(locale); // all self-regulation + LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation + LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation String actual1 = l1.format(input).toString(); assertEquals(message + ": L1: " + input, expected, actual1); String actual2 = l2.format(input).toString(); @@ -1238,8 +1214,8 @@ public class NumberFormatterTest { Measure input, String expected) { assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton()); - NumberFormatterImpl l1 = ((NumberFormatterImpl) f).threshold(0L).locale(locale); // no self-regulation - NumberFormatterImpl l2 = ((NumberFormatterImpl) f).threshold(1L).locale(locale); // all self-regulation + LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation + LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation String actual1 = l1.format(input).toString(); assertEquals(message + ": L1: " + input, expected, actual1); String actual2 = l2.format(input).toString(); -- 2.40.0