ICU-13177 Adding new number formatting code to repository.

X-SVN-Rev: 40315
This commit is contained in:
Shane Carr 2017-08-05 01:02:35 +00:00
parent 8540468f28
commit f5af411b7f
29 changed files with 6585 additions and 0 deletions

View File

@ -0,0 +1,447 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
import newapi.impl.AffixPatternProvider;
/** Implements a recursive descent parser for decimal format patterns. */
public class LdmlPatternInfo {
public static PatternParseResult parse(String patternString) {
ParserState state = new ParserState(patternString);
PatternParseResult result = new PatternParseResult(patternString);
consumePattern(state, result);
return result;
}
/**
* An internal, intermediate data structure used for storing parse results before they are
* finalized into a DecimalFormatPattern.Builder.
*/
public static class PatternParseResult implements AffixPatternProvider {
public String pattern;
public LdmlPatternInfo.SubpatternParseResult positive;
public LdmlPatternInfo.SubpatternParseResult negative;
private PatternParseResult(String pattern) {
this.pattern = pattern;
}
@Override
public char charAt(int flags, int index) {
long endpoints = getEndpoints(flags);
int left = (int) (endpoints & 0xffffffff);
int right = (int) (endpoints >>> 32);
if (index < 0 || index >= right - left) {
throw new IndexOutOfBoundsException();
}
return pattern.charAt(left + index);
}
@Override
public int length(int flags) {
return getLengthFromEndpoints(getEndpoints(flags));
}
public static int getLengthFromEndpoints(long endpoints) {
int left = (int) (endpoints & 0xffffffff);
int right = (int) (endpoints >>> 32);
return right - left;
}
public String getString(int flags) {
long endpoints = getEndpoints(flags);
int left = (int) (endpoints & 0xffffffff);
int right = (int) (endpoints >>> 32);
if (left == right) {
return "";
}
return pattern.substring(left, right);
}
private long getEndpoints(int flags) {
boolean prefix = (flags & Flags.PREFIX) != 0;
boolean isNegative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
boolean padding = (flags & Flags.PADDING) != 0;
if (isNegative && padding) {
return negative.paddingEndpoints;
} else if (padding) {
return positive.paddingEndpoints;
} else if (prefix && isNegative) {
return negative.prefixEndpoints;
} else if (prefix) {
return positive.prefixEndpoints;
} else if (isNegative) {
return negative.suffixEndpoints;
} else {
return positive.suffixEndpoints;
}
}
@Override
public boolean positiveHasPlusSign() {
return positive.hasPlusSign;
}
@Override
public boolean hasNegativeSubpattern() {
return negative != null;
}
@Override
public boolean negativeHasMinusSign() {
return negative.hasMinusSign;
}
@Override
public boolean hasCurrencySign() {
return positive.hasCurrencySign || (negative != null && negative.hasCurrencySign);
}
@Override
public boolean containsSymbolType(int type) {
return AffixPatternUtils.containsType(pattern, type);
}
}
public static class SubpatternParseResult {
public long groupingSizes = 0x0000ffffffff0000L;
public int minimumIntegerDigits = 0;
public int totalIntegerDigits = 0;
public int minimumFractionDigits = 0;
public int maximumFractionDigits = 0;
public int minimumSignificantDigits = 0;
public int maximumSignificantDigits = 0;
public boolean hasDecimal = false;
public int paddingWidth = 0;
public PadPosition paddingLocation = null;
public FormatQuantity4 rounding = null;
public boolean exponentShowPlusSign = false;
public int exponentDigits = 0;
public boolean hasPercentSign = false;
public boolean hasPerMilleSign = false;
public boolean hasCurrencySign = false;
public boolean hasMinusSign = false;
public boolean hasPlusSign = false;
public long prefixEndpoints = 0;
public long suffixEndpoints = 0;
public long paddingEndpoints = 0;
}
/** An internal class used for tracking the cursor during parsing of a pattern string. */
private static class ParserState {
final String pattern;
int offset;
ParserState(String pattern) {
this.pattern = pattern;
this.offset = 0;
}
int peek() {
if (offset == pattern.length()) {
return -1;
} else {
return pattern.codePointAt(offset);
}
}
int next() {
int codePoint = peek();
offset += Character.charCount(codePoint);
return codePoint;
}
IllegalArgumentException toParseException(String message) {
StringBuilder sb = new StringBuilder();
sb.append("Malformed pattern for ICU DecimalFormat: \"");
sb.append(pattern);
sb.append("\": ");
sb.append(message);
sb.append(" at position ");
sb.append(offset);
return new IllegalArgumentException(sb.toString());
}
}
private static void consumePattern(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.PatternParseResult result) {
// pattern := subpattern (';' subpattern)?
result.positive = new SubpatternParseResult();
consumeSubpattern(state, result.positive);
if (state.peek() == ';') {
state.next(); // consume the ';'
// Don't consume the negative subpattern if it is empty (trailing ';')
if (state.peek() != -1) {
result.negative = new SubpatternParseResult();
consumeSubpattern(state, result.negative);
}
}
if (state.peek() != -1) {
throw state.toParseException("Found unquoted special character");
}
}
private static void consumeSubpattern(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
// subpattern := literals? number exponent? literals?
consumePadding(state, result, PadPosition.BEFORE_PREFIX);
result.prefixEndpoints = consumeAffix(state, result);
consumePadding(state, result, PadPosition.AFTER_PREFIX);
consumeFormat(state, result);
consumeExponent(state, result);
consumePadding(state, result, PadPosition.BEFORE_SUFFIX);
result.suffixEndpoints = consumeAffix(state, result);
consumePadding(state, result, PadPosition.AFTER_SUFFIX);
}
private static void consumePadding(
LdmlPatternInfo.ParserState state,
LdmlPatternInfo.SubpatternParseResult result,
PadPosition paddingLocation) {
if (state.peek() != '*') {
return;
}
result.paddingLocation = paddingLocation;
state.next(); // consume the '*'
result.paddingEndpoints |= state.offset;
consumeLiteral(state);
result.paddingEndpoints |= ((long) state.offset) << 32;
}
private static long consumeAffix(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
// literals := { literal }
long endpoints = state.offset;
outer:
while (true) {
switch (state.peek()) {
case '#':
case '@':
case ';':
case '*':
case '.':
case ',':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case -1:
// Characters that cannot appear unquoted in a literal
break outer;
case '%':
result.hasPercentSign = true;
break;
case '‰':
result.hasPerMilleSign = true;
break;
case '¤':
result.hasCurrencySign = true;
break;
case '-':
result.hasMinusSign = true;
break;
case '+':
result.hasPlusSign = true;
break;
}
consumeLiteral(state);
}
endpoints |= ((long) state.offset) << 32;
return endpoints;
}
private static void consumeLiteral(LdmlPatternInfo.ParserState state) {
if (state.peek() == -1) {
throw state.toParseException("Expected unquoted literal but found EOL");
} else if (state.peek() == '\'') {
state.next(); // consume the starting quote
while (state.peek() != '\'') {
if (state.peek() == -1) {
throw state.toParseException("Expected quoted literal but found EOL");
} else {
state.next(); // consume a quoted character
}
}
state.next(); // consume the ending quote
} else {
// consume a non-quoted literal character
state.next();
}
}
private static void consumeFormat(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
consumeIntegerFormat(state, result);
if (state.peek() == '.') {
state.next(); // consume the decimal point
result.hasDecimal = true;
result.paddingWidth += 1;
consumeFractionFormat(state, result);
}
}
private static void consumeIntegerFormat(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
boolean seenSignificantDigitMarker = false;
boolean seenDigit = false;
outer:
while (true) {
switch (state.peek()) {
case ',':
result.paddingWidth += 1;
result.groupingSizes <<= 16;
break;
case '#':
if (seenDigit) throw state.toParseException("# cannot follow 0 before decimal point");
result.paddingWidth += 1;
result.groupingSizes += 1;
result.totalIntegerDigits += (seenSignificantDigitMarker ? 0 : 1);
// no change to result.minimumIntegerDigits
// no change to result.minimumSignificantDigits
result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0);
if (result.rounding != null) {
result.rounding.appendDigit((byte) 0, 0, true);
}
break;
case '@':
seenSignificantDigitMarker = true;
if (seenDigit) throw state.toParseException("Cannot mix 0 and @");
result.paddingWidth += 1;
result.groupingSizes += 1;
result.totalIntegerDigits += 1;
// no change to result.minimumIntegerDigits
result.minimumSignificantDigits += 1;
result.maximumSignificantDigits += 1;
if (result.rounding != null) {
result.rounding.appendDigit((byte) 0, 0, true);
}
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
seenDigit = true;
if (seenSignificantDigitMarker) throw state.toParseException("Cannot mix @ and 0");
// TODO: Crash here if we've seen the significant digit marker? See NumberFormatTestCases.txt
result.paddingWidth += 1;
result.groupingSizes += 1;
result.totalIntegerDigits += 1;
result.minimumIntegerDigits += 1;
// no change to result.minimumSignificantDigits
// no change to result.maximumSignificantDigits
if (state.peek() != '0' && result.rounding == null) {
result.rounding = new FormatQuantity4();
}
if (result.rounding != null) {
result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true);
}
break;
default:
break outer;
}
state.next(); // consume the symbol
}
// Disallow patterns with a trailing ',' or with two ',' next to each other
short grouping1 = (short) (result.groupingSizes & 0xffff);
short grouping2 = (short) ((result.groupingSizes >>> 16) & 0xffff);
short grouping3 = (short) ((result.groupingSizes >>> 32) & 0xffff);
if (grouping1 == 0 && grouping2 != -1) {
throw state.toParseException("Trailing grouping separator is invalid");
}
if (grouping2 == 0 && grouping3 != -1) {
throw state.toParseException("Grouping width of zero is invalid");
}
}
private static void consumeFractionFormat(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
int zeroCounter = 0;
boolean seenHash = false;
while (true) {
switch (state.peek()) {
case '#':
seenHash = true;
result.paddingWidth += 1;
// no change to result.minimumFractionDigits
result.maximumFractionDigits += 1;
zeroCounter++;
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
if (seenHash) throw state.toParseException("0 cannot follow # after decimal point");
result.paddingWidth += 1;
result.minimumFractionDigits += 1;
result.maximumFractionDigits += 1;
if (state.peek() == '0') {
zeroCounter++;
} else {
if (result.rounding == null) {
result.rounding = new FormatQuantity4();
}
result.rounding.appendDigit((byte) (state.peek() - '0'), zeroCounter, false);
zeroCounter = 0;
}
break;
default:
return;
}
state.next(); // consume the symbol
}
}
private static void consumeExponent(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
if (state.peek() != 'E') {
return;
}
state.next(); // consume the E
result.paddingWidth++;
if (state.peek() == '+') {
state.next(); // consume the +
result.exponentShowPlusSign = true;
result.paddingWidth++;
}
while (state.peek() == '0') {
state.next(); // consume the 0
result.exponentDigits += 1;
result.paddingWidth++;
}
}
}

View File

