ICU-21123 Support unit inflections in ICU4J

See #1590
This commit is contained in:
younies 2021-02-24 14:03:05 +00:00 committed by Younies Mahmoud
parent aebe91cdda
commit f6361ebf76
12 changed files with 464 additions and 35 deletions

View file

@ -24,19 +24,26 @@ import com.ibm.icu.util.MeasureUnit;
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();
}
@ -71,14 +78,16 @@ public class LongNameHandler
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;
}
@ -86,11 +95,11 @@ public class LongNameHandler
}
// 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;
@ -98,6 +107,7 @@ public class LongNameHandler
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) {
@ -115,6 +125,29 @@ public class LongNameHandler
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) {
@ -161,6 +194,28 @@ public class LongNameHandler
}
}
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 ///
////////////////////////
@ -168,6 +223,8 @@ public class LongNameHandler
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,
@ -180,7 +237,7 @@ public class LongNameHandler
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];
}
@ -207,6 +264,7 @@ public class LongNameHandler
* @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.
*/
@ -214,6 +272,7 @@ public class LongNameHandler
ULocale locale,
MeasureUnit unit,
UnitWidth width,
String unitDisplayCase,
PluralRules rules,
MicroPropsGenerator parent) {
if (unit.getType() == null) {
@ -240,24 +299,91 @@ public class LongNameHandler
}
}
}
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) {
@ -267,10 +393,20 @@ public class LongNameHandler
"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];
@ -286,14 +422,34 @@ public class LongNameHandler
// 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;
}
@ -336,6 +492,7 @@ public class LongNameHandler
MicroProps micros = parent.processQuantity(quantity);
StandardPlural pluralForm = RoundingUtils.getPluralSafe(micros.rounder, rules, quantity);
micros.modOuter = modifiers.get(pluralForm);
micros.gender = this.gender;
return micros;
}

View file

@ -45,6 +45,7 @@ public class LongNameMultiplexer implements MicroPropsGenerator {
public static LongNameMultiplexer forMeasureUnits(ULocale locale,
List<MeasureUnit> units,
NumberFormatter.UnitWidth width,
String unitDisplayCase,
PluralRules rules,
MicroPropsGenerator parent) {
LongNameMultiplexer result = new LongNameMultiplexer(parent);
@ -60,10 +61,10 @@ public class LongNameMultiplexer implements MicroPropsGenerator {
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);
}
}
@ -75,7 +76,7 @@ public class LongNameMultiplexer implements MicroPropsGenerator {
// 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);

View file

@ -27,6 +27,7 @@ public class MacroProps implements Cloneable {
public IntegerWidth integerWidth;
public Object symbols;
public UnitWidth unitWidth;
public String unitDisplayCase;
public SignDisplay sign;
public DecimalSeparatorDisplay decimal;
public Scale scale;
@ -63,6 +64,8 @@ public class MacroProps implements Cloneable {
symbols = fallback.symbols;
if (unitWidth == null)
unitWidth = fallback.unitWidth;
if (unitDisplayCase == null)
unitDisplayCase = fallback.unitDisplayCase;
if (sign == null)
sign = fallback.sign;
if (decimal == null)
@ -91,6 +94,7 @@ public class MacroProps implements Cloneable {
integerWidth,
symbols,
unitWidth,
unitDisplayCase,
sign,
decimal,
affixProvider,
@ -119,6 +123,7 @@ public class MacroProps implements Cloneable {
&& 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)

View file

@ -44,6 +44,7 @@ public class MicroProps implements Cloneable, MicroPropsGenerator {
public Precision rounder;
public Grouper grouping;
public boolean useCurrency;
public String gender;
// Internal fields:
private final boolean immutable;

View file

@ -51,11 +51,15 @@ public class MixedUnitLongNameHandler
* @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);
@ -66,7 +70,7 @@ public class MixedUnitLongNameHandler
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);
}

View file

@ -3,13 +3,11 @@
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;

View file

@ -28,10 +28,14 @@ public class FormattedNumber implements FormattedValue {
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;
}
/**
@ -133,6 +137,16 @@ public class FormattedNumber implements FormattedValue {
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.

View file

@ -102,7 +102,7 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
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);
}
/**
@ -127,7 +127,7 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
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);
}
/**

View file

@ -62,10 +62,10 @@ class NumberFormatterImpl {
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;
}
/**
@ -93,10 +93,10 @@ class NumberFormatterImpl {
* 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;
}
/**
@ -226,6 +226,9 @@ class NumberFormatterImpl {
}
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;
@ -375,6 +378,10 @@ class NumberFormatterImpl {
// 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);
@ -389,6 +396,7 @@ class NumberFormatterImpl {
macros.loc,
usagePrefsHandler.getOutputUnits(),
unitWidth,
unitDisplayCase,
pluralRules,
chain);
} else if (isMixedUnit) {
@ -396,6 +404,7 @@ class NumberFormatterImpl {
macros.loc,
macros.unit,
unitWidth,
unitDisplayCase,
pluralRules,
chain);
} else {
@ -403,7 +412,13 @@ class NumberFormatterImpl {
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) {

View file

@ -45,7 +45,8 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
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;
@ -548,6 +549,18 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
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.
*
@ -675,6 +688,9 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
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);
}

View file

@ -947,6 +947,10 @@ class NumberSkeletonImpl {
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");

View file

@ -1922,6 +1922,220 @@ public class NumberFormatterApiTest extends TestFmwk {
"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(