ICU-20568 Add .unit().usage() support to ICU4J NumberFormatter (1/2)
This commit is contained in:
parent
5e0cec2c2b
commit
7ba2b48f7b
@ -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,
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
*
|
||||
|
@ -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});*/
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -20,6 +20,7 @@ import com.ibm.icu.util.Currency.CurrencyUsage;
|
||||
*
|
||||
* @stable ICU 62
|
||||
* @see NumberFormatter
|
||||
* @internal
|
||||
*/
|
||||
public abstract class Precision {
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
Loading…
Reference in New Issue
Block a user