From: Shane Carr Date: Wed, 29 Aug 2018 02:50:55 +0000 (-0700) Subject: ICU-11276 Adding initial Java NumberRangeFormatter boilerplate. X-Git-Tag: release-63-rc~63^2~36 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=553f22585d30661359183b2fd65b22138558f8c4;p=icu ICU-11276 Adding initial Java NumberRangeFormatter boilerplate. --- diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/RangeMacroProps.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/RangeMacroProps.java new file mode 100644 index 00000000000..9595e7d55a9 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/RangeMacroProps.java @@ -0,0 +1,47 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.range; + +import java.util.Objects; + +import com.ibm.icu.number.NumberFormatterSettings; +import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse; +import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback; +import com.ibm.icu.util.ULocale; + +/** + * @author sffc + * + */ +public class RangeMacroProps { + public NumberFormatterSettings formatter1; + public NumberFormatterSettings formatter2; + public RangeCollapse collapse; + public RangeIdentityFallback identityFallback; + public ULocale loc; + + @Override + public int hashCode() { + return Objects.hash(formatter1, + formatter2, + collapse, + identityFallback, + loc); + } + + @Override + public boolean equals(Object _other) { + if (_other == null) + return false; + if (this == _other) + return true; + if (!(_other instanceof RangeMacroProps)) + return false; + RangeMacroProps other = (RangeMacroProps) _other; + return Objects.equals(formatter1, other.formatter1) + && Objects.equals(formatter2, other.formatter2) + && Objects.equals(collapse, other.collapse) + && Objects.equals(identityFallback, other.identityFallback) + && Objects.equals(loc, other.loc); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java b/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java new file mode 100644 index 00000000000..f3add3297ea --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java @@ -0,0 +1,211 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.number; + +import java.io.IOException; +import java.math.BigDecimal; +import java.text.AttributedCharacterIterator; +import java.text.FieldPosition; +import java.util.Arrays; + +import com.ibm.icu.impl.number.DecimalQuantity; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.util.ICUUncheckedIOException; + +/** + * The result of a number range formatting operation. This class allows the result to be exported in several data types, + * including a String, an AttributedCharacterIterator, and a BigDecimal. + * + * @author sffc + * @draft ICU 62 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ +public class FormattedNumberRange { + final NumberStringBuilder nsb; + final DecimalQuantity first; + final DecimalQuantity second; + final RangeIdentityType identityType; + + public static enum RangeIdentityType { + EQUAL_BEFORE_ROUNDING, EQUAL_AFTER_ROUNDING, NOT_EQUAL + } + + FormattedNumberRange(NumberStringBuilder nsb, DecimalQuantity first, DecimalQuantity second, + RangeIdentityType identityType) { + this.nsb = nsb; + this.first = first; + this.second = second; + this.identityType = identityType; + } + + /** + * Creates a String representation of the the formatted number range. + * + * @return a String containing the localized number range. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + @Override + public String toString() { + return nsb.toString(); + } + + /** + * Append the formatted number range to an Appendable, such as a StringBuilder. This may be slightly more efficient + * than creating a String. + * + *

+ * If an IOException occurs when appending to the Appendable, an unchecked {@link ICUUncheckedIOException} is thrown + * instead. + * + * @param appendable + * The Appendable to which to append the formatted number range string. + * @return The same Appendable, for chaining. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see Appendable + * @see NumberRangeFormatter + */ + public A appendTo(A appendable) { + try { + appendable.append(nsb); + } catch (IOException e) { + // Throw as an unchecked exception to avoid users needing try/catch + throw new ICUUncheckedIOException(e); + } + return appendable; + } + + /** + * Determines the start and end indices of the next occurrence of the given field in the output string. + * This allows you to determine the locations of, for example, the integer part, fraction part, or symbols. + *

+ * If both sides of the range have the same field, the field will occur twice, once before the range separator and + * once after the range separator, if applicable. + *

+ * If a field occurs just once, calling this method will find that occurrence and return it. If a field occurs + * multiple times, this method may be called repeatedly with the following pattern: + * + *

+     * FieldPosition fpos = new FieldPosition(NumberFormat.Field.GROUPING_SEPARATOR);
+     * while (formattedNumber.nextFieldPosition(fpos, status)) {
+     *     // do something with fpos.
+     * }
+     * 
+ *

+ * This method is useful if you know which field to query. If you want all available field position information, use + * {@link #toCharacterIterator()}. + * + * @param fieldPosition + * Input+output variable. See {@link FormattedNumber#nextFieldPosition(FieldPosition)}. + * @return true if a new occurrence of the field was found; false otherwise. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see com.ibm.icu.text.NumberFormat.Field + * @see NumberRangeFormatter + */ + public boolean nextFieldPosition(FieldPosition fieldPosition) { + return nsb.nextFieldPosition(fieldPosition); + } + + /** + * Export the formatted number range as an AttributedCharacterIterator. This allows you to determine which + * characters in the output string correspond to which fields, such as the integer part, fraction part, and + * sign. + *

+ * If information on only one field is needed, use {@link #nextFieldPosition(FieldPosition)} instead. + * + * @return An AttributedCharacterIterator, containing information on the field attributes of the number range + * string. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see com.ibm.icu.text.NumberFormat.Field + * @see AttributedCharacterIterator + * @see NumberRangeFormatter + */ + public AttributedCharacterIterator toCharacterIterator() { + return nsb.toCharacterIterator(); + } + + /** + * Export the first formatted number as a BigDecimal. This endpoint is useful for obtaining the exact number being + * printed after scaling and rounding have been applied by the number range formatting pipeline. + * + * @return A BigDecimal representation of the first formatted number. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + * @see #getSecondBigDecimal + */ + public BigDecimal getFirstBigDecimal() { + return first.toBigDecimal(); + } + + /** + * Export the second formatted number as a BigDecimal. This endpoint is useful for obtaining the exact number being + * printed after scaling and rounding have been applied by the number range formatting pipeline. + * + * @return A BigDecimal representation of the second formatted number. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + * @see #getFirstBigDecimal + */ + public BigDecimal getSecondBigDecimal() { + return second.toBigDecimal(); + } + + /** + * Returns whether the pair of numbers was successfully formatted as a range or whether an identity fallback was + * used. For example, if the first and second number were the same either before or after rounding occurred, an + * identity fallback was used. + * + * @return A IdentityType indicating the resulting identity situation in the formatted number range. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + * @see NumberRangeFormatter.RangeIdentityFallback + */ + public RangeIdentityType getIdentityType() { + return identityType; + } + + /** + * {@inheritDoc} + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + */ + @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()) + ^ first.toBigDecimal().hashCode() ^ second.toBigDecimal().hashCode(); + } + + /** + * {@inheritDoc} + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + */ + @Override + public boolean equals(Object other) { + if (this == other) + return true; + if (other == null) + return false; + if (!(other instanceof FormattedNumberRange)) + return false; + // NumberStringBuilder and BigDecimal are mutable, so we can't call + // #equals() or #hashCode() on them directly. + FormattedNumberRange _other = (FormattedNumberRange) other; + return Arrays.equals(nsb.toCharArray(), _other.nsb.toCharArray()) + && Arrays.equals(nsb.toFieldArray(), _other.nsb.toFieldArray()) + && first.toBigDecimal().equals(_other.first.toBigDecimal()) + && second.toBigDecimal().equals(_other.second.toBigDecimal()); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberRangeFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberRangeFormatter.java new file mode 100644 index 00000000000..0721ab71bb6 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberRangeFormatter.java @@ -0,0 +1,56 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.number; + +import com.ibm.icu.impl.number.DecimalQuantity; +import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.number.FormattedNumberRange.RangeIdentityType; +import com.ibm.icu.text.NumberFormat; + +/** + * A NumberRangeFormatter that has a locale associated with it; this means .formatRange() methods are available. + * + * @author sffc + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ +public class LocalizedNumberRangeFormatter extends NumberRangeFormatterSettings { + + LocalizedNumberRangeFormatter(NumberRangeFormatterSettings parent, int key, Object value) { + super(parent, key, value); + } + + /** + * Format the given integers to a string using the settings specified in the NumberRangeFormatter fluent setting + * chain. + * + * @param first + * The first number in the range, usually to the left in LTR locales. + * @param second + * The second number in the range, usually to the right in LTR locales. + * @return A FormattedNumber object; call .toString() to get the string. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + public FormattedNumberRange formatRange(int first, int second) { + // TODO: This is a placeholder implementation. + DecimalQuantity dq1 = new DecimalQuantity_DualStorageBCD(first); + DecimalQuantity dq2 = new DecimalQuantity_DualStorageBCD(second); + NumberStringBuilder nsb = new NumberStringBuilder(); + nsb.append(dq1.toPlainString(), NumberFormat.Field.INTEGER); + nsb.append(" --- ", null); + nsb.append(dq2.toPlainString(), NumberFormat.Field.INTEGER); + RangeIdentityType identityType = (first == second) ? RangeIdentityType.EQUAL_BEFORE_ROUNDING + : RangeIdentityType.NOT_EQUAL; + return new FormattedNumberRange(nsb, dq1, dq2, identityType); + } + + @Override + LocalizedNumberRangeFormatter create(int key, Object value) { + return new LocalizedNumberRangeFormatter(this, key, value); + } + +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatter.java new file mode 100644 index 00000000000..8c351b35cf8 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatter.java @@ -0,0 +1,19 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.number; + +/** + * The main entrypoint to the formatting of ranges of numbers, including currencies and other units of measurement. + * + * @author sffc + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberFormatter + */ +public abstract class NumberRangeFormatter { + + public static enum RangeCollapse {} + + public static enum RangeIdentityFallback {} + +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java new file mode 100644 index 00000000000..d7725e8a2f9 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java @@ -0,0 +1,139 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.number; + +import com.ibm.icu.impl.number.range.RangeMacroProps; +import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse; +import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback; +import com.ibm.icu.util.ULocale; + +/** + * An abstract base class for specifying settings related to number formatting. This class is implemented by + * {@link UnlocalizedNumberRangeFormatter} and {@link LocalizedNumberRangeFormatter}. This class is not intended for + * public subclassing. + * + * @author sffc + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ +public abstract class NumberRangeFormatterSettings> { + + static final int KEY_MACROS = 0; // not used + static final int KEY_LOCALE = 1; + static final int KEY_FORMATTER_1 = 2; + static final int KEY_FORMATTER_2 = 3; + static final int KEY_COLLAPSE = 4; + static final int KEY_IDENTITY_FALLBACK = 5; + static final int KEY_MAX = 6; + + final NumberRangeFormatterSettings parent; + final int key; + final Object value; + volatile RangeMacroProps resolvedMacros; + + NumberRangeFormatterSettings(NumberRangeFormatterSettings parent, int key, Object value) { + this.parent = parent; + this.key = key; + this.value = value; + } + + public T numberFormatter(NumberFormatterSettings formatter) { + return numberFormatters(formatter, formatter); + } + + public T numberFormatters(NumberFormatterSettings formatterFirst, NumberFormatterSettings formatterSecond) { + T intermediate = create(KEY_FORMATTER_1, formatterFirst); + return (T) intermediate.create(KEY_FORMATTER_2, formatterSecond); + } + + public T collapse(RangeCollapse collapse) { + return create(KEY_COLLAPSE, collapse); + } + + public T identityFallback(RangeIdentityFallback identityFallback) { + return create(KEY_IDENTITY_FALLBACK, identityFallback); + } + + /* package-protected */ abstract T create(int key, Object value); + + RangeMacroProps 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. + // TODO: Remove the reference to the parent after the macros are resolved? + RangeMacroProps macros = new RangeMacroProps(); + NumberRangeFormatterSettings current = this; + while (current != null) { + switch (current.key) { + case KEY_MACROS: + // ignored for now + break; + case KEY_LOCALE: + if (macros.loc == null) { + macros.loc = (ULocale) current.value; + } + break; + case KEY_FORMATTER_1: + if (macros.formatter1 == null) { + macros.formatter1 = (NumberFormatterSettings) current.value; + } + break; + case KEY_FORMATTER_2: + if (macros.formatter2 == null) { + macros.formatter2 = (NumberFormatterSettings) current.value; + } + break; + case KEY_COLLAPSE: + if (macros.collapse == null) { + macros.collapse = (RangeCollapse) current.value; + } + break; + case KEY_IDENTITY_FALLBACK: + if (macros.identityFallback == null) { + macros.identityFallback = (RangeIdentityFallback) current.value; + } + break; + default: + throw new AssertionError("Unknown key: " + current.key); + } + current = current.parent; + } + resolvedMacros = macros; + return macros; + } + + /** + * {@inheritDoc} + * + * @draft ICU 60 + * @provisional This API might change or be removed in a future release. + */ + @Override + public int hashCode() { + return resolve().hashCode(); + } + + /** + * {@inheritDoc} + * + * @draft ICU 60 + * @provisional This API might change or be removed in a future release. + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null) { + return false; + } + if (!(other instanceof NumberFormatterSettings)) { + return false; + } + return resolve().equals(((NumberFormatterSettings) other).resolve()); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/UnlocalizedNumberRangeFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/number/UnlocalizedNumberRangeFormatter.java new file mode 100644 index 00000000000..c3ee21e43d0 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/UnlocalizedNumberRangeFormatter.java @@ -0,0 +1,67 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.number; + +import java.util.Locale; + +import com.ibm.icu.util.ULocale; + +/** + * A NumberRangeFormatter that does not yet have a locale. In order to format, a locale must be specified. + * + * @author sffc + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ +public class UnlocalizedNumberRangeFormatter extends NumberRangeFormatterSettings { + + /** Base constructor; called during startup only. */ + UnlocalizedNumberRangeFormatter() { + super(null, KEY_MACROS, null); + } + + UnlocalizedNumberRangeFormatter(NumberRangeFormatterSettings parent, int key, Object value) { + super(parent, key, value); + } + + /** + * Associate the given locale with the number range formatter. The locale is used for picking the + * appropriate symbols, formats, and other data for number display. + * + *

+ * To use the Java default locale, call Locale.getDefault(): + * + *

+     * NumberFormatter.with(). ... .locale(Locale.getDefault())
+     * 
+ * + * @param locale + * The locale to use when loading data for number range formatting. + * @return The fluent chain + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + */ + public LocalizedNumberRangeFormatter locale(Locale locale) { + return new LocalizedNumberRangeFormatter(this, KEY_LOCALE, ULocale.forLocale(locale)); + } + + /** + * ULocale version of the {@link #locale(Locale)} setter above. + * + * @param locale + * The locale to use when loading data for number range formatting. + * @return The fluent chain + * @see #locale(Locale) + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + */ + public LocalizedNumberRangeFormatter locale(ULocale locale) { + return new LocalizedNumberRangeFormatter(this, KEY_LOCALE, locale); + } + + @Override + UnlocalizedNumberRangeFormatter create(int key, Object value) { + return new UnlocalizedNumberRangeFormatter(this, key, value); + } +}