From 553f22585d30661359183b2fd65b22138558f8c4 Mon Sep 17 00:00:00 2001 From: Shane Carr Date: Tue, 28 Aug 2018 19:50:55 -0700 Subject: [PATCH 01/37] ICU-11276 Adding initial Java NumberRangeFormatter boilerplate. --- .../impl/number/range/RangeMacroProps.java | 47 ++++ .../ibm/icu/number/FormattedNumberRange.java | 211 ++++++++++++++++++ .../number/LocalizedNumberRangeFormatter.java | 56 +++++ .../ibm/icu/number/NumberRangeFormatter.java | 19 ++ .../number/NumberRangeFormatterSettings.java | 139 ++++++++++++ .../UnlocalizedNumberRangeFormatter.java | 67 ++++++ 6 files changed, 539 insertions(+) create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/RangeMacroProps.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberRangeFormatter.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatter.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/number/UnlocalizedNumberRangeFormatter.java 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); + } +} From 37a40b31ede1e43fca38f6a609873f0322c4d976 Mon Sep 17 00:00:00 2001 From: Shane Carr Date: Tue, 28 Aug 2018 20:34:41 -0700 Subject: [PATCH 02/37] ICU-11276 Adding placeholder implementation and more API functions. --- .../impl/number/range/RangeMacroProps.java | 6 +- .../number/LocalizedNumberRangeFormatter.java | 66 ++++++- .../ibm/icu/number/NumberRangeFormatter.java | 18 ++ .../number/NumberRangeFormatterSettings.java | 12 +- .../test/number/NumberRangeFormatterTest.java | 162 ++++++++++++++++++ 5 files changed, 250 insertions(+), 14 deletions(-) create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberRangeFormatterTest.java 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 index 9595e7d55a9..800dcf6ab9b 100644 --- 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 @@ -4,9 +4,9 @@ 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.number.UnlocalizedNumberFormatter; import com.ibm.icu.util.ULocale; /** @@ -14,8 +14,8 @@ import com.ibm.icu.util.ULocale; * */ public class RangeMacroProps { - public NumberFormatterSettings formatter1; - public NumberFormatterSettings formatter2; + public UnlocalizedNumberFormatter formatter1; + public UnlocalizedNumberFormatter formatter2; public RangeCollapse collapse; public RangeIdentityFallback identityFallback; public ULocale loc; 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 index 0721ab71bb6..87d94941bb0 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberRangeFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberRangeFormatter.java @@ -5,8 +5,8 @@ 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.impl.number.range.RangeMacroProps; 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. @@ -36,16 +36,72 @@ public class LocalizedNumberRangeFormatter extends NumberRangeFormatterSettings< * @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); + return formatImpl(dq1, dq2); + } + + /** + * Format the given doubles 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(double first, double second) { + DecimalQuantity dq1 = new DecimalQuantity_DualStorageBCD(first); + DecimalQuantity dq2 = new DecimalQuantity_DualStorageBCD(second); + return formatImpl(dq1, dq2); + } + + /** + * Format the given Numbers 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(Number first, Number second) { + DecimalQuantity dq1 = new DecimalQuantity_DualStorageBCD(first); + DecimalQuantity dq2 = new DecimalQuantity_DualStorageBCD(second); + return formatImpl(dq1, dq2); + } + + FormattedNumberRange formatImpl(DecimalQuantity first, DecimalQuantity second) { + // TODO: This is a placeholder implementation. + RangeMacroProps macros = resolve(); + LocalizedNumberFormatter f1 , f2; + if (macros.formatter1 != null) { + f1 = macros.formatter1.locale(macros.loc); + } else { + f1 = NumberFormatter.withLocale(macros.loc); + } + if (macros.formatter2 != null) { + f2 = macros.formatter2.locale(macros.loc); + } else { + f2 = NumberFormatter.withLocale(macros.loc); + } + FormattedNumber r1 = f1.format(first); + FormattedNumber r2 = f2.format(second); NumberStringBuilder nsb = new NumberStringBuilder(); - nsb.append(dq1.toPlainString(), NumberFormat.Field.INTEGER); + nsb.append(r1.nsb); nsb.append(" --- ", null); - nsb.append(dq2.toPlainString(), NumberFormat.Field.INTEGER); + nsb.append(r2.nsb); RangeIdentityType identityType = (first == second) ? RangeIdentityType.EQUAL_BEFORE_ROUNDING : RangeIdentityType.NOT_EQUAL; - return new FormattedNumberRange(nsb, dq1, dq2, identityType); + return new FormattedNumberRange(nsb, first, second, identityType); } @Override 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 index 8c351b35cf8..da6618b5626 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatter.java @@ -2,6 +2,10 @@ // 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; + /** * The main entrypoint to the formatting of ranges of numbers, including currencies and other units of measurement. * @@ -16,4 +20,18 @@ public abstract class NumberRangeFormatter { public static enum RangeIdentityFallback {} + private static final UnlocalizedNumberRangeFormatter BASE = new UnlocalizedNumberRangeFormatter(); + + public static UnlocalizedNumberRangeFormatter with() { + return BASE; + } + + public static LocalizedNumberRangeFormatter withLocale(Locale locale) { + return BASE.locale(locale); + } + + public static LocalizedNumberRangeFormatter withLocale(ULocale locale) { + return BASE.locale(locale); + } + } 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 index d7725e8a2f9..23de02a0716 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java @@ -38,11 +38,11 @@ public abstract class NumberRangeFormatterSettings formatter) { + public T numberFormatter(UnlocalizedNumberFormatter formatter) { return numberFormatters(formatter, formatter); } - public T numberFormatters(NumberFormatterSettings formatterFirst, NumberFormatterSettings formatterSecond) { + public T numberFormatters(UnlocalizedNumberFormatter formatterFirst, UnlocalizedNumberFormatter formatterSecond) { T intermediate = create(KEY_FORMATTER_1, formatterFirst); return (T) intermediate.create(KEY_FORMATTER_2, formatterSecond); } @@ -79,12 +79,12 @@ public abstract class NumberRangeFormatterSettings) current.value; + macros.formatter1 = (UnlocalizedNumberFormatter) current.value; } break; case KEY_FORMATTER_2: if (macros.formatter2 == null) { - macros.formatter2 = (NumberFormatterSettings) current.value; + macros.formatter2 = (UnlocalizedNumberFormatter) current.value; } break; case KEY_COLLAPSE: @@ -131,9 +131,9 @@ public abstract class NumberRangeFormatterSettings) other).resolve()); + return resolve().equals(((NumberRangeFormatterSettings) other).resolve()); } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberRangeFormatterTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberRangeFormatterTest.java new file mode 100644 index 00000000000..c0cfce5c1f5 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberRangeFormatterTest.java @@ -0,0 +1,162 @@ +// © 2018 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 java.util.Locale; + +import org.junit.Test; + +import com.ibm.icu.number.LocalizedNumberRangeFormatter; +import com.ibm.icu.number.NumberFormatter; +import com.ibm.icu.number.NumberFormatter.GroupingStrategy; +import com.ibm.icu.number.NumberRangeFormatter; +import com.ibm.icu.number.UnlocalizedNumberRangeFormatter; +import com.ibm.icu.util.Currency; +import com.ibm.icu.util.ULocale; + +/** + * @author sffc + * + */ +public class NumberRangeFormatterTest { + + private static final Currency USD = Currency.getInstance("USD"); + private static final Currency GBP = Currency.getInstance("GBP"); + + @Test + public void testSanity() { + LocalizedNumberRangeFormatter lnrf1 = NumberRangeFormatter.withLocale(ULocale.US); + LocalizedNumberRangeFormatter lnrf2 = NumberRangeFormatter.with().locale(ULocale.US); + LocalizedNumberRangeFormatter lnrf3 = NumberRangeFormatter.withLocale(Locale.US); + LocalizedNumberRangeFormatter lnrf4 = NumberRangeFormatter.with().locale(Locale.US); + assertEquals("Formatters should be equal 1", lnrf1, lnrf2); + assertEquals("Formatters should be equal 2", lnrf2, lnrf3); + assertEquals("Formatters should be equal 3", lnrf3, lnrf4); + assertEquals("Formatters should have same behavior 1", lnrf1.formatRange(4, 6), lnrf2.formatRange(4, 6)); + assertEquals("Formatters should have same behavior 2", lnrf2.formatRange(4, 6), lnrf3.formatRange(4, 6)); + assertEquals("Formatters should have same behavior 3", lnrf3.formatRange(4, 6), lnrf4.formatRange(4, 6)); + } + + @Test + public void testBasic() { + assertFormatRange( + "Basic", + NumberRangeFormatter.with(), + ULocale.US, + "1 --- 5", + "5 --- 5", + "5 --- 5", + "0 --- 3", + "0 --- 0", + "3 --- 3,000", + "3,000 --- 5,000", + "4,999 --- 5,001", + "5,000 --- 5,000", + "5,000 --- 5,000,000"); + } + + @Test + public void testNullBehavior() { + assertFormatRange( + "Basic", + NumberRangeFormatter.with().numberFormatter(null), + ULocale.US, + "1 --- 5", + "5 --- 5", + "5 --- 5", + "0 --- 3", + "0 --- 0", + "3 --- 3,000", + "3,000 --- 5,000", + "4,999 --- 5,001", + "5,000 --- 5,000", + "5,000 --- 5,000,000"); + + assertFormatRange( + "Basic", + NumberRangeFormatter.with().numberFormatters(null, null), + ULocale.US, + "1 --- 5", + "5 --- 5", + "5 --- 5", + "0 --- 3", + "0 --- 0", + "3 --- 3,000", + "3,000 --- 5,000", + "4,999 --- 5,001", + "5,000 --- 5,000", + "5,000 --- 5,000,000"); + + assertFormatRange( + "Basic", + NumberRangeFormatter.with().numberFormatters( + NumberFormatter.with().grouping(GroupingStrategy.OFF), + null + ), + ULocale.US, + "1 --- 5", + "5 --- 5", + "5 --- 5", + "0 --- 3", + "0 --- 0", + "3 --- 3,000", + "3000 --- 5,000", + "4999 --- 5,001", + "5000 --- 5,000", + "5000 --- 5,000,000"); + + assertFormatRange( + "Basic", + NumberRangeFormatter.with().numberFormatters( + null, + NumberFormatter.with().grouping(GroupingStrategy.OFF) + ), + ULocale.US, + "1 --- 5", + "5 --- 5", + "5 --- 5", + "0 --- 3", + "0 --- 0", + "3 --- 3000", + "3,000 --- 5000", + "4,999 --- 5001", + "5,000 --- 5000", + "5,000 --- 5000000"); + } + + static void assertFormatRange( + String message, + UnlocalizedNumberRangeFormatter f, + ULocale locale, + String expected_10_50, + String expected_49_51, + String expected_50_50, + String expected_00_30, + String expected_00_00, + String expected_30_3K, + String expected_30K_50K, + String expected_49K_51K, + String expected_50K_50K, + String expected_50K_50M) { + LocalizedNumberRangeFormatter l = f.locale(locale); + assertFormattedRangeEquals(message, l, 1, 5, expected_10_50); + assertFormattedRangeEquals(message, l, 4.9999999, 5.0000001, expected_49_51); + assertFormattedRangeEquals(message, l, 5, 5, expected_50_50); + assertFormattedRangeEquals(message, l, 0, 3, expected_00_30); + assertFormattedRangeEquals(message, l, 0, 0, expected_00_00); + assertFormattedRangeEquals(message, l, 3, 3000, expected_30_3K); + assertFormattedRangeEquals(message, l, 3000, 5000, expected_30K_50K); + assertFormattedRangeEquals(message, l, 4999, 5001, expected_49K_51K); + assertFormattedRangeEquals(message, l, 5000, 5000, expected_50K_50K); + assertFormattedRangeEquals(message, l, 5e3, 5e6, expected_50K_50M); + } + + private static void assertFormattedRangeEquals(String message, LocalizedNumberRangeFormatter l, Number first, + Number second, String expected) { + String actual1 = l.formatRange(first, second).toString(); + assertEquals(message + ": " + first + ", " + second, expected, actual1); + } + +} From fc0e6258dbee3d10ff52e4573ecfdee951669d2b Mon Sep 17 00:00:00 2001 From: Shane Carr Date: Tue, 28 Aug 2018 21:10:28 -0700 Subject: [PATCH 03/37] ICU-11276 Adding enums and more API docs. --- .../number/LocalizedNumberRangeFormatter.java | 15 ++- .../ibm/icu/number/NumberRangeFormatter.java | 127 +++++++++++++++++- .../number/NumberRangeFormatterSettings.java | 71 ++++++++++ 3 files changed, 206 insertions(+), 7 deletions(-) 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 index 87d94941bb0..193706b3a96 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberRangeFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberRangeFormatter.java @@ -38,7 +38,7 @@ public class LocalizedNumberRangeFormatter extends NumberRangeFormatterSettings< public FormattedNumberRange formatRange(int first, int second) { DecimalQuantity dq1 = new DecimalQuantity_DualStorageBCD(first); DecimalQuantity dq2 = new DecimalQuantity_DualStorageBCD(second); - return formatImpl(dq1, dq2); + return formatImpl(dq1, dq2, first == second); } /** @@ -57,7 +57,9 @@ public class LocalizedNumberRangeFormatter extends NumberRangeFormatterSettings< public FormattedNumberRange formatRange(double first, double second) { DecimalQuantity dq1 = new DecimalQuantity_DualStorageBCD(first); DecimalQuantity dq2 = new DecimalQuantity_DualStorageBCD(second); - return formatImpl(dq1, dq2); + // Note: double equality could be changed to epsilon equality later if there is demand. + // The epsilon should be set via an API method. + return formatImpl(dq1, dq2, first == second); } /** @@ -74,12 +76,15 @@ public class LocalizedNumberRangeFormatter extends NumberRangeFormatterSettings< * @see NumberRangeFormatter */ public FormattedNumberRange formatRange(Number first, Number second) { + if (first == null || second == null) { + throw new IllegalArgumentException("Cannot format null values in range"); + } DecimalQuantity dq1 = new DecimalQuantity_DualStorageBCD(first); DecimalQuantity dq2 = new DecimalQuantity_DualStorageBCD(second); - return formatImpl(dq1, dq2); + return formatImpl(dq1, dq2, first.equals(second)); } - FormattedNumberRange formatImpl(DecimalQuantity first, DecimalQuantity second) { + FormattedNumberRange formatImpl(DecimalQuantity first, DecimalQuantity second, boolean equalBeforeRounding) { // TODO: This is a placeholder implementation. RangeMacroProps macros = resolve(); LocalizedNumberFormatter f1 , f2; @@ -99,7 +104,7 @@ public class LocalizedNumberRangeFormatter extends NumberRangeFormatterSettings< nsb.append(r1.nsb); nsb.append(" --- ", null); nsb.append(r2.nsb); - RangeIdentityType identityType = (first == second) ? RangeIdentityType.EQUAL_BEFORE_ROUNDING + RangeIdentityType identityType = equalBeforeRounding ? RangeIdentityType.EQUAL_BEFORE_ROUNDING : RangeIdentityType.NOT_EQUAL; return new FormattedNumberRange(nsb, first, second, identityType); } 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 index da6618b5626..a138a6d2daf 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatter.java @@ -16,9 +16,132 @@ import com.ibm.icu.util.ULocale; */ public abstract class NumberRangeFormatter { - public static enum RangeCollapse {} + /** + * Defines how to merge fields that are identical across the range sign. + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + public enum RangeCollapse { + /** + * Use locale data and heuristics to determine how much of the string to collapse. Could end up collapsing none, + * some, or all repeated pieces in a locale-sensitive way. + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + AUTO, - public static enum RangeIdentityFallback {} + /** + * Do not collapse any part of the number. Example: "3.2 thousand kilograms – 5.3 thousand kilograms" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + NONE, + + /** + * Collapse the unit part of the number, but not the notation, if present. Example: "3.2 thousand – 5.3 thousand + * kilograms" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + UNIT, + + /** + * Collapse any field that is equal across the range sign. May introduce ambiguity on the magnitude of the + * number. Example: "3.2 – 5.3 thousand kilograms" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + ALL + } + + /** + * Defines the behavior when the two numbers in the range are identical after rounding. To programmatically detect + * when the identity fallback is used, compare the lower and upper BigDecimals via FormattedNumber. + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + public enum IdentityFallback { + /** + * Show the number as a single value rather than a range. Example: "$5" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + SINGLE_VALUE, + + /** + * Show the number using a locale-sensitive approximation pattern. If the numbers were the same before rounding, + * show the single value. Example: "~$5" or "$5" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + APPROXIMATELY_OR_SINGLE_VALUE, + + /** + * Show the number using a locale-sensitive approximation pattern. Use the range pattern always, even if the + * inputs are the same. Example: "~$5" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + APPROXIMATELY, + + /** + * Show the number as the range of two equal values. Use the range pattern always, even if the inputs are the + * same. Example (with RangeCollapse.NONE): "$5 – $5" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + RANGE + } + + /** + * Defines the behavior when the two numbers in the range are identical after rounding. To programmatically detect + * when the identity fallback is used, compare the lower and upper BigDecimals via FormattedNumber. + */ + + public static enum RangeIdentityFallback { + /** + * Show the number as a single value rather than a range. Example: "$5" + */ + SINGLE_VALUE, + + /** + * Show the number using a locale-sensitive approximation pattern. If the numbers were the same before rounding, + * show the single value. Example: "~$5" or "$5" + */ + APPROXIMATELY_OR_SINGLE_VALUE, + + /** + * Show the number using a locale-sensitive approximation pattern. Use the range pattern always, even if the + * inputs are the same. Example: "~$5" + */ + APPROXIMATELY, + + /** + * Show the number as the range of two equal values. Use the range pattern always, even if the inputs are the + * same. Example (with RangeCollapse.NONE): "$5 – $5" + */ + RANGE + } private static final UnlocalizedNumberRangeFormatter BASE = new UnlocalizedNumberRangeFormatter(); 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 index 23de02a0716..7c9a70fb23b 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java @@ -38,19 +38,90 @@ public abstract class NumberRangeFormatterSettings + * The NumberFormatter instances must not have a locale applied yet; the locale specified on the + * NumberRangeFormatter will be used. + * + * @param formatter + * The formatter to use for both numbers in the range. + * @return The fluent chain. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberFormatter + * @see NumberRangeFormatter + */ public T numberFormatter(UnlocalizedNumberFormatter formatter) { return numberFormatters(formatter, formatter); } + /** + * Sets the NumberFormatter instances to use for the numbers in the range. This method allows you to set a different + * formatter for the first and second numbers. + *

+ * The NumberFormatter instances must not have a locale applied yet; the locale specified on the + * NumberRangeFormatter will be used. + * + * @param formatterFirst + * The formatter to use for the first number in the range. + * @param formatterSecond + * The formatter to use for the second number in the range. + * @return The fluent chain. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberFormatter + * @see NumberRangeFormatter + */ public T numberFormatters(UnlocalizedNumberFormatter formatterFirst, UnlocalizedNumberFormatter formatterSecond) { T intermediate = create(KEY_FORMATTER_1, formatterFirst); return (T) intermediate.create(KEY_FORMATTER_2, formatterSecond); } + /** + * Sets the aggressiveness of "collapsing" fields across the range separator. Possible values: + *

+ *

*

* The default value is APPROXIMATELY. * @@ -195,7 +197,7 @@ public abstract class NumberRangeFormatterSettings Date: Wed, 29 Aug 2018 18:02:16 -0700 Subject: [PATCH 09/37] ICU-11276 Fixing Javadoc warnings in NumberRangeFormatter. --- .../core/src/com/ibm/icu/number/NumberRangeFormatter.java | 2 -- .../com/ibm/icu/number/NumberRangeFormatterSettings.java | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) 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 index c1ad0aab600..fa0322bffed 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatter.java @@ -10,8 +10,6 @@ import com.ibm.icu.util.ULocale; * The main entrypoint to the formatting of ranges of numbers, including currencies and other units of measurement. *

* Usage example: - *

- * *

  * NumberRangeFormatter.with()
  *         .identityFallback(RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE)
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
index 683ef9ffcd7..dd33b1b08c5 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java
@@ -53,6 +53,7 @@ public abstract class NumberRangeFormatterSettings
      * The NumberFormatter instance must not have a locale applied yet; the locale specified on the
      * NumberRangeFormatter will be used.
@@ -96,7 +96,6 @@ public abstract class NumberRangeFormatterSettings
      *