@ -0,0 +1,684 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.util.Arrays;
import java.util.Locale;
import com.ibm.icu.impl.number.FormatQuantityBCD;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.text.PluralRules.IFixedDecimal;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.ICUUncheckedIOException;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
import newapi.impl.GroupingImpl;
import newapi.impl.IntegerWidthImpl;
import newapi.impl.MicroProps;
import newapi.impl.NotationImpl.NotationCompactImpl;
import newapi.impl.NotationImpl.NotationScientificImpl;
import newapi.impl.NumberFormatterImpl;
import newapi.impl.PaddingImpl;
import newapi.impl.RoundingImpl.RoundingImplCurrency;
import newapi.impl.RoundingImpl.RoundingImplFraction;
import newapi.impl.RoundingImpl.RoundingImplIncrement;
import newapi.impl.RoundingImpl.RoundingImplInfinity;
import newapi.impl.RoundingImpl.RoundingImplSignificant;
public final class NumberFormatter {
public interface IRounding {
public BigDecimal round(BigDecimal input);
}
public interface IGrouping {
public boolean groupAtPosition(int position, BigDecimal input);
}
// This could possibly be combined into MeasureFormat.FormatWidth
public static enum CurrencyDisplay {
SYMBOL, // ¤
ISO_4217, // ¤¤
DISPLAY_NAME, // ¤¤¤
SYMBOL_NARROW, // ¤¤¤¤
HIDDEN, // uses currency rounding and formatting but omits the currency symbol
// TODO: For hidden, what to do if currency symbol appears in the middle, as in Portugal ?
}
public static enum DecimalMarkDisplay {
AUTO,
ALWAYS_SHOWN,
}
public static enum SignDisplay {
AUTO,
ALWAYS_SHOWN,
NEVER_SHOWN,
}
public static class UnlocalizedNumberFormatter {
public UnlocalizedNumberFormatter notation(Notation notation) {
throw new AssertionError("See NumberFormatterImpl");
}
public UnlocalizedNumberFormatter unit(MeasureUnit unit) {
throw new AssertionError("See NumberFormatterImpl");
}
public UnlocalizedNumberFormatter rounding(IRounding rounding) {
throw new AssertionError("See NumberFormatterImpl");
}
public UnlocalizedNumberFormatter grouping(IGrouping grouping) {
throw new AssertionError("See NumberFormatterImpl");
}
public UnlocalizedNumberFormatter padding(Padding padding) {
throw new AssertionError("See NumberFormatterImpl");
}
public UnlocalizedNumberFormatter integerWidth(IntegerWidth style) {
throw new AssertionError("See NumberFormatterImpl");
}
public UnlocalizedNumberFormatter symbols(DecimalFormatSymbols symbols) {
throw new AssertionError("See NumberFormatterImpl");
}
public UnlocalizedNumberFormatter symbols(NumberingSystem ns) {
throw new AssertionError("See NumberFormatterImpl");
}
public UnlocalizedNumberFormatter unitWidth(FormatWidth style) {
throw new AssertionError("See NumberFormatterImpl");
}
public UnlocalizedNumberFormatter sign(SignDisplay style) {
throw new AssertionError("See NumberFormatterImpl");
}
public UnlocalizedNumberFormatter decimal(DecimalMarkDisplay style) {
throw new AssertionError("See NumberFormatterImpl");
}
public LocalizedNumberFormatter locale(Locale locale) {
throw new AssertionError("See NumberFormatterImpl");
}
public LocalizedNumberFormatter locale(ULocale locale) {
throw new AssertionError("See NumberFormatterImpl");
}
public String toSkeleton() {
throw new AssertionError("See NumberFormatterImpl");
}
// Prevent external subclassing with private constructor
private UnlocalizedNumberFormatter() {}
}
public static class LocalizedNumberFormatter extends UnlocalizedNumberFormatter {
@Override
public UnlocalizedNumberFormatter notation(Notation notation) {
throw new AssertionError("See NumberFormatterImpl");
}
@Override
public LocalizedNumberFormatter unit(MeasureUnit unit) {
throw new AssertionError("See NumberFormatterImpl");
}
@Override
public LocalizedNumberFormatter rounding(IRounding rounding) {
throw new AssertionError("See NumberFormatterImpl");
}
@Override
public LocalizedNumberFormatter grouping(IGrouping grouping) {
throw new AssertionError("See NumberFormatterImpl");
}
@Override
public LocalizedNumberFormatter padding(Padding padding) {
throw new AssertionError("See NumberFormatterImpl");
}
@Override
public LocalizedNumberFormatter integerWidth(IntegerWidth style) {
throw new AssertionError("See NumberFormatterImpl");
}
@Override
public LocalizedNumberFormatter symbols(DecimalFormatSymbols symbols) {
throw new AssertionError("See NumberFormatterImpl");
}
@Override
public LocalizedNumberFormatter symbols(NumberingSystem ns) {
throw new AssertionError("See NumberFormatterImpl");
}
@Override
public LocalizedNumberFormatter unitWidth(FormatWidth style) {
throw new AssertionError("See NumberFormatterImpl");
}
@Override
public LocalizedNumberFormatter sign(SignDisplay style) {
throw new AssertionError("See NumberFormatterImpl");
}
@Override
public LocalizedNumberFormatter decimal(DecimalMarkDisplay style) {
throw new AssertionError("See NumberFormatterImpl");
}
public NumberFormatterResult format(long input) {
throw new AssertionError("See NumberFormatterImpl");
}
public NumberFormatterResult format(double input) {
throw new AssertionError("See NumberFormatterImpl");
}
public NumberFormatterResult format(Number input) {
throw new AssertionError("See NumberFormatterImpl");
}
public NumberFormatterResult format(Measure input) {
throw new AssertionError("See NumberFormatterImpl");
}
// Prevent external subclassing with private constructor
private LocalizedNumberFormatter() {}
/**
* @internal
* @deprecated This API is for ICU internal use only.
*/
@Deprecated
public static class Internal extends LocalizedNumberFormatter {}
}
public static UnlocalizedNumberFormatter fromSkeleton(String skeleton) {
// FIXME
throw new UnsupportedOperationException();
}
public static UnlocalizedNumberFormatter with() {
return NumberFormatterImpl.with();
}
public static LocalizedNumberFormatter withLocale(Locale locale) {
return NumberFormatterImpl.with().locale(locale);
}
public static LocalizedNumberFormatter withLocale(ULocale locale) {
return NumberFormatterImpl.with().locale(locale);
}
public static class NumberFormatterResult {
NumberStringBuilder nsb;
FormatQuantityBCD fq;
MicroProps micros;
/**
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public NumberFormatterResult(NumberStringBuilder nsb, FormatQuantityBCD fq, MicroProps micros) {
this.nsb = nsb;
this.fq = fq;
this.micros = micros;
}
@Override
public String toString() {
return nsb.toString();
}
public <A extends Appendable> A appendTo(A appendable) {
try {
appendable.append(nsb);
} catch (IOException e) {
// Throw as an unchecked exception to avoid users needing try/catch
throw new ICUUncheckedIOException(e);
}
return appendable;
}
public AttributedCharacterIterator toAttributedCharacterIterator() {
return nsb.getIterator();
}
/**
* @internal
* @deprecated This API a technology preview. It is not stable and may change or go away in an
* upcoming release.
*/
@Deprecated
public void populateFieldPosition(FieldPosition fieldPosition, int offset) {
nsb.populateFieldPosition(fieldPosition, offset);
fq.populateUFieldPosition(fieldPosition);
}
/**
* @internal
* @deprecated This API a technology preview. It is not stable and may change or go away in an
* upcoming release.
*/
@Deprecated
public String getPrefix() {
return micros.modOuter.getPrefix()
+ micros.modMiddle.getPrefix()
+ micros.modInner.getPrefix();
}
/**
* @internal
* @deprecated This API a technology preview. It is not stable and may change or go away in an
* upcoming release.
*/
@Deprecated
public String getSuffix() {
return micros.modInner.getSuffix()
+ micros.modMiddle.getSuffix()
+ micros.modOuter.getSuffix();
}
/**
* @internal
* @deprecated This API a technology preview. It is not stable and may change or go away in an
* upcoming release.
*/
@Deprecated
public IFixedDecimal getFixedDecimal() {
return fq;
}
public BigDecimal toBigDecimal() {
return fq.toBigDecimal();
}
@Override
public int hashCode() {
// NumberStringBuilder and BigDecimal are mutable, so we can't call
// #equals() or #hashCode() on them directly.
return Arrays.hashCode(nsb.toCharArray())
^ Arrays.hashCode(nsb.toFieldArray())
^ fq.toBigDecimal().hashCode();
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (other == null) return false;
if (!(other instanceof NumberFormatterResult)) return false;
// NumberStringBuilder and BigDecimal are mutable, so we can't call
// #equals() or #hashCode() on them directly.
NumberFormatterResult _other = (NumberFormatterResult) other;
return Arrays.equals(nsb.toCharArray(), _other.nsb.toCharArray())
^ Arrays.equals(nsb.toFieldArray(), _other.nsb.toFieldArray())
^ fq.toBigDecimal().equals(_other.fq.toBigDecimal());
}
}
public static class Notation {
// FIXME: Support engineering intervals other than 3?
public static final NotationScientific SCIENTIFIC = new NotationScientificImpl(1);
public static final NotationScientific ENGINEERING = new NotationScientificImpl(3);
public static final NotationCompact COMPACT_SHORT = new NotationCompactImpl(CompactStyle.SHORT);
public static final NotationCompact COMPACT_LONG = new NotationCompactImpl(CompactStyle.LONG);
public static final NotationSimple SIMPLE = new NotationSimple();
// Prevent subclassing
private Notation() {}
}
@SuppressWarnings("unused")
public static class NotationScientific extends Notation {
public NotationScientific withMinExponentDigits(int minExponentDigits) {
// Overridden in NotationImpl
throw new AssertionError();
}
public NotationScientific withExponentSignDisplay(SignDisplay exponentSignDisplay) {
// Overridden in NotationImpl
throw new AssertionError();
}
// Prevent subclassing
private NotationScientific() {}
/**
* @internal
* @deprecated This API is for ICU internal use only.
*/
@Deprecated
public static class Internal extends NotationScientific {}
}
public static class NotationCompact extends Notation {
// Prevent subclassing
private NotationCompact() {}
/**
* @internal
* @deprecated This API is for ICU internal use only.
*/
@Deprecated
public static class Internal extends NotationCompact {}
}
public static class NotationSimple extends Notation {
// Prevent subclassing
private NotationSimple() {}
}
public static class Rounding implements IRounding {
protected static final int MAX_VALUE = 100;
public static final Rounding NONE = new RoundingImplInfinity();
public static final Rounding INTEGER = new RoundingImplFraction();
public static FractionRounding fixedFraction(int minMaxFrac) {
if (minMaxFrac >= 0 && minMaxFrac <= MAX_VALUE) {
return RoundingImplFraction.getInstance(minMaxFrac, minMaxFrac);
} else {
throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
}
}
public static FractionRounding minFraction(int minFrac) {
if (minFrac >= 0 && minFrac < MAX_VALUE) {
return RoundingImplFraction.getInstance(minFrac, Integer.MAX_VALUE);
} else {
throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
}
}
public static FractionRounding maxFraction(int maxFrac) {
if (maxFrac >= 0 && maxFrac < MAX_VALUE) {
return RoundingImplFraction.getInstance(0, maxFrac);
} else {
throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
}
}
public static FractionRounding minMaxFraction(int minFrac, int maxFrac) {
if (minFrac >= 0 && maxFrac <= MAX_VALUE && minFrac <= maxFrac) {
return RoundingImplFraction.getInstance(minFrac, maxFrac);
} else {
throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
}
}
public static Rounding fixedFigures(int minMaxSig) {
if (minMaxSig > 0 && minMaxSig <= MAX_VALUE) {
return RoundingImplSignificant.getInstance(minMaxSig, minMaxSig);
} else {
throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
}
}
public static Rounding minFigures(int minSig) {
if (minSig > 0 && minSig <= MAX_VALUE) {
return RoundingImplSignificant.getInstance(minSig, Integer.MAX_VALUE);
} else {
throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
}
}
public static Rounding maxFigures(int maxSig) {
if (maxSig > 0 && maxSig <= MAX_VALUE) {
return RoundingImplSignificant.getInstance(0, maxSig);
} else {
throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
}
}
public static Rounding minMaxFigures(int minSig, int maxSig) {
if (minSig > 0 && maxSig <= MAX_VALUE && minSig <= maxSig) {
return RoundingImplSignificant.getInstance(minSig, maxSig);
} else {
throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
}
}
public static Rounding increment(BigDecimal roundingIncrement) {
if (roundingIncrement == null) {
throw new IllegalArgumentException("Rounding increment must be non-null");
} else if (roundingIncrement.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Rounding increment must be positive");
} else {
return RoundingImplIncrement.getInstance(roundingIncrement);
}
}
public static CurrencyRounding currency(CurrencyUsage currencyUsage) {
if (currencyUsage != CurrencyUsage.STANDARD && currencyUsage != CurrencyUsage.CASH) {
throw new IllegalArgumentException("Unknown CurrencyUsage: " + currencyUsage);
} else {
return RoundingImplCurrency.getInstance(currencyUsage);
}
}
/**
* Sets the {@link java.math.RoundingMode} to use when picking the direction to round (up or
* down).
*
* <p>Common values include {@link RoundingMode#HALF_EVEN}, {@link RoundingMode#HALF_UP}, and
* {@link RoundingMode#CEILING}. The default is HALF_EVEN.
*
* @param roundingMode The RoundingMode to use.
* @return An immutable object for chaining.
*/
public Rounding withMode(RoundingMode roundingMode) {
// Overridden in RoundingImpl
throw new AssertionError();
}
/**
* Sets a MathContext directly instead of RoundingMode.
*
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public Rounding withMode(MathContext mathContext) {
// Overridden in RoundingImpl
throw new AssertionError();
}
@Override
public BigDecimal round(BigDecimal input) {
// Overridden in RoundingImpl
throw new AssertionError();
}
// Prevent subclassing
private Rounding() {}
/**
* @internal
* @deprecated This API is for ICU internal use only.
*/
@Deprecated
public static class Internal extends Rounding {}
}
/**
* A rounding strategy based on a minimum and/or maximum number of fraction digits. Allows for a
* minimum or maximum number of significant digits to be specified.
*/
public static class FractionRounding extends Rounding {
/**
* Ensures that no less than this number of significant figures are retained when rounding
* according to fraction rules.
*
* <p>For example, with integer rounding, the number 3.141 becomes "3". However, with minimum
* figures set to 2, 3.141 becomes "3.1" instead.
*
* <p>This setting does not affect the number of trailing zeros. For example, 3.01 would print
* as "3", not "3.0".
*
* @param minFigures The number of significant figures to guarantee.
* @return An immutable object for chaining.
*/
public Rounding withMinFigures(int minFigures) {
// Overridden in RoundingImpl
throw new AssertionError();
}
/**
* Ensures that no more than this number of significant figures are retained when rounding
* according to fraction rules.
*
* <p>For example, with integer rounding, the number 123.4 becomes "123". However, with maximum
* figures set to 2, 123.4 becomes "120" instead.
*
* <p>This setting does not affect the number of trailing zeros. For example, with fixed
* fraction of 2, 123.4 would become "120.00".
*
* @param maxFigures
* @return An immutable object for chaining.
*/
public Rounding withMaxFigures(int maxFigures) {
// Overridden in RoundingImpl
throw new AssertionError();
}
// Prevent subclassing
private FractionRounding() {}
/**
* @internal
* @deprecated This API is for ICU internal use only.
*/
@Deprecated
public static class Internal extends FractionRounding {}
}
/** A rounding strategy parameterized by a currency. */
public static class CurrencyRounding extends Rounding {
/**
* Associates a {@link com.ibm.icu.util.Currency} with this rounding strategy. Only applies to
* rounding strategies returned from {@link #currency(CurrencyUsage)}.
*
* <p><strong>Calling this method is <em>not required</em></strong>, because the currency
* specified in {@link NumberFormatter#unit(MeasureUnit)} or via a {@link CurrencyAmount} passed
* into {@link LocalizedNumberFormatter#format(Measure)} is automatically applied to currency
* rounding strategies. However, this method enables you to override that automatic association.
*
* <p>This method also enables numbers to be formatted using currency rounding rules without
* explicitly using a currency format.
*
* @param currency The currency to associate with this rounding strategy.
* @return An immutable object for chaining.
*/
public Rounding withCurrency(Currency currency) {
// Overridden in RoundingImpl
throw new AssertionError();
}
// Prevent subclassing
private CurrencyRounding() {}
/**
* @internal
* @deprecated This API is for ICU internal use only.
*/
@Deprecated
public static class Internal extends CurrencyRounding {}
}
public static class Grouping implements IGrouping {
public static final Grouping DEFAULT = new GroupingImpl(GroupingImpl.TYPE_PLACEHOLDER);
public static final Grouping DEFAULT_MIN_2_DIGITS = new GroupingImpl(GroupingImpl.TYPE_MIN2);
public static final Grouping NONE = new GroupingImpl(GroupingImpl.TYPE_NONE);
@Override
public boolean groupAtPosition(int position, BigDecimal input) {
throw new UnsupportedOperationException(
"This grouping strategy cannot be used outside of number formatting.");
}
// Prevent subclassing
private Grouping() {}
/**
* @internal
* @deprecated This API is for ICU internal use only.
*/
@Deprecated
public static class Internal extends Grouping {}
}
public static class Padding {
public static final Padding NONE = new PaddingImpl();
public static Padding codePoints(int cp, int targetWidth, PadPosition position) {
String paddingString = String.valueOf(Character.toChars(cp));
return PaddingImpl.getInstance(paddingString, targetWidth, position);
}
// Prevent subclassing
private Padding() {}
/**
* @internal
* @deprecated This API is for ICU internal use only.
*/
@Deprecated
public static class Internal extends Padding {}
}
@SuppressWarnings("unused")
public static class IntegerWidth {
public static final IntegerWidth DEFAULT = new IntegerWidthImpl();
public static IntegerWidth zeroFillTo(int minInt) {
return new IntegerWidthImpl(minInt, Integer.MAX_VALUE);
}
public IntegerWidth truncateAt(int maxInt) {
// Implemented in IntegerWidthImpl
throw new AssertionError();
}
// Prevent subclassing
private IntegerWidth() {}
/**
* @internal
* @deprecated This API is for ICU internal use only.
*/
@Deprecated
public static class Internal extends IntegerWidth {}
}
}

View File

@ -0,0 +1,63 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi;
import java.math.RoundingMode;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.Dimensionless;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.Grouping;
import newapi.NumberFormatter.Notation;
import newapi.NumberFormatter.Rounding;
import newapi.NumberFormatter.SignDisplay;
import newapi.NumberFormatter.UnlocalizedNumberFormatter;
public class demo {
public static void main(String[] args) {
System.out.println(NumberingSystem.LATIN.getDescription());
UnlocalizedNumberFormatter formatter =
NumberFormatter.with()
.notation(Notation.COMPACT_SHORT)
.notation(Notation.SCIENTIFIC.withExponentSignDisplay(SignDisplay.ALWAYS_SHOWN))
.notation(Notation.ENGINEERING.withMinExponentDigits(2))
.notation(Notation.SIMPLE)
.unit(Currency.getInstance("GBP"))
.unit(Dimensionless.PERCENT)
.unit(MeasureUnit.CUBIC_METER)
.unitWidth(FormatWidth.SHORT)
// .rounding(Rounding.fixedSignificantDigits(3))
// .rounding(
// (BigDecimal input) -> {
// return input.divide(new BigDecimal("0.02"), 0).multiply(new BigDecimal("0.02"));
// })
.rounding(Rounding.fixedFraction(2).withMode(RoundingMode.HALF_UP))
.rounding(Rounding.INTEGER.withMode(RoundingMode.CEILING))
.rounding(Rounding.currency(CurrencyUsage.STANDARD))
// .grouping(
// (int position, BigDecimal number) -> {
// return (position % 3) == 0;
// })
.grouping(Grouping.DEFAULT)
.grouping(Grouping.NONE)
.grouping(Grouping.DEFAULT_MIN_2_DIGITS)
// .padding(Padding.codePoints(' ', 8, PadPosition.AFTER_PREFIX))
.sign(SignDisplay.ALWAYS_SHOWN)
.decimal(DecimalMarkDisplay.ALWAYS_SHOWN)
.symbols(DecimalFormatSymbols.getInstance(new ULocale("fr@digits=ascii")))
.symbols(NumberingSystem.getInstanceByName("arab"))
.symbols(NumberingSystem.LATIN);
System.out.println(formatter.toSkeleton());
System.out.println(formatter.locale(ULocale.ENGLISH).format(0.98381).toString());
// .locale(Locale.ENGLISH)
// .format(123.45)
// .toString();
}
}

View File

@ -0,0 +1,26 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
public interface AffixPatternProvider {
public static final class Flags {
public static final int PLURAL_MASK = 0xff;
public static final int PREFIX = 0x100;
public static final int NEGATIVE_SUBPATTERN = 0x200;
public static final int PADDING = 0x400;
}
public char charAt(int flags, int i);
public int length(int flags);
public boolean hasCurrencySign();
public boolean positiveHasPlusSign();
public boolean hasNegativeSubpattern();
public boolean negativeHasMinusSign();
public boolean containsSymbolType(int type);
}

View File

@ -0,0 +1,276 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.CompactDecimalFormat.CompactType;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
class CompactData implements RoundingImpl.MultiplierProducer {
public static CompactData getInstance(
ULocale locale, CompactType compactType, CompactStyle compactStyle) {
// TODO: Add a data cache? It would be keyed by locale, compact type, and compact style.
CompactData data = new CompactData();
CompactDataSink sink = new CompactDataSink(data, compactType, compactStyle);
String nsName = NumberingSystem.getInstance(locale).getName();
ICUResourceBundle rb =
(ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
CompactData.internalPopulateData(nsName, rb, sink, data);
if (data.isEmpty() && compactStyle == CompactStyle.LONG) {
// No long data is available; load short data instead
sink.compactStyle = CompactStyle.SHORT;
CompactData.internalPopulateData(nsName, rb, sink, data);
}
return data;
}
public static CompactData getInstance(
Map<String, Map<String, String>> powersToPluralsToPatterns) {
CompactData data = new CompactData();
for (Map.Entry<String, Map<String, String>> magnitudeEntry :
powersToPluralsToPatterns.entrySet()) {
byte magnitude = (byte) (magnitudeEntry.getKey().length() - 1);
for (Map.Entry<String, String> pluralEntry : magnitudeEntry.getValue().entrySet()) {
StandardPlural plural = StandardPlural.fromString(pluralEntry.getKey().toString());
String patternString = pluralEntry.getValue().toString();
data.setPattern(patternString, magnitude, plural);
int numZeros = countZeros(patternString);
if (numZeros > 0) { // numZeros==0 in certain cases, like Somali "Kun"
data.setMultiplier(magnitude, (byte) (numZeros - magnitude - 1));
}
}
}
return data;
}
private static void internalPopulateData(
String nsName, ICUResourceBundle rb, CompactDataSink sink, CompactData data) {
try {
rb.getAllItemsWithFallback("NumberElements/" + nsName, sink);
} catch (MissingResourceException e) {
// Fall back to latn
}
if (data.isEmpty() && !nsName.equals("latn")) {
rb.getAllItemsWithFallback("NumberElements/latn", sink);
}
if (sink.exception != null) {
throw sink.exception;
}
}
// A dummy object used when a "0" compact decimal entry is encountered. This is necessary
// in order to prevent falling back to root. Object equality ("==") is intended.
private static final String USE_FALLBACK = "<USE FALLBACK>";
private final String[] patterns;
private final byte[] multipliers;
private boolean isEmpty;
private int largestMagnitude;
private static final int MAX_DIGITS = 15;
private CompactData() {
patterns = new String[(CompactData.MAX_DIGITS + 1) * StandardPlural.COUNT];
multipliers = new byte[CompactData.MAX_DIGITS + 1];
isEmpty = true;
largestMagnitude = 0;
}
public boolean isEmpty() {
return isEmpty;
}
@Override
public int getMultiplier(int magnitude) {
if (magnitude < 0) {
return 0;
}
if (magnitude > largestMagnitude) {
magnitude = largestMagnitude;
}
return multipliers[magnitude];
}
/** Returns the multiplier from the array directly without bounds checking. */
public int getMultiplierDirect(int magnitude) {
return multipliers[magnitude];
}
private void setMultiplier(int magnitude, byte multiplier) {
if (multipliers[magnitude] != 0) {
assert multipliers[magnitude] == multiplier;
return;
}
multipliers[magnitude] = multiplier;
isEmpty = false;
if (magnitude > largestMagnitude) largestMagnitude = magnitude;
}
public String getPattern(int magnitude, StandardPlural plural) {
if (magnitude < 0) {
return null;
}
if (magnitude > largestMagnitude) {
magnitude = largestMagnitude;
}
String patternString = patterns[getIndex(magnitude, plural)];
if (patternString == null && plural != StandardPlural.OTHER) {
// Fall back to "other" plural variant
patternString = patterns[getIndex(magnitude, StandardPlural.OTHER)];
}
if (patternString == USE_FALLBACK) {
// Return null if USE_FALLBACK is present
patternString = null;
}
return patternString;
}
public Set<String> getAllPatterns() {
Set<String> result = new HashSet<String>();
result.addAll(Arrays.asList(patterns));
result.remove(USE_FALLBACK);
result.remove(null);
return result;
}
private boolean has(int magnitude, StandardPlural plural) {
// Return true if USE_FALLBACK is present
return patterns[getIndex(magnitude, plural)] != null;
}
private void setPattern(String patternString, int magnitude, StandardPlural plural) {
patterns[getIndex(magnitude, plural)] = patternString;
isEmpty = false;
if (magnitude > largestMagnitude) largestMagnitude = magnitude;
}
private void setNoFallback(int magnitude, StandardPlural plural) {
setPattern(USE_FALLBACK, magnitude, plural);
}
private static final int getIndex(int magnitude, StandardPlural plural) {
return magnitude * StandardPlural.COUNT + plural.ordinal();
}
private static final class CompactDataSink extends UResource.Sink {
CompactData data;
CompactStyle compactStyle;
CompactType compactType;
IllegalArgumentException exception;
/*
* NumberElements{ <-- top (numbering system table)
* latn{ <-- patternsTable (one per numbering system)
* patternsLong{ <-- formatsTable (one per pattern)
* decimalFormat{ <-- powersOfTenTable (one per format)
* 1000{ <-- pluralVariantsTable (one per power of ten)
* one{"0 thousand"} <-- plural variant and template
*/
public CompactDataSink(CompactData data, CompactType compactType, CompactStyle compactStyle) {
this.data = data;
this.compactType = compactType;
this.compactStyle = compactStyle;
}
@Override
public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
UResource.Table patternsTable = value.getTable();
for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) {
if (key.contentEquals("patternsShort") && compactStyle == CompactStyle.SHORT) {
} else if (key.contentEquals("patternsLong") && compactStyle == CompactStyle.LONG) {
} else {
continue;
}
// traverse into the table of formats
UResource.Table formatsTable = value.getTable();
for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) {
if (key.contentEquals("decimalFormat") && compactType == CompactType.DECIMAL) {
} else if (key.contentEquals("currencyFormat") && compactType == CompactType.CURRENCY) {
} else {
continue;
}
// traverse into the table of powers of ten
UResource.Table powersOfTenTable = value.getTable();
for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) {
// Assumes that the keys are always of the form "10000" where the magnitude is the
// length of the key minus one
byte magnitude = (byte) (key.length() - 1);
byte multiplier = (byte) data.getMultiplierDirect(magnitude);
// Silently ignore divisors that are too big.
if (magnitude >= CompactData.MAX_DIGITS) continue;
// Iterate over the plural variants ("one", "other", etc)
UResource.Table pluralVariantsTable = value.getTable();
for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
// Skip this magnitude/plural if we already have it from a child locale.
StandardPlural plural = StandardPlural.fromString(key.toString());
if (data.has(magnitude, plural)) {
continue;
}
// The value "0" means that we need to use the default pattern and not fall back
// to parent locales. Example locale where this is relevant: 'it'.
String patternString = value.toString();
if (patternString.equals("0")) {
data.setNoFallback(magnitude, plural);
continue;
}
// Save the pattern string. We will parse it lazily.
data.setPattern(patternString, magnitude, plural);
// If necessary, compute the multiplier: the difference between the magnitude
// and the number of zeros in the pattern.
if (multiplier == 0) {
int numZeros = countZeros(patternString);
if (numZeros > 0) { // numZeros==0 in certain cases, like Somali "Kun"
multiplier = (byte) (numZeros - magnitude - 1);
}
}
}
data.setMultiplier(magnitude, multiplier);
}
// We want only one table of compact decimal formats, so if we get here, stop consuming.
// The data.isEmpty() check will prevent further bundles from being traversed.
return;
}
}
}
}
private static final int countZeros(String patternString) {
// NOTE: This strategy for computing the number of zeros is a hack for efficiency.
// It could break if there are any 0s that aren't part of the main pattern.
int numZeros = 0;
for (int i = 0; i < patternString.length(); i++) {
if (patternString.charAt(i) == '0') {
numZeros++;
} else if (numZeros > 0) {
break; // zeros should always be contiguous
}
}
return numZeros;
}
}

