import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
+/**
+ * Takes care of formatting currency and measurement unit names, as well as populating the gender of measure units.
+ */
public class LongNameHandler
implements MicroPropsGenerator, ModifierStore, LongNameMultiplexer.ParentlessMicroPropsGenerator {
- private static final int DNAM_INDEX = StandardPlural.COUNT;
- private static final int PER_INDEX = StandardPlural.COUNT + 1;
- static final int ARRAY_LENGTH = StandardPlural.COUNT + 2;
+ private static int i = 0;
+ private static final int DNAM_INDEX = StandardPlural.COUNT + i++;
+ private static final int PER_INDEX = StandardPlural.COUNT + i++;
+ private static final int GENDER_INDEX = StandardPlural.COUNT + i++;
+ static final int ARRAY_LENGTH = StandardPlural.COUNT + i++;
private static int getIndex(String pluralKeyword) {
- // pluralKeyword can also be "dnam" or "per"
+ // pluralKeyword can also be "dnam", "per" or "gender"
if (pluralKeyword.equals("dnam")) {
return DNAM_INDEX;
} else if (pluralKeyword.equals("per")) {
return PER_INDEX;
+ } else if (pluralKeyword.equals("gender")) {
+ return GENDER_INDEX;
} else {
return StandardPlural.fromString(pluralKeyword).ordinal();
}
UResource.Table pluralsTable = value.getTable();
for (int i = 0; pluralsTable.getKeyAndValue(i, key, value); ++i) {
String keyString = key.toString();
- if (keyString.equals("case") || keyString.equals("gender")) {
- // TODO: @Hugo to fix for new grammatical stuff
+
+ if (keyString.equals("case")) {
continue;
}
+
int index = getIndex(keyString);
if (outArray[index] != null) {
continue;
}
+
String formatString = value.getString();
outArray[index] = formatString;
}
}
// NOTE: outArray MUST have at least ARRAY_LENGTH entries. No bounds checking is performed.
-
static void getMeasureData(
ULocale locale,
MeasureUnit unit,
UnitWidth width,
+ String unitDisplayCase,
String[] outArray) {
PluralTableSink sink = new PluralTableSink(outArray);
ICUResourceBundle resource;
locale);
StringBuilder key = new StringBuilder();
key.append("units");
+ // TODO(icu-units#140): support gender for other unit widths.
if (width == UnitWidth.NARROW) {
key.append("Narrow");
} else if (width == UnitWidth.SHORT) {
key.append(unit.getSubtype());
}
+ // Grab desired case first, if available. Then grab nominative case to fill
+ // in the gaps.
+ //
+ // TODO(icu-units#138): check that fallback is spec-compliant
+ if (width == UnitWidth.FULL_NAME
+ && unitDisplayCase != null
+ && !unitDisplayCase.isEmpty()) {
+ StringBuilder caseKey = new StringBuilder();
+ caseKey.append(key);
+ caseKey.append("/case/");
+ caseKey.append(unitDisplayCase);
+
+ try {
+ resource.getAllItemsWithFallback(caseKey.toString(), sink);
+ // TODO(icu-units#138): our fallback logic is not spec-compliant: we
+ // check the given case, then go straight to the no-case data. The spec
+ // states we should first look for case="nominative". As part of #138,
+ // either get the spec changed, or add unit tests that warn us if
+ // case="nominative" data differs from no-case data?
+ } catch (MissingResourceException e) {
+ // continue.
+ }
+ }
try {
resource.getAllItemsWithFallback(key.toString(), sink);
} catch (MissingResourceException e) {
}
}
+ private static String getDeriveCompoundRule(ULocale locale, String feature, String structure) {
+ ICUResourceBundle derivationsBundle =
+ (ICUResourceBundle) UResourceBundle
+ .getBundleInstance(ICUData.ICU_BASE_NAME, "grammaticalFeatures");
+
+ derivationsBundle = (ICUResourceBundle) derivationsBundle.get("grammaticalData");
+ derivationsBundle = (ICUResourceBundle) derivationsBundle.get("derivations");
+
+ ICUResourceBundle stackBundle;
+ try {
+ // TODO: use standard normal locale resolution algorithms rather than just grabbing language:
+ stackBundle = (ICUResourceBundle) derivationsBundle.get(locale.getLanguage());
+ } catch (MissingResourceException e) {
+ stackBundle = (ICUResourceBundle) derivationsBundle.get("root");
+ }
+
+ stackBundle = (ICUResourceBundle) stackBundle.get("compound");
+ stackBundle = (ICUResourceBundle) stackBundle.get(feature);
+
+ return stackBundle.getString(structure);
+ }
+
////////////////////////
/// END DATA LOADING ///
////////////////////////
private final Map<StandardPlural, SimpleModifier> modifiers;
private final PluralRules rules;
private final MicroPropsGenerator parent;
+ // Grammatical gender of the formatted result.
+ private String gender = "";
private LongNameHandler(
Map<StandardPlural, SimpleModifier> modifiers,
public static String getUnitDisplayName(ULocale locale, MeasureUnit unit, UnitWidth width) {
String[] measureData = new String[ARRAY_LENGTH];
- getMeasureData(locale, unit, width, measureData);
+ getMeasureData(locale, unit, width, "", measureData);
return measureData[DNAM_INDEX];
}
* @param locale The desired locale.
* @param unit The measure unit to construct a LongNameHandler for.
* @param width Specifies the desired unit rendering.
+ * @param unitDisplayCase
* @param rules Plural rules.
* @param parent Plural rules.
*/
ULocale locale,
MeasureUnit unit,
UnitWidth width,
+ String unitDisplayCase,
PluralRules rules,
MicroPropsGenerator parent) {
if (unit.getType() == null) {
}
}
}
- return forCompoundUnit(locale, unit, perUnit, width, rules, parent);
+ return forCompoundUnit(locale, unit, perUnit, width, unitDisplayCase, rules, parent);
}
String[] simpleFormats = new String[ARRAY_LENGTH];
- getMeasureData(locale, unit, width, simpleFormats);
+ getMeasureData(locale, unit, width, unitDisplayCase, simpleFormats);
// TODO(ICU4J): Reduce the number of object creations here?
Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<>(
StandardPlural.class);
LongNameHandler result = new LongNameHandler(modifiers, rules, parent);
result.simpleFormatsToModifiers(simpleFormats, NumberFormat.Field.MEASURE_UNIT);
+ if (simpleFormats[GENDER_INDEX] != null) {
+ result.gender = simpleFormats[GENDER_INDEX];
+ }
+
return result;
}
+ /**
+ * Loads and applies deriveComponent rules from CLDR's grammaticalFeatures.xml.
+ * <pre>
+ * Consider a deriveComponent rule that looks like this:
+ * </pre>
+ * <deriveComponent feature="case" structure="per" value0="compound" value1="nominative"/>
+ * <p>
+ * Instantiating an instance as follows:
+ * <pre>
+ * DerivedComponents d(loc, "case", "per", "foo");
+ * </pre>
+ * <p>
+ * Applying the rule in the XML element above, <code>d.value0()</code> will be "foo", and
+ * <code>d.value1()</code> will be "nominative".
+ * <p>
+ * <p>
+ * In case of any kind of failure, value0() and value1() will simply return "".
+ */
+ private static class DerivedComponents {
+ /**
+ * Constructor.
+ */
+ public DerivedComponents(ULocale locale,
+ String feature,
+ String structure,
+ String compoundValue) {
+ ICUResourceBundle derivationsBundle =
+ (ICUResourceBundle) UResourceBundle
+ .getBundleInstance(ICUData.ICU_BASE_NAME, "grammaticalFeatures");
+ derivationsBundle = (ICUResourceBundle) derivationsBundle.get("grammaticalData");
+ derivationsBundle = (ICUResourceBundle) derivationsBundle.get("derivations");
+
+ ICUResourceBundle stackBundle;
+ try {
+ // TODO: use standard normal locale resolution algorithms rather than just grabbing language:
+ stackBundle = (ICUResourceBundle) derivationsBundle.get(locale.getLanguage());
+ } catch (MissingResourceException e) {
+ stackBundle = (ICUResourceBundle) derivationsBundle.get("root");
+ }
+
+ stackBundle = (ICUResourceBundle) stackBundle.get("component");
+ stackBundle = (ICUResourceBundle) stackBundle.get(feature);
+ stackBundle = (ICUResourceBundle) stackBundle.get(structure);
+
+ String value = stackBundle.getString(0);
+ if (value.compareTo("compound") == 0) {
+ this.value0 = compoundValue;
+ } else {
+ this.value0 = value;
+ }
+
+ value = stackBundle.getString(1);
+ if (value.compareTo("compound") == 0) {
+ this.value1 = compoundValue;
+ } else {
+ this.value1 = value;
+ }
+ }
+
+ public final String value0, value1;
+ }
+
private static LongNameHandler forCompoundUnit(
ULocale locale,
MeasureUnit unit,
MeasureUnit perUnit,
UnitWidth width,
+ String unitDisplayCase,
PluralRules rules,
MicroPropsGenerator parent) {
if (unit.getType() == null || perUnit.getType() == null) {
"Unsanctioned units, not yet supported: " + unit.getIdentifier() + "/" +
perUnit.getIdentifier());
}
+
+ DerivedComponents derivedPerCases = new DerivedComponents(locale, "case", "per", unitDisplayCase);
+
+
String[] primaryData = new String[ARRAY_LENGTH];
- getMeasureData(locale, unit, width, primaryData);
+ getMeasureData(locale, unit, width, derivedPerCases.value0, primaryData);
String[] secondaryData = new String[ARRAY_LENGTH];
- getMeasureData(locale, perUnit, width, secondaryData);
+ getMeasureData(locale, perUnit, width, derivedPerCases.value1, secondaryData);
+
+ // TODO(icu-units#139): implement these rules:
+ // - <deriveComponent feature="plural" structure="per" ...>
+ // - This has impact on multiSimpleFormatsToModifiers(...) below too.
+ //
+ // These rules are currently (ICU 69) all the same and hard-coded below.
String perUnitFormat;
if (secondaryData[PER_INDEX] != null) {
perUnitFormat = secondaryData[PER_INDEX];
// Some "one" pattern may not contain "{0}". For example in "ar" or "ne" locale.
String secondaryCompiled = SimpleFormatterImpl
.compileToStringMinMaxArguments(secondaryFormat, sb, 0, 1);
- String secondaryString = SimpleFormatterImpl.getTextWithNoArguments(secondaryCompiled)
- .trim();
+ String secondaryFormatString = SimpleFormatterImpl.getTextWithNoArguments(secondaryCompiled);
+
+ // TODO(icu-units#28): do not use regular expression
+ String secondaryString = secondaryFormatString.replaceAll("(^\\h*)|(\\h*$)",""); // Trim all spaces.
+
perUnitFormat = SimpleFormatterImpl.formatCompiledPattern(compiled, "{0}", secondaryString);
}
Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<>(
StandardPlural.class);
LongNameHandler result = new LongNameHandler(modifiers, rules, parent);
result.multiSimpleFormatsToModifiers(primaryData, perUnitFormat, NumberFormat.Field.MEASURE_UNIT);
+
+ // Gender
+ String val = getDeriveCompoundRule(locale, "gender", "per");
+
+ assert (val != null && val.length() == 1);
+ switch (val.charAt(0)) {
+ case '0':
+ result.gender = primaryData[GENDER_INDEX];
+ break;
+ case '1':
+ result.gender = secondaryData[GENDER_INDEX];
+ break;
+ default:
+ // Data error. Assert-fail in debug mode, else return no gender.
+ assert false;
+ }
+
return result;
}
MicroProps micros = parent.processQuantity(quantity);
StandardPlural pluralForm = RoundingUtils.getPluralSafe(micros.rounder, rules, quantity);
micros.modOuter = modifiers.get(pluralForm);
+ micros.gender = this.gender;
return micros;
}
public static LongNameMultiplexer forMeasureUnits(ULocale locale,
List<MeasureUnit> units,
NumberFormatter.UnitWidth width,
+ String unitDisplayCase,
PluralRules rules,
MicroPropsGenerator parent) {
LongNameMultiplexer result = new LongNameMultiplexer(parent);
result.fMeasureUnits.add(unit);
if (unit.getComplexity() == MeasureUnit.Complexity.MIXED) {
MixedUnitLongNameHandler mlnh = MixedUnitLongNameHandler
- .forMeasureUnit(locale, unit, width, rules, null);
+ .forMeasureUnit(locale, unit, width, unitDisplayCase, rules, null);
result.fHandlers.add(mlnh);
} else {
- LongNameHandler lnh = LongNameHandler.forMeasureUnit(locale, unit, width, rules, null);
+ LongNameHandler lnh = LongNameHandler.forMeasureUnit(locale, unit, width, unitDisplayCase, rules, null);
result.fHandlers.add(lnh);
}
}
// one of the units provided to the factory function.
@Override
public MicroProps processQuantity(DecimalQuantity quantity) {
- // We call parent->processQuantity() from the Multiplexer, instead of
+ // 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);
public IntegerWidth integerWidth;
public Object symbols;
public UnitWidth unitWidth;
+ public String unitDisplayCase;
public SignDisplay sign;
public DecimalSeparatorDisplay decimal;
public Scale scale;
symbols = fallback.symbols;
if (unitWidth == null)
unitWidth = fallback.unitWidth;
+ if (unitDisplayCase == null)
+ unitDisplayCase = fallback.unitDisplayCase;
if (sign == null)
sign = fallback.sign;
if (decimal == null)
integerWidth,
symbols,
unitWidth,
+ unitDisplayCase,
sign,
decimal,
affixProvider,
&& Objects.equals(integerWidth, other.integerWidth)
&& Objects.equals(symbols, other.symbols)
&& Objects.equals(unitWidth, other.unitWidth)
+ && Objects.equals(unitDisplayCase, other.unitDisplayCase)
&& Objects.equals(sign, other.sign)
&& Objects.equals(decimal, other.decimal)
&& Objects.equals(affixProvider, other.affixProvider)
public Precision rounder;
public Grouper grouping;
public boolean useCurrency;
+ public String gender;
// Internal fields:
private final boolean immutable;
* @param mixedUnit The mixed measure unit to construct a
* MixedUnitLongNameHandler for.
* @param width Specifies the desired unit rendering.
+ * @param unitDisplayName
* @param rules PluralRules instance.
* @param parent MicroPropsGenerator instance.
*/
- public static MixedUnitLongNameHandler forMeasureUnit(ULocale locale, MeasureUnit mixedUnit,
- NumberFormatter.UnitWidth width, PluralRules rules,
+ public static MixedUnitLongNameHandler forMeasureUnit(ULocale locale,
+ MeasureUnit mixedUnit,
+ NumberFormatter.UnitWidth width,
+ String unitDisplayName,
+ PluralRules rules,
MicroPropsGenerator parent) {
assert (mixedUnit.getComplexity() == MeasureUnit.Complexity.MIXED);
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);
+ LongNameHandler.getMeasureData(locale, individualUnits.get(i), width, unitDisplayName, unitData);
result.fMixedUnitData.add(unitData);
}
package com.ibm.icu.impl.number;
import java.math.BigDecimal;
-import java.util.ArrayList;
import java.util.List;
import com.ibm.icu.impl.units.ComplexUnitsConverter;
import com.ibm.icu.impl.units.MeasureUnitImpl;
import com.ibm.icu.impl.units.UnitsRouter;
-import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
final DecimalQuantity fq;
final MeasureUnit outputUnit;
- FormattedNumber(FormattedStringBuilder nsb, DecimalQuantity fq, MeasureUnit outputUnit) {
+ // Grammatical gender of the formatted result.
+ final String gender;
+
+ FormattedNumber(FormattedStringBuilder nsb, DecimalQuantity fq, MeasureUnit outputUnit, String gender) {
this.string = nsb;
this.fq = fq;
this.outputUnit = outputUnit;
+ this.gender = gender;
}
/**
return this.outputUnit;
}
+ /**
+ * The gender of the formatted output.
+ *
+ * @internal ICU 69 technology preview
+ * @deprecated This API is for technology preview only.
+ */
+ public String getGender() {
+ return this.gender;
+ }
+
/**
* @internal
* @deprecated This API is ICU internal only.
MeasureUnit unit = input.getUnit();
FormattedStringBuilder string = new FormattedStringBuilder();
MicroProps micros = formatImpl(fq, unit, string);
- return new FormattedNumber(string, fq, micros.outputUnit);
+ return new FormattedNumber(string, fq, micros.outputUnit, micros.gender);
}
/**
private FormattedNumber format(DecimalQuantity fq) {
FormattedStringBuilder string = new FormattedStringBuilder();
MicroProps micros = formatImpl(fq, string);
- return new FormattedNumber(string, fq, micros.outputUnit);
+ return new FormattedNumber(string, fq, micros.outputUnit, micros.gender);
}
/**
MacroProps macros,
DecimalQuantity inValue,
FormattedStringBuilder outString) {
- MicroProps micros = preProcessUnsafe(macros, inValue);
- int length = writeNumber(micros, inValue, outString, 0);
- writeAffixes(micros, outString, 0, length);
- return micros;
+ MicroProps result = preProcessUnsafe(macros, inValue);
+ int length = writeNumber(result, inValue, outString, 0);
+ writeAffixes(result, outString, 0, length);
+ return result;
}
/**
* Evaluates the "safe" MicroPropsGenerator created by "fromMacros".
*/
public MicroProps format(DecimalQuantity inValue, FormattedStringBuilder outString) {
- MicroProps micros = preProcess(inValue);
- int length = writeNumber(micros, inValue, outString, 0);
- writeAffixes(micros, outString, 0, length);
- return micros;
+ MicroProps result = preProcess(inValue);
+ int length = writeNumber(result, inValue, outString, 0);
+ writeAffixes(result, outString, 0, length);
+ return result;
}
/**
}
micros.nsName = ns.getName();
+ // Default gender: none.
+ micros.gender = "";
+
// Resolve the symbols. Do this here because currency may need to customize them.
if (macros.symbols instanceof DecimalFormatSymbols) {
micros.symbols = (DecimalFormatSymbols) macros.symbols;
// Outer modifier (CLDR units and currency long names)
if (isCldrUnit) {
+ String unitDisplayCase = null;
+ if (macros.unitDisplayCase != null) {
+ unitDisplayCase = macros.unitDisplayCase;
+ }
if (rules == null) {
// Lazily create PluralRules
rules = PluralRules.forLocale(macros.loc);
macros.loc,
usagePrefsHandler.getOutputUnits(),
unitWidth,
+ unitDisplayCase,
pluralRules,
chain);
} else if (isMixedUnit) {
macros.loc,
macros.unit,
unitWidth,
+ unitDisplayCase,
pluralRules,
chain);
} else {
if (macros.perUnit != null) {
unit = unit.product(macros.perUnit.reciprocal());
}
- chain = LongNameHandler.forMeasureUnit(macros.loc, unit, unitWidth, pluralRules, chain);
+ chain = LongNameHandler.forMeasureUnit(
+ macros.loc,
+ unit,
+ unitWidth,
+ unitDisplayCase,
+ pluralRules,
+ chain);
}
} else if (isCurrency && unitWidth == UnitWidth.FULL_NAME) {
if (rules == null) {
static final int KEY_THRESHOLD = 14;
static final int KEY_PER_UNIT = 15;
static final int KEY_USAGE = 16;
- static final int KEY_MAX = 17;
+ static final int KEY_UNIT_DISPLAY_CASE = 17;
+ static final int KEY_MAX = 18;
private final NumberFormatterSettings<?> parent;
private final int key;
return create(KEY_USAGE, usage);
}
+ /**
+ * Specifies the desired case for a unit formatter's output (e.g.
+ * accusative, dative, genitive).
+ *
+ * @return The fluent chain
+ * @internal ICU 69 technology preview
+ * @deprecated This API is for technology preview only.
+ */
+ public T unitDisplayCase(String unitDisplayCase) {
+ return create(KEY_UNIT_DISPLAY_CASE, unitDisplayCase);
+ }
+
/**
* Internal method to set a starting macros.
*
case KEY_USAGE:
macros.usage = (String) current.value;
break;
+ case KEY_UNIT_DISPLAY_CASE:
+ macros.unitDisplayCase = (String) current.value;
+ break;
default:
throw new AssertionError("Unknown key: " + current.key);
}
throw new UnsupportedOperationException(
"Cannot generate number skeleton with custom padder");
}
+ if (macros.unitDisplayCase != null && !macros.unitDisplayCase.isEmpty()) {
+ throw new UnsupportedOperationException(
+ "Cannot generate number skeleton with custom unit display case");
+ }
if (macros.affixProvider != null) {
throw new UnsupportedOperationException(
"Cannot generate number skeleton with custom affix provider");
"123,12 CN¥");
}
+ public static class UnitInflectionTestCase {
+ public final String locale;
+ public final String unitDisplayCase;
+ public final double value;
+ public final String expected;
+
+ UnitInflectionTestCase(String locale, String unitDisplayCase, double value, String expected) {
+ this.locale = locale;
+ this.unitDisplayCase = unitDisplayCase;
+ this.value = value;
+ this.expected = expected;
+ }
+
+ public static void runUnitInflectionsTestCases(UnlocalizedNumberFormatter unf,
+ String skeleton,
+ String conciseSkeleton,
+ UnitInflectionTestCase cases[]) {
+ for (UnitInflectionTestCase t : cases) {
+ String skel;
+ String cSkel;
+ if (t.unitDisplayCase == null || t.unitDisplayCase.isEmpty()) {
+ unf = unf.unitDisplayCase("");
+ skel = skeleton;
+ cSkel = conciseSkeleton;
+ } else {
+ unf = unf.unitDisplayCase(t.unitDisplayCase);
+ skel = null;
+ cSkel = null;
+ }
+
+ assertFormatSingle(
+ "\"" + skeleton + "\", locale=\"" + t.locale + "\", case=\"" +
+ (t.unitDisplayCase != null ? t.unitDisplayCase : "")
+ + "\", value=" + t.value,
+ skel,
+ cSkel,
+ unf, new ULocale(t.locale),
+ t.value,
+ t.expected);
+ }
+ }
+ }
+
+ @Test
+ public void unitInflections() {
+ UnlocalizedNumberFormatter unf;
+ String skeleton;
+ String conciseSkeleton;
+
+ {
+ // Simple inflected form test - test case based on the example in CLDR's
+ // grammaticalFeatures.xml
+ unf = NumberFormatter.with().unit(NoUnit.PERCENT).unitWidth(UnitWidth.FULL_NAME);
+ skeleton = "percent unit-width-full-name";
+ conciseSkeleton = "% unit-width-full-name";
+ final UnitInflectionTestCase percentCases[] = {
+ new UnitInflectionTestCase("ru", null, 10, "10 процентов"), // many
+ new UnitInflectionTestCase("ru", "genitive", 10, "10 процентов"), // many
+ new UnitInflectionTestCase("ru", null, 33, "33 процента"), // few
+ new UnitInflectionTestCase("ru", "genitive", 33, "33 процентов"), // few
+ new UnitInflectionTestCase("ru", null, 1, "1 процент"), // one
+ new UnitInflectionTestCase("ru", "genitive", 1, "1 процента"), // one
+ };
+
+ for (UnitInflectionTestCase testCase :
+ percentCases) {
+ UnitInflectionTestCase.runUnitInflectionsTestCases(unf, skeleton, conciseSkeleton, percentCases);
+ }
+ }
+ {
+ // Testing "de" rules:
+ // <deriveComponent feature="case" structure="per" value0="compound" value1="accusative"/>
+ // <deriveComponent feature="plural" structure="per" value0="compound" value1="one"/>
+ //
+ // per-patterns use accusative, but happen to match nominative, so we're
+ // not testing value1 in the first rule above.
+
+ unf = NumberFormatter.with().unit(MeasureUnit.METER).unitWidth(UnitWidth.FULL_NAME);
+ skeleton = "unit/meter unit-width-full-name";
+ conciseSkeleton = "unit/meter unit-width-full-name";
+ final UnitInflectionTestCase meterCases[] = {
+ new UnitInflectionTestCase("de", null, 1, "1 Meter"),
+ new UnitInflectionTestCase("de", "genitive", 1, "1 Meters"),
+ new UnitInflectionTestCase("de", null, 2, "2 Meter"),
+ new UnitInflectionTestCase("de", "dative", 2, "2 Metern"),
+ };
+ UnitInflectionTestCase.runUnitInflectionsTestCases(unf, skeleton, conciseSkeleton, meterCases);
+
+ unf = NumberFormatter.with().unit(MeasureUnit.DAY).unitWidth(UnitWidth.FULL_NAME);
+ skeleton = "unit/day unit-width-full-name";
+ conciseSkeleton = "unit/day unit-width-full-name";
+ final UnitInflectionTestCase dayCases[] = {
+ new UnitInflectionTestCase("de", null, 1, "1 Tag"),
+ new UnitInflectionTestCase("de", "genitive", 1, "1 Tages"),
+ new UnitInflectionTestCase("de", null, 2, "2 Tage"),
+ new UnitInflectionTestCase("de", "dative", 2, "2 Tagen"),
+ };
+ UnitInflectionTestCase.runUnitInflectionsTestCases(unf, skeleton, conciseSkeleton, dayCases);
+
+ // Day has a perUnitPattern
+ unf = NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("meter-per-day"))
+ .unitWidth(UnitWidth.FULL_NAME);
+ skeleton = "unit/meter-per-day unit-width-full-name";
+ conciseSkeleton = "unit/meter-per-day unit-width-full-name";
+ final UnitInflectionTestCase meterPerDayCases[] = {
+ new UnitInflectionTestCase("de", null, 1, "1 Meter pro Tag"),
+ new UnitInflectionTestCase("de", "genitive", 1, "1 Meters pro Tag"),
+ new UnitInflectionTestCase("de", null, 2, "2 Meter pro Tag"),
+ new UnitInflectionTestCase("de", "dative", 2, "2 Metern pro Tag"),
+ // testing code path that falls back to "root" but does not inflect:
+ new UnitInflectionTestCase("af", null, 1, "1 meter per dag"),
+ new UnitInflectionTestCase("af", "dative", 1, "1 meter per dag"),
+ };
+ UnitInflectionTestCase.runUnitInflectionsTestCases(unf, skeleton, conciseSkeleton, meterPerDayCases);
+
+ // Decade does not have a perUnitPattern at this time (CLDR 39 / ICU
+ // 69), so we can test for the correct form of the per part:
+ unf = NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("parsec-per-decade"))
+ .unitWidth(UnitWidth.FULL_NAME);
+ skeleton = "unit/parsec-per-decade unit-width-full-name";
+ conciseSkeleton = "unit/parsec-per-decade unit-width-full-name";
+ // Fragile test cases: these cases will break when whitespace is more
+ // consistently applied.
+ final UnitInflectionTestCase parsecPerDecadeCases[] = {
+ new UnitInflectionTestCase("de", null, 1, "1\u00A0Parsec pro Jahrzehnt"),
+ new UnitInflectionTestCase("de", "genitive", 1, "1 Parsec pro Jahrzehnt"),
+ new UnitInflectionTestCase("de", null, 2, "2\u00A0Parsec pro Jahrzehnt"),
+ new UnitInflectionTestCase("de", "dative", 2, "2 Parsec pro Jahrzehnt"),
+ };
+ UnitInflectionTestCase.runUnitInflectionsTestCases(unf, skeleton, conciseSkeleton, parsecPerDecadeCases);
+ }
+ {
+ // Testing inflection of mixed units:
+ unf = NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("meter-and-centimeter"))
+ .unitWidth(UnitWidth.FULL_NAME);
+ skeleton = "unit/meter-and-centimeter unit-width-full-name";
+ conciseSkeleton = "unit/meter-and-centimeter unit-width-full-name";
+ final UnitInflectionTestCase meterPerDayCases[] = {
+ // TODO(CLDR-14502): check that these inflections are correct, and
+ // whether CLDR needs any rules for them (presumably CLDR spec
+ // should mention it, if it's a consistent rule):
+ new UnitInflectionTestCase("de", null, 1.01, "1 Meter, 1 Zentimeter"),
+ new UnitInflectionTestCase("de", "genitive", 1.01, "1 Meters, 1 Zentimeters"),
+ new UnitInflectionTestCase("de", "genitive", 1.1, "1 Meters, 10 Zentimeter"),
+ new UnitInflectionTestCase("de", "dative", 1.1, "1 Meter, 10 Zentimetern"),
+ new UnitInflectionTestCase("de", "dative", 2.1, "2 Metern, 10 Zentimetern"),
+ };
+ UnitInflectionTestCase.runUnitInflectionsTestCases(unf, skeleton, conciseSkeleton, meterPerDayCases);
+ }
+ // TODO: add a usage case that selects between preferences with different
+ // genders (e.g. year, month, day, hour).
+ // TODO: look at "↑↑↑" cases: check that inheritance is done right.
+
+ }
+
+ @Test
+ public void unitGender() {
+ class TestCase {
+ public String locale;
+ public String unitIdentifier;
+ public String expectedGender;
+
+ public TestCase(String locale, String unitIdentifier, String expectedGender) {
+ this.locale = locale;
+ this.unitIdentifier = unitIdentifier;
+ this.expectedGender = expectedGender;
+ }
+ }
+
+ TestCase cases[] = {
+ new TestCase("de", "meter", "masculine"),
+ new TestCase("de", "minute", "feminine"),
+ new TestCase("de", "hour", "feminine"),
+ new TestCase("de", "day", "masculine"),
+ new TestCase("de", "year", "neuter"),
+ new TestCase("fr", "minute", "feminine"),
+ new TestCase("fr", "hour", "feminine"),
+ new TestCase("fr", "day", "masculine"),
+ // grammaticalFeatures deriveCompound "per" rule:
+ new TestCase("de", "meter-per-hour", "masculine"),
+ new TestCase("af", "meter-per-hour", null),
+ // TODO(ICU-21494): determine whether list genders behave as follows,
+ // and implement proper getListGender support (covering more than just
+ // two genders):
+ // // gender rule for lists of people: de "neutral", fr "maleTaints"
+ // new TestCase("de", "day-and-hour-and-minute", "neuter"),
+ // new TestCase("de", "hour-and-minute", "feminine"),
+ // new TestCase("fr", "day-and-hour-and-minute", "masculine"),
+ // new TestCase("fr", "hour-and-minute", "feminine"),
+ };
+
+ LocalizedNumberFormatter formatter;
+ FormattedNumber fn;
+ for (TestCase t : cases) {
+ // TODO(icu-units#140): make this work for more than just UnitWidth.FULL_NAME
+ formatter = NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier(t.unitIdentifier))
+ .unitWidth(UnitWidth.FULL_NAME)
+ .locale(new ULocale(t.locale));
+ fn = formatter.format(1.1);
+ assertEquals("Testing gender, unit: " + t.unitIdentifier +
+ ", locale: " + t.locale,
+ t.expectedGender, fn.getGender());
+ }
+
+ // Make sure getGender does not return garbage for genderless languages
+ formatter = NumberFormatter.with().locale(ULocale.ENGLISH);
+ fn = formatter.format(1.1);
+ assertEquals("getGender for a genderless language", "", fn.getGender());
+ }
+
@Test
public void unitPercent() {
assertFormatDescending(