--- /dev/null
+// © 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);
+ }
+}
--- /dev/null
+// © 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.
+ *
+ * <p>
+ * 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 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;
+ }
+
+ /**
+ * Determines the start and end indices of the next occurrence of the given <em>field</em> in the output string.
+ * This allows you to determine the locations of, for example, the integer part, fraction part, or symbols.
+ * <p>
+ * 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.
+ * <p>
+ * 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:
+ *
+ * <pre>
+ * FieldPosition fpos = new FieldPosition(NumberFormat.Field.GROUPING_SEPARATOR);
+ * while (formattedNumber.nextFieldPosition(fpos, status)) {
+ * // do something with fpos.
+ * }
+ * </pre>
+ * <p>
+ * 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 <em>fields</em>, such as the integer part, fraction part, and
+ * sign.
+ * <p>
+ * 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());
+ }
+}
--- /dev/null
+// © 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> {
+
+ 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);
+ }
+
+}
--- /dev/null
+// © 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 {}
+
+}
--- /dev/null
+// © 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<T extends 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());
+ }
+}
--- /dev/null
+// © 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<UnlocalizedNumberRangeFormatter> {
+
+ /** 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.
+ *
+ * <p>
+ * To use the Java default locale, call Locale.getDefault():
+ *
+ * <pre>
+ * NumberFormatter.with(). ... .locale(Locale.getDefault())
+ * </pre>
+ *
+ * @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);
+ }
+}