View File

@ -0,0 +1,114 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.LdmlPatternInfo;
import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.CompactDecimalFormat.CompactType;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.ULocale;
import newapi.impl.MurkyModifier.ImmutableMurkyModifier;
import newapi.impl.RoundingImpl.RoundingImplDummy;
public class CompactImpl implements QuantityChain {
final PluralRules rules;
final CompactData data;
/* final */ Map<String, CompactModInfo> precomputedMods;
/* final */ QuantityChain parent;
public static CompactImpl getInstance(
ULocale dataLocale, CompactType compactType, CompactStyle compactStyle, PluralRules rules) {
CompactData data = CompactData.getInstance(dataLocale, compactType, compactStyle);
return new CompactImpl(data, rules);
}
public static CompactImpl getInstance(
Map<String, Map<String, String>> compactCustomData, PluralRules rules) {
CompactData data = CompactData.getInstance(compactCustomData);
return new CompactImpl(data, rules);
}
private CompactImpl(CompactData data, PluralRules rules) {
this.data = data;
this.rules = rules;
}
/** To be used by the building code path */
public void precomputeAllModifiers(MurkyModifier reference) {
precomputedMods = new HashMap<String, CompactModInfo>();
Set<String> allPatterns = data.getAllPatterns();
for (String patternString : allPatterns) {
CompactModInfo info = new CompactModInfo();
PatternParseResult patternInfo = LdmlPatternInfo.parse(patternString);
reference.setPatternInfo(patternInfo);
info.mod = reference.createImmutable();
info.numDigits = patternInfo.positive.totalIntegerDigits;
precomputedMods.put(patternString, info);
}
}
private static class CompactModInfo {
public ImmutableMurkyModifier mod;
public int numDigits;
}
@Override
public QuantityChain chain(QuantityChain parent) {
this.parent = parent;
return this;
}
@Override
public MicroProps withQuantity(FormatQuantity input) {
MicroProps micros = parent.withQuantity(input);
assert micros.rounding != null;
// Treat zero as if it had magnitude 0
int magnitude;
if (input.isZero()) {
magnitude = 0;
micros.rounding.apply(input);
} else {
// TODO: Revisit chooseMultiplierAndApply
int multiplier = micros.rounding.chooseMultiplierAndApply(input, data);
magnitude = input.isZero() ? 0 : input.getMagnitude();
magnitude -= multiplier;
}
StandardPlural plural = input.getStandardPlural(rules);
String patternString = data.getPattern(magnitude, plural);
int numDigits = -1;
if (patternString == null) {
// Use the default (non-compact) modifier.
// No need to take any action.
} else if (precomputedMods != null) {
// Build code path.
CompactModInfo info = precomputedMods.get(patternString);
info.mod.applyToMicros(micros, input);
numDigits = info.numDigits;
} else {
// Non-build code path.
// Overwrite the PatternInfo in the existing modMiddle
assert micros.modMiddle instanceof MurkyModifier;
PatternParseResult patternInfo = LdmlPatternInfo.parse(patternString);
((MurkyModifier) micros.modMiddle).setPatternInfo(patternInfo);
numDigits = patternInfo.positive.totalIntegerDigits;
}
// FIXME: Deal with numDigits == 0 (Awaiting a test case)
// We already performed rounding. Do not perform it again.
micros.rounding = RoundingImplDummy.INSTANCE;
return micros;
}
}

View File

@ -0,0 +1,173 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.UnicodeSet;
/** Identical to {@link ConstantMultiFieldModifier}, but supports currency spacing. */
public class CurrencySpacingEnabledModifier extends ConstantMultiFieldModifier {
// These are the default currency spacing UnicodeSets in CLDR.
// Pre-compute them for performance.
// TODO: Is there a way to write a unit test to make sure these hard-coded values
// stay consistent with CLDR?
private static final UnicodeSet UNISET_DIGIT = new UnicodeSet("[:digit:]").freeze();
private static final UnicodeSet UNISET_NOTS = new UnicodeSet("[:^S:]").freeze();
// Constants for better readability. Types are for compiler checking.
static final byte PREFIX = 0;
static final byte SUFFIX = 1;
static final short IN_CURRENCY = 0;
static final short IN_NUMBER = 1;
private final UnicodeSet afterPrefixUnicodeSet;
private final String afterPrefixInsert;
private final UnicodeSet beforeSuffixUnicodeSet;
private final String beforeSuffixInsert;
/** Build code path */
public CurrencySpacingEnabledModifier(
NumberStringBuilder prefix,
NumberStringBuilder suffix,
boolean strong,
DecimalFormatSymbols symbols) {
super(prefix, suffix, strong);
// Check for currency spacing. Do not build the UnicodeSets unless there is
// a currency code point at a boundary.
if (prefixFields.length > 0
&& prefixFields[prefixFields.length - 1] == NumberFormat.Field.CURRENCY) {
int prefixCp = Character.codePointBefore(prefixChars, prefixChars.length);
UnicodeSet prefixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, PREFIX);
if (prefixUnicodeSet.contains(prefixCp)) {
afterPrefixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, PREFIX);
afterPrefixInsert = getInsertString(symbols, PREFIX);
} else {
afterPrefixUnicodeSet = null;
afterPrefixInsert = null;
}
} else {
afterPrefixUnicodeSet = null;
afterPrefixInsert = null;
}
if (suffixFields.length > 0 && suffixFields[0] == NumberFormat.Field.CURRENCY) {
int suffixCp = Character.codePointAt(suffixChars, 0);
UnicodeSet suffixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, SUFFIX);
if (suffixUnicodeSet.contains(suffixCp)) {
beforeSuffixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, SUFFIX);
beforeSuffixInsert = getInsertString(symbols, SUFFIX);
} else {
beforeSuffixUnicodeSet = null;
beforeSuffixInsert = null;
}
} else {
beforeSuffixUnicodeSet = null;
beforeSuffixInsert = null;
}
}
/** Non-build code path */
public static int applyCurrencySpacing(
NumberStringBuilder output,
int prefixStart,
int prefixLen,
int suffixStart,
int suffixLen,
DecimalFormatSymbols symbols) {
int length = 0;
boolean hasPrefix = (prefixLen > 0);
boolean hasSuffix = (suffixLen > 0);
boolean hasNumber = (suffixStart - prefixStart - prefixLen > 0); // could be empty string
if (hasPrefix && hasNumber) {
length += applyCurrencySpacingAffix(output, prefixStart + prefixLen, PREFIX, symbols);
}
if (hasSuffix && hasNumber) {
length += applyCurrencySpacingAffix(output, suffixStart + length, SUFFIX, symbols);
}
return length;
}
private static int applyCurrencySpacingAffix(
NumberStringBuilder output, int index, byte affix, DecimalFormatSymbols symbols) {
// NOTE: For prefix, output.fieldAt(index-1) gets the last field type in the prefix.
// This works even if the last code point in the prefix is 2 code units because the
// field value gets populated to both indices in the field array.
NumberFormat.Field affixField =
(affix == PREFIX) ? output.fieldAt(index - 1) : output.fieldAt(index);
if (affixField != NumberFormat.Field.CURRENCY) {
return 0;
}
int affixCp =
(affix == PREFIX)
? Character.codePointBefore(output, index)
: Character.codePointAt(output, index);
UnicodeSet affixUniset = getUnicodeSet(symbols, IN_CURRENCY, affix);
if (!affixUniset.contains(affixCp)) {
return 0;
}
int numberCp =
(affix == PREFIX)
? Character.codePointAt(output, index)
: Character.codePointBefore(output, index);
UnicodeSet numberUniset = getUnicodeSet(symbols, IN_NUMBER, affix);
if (!numberUniset.contains(numberCp)) {
return 0;
}
String spacingString = getInsertString(symbols, affix);
// NOTE: This next line *inserts* the spacing string, triggering an arraycopy.
// It would be more efficient if this could be done before affixes were attached,
// so that it could be prepended/appended instead of inserted.
// However, the build code path is more efficient, and this is the most natural
// place to put currency spacing in the non-build code path.
// TODO: Should we use the CURRENCY field here?
return output.insert(index, spacingString, null);
}
private static UnicodeSet getUnicodeSet(DecimalFormatSymbols symbols, short position, byte affix) {
String pattern =
symbols.getPatternForCurrencySpacing(
position == IN_CURRENCY
? DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH
: DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH,
affix == SUFFIX);
if (pattern.equals("[:digit:]")) {
return UNISET_DIGIT;
} else if (pattern.equals("[:^S:]")) {
return UNISET_NOTS;
} else {
return new UnicodeSet(pattern);
}
}
private static String getInsertString(DecimalFormatSymbols symbols, byte affix) {
return symbols.getPatternForCurrencySpacing(
DecimalFormatSymbols.CURRENCY_SPC_INSERT, affix == SUFFIX);
}
@Override
public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
// Currency spacing logic
int length = 0;
if (rightIndex - leftIndex > 0
&& afterPrefixUnicodeSet != null
&& afterPrefixUnicodeSet.contains(Character.codePointAt(output, leftIndex))) {
// TODO: Should we use the CURRENCY field here?
length += output.insert(leftIndex, afterPrefixInsert, null);
}
if (rightIndex - leftIndex > 0
&& beforeSuffixUnicodeSet != null
&& beforeSuffixUnicodeSet.contains(Character.codePointBefore(output, rightIndex))) {
// TODO: Should we use the CURRENCY field here?
length += output.insert(rightIndex + length, beforeSuffixInsert, null);
}
// Call super for the remaining logic
length += super.apply(output, leftIndex, rightIndex + length);
return length;
}
}

View File

@ -0,0 +1,61 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.ULocale;
public class CustomSymbolCurrency extends Currency {
private String symbol1;
private String symbol2;
public static Currency resolve(Currency currency, ULocale locale, DecimalFormatSymbols symbols) {
if (currency == null) {
currency = symbols.getCurrency();
}
String currency1Sym = symbols.getCurrencySymbol();
String currency2Sym = symbols.getInternationalCurrencySymbol();
if (currency == null) {
return new CustomSymbolCurrency("XXX", currency1Sym, currency2Sym);
}
if (!currency.equals(symbols.getCurrency())) {
return currency;
}
String currency1 = currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
String currency2 = currency.getCurrencyCode();
if (!currency1.equals(currency1Sym) || !currency2.equals(currency2Sym)) {
return new CustomSymbolCurrency(currency2, currency1Sym, currency2Sym);
}
return currency;
}
public CustomSymbolCurrency(String isoCode, String currency1Sym, String currency2Sym) {
super(isoCode);
this.symbol1 = currency1Sym;
this.symbol2 = currency2Sym;
}
@Override
public String getName(ULocale locale, int nameStyle, boolean[] isChoiceFormat) {
if (nameStyle == SYMBOL_NAME) {
return symbol1;
}
return super.getName(locale, nameStyle, isChoiceFormat);
}
@Override
public String getName(
ULocale locale, int nameStyle, String pluralCount, boolean[] isChoiceFormat) {
if (nameStyle == PLURAL_LONG_NAME && subType.equals("XXX")) {
// Plural in absence of a currency should return the symbol
return symbol1;
}
return super.getName(locale, nameStyle, pluralCount, isChoiceFormat);
}
@Override
public String getCurrencyCode() {
return symbol2;
}
}

View File

@ -0,0 +1,67 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.util.EnumMap;
import java.util.Map;
import com.ibm.icu.impl.CurrencyData;
import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.modifiers.SimpleModifier;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.NumberFormat.Field;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
public class DataUtils {
public static Map<StandardPlural, Modifier> getCurrencyLongNameModifiers(
ULocale loc, Currency currency) {
Map<String, String> data = CurrencyData.provider.getInstance(loc, true).getUnitPatterns();
Map<StandardPlural, Modifier> result =
new EnumMap<StandardPlural, Modifier>(StandardPlural.class);
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> e : data.entrySet()) {
String pluralKeyword = e.getKey();
StandardPlural plural = StandardPlural.fromString(e.getKey());
String longName = currency.getName(loc, Currency.PLURAL_LONG_NAME, pluralKeyword, null);
String simpleFormat = e.getValue(); // e.g., "{0} {1}"
simpleFormat = simpleFormat.replace("{1}", longName);
String compiled = SimpleFormatterImpl.compileToStringMinMaxArguments(simpleFormat, sb, 1, 1);
Modifier mod = new SimpleModifier(compiled, Field.CURRENCY, false);
result.put(plural, mod);
}
return result;
}
public static Map<StandardPlural, Modifier> getMeasureUnitModifiers(
ULocale loc, MeasureUnit unit, FormatWidth width) {
Map<StandardPlural, String> simpleFormats = MeasureData.getMeasureData(loc, unit, width);
Map<StandardPlural, Modifier> result =
new EnumMap<StandardPlural, Modifier>(StandardPlural.class);
StringBuilder sb = new StringBuilder();
for (StandardPlural plural : StandardPlural.VALUES) {
if (simpleFormats.get(plural) == null) {
plural = StandardPlural.OTHER;
}
String simpleFormat = simpleFormats.get(plural);
String compiled = SimpleFormatterImpl.compileToStringMinMaxArguments(simpleFormat, sb, 1, 1);
Modifier mod = new SimpleModifier(compiled, Field.CURRENCY, false);
result.put(plural, mod);
}
return result;
// Map<StandardPlural, Modifier> result =
// new EnumMap<StandardPlural, Modifier>(StandardPlural.class);
// // TODO: Get the data directly instead of taking the detour through MeasureFormat.
// MeasureFormat mf = MeasureFormat.getInstance(loc, width);
// for (StandardPlural plural : StandardPlural.VALUES) {
// String compiled = mf.getPluralFormatter(unit, width, plural.ordinal());
// Modifier mod = new SimpleModifier(compiled, null, false);
// result.put(plural, mod);
// }
// return result;
}
}

View File

@ -0,0 +1,134 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.math.BigDecimal;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
import newapi.NumberFormatter.Grouping;
import newapi.NumberFormatter.IGrouping;
public class GroupingImpl extends Grouping.Internal {
// Conveniences for Java handling of shorts
private static final short S2 = 2;
private static final short S3 = 3;
// For the "placeholder constructor"
public static final char TYPE_PLACEHOLDER = 0;
public static final char TYPE_MIN2 = 1;
public static final char TYPE_NONE = 2;
// Statically initialized objects (cannot be used statically by other ICU classes)
static final GroupingImpl NONE = new GroupingImpl(TYPE_NONE);
static final GroupingImpl GROUPING_3 = new GroupingImpl(S3, S3, false);
static final GroupingImpl GROUPING_3_2 = new GroupingImpl(S3, S2, false);
static final GroupingImpl GROUPING_3_MIN2 = new GroupingImpl(S3, S3, true);
static final GroupingImpl GROUPING_3_2_MIN2 = new GroupingImpl(S3, S2, true);
static GroupingImpl getInstance(short grouping1, short grouping2, boolean min2) {
if (grouping1 == -1) {
return NONE;
} else if (!min2 && grouping1 == 3 && grouping2 == 3) {
return GROUPING_3;
} else if (!min2 && grouping1 == 3 && grouping2 == 2) {
return GROUPING_3_2;
} else if (min2 && grouping1 == 3 && grouping2 == 3) {
return GROUPING_3_MIN2;
} else if (min2 && grouping1 == 3 && grouping2 == 2) {
return GROUPING_3_2_MIN2;
} else {
return new GroupingImpl(grouping1, grouping2, min2);
}
}
public static GroupingImpl normalizeType(IGrouping grouping, PatternParseResult patternInfo) {
assert grouping != null;
if (grouping instanceof GroupingImpl) {
return ((GroupingImpl) grouping).withLocaleData(patternInfo);
} else {
return new GroupingImpl(grouping);
}
}
final IGrouping lambda;
final short grouping1; // -2 means "needs locale data"; -1 means "no grouping"
final short grouping2;
final boolean min2;
/** The "placeholder constructor". Pass in one of the GroupingImpl.TYPE_* variables. */
public GroupingImpl(char type) {
lambda = null;
switch (type) {
case TYPE_PLACEHOLDER:
grouping1 = -2;
grouping2 = -2;
min2 = false;
break;
case TYPE_MIN2:
grouping1 = -2;
grouping2 = -2;
min2 = true;
break;
case TYPE_NONE:
grouping1 = -1;
grouping2 = -1;
min2 = false;
break;
default:
throw new AssertionError();
}
}
private GroupingImpl(short grouping1, short grouping2, boolean min2) {
this.lambda = null;
this.grouping1 = grouping1;
this.grouping2 = grouping2;
this.min2 = min2;
}
private GroupingImpl(IGrouping lambda) {
this.lambda = lambda;
this.grouping1 = -3;
this.grouping2 = -3;
this.min2 = false;
}
GroupingImpl withLocaleData(PatternParseResult patternInfo) {
if (grouping1 != -2) {
return this;
}
assert lambda == null;
short grouping1 = (short) (patternInfo.positive.groupingSizes & 0xffff);
short grouping2 = (short) ((patternInfo.positive.groupingSizes >>> 16) & 0xffff);
short grouping3 = (short) ((patternInfo.positive.groupingSizes >>> 32) & 0xffff);
if (grouping2 == -1) {
grouping1 = -1;
}
if (grouping3 == -1) {
grouping2 = grouping1;
}
return getInstance(grouping1, grouping2, min2);
}
boolean groupAtPosition(int position, FormatQuantity value) {
// Check for lambda function
if (lambda != null) {
// TODO: Cache the BigDecimal
BigDecimal temp = value.toBigDecimal();
return lambda.groupAtPosition(position, temp);
}
assert grouping1 != -2;
if (grouping1 == -1 || grouping1 == 0) {
// Either -1 or 0 means "no grouping"
return false;
}
position -= grouping1;
return position >= 0
&& (position % grouping2) == 0
&& value.getUpperDisplayMagnitude() - grouping1 + 1 >= (min2 ? 2 : 1);
}
}

View File

@ -0,0 +1,27 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import newapi.NumberFormatter.IntegerWidth;
public final class IntegerWidthImpl extends IntegerWidth.Internal {
public final int minInt;
public final int maxInt;
public static final IntegerWidthImpl DEFAULT = new IntegerWidthImpl();
/** Default constructor */
public IntegerWidthImpl() {
this(1, Integer.MAX_VALUE);
}
public IntegerWidthImpl(int minInt, int maxInt) {
this.minInt = minInt;
this.maxInt = maxInt;
}
@Override
public IntegerWidthImpl truncateAt(int maxInt) {
return new IntegerWidthImpl(minInt, maxInt);
}
}

View File

