]> granicus.if.org Git - icu/commitdiff
ICU-21123 Support unit inflections in ICU4J
authoryounies <younies@chromium.org>
Wed, 24 Feb 2021 14:03:05 +0000 (14:03 +0000)
committerYounies Mahmoud <younies@chromium.org>
Wed, 24 Feb 2021 16:27:09 +0000 (17:27 +0100)
See #1590

12 files changed:
icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameMultiplexer.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/MacroProps.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/MicroProps.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/MixedUnitLongNameHandler.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/UsagePrefsHandler.java
icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumber.java
icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java
icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java
icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java
icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java

index 906a367c19e951e7d7b03873b42211944929ce65..d3e6317dcf3731f16e0fb043f2e13cdb5b01b0b7 100644 (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;
     }
 
index 044a48f5397bc6582a90502a14ddedfa6948fbcd..0ba961d29529c9e7c9fbc9618d0d91a034258a93 100644 (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);
index 5f373b0ba2b9047394a2e2e2f1c0cf22793d661f..f9b957143336b6eb8e2a44caa9c5e3b41570846d 100644 (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)
index dd16a5883f688c300583660af6fbd65097811a75..3b2b76d7f019c9931452b947a7c4cd5e9e8fd64c 100644 (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;
index 8487524bcb3949ff89ccf4cddf3808cdd822795d..8b9f159e25a623ed1e0f9820f1ca3655e440d9e3 100644 (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);
         }
 
index b2c7ec8fa019731991846399f93bf13141ebc991..6de6152d08ca80dd9a30cee3248afd6d59111e4e 100644 (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;
 
index 34069a7800835c0b32ca00af4b507ea47309c4cc..82e32541d4cf542363eeb9c593894e81090a58a3 100644 (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.
index e6f714de5834ad805d5ad396f54ef50a685887c1..c34d889207f8297afd3ceaaf147f8a11a4960aee 100644 (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);
     }
 
     /**
index 3954c4f727b849ae2270ed68bb9845a3ad3e0513..458001bba3ba9a5e3235d42e2e39f44f2af466ee 100644 (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) {
index 33af7e029e0fbd2204325f037d90b10de1f8cbb2..fed476b2a2635867362107fcc77bbdf2721189be 100644 (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);
             }
index ab4047757115fa5ac493bde71f1292d5504b6231..25411aaf84c48f762bddc1422952503e82cf7013 100644 (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");
index 51a879419d665c8db910cc322ed2d8668744032c..519958e04a9a766f3beb2f710f81a8d88aba0929 100644 (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(