ICU-20568 Add .unit().usage() support to ICU4J NumberFormatter (1/2)
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java
index 7068e0b..695fc6a 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java
@@ -26,7 +26,7 @@
private static final int DNAM_INDEX = StandardPlural.COUNT;
private static final int PER_INDEX = StandardPlural.COUNT + 1;
- private static final int ARRAY_LENGTH = StandardPlural.COUNT + 2;
+ protected static final int ARRAY_LENGTH = StandardPlural.COUNT + 2;
private static int getIndex(String pluralKeyword) {
// pluralKeyword can also be "dnam" or "per"
@@ -39,7 +39,7 @@
}
}
- private static String getWithPlural(String[] strings, StandardPlural plural) {
+ protected static String getWithPlural(String[] strings, StandardPlural plural) {
String result = strings[plural.ordinal()];
if (result == null) {
result = strings[StandardPlural.OTHER.ordinal()];
@@ -79,7 +79,7 @@
// NOTE: outArray MUST have at least ARRAY_LENGTH entries. No bounds checking is performed.
- private static void getMeasureData(
+ protected static void getMeasureData(
ULocale locale,
MeasureUnit unit,
UnitWidth width,
@@ -101,7 +101,7 @@
// Map duration-year-person, duration-week-person, etc. to duration-year, duration-week, ...
// TODO(ICU-20400): Get duration-*-person data properly with aliases.
- if (unit.getSubtype().endsWith("-person")) {
+ if (unit.getSubtype() != null && unit.getSubtype().endsWith("-person")) {
key.append(unit.getSubtype(), 0, unit.getSubtype().length() - 7);
} else {
key.append(unit.getSubtype());
@@ -191,6 +191,22 @@
return result;
}
+ /**
+ * Construct a localized LongNameHandler for the specified MeasureUnit.
+ * <p>
+ * Compound units can be constructed via `unit` and `perUnit`. Both of these
+ * must then be built-in units.
+ * <p>
+ * Mixed units are not supported, use MixedUnitLongNameHandler.forMeasureUnit.
+ *
+ * @param locale The desired locale.
+ * @param unit The measure unit to construct a LongNameHandler for. If
+ * `perUnit` is also defined, `unit` must not be a mixed unit.
+ * @param perUnit If `unit` is a mixed unit, `perUnit` must be "none".
+ * @param width Specifies the desired unit rendering.
+ * @param rules Does not take ownership.
+ * @param parent Does not take ownership.
+ */
public static LongNameHandler forMeasureUnit(
ULocale locale,
MeasureUnit unit,
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameMultiplexer.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameMultiplexer.java
new file mode 100644
index 0000000..9f21d00
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameMultiplexer.java
@@ -0,0 +1,82 @@
+// © 2020 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+
+package com.ibm.icu.impl.number;
+
+import com.ibm.icu.number.NumberFormatter;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.NoUnit;
+import com.ibm.icu.util.ULocale;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class LongNameMultiplexer implements MicroPropsGenerator {
+ private final MicroPropsGenerator fParent;
+
+ private List<MicroPropsGenerator> fHandlers;
+
+ // Each MeasureUnit corresponds to the same-index MicroPropsGenerator
+ // pointed to in fHandlers.
+ private List<MeasureUnit> fMeasureUnits;
+
+ public LongNameMultiplexer(MicroPropsGenerator fParent) {
+ this.fParent = fParent;
+ }
+
+ // Produces a multiplexer for LongNameHandlers, one for each unit in
+ // `units`. An individual unit might be a mixed unit.
+ public static LongNameMultiplexer forMeasureUnits(ULocale locale,
+ List<MeasureUnit> units,
+ NumberFormatter.UnitWidth width,
+ PluralRules rules,
+ MicroPropsGenerator parent) {
+ LongNameMultiplexer result = new LongNameMultiplexer(parent);
+
+ assert (units.size() > 0);
+
+ result.fMeasureUnits = new ArrayList<>();
+ result.fHandlers = new ArrayList<>();
+
+
+ for (int i = 0; i < units.size(); i++) {
+ MeasureUnit unit = units.get(i);
+ result.fMeasureUnits.add(unit);
+ if (unit.getComplexity() == MeasureUnit.Complexity.MIXED) {
+ MixedUnitLongNameHandler mlnh = MixedUnitLongNameHandler
+ .forMeasureUnit(locale, unit, width, rules, null);
+ result.fHandlers.add(mlnh);
+ } else {
+ LongNameHandler lnh = LongNameHandler
+ .forMeasureUnit(locale, unit, NoUnit.BASE, width, rules, null );
+ result.fHandlers.add(lnh);
+ }
+ }
+
+ return result;
+ }
+
+ // The output unit must be provided via `micros.outputUnit`, it must match
+ // one of the units provided to the factory function.
+ @Override
+ public MicroProps processQuantity(DecimalQuantity quantity) {
+
+ // We call parent->processQuantity() from the Multiplexer, instead of
+ // letting LongNameHandler handle it: we don't know which LongNameHandler to
+ // call until we've called the parent!
+ MicroProps micros = this.fParent.processQuantity(quantity);
+
+ // Call the correct LongNameHandler based on outputUnit
+ for (int i = 0; i < this.fHandlers.size(); i++) {
+ if (fMeasureUnits.get(i).equals( micros.outputUnit)) {
+ return fHandlers.get(i).processQuantity(quantity);
+ }
+
+ }
+
+ throw new AssertionError
+ (" We shouldn't receive any outputUnit for which we haven't already got a LongNameHandler");
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MacroProps.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MacroProps.java
index aa8fa69..5f373b0 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MacroProps.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MacroProps.java
@@ -30,6 +30,7 @@
public SignDisplay sign;
public DecimalSeparatorDisplay decimal;
public Scale scale;
+ public String usage;
public AffixPatternProvider affixProvider; // not in API; for JDK compatibility mode only
public PluralRules rules; // not in API; could be made public in the future
public Long threshold; // not in API; controls internal self-regulation threshold
@@ -70,6 +71,8 @@
affixProvider = fallback.affixProvider;
if (scale == null)
scale = fallback.scale;
+ if (usage == null)
+ usage = fallback.usage;
if (rules == null)
rules = fallback.rules;
if (loc == null)
@@ -92,6 +95,7 @@
decimal,
affixProvider,
scale,
+ usage,
rules,
loc);
}
@@ -119,6 +123,7 @@
&& Objects.equals(decimal, other.decimal)
&& Objects.equals(affixProvider, other.affixProvider)
&& Objects.equals(scale, other.scale)
+ && Objects.equals(usage, other.usage)
&& Objects.equals(rules, other.rules)
&& Objects.equals(loc, other.loc);
}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java
index 81a1675..3179e6c 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java
@@ -7,7 +7,16 @@
import com.ibm.icu.number.NumberFormatter.SignDisplay;
import com.ibm.icu.number.Precision;
import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.Measure;
+import com.ibm.icu.util.MeasureUnit;
+import java.util.List;
+
+// TODO(units): generated by MicroPropsGenerator, but inherits from it too. Do we want to better document why?
+// There's an explanation for processQuantity:
+// - As MicroProps is the "base instance", this implementation of
+// - MicoPropsGenerator::processQuantity() just ensures that the output
+// - `micros` is correctly initialized.
public class MicroProps implements Cloneable, MicroPropsGenerator {
// Populated globally:
public SignDisplay sign;
@@ -17,16 +26,37 @@
public DecimalSeparatorDisplay decimal;
public IntegerWidth integerWidth;
- // Populated by notation/unit:
+ // Modifiers provided by the number formatting pipeline (when the value is known):
+
+ // A Modifier provided by LongNameHandler, used for currency long names and
+ // units. If there is no LongNameHandler needed, this should be an
+ // null. (This is typically the third modifier applied.)
public Modifier modOuter;
+
+ // A Modifier for short currencies and compact notation. (This is typically
+ // the second modifier applied.)
public Modifier modMiddle;
+
+ // A Modifier provided by ScientificHandler, used for scientific notation.
+ // This is typically the first modifier applied.
public Modifier modInner;
+
public Precision rounder;
public Grouper grouping;
public boolean useCurrency;
// Internal fields:
private final boolean immutable;
+
+ // The MeasureUnit with which the output is represented. May also have
+ // MeasureUnit.Complexity.MIXED complexity, in which case mixedMeasures comes into
+ // play.
+ public MeasureUnit outputUnit;
+
+ // In the case of mixed units, this is the set of integer-only units
+ // *preceding* the final unit.
+ public List<Measure> mixedMeasures ;
+
private volatile boolean exhausted;
/**
@@ -38,17 +68,28 @@
this.immutable = immutable;
}
+ /**
+ * As MicroProps is the "base instance", this implementation of
+ * MircoPropsGenerator.processQuantity() just ensures that the output
+ * `micros` is correctly initialized.
+ * <p>
+ * For the "safe" invocation of this function, micros must not be *this,
+ * such that a copy of the base instance is made. For the "unsafe" path,
+ * this function can be used only once, because the base MicroProps instance
+ * will be modified and thus not be available for re-use.
+ *
+ * @param quantity The quantity for consideration and optional mutation.
+ * @return a MicroProps instance to populate.
+ */
@Override
public MicroProps processQuantity(DecimalQuantity quantity) {
if (immutable) {
return (MicroProps) this.clone();
- } else if (exhausted) {
- // Safety check
- throw new AssertionError("Cannot re-use a mutable MicroProps in the quantity chain");
- } else {
- exhausted = true;
- return this;
}
+
+ assert !exhausted : "Cannot re-use a mutable MicroProps in the quantity chain";
+ exhausted = true;
+ return this;
}
@Override
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroPropsGenerator.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroPropsGenerator.java
index c975047..1ed6119 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroPropsGenerator.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroPropsGenerator.java
@@ -19,6 +19,9 @@
* {@link MicroProps} with properties that are not quantity-dependent. Each element in the linked list
* calls {@link #processQuantity} on its "parent", then does its work, and then returns the result.
*
+ * This chain of MicroPropsGenerators is typically constructed by NumberFormatterImpl::macrosToMicroGenerator() when
+ * constructing a NumberFormatter.
+ *
* <p>
* A class implementing MicroPropsGenerator looks something like this:
*
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MixedUnitLongNameHandler.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MixedUnitLongNameHandler.java
new file mode 100644
index 0000000..7ad64bb
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MixedUnitLongNameHandler.java
@@ -0,0 +1,182 @@
+// © 2020 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+
+package com.ibm.icu.impl.number;
+
+import com.ibm.icu.impl.FormattedStringBuilder;
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.number.LocalizedNumberFormatter;
+import com.ibm.icu.number.NumberFormatter;
+import com.ibm.icu.text.ListFormatter;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.text.SimpleFormatter;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MixedUnitLongNameHandler implements MicroPropsGenerator, ModifierStore {
+ // Not owned
+ private final PluralRules rules;
+ // Not owned
+ private final MicroPropsGenerator parent;
+
+ // If this LongNameHandler is for a mixed unit, this stores unit data for
+ // each of the individual units. For each unit, it stores ARRAY_LENGTH
+ // strings, as returned by getMeasureData.
+ private List<String[]> fMixedUnitData;
+
+ // A localized NumberFormatter used to format the integer-valued bigger
+ // units of Mixed Unit measurements.
+ private LocalizedNumberFormatter fIntegerFormatter;
+
+ // A localised list formatter for joining mixed units together.
+ private ListFormatter fListFormatter;
+
+ private MixedUnitLongNameHandler(PluralRules rules, MicroPropsGenerator parent) {
+ this.rules = rules;
+ this.parent = parent;
+ }
+
+ /**
+ * Construct a localized MixedUnitLongNameHandler for the specified
+ * MeasureUnit. It must be a MIXED unit.
+ * <p>
+ *
+ * @param locale The desired locale.
+ * @param mixedUnit The mixed measure unit to construct a
+ * MixedUnitLongNameHandler for.
+ * @param width Specifies the desired unit rendering.
+ * @param rules Does not take ownership.
+ * @param parent Does not take ownership.
+ */
+ public static MixedUnitLongNameHandler forMeasureUnit(ULocale locale, MeasureUnit mixedUnit,
+ NumberFormatter.UnitWidth width, PluralRules rules,
+ MicroPropsGenerator parent) {
+ assert (mixedUnit.getComplexity() == MeasureUnit.Complexity.MIXED);
+
+ MixedUnitLongNameHandler result = new MixedUnitLongNameHandler(rules, parent);
+ List<MeasureUnit> individualUnits = mixedUnit.splitToSingleUnits();
+
+ result.fMixedUnitData = new ArrayList<>();
+ for (int i = 0; i < individualUnits.size(); i++) {
+ // Grab data for each of the components.
+ String[] unitData = new String[LongNameHandler.ARRAY_LENGTH];
+ LongNameHandler.getMeasureData(locale, individualUnits.get(i), width, unitData);
+ result.fMixedUnitData.add(unitData);
+ }
+
+ ListFormatter.Width listWidth = ListFormatter.Width.SHORT;
+ if (width == NumberFormatter.UnitWidth.NARROW) {
+ listWidth = ListFormatter.Width.NARROW;
+ } else if (width == NumberFormatter.UnitWidth.FULL_NAME) {
+ // This might be the same as SHORT in most languages:
+ listWidth = ListFormatter.Width.WIDE;
+ }
+
+ result.fListFormatter = ListFormatter.getInstance(locale, ListFormatter.Type.UNITS, listWidth);
+
+
+ // We need a localised NumberFormatter for the integers of the bigger units
+ // (providing Arabic numerals, for example).
+ result.fIntegerFormatter = NumberFormatter.withLocale(locale);
+
+ return result;
+ }
+
+ /**
+ * Produces a plural-appropriate Modifier for a mixed unit: `quantity` is
+ * taken as the final smallest unit, while the larger unit values must be
+ * provided via `micros.mixedMeasures`.
+ */
+ @Override
+ public MicroProps processQuantity(DecimalQuantity quantity) {
+ assert (fMixedUnitData.size() > 1);
+ MicroProps micros;
+ // if (parent != null)
+ micros = parent.processQuantity(quantity);
+ micros.modOuter = getMixedUnitModifier(quantity, micros);
+ return micros;
+ }
+
+ // Required for ModifierStore. And ModifierStore is required by
+ // SimpleModifier constructor's last parameter. We assert his will never get
+ // called though.
+ @Override
+ public Modifier getModifier(Modifier.Signum signum, StandardPlural plural) {
+ // TODO(units): investigate this method while investigating where
+ // LongNameHandler.getModifier() gets used. To be sure it remains
+ // unreachable:
+
+ return null;
+ }
+
+ // For a mixed unit, returns a Modifier that takes only one parameter: the
+ // smallest and final unit of the set. The bigger units' values and labels
+ // get baked into this Modifier, together with the unit label of the final
+ // unit.
+ private Modifier getMixedUnitModifier(DecimalQuantity quantity, MicroProps micros) {
+ // TODO(icu-units#21): mixed units without usage() is not yet supported.
+ // That should be the only reason why this happens, so delete this whole if
+ // once fixed:
+ if (micros.mixedMeasures.size() == 0) {
+ throw new UnsupportedOperationException();
+ }
+
+
+ // Algorithm:
+ //
+ // For the mixed-units measurement of: "3 yard, 1 foot, 2.6 inch", we should
+ // find "3 yard" and "1 foot" in micros.mixedMeasures.
+ //
+ // Obtain long-names with plural forms corresponding to measure values:
+ // * {0} yards, {0} foot, {0} inches
+ //
+ // Format the integer values appropriately and modify with the format
+ // strings:
+ // - 3 yards, 1 foot
+ //
+ // Use ListFormatter to combine, with one placeholder:
+ // - 3 yards, 1 foot and {0} inches /* TODO: how about the case of `1 inch` */
+ //
+ // Return a SimpleModifier for this pattern, letting the rest of the
+ // pipeline take care of the remaining inches.
+
+ List<String> outputMeasuresList = new ArrayList<>();
+
+ for (int i = 0; i < micros.mixedMeasures.size(); i++) {
+ DecimalQuantity fdec = new DecimalQuantity_DualStorageBCD(micros.mixedMeasures.get(i).getNumber());
+ StandardPlural pluralForm = fdec.getStandardPlural(rules);
+
+ String simpleFormat = LongNameHandler.getWithPlural(this.fMixedUnitData.get(i), pluralForm);
+ SimpleFormatter compiledFormatter = SimpleFormatter.compileMinMaxArguments(simpleFormat, 0, 1);
+
+
+ FormattedStringBuilder appendable = new FormattedStringBuilder();
+ this.fIntegerFormatter.formatImpl(fdec, appendable);
+ outputMeasuresList.add(compiledFormatter.format(appendable.toString()));
+ // TODO: fix this issue https://github.com/icu-units/icu/issues/67
+ }
+
+ String[] finalSimpleFormats = this.fMixedUnitData.get(this.fMixedUnitData.size() - 1);
+ StandardPlural finalPlural = RoundingUtils.getPluralSafe(micros.rounder, rules, quantity);
+ String finalSimpleFormat = LongNameHandler.getWithPlural(finalSimpleFormats, finalPlural);
+ SimpleFormatter finalFormatter = SimpleFormatter.compileMinMaxArguments(finalSimpleFormat, 0, 1);
+ finalFormatter.format("{0}", outputMeasuresList.get(outputMeasuresList.size() -1));
+
+ // Combine list into a "premixed" pattern
+ String premixedFormatPattern = this.fListFormatter.format(outputMeasuresList);
+ SimpleFormatter premixedCompiled = SimpleFormatter.compileMinMaxArguments(premixedFormatPattern, 0, 1);
+
+ // Return a SimpleModifier for the "premixed" pattern
+ Modifier.Parameters params = new Modifier.Parameters();
+ params.obj = this;
+ params.signum = Modifier.Signum.POS_ZERO;
+ params.plural = finalPlural;
+
+ return new SimpleModifier(premixedCompiled.getTextWithNoArguments(), null, false, params);
+ /*TODO: it was SimpleModifier(premixedCompiled, kUndefinedField, false, {this, SIGNUM_POS_ZERO, finalPlural});*/
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UnitConversionHandler.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UnitConversionHandler.java
new file mode 100644
index 0000000..cf3fd23
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UnitConversionHandler.java
@@ -0,0 +1,62 @@
+// © 2020 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.impl.number;
+
+import com.ibm.icu.impl.units.ComplexUnitsConverter;
+import com.ibm.icu.impl.units.MeasureUnitImpl;
+import com.ibm.icu.impl.units.UnitsData;
+import com.ibm.icu.util.Measure;
+import com.ibm.icu.util.MeasureUnit;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A MicroPropsGenerator which converts a measurement from a simple MeasureUnit
+ * to a Mixed MeasureUnit.
+ */
+public class UnitConversionHandler implements MicroPropsGenerator {
+
+ private final MicroPropsGenerator fParent;
+ private MeasureUnit fOutputUnit;
+ private ComplexUnitsConverter fComplexUnitConverter;
+
+ public UnitConversionHandler(MeasureUnit outputUnit, MicroPropsGenerator parent) {
+ this.fOutputUnit = outputUnit;
+ this.fParent = parent;
+
+ List<MeasureUnit> singleUnits = outputUnit.splitToSingleUnits();
+
+ assert outputUnit.getComplexity() == MeasureUnit.Complexity.MIXED;
+ assert singleUnits.size() > 1;
+
+ MeasureUnitImpl outputUnitImpl = MeasureUnitImpl.forIdentifier(outputUnit.getIdentifier());
+ // TODO(icu-units#97): The input unit should be the largest unit, not the first unit, in the identifier.
+ this.fComplexUnitConverter =
+ new ComplexUnitsConverter(
+ new MeasureUnitImpl(outputUnitImpl.getSingleUnits().get(0)),
+ outputUnitImpl,
+ new UnitsData().getConversionRates());
+ }
+
+ /**
+ * Obtains the appropriate output values from the Unit Converter.
+ */
+ @Override
+ public MicroProps processQuantity(DecimalQuantity quantity) {
+ /*TODO: Questions : shall we check the parent if it is equals null */
+ MicroProps result = this.fParent == null?
+ this.fParent.processQuantity(quantity):
+ new MicroProps(false);
+
+ quantity.roundToInfinity(); // Enables toDouble
+ List<Measure> measures = this.fComplexUnitConverter.convert(quantity.toBigDecimal());
+
+ result.outputUnit = this.fOutputUnit;
+ result.mixedMeasures = new ArrayList<>();
+ UsagePrefsHandler.mixedMeasuresToMicros(measures, quantity, result);
+
+ return result;
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UsagePrefsHandler.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UsagePrefsHandler.java
new file mode 100644
index 0000000..ee012d2
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UsagePrefsHandler.java
@@ -0,0 +1,118 @@
+// © 2020 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.impl.number;
+
+import com.ibm.icu.impl.IllegalIcuArgumentException;
+import com.ibm.icu.impl.units.MeasureUnitImpl;
+import com.ibm.icu.impl.units.UnitsRouter;
+import com.ibm.icu.number.Precision;
+import com.ibm.icu.util.Measure;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.util.ArrayList;
+import java.util.List;
+
+public class UsagePrefsHandler implements MicroPropsGenerator {
+
+ private final MicroPropsGenerator fParent;
+ private UnitsRouter fUnitsRouter;
+
+ public UsagePrefsHandler(ULocale locale, MeasureUnit inputUnit, String usage, MicroPropsGenerator parent) {
+ assert parent != null;
+
+ this.fParent = parent;
+ this.fUnitsRouter =
+ new UnitsRouter(MeasureUnitImpl.forIdentifier(inputUnit.getIdentifier()), locale.getCountry(), usage);
+ }
+
+ private static Precision parseSkeletonToPrecision(String precisionSkeleton) {
+ final String kSuffixPrefix = "precision-increment/";
+ if (!precisionSkeleton.startsWith(kSuffixPrefix)) {
+ throw new IllegalIcuArgumentException("precisionSkeleton is only precision-increment");
+ }
+
+ String skeleton = precisionSkeleton.substring(kSuffixPrefix.length());
+ String skeletons[] = skeleton.split("/");
+ BigDecimal num = new BigDecimal(skeletons[0]);
+ BigDecimal den =
+ skeletons.length == 2 ?
+ new BigDecimal(skeletons[1]) :
+ new BigDecimal("1");
+
+
+ return Precision.increment(num.divide(den, MathContext.DECIMAL128));
+ }
+
+ protected static void mixedMeasuresToMicros(List<Measure> measures, DecimalQuantity quantity, MicroProps micros) {
+ if (measures.size() > 1) {
+ // For debugging
+ assert (micros.outputUnit.getComplexity() == MeasureUnit.Complexity.MIXED);
+
+ // Check that we received measurements with the expected MeasureUnits:
+ List<MeasureUnit> singleUnits = micros.outputUnit.splitToSingleUnits();
+
+ assert measures.size() == singleUnits.size();
+
+ // Mixed units: except for the last value, we pass all values to the
+ // LongNameHandler via micros->mixedMeasures.
+ for (int i = 0, n = measures.size() - 1; i < n; i++) {
+ micros.mixedMeasures.add(measures.get(i));
+ }
+ }
+
+ // The last value (potentially the only value) gets passed on via quantity.
+ quantity.setToBigDecimal((BigDecimal) measures.get(measures.size()- 1).getNumber());
+ }
+
+ /**
+ * Returns the list of possible output units, i.e. the full set of
+ * preferences, for the localized, usage-specific unit preferences.
+ * <p>
+ * The returned pointer should be valid for the lifetime of the
+ * UsagePrefsHandler instance.
+ */
+ public List<MeasureUnit> getOutputUnits() {
+ return fUnitsRouter.getOutputUnits();
+ }
+
+ /**
+ * Obtains the appropriate output value, MeasureUnit and
+ * rounding/precision behaviour from the UnitsRouter.
+ * <p>
+ * The output unit is passed on to the LongNameHandler via
+ * micros.outputUnit.
+ */
+ @Override
+ public MicroProps processQuantity(DecimalQuantity quantity) {
+ MicroProps micros = this.fParent.processQuantity(quantity);
+
+ quantity.roundToInfinity(); // Enables toDouble
+ final UnitsRouter.RouteResult routed = fUnitsRouter.route(quantity.toBigDecimal());
+
+ final List<Measure> routedMeasures = routed.measures;
+ micros.outputUnit = routed.outputUnit.build();
+ micros.mixedMeasures = new ArrayList<>();
+
+ UsagePrefsHandler.mixedMeasuresToMicros(routedMeasures, quantity, micros);
+
+ String precisionSkeleton = routed.precision;
+
+ assert micros.rounder != null;
+
+ // TODO: use the user precision if the user already set precision.
+ if (precisionSkeleton != null && precisionSkeleton.length() > 0) {
+ micros.rounder = parseSkeletonToPrecision(precisionSkeleton);
+ } else {
+ // We use the same rounding mode as COMPACT notation: known to be a
+ // human-friendly rounding mode: integers, but add a decimal digit
+ // as needed to ensure we have at least 2 significant digits.
+ micros.rounder = Precision.integer().withMinDigits(2);
+ }
+
+ return micros;
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java
index 19acafc..4f8fbb6 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java
@@ -83,13 +83,21 @@
for (ConverterPreference converterPreference :
converterPreferences_) {
if (converterPreference.converter.greaterThanOrEqual(quantity, converterPreference.limit)) {
- return new RouteResult(converterPreference.converter.convert(quantity), converterPreference.precision);
+ return new RouteResult(
+ converterPreference.converter.convert(quantity),
+ converterPreference.precision,
+ converterPreference.targetUnit
+ );
}
}
// In case of the `quantity` does not fit in any converter limit, use the last converter.
ConverterPreference lastConverterPreference = converterPreferences_.get(converterPreferences_.size() - 1);
- return new RouteResult(lastConverterPreference.converter.convert(quantity), lastConverterPreference.precision);
+ return new RouteResult(
+ lastConverterPreference.converter.convert(quantity),
+ lastConverterPreference.precision,
+ lastConverterPreference.targetUnit
+ );
}
/**
@@ -99,7 +107,7 @@
* The returned pointer should be valid for the lifetime of the
* UnitsRouter instance.
*/
- public ArrayList<MeasureUnit> getOutputUnits() {
+ public List<MeasureUnit> getOutputUnits() {
return this.outputUnits_;
}
@@ -113,32 +121,52 @@
* is no limit for the converter.
*/
public static class ConverterPreference {
- ComplexUnitsConverter converter;
- BigDecimal limit;
- String precision;
+ // The output unit for this ConverterPreference. This may be a MIXED unit -
+ // for example: "yard-and-foot-and-inch".
+ final MeasureUnitImpl targetUnit;
+ final ComplexUnitsConverter converter;
+ final BigDecimal limit;
+ final String precision;
// In case there is no limit, the limit will be -inf.
- public ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl outputUnits,
+ public ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl targetUnit,
String precision, ConversionRates conversionRates) {
- this(source, outputUnits, BigDecimal.valueOf(Double.MIN_VALUE), precision,
+ this(source, targetUnit, BigDecimal.valueOf(Double.MIN_VALUE), precision,
conversionRates);
}
- public ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl outputUnits,
+ public ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl targetUnit,
BigDecimal limit, String precision, ConversionRates conversionRates) {
- this.converter = new ComplexUnitsConverter(source, outputUnits, conversionRates);
+ this.converter = new ComplexUnitsConverter(source, targetUnit, conversionRates);
this.limit = limit;
this.precision = precision;
+ this.targetUnit = targetUnit;
+
}
}
public class RouteResult {
- public List<Measure> measures;
- public String precision;
+ // A list of measures: a single measure for single units, multiple measures
+ // for mixed units.
+ //
+ // TODO(icu-units/icu#21): figure out the right mixed unit API.
+ public final List<Measure> measures;
- RouteResult(List<Measure> measures, String precision) {
+ // A skeleton string starting with a precision-increment.
+ //
+ // TODO(hugovdm): generalise? or narrow down to only a precision-increment?
+ // or document that other skeleton elements are ignored?
+ public final String precision;
+
+ // The output unit for this RouteResult. This may be a MIXED unit - for
+ // example: "yard-and-foot-and-inch", for which `measures` will have three
+ // elements.
+ public final MeasureUnitImpl outputUnit;
+
+ RouteResult(List<Measure> measures, String precision, MeasureUnitImpl outputUnit) {
this.measures = measures;
this.precision = precision;
+ this.outputUnit = outputUnit;
}
}
}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumber.java b/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumber.java
index e4fe056..4eb26ec 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumber.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumber.java
@@ -12,6 +12,7 @@
import com.ibm.icu.text.ConstrainedFieldPosition;
import com.ibm.icu.text.FormattedValue;
import com.ibm.icu.text.PluralRules.IFixedDecimal;
+import com.ibm.icu.util.MeasureUnit;
/**
* The result of a number formatting operation. This class allows the result to be exported in several
@@ -25,10 +26,12 @@
public class FormattedNumber implements FormattedValue {
final FormattedStringBuilder string;
final DecimalQuantity fq;
+ final MeasureUnit outputUnit;
- FormattedNumber(FormattedStringBuilder nsb, DecimalQuantity fq) {
+ FormattedNumber(FormattedStringBuilder nsb, DecimalQuantity fq, MeasureUnit outputUnit) {
this.string = nsb;
this.fq = fq;
+ this.outputUnit = outputUnit;
}
/**
@@ -115,6 +118,21 @@
}
/**
+ * Gets the resolved output unit.
+ * <p>
+ * The output unit is dependent upon the localized preferences for the usage
+ * specified via NumberFormatterSettings.usage(), and may be a unit with
+ * MeasureUnit.Complexity.MIXED unit complexity (MeasureUnit.getComplexity()), such
+ * as "foot-and-inch" or "hour-and-minute-and-second".
+ *
+ * @return `MeasureUnit`.
+ * @draft ICU 68
+ */
+ public MeasureUnit getOutputUnit() {
+ return this.outputUnit;
+ }
+
+ /**
* @internal
* @deprecated This API is ICU internal only.
*/
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java
index de0d35a..e6f714d 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java
@@ -13,6 +13,7 @@
import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
import com.ibm.icu.impl.number.LocalizedNumberFormatterAsFormat;
import com.ibm.icu.impl.number.MacroProps;
+import com.ibm.icu.impl.number.MicroProps;
import com.ibm.icu.math.BigDecimal;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.Measure;
@@ -100,8 +101,8 @@
DecimalQuantity fq = new DecimalQuantity_DualStorageBCD(input.getNumber());
MeasureUnit unit = input.getUnit();
FormattedStringBuilder string = new FormattedStringBuilder();
- formatImpl(fq, unit, string);
- return new FormattedNumber(string, fq);
+ MicroProps micros = formatImpl(fq, unit, string);
+ return new FormattedNumber(string, fq, micros.outputUnit);
}
/**
@@ -120,11 +121,13 @@
return new LocalizedNumberFormatterAsFormat(this, resolve().loc);
}
- /** Helper method that creates a FormattedStringBuilder and formats. */
+ /**
+ * Helper method that creates a FormattedStringBuilder and formats.
+ */
private FormattedNumber format(DecimalQuantity fq) {
FormattedStringBuilder string = new FormattedStringBuilder();
- formatImpl(fq, string);
- return new FormattedNumber(string, fq);
+ MicroProps micros = formatImpl(fq, string);
+ return new FormattedNumber(string, fq, micros.outputUnit);
}
/**
@@ -144,12 +147,11 @@
* @deprecated ICU 60 This API is ICU internal only.
*/
@Deprecated
- public void formatImpl(DecimalQuantity fq, FormattedStringBuilder string) {
+ public MicroProps formatImpl(DecimalQuantity fq, FormattedStringBuilder string) {
if (computeCompiled()) {
- compiled.format(fq, string);
- } else {
- NumberFormatterImpl.formatStatic(resolve(), fq, string);
+ return compiled.format(fq, string);
}
+ return NumberFormatterImpl.formatStatic(resolve(), fq, string);
}
/**
@@ -159,11 +161,11 @@
* @deprecated ICU 67 This API is ICU internal only.
*/
@Deprecated
- public void formatImpl(DecimalQuantity fq, MeasureUnit unit, FormattedStringBuilder string) {
+ public MicroProps formatImpl(DecimalQuantity fq, MeasureUnit unit, FormattedStringBuilder string) {
// Use this formatter if possible
if (Objects.equals(resolve().unit, unit)) {
- formatImpl(fq, string);
- return;
+ return formatImpl(fq, string);
+
}
// 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.
@@ -172,7 +174,7 @@
withUnit = new LocalizedNumberFormatter(this, KEY_UNIT, unit);
savedWithUnit = withUnit;
}
- withUnit.formatImpl(fq, string);
+ return withUnit.formatImpl(fq, string);
}
/**
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java
index abb93bb..1b91cbb 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java
@@ -3,6 +3,7 @@
package com.ibm.icu.number;
import com.ibm.icu.impl.FormattedStringBuilder;
+import com.ibm.icu.impl.IllegalIcuArgumentException;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.CompactData.CompactType;
import com.ibm.icu.impl.number.ConstantAffixModifier;
@@ -10,9 +11,11 @@
import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
import com.ibm.icu.impl.number.Grouper;
import com.ibm.icu.impl.number.LongNameHandler;
+import com.ibm.icu.impl.number.LongNameMultiplexer;
import com.ibm.icu.impl.number.MacroProps;
import com.ibm.icu.impl.number.MicroProps;
import com.ibm.icu.impl.number.MicroPropsGenerator;
+import com.ibm.icu.impl.number.MixedUnitLongNameHandler;
import com.ibm.icu.impl.number.MultiplierFormatHandler;
import com.ibm.icu.impl.number.MutablePatternModifier;
import com.ibm.icu.impl.number.MutablePatternModifier.ImmutablePatternModifier;
@@ -20,6 +23,8 @@
import com.ibm.icu.impl.number.PatternStringParser;
import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo;
import com.ibm.icu.impl.number.RoundingUtils;
+import com.ibm.icu.impl.number.UnitConversionHandler;
+import com.ibm.icu.impl.number.UsagePrefsHandler;
import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
import com.ibm.icu.number.NumberFormatter.SignDisplay;
@@ -41,7 +46,9 @@
*/
class NumberFormatterImpl {
- /** Builds a "safe" MicroPropsGenerator, which is thread-safe and can be used repeatedly. */
+ /**
+ * Builds a "safe" MicroPropsGenerator, which is thread-safe and can be used repeatedly.
+ */
public NumberFormatterImpl(MacroProps macros) {
micros = new MicroProps(true);
microPropsGenerator = macrosToMicroGenerator(macros, micros, true);
@@ -50,14 +57,14 @@
/**
* Builds and evaluates an "unsafe" MicroPropsGenerator, which is cheaper but can be used only once.
*/
- public static int formatStatic(
+ public static MicroProps formatStatic(
MacroProps macros,
DecimalQuantity inValue,
FormattedStringBuilder outString) {
MicroProps micros = preProcessUnsafe(macros, inValue);
int length = writeNumber(micros, inValue, outString, 0);
- length += writeAffixes(micros, outString, 0, length);
- return length;
+ writeAffixes(micros, outString, 0, length);
+ return micros;
}
/**
@@ -84,11 +91,11 @@
/**
* Evaluates the "safe" MicroPropsGenerator created by "fromMacros".
*/
- public int format(DecimalQuantity inValue, FormattedStringBuilder outString) {
+ public MicroProps format(DecimalQuantity inValue, FormattedStringBuilder outString) {
MicroProps micros = preProcess(inValue);
int length = writeNumber(micros, inValue, outString, 0);
- length += writeAffixes(micros, outString, 0, length);
- return length;
+ writeAffixes(micros, outString, 0, length);
+ return micros;
}
/**
@@ -203,6 +210,10 @@
|| !(isPercent || isPermille)
|| isCompactNotation
);
+
+ // TODO(icu-units#95): Add the logic in this file that sets the rounder to bogus/pass-through if isMixedUnit is true.
+ boolean isMixedUnit = isCldrUnit && macros.unit.getType() == null &&
+ macros.unit.getComplexity() == MeasureUnit.Complexity.MIXED;
PluralRules rules = macros.rules;
// Select the numbering system.
@@ -255,6 +266,18 @@
/// START POPULATING THE DEFAULT MICROPROPS AND BUILDING THE MICROPROPS GENERATOR ///
/////////////////////////////////////////////////////////////////////////////////////
+ // Unit Preferences and Conversions as our first step
+ UsagePrefsHandler usagePrefsHandler = null;
+ if (macros.usage != null) {
+ if (!isCldrUnit) {
+ throw new IllegalIcuArgumentException(
+ "We only support \"usage\" when the input unit is specified, and is a CLDR Unit.");
+ }
+ chain = usagePrefsHandler = new UsagePrefsHandler(macros.loc, macros.unit, macros.usage, chain);
+ } else if (isMixedUnit) {
+ chain = new UnitConversionHandler(macros.unit, chain);
+ }
+
// Multiplier
if (macros.scale != null) {
chain = new MultiplierFormatHandler(macros.scale, chain);
@@ -353,8 +376,33 @@
// Lazily create PluralRules
rules = PluralRules.forLocale(macros.loc);
}
- chain = LongNameHandler
- .forMeasureUnit(macros.loc, macros.unit, macros.perUnit, unitWidth, rules, chain);
+ PluralRules pluralRules = macros.rules != null ?
+ macros.rules :
+ PluralRules.forLocale(macros.loc);
+
+ if (macros.usage != null) {
+ assert usagePrefsHandler != null;
+ chain = LongNameMultiplexer.forMeasureUnits(
+ macros.loc,
+ usagePrefsHandler.getOutputUnits(),
+ unitWidth,
+ pluralRules,
+ chain);
+ } else if (isMixedUnit) {
+ chain = MixedUnitLongNameHandler.forMeasureUnit(
+ macros.loc,
+ macros.unit,
+ unitWidth,
+ pluralRules,
+ chain);
+ } else {
+ chain = LongNameHandler.forMeasureUnit(macros.loc,
+ macros.unit,
+ macros.perUnit,
+ unitWidth,
+ pluralRules,
+ chain);
+ }
} else if (isCurrency && unitWidth == UnitWidth.FULL_NAME) {
if (rules == null) {
// Lazily create PluralRules
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java
index 190885e..fb1072b 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java
@@ -45,6 +45,7 @@
static final int KEY_THRESHOLD = 14;
static final int KEY_PER_UNIT = 15;
static final int KEY_MAX = 16;
+ static final int KEY_USAGE = 17;
private final NumberFormatterSettings<?> parent;
private final int key;
@@ -133,6 +134,11 @@
* <p>
* The default is to render without units (equivalent to {@link NoUnit#BASE}).
*
+ * <P>
+ * If the input usage is correctly set the output unit <b>will change</b>
+ * according to `usage`, `locale` and `unit` value.
+ * </p>
+ *
* @param unit
* The unit to render.
* @return The fluent chain.
@@ -486,6 +492,50 @@
return create(KEY_SCALE, scale);
}
+ /**
+ * Specifies the usage for which numbers will be formatted ("person-height",
+ * "road", "rainfall", etc.)
+ *
+ * When a `usage` is specified, the output unit will change depending on the
+ * `Locale` and the unit quantity. For example, formatting length
+ * measurements specified in meters:
+ *
+ * `NumberFormatter.with().usage("person").unit(MeasureUnit.METER).locale(new ULocale("en-US"))`
+ * * When formatting 0.25, the output will be "10 inches".
+ * * When formatting 1.50, the output will be "4 feet and 11 inches".
+ *
+ * The input unit specified via unit() determines the type of measurement
+ * being formatted (e.g. "length" when the unit is "foot"). The usage
+ * requested will be looked for only within this category of measurement
+ * units.
+ *
+ * The output unit can be found via FormattedNumber.getOutputUnit().
+ *
+ * If the usage has multiple parts (e.g. "land-agriculture-grain") and does
+ * not match a known usage preference, the last part will be dropped
+ * repeatedly until a match is found (e.g. trying "land-agriculture", then
+ * "land"). If a match is still not found, usage will fall back to
+ * "default".
+ *
+ * Setting usage to an empty string clears the usage (disables usage-based
+ * localized formatting).
+ *
+ *
+ * When using usage, specifying rounding or precision is unnecessary.
+ * Specifying a precision in some manner will override the default
+ * formatting.
+ *
+ *
+ * @param usage A usage parameter from the units resource.
+ * @return The fluent chain
+ * @throws IllegalArgumentException in case of Setting a usage string but not a correct input unit.
+ * @draft ICU 67
+ * @provisional This API might change or be removed in a future release.
+ */
+ public T usage(String usage) {
+ return create(KEY_USAGE, usage);
+ }
+
/**
* Internal method to set a starting macros.
*
@@ -632,6 +682,11 @@
macros.perUnit = (MeasureUnit) current.value;
}
break;
+ case KEY_USAGE:
+ if(macros.usage == null) {
+ macros.usage = (String) current.value;
+ }
+ break;
default:
throw new AssertionError("Unknown key: " + current.key);
}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
index 76a18f8..02565f0 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
@@ -33,10 +33,12 @@
*/
class NumberSkeletonImpl {
- ///////////////////////////////////////////////////////////////////////////////////////
- // NOTE: For an example of how to add a new stem to the number skeleton parser, see: //
- // http://bugs.icu-project.org/trac/changeset/41193 //
- ///////////////////////////////////////////////////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////////////////
+ // NOTE: For examples of how to add a new stem to the number skeleton parser, see: //
+ // https://github.com/unicode-org/icu/commit/a2a7982216b2348070dc71093775ac7195793d73 //
+ // and //
+ // https://github.com/unicode-org/icu/commit/6fe86f3934a8a5701034f648a8f7c5087e84aa28 //
+ //////////////////////////////////////////////////////////////////////////////////////////
/**
* While parsing a skeleton, this enum records what type of option we expect to find next.
@@ -54,6 +56,7 @@
STATE_MEASURE_UNIT,
STATE_PER_MEASURE_UNIT,
STATE_IDENTIFIER_UNIT,
+ STATE_UNIT_USAGE,
STATE_CURRENCY_UNIT,
STATE_INTEGER_WIDTH,
STATE_NUMBERING_SYSTEM,
@@ -118,6 +121,7 @@
STEM_MEASURE_UNIT,
STEM_PER_MEASURE_UNIT,
STEM_UNIT,
+ STEM_UNIT_USAGE,
STEM_CURRENCY,
STEM_INTEGER_WIDTH,
STEM_NUMBERING_SYSTEM,
@@ -193,6 +197,7 @@
b.add("measure-unit", StemEnum.STEM_MEASURE_UNIT.ordinal());
b.add("per-measure-unit", StemEnum.STEM_PER_MEASURE_UNIT.ordinal());
b.add("unit", StemEnum.STEM_UNIT.ordinal());
+ b.add("usage", StemEnum.STEM_UNIT_USAGE.ordinal());
b.add("currency", StemEnum.STEM_CURRENCY.ordinal());
b.add("integer-width", StemEnum.STEM_INTEGER_WIDTH.ordinal());
b.add("numbering-system", StemEnum.STEM_NUMBERING_SYSTEM.ordinal());
@@ -613,6 +618,7 @@
case STATE_INCREMENT_PRECISION:
case STATE_MEASURE_UNIT:
case STATE_PER_MEASURE_UNIT:
+ case STATE_UNIT_USAGE:
case STATE_CURRENCY_UNIT:
case STATE_INTEGER_WIDTH:
case STATE_NUMBERING_SYSTEM:
@@ -786,6 +792,10 @@
checkNull(macros.perUnit, segment);
return ParseState.STATE_IDENTIFIER_UNIT;
+ case STEM_UNIT_USAGE:
+ checkNull(macros.usage, segment);
+ return ParseState.STATE_UNIT_USAGE;
+
case STEM_CURRENCY:
checkNull(macros.unit, segment);
return ParseState.STATE_CURRENCY_UNIT;
@@ -830,6 +840,9 @@
case STATE_IDENTIFIER_UNIT:
BlueprintHelpers.parseIdentifierUnitOption(segment, macros);
return ParseState.STATE_NULL;
+ case STATE_UNIT_USAGE:
+ BlueprintHelpers.parseUnitUsageOption(segment, macros);
+ return ParseState.STATE_NULL;
case STATE_INCREMENT_PRECISION:
BlueprintHelpers.parseIncrementOption(segment, macros);
return ParseState.STATE_NULL;
@@ -894,6 +907,9 @@
if (macros.perUnit != null && GeneratorHelpers.perUnit(macros, sb)) {
sb.append(' ');
}
+ if (macros.usage != null && GeneratorHelpers.usage(macros, sb)) {
+ sb.append(' ');
+ }
if (macros.precision != null && GeneratorHelpers.precision(macros, sb)) {
sb.append(' ');
}
@@ -1047,6 +1063,10 @@
macros.unit = numerator;
}
+ /**
+ * Parses unit identifiers like "meter-per-second" and "foot-and-inch", as
+ * specified via a "unit/" concise skeleton.
+ */
private static void parseIdentifierUnitOption(StringSegment segment, MacroProps macros) {
MeasureUnit[] units = MeasureUnit.parseCoreUnitIdentifier(segment.asString());
if (units == null) {
@@ -1058,6 +1078,12 @@
}
}
+ private static void parseUnitUsageOption(StringSegment segment, MacroProps macros) {
+ macros.usage = segment.asString();
+ // We do not do any validation of the usage string: it depends on the
+ // unitPreferenceData in the units resources.
+ }
+
private static void parseFractionStem(StringSegment segment, MacroProps macros) {
assert segment.charAt(0) == '.';
int offset = 1;
@@ -1461,6 +1487,16 @@
}
}
+ private static boolean usage(MacroProps macros, StringBuilder sb) {
+ if (macros.usage != null && macros.usage.length() > 0) {
+ sb.append("usage/");
+ sb.append(macros.usage);
+
+ return true;
+ }
+ return false;
+ }
+
private static boolean precision(MacroProps macros, StringBuilder sb) {
if (macros.precision instanceof Precision.InfiniteRounderImpl) {
sb.append("precision-unlimited");
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/Precision.java b/icu4j/main/classes/core/src/com/ibm/icu/number/Precision.java
index 8f9c7db..d2dfb6b 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/Precision.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/Precision.java
@@ -20,6 +20,7 @@
*
* @stable ICU 62
* @see NumberFormatter
+ * @internal
*/
public abstract class Precision {
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java b/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
index 3e81417..9f23eaa 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
@@ -73,7 +73,7 @@
*
* @internal
*/
- private MeasureUnitImpl measureUnitImpl = null;
+ private MeasureUnitImpl measureUnitImpl;
/**
* Enumeration for unit complexity. There are three levels:
@@ -346,6 +346,7 @@
/**
* @internal
* @param measureUnitImpl
+ * @deprecated Internal API for ICU use only.
*/
public static MeasureUnit fromMeasureUnitImpl(MeasureUnitImpl measureUnitImpl) {
measureUnitImpl.serialize();
@@ -2094,4 +2095,4 @@
return MeasureUnit.internalGetInstance(type, subType);
}
}
-}
\ No newline at end of file
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
index 5198c59..b10b414 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
@@ -41,6 +41,7 @@
import com.ibm.icu.number.Precision;
import com.ibm.icu.number.Scale;
import com.ibm.icu.number.ScientificNotation;
+import com.ibm.icu.number.SkeletonSyntaxException;
import com.ibm.icu.number.UnlocalizedNumberFormatter;
import com.ibm.icu.text.ConstrainedFieldPosition;
import com.ibm.icu.text.DecimalFormatSymbols;
@@ -642,6 +643,81 @@
ULocale.forLanguageTag("es-MX"),
1,
"kelvin");
+
+ // TODO(icu-units#35): skeleton generation.
+ assertFormatSingle(
+ "Mixed unit",
+ null,
+ "unit/yard-and-foot-and-inch",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("yard-and-foot-and-inch")),
+ new ULocale("en-US"),
+ 3.65,
+ "3 yd, 1 ft, 11.4 in");
+
+ // TODO(icu-units#35): skeleton generation.
+ assertFormatSingle(
+ "Mixed unit, Scientific",
+ null,
+ "unit/yard-and-foot-and-inch E0",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("yard-and-foot-and-inch"))
+ .notation(Notation.scientific()),
+ new ULocale("en-US"),
+ 3.65,
+ "3 yd, 1 ft, 1.14E1 in");
+
+ // TODO(icu-units#35): skeleton generation.
+ assertFormatSingle(
+ "Mixed Unit (Narrow Version)",
+ null,
+ "unit/metric-ton-and-kilogram-and-gram unit-width-narrow",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("metric-ton-and-kilogram-and-gram"))
+ .unitWidth(UnitWidth.NARROW),
+ new ULocale("en-US"),
+ 4.28571,
+ "4t 285kg 710g");
+
+ // TODO(icu-units#35): skeleton generation.
+ assertFormatSingle(
+ "Mixed Unit (Short Version)",
+ null,
+ "unit/metric-ton-and-kilogram-and-gram unit-width-short",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("metric-ton-and-kilogram-and-gram"))
+ .unitWidth(UnitWidth.SHORT),
+ new ULocale("en-US"),
+ 4.28571,
+ "4 t, 285 kg, 710 g");
+
+ // TODO(icu-units#35): skeleton generation.
+ assertFormatSingle(
+ "Mixed Unit (Full Name Version)",
+ null,
+ "unit/metric-ton-and-kilogram-and-gram unit-width-full-name",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("metric-ton-and-kilogram-and-gram"))
+ .unitWidth(UnitWidth.FULL_NAME),
+ new ULocale("en-US"),
+ 4.28571,
+ "4 metric tons, 285 kilograms, 710 grams");
+
+// // TODO(icu-units#73): deal with this "1 foot 12 inches" problem.
+// // At the time of writing, this test would pass, but is commented out
+// // because it reflects undesired behaviour:
+// assertFormatSingle(
+// u"Demonstrating the \"1 foot 12 inches\" problem",
+// nullptr,
+// u"unit/foot-and-inch",
+// NumberFormatter::with()
+// .unit(MeasureUnit::forIdentifier("foot-and-inch"))
+// .precision(Precision::maxSignificantDigits(4))
+// .unitWidth(UNUM_UNIT_WIDTH_FULL_NAME),
+// Locale("en-US"),
+// 1.9999,
+// // This is undesireable but current behaviour:
+// u"1 foot, 12 inches");
}
@Test
@@ -693,8 +769,468 @@
"0.08765 J/fur",
"0.008765 J/fur",
"0 J/fur");
+
+ // TODO(icu-units#35): does not normalize as desired: while "unit/*" does
+ // get split into unit/perUnit, ".unit(*)" and "measure-unit/*" don't:
+ assertFormatSingle(
+ "Built-in unit, meter-per-second",
+ "measure-unit/speed-meter-per-second",
+ "~unit/meter-per-second",
+ NumberFormatter.with().unit(MeasureUnit.METER_PER_SECOND),
+ new ULocale("en-GB"),
+ 2.4,
+ "2.4 m/s");
+
+ // TODO(icu-units#59): THIS UNIT TEST DEMONSTRATES UNDESIRABLE BEHAVIOUR!
+ // When specifying built-in types, one can give both a unit and a perUnit.
+ // Resolving to a built-in unit does not always work.
+ //
+ // (Unit-testing philosophy: do we leave this enabled to demonstrate current
+ // behaviour, and changing behaviour in the future? Or comment it out to
+ // avoid asserting this is "correct"?)
+ assertFormatSingle(
+ "DEMONSTRATING BAD BEHAVIOUR, TODO(icu-units#59)",
+ "measure-unit/speed-meter-per-second per-measure-unit/duration-second",
+ "measure-unit/speed-meter-per-second per-measure-unit/duration-second",
+ NumberFormatter.with()
+ .unit(MeasureUnit.METER_PER_SECOND)
+ .perUnit(MeasureUnit.SECOND),
+ new ULocale("en-GB"),
+ 2.4,
+ "2.4 m/s/s");
+
+ // Testing the rejection of invalid specifications
+
+ // If .unit() is not given a built-in type, .perUnit() is not allowed
+ // (because .unit is now flexible enough to handle compound units,
+ // .perUnit() is supported for backward compatibility).
+ LocalizedNumberFormatter nf = NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("furlong-pascal"))
+ .perUnit(MeasureUnit.METER)
+ .locale(new ULocale("en-GB"));
+
+ try {
+ nf.format(2.4d);
+ fail("Expected failure, got: " + nf.format(2.4d) + ".");
+ } catch (UnsupportedOperationException e) {
+ // Pass
+ }
+
+ // .perUnit() may only be passed a built-in type, "square-second" is not a
+ // built-in type.
+ nf = NumberFormatter.with()
+ .unit(MeasureUnit.METER)
+ .perUnit(MeasureUnit.forIdentifier("square-second"))
+ .locale(new ULocale("en-GB"));
+
+ try {
+ nf.format(2.4d);
+ fail("Expected failure, got: " + nf.format(2.4d) + ".");
+ } catch (UnsupportedOperationException e) {
+ // pass
+ }
}
+
+ @Test
+ public void unitUsage() {
+ UnlocalizedNumberFormatter unloc_formatter;
+ LocalizedNumberFormatter formatter;
+ FormattedNumber formattedNum;
+ String uTestCase;
+
+ unloc_formatter = NumberFormatter.with().usage("road").unit(MeasureUnit.METER);
+
+ uTestCase = "unitUsage() en-ZA road";
+ formatter = unloc_formatter.locale(new ULocale("en-ZA"));
+ formattedNum = formatter.format(321d);
+
+
+ assertTrue(
+ uTestCase + ", got outputUnit: \"" + formattedNum.getOutputUnit().getIdentifier() + "\"",
+ MeasureUnit.METER.equals(formattedNum.getOutputUnit()));
+ assertEquals(uTestCase, "300 m", formattedNum.toString());
+ {
+ final Object[][] expectedFieldPositions = {
+ {NumberFormat.Field.INTEGER, 0, 3},
+ {NumberFormat.Field.MEASURE_UNIT, 4, 5}
+ };
+
+ assertNumberFieldPositions(
+ uTestCase + " field positions",
+ formattedNum,
+ expectedFieldPositions);
+ }
+
+ assertFormatDescendingBig(
+ uTestCase,
+ "measure-unit/length-meter usage/road",
+ "unit/meter usage/road",
+ unloc_formatter,
+ new ULocale("en-ZA"),
+ "87\u00A0650 km",
+ "8\u00A0765 km",
+ "876 km", // 6.5 rounds down, 7.5 rounds up.
+ "88 km",
+ "8,8 km",
+ "900 m",
+ "90 m",
+ "10 m",
+ "0 m");
+
+ uTestCase = "unitUsage() en-GB road";
+ formatter = unloc_formatter.locale(new ULocale("en-GB"));
+ formattedNum = formatter.format(321d);
+
+ // status.errIfFailureAndReset("unitUsage() en-GB road, formatDouble(...)");
+
+ assertTrue(
+ uTestCase + ", got outputUnit: \"" + formattedNum.getOutputUnit().getIdentifier() + "\"",
+ MeasureUnit.YARD.equals(formattedNum.getOutputUnit()));
+ // status.errIfFailureAndReset("unitUsage() en-GB road, getOutputUnit(...)");
+ assertEquals(uTestCase, "350 yd", formattedNum.toString());
+ //status.errIfFailureAndReset("unitUsage() en-GB road, toString(...)");
+ {
+ final Object[][] expectedFieldPositions = {
+ {NumberFormat.Field.INTEGER, 0, 3},
+ {NumberFormat.Field.MEASURE_UNIT, 4, 6}};
+ assertNumberFieldPositions(
+ (uTestCase + " field positions"),
+ formattedNum,
+ expectedFieldPositions);
+ }
+
+ assertFormatDescendingBig(
+ uTestCase,
+ "measure-unit/length-meter usage/road",
+ "unit/meter usage/road",
+ unloc_formatter,
+ new ULocale("en-GB"),
+ "54,463 mi",
+ "5,446 mi",
+ "545 mi",
+ "54 mi",
+ "5.4 mi",
+ "0.54 mi",
+ "96 yd",
+ "9.6 yd",
+ "0 yd");
+
+ uTestCase = "unitUsage() en-US road";
+ formatter = unloc_formatter.locale(new ULocale("en-US"));
+ formattedNum = formatter.format(321d);
+ // status.errIfFailureAndReset("unitUsage() en-US road, formatDouble(...)");
+
+ assertTrue(
+ uTestCase + ", got outputUnit: \"" + formattedNum.getOutputUnit().getIdentifier() + "\"",
+ MeasureUnit.FOOT == formattedNum.getOutputUnit());
+ // status.errIfFailureAndReset("unitUsage() en-US road, getOutputUnit(...)");
+
+ assertEquals(uTestCase, "1,050 ft", formattedNum.toString());
+ // status.errIfFailureAndReset("unitUsage() en-US road, toString(...)");
+ {
+ final Object[][] expectedFieldPositions = {
+ {NumberFormat.Field.GROUPING_SEPARATOR, 1, 2},
+ {NumberFormat.Field.INTEGER, 0, 5},
+ {NumberFormat.Field.MEASURE_UNIT, 6, 8}};
+ assertNumberFieldPositions(
+ uTestCase + " field positions",
+ formattedNum,
+ expectedFieldPositions);
+ }
+ assertFormatDescendingBig(
+ uTestCase,
+ "measure-unit/length-meter usage/road",
+ "unit/meter usage/road",
+ unloc_formatter,
+ new ULocale("en-US"),
+ "54,463 mi",
+ "5,446 mi",
+ "545 mi",
+ "54 mi",
+ "5.4 mi",
+ "0.54 mi",
+ "300 ft",
+ "30 ft",
+ "0 ft");
+
+ unloc_formatter = NumberFormatter.with().usage("person").unit(MeasureUnit.KILOGRAM);
+ uTestCase = "unitUsage() en-GB person";
+ formatter = unloc_formatter.locale(new ULocale("en-GB"));
+ formattedNum = formatter.format(80d);
+ // status.errIfFailureAndReset("unitUsage() en-GB person formatDouble");
+
+ assertTrue(
+ uTestCase + ", got outputUnit: \"" + formattedNum.getOutputUnit().getIdentifier() + "\"",
+ MeasureUnit.forIdentifier("stone-and-pound").equals(formattedNum.getOutputUnit()));
+ // status.errIfFailureAndReset("unitUsage() en-GB person - formattedNum.getOutputUnit(status)");
+
+ assertEquals(uTestCase, "12 st, 8.4 lb", formattedNum.toString());
+ //status.errIfFailureAndReset("unitUsage() en-GB person, toString(...)");
+ {
+ final Object[][] expectedFieldPositions = {
+ // // Desired output: TODO(icu-units#67)
+ // {NumberFormat.Field.INTEGER, 0, 2},
+ // {NumberFormat.Field.MEASURE_UNIT, 3, 5},
+ // {NumberFormat.ULISTFMT_LITERAL_FIELD, 5, 6},
+ // {NumberFormat.Field.INTEGER, 7, 8},
+ // {NumberFormat.DECIMAL_SEPARATOR_FIELD, 8, 9},
+ // {NumberFormat.FRACTION_FIELD, 9, 10},
+ // {NumberFormat.Field.MEASURE_UNIT, 11, 13}};
+
+ // Current output: rather no fields than wrong fields
+ {NumberFormat.Field.INTEGER, 7, 8},
+ {NumberFormat.Field.DECIMAL_SEPARATOR, 8, 9},
+ {NumberFormat.Field.FRACTION, 9, 10},
+ };
+
+ assertNumberFieldPositions(
+ uTestCase + " field positions",
+ formattedNum,
+ expectedFieldPositions);
+ }
+ assertFormatDescending(
+ uTestCase,
+ "measure-unit/mass-kilogram usage/person",
+ "unit/kilogram usage/person",
+ unloc_formatter,
+ new ULocale("en-GB"),
+ "13,802 st, 7.2 lb",
+ "1,380 st, 3.5 lb",
+ "138 st, 0.35 lb",
+ "13 st, 11 lb",
+ "1 st, 5.3 lb",
+ "1 lb, 15 oz",
+ "0 lb, 3.1 oz",
+ "0 lb, 0.31 oz",
+ "0 lb, 0 oz");
+
+ assertFormatDescending(
+ uTestCase,
+ "usage/person unit-width-narrow measure-unit/mass-kilogram",
+ "usage/person unit-width-narrow unit/kilogram",
+ unloc_formatter.unitWidth(UnitWidth.NARROW),
+ new ULocale("en-GB"),
+ "13,802st 7.2lb",
+ "1,380st 3.5lb",
+ "138st 0.35lb",
+ "13st 11lb",
+ "1st 5.3lb",
+ "1lb 15oz",
+ "0lb 3.1oz",
+ "0lb 0.31oz",
+ "0lb 0oz");
+
+ assertFormatDescending(
+ uTestCase,
+ "usage/person unit-width-short measure-unit/mass-kilogram",
+ "usage/person unit-width-short unit/kilogram",
+ unloc_formatter.unitWidth(UnitWidth.SHORT),
+ new ULocale("en-GB"),
+ "13,802 st, 7.2 lb",
+ "1,380 st, 3.5 lb",
+ "138 st, 0.35 lb",
+ "13 st, 11 lb",
+ "1 st, 5.3 lb",
+ "1 lb, 15 oz",
+ "0 lb, 3.1 oz",
+ "0 lb, 0.31 oz",
+ "0 lb, 0 oz");
+
+ assertFormatDescending(
+ uTestCase,
+ "usage/person unit-width-full-name measure-unit/mass-kilogram",
+ "usage/person unit-width-full-name unit/kilogram",
+ unloc_formatter.unitWidth(UnitWidth.FULL_NAME),
+ new ULocale("en-GB"),
+ "13,802 stone, 7.2 pounds",
+ "1,380 stone, 3.5 pounds",
+ "138 stone, 0.35 pounds",
+ "13 stone, 11 pounds",
+ "1 stone, 5.3 pounds",
+ "1 pound, 15 ounces",
+ "0 pounds, 3.1 ounces",
+ "0 pounds, 0.31 ounces",
+ "0 pounds, 0 ounces");
+
+ // TODO: this is about the user overriding the usage precision.
+ // TODO: should be done!
+// assertFormatDescendingBig(
+// "Scientific notation with Usage: possible when using a reasonable Precision",
+// "scientific @### usage/default measure-unit/area-square-meter unit-width-full-name",
+// "scientific @### usage/default unit/square-meter unit-width-full-name",
+// NumberFormatter.with()
+// .unit(MeasureUnit.SQUARE_METER)
+// .usage("default")
+// .notation(Notation.scientific())
+// .precision(Precision.minMaxSignificantDigits(1, 4))
+// .unitWidth(UnitWidth.FULL_NAME),
+// new ULocale("en-ZA"),
+// "8,765E1 square kilometres",
+// "8,765E0 square kilometres",
+// "8,765E1 hectares",
+// "8,765E0 hectares",
+// "8,765E3 square metres",
+// "8,765E2 square metres",
+// "8,765E1 square metres",
+// "8,765E0 square metres",
+// "0E0 square centimetres");
+ }
+
+
+ @Test
+ public void unitUsageErrorCodes() {
+ UnlocalizedNumberFormatter unloc_formatter;
+
+ try {
+ NumberFormatter.forSkeleton("unit/foobar");
+ fail("should give an error, because foobar is an invalid unit");
+ } catch (SkeletonSyntaxException e) {
+ // Pass
+ }
+
+ unloc_formatter = NumberFormatter.forSkeleton("usage/foobar");
+ // This does not give an error, because usage is not looked up yet.
+ //status.errIfFailureAndReset("Expected behaviour: no immediate error for invalid usage");
+
+ try {
+ // Lacking a unit results in a failure. The skeleton is "incomplete", but we
+ // support adding the unit via the fluent API, so it is not an error until
+ // we build the formatting pipeline itself.
+ unloc_formatter.locale(new ULocale("en-GB")).format(1);
+ fail("should throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // Pass
+ }
+
+ // Adding the unit as part of the fluent chain leads to success.
+ unloc_formatter.unit(MeasureUnit.METER).locale(new ULocale("en-GB")).format(1); /* No Exception should be thrown */
+
+ }
+
+
+ // Tests for the "skeletons" field in unitPreferenceData, as well as precision
+ // and notation overrides.
+ @Test
+ public void unitUsageSkeletons() {
+ assertFormatSingle(
+ "Default >300m road preference skeletons round to 50m",
+ "usage/road measure-unit/length-meter",
+ "usage/road unit/meter",
+ NumberFormatter.with().unit(MeasureUnit.METER).usage("road"),
+ new ULocale("en-ZA"),
+ 321,
+ "300 m");
+
+ // TODO(younies): enable this test case
+// assertFormatSingle(
+// "Precision can be overridden: override takes precedence",
+// "usage/road measure-unit/length-meter @#",
+// "usage/road unit/meter @#",
+// NumberFormatter.with()
+// .unit(MeasureUnit.METER)
+// .usage("road")
+// .precision(Precision.maxSignificantDigits(2)),
+// new ULocale("en-ZA"),
+// 321,
+// "320 m");
+
+ assertFormatSingle(
+ "Compact notation with Usage: bizarre, but possible (short)",
+ "compact-short usage/road measure-unit/length-meter",
+ "compact-short usage/road unit/meter",
+ NumberFormatter.with()
+ .unit(MeasureUnit.METER)
+ .usage("road")
+ .notation(Notation.compactShort()),
+ new ULocale("en-ZA"),
+ 987654321L,
+ "988K km");
+
+
+
+ // TODO(younies): enable override precision test cases.
+// assertFormatSingle(
+// "Compact notation with Usage: bizarre, but possible (short, precision override)",
+// "compact-short usage/road measure-unit/length-meter @#",
+// "compact-short usage/road unit/meter @#",
+// NumberFormatter.with()
+// .unit(MeasureUnit.METER)
+// .usage("road")
+// .notation(Notation.compactShort())
+// .precision(Precision.maxSignificantDigits(2)),
+// new ULocale("en-ZA"),
+// 987654321L,
+// "990K km");
+
+ // TODO(younies): enable override precision test cases.
+// assertFormatSingle(
+// "Compact notation with Usage: unusual but possible (long)",
+// "compact-long usage/road measure-unit/length-meter @#",
+// "compact-long usage/road unit/meter @#",
+// NumberFormatter.with()
+// .unit(MeasureUnit.METER)
+// .usage("road")
+// .notation(Notation.compactLong())
+// .precision(Precision.maxSignificantDigits(2)),
+// new ULocale("en-ZA"),
+// 987654321,
+// "990 thousand km");
+
+ // TODO(younies): enable override precision test cases.
+// assertFormatSingle(
+// "Compact notation with Usage: unusual but possible (long, precision override)",
+// "compact-long usage/road measure-unit/length-meter @#",
+// "compact-long usage/road unit/meter @#",
+// NumberFormatter.with()
+// .unit(MeasureUnit.METER)
+// .usage("road")
+// .notation(Notation.compactLong())
+// .precision(Precision.maxSignificantDigits(2)),
+// new ULocale("en-ZA"),
+// 987654321,
+// "990 thousand km");
+
+ // TODO(younies): enable override precision test cases.
+// assertFormatSingle(
+// "Scientific notation, not recommended, requires precision override for road",
+// "scientific usage/road measure-unit/length-meter",
+// "scientific usage/road unit/meter",
+// NumberFormatter.with().unit(MeasureUnit.METER).usage("road").notation(Notation.scientific()),
+// new ULocale("en-ZA"),
+// 321.45,
+// // Rounding to the nearest "50" is not exponent-adjusted in scientific notation:
+// "0E2 m");
+
+ // TODO(younies): enable override precision test cases.
+// assertFormatSingle(
+// "Scientific notation with Usage: possible when using a reasonable Precision",
+// "scientific usage/road measure-unit/length-meter @###",
+// "scientific usage/road unit/meter @###",
+// NumberFormatter.with()
+// .unit(MeasureUnit.METER)
+// .usage("road")
+// .notation(Notation.scientific())
+// .precision(Precision.maxSignificantDigits(4)),
+// new ULocale("en-ZA"),
+// 321.45, // 0.45 rounds down, 0.55 rounds up.
+// "3,214E2 m");
+
+ assertFormatSingle(
+ "Scientific notation with Usage: possible when using a reasonable Precision",
+ "scientific usage/default measure-unit/length-astronomical-unit unit-width-full-name",
+ "scientific usage/default unit/astronomical-unit unit-width-full-name",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("astronomical-unit"))
+ .usage("default")
+ .notation(Notation.scientific())
+ .unitWidth(UnitWidth.FULL_NAME),
+ new ULocale("en-ZA"),
+ 1e20,
+ "1,5E28 kilometres");
+ }
+
+
@Test
public void unitCurrency() {
assertFormatDescending(
@@ -3332,6 +3868,7 @@
conciseSkeleton = conciseSkeleton.substring(1);
shouldRoundTrip = false;
}
+
LocalizedNumberFormatter l4 = NumberFormatter.forSkeleton(conciseSkeleton).locale(locale);
if (shouldRoundTrip) {
assertEquals(message + ": Concise Skeleton:", normalized, l4.toSkeleton());