@ -0,0 +1,105 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.util.Objects;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.IGrouping;
import newapi.NumberFormatter.IRounding;
import newapi.NumberFormatter.IntegerWidth;
import newapi.NumberFormatter.Notation;
import newapi.NumberFormatter.Padding;
import newapi.NumberFormatter.SignDisplay;
public class MacroProps implements Cloneable {
public Notation notation;
public MeasureUnit unit;
public IRounding rounding;
public IGrouping grouping;
public Padding padding;
public IntegerWidth integerWidth;
public Object symbols;
public FormatWidth unitWidth;
public SignDisplay sign;
public DecimalMarkDisplay decimal;
public AffixPatternProvider affixProvider; // not in API; for JDK compatibility mode only
public MultiplierImpl multiplier; // not in API; for JDK compatibility mode only
public PluralRules rules; // not in API; could be made public in the future
public ULocale loc;
/**
* Copies values from fallback into this instance if they are null in this instance.
*
* @param fallback The instance to copy from; not modified by this operation.
*/
public void fallback(MacroProps fallback) {
if (notation == null) notation = fallback.notation;
if (unit == null) unit = fallback.unit;
if (rounding == null) rounding = fallback.rounding;
if (grouping == null) grouping = fallback.grouping;
if (padding == null) padding = fallback.padding;
if (integerWidth == null) integerWidth = fallback.integerWidth;
if (symbols == null) symbols = fallback.symbols;
if (unitWidth == null) unitWidth = fallback.unitWidth;
if (sign == null) sign = fallback.sign;
if (decimal == null) decimal = fallback.decimal;
if (affixProvider == null) affixProvider = fallback.affixProvider;
if (multiplier == null) multiplier = fallback.multiplier;
if (rules == null) rules = fallback.rules;
if (loc == null) loc = fallback.loc;
}
@Override
public int hashCode() {
return Objects.hash(
notation,
unit,
rounding,
grouping,
padding,
integerWidth,
symbols,
unitWidth,
sign,
decimal,
affixProvider,
multiplier,
rules,
loc);
}
@Override
public boolean equals(Object _other) {
MacroProps other = (MacroProps) _other;
return Objects.equals(notation, other.notation)
&& Objects.equals(unit, other.unit)
&& Objects.equals(rounding, other.rounding)
&& Objects.equals(grouping, other.grouping)
&& Objects.equals(padding, other.padding)
&& Objects.equals(integerWidth, other.integerWidth)
&& Objects.equals(symbols, other.symbols)
&& Objects.equals(unitWidth, other.unitWidth)
&& Objects.equals(sign, other.sign)
&& Objects.equals(decimal, other.decimal)
&& Objects.equals(affixProvider, other.affixProvider)
&& Objects.equals(multiplier, other.multiplier)
&& Objects.equals(rules, other.rules)
&& Objects.equals(loc, other.loc);
}
@Override
public Object clone() {
// TODO: Remove this method?
try {
return super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}

View File

@ -0,0 +1,64 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.util.EnumMap;
import java.util.Map;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
public class MeasureData {
private static final class ShanesMeasureUnitSink extends UResource.Sink {
Map<StandardPlural, String> output;
public ShanesMeasureUnitSink(Map<StandardPlural, String> output) {
this.output = output;
}
@Override
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
UResource.Table pluralsTable = value.getTable();
for (int i1 = 0; pluralsTable.getKeyAndValue(i1, key, value); ++i1) {
if (key.contentEquals("dnam") || key.contentEquals("per")) {
continue;
}
StandardPlural plural = StandardPlural.fromString(key);
if (output.containsKey(plural)) {
continue;
}
String formatString = value.getString();
output.put(plural, formatString);
}
}
}
public static Map<StandardPlural, String> getMeasureData(
ULocale locale, MeasureUnit unit, FormatWidth width) {
ICUResourceBundle resource =
(ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale);
StringBuilder key = new StringBuilder();
key.append("units");
if (width == FormatWidth.NARROW) {
key.append("Narrow");
} else if (width == FormatWidth.SHORT) {
key.append("Short");
}
key.append("/");
key.append(unit.getType());
key.append("/");
key.append(unit.getSubtype());
Map<StandardPlural, String> output = new EnumMap<StandardPlural, String>(StandardPlural.class);
ShanesMeasureUnitSink sink = new ShanesMeasureUnitSink(output);
resource.getAllItemsWithFallback(key.toString(), sink);
return output;
}
}

View File

@ -0,0 +1,58 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.text.DecimalFormatSymbols;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.SignDisplay;
public class MicroProps implements Cloneable, QuantityChain {
// Populated globally:
public SignDisplay sign;
public DecimalFormatSymbols symbols;
public PaddingImpl padding;
public DecimalMarkDisplay decimal;
public IntegerWidthImpl integerWidth;
// Populated by notation/unit:
public Modifier modOuter;
public Modifier modMiddle;
public Modifier modInner;
public RoundingImpl rounding;
public GroupingImpl grouping;
public int multiplier;
public boolean useCurrency;
private boolean frozen = false;
public void enableCloneInChain() {
frozen = true;
}
@Override
public QuantityChain chain(QuantityChain parent) {
// The MicroProps instance should always be at the top of the chain!
throw new AssertionError();
}
@Override
public MicroProps withQuantity(FormatQuantity quantity) {
if (frozen) {
return (MicroProps) this.clone();
} else {
return this;
}
}
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}

View File

@ -0,0 +1,39 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.math.BigDecimal;
import com.ibm.icu.impl.number.FormatQuantity;
public class MultiplierImpl implements QuantityChain {
final int magnitudeMultiplier;
final BigDecimal bigDecimalMultiplier;
/* final */ QuantityChain parent;
public MultiplierImpl(int magnitudeMultiplier) {
this.magnitudeMultiplier = magnitudeMultiplier;
this.bigDecimalMultiplier = null;
}
public MultiplierImpl(BigDecimal bigDecimalMultiplier) {
this.magnitudeMultiplier = 0;
this.bigDecimalMultiplier = bigDecimalMultiplier;
}
@Override
public QuantityChain chain(QuantityChain parent) {
this.parent = parent;
return this;
}
@Override
public MicroProps withQuantity(FormatQuantity quantity) {
MicroProps micros = parent.withQuantity(quantity);
quantity.adjustMagnitude(magnitudeMultiplier);
if (bigDecimalMultiplier != null) {
quantity.multiplyBy(bigDecimalMultiplier);
}
return micros;
}
}

View File

@ -0,0 +1,414 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.AffixPatternUtils;
import com.ibm.icu.impl.number.AffixPatternUtils.SymbolProvider;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
import newapi.NumberFormatter.SignDisplay;
/**
* This is a MUTABLE, NON-THREAD-SAFE class designed for performance. Do NOT save references to this
* or attempt to use it from multiple threads!!!
*
* <p>This class takes a parsed pattern and returns a Modifier, without creating any objects. When
* the Modifier methods are called, symbols are substituted directly into the output
* NumberStringBuilder, without creating any intermediate Strings.
*/
public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, QuantityChain {
// Modifier details
final boolean isStrong;
// Pattern details
AffixPatternProvider patternInfo;
SignDisplay signDisplay;
boolean perMilleReplacesPercent;
// Symbol details
DecimalFormatSymbols symbols;
FormatWidth unitWidth;
String currency1;
String currency2;
String[] currency3;
PluralRules rules;
// Number details
boolean isNegative;
StandardPlural plural;
// QuantityChain details
QuantityChain parent;
// Transient CharSequence fields
boolean inCharSequenceMode;
int flags;
int length;
boolean prependSign;
boolean plusReplacesMinusSign;
public MurkyModifier(boolean isStrong) {
this.isStrong = isStrong;
}
public void setPatternInfo(AffixPatternProvider patternInfo) {
this.patternInfo = patternInfo;
}
public void setPatternAttributes(SignDisplay signDisplay, boolean perMille) {
this.signDisplay = signDisplay;
this.perMilleReplacesPercent = perMille;
}
public void setSymbols(
DecimalFormatSymbols symbols, Currency currency, FormatWidth unitWidth, PluralRules rules) {
assert (rules != null) == needsPlurals();
this.symbols = symbols;
this.unitWidth = unitWidth;
this.rules = rules;
currency1 = currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
currency2 = currency.getCurrencyCode();
if (rules != null) {
currency3 = new String[StandardPlural.COUNT];
for (StandardPlural plural : StandardPlural.VALUES) {
currency3[plural.ordinal()] =
currency.getName(
symbols.getULocale(), Currency.PLURAL_LONG_NAME, plural.getKeyword(), null);
}
}
}
public void setNumberProperties(boolean isNegative, StandardPlural plural) {
assert (plural != null) == needsPlurals();
this.isNegative = isNegative;
this.plural = plural;
}
/**
* Returns true if the pattern represented by this MurkyModifier requires a plural keyword in
* order to localize. This is currently true only if there is a currency long name placeholder in
* the pattern.
*/
public boolean needsPlurals() {
return patternInfo.containsSymbolType(AffixPatternUtils.TYPE_CURRENCY_TRIPLE);
}
@Override
public QuantityChain chain(QuantityChain parent) {
this.parent = parent;
return this;
}
@Override
public MicroProps withQuantity(FormatQuantity fq) {
MicroProps micros = parent.withQuantity(fq);
if (needsPlurals()) {
// TODO: Fix this. Avoid the copy.
FormatQuantity copy = fq.createCopy();
micros.rounding.apply(copy);
setNumberProperties(fq.isNegative(), copy.getStandardPlural(rules));
} else {
setNumberProperties(fq.isNegative(), null);
}
micros.modMiddle = this;
return micros;
}
/**
* Creates a new quantity-dependent Modifier that behaves the same as the current instance, but
* which is immutable and can be saved for future use. The current instance is not changed by
* calling this method except for the number properties.
*
* @return An immutable that supports both positive and negative numbers.
*/
public ImmutableMurkyModifier createImmutable() {
NumberStringBuilder a = new NumberStringBuilder();
NumberStringBuilder b = new NumberStringBuilder();
if (needsPlurals()) {
// Slower path when we require the plural keyword.
Modifier[] mods = new Modifier[ImmutableMurkyModifierWithPlurals.getModsLength()];
for (StandardPlural plural : StandardPlural.VALUES) {
setNumberProperties(false, plural);
Modifier positive = createConstantModifier(a, b);
setNumberProperties(true, plural);
Modifier negative = createConstantModifier(a, b);
mods[ImmutableMurkyModifierWithPlurals.getModIndex(false, plural)] = positive;
mods[ImmutableMurkyModifierWithPlurals.getModIndex(true, plural)] = negative;
}
return new ImmutableMurkyModifierWithPlurals(mods, rules);
} else {
// Faster path when plural keyword is not needed.
setNumberProperties(false, null);
Modifier positive = createConstantModifier(a, b);
setNumberProperties(true, null);
Modifier negative = createConstantModifier(a, b);
return new ImmutableMurkyModifierWithoutPlurals(positive, negative);
}
}
private Modifier createConstantModifier(NumberStringBuilder a, NumberStringBuilder b) {
insertPrefix(a.clear(), 0);
insertSuffix(b.clear(), 0);
if (patternInfo.hasCurrencySign()) {
return new CurrencySpacingEnabledModifier(a, b, isStrong, symbols);
} else {
return new ConstantMultiFieldModifier(a, b, isStrong);
}
}
public static interface ImmutableMurkyModifier extends QuantityChain {
public void applyToMicros(MicroProps micros, FormatQuantity quantity);
}
public static class ImmutableMurkyModifierWithoutPlurals implements ImmutableMurkyModifier {
final Modifier positive;
final Modifier negative;
/* final */ QuantityChain parent;
public ImmutableMurkyModifierWithoutPlurals(Modifier positive, Modifier negative) {
this.positive = positive;
this.negative = negative;
}
@Override
public QuantityChain chain(QuantityChain parent) {
this.parent = parent;
return this;
}
@Override
public MicroProps withQuantity(FormatQuantity quantity) {
MicroProps micros = parent.withQuantity(quantity);
applyToMicros(micros, quantity);
return micros;
}
@Override
public void applyToMicros(MicroProps micros, FormatQuantity quantity) {
if (quantity.isNegative()) {
micros.modMiddle = negative;
} else {
micros.modMiddle = positive;
}
}
}
public static class ImmutableMurkyModifierWithPlurals implements ImmutableMurkyModifier {
final Modifier[] mods;
final PluralRules rules;
/* final */ QuantityChain parent;
public ImmutableMurkyModifierWithPlurals(Modifier[] mods, PluralRules rules) {
assert mods.length == getModsLength();
assert rules != null;
this.mods = mods;
this.rules = rules;
}
public static int getModsLength() {
return 2 * StandardPlural.COUNT;
}
public static int getModIndex(boolean isNegative, StandardPlural plural) {
return plural.ordinal() * 2 + (isNegative ? 1 : 0);
}
@Override
public QuantityChain chain(QuantityChain parent) {
this.parent = parent;
return this;
}
@Override
public MicroProps withQuantity(FormatQuantity quantity) {
MicroProps micros = parent.withQuantity(quantity);
applyToMicros(micros, quantity);
return micros;
}
@Override
public void applyToMicros(MicroProps micros, FormatQuantity quantity) {
// TODO: Fix this. Avoid the copy.
FormatQuantity copy = quantity.createCopy();
copy.roundToInfinity();
StandardPlural plural = copy.getStandardPlural(rules);
Modifier mod = mods[getModIndex(quantity.isNegative(), plural)];
micros.modMiddle = mod;
}
}
@Override
public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
int prefixLen = insertPrefix(output, leftIndex);
int suffixLen = insertSuffix(output, rightIndex + prefixLen);
CurrencySpacingEnabledModifier.applyCurrencySpacing(
output, leftIndex, prefixLen, rightIndex + prefixLen, suffixLen, symbols);
return prefixLen + suffixLen;
}
@Override
public boolean isStrong() {
return isStrong;
}
@Override
public String getPrefix() {
NumberStringBuilder sb = new NumberStringBuilder(10);
insertPrefix(sb, 0);
return sb.toString();
}
@Override
public String getSuffix() {
NumberStringBuilder sb = new NumberStringBuilder(10);
insertSuffix(sb, 0);
return sb.toString();
}
private int insertPrefix(NumberStringBuilder sb, int position) {
enterCharSequenceMode(true);
int length = AffixPatternUtils.unescape(this, sb, position, this);
exitCharSequenceMode();
return length;
}
private int insertSuffix(NumberStringBuilder sb, int position) {
enterCharSequenceMode(false);
int length = AffixPatternUtils.unescape(this, sb, position, this);
exitCharSequenceMode();
return length;
}
@Override
public CharSequence getSymbol(int type) {
switch (type) {
case AffixPatternUtils.TYPE_MINUS_SIGN:
return symbols.getMinusSignString();
case AffixPatternUtils.TYPE_PLUS_SIGN:
return symbols.getPlusSignString();
case AffixPatternUtils.TYPE_PERCENT:
return symbols.getPercentString();
case AffixPatternUtils.TYPE_PERMILLE:
return symbols.getPerMillString();
case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
// FormatWidth ISO overrides the singular currency symbol
if (unitWidth == FormatWidth.SHORT) {
return currency2;
} else {
return currency1;
}
case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
return currency2;
case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
// NOTE: This is the code path only for patterns containing "".
// Most plural currencies are formatted in DataUtils.
assert plural != null;
if (currency3 == null) {
return currency2;
} else {
return currency3[plural.ordinal()];
}
case AffixPatternUtils.TYPE_CURRENCY_QUAD:
return "\uFFFD";
case AffixPatternUtils.TYPE_CURRENCY_QUINT:
return "\uFFFD";
default:
throw new AssertionError();
}
}
/** This method contains the heart of the logic for rendering LDML affix strings. */
private void enterCharSequenceMode(boolean isPrefix) {
assert !inCharSequenceMode;
inCharSequenceMode = true;
// Should the output render '+' where '-' would normally appear in the pattern?
plusReplacesMinusSign =
!isNegative
&& signDisplay == SignDisplay.ALWAYS_SHOWN
&& patternInfo.positiveHasPlusSign() == false;
// Should we use the negative affix pattern? (If not, we will use the positive one)
boolean useNegativeAffixPattern =
patternInfo.hasNegativeSubpattern()
&& (isNegative || (patternInfo.negativeHasMinusSign() && plusReplacesMinusSign));
// Resolve the flags for the affix pattern.
flags = 0;
if (useNegativeAffixPattern) {
flags |= AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN;
}
if (isPrefix) {
flags |= AffixPatternProvider.Flags.PREFIX;
}
if (plural != null) {
assert plural.ordinal() == (AffixPatternProvider.Flags.PLURAL_MASK & plural.ordinal());
flags |= plural.ordinal();
}
// Should we prepend a sign to the pattern?
if (!isPrefix || useNegativeAffixPattern) {
prependSign = false;
} else if (isNegative) {
prependSign = signDisplay != SignDisplay.NEVER_SHOWN;
} else {
prependSign = plusReplacesMinusSign;
}
// Finally, compute the length of the affix pattern.
length = patternInfo.length(flags) + (prependSign ? 1 : 0);
}
private void exitCharSequenceMode() {
assert inCharSequenceMode;
inCharSequenceMode = false;
}
@Override
public int length() {
if (inCharSequenceMode) {
return length;
} else {
NumberStringBuilder sb = new NumberStringBuilder(20);
apply(sb, 0, 0);
return sb.length();
}
}
@Override
public char charAt(int index) {
assert inCharSequenceMode;
char candidate;
if (prependSign && index == 0) {
candidate = '-';
} else if (prependSign) {
candidate = patternInfo.charAt(flags, index - 1);
} else {
candidate = patternInfo.charAt(flags, index);
}
if (plusReplacesMinusSign && candidate == '-') {
return '+';
}
if (perMilleReplacesPercent && candidate == '%') {
return '‰';
}
return candidate;
}
@Override
public CharSequence subSequence(int start, int end) {
// Should never be called in normal circumstances
throw new AssertionError();
}
}

View File

@ -0,0 +1,81 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.util.Map;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import newapi.NumberFormatter.NotationCompact;
import newapi.NumberFormatter.NotationScientific;
import newapi.NumberFormatter.SignDisplay;
@SuppressWarnings("deprecation")
public class NotationImpl {
public static class NotationScientificImpl extends NotationScientific.Internal
implements Cloneable {
int engineeringInterval;
boolean requireMinInt;
int minExponentDigits;
SignDisplay exponentSignDisplay;
public NotationScientificImpl(int engineeringInterval) {
this.engineeringInterval = engineeringInterval;
requireMinInt = false;
minExponentDigits = 1;
exponentSignDisplay = SignDisplay.AUTO;
}
public NotationScientificImpl(
int engineeringInterval,
boolean requireMinInt,
int minExponentDigits,
SignDisplay exponentSignDisplay) {
this.engineeringInterval = engineeringInterval;
this.requireMinInt = requireMinInt;
this.minExponentDigits = minExponentDigits;
this.exponentSignDisplay = exponentSignDisplay;
}
@Override
public NotationScientific withMinExponentDigits(int minExponentDigits) {
NotationScientificImpl other = (NotationScientificImpl) this.clone();
other.minExponentDigits = minExponentDigits;
return other;
}
@Override
public NotationScientific withExponentSignDisplay(SignDisplay exponentSignDisplay) {
NotationScientificImpl other = (NotationScientificImpl) this.clone();
other.exponentSignDisplay = exponentSignDisplay;
return other;
}
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
// Should not happen since parent is Object
throw new AssertionError(e);
}
}
}
public static class NotationCompactImpl extends NotationCompact.Internal {
final CompactStyle compactStyle;
final Map<String, Map<String, String>> compactCustomData;
public NotationCompactImpl(CompactStyle compactStyle) {
compactCustomData = null;
this.compactStyle = compactStyle;
}
public NotationCompactImpl(Map<String, Map<String, String>> compactCustomData) {
compactStyle = null;
this.compactCustomData = compactCustomData;
}
}
}

View File

