ICU-20568 Add .unit().usage() support to ICU4J NumberFormatter (1/2)

This commit is contained in:
younies 2020-09-03 15:26:46 +02:00 committed by Hugo van der Merwe
parent 5e0cec2c2b
commit 7ba2b48f7b
17 changed files with 1288 additions and 53 deletions

View File

@ -26,7 +26,7 @@ public class LongNameHandler implements MicroPropsGenerator, ModifierStore {
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 @@ public class LongNameHandler implements MicroPropsGenerator, ModifierStore {
}
}
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 @@ public class LongNameHandler implements MicroPropsGenerator, ModifierStore {
// 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 @@ public class LongNameHandler implements MicroPropsGenerator, ModifierStore {
// 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 @@ public class LongNameHandler implements MicroPropsGenerator, ModifierStore {
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,

View File

@ -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");
}
}

View File

@ -30,6 +30,7 @@ public class MacroProps implements Cloneable {
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 @@ public class MacroProps implements Cloneable {
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 @@ public class MacroProps implements Cloneable {
decimal,
affixProvider,
scale,
usage,
rules,
loc);
}
@ -119,6 +123,7 @@ public class MacroProps implements Cloneable {
&& 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);
}

View File

@ -7,7 +7,16 @@ import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
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 class MicroProps implements Cloneable, MicroPropsGenerator {
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 @@ public class MicroProps implements Cloneable, MicroPropsGenerator {
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

View File

@ -19,6 +19,9 @@ package com.ibm.icu.impl.number;
* {@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:
*

View File

@ -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});*/
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -83,13 +83,21 @@ public class UnitsRouter {
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 @@ public class UnitsRouter {
* 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 @@ public class UnitsRouter {
* 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;
}
}
}

View File

@ -12,6 +12,7 @@ import com.ibm.icu.impl.number.DecimalQuantity;
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 @@ import com.ibm.icu.text.PluralRules.IFixedDecimal;
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;
}
/**
@ -114,6 +117,21 @@ public class FormattedNumber implements FormattedValue {
return fq.toBigDecimal();
}
/**
* 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.

View File

@ -13,6 +13,7 @@ import com.ibm.icu.impl.number.DecimalQuantity;
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 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
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 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
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 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
* @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 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
* @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 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
withUnit = new LocalizedNumberFormatter(this, KEY_UNIT, unit);
savedWithUnit = withUnit;
}
withUnit.formatImpl(fq, string);
return withUnit.formatImpl(fq, string);
}
/**

View File

@ -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;
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.Padder;
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 @@ import com.ibm.icu.util.MeasureUnit;
*/
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 @@ class NumberFormatterImpl {
/**
* 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 @@ class NumberFormatterImpl {
/**
* 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 @@ class NumberFormatterImpl {
|| !(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 @@ class NumberFormatterImpl {
/// 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 @@ class NumberFormatterImpl {
// 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

View File

@ -45,6 +45,7 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
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 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
* <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 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
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 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
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);
}

View File

@ -33,10 +33,12 @@ import com.ibm.icu.util.StringTrieBuilder;
*/
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 @@ class NumberSkeletonImpl {
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 @@ class NumberSkeletonImpl {
STEM_MEASURE_UNIT,
STEM_PER_MEASURE_UNIT,
STEM_UNIT,
STEM_UNIT_USAGE,
STEM_CURRENCY,
STEM_INTEGER_WIDTH,
STEM_NUMBERING_SYSTEM,
@ -193,6 +197,7 @@ class NumberSkeletonImpl {
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 @@ class NumberSkeletonImpl {
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 @@ class NumberSkeletonImpl {
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 @@ class NumberSkeletonImpl {
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 @@ class NumberSkeletonImpl {
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 @@ class NumberSkeletonImpl {
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 @@ class NumberSkeletonImpl {
}
}
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 @@ class NumberSkeletonImpl {
}
}
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");

View File

@ -20,6 +20,7 @@ import com.ibm.icu.util.Currency.CurrencyUsage;
*
* @stable ICU 62
* @see NumberFormatter
* @internal
*/
public abstract class Precision {

View File

@ -73,7 +73,7 @@ public class MeasureUnit implements Serializable {
*
* @internal
*/
private MeasureUnitImpl measureUnitImpl = null;
private MeasureUnitImpl measureUnitImpl;
/**
* Enumeration for unit complexity. There are three levels:
@ -346,6 +346,7 @@ public class MeasureUnit implements Serializable {
/**
* @internal
* @param measureUnitImpl
* @deprecated Internal API for ICU use only.
*/
public static MeasureUnit fromMeasureUnitImpl(MeasureUnitImpl measureUnitImpl) {
measureUnitImpl.serialize();
@ -2094,4 +2095,4 @@ public class MeasureUnit implements Serializable {
return MeasureUnit.internalGetInstance(type, subType);
}
}
}
}

View File

@ -41,6 +41,7 @@ import com.ibm.icu.number.NumberFormatter.UnitWidth;
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 @@ public class NumberFormatterApiTest extends TestFmwk {
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 @@ public class NumberFormatterApiTest extends TestFmwk {
"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 @@ public class NumberFormatterApiTest extends TestFmwk {
conciseSkeleton = conciseSkeleton.substring(1);
shouldRoundTrip = false;
}
LocalizedNumberFormatter l4 = NumberFormatter.forSkeleton(conciseSkeleton).locale(locale);
if (shouldRoundTrip) {
assertEquals(message + ": Concise Skeleton:", normalized, l4.toSkeleton());