@ -0,0 +1,328 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import com.ibm.icu.impl.number.FormatQuantity4;
import com.ibm.icu.impl.number.FormatQuantityBCD;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.PatternString;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
import newapi.NumberFormatter;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.IGrouping;
import newapi.NumberFormatter.IRounding;
import newapi.NumberFormatter.IntegerWidth;
import newapi.NumberFormatter.Notation;
import newapi.NumberFormatter.NumberFormatterResult;
import newapi.NumberFormatter.Padding;
import newapi.NumberFormatter.SignDisplay;
import newapi.NumberFormatter.UnlocalizedNumberFormatter;
/** @author sffc */
public class NumberFormatterImpl extends NumberFormatter.LocalizedNumberFormatter.Internal {
private static final NumberFormatterImpl BASE = new NumberFormatterImpl();
// TODO: Set a good value here.
static final int DEFAULT_THRESHOLD = 3;
static final int KEY_MACROS = 0;
static final int KEY_LOCALE = 1;
static final int KEY_NOTATION = 2;
static final int KEY_UNIT = 3;
static final int KEY_ROUNDING = 4;
static final int KEY_GROUPING = 5;
static final int KEY_PADDING = 6;
static final int KEY_INTEGER = 7;
static final int KEY_SYMBOLS = 8;
static final int KEY_UNIT_WIDTH = 9;
static final int KEY_SIGN = 10;
static final int KEY_DECIMAL = 11;
static final int KEY_MAX = 12;
public static NumberFormatterImpl with() {
return BASE;
}
/** Internal method to set a starting macros. */
public static NumberFormatterImpl fromMacros(MacroProps macros) {
return new NumberFormatterImpl(BASE, KEY_MACROS, macros);
}
/**
* Internal method to construct a chain from a pattern using {@link NumberPropertyMapper}. Could
* be added to the public API if the feature is requested. In that case, a more efficient
* implementation may be desired.
*/
public static UnlocalizedNumberFormatter fromPattern(
String string, DecimalFormatSymbols symbols) {
Properties props = PatternString.parseToProperties(string);
MacroProps macros = NumberPropertyMapper.oldToNew(props, symbols, null);
return fromMacros(macros);
}
// TODO: Reduce the number of fields.
final NumberFormatterImpl parent;
final int key;
final Object value;
volatile MacroProps resolvedMacros;
volatile AtomicInteger callCount;
volatile NumberFormatterImpl savedWithUnit;
volatile Worker1 compiled;
/** Base constructor; called during startup only */
private NumberFormatterImpl() {
parent = null;
key = -1;
value = null;
}
/** Primary constructor */
private NumberFormatterImpl(NumberFormatterImpl parent, int key, Object value) {
this.parent = parent;
this.key = key;
this.value = value;
}
@Override
public NumberFormatterImpl notation(Notation notation) {
return new NumberFormatterImpl(this, KEY_NOTATION, notation);
}
@Override
public NumberFormatterImpl unit(MeasureUnit unit) {
return new NumberFormatterImpl(this, KEY_UNIT, unit);
}
@Override
public NumberFormatterImpl rounding(IRounding rounding) {
return new NumberFormatterImpl(this, KEY_ROUNDING, rounding);
}
@Override
public NumberFormatterImpl grouping(IGrouping grouping) {
return new NumberFormatterImpl(this, KEY_GROUPING, grouping);
}
@Override
public NumberFormatterImpl padding(Padding padding) {
return new NumberFormatterImpl(this, KEY_PADDING, padding);
}
@Override
public NumberFormatterImpl integerWidth(IntegerWidth style) {
return new NumberFormatterImpl(this, KEY_INTEGER, style);
}
@Override
public NumberFormatterImpl symbols(DecimalFormatSymbols symbols) {
return new NumberFormatterImpl(this, KEY_SYMBOLS, symbols);
}
@Override
public NumberFormatterImpl symbols(NumberingSystem ns) {
return new NumberFormatterImpl(this, KEY_SYMBOLS, ns);
}
@Override
public NumberFormatterImpl unitWidth(FormatWidth style) {
return new NumberFormatterImpl(this, KEY_UNIT_WIDTH, style);
}
@Override
public NumberFormatterImpl sign(SignDisplay style) {
return new NumberFormatterImpl(this, KEY_SIGN, style);
}
@Override
public NumberFormatterImpl decimal(DecimalMarkDisplay style) {
return new NumberFormatterImpl(this, KEY_DECIMAL, style);
}
@Override
public NumberFormatterImpl locale(Locale locale) {
return new NumberFormatterImpl(this, KEY_LOCALE, ULocale.forLocale(locale));
}
@Override
public NumberFormatterImpl locale(ULocale locale) {
return new NumberFormatterImpl(this, KEY_LOCALE, locale);
}
@Override
public String toSkeleton() {
return SkeletonBuilder.macrosToSkeleton(resolve());
}
@Override
public NumberFormatterResult format(long input) {
return format(new FormatQuantity4(input), DEFAULT_THRESHOLD);
}
@Override
public NumberFormatterResult format(double input) {
return format(new FormatQuantity4(input), DEFAULT_THRESHOLD);
}
@Override
public NumberFormatterResult format(Number input) {
return format(new FormatQuantity4(input), DEFAULT_THRESHOLD);
}
@Override
public NumberFormatterResult format(Measure input) {
return formatWithThreshold(input, DEFAULT_THRESHOLD);
}
/**
* Internal version of format with support for a custom regulation threshold. A threshold of 1
* causes the data structures to be built right away. A threshold of 0 prevents the data
* structures from being built.
*/
public NumberFormatterResult formatWithThreshold(Number number, int threshold) {
return format(new FormatQuantity4(number), threshold);
}
/**
* Internal version of format with support for a custom regulation threshold. A threshold of 1
* causes the data structures to be built right away. A threshold of 0 prevents the data
* structures from being built.
*/
public NumberFormatterResult formatWithThreshold(Measure input, int threshold) {
MeasureUnit unit = input.getUnit();
Number number = input.getNumber();
// Use this formatter if possible
if (Objects.equals(resolve().unit, unit)) {
return formatWithThreshold(number, threshold);
}
// This mechanism saves the previously used unit, so if the user calls this method with the
// same unit multiple times in a row, they get a more efficient code path.
NumberFormatterImpl withUnit = savedWithUnit;
if (withUnit == null || !Objects.equals(withUnit.resolve().unit, unit)) {
withUnit = new NumberFormatterImpl(this, KEY_UNIT, unit);
savedWithUnit = withUnit;
}
return withUnit.formatWithThreshold(number, threshold);
}
private NumberFormatterResult format(FormatQuantityBCD fq, int threshold) {
NumberStringBuilder string = new NumberStringBuilder();
// Lazily create the AtomicInteger
if (callCount == null) {
callCount = new AtomicInteger();
}
int currentCount = callCount.incrementAndGet();
MicroProps micros;
if (currentCount == threshold) {
compiled = Worker1.fromMacros(resolve());
micros = compiled.apply(fq, string);
} else if (compiled != null) {
micros = compiled.apply(fq, string);
} else {
micros = Worker1.applyStatic(resolve(), fq, string);
}
return new NumberFormatterResult(string, fq, micros);
}
@Override
public int hashCode() {
return resolve().hashCode();
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (other == null) return false;
if (!(other instanceof NumberFormatterImpl)) return false;
return resolve().equals(((NumberFormatterImpl) other).resolve());
}
private MacroProps resolve() {
if (resolvedMacros != null) {
return resolvedMacros;
}
// Although the linked-list fluent storage approach requires this method,
// my benchmarks show that linked-list is still faster than a full clone
// of a MacroProps object at each step.
MacroProps macros = new MacroProps();
NumberFormatterImpl current = this;
while (current != BASE) {
switch (current.key) {
case KEY_MACROS:
macros.fallback((MacroProps) current.value);
break;
case KEY_LOCALE:
if (macros.loc == null) {
macros.loc = (ULocale) current.value;
}
break;
case KEY_NOTATION:
if (macros.notation == null) {
macros.notation = (Notation) current.value;
}
break;
case KEY_UNIT:
if (macros.unit == null) {
macros.unit = (MeasureUnit) current.value;
}
break;
case KEY_ROUNDING:
if (macros.rounding == null) {
macros.rounding = (IRounding) current.value;
}
break;
case KEY_GROUPING:
if (macros.grouping == null) {
macros.grouping = (IGrouping) current.value;
}
break;
case KEY_PADDING:
if (macros.padding == null) {
macros.padding = (Padding) current.value;
}
break;
case KEY_INTEGER:
if (macros.integerWidth == null) {
macros.integerWidth = (IntegerWidth) current.value;
}
break;
case KEY_SYMBOLS:
if (macros.symbols == null) {
macros.symbols = /*(Object)*/ current.value;
}
break;
case KEY_UNIT_WIDTH:
if (macros.unitWidth == null) {
macros.unitWidth = (FormatWidth) current.value;
}
break;
case KEY_SIGN:
if (macros.sign == null) {
macros.sign = (SignDisplay) current.value;
}
break;
case KEY_DECIMAL:
if (macros.decimal == null) {
macros.decimal = (DecimalMarkDisplay) current.value;
}
break;
default:
throw new AssertionError();
}
current = current.parent;
}
resolvedMacros = macros;
return macros;
}
}

View File

@ -0,0 +1,447 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.math.BigDecimal;
import java.math.MathContext;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.AffixPatternUtils;
import com.ibm.icu.impl.number.LdmlPatternInfo;
import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.impl.number.RoundingUtils;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.CurrencyPluralInfo;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.ULocale;
import newapi.NumberFormatter.CurrencyRounding;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.IntegerWidth;
import newapi.NumberFormatter.Notation;
import newapi.NumberFormatter.Rounding;
import newapi.NumberFormatter.SignDisplay;
import newapi.impl.NotationImpl.NotationScientificImpl;
import newapi.impl.RoundingImpl.RoundingImplCurrency;
import newapi.impl.RoundingImpl.RoundingImplFraction;
import newapi.impl.RoundingImpl.RoundingImplIncrement;
import newapi.impl.RoundingImpl.RoundingImplSignificant;
/** @author sffc */
public final class NumberPropertyMapper {
/**
* Creates a new {@link MacroProps} object based on the content of a {@link Properties} object. In
* other words, maps Properties to MacroProps. This function is used by the JDK-compatibility API
* to call into the ICU 60 fluent number formatting pipeline.
*
* @param properties The property bag to be mapped.
* @param symbols The symbols associated with the property bag.
* @param exportedProperties A property bag in which to store validated properties.
* @return A new MacroProps containing all of the information in the Properties.
*/
public static MacroProps oldToNew(
Properties properties, DecimalFormatSymbols symbols, Properties exportedProperties) {
MacroProps macros = new MacroProps();
ULocale locale = symbols.getULocale();
/////////////
// SYMBOLS //
/////////////
macros.symbols = symbols;
//////////////////
// PLURAL RULES //
//////////////////
macros.rules = properties.getPluralRules();
/////////////
// AFFIXES //
/////////////
AffixPatternProvider affixProvider;
if (properties.getCurrencyPluralInfo() == null) {
affixProvider =
new PropertiesAffixPatternProvider(
properties.getPositivePrefix() != null
? AffixPatternUtils.escape(properties.getPositivePrefix())
: properties.getPositivePrefixPattern(),
properties.getPositiveSuffix() != null
? AffixPatternUtils.escape(properties.getPositiveSuffix())
: properties.getPositiveSuffixPattern(),
properties.getNegativePrefix() != null
? AffixPatternUtils.escape(properties.getNegativePrefix())
: properties.getNegativePrefixPattern(),
properties.getNegativeSuffix() != null
? AffixPatternUtils.escape(properties.getNegativeSuffix())
: properties.getNegativeSuffixPattern());
} else {
affixProvider = new CurrencyPluralInfoAffixProvider(properties.getCurrencyPluralInfo());
}
macros.affixProvider = affixProvider;
///////////
// UNITS //
///////////
boolean useCurrency =
((properties.getCurrency() != null)
|| properties.getCurrencyPluralInfo() != null
|| properties.getCurrencyUsage() != null
|| affixProvider.hasCurrencySign());
Currency currency = CustomSymbolCurrency.resolve(properties.getCurrency(), locale, symbols);
CurrencyUsage currencyUsage = properties.getCurrencyUsage();
boolean explicitCurrencyUsage = currencyUsage != Properties.DEFAULT_CURRENCY_USAGE;
if (!explicitCurrencyUsage) {
currencyUsage = CurrencyUsage.STANDARD;
}
if (useCurrency) {
macros.unit = currency;
}
///////////////////////
// ROUNDING STRATEGY //
///////////////////////
int maxInt = properties.getMaximumIntegerDigits();
int minInt = properties.getMinimumIntegerDigits();
int maxFrac = properties.getMaximumFractionDigits();
int minFrac = properties.getMinimumFractionDigits();
int minSig = properties.getMinimumSignificantDigits();
int maxSig = properties.getMaximumSignificantDigits();
BigDecimal roundingIncrement = properties.getRoundingIncrement();
MathContext mathContext = RoundingUtils.getMathContextOrUnlimited(properties);
boolean explicitMinMaxFrac =
minFrac != Properties.DEFAULT_MINIMUM_FRACTION_DIGITS
|| maxFrac != Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS;
boolean explicitMinMaxSig =
minSig != Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS
|| maxSig != Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS;
// Validate min/max int/frac.
// For backwards compatibility, minimum overrides maximum if the two conflict.
// The following logic ensures that there is always a minimum of at least one digit.
if (minInt == 0 && maxFrac != 0) {
// Force a digit after the decimal point.
minFrac = minFrac <= 0 ? 1 : minFrac;
maxFrac = maxFrac < 0 ? Integer.MAX_VALUE : maxFrac < minFrac ? minFrac : maxFrac;
minInt = 0;
maxInt = maxInt < 0 ? Integer.MAX_VALUE : maxInt;
} else {
// Force a digit before the decimal point.
minFrac = minFrac < 0 ? 0 : minFrac;
maxFrac = maxFrac < 0 ? Integer.MAX_VALUE : maxFrac < minFrac ? minFrac : maxFrac;
minInt = minInt <= 0 ? 1 : minInt;
maxInt = maxInt < 0 ? Integer.MAX_VALUE : maxInt < minInt ? minInt : maxInt;
}
Rounding rounding = null;
if (explicitCurrencyUsage) {
rounding = RoundingImplCurrency.getInstance(currencyUsage).withCurrency(currency);
} else if (roundingIncrement != null) {
rounding = RoundingImplIncrement.getInstance(roundingIncrement);
} else if (explicitMinMaxSig) {
minSig = minSig < 1 ? 1 : minSig > 1000 ? 1000 : minSig;
maxSig = maxSig < 0 ? 1000 : maxSig < minSig ? minSig : maxSig > 1000 ? 1000 : maxSig;
rounding = RoundingImplSignificant.getInstance(minSig, maxSig);
} else if (explicitMinMaxFrac) {
rounding = RoundingImplFraction.getInstance(minFrac, maxFrac);
} else if (useCurrency) {
rounding = RoundingImplCurrency.getInstance(currencyUsage);
}
if (rounding != null) {
rounding = rounding.withMode(mathContext);
macros.rounding = rounding;
}
///////////////////
// INTEGER WIDTH //
///////////////////
macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt);
///////////////////////
// GROUPING STRATEGY //
///////////////////////
int grouping1 = properties.getGroupingSize();
int grouping2 = properties.getSecondaryGroupingSize();
int minGrouping = properties.getMinimumGroupingDigits();
assert grouping1 >= -2; // value of -2 means to forward no grouping information
grouping1 = grouping1 > 0 ? grouping1 : grouping2 > 0 ? grouping2 : grouping1;
grouping2 = grouping2 > 0 ? grouping2 : grouping1;
// TODO: Is it important to handle minGrouping > 2?
macros.grouping =
GroupingImpl.getInstance((short) grouping1, (short) grouping2, minGrouping == 2);
/////////////
// PADDING //
/////////////
if (properties.getFormatWidth() != Properties.DEFAULT_FORMAT_WIDTH) {
macros.padding =
PaddingImpl.getInstance(
properties.getPadString(), properties.getFormatWidth(), properties.getPadPosition());
}
///////////////////////////////
// DECIMAL MARK ALWAYS SHOWN //
///////////////////////////////
macros.decimal =
properties.getDecimalSeparatorAlwaysShown()
? DecimalMarkDisplay.ALWAYS_SHOWN
: DecimalMarkDisplay.AUTO;
///////////////////////
// SIGN ALWAYS SHOWN //
///////////////////////
macros.sign = properties.getSignAlwaysShown() ? SignDisplay.ALWAYS_SHOWN : SignDisplay.AUTO;
/////////////////////////
// SCIENTIFIC NOTATION //
/////////////////////////
if (properties.getMinimumExponentDigits() != Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS) {
// Scientific notation is required.
// The mapping from property bag to scientific notation is nontrivial due to LDML rules.
// The maximum of 8 engineering digits has unknown origins and is not in the spec.
int engineering =
(maxInt != Integer.MAX_VALUE) ? maxInt : properties.getMaximumIntegerDigits();
engineering = (engineering < 0) ? 0 : (engineering > 8) ? minInt : engineering;
macros.notation =
new NotationScientificImpl(
// Engineering interval:
engineering,
// Enforce minimum integer digits (for patterns like "000.00E0"):
(engineering == minInt),
// Minimum exponent digits:
properties.getMinimumExponentDigits(),
// Exponent sign always shown:
properties.getExponentSignAlwaysShown()
? SignDisplay.ALWAYS_SHOWN
: SignDisplay.AUTO);
// Scientific notation also involves overriding the rounding mode.
if (macros.rounding instanceof RoundingImplFraction) {
int minInt_ = properties.getMinimumIntegerDigits();
int minFrac_ = properties.getMinimumFractionDigits();
int maxFrac_ = properties.getMaximumFractionDigits();
if (minInt_ == 0 && maxFrac_ == 0) {
// Patterns like "#E0" and "##E0", which mean no rounding!
macros.rounding = Rounding.NONE.withMode(mathContext);
} else if (minInt_ == 0 && minFrac_ == 0) {
// Patterns like "#.##E0" (no zeros in the mantissa), which mean round to maxFrac+1
macros.rounding = new RoundingImplSignificant(1, maxFrac_ + 1).withMode(mathContext);
} else {
// All other scientific patterns, which mean round to minInt+maxFrac
macros.rounding =
new RoundingImplSignificant(minInt_ + minFrac_, minInt_ + maxFrac_)
.withMode(mathContext);
}
}
}
//////////////////////
// COMPACT NOTATION //
//////////////////////
if (properties.getCompactStyle() != Properties.DEFAULT_COMPACT_STYLE) {
if (properties.getCompactCustomData() != null) {
macros.notation = new NotationImpl.NotationCompactImpl(properties.getCompactCustomData());
} else if (properties.getCompactStyle() == CompactStyle.LONG) {
macros.notation = Notation.COMPACT_LONG;
} else {
macros.notation = Notation.COMPACT_SHORT;
}
// Do not forward the affix provider.
macros.affixProvider = null;
}
/////////////////
// MULTIPLIERS //
/////////////////
if (properties.getMagnitudeMultiplier() != Properties.DEFAULT_MAGNITUDE_MULTIPLIER) {
macros.multiplier = new MultiplierImpl(properties.getMagnitudeMultiplier());
} else if (properties.getMultiplier() != Properties.DEFAULT_MULTIPLIER) {
macros.multiplier = new MultiplierImpl(properties.getMultiplier());
}
//////////////////////
// PROPERTY EXPORTS //
//////////////////////
if (exportedProperties != null) {
exportedProperties.setMathContext(mathContext);
exportedProperties.setRoundingMode(mathContext.getRoundingMode());
exportedProperties.setMinimumIntegerDigits(minInt);
exportedProperties.setMaximumIntegerDigits(maxInt);
Rounding rounding_;
if (rounding instanceof CurrencyRounding) {
rounding_ = ((CurrencyRounding) rounding).withCurrency(currency);
} else {
rounding_ = rounding;
}
int minFrac_ = minFrac;
int maxFrac_ = maxFrac;
int minSig_ = minSig;
int maxSig_ = maxSig;
BigDecimal increment_ = null;
if (rounding_ instanceof RoundingImplFraction) {
minFrac_ = ((RoundingImplFraction) rounding_).minFrac;
maxFrac_ = ((RoundingImplFraction) rounding_).maxFrac;
} else if (rounding_ instanceof RoundingImplIncrement) {
increment_ = ((RoundingImplIncrement) rounding_).increment;
minFrac_ = increment_.scale();
maxFrac_ = increment_.scale();
} else if (rounding_ instanceof RoundingImplSignificant) {
minSig_ = ((RoundingImplSignificant) rounding_).minSig;
maxSig_ = ((RoundingImplSignificant) rounding_).maxSig;
}
exportedProperties.setMinimumFractionDigits(minFrac_);
exportedProperties.setMaximumFractionDigits(maxFrac_);
exportedProperties.setMinimumSignificantDigits(minSig_);
exportedProperties.setMaximumSignificantDigits(maxSig_);
exportedProperties.setRoundingIncrement(increment_);
}
return macros;
}
private static class PropertiesAffixPatternProvider implements AffixPatternProvider {
private final String posPrefixPattern;
private final String posSuffixPattern;
private final String negPrefixPattern;
private final String negSuffixPattern;
public PropertiesAffixPatternProvider(String ppp, String psp, String npp, String nsp) {
if (ppp == null) ppp = "";
if (psp == null) psp = "";
if (npp == null && nsp != null) npp = "-"; // TODO: This is a hack.
if (nsp == null && npp != null) nsp = "";
posPrefixPattern = ppp;
posSuffixPattern = psp;
negPrefixPattern = npp;
negSuffixPattern = nsp;
}
@Override
public char charAt(int flags, int i) {
boolean prefix = (flags & Flags.PREFIX) != 0;
boolean negative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
if (prefix && negative) {
return negPrefixPattern.charAt(i);
} else if (prefix) {
return posPrefixPattern.charAt(i);
} else if (negative) {
return negSuffixPattern.charAt(i);
} else {
return posSuffixPattern.charAt(i);
}
}
@Override
public int length(int flags) {
boolean prefix = (flags & Flags.PREFIX) != 0;
boolean negative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
if (prefix && negative) {
return negPrefixPattern.length();
} else if (prefix) {
return posPrefixPattern.length();
} else if (negative) {
return negSuffixPattern.length();
} else {
return posSuffixPattern.length();
}
}
@Override
public boolean positiveHasPlusSign() {
return AffixPatternUtils.containsType(posPrefixPattern, AffixPatternUtils.TYPE_PLUS_SIGN)
|| AffixPatternUtils.containsType(posSuffixPattern, AffixPatternUtils.TYPE_PLUS_SIGN);
}
@Override
public boolean hasNegativeSubpattern() {
return negPrefixPattern != null;
}
@Override
public boolean negativeHasMinusSign() {
return AffixPatternUtils.containsType(negPrefixPattern, AffixPatternUtils.TYPE_MINUS_SIGN)
|| AffixPatternUtils.containsType(negSuffixPattern, AffixPatternUtils.TYPE_MINUS_SIGN);
}
@Override
public boolean hasCurrencySign() {
return AffixPatternUtils.hasCurrencySymbols(posPrefixPattern)
|| AffixPatternUtils.hasCurrencySymbols(posSuffixPattern)
|| AffixPatternUtils.hasCurrencySymbols(negPrefixPattern)
|| AffixPatternUtils.hasCurrencySymbols(negSuffixPattern);
}
@Override
public boolean containsSymbolType(int type) {
return AffixPatternUtils.containsType(posPrefixPattern, type)
|| AffixPatternUtils.containsType(posSuffixPattern, type)
|| AffixPatternUtils.containsType(negPrefixPattern, type)
|| AffixPatternUtils.containsType(negSuffixPattern, type);
}
}
private static class CurrencyPluralInfoAffixProvider implements AffixPatternProvider {
private final AffixPatternProvider[] affixesByPlural;
public CurrencyPluralInfoAffixProvider(CurrencyPluralInfo cpi) {
affixesByPlural = new PatternParseResult[StandardPlural.COUNT];
for (StandardPlural plural : StandardPlural.VALUES) {
affixesByPlural[plural.ordinal()] =
LdmlPatternInfo.parse(cpi.getCurrencyPluralPattern(plural.getKeyword()));
}
}
@Override
public char charAt(int flags, int i) {
int pluralOrdinal = (flags & Flags.PLURAL_MASK);
return affixesByPlural[pluralOrdinal].charAt(flags, i);
}
@Override
public int length(int flags) {
int pluralOrdinal = (flags & Flags.PLURAL_MASK);
return affixesByPlural[pluralOrdinal].length(flags);
}
@Override
public boolean positiveHasPlusSign() {
return affixesByPlural[StandardPlural.OTHER.ordinal()].positiveHasPlusSign();
}
@Override
public boolean hasNegativeSubpattern() {
return affixesByPlural[StandardPlural.OTHER.ordinal()].hasNegativeSubpattern();
}
@Override
public boolean negativeHasMinusSign() {
return affixesByPlural[StandardPlural.OTHER.ordinal()].negativeHasMinusSign();
}
@Override
public boolean hasCurrencySign() {
return affixesByPlural[StandardPlural.OTHER.ordinal()].hasCurrencySign();
}
@Override
public boolean containsSymbolType(int type) {
return affixesByPlural[StandardPlural.OTHER.ordinal()].containsSymbolType(type);
}
}
}

View File

@ -0,0 +1,109 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
import newapi.NumberFormatter.Padding;
public class PaddingImpl extends Padding.Internal {
String paddingString;
int targetWidth;
PadPosition position;
public static final PaddingImpl NONE = new PaddingImpl();
public static PaddingImpl getInstance(
String paddingString, int targetWidth, PadPosition position) {
// TODO: Add a few default implementations
return new PaddingImpl(paddingString, targetWidth, position);
}
/** Default constructor, producing an empty instance */
public PaddingImpl() {
paddingString = null;
targetWidth = -1;
position = null;
}
private PaddingImpl(String paddingString, int targetWidth, PadPosition position) {
this.paddingString = (paddingString == null) ? " " : paddingString;
this.targetWidth = targetWidth;
this.position = (position == null) ? PadPosition.BEFORE_PREFIX : position;
}
public int applyModsAndMaybePad(
MicroProps micros, NumberStringBuilder string, int leftIndex, int rightIndex) {
// Apply modInner (scientific notation) before padding
int innerLength = micros.modInner.apply(string, leftIndex, rightIndex);
// No padding; apply the mods and leave.
if (targetWidth < 0) {
return applyMicroMods(micros, string, leftIndex, rightIndex + innerLength);
}
// Estimate the padding width needed.
// TODO: Make this more efficient (less copying)
// TODO: How to handle when padding is inserted between a currency sign and the number
// when currency spacing is in play?
NumberStringBuilder backup = new NumberStringBuilder(string);
int length = innerLength + applyMicroMods(micros, string, leftIndex, rightIndex + innerLength);
int requiredPadding = targetWidth - string.codePointCount();
if (requiredPadding <= 0) {
// Padding is not required.
return length;
}
length = innerLength;
string.copyFrom(backup);
if (position == PadPosition.AFTER_PREFIX) {
length += addPaddingHelper(paddingString, requiredPadding, string, leftIndex);
} else if (position == PadPosition.BEFORE_SUFFIX) {
length += addPaddingHelper(paddingString, requiredPadding, string, rightIndex + length);
}
length += applyMicroMods(micros, string, leftIndex, rightIndex + length);
if (position == PadPosition.BEFORE_PREFIX) {
length = addPaddingHelper(paddingString, requiredPadding, string, leftIndex);
} else if (position == PadPosition.AFTER_SUFFIX) {
length = addPaddingHelper(paddingString, requiredPadding, string, rightIndex + length);
}
// The length might not be exactly right due to currency spacing.
// Make an adjustment if needed.
while (string.codePointCount() < targetWidth) {
int insertIndex;
switch (position) {
case AFTER_PREFIX:
insertIndex = leftIndex + length;
break;
case BEFORE_SUFFIX:
insertIndex = rightIndex + length;
break;
default:
// Should not happen since currency spacing is always on the inside.
throw new AssertionError();
}
length += string.insert(insertIndex, paddingString, null);
}
return length;
}
private static int applyMicroMods(
MicroProps micros, NumberStringBuilder string, int leftIndex, int rightIndex) {
int length = micros.modMiddle.apply(string, leftIndex, rightIndex);
length += micros.modOuter.apply(string, leftIndex, rightIndex + length);
return length;
}
private static int addPaddingHelper(
String paddingString, int requiredPadding, NumberStringBuilder string, int index) {
for (int i = 0; i < requiredPadding; i++) {
string.insert(index, paddingString, null);
}
return paddingString.length() * requiredPadding;
}
}

View File

@ -0,0 +1,114 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
// License & terms of use: http://www.unicode.org/copyright.html#License
import com.ibm.icu.impl.number.Format;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.text.NumberFormat;
import newapi.NumberFormatter.DecimalMarkDisplay;
public class PositiveDecimalImpl implements Format.TargetFormat {
@Override
public int target(FormatQuantity input, NumberStringBuilder string, int startIndex) {
// FIXME
throw new UnsupportedOperationException();
}
/**
* @param micros
* @param fq
* @param output
* @return
*/
public static int apply(MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
int length = 0;
if (input.isInfinite()) {
length += string.insert(length, micros.symbols.getInfinity(), NumberFormat.Field.INTEGER);
} else if (input.isNaN()) {
length += string.insert(length, micros.symbols.getNaN(), NumberFormat.Field.INTEGER);
} else {
// Add the integer digits
length += addIntegerDigits(micros, input, string);
// Add the decimal point
if (input.getLowerDisplayMagnitude() < 0
|| micros.decimal == DecimalMarkDisplay.ALWAYS_SHOWN) {
length +=
string.insert(
length,
micros.useCurrency
? micros.symbols.getMonetaryDecimalSeparatorString()
: micros.symbols.getDecimalSeparatorString(),
NumberFormat.Field.DECIMAL_SEPARATOR);
}
// Add the fraction digits
length += addFractionDigits(micros, input, string);
}
return length;
}
private static int addIntegerDigits(
MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
int length = 0;
int integerCount = input.getUpperDisplayMagnitude() + 1;
for (int i = 0; i < integerCount; i++) {
// Add grouping separator
if (micros.grouping.groupAtPosition(i, input)) {
length +=
string.insert(
0,
micros.useCurrency
? micros.symbols.getMonetaryGroupingSeparatorString()
: micros.symbols.getGroupingSeparatorString(),
NumberFormat.Field.GROUPING_SEPARATOR);
}
// Get and append the next digit value
byte nextDigit = input.getDigit(i);
if (micros.symbols.getCodePointZero() != -1) {
length +=
string.insertCodePoint(
0, micros.symbols.getCodePointZero() + nextDigit, NumberFormat.Field.INTEGER);
} else {
length +=
string.insert(
0, micros.symbols.getDigitStringsLocal()[nextDigit], NumberFormat.Field.INTEGER);
}
}
return length;
}
private static int addFractionDigits(
MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
int length = 0;
int fractionCount = -input.getLowerDisplayMagnitude();
for (int i = 0; i < fractionCount; i++) {
// Get and append the next digit value
byte nextDigit = input.getDigit(-i - 1);
if (micros.symbols.getCodePointZero() != -1) {
length +=
string.appendCodePoint(
micros.symbols.getCodePointZero() + nextDigit, NumberFormat.Field.FRACTION);
} else {
length +=
string.append(
micros.symbols.getDigitStringsLocal()[nextDigit], NumberFormat.Field.FRACTION);
}
}
return length;
}
@Override
public void export(Properties properties) {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,10 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import com.ibm.icu.impl.number.FormatQuantity;
public interface QuantityChain {
QuantityChain chain(QuantityChain parent);
MicroProps withQuantity(FormatQuantity quantity);
}

View File

@ -0,0 +1,37 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.util.Map;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.text.PluralRules;
public class QuantityDependentModOuter implements QuantityChain {
final Map<StandardPlural, Modifier> data;
final PluralRules rules;
/* final */ QuantityChain parent;
public QuantityDependentModOuter(Map<StandardPlural, Modifier> data, PluralRules rules) {
this.data = data;
this.rules = rules;
}
@Override
public QuantityChain chain(QuantityChain parent) {
this.parent = parent;
return this;
}
@Override
public MicroProps withQuantity(FormatQuantity quantity) {
MicroProps micros = parent.withQuantity(quantity);
// TODO: Avoid the copy here?
FormatQuantity copy = quantity.createCopy();
micros.rounding.apply(copy);
micros.modOuter = data.get(copy.getStandardPlural(rules));
return micros;
}
}

View File

@ -0,0 +1,445 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.FormatQuantity4;
import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
import com.ibm.icu.impl.number.RoundingUtils;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import newapi.NumberFormatter.CurrencyRounding;
import newapi.NumberFormatter.FractionRounding;
import newapi.NumberFormatter.IRounding;
import newapi.NumberFormatter.Rounding;
/**
* The internal version of {@link Rounding} with additional methods.
*
* <p>Although it seems as though RoundingImpl should extend Rounding, it actually extends
* FractionRounding. This is because instances of FractionRounding are self-contained rounding
* instances themselves, and they need to implement RoundingImpl. When ICU adopts Java 8, there will
* be more options for the polymorphism, such as multiple inheritance with interfaces having default
* methods and static factory methods on interfaces.
*/
@SuppressWarnings("deprecation")
public abstract class RoundingImpl extends FractionRounding.Internal implements Cloneable {
public static RoundingImpl forPattern(PatternParseResult patternInfo) {
if (patternInfo.positive.rounding != null) {
return RoundingImplIncrement.getInstance(patternInfo.positive.rounding.toBigDecimal());
} else if (patternInfo.positive.minimumSignificantDigits > 0) {
return RoundingImplSignificant.getInstance(
patternInfo.positive.minimumSignificantDigits,
patternInfo.positive.maximumSignificantDigits);
} else if (patternInfo.positive.exponentDigits > 0) {
// FIXME
throw new UnsupportedOperationException();
} else {
return RoundingImplFraction.getInstance(
patternInfo.positive.minimumFractionDigits, patternInfo.positive.maximumFractionDigits);
}
}
/**
* Returns a RoundingImpl no matter what is the type of the provided argument. If the argument is
* already a RoundingImpl, this method just returns the same object. Otherwise, it does some
* processing to build a RoundingImpl.
*
* @param rounding The input object, which might or might not be a RoundingImpl.
* @param currency A currency object to use in case the input object needs it.
* @return A RoundingImpl object.
*/
public static RoundingImpl normalizeType(IRounding rounding, Currency currency) {
if (rounding instanceof RoundingImpl) {
return (RoundingImpl) rounding;
} else if (rounding instanceof RoundingImplCurrency) {
return ((RoundingImplCurrency) rounding).withCurrency(currency);
} else {
return RoundingImplLambda.getInstance(rounding);
}
}
private static final MathContext DEFAULT_MATH_CONTEXT =
RoundingUtils.mathContextUnlimited(RoundingMode.HALF_EVEN);
public MathContext mathContext;
public RoundingImpl() {
this.mathContext = DEFAULT_MATH_CONTEXT;
// TODO: This is ugly, but necessary if a RoundingImpl is created
// before this class has been initialized.
if (this.mathContext == null) {
this.mathContext = RoundingUtils.mathContextUnlimited(RoundingMode.HALF_EVEN);
}
}
@Override
public Rounding withMode(RoundingMode roundingMode) {
return withMode(RoundingUtils.mathContextUnlimited(roundingMode));
}
@Override
public Rounding withMode(MathContext mathContext) {
if (this.mathContext.equals(mathContext)) {
return this;
}
RoundingImpl other = (RoundingImpl) this.clone();
other.mathContext = mathContext;
return other;
}
abstract void apply(FormatQuantity value);
@Override
public BigDecimal round(BigDecimal input) {
// Provided for API compatibility.
FormatQuantity fq = new FormatQuantity4(input);
this.apply(fq);
return fq.toBigDecimal();
}
static interface MultiplierProducer {
int getMultiplier(int magnitude);
}
int chooseMultiplierAndApply(FormatQuantity input, MultiplierProducer producer) {
// TODO: Make a better and more efficient implementation.
// TODO: Avoid the object creation here.
FormatQuantity copy = input.createCopy();
assert !input.isZero();
int magnitude = input.getMagnitude();
int multiplier = producer.getMultiplier(magnitude);
input.adjustMagnitude(multiplier);
apply(input);
// If the number turned to zero when rounding, do not re-attempt the rounding.
if (!input.isZero() && input.getMagnitude() == magnitude + multiplier + 1) {
magnitude += 1;
input.copyFrom(copy);
multiplier = producer.getMultiplier(magnitude);
input.adjustMagnitude(multiplier);
assert input.getMagnitude() == magnitude + multiplier - 1;
apply(input);
assert input.getMagnitude() == magnitude + multiplier;
}
return multiplier;
}
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
// Should not happen since parent is Object
throw new AssertionError(e);
}
}
/** A dummy class used when the number has already been rounded elsewhere. */
public static class RoundingImplDummy extends RoundingImpl {
public static final RoundingImplDummy INSTANCE = new RoundingImplDummy();
private RoundingImplDummy() {}
@Override
void apply(FormatQuantity value) {}
}
public static class RoundingImplInfinity extends RoundingImpl {
@Override
void apply(FormatQuantity value) {
value.roundToInfinity();
value.setFractionLength(0, Integer.MAX_VALUE);
}
}
public static class RoundingImplFraction extends RoundingImpl {
int minFrac;
int maxFrac;
private static final RoundingImplFraction FIXED_0 = new RoundingImplFraction(0, 0);
private static final RoundingImplFraction FIXED_2 = new RoundingImplFraction(2, 2);
/** Assumes that minFrac <= maxFrac. */
public static RoundingImplFraction getInstance(int minFrac, int maxFrac) {
assert minFrac >= 0 && minFrac <= maxFrac;
if (minFrac == 0 && maxFrac == 0) {
return FIXED_0;
} else if (minFrac == 2 && maxFrac == 2) {
return FIXED_2;
} else {
return new RoundingImplFraction(minFrac, maxFrac);
}
}
/** Hook for public static final; uses integer rounding */
public RoundingImplFraction() {
this(0, 0);
}
private RoundingImplFraction(int minFrac, int maxFrac) {
this.minFrac = minFrac;
this.maxFrac = maxFrac;
}
@Override
void apply(FormatQuantity value) {
value.roundToMagnitude(getRoundingMagnitude(maxFrac), mathContext);
value.setFractionLength(Math.max(0, -getDisplayMagnitude(minFrac)), Integer.MAX_VALUE);
}
static int getRoundingMagnitude(int maxFrac) {
if (maxFrac == Integer.MAX_VALUE) {
return Integer.MIN_VALUE;
}
return -maxFrac;
}
static int getDisplayMagnitude(int minFrac) {
if (minFrac == 0) {
return Integer.MAX_VALUE;
}
return -minFrac;
}
@Override
public Rounding withMinFigures(int minFigures) {
if (minFigures > 0 && minFigures <= MAX_VALUE) {
return RoundingImplFractionSignificant.getInstance(this, minFigures, -1);
} else {
throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
}
}
@Override
public Rounding withMaxFigures(int maxFigures) {
if (maxFigures > 0 && maxFigures <= MAX_VALUE) {
return RoundingImplFractionSignificant.getInstance(this, -1, maxFigures);
} else {
throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
}
}
}
public static class RoundingImplSignificant extends RoundingImpl {
int minSig;
int maxSig;
private static final RoundingImplSignificant FIXED_2 = new RoundingImplSignificant(2, 2);
private static final RoundingImplSignificant FIXED_3 = new RoundingImplSignificant(3, 3);
private static final RoundingImplSignificant RANGE_2_3 = new RoundingImplSignificant(2, 3);
/** Assumes that minSig <= maxSig. */
public static RoundingImplSignificant getInstance(int minSig, int maxSig) {
assert minSig >= 0 && minSig <= maxSig;
if (minSig == 2 && maxSig == 2) {
return FIXED_2;
} else if (minSig == 3 && maxSig == 3) {
return FIXED_3;
} else if (minSig == 2 && maxSig == 3) {
return RANGE_2_3;
} else {
return new RoundingImplSignificant(minSig, maxSig);
}
}
RoundingImplSignificant(int minSig, int maxSig) {
this.minSig = minSig;
this.maxSig = maxSig;
}
@Override
void apply(FormatQuantity value) {
value.roundToMagnitude(getRoundingMagnitude(value, maxSig), mathContext);
value.setFractionLength(Math.max(0, -getDisplayMagnitude(value, minSig)), Integer.MAX_VALUE);
}
/** Version of {@link #apply} that obeys minInt constraints. */
public void apply(FormatQuantity quantity, int minInt) {
assert quantity.isZero();
quantity.setFractionLength(minSig - minInt, Integer.MAX_VALUE);
}
static int getRoundingMagnitude(FormatQuantity value, int maxSig) {
if (maxSig == Integer.MAX_VALUE) {
return Integer.MIN_VALUE;
}
int magnitude = value.isZero() ? 0 : value.getMagnitude();
return magnitude - maxSig + 1;
}
static int getDisplayMagnitude(FormatQuantity value, int minSig) {
int magnitude = value.isZero() ? 0 : value.getMagnitude();
return magnitude - minSig + 1;
}
}
public static class RoundingImplFractionSignificant extends RoundingImpl {
int minFrac;
int maxFrac;
int minSig;
int maxSig;
// Package-private
static final RoundingImplFractionSignificant COMPACT_STRATEGY =
new RoundingImplFractionSignificant(0, 0, 2, -1);
public static Rounding getInstance(FractionRounding _base, int minSig, int maxSig) {
assert _base instanceof RoundingImplFraction;
RoundingImplFraction base = (RoundingImplFraction) _base;
if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 2 /* && maxSig == -1 */) {
return COMPACT_STRATEGY;
} else {
return new RoundingImplFractionSignificant(base.minFrac, base.maxFrac, minSig, maxSig);
}
}
/** Assumes that minFrac <= maxFrac and minSig <= maxSig except for -1. */
private RoundingImplFractionSignificant(int minFrac, int maxFrac, int minSig, int maxSig) {
// Exactly one of the arguments should be -1, either minSig or maxSig.
assert (minFrac != -1 && maxFrac != -1 && minSig == -1 && maxSig != -1 && minFrac <= maxFrac)
|| (minFrac != -1 && maxFrac != -1 && minSig != -1 && maxSig == -1 && minFrac <= maxFrac);
this.minFrac = minFrac;
this.maxFrac = maxFrac;
this.minSig = minSig;
this.maxSig = maxSig;
}
@Override
void apply(FormatQuantity value) {
int displayMag = RoundingImplFraction.getDisplayMagnitude(minFrac);
int roundingMag = RoundingImplFraction.getRoundingMagnitude(maxFrac);
if (minSig == -1) {
// Max Sig override
int candidate = RoundingImplSignificant.getRoundingMagnitude(value, maxSig);
roundingMag = Math.max(roundingMag, candidate);
} else {
// Min Sig override
int candidate = RoundingImplSignificant.getDisplayMagnitude(value, minSig);
roundingMag = Math.min(roundingMag, candidate);
}
value.roundToMagnitude(roundingMag, mathContext);
value.setFractionLength(Math.max(0, -displayMag), Integer.MAX_VALUE);
}
}
public static class RoundingImplIncrement extends RoundingImpl {
BigDecimal increment;
private static final RoundingImplIncrement NICKEL =
new RoundingImplIncrement(BigDecimal.valueOf(0.5));
public static RoundingImplIncrement getInstance(BigDecimal increment) {
assert increment != null;
if (increment.compareTo(NICKEL.increment) == 0) {
return NICKEL;
} else {
return new RoundingImplIncrement(increment);
}
}
private RoundingImplIncrement(BigDecimal increment) {
this.increment = increment;
}
@Override
void apply(FormatQuantity value) {
value.roundToIncrement(increment, mathContext);
value.setFractionLength(increment.scale(), increment.scale());
}
}
public static class RoundingImplLambda extends RoundingImpl {
IRounding lambda;
public static RoundingImplLambda getInstance(IRounding lambda) {
assert !(lambda instanceof Rounding);
return new RoundingImplLambda(lambda);
}
private RoundingImplLambda(IRounding lambda) {
this.lambda = lambda;
}
@Override
void apply(FormatQuantity value) {
// TODO: Cache the BigDecimal between calls?
BigDecimal temp = value.toBigDecimal();
temp = lambda.round(temp);
value.setToBigDecimal(temp);
value.setFractionLength(temp.scale(), Integer.MAX_VALUE);
}
}
/**
* NOTE: This is unlike the other classes here. It is NOT a standalone rounder and it does NOT
* extend RoundingImpl.
*/
public static class RoundingImplCurrency extends CurrencyRounding.Internal {
final CurrencyUsage usage;
final MathContext mc;
private static final RoundingImplCurrency MONETARY_STANDARD =
new RoundingImplCurrency(CurrencyUsage.STANDARD, DEFAULT_MATH_CONTEXT);
private static final RoundingImplCurrency MONETARY_CASH =
new RoundingImplCurrency(CurrencyUsage.CASH, DEFAULT_MATH_CONTEXT);
public static RoundingImplCurrency getInstance(CurrencyUsage usage) {
if (usage == CurrencyUsage.STANDARD) {
return MONETARY_STANDARD;
} else if (usage == CurrencyUsage.CASH) {
return MONETARY_CASH;
} else {
throw new AssertionError();
}
}
private RoundingImplCurrency(CurrencyUsage usage, MathContext mc) {
this.usage = usage;
this.mc = mc;
}
@Override
public RoundingImpl withCurrency(Currency currency) {
assert currency != null;
double incrementDouble = currency.getRoundingIncrement(usage);
if (incrementDouble != 0.0) {
BigDecimal increment = BigDecimal.valueOf(incrementDouble);
return RoundingImplIncrement.getInstance(increment);
} else {
int minMaxFrac = currency.getDefaultFractionDigits(usage);
return RoundingImplFraction.getInstance(minMaxFrac, minMaxFrac);
}
}
@Override
public RoundingImplCurrency withMode(RoundingMode roundingMode) {
// This is similar to RoundingImpl#withMode().
return withMode(RoundingUtils.mathContextUnlimited(roundingMode));
}
@Override
public RoundingImplCurrency withMode(MathContext mathContext) {
// This is similar to RoundingImpl#withMode().
if (mc.equals(mathContext)) {
return this;
}
return new RoundingImplCurrency(usage, mathContext);
}
@Override
public BigDecimal round(BigDecimal input) {
throw new UnsupportedOperationException(
"A currency must be specified before calling this method.");
}
}
}

View File

@ -0,0 +1,144 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat;
import newapi.NumberFormatter.SignDisplay;
import newapi.impl.RoundingImpl.RoundingImplDummy;
import newapi.impl.RoundingImpl.RoundingImplSignificant;
public class ScientificImpl implements QuantityChain, RoundingImpl.MultiplierProducer {
final NotationImpl.NotationScientificImpl notation;
final DecimalFormatSymbols symbols;
final ScientificModifier[] precomputedMods;
/* final */ QuantityChain parent;
public static ScientificImpl getInstance(
NotationImpl.NotationScientificImpl notation, DecimalFormatSymbols symbols, boolean build) {
return new ScientificImpl(notation, symbols, build);
}
private ScientificImpl(
NotationImpl.NotationScientificImpl notation, DecimalFormatSymbols symbols, boolean build) {
this.notation = notation;
this.symbols = symbols;
if (build) {
// Pre-build the modifiers for exponents -12 through 12
precomputedMods = new ScientificModifier[25];
for (int i = -12; i <= 12; i++) {
precomputedMods[i + 12] = new ScientificModifier(i);
}
} else {
precomputedMods = null;
}
}
@Override
public QuantityChain chain(QuantityChain parent) {
this.parent = parent;
return this;
}
@Override
public MicroProps withQuantity(FormatQuantity quantity) {
MicroProps micros = parent.withQuantity(quantity);
assert micros.rounding != null;
// Treat zero as if it had magnitude 0
int exponent;
if (quantity.isZero()) {
if (notation.requireMinInt && micros.rounding instanceof RoundingImplSignificant) {
// Shown "00.000E0" on pattern "00.000E0"
((RoundingImplSignificant) micros.rounding).apply(quantity, notation.engineeringInterval);
exponent = 0;
} else {
micros.rounding.apply(quantity);
exponent = 0;
}
} else {
exponent = -micros.rounding.chooseMultiplierAndApply(quantity, this);
}
// Add the Modifier for the scientific format.
if (precomputedMods != null && exponent >= -12 && exponent <= 12) {
micros.modInner = precomputedMods[exponent + 12];
} else {
micros.modInner = new ScientificModifier(exponent);
}
// We already performed rounding. Do not perform it again.
micros.rounding = RoundingImplDummy.INSTANCE;
return micros;
}
private class ScientificModifier implements Modifier {
final int exponent;
ScientificModifier(int exponent) {
this.exponent = exponent;
}
@Override
public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
// FIXME: Localized exponent separator location.
int i = rightIndex;
// Append the exponent separator and sign
i += output.insert(i, symbols.getExponentSeparator(), NumberFormat.Field.EXPONENT_SYMBOL);
if (exponent < 0 && notation.exponentSignDisplay != SignDisplay.NEVER_SHOWN) {
i += output.insert(i, symbols.getMinusSignString(), NumberFormat.Field.EXPONENT_SIGN);
} else if (notation.exponentSignDisplay == SignDisplay.ALWAYS_SHOWN) {
i += output.insert(i, symbols.getPlusSignString(), NumberFormat.Field.EXPONENT_SIGN);
}
// Append the exponent digits (using a simple inline algorithm)
int disp = Math.abs(exponent);
for (int j = 0; j < notation.minExponentDigits || disp > 0; j++, disp /= 10) {
int d = disp % 10;
String digitString = symbols.getDigitStringsLocal()[d];
i += output.insert(i - j, digitString, NumberFormat.Field.EXPONENT);
}
return i - rightIndex;
}
@Override
public boolean isStrong() {
return true;
}
@Override
public String getPrefix() {
// Should never get called
throw new AssertionError();
}
@Override
public String getSuffix() {
// Should never get called
throw new AssertionError();
}
}
@Override
public int getMultiplier(int magnitude) {
int interval = notation.engineeringInterval;
int digitsShown;
if (notation.requireMinInt) {
// For patterns like "000.00E0" and ".00E0"
digitsShown = interval;
} else if (interval <= 1) {
// For patterns like "0.00E0" and "@@@E0"
digitsShown = 1;
} else {
// For patterns like "##0.00"
digitsShown = ((magnitude % interval + interval) % interval) + 1;
}
return digitsShown - magnitude - 1;
}
}

View File

@ -0,0 +1,549 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.Dimensionless;
import com.ibm.icu.util.MeasureUnit;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.FractionRounding;
import newapi.NumberFormatter.Grouping;
import newapi.NumberFormatter.IGrouping;
import newapi.NumberFormatter.IRounding;
import newapi.NumberFormatter.IntegerWidth;
import newapi.NumberFormatter.Notation;
import newapi.NumberFormatter.NotationCompact;
import newapi.NumberFormatter.NotationScientific;
import newapi.NumberFormatter.NotationSimple;
import newapi.NumberFormatter.Padding;
import newapi.NumberFormatter.Rounding;
import newapi.NumberFormatter.SignDisplay;
import newapi.impl.RoundingImpl.RoundingImplCurrency;
import newapi.impl.RoundingImpl.RoundingImplFraction;
import newapi.impl.RoundingImpl.RoundingImplFractionSignificant;
import newapi.impl.RoundingImpl.RoundingImplIncrement;
import newapi.impl.RoundingImpl.RoundingImplInfinity;
import newapi.impl.RoundingImpl.RoundingImplSignificant;
public final class SkeletonBuilder {
public static String macrosToSkeleton(MacroProps macros) {
// Print out the values in their canonical order.
StringBuilder sb = new StringBuilder();
if (macros.notation != null) {
// sb.append("notation=");
notationToSkeleton(macros.notation, sb);
sb.append(' ');
}
if (macros.unit != null) {
// sb.append("unit=");
unitToSkeleton(macros.unit, sb);
sb.append(' ');
}
if (macros.rounding != null) {
// sb.append("rounding=");
roundingToSkeleton(macros.rounding, sb);
sb.append(' ');
}
if (macros.grouping != null) {
sb.append("grouping=");
groupingToSkeleton(macros.grouping, sb);
sb.append(' ');
}
if (macros.padding != null) {
sb.append("padding=");
paddingToSkeleton(macros.padding, sb);
sb.append(' ');
}
if (macros.integerWidth != null) {
sb.append("integer-width=");
integerWidthToSkeleton(macros.integerWidth, sb);
sb.append(' ');
}
if (macros.symbols != null) {
sb.append("symbols=");
symbolsToSkeleton(macros.symbols, sb);
sb.append(' ');
}
if (macros.unitWidth != null) {
sb.append("unit-width=");
unitWidthToSkeleton(macros.unitWidth, sb);
sb.append(' ');
}
if (macros.sign != null) {
sb.append("sign=");
signToSkeleton(macros.sign, sb);
sb.append(' ');
}
if (macros.decimal != null) {
sb.append("decimal=");
decimalToSkeleton(macros.decimal, sb);
sb.append(' ');
}
if (sb.length() > 0) {
// Remove the trailing space
sb.setLength(sb.length() - 1);
}
return sb.toString();
}
public static MacroProps skeletonToMacros(String skeleton) {
MacroProps macros = new MacroProps();
for (int offset = 0; offset < skeleton.length(); ) {
char c = skeleton.charAt(offset);
switch (c) {
case ' ':
offset++;
break;
case 'E':
case 'C':
case 'I':
offset += skeletonToNotation(skeleton, offset, macros);
break;
case '%':
case 'B':
case '$':
case 'U':
offset += skeletonToUnit(skeleton, offset, macros);
break;
case 'F':
case 'S':
case 'M':
case 'G':
case 'Y':
offset += skeletonToRounding(skeleton, offset, macros);
break;
default:
if (skeleton.regionMatches(offset, "notation=", 0, 9)) {
offset += 9;
offset += skeletonToNotation(skeleton, offset, macros);
} else if (skeleton.regionMatches(offset, "unit=", 0, 5)) {
offset += 5;
offset += skeletonToUnit(skeleton, offset, macros);
} else if (skeleton.regionMatches(offset, "rounding=", 0, 9)) {
offset += 9;
offset += skeletonToRounding(skeleton, offset, macros);
} else if (skeleton.regionMatches(offset, "grouping=", 0, 9)) {
offset += 9;
offset += skeletonToGrouping(skeleton, offset, macros);
} else if (skeleton.regionMatches(offset, "padding=", 0, 9)) {
offset += 8;
offset += skeletonToPadding(skeleton, offset, macros);
} else if (skeleton.regionMatches(offset, "integer-width=", 0, 9)) {
offset += 14;
offset += skeletonToIntegerWidth(skeleton, offset, macros);
} else if (skeleton.regionMatches(offset, "symbols=", 0, 9)) {
offset += 8;
offset += skeletonToSymbols(skeleton, offset, macros);
} else if (skeleton.regionMatches(offset, "unit-width=", 0, 9)) {
offset += 11;
offset += skeletonToUnitWidth(skeleton, offset, macros);
} else if (skeleton.regionMatches(offset, "sign=", 0, 9)) {
offset += 5;
offset += skeletonToSign(skeleton, offset, macros);
} else if (skeleton.regionMatches(offset, "decimal=", 0, 9)) {
offset += 8;
offset += skeletonToDecimal(skeleton, offset, macros);
} else {
throw new IllegalArgumentException(
"Unexpected token at offset " + offset + " in skeleton string: " + c);
}
}
}
return macros;
}
private static void notationToSkeleton(Notation value, StringBuilder sb) {
if (value instanceof NotationScientific) {
NotationImpl.NotationScientificImpl notation = (NotationImpl.NotationScientificImpl) value;
sb.append('E');
if (notation.engineeringInterval != 1) {
sb.append(notation.engineeringInterval);
}
if (notation.exponentSignDisplay == SignDisplay.ALWAYS_SHOWN) {
sb.append('+');
} else if (notation.exponentSignDisplay == SignDisplay.NEVER_SHOWN) {
sb.append('!');
} else {
assert notation.exponentSignDisplay == SignDisplay.AUTO;
}
if (notation.minExponentDigits != 1) {
for (int i = 0; i < notation.minExponentDigits; i++) {
sb.append('0');
}
}
} else if (value instanceof NotationCompact) {
NotationImpl.NotationCompactImpl notation = (NotationImpl.NotationCompactImpl) value;
if (notation.compactStyle == CompactStyle.SHORT) {
sb.append('C');
} else {
// FIXME: CCC or CCCC instead?
sb.append("CC");
}
} else {
assert value instanceof NotationSimple;
sb.append('I');
}
}
private static int skeletonToNotation(String skeleton, int offset, MacroProps output) {
int originalOffset = offset;
char c0 = skeleton.charAt(offset++);
Notation result = null;
if (c0 == 'E') {
int engineering = 1;
SignDisplay sign = SignDisplay.AUTO;
int minExponentDigits = 0;
char c = safeCharAt(skeleton, offset++);
if (c >= '1' && c <= '9') {
engineering = c - '0';
c = safeCharAt(skeleton, offset++);
}
if (c == '+') {
sign = SignDisplay.ALWAYS_SHOWN;
c = safeCharAt(skeleton, offset++);
}
if (c == '!') {
sign = SignDisplay.NEVER_SHOWN;
c = safeCharAt(skeleton, offset++);
}
while (c == '0') {
minExponentDigits++;
c = safeCharAt(skeleton, offset++);
}
minExponentDigits = Math.max(1, minExponentDigits);
result = new NotationImpl.NotationScientificImpl(engineering, false, minExponentDigits, sign);
} else if (c0 == 'C') {
char c = safeCharAt(skeleton, offset++);
if (c == 'C') {
result = Notation.COMPACT_LONG;
} else {
result = Notation.COMPACT_SHORT;
}
} else if (c0 == 'I') {
result = Notation.SIMPLE;
}
output.notation = result;
return offset - originalOffset;
}
private static void unitToSkeleton(MeasureUnit value, StringBuilder sb) {
if (value.getType().equals("dimensionless")) {
if (value.getSubtype().equals("percent")) {
sb.append('%');
} else if (value.getSubtype().equals("permille")) {
sb.append("%%");
} else {
assert value.getSubtype().equals("base");
sb.append('B');
}
} else if (value.getType().equals("currency")) {
sb.append('$');
sb.append(value.getSubtype());
} else {
sb.append('U');
sb.append(value.getType());
sb.append(':');
sb.append(value.getSubtype());
}
}
private static int skeletonToUnit(String skeleton, int offset, MacroProps output) {
int originalOffset = offset;
char c0 = skeleton.charAt(offset++);
MeasureUnit result = null;
if (c0 == '%') {
char c = safeCharAt(skeleton, offset++);
if (c == '%') {
result = Dimensionless.PERCENT;
} else {
result = Dimensionless.PERMILLE;
}
} else if (c0 == 'B') {
result = Dimensionless.BASE;
} else if (c0 == '$') {
String currencyCode = skeleton.substring(offset, offset + 3);
offset += 3;
result = Currency.getInstance(currencyCode);
} else if (c0 == 'U') {
StringBuilder sb = new StringBuilder();
offset += consumeUntil(skeleton, offset, ':', sb);
String type = sb.toString();
sb.setLength(0);
offset += consumeUntil(skeleton, offset, ' ', sb);
String subtype = sb.toString();
for (MeasureUnit candidate : MeasureUnit.getAvailable(type)) {
if (candidate.getSubtype().equals(subtype)) {
result = candidate;
break;
}
}
}
output.unit = result;
return offset - originalOffset;
}
private static void roundingToSkeleton(IRounding value, StringBuilder sb) {
if (!(value instanceof Rounding)) {
// FIXME: Throw an exception here instead?
return;
}
MathContext mathContext;
if (value instanceof RoundingImplFraction) {
RoundingImplFraction rounding = (RoundingImplFraction) value;
sb.append('F');
minMaxToSkeletonHelper(rounding.minFrac, rounding.maxFrac, sb);
mathContext = rounding.mathContext;
} else if (value instanceof RoundingImplSignificant) {
RoundingImplSignificant rounding = (RoundingImplSignificant) value;
sb.append('S');
minMaxToSkeletonHelper(rounding.minSig, rounding.maxSig, sb);
mathContext = rounding.mathContext;
} else if (value instanceof RoundingImplFractionSignificant) {
RoundingImplFractionSignificant rounding = (RoundingImplFractionSignificant) value;
sb.append('F');
minMaxToSkeletonHelper(rounding.minFrac, rounding.maxFrac, sb);
if (rounding.minSig != -1) {
sb.append('>');
sb.append(rounding.minSig);
} else {
sb.append('<');
sb.append(rounding.maxSig);
}
mathContext = rounding.mathContext;
} else if (value instanceof RoundingImplIncrement) {
RoundingImplIncrement rounding = (RoundingImplIncrement) value;
sb.append('M');
sb.append(rounding.increment.toString());
mathContext = rounding.mathContext;
} else if (value instanceof RoundingImplCurrency) {
RoundingImplCurrency rounding = (RoundingImplCurrency) value;
sb.append('G');
sb.append(rounding.usage.name());
mathContext = rounding.mc;
} else {
RoundingImplInfinity rounding = (RoundingImplInfinity) value;
sb.append('Y');
mathContext = rounding.mathContext;
}
// RoundingMode
RoundingMode roundingMode = mathContext.getRoundingMode();
if (roundingMode != RoundingMode.HALF_EVEN) {
sb.append(';');
sb.append(roundingMode.name());
}
}
private static void minMaxToSkeletonHelper(int minFrac, int maxFrac, StringBuilder sb) {
if (minFrac == maxFrac) {
sb.append(minFrac);
} else {
boolean showMaxFrac = (maxFrac >= 0 && maxFrac < Integer.MAX_VALUE);
if (minFrac > 0 || !showMaxFrac) {
sb.append(minFrac);
}
sb.append('-');
if (showMaxFrac) {
sb.append(maxFrac);
}
}
}
private static int skeletonToRounding(String skeleton, int offset, MacroProps output) {
int originalOffset = offset;
char c0 = skeleton.charAt(offset++);
Rounding result = null;
if (c0 == 'F') {
int[] minMax = new int[2];
offset += skeletonToMinMaxHelper(skeleton, offset, minMax);
FractionRounding temp = RoundingImplFraction.getInstance(minMax[0], minMax[1]);
char c1 = skeleton.charAt(offset++);
if (c1 == '<') {
char c2 = skeleton.charAt(offset++);
result = temp.withMaxFigures(c2 - '0');
} else if (c1 == '>') {
char c2 = skeleton.charAt(offset++);
result = temp.withMinFigures(c2 - '0');
} else {
result = temp;
}
} else if (c0 == 'S') {
int[] minMax = new int[2];
offset += skeletonToMinMaxHelper(skeleton, offset, minMax);
result = RoundingImplSignificant.getInstance(minMax[0], minMax[1]);
} else if (c0 == 'M') {
StringBuilder sb = new StringBuilder();
offset += consumeUntil(skeleton, offset, ' ', sb);
BigDecimal increment = new BigDecimal(sb.toString());
result = RoundingImplIncrement.getInstance(increment);
} else if (c0 == 'G') {
StringBuilder sb = new StringBuilder();
offset += consumeUntil(skeleton, offset, ' ', sb);
CurrencyUsage usage = Enum.valueOf(CurrencyUsage.class, sb.toString());
result = Rounding.currency(usage);
} else if (c0 == 'Y') {
result = Rounding.NONE;
}
output.rounding = result;
return offset - originalOffset;
}
private static int skeletonToMinMaxHelper(String skeleton, int offset, int[] output) {
int originalOffset = offset;
char c0 = safeCharAt(skeleton, offset++);
char c1 = safeCharAt(skeleton, offset++);
// TODO: This algorithm breaks if the number is more than 1 char wide.
if (c1 == '-') {
output[0] = c0 - '0';
char c2 = safeCharAt(skeleton, offset++);
if (c2 == ' ') {
output[1] = Integer.MAX_VALUE;
} else {
output[1] = c2 - '0';
}
} else if ('0' <= c1 && c1 <= '9') {
output[0] = 0;
output[1] = c1 - '0';
} else {
offset--;
output[0] = c0 - '0';
output[1] = c0 - '0';
}
return offset - originalOffset;
}
private static void groupingToSkeleton(IGrouping value, StringBuilder sb) {
if (!(value instanceof Grouping)) {
// FIXME: Throw an exception here instead?
sb.append("custom");
return;
}
if (value.equals(Grouping.DEFAULT)) {
sb.append("DEFAULT");
} else if (value.equals(Grouping.DEFAULT_MIN_2_DIGITS)) {
sb.append("DEFAULT_MIN_2_DIGITS");
} else if (value.equals(Grouping.NONE)) {
sb.append("NONE");
} else {
GroupingImpl grouping = (GroupingImpl) value;
if (grouping.grouping2 == -1 || grouping.grouping2 == 0) {
sb.append("NONE");
} else {
sb.append(grouping.grouping1);
if (grouping.grouping2 != grouping.grouping1) {
sb.append(',');
sb.append(grouping.grouping2);
}
if (grouping.min2) {
sb.append('&');
}
}
}
}
private static int skeletonToGrouping(String skeleton, int offset, MacroProps output) {
int originalOffset = offset;
char c0 = skeleton.charAt(offset++);
Grouping result = null;
if ('0' <= c0 && c0 <= '9') {
char c1 = safeCharAt(skeleton, offset++);
if (c1 == ',') {
char c2 = safeCharAt(skeleton, offset++);
char c3 = safeCharAt(skeleton, offset++);
result = GroupingImpl.getInstance((short) (c0 - '0'), (short) (c2 - '0'), c3 == '&');
} else {
result = GroupingImpl.getInstance((short) (c0 - '0'), (short) (c0 - '0'), c1 == '&');
}
} else {
StringBuilder sb = new StringBuilder();
offset += consumeUntil(skeleton, --offset, ' ', sb);
String name = sb.toString();
if (name.equals("DEFAULT")) {
result = Grouping.DEFAULT;
} else if (name.equals("DEFAULT_MIN_2_DIGITS")) {
result = Grouping.DEFAULT_MIN_2_DIGITS;
} else if (name.equals("NONE")) {
result = Grouping.NONE;
}
}
output.grouping = result;
return offset - originalOffset;
}
private static void paddingToSkeleton(Padding value, StringBuilder sb) {
PaddingImpl padding = (PaddingImpl) value;
if (padding == Padding.NONE) {
sb.append("NONE");
return;
}
sb.append("CP:");
// TODO: Handle padding strings that contain ':'
sb.append(padding.paddingString);
sb.append(':');
sb.append(padding.targetWidth);
sb.append(':');
sb.append(padding.position.name());
}
private static void integerWidthToSkeleton(IntegerWidth value, StringBuilder sb) {
IntegerWidthImpl impl = (IntegerWidthImpl) value;
sb.append(impl.minInt);
if (impl.maxInt != impl.minInt) {
sb.append('-');
if (impl.maxInt < Integer.MAX_VALUE) {
sb.append(impl.maxInt);
}
}
}
private static void symbolsToSkeleton(Object value, StringBuilder sb) {
if (value instanceof DecimalFormatSymbols) {
// TODO: Check to see if any of the symbols are not default?
sb.append("loc:");
sb.append(((DecimalFormatSymbols) value).getULocale());
} else {
sb.append("ns:");
sb.append(((NumberingSystem) value).getName());
}
}
private static void unitWidthToSkeleton(FormatWidth value, StringBuilder sb) {
sb.append(value.name());
}
private static void signToSkeleton(SignDisplay value, StringBuilder sb) {
sb.append(value.name());
}
private static void decimalToSkeleton(DecimalMarkDisplay value, StringBuilder sb) {
sb.append(value.name());
}
private static char safeCharAt(String str, int offset) {
if (offset < str.length()) {
return str.charAt(offset);
} else {
return ' ';
}
}
private static int consumeUntil(String skeleton, int offset, char brk, StringBuilder sb) {
int originalOffset = offset;
char c = safeCharAt(skeleton, offset++);
while (c != brk) {
sb.append(c);
c = safeCharAt(skeleton, offset++);
}
return offset - originalOffset;
}
}

View File

@ -0,0 +1,263 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import java.util.Map;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.LdmlPatternInfo;
import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.CompactDecimalFormat.CompactType;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.Dimensionless;
import com.ibm.icu.util.ULocale;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.Grouping;
import newapi.NumberFormatter.NotationCompact;
import newapi.NumberFormatter.NotationScientific;
import newapi.NumberFormatter.Rounding;
import newapi.NumberFormatter.SignDisplay;
public class Worker1 {
public static Worker1 fromMacros(MacroProps macros) {
return new Worker1(make(macros, true));
}
public static MicroProps applyStatic(
MacroProps macros, FormatQuantity inValue, NumberStringBuilder outString) {
MicroProps micros = make(macros, false).withQuantity(inValue);
applyStatic(micros, inValue, outString);
return micros;
}
private static final Currency DEFAULT_CURRENCY = Currency.getInstance("XXX");
final QuantityChain microsGenerator;
private Worker1(QuantityChain microsGenerator) {
this.microsGenerator = microsGenerator;
}
public MicroProps apply(FormatQuantity inValue, NumberStringBuilder outString) {
MicroProps micros = microsGenerator.withQuantity(inValue);
applyStatic(micros, inValue, outString);
return micros;
}
//////////
private static QuantityChain make(MacroProps input, boolean build) {
String innerPattern = null;
Map<StandardPlural, Modifier> outerMods = null;
Rounding defaultRounding = Rounding.NONE;
Currency currency = DEFAULT_CURRENCY;
FormatWidth unitWidth = null;
boolean perMille = false;
PluralRules rules = input.rules;
MicroProps micros = new MicroProps();
QuantityChain chain = micros;
// Copy over the simple settings
micros.sign = input.sign == null ? SignDisplay.AUTO : input.sign;
micros.decimal = input.decimal == null ? DecimalMarkDisplay.AUTO : input.decimal;
micros.multiplier = 0;
micros.integerWidth =
input.integerWidth == null
? IntegerWidthImpl.DEFAULT
: (IntegerWidthImpl) input.integerWidth;
if (input.unit == null || input.unit == Dimensionless.BASE) {
// No units; default format
innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE);
} else if (input.unit == Dimensionless.PERCENT) {
// Percent
innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.PERCENTSTYLE);
micros.multiplier += 2;
} else if (input.unit == Dimensionless.PERMILLE) {
// Permille
innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.PERCENTSTYLE);
micros.multiplier += 3;
perMille = true;
} else if (input.unit instanceof Currency && input.unitWidth != FormatWidth.WIDE) {
// Narrow, short, or ISO currency.
// TODO: Accounting style?
innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.CURRENCYSTYLE);
defaultRounding = Rounding.currency(CurrencyUsage.STANDARD);
currency = (Currency) input.unit;
micros.useCurrency = true;
unitWidth = (input.unitWidth == null) ? FormatWidth.NARROW : input.unitWidth;
} else if (input.unit instanceof Currency) {
// Currency long name
innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE);
outerMods = DataUtils.getCurrencyLongNameModifiers(input.loc, (Currency) input.unit);
defaultRounding = Rounding.currency(CurrencyUsage.STANDARD);
currency = (Currency) input.unit;
micros.useCurrency = true;
unitWidth = input.unitWidth = FormatWidth.WIDE;
} else {
// MeasureUnit
innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE);
unitWidth = (input.unitWidth == null) ? FormatWidth.SHORT : input.unitWidth;
outerMods = DataUtils.getMeasureUnitModifiers(input.loc, input.unit, unitWidth);
}
// Parse the pattern, which is used for grouping and affixes only.
PatternParseResult patternInfo = LdmlPatternInfo.parse(innerPattern);
// Symbols
if (input.symbols == null) {
micros.symbols = DecimalFormatSymbols.getInstance(input.loc);
} else if (input.symbols instanceof DecimalFormatSymbols) {
micros.symbols = (DecimalFormatSymbols) input.symbols;
} else if (input.symbols instanceof NumberingSystem) {
// TODO: Do this more efficiently. Will require modifying DecimalFormatSymbols.
NumberingSystem ns = (NumberingSystem) input.symbols;
ULocale temp = input.loc.setKeywordValue("numbers", ns.getName());
micros.symbols = DecimalFormatSymbols.getInstance(temp);
} else {
throw new AssertionError();
}
// TODO: Normalize the currency (accept symbols from DecimalFormatSymbols)?
// currency = CustomSymbolCurrency.resolve(currency, input.loc, micros.symbols);
// Multiplier (compatibility mode value).
// An int magnitude multiplier is used when not in compatibility mode to
// reduce object creations.
if (input.multiplier != null) {
// TODO: Make sure this is thread safe.
chain = input.multiplier.chain(chain);
}
// Rounding strategy
if (input.rounding != null) {
micros.rounding = RoundingImpl.normalizeType(input.rounding, currency);
} else if (input.notation instanceof NotationCompact) {
micros.rounding = RoundingImpl.RoundingImplFractionSignificant.COMPACT_STRATEGY;
} else {
micros.rounding = RoundingImpl.normalizeType(defaultRounding, currency);
}
// Grouping strategy
if (input.grouping != null) {
micros.grouping = GroupingImpl.normalizeType(input.grouping, patternInfo);
} else if (input.notation instanceof NotationCompact) {
// Compact notation uses minGrouping by default since ICU 59
micros.grouping = GroupingImpl.normalizeType(Grouping.DEFAULT_MIN_2_DIGITS, patternInfo);
} else {
micros.grouping = GroupingImpl.normalizeType(Grouping.DEFAULT, patternInfo);
}
// Inner modifier (scientific notation)
if (input.notation instanceof NotationScientific) {
assert input.notation instanceof NotationImpl.NotationScientificImpl;
chain =
ScientificImpl.getInstance(
(NotationImpl.NotationScientificImpl) input.notation, micros.symbols, build)
.chain(chain);
} else {
// No inner modifier required
micros.modInner = ConstantAffixModifier.EMPTY;
}
// Middle modifier (patterns, positive/negative, currency symbols, percent)
MurkyModifier murkyMod = new MurkyModifier(false);
murkyMod.setPatternInfo((input.affixProvider != null) ? input.affixProvider : patternInfo);
murkyMod.setPatternAttributes(micros.sign, perMille);
if (murkyMod.needsPlurals()) {
if (rules == null) {
// Lazily create PluralRules
rules = PluralRules.forLocale(input.loc);
}
murkyMod.setSymbols(micros.symbols, currency, unitWidth, rules);
} else {
murkyMod.setSymbols(micros.symbols, currency, unitWidth, null);
}
if (build) {
chain = murkyMod.createImmutable().chain(chain);
} else {
chain = murkyMod.chain(chain);
}
// Outer modifier (CLDR units and currency long names)
if (outerMods != null) {
if (rules == null) {
// Lazily create PluralRules
rules = PluralRules.forLocale(input.loc);
}
chain = new QuantityDependentModOuter(outerMods, rules).chain(chain);
} else {
// No outer modifier required
micros.modOuter = ConstantAffixModifier.EMPTY;
}
// Padding strategy
if (input.padding != null) {
micros.padding = (PaddingImpl) input.padding;
} else {
micros.padding = PaddingImpl.NONE;
}
// Compact notation
// NOTE: Compact notation can (but might not) override the middle modifier and rounding.
// It therefore needs to go at the end of the chain.
if (input.notation instanceof NotationCompact) {
assert input.notation instanceof NotationImpl.NotationCompactImpl;
if (rules == null) {
// Lazily create PluralRules
rules = PluralRules.forLocale(input.loc);
}
CompactStyle compactStyle = ((NotationImpl.NotationCompactImpl) input.notation).compactStyle;
CompactImpl worker;
if (compactStyle == null) {
// Use compact custom data
worker =
CompactImpl.getInstance(
((NotationImpl.NotationCompactImpl) input.notation).compactCustomData, rules);
} else {
CompactType compactType =
(input.unit instanceof Currency) ? CompactType.CURRENCY : CompactType.DECIMAL;
worker = CompactImpl.getInstance(input.loc, compactType, compactStyle, rules);
}
if (build) {
worker.precomputeAllModifiers(murkyMod);
}
chain = worker.chain(chain);
}
if (build) {
micros.enableCloneInChain();
}
return chain;
}
//////////
private static int applyStatic(
MicroProps micros, FormatQuantity inValue, NumberStringBuilder outString) {
inValue.adjustMagnitude(micros.multiplier);
micros.rounding.apply(inValue);
inValue.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt);
int length = PositiveDecimalImpl.apply(micros, inValue, outString);
// NOTE: When range formatting is added, these modifiers can bubble up.
// For now, apply them all here at once.
length += micros.padding.applyModsAndMaybePad(micros, outString, 0, length);
return length;
}
}

View File

@ -0,0 +1,58 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.number;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import com.ibm.icu.impl.number.LdmlPatternInfo;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.ULocale;
import newapi.NumberFormatter.SignDisplay;
import newapi.impl.MurkyModifier;
public class MurkyModifierTest {
@Test
public void basic() {
MurkyModifier murky = new MurkyModifier(false);
murky.setPatternInfo(LdmlPatternInfo.parse("a0b"));
murky.setPatternAttributes(SignDisplay.AUTO, false);
murky.setSymbols(
DecimalFormatSymbols.getInstance(ULocale.ENGLISH),
Currency.getInstance("USD"),
FormatWidth.SHORT,
null);
murky.setNumberProperties(false, null);
assertEquals("a", murky.getPrefix());
assertEquals("b", murky.getSuffix());
murky.setPatternAttributes(SignDisplay.ALWAYS_SHOWN, false);
assertEquals("+a", murky.getPrefix());
assertEquals("b", murky.getSuffix());
murky.setNumberProperties(true, null);
assertEquals("-a", murky.getPrefix());
assertEquals("b", murky.getSuffix());
murky.setPatternAttributes(SignDisplay.NEVER_SHOWN, false);
assertEquals("a", murky.getPrefix());
assertEquals("b", murky.getSuffix());
murky.setPatternInfo(LdmlPatternInfo.parse("a0b;c-0d"));
murky.setPatternAttributes(SignDisplay.AUTO, false);
murky.setNumberProperties(false, null);
assertEquals("a", murky.getPrefix());
assertEquals("b", murky.getSuffix());
murky.setPatternAttributes(SignDisplay.ALWAYS_SHOWN, false);
assertEquals("c+", murky.getPrefix());
assertEquals("d", murky.getSuffix());
murky.setNumberProperties(true, null);
assertEquals("c-", murky.getPrefix());
assertEquals("d", murky.getSuffix());
murky.setPatternAttributes(SignDisplay.NEVER_SHOWN, false);
assertEquals("c-", murky.getPrefix()); // TODO: What should this behavior be?
assertEquals("d", murky.getSuffix());
}
}

File diff suppressed because it is too large Load Diff