* found.
* @param joinerChar Iff the placeholder was at the beginning or end, joinerChar
* contains the space character (if any) that separated the placeholder from
- * the rest of the pattern. Otherwise, joinerChar is set to NUL.
+ * the rest of the pattern. Otherwise, joinerChar is set to NUL. Only one
+ * space character is considered.
*/
void extractCorePattern(const UnicodeString &pattern,
UnicodeString &coreUnit,
PlaceholderPosition &placeholderPosition,
UChar &joinerChar) {
joinerChar = 0;
+ int32_t len = pattern.length();
if (pattern.startsWith(u"{0}", 3)) {
placeholderPosition = PH_BEGINNING;
if (u_isJavaSpaceChar(pattern[3])) {
joinerChar = pattern[3];
- coreUnit.setTo(pattern, 4, pattern.length() - 4);
- // Expecting no double spaces
- U_ASSERT(!u_isJavaSpaceChar(pattern[4]));
+ coreUnit.setTo(pattern, 4, len - 4);
} else {
- coreUnit.setTo(pattern, 3, pattern.length() - 3);
+ coreUnit.setTo(pattern, 3, len - 3);
}
} else if (pattern.endsWith(u"{0}", 3)) {
placeholderPosition = PH_END;
- int32_t len = pattern.length();
if (u_isJavaSpaceChar(pattern[len - 4])) {
- coreUnit.setTo(pattern, 0, pattern.length() - 4);
+ coreUnit.setTo(pattern, 0, len - 4);
joinerChar = pattern[len - 4];
- // Expecting no double spaces
- U_ASSERT(!u_isJavaSpaceChar(pattern[len - 5]));
} else {
- coreUnit.setTo(pattern, 0, pattern.length() - 3);
+ coreUnit.setTo(pattern, 0, len - 3);
}
- } else if (pattern.indexOf(u"{0}", 0, 1, pattern.length() - 2) == -1) {
+ } else if (pattern.indexOf(u"{0}", 3, 1, len - 2) == -1) {
placeholderPosition = PH_NONE;
coreUnit = pattern;
} else {
key.append(subKey, status);
UErrorCode localStatus = status;
- ures_getAllItemsWithFallback(unitsBundle.getAlias(), key.data(), sink, status);
+ ures_getAllItemsWithFallback(unitsBundle.getAlias(), key.data(), sink, localStatus);
if (width == UNUM_UNIT_WIDTH_SHORT) {
status = localStatus;
return;
//
// 1. If the unitId is empty or invalid, fail
// 2. Put the unitId into normalized order
- //
- // We just need to check if it is a MeasureUnit this constructor handles:
- // this constructor does not handle mixed units
- U_ASSERT(uprv_strcmp(unitRef.getType(), "") != 0 ||
- unitRef.getComplexity(status) != UMEASURE_UNIT_MIXED);
U_ASSERT(fillIn != nullptr);
if (uprv_strcmp(unitRef.getType(), "") != 0) {
// fillIn->parent = parent;
// return;
} else {
+ // Check if it is a MeasureUnit this constructor handles: this
+ // constructor does not handle mixed units
+ U_ASSERT(unitRef.getComplexity(status) != UMEASURE_UNIT_MIXED);
forArbitraryUnit(loc, unitRef, width, unitDisplayCase, fillIn, status);
fillIn->rules = rules;
fillIn->parent = parent;
getInflectedMeasureData(dimensionalityKey.toStringPiece(), loc, width, gender,
singleCaseVariant, dimensionalityPrefixPatterns, status);
if (U_FAILURE(status)) {
- // At the time of writing, only power2 and power3 are supported.
+ // At the time of writing, only pow2 and pow3 are supported.
// Attempting to format other powers results in a
// U_RESOURCE_TYPE_MISMATCH. We convert the error if we
// understand it:
const MicroPropsGenerator *parent,
MixedUnitLongNameHandler *fillIn,
UErrorCode &status) {
+ U_ASSERT(mixedUnit.getComplexity(status) == UMEASURE_UNIT_MIXED);
U_ASSERT(fillIn != nullptr);
if (U_FAILURE(status)) {
return;
MeasureUnitImpl temp;
const MeasureUnitImpl &impl = MeasureUnitImpl::forMeasureUnit(mixedUnit, temp, status);
+ // Defensive, for production code:
if (impl.complexity != UMEASURE_UNIT_MIXED) {
// Should be using the normal LongNameHandler
status = U_UNSUPPORTED_ERROR;
* @param loc The desired locale.
* @param unitRef The measure unit to construct a LongNameHandler for.
* @param width Specifies the desired unit rendering.
- * @param unitDisplayCase Specifies the desired grammatical case. The empty
- * string and "nominative" are treated the same. For other cases,
- * strings for the requested case are used if found. (For any missing
- * case-specific data, we fall back to nominative.)
+ * @param unitDisplayCase Specifies the desired grammatical case. If the
+ * specified case is not found, we fall back to nominative or no-case.
* @param rules Does not take ownership.
* @param parent Does not take ownership.
* @param fillIn Required.
* @param mixedUnit The mixed measure unit to construct a
* MixedUnitLongNameHandler for.
* @param width Specifies the desired unit rendering.
- * @param unitDisplayCase Specifies the desired grammatical case. The empty
- * string and "nominative" are treated the same. For other cases,
- * strings for the requested case are used if found. (For any missing
- * case-specific data, we fall back to nominative.)
+ * @param unitDisplayCase Specifies the desired grammatical case. If the
+ * specified case is not found, we fall back to nominative or no-case.
* @param rules Does not take ownership.
* @param parent Does not take ownership.
* @param fillIn Required.
};
UnicodeString skelString = UnicodeString("unit/") + t.unitIdentifier + u" " + skeleton;
const UChar *skel;
- const UChar *cSkel;
if (t.unitDisplayCase == nullptr || t.unitDisplayCase[0] == 0) {
unf = unf.unit(mu).unitDisplayCase("");
skel = skelString.getTerminatedBuffer();
- cSkel = skelString.getTerminatedBuffer();
} else {
unf = unf.unit(mu).unitDisplayCase(t.unitDisplayCase);
// No skeleton support for unitDisplayCase yet.
skel = nullptr;
- cSkel = nullptr;
}
assertFormatSingle((UnicodeString("Unit: \"") + t.unitIdentifier + ("\", \"") + skeleton +
u"\", locale=\"" + t.locale + u"\", case=\"" +
(t.unitDisplayCase ? t.unitDisplayCase : "") + u"\", value=" + t.value)
.getTerminatedBuffer(),
- skel, cSkel, unf, Locale(t.locale), t.value, t.expected);
+ skel, skel, unf, Locale(t.locale), t.value, t.expected);
status.assertSuccess();
}
}
// General testing of inflection rules
unf = NumberFormatter::with().unitWidth(UNUM_UNIT_WIDTH_FULL_NAME);
skeleton = u"unit-width-full-name";
- const UnitInflectionTestCase meterCases[] = {
+ const UnitInflectionTestCase testCases[] = {
// Check up on the basic values that the compound patterns below are
// derived from:
{"meter", "de", nullptr, 1, u"1 Meter"},
{"square-decimeter-square-second", "fr", nullptr, 1, u"1\u00A0décimètre carré-seconde carrée"},
{"square-decimeter-square-second", "fr", nullptr, 2, u"2\u00A0décimètres carrés-secondes carrées"},
};
- runUnitInflectionsTestCases(unf, skeleton, meterCases, UPRV_LENGTHOF(meterCases), status);
+ runUnitInflectionsTestCases(unf, skeleton, testCases, UPRV_LENGTHOF(testCases), status);
}
{
// Testing inflection of mixed units:
unf = NumberFormatter::with().unitWidth(UNUM_UNIT_WIDTH_FULL_NAME);
skeleton = u"unit-width-full-name";
- const UnitInflectionTestCase meterPerDayCases[] = {
+ const UnitInflectionTestCase testCases[] = {
{"meter", "de", nullptr, 1, u"1 Meter"},
{"meter", "de", "genitive", 1, u"1 Meters"},
{"meter", "de", "dative", 2, u"2 Metern"},
{"meter-and-centimeter", "de", "dative", 1.1, u"1 Meter, 10 Zentimetern"},
{"meter-and-centimeter", "de", "dative", 2.1, u"2 Metern, 10 Zentimetern"},
};
- runUnitInflectionsTestCases(unf, skeleton, meterPerDayCases, UPRV_LENGTHOF(meterPerDayCases),
+ runUnitInflectionsTestCases(unf, skeleton, testCases, UPRV_LENGTHOF(testCases),
status);
}
// TODO: add a usage case that selects between preferences with different
return s.substring(start, limit);
}
+ /**
+ * @return s except with leading and trailing SpaceChar characters removed.
+ */
+ public static String trimSpaceChar(String s) {
+ if (s.length() == 0 ||
+ (!Character.isSpaceChar(s.charAt(0)) && !Character.isSpaceChar(s.charAt(s.length() - 1)))) {
+ return s;
+ }
+ int start = 0;
+ int limit = s.length();
+ while (start < limit && Character.isSpaceChar(s.charAt(start))) {
+ ++start;
+ }
+ if (start < limit) {
+ // There is non-SpaceChar at start; we will not move limit below that,
+ // so we need not test start<limit in the loop.
+ while (isWhiteSpace(s.charAt(limit - 1))) {
+ --limit;
+ }
+ }
+ return s.substring(start, limit);
+ }
+
/**
* Tests whether the CharSequence contains a "pattern identifier", that is,
* whether it contains only non-Pattern_White_Space, non-Pattern_Syntax characters.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.impl.number;
+import java.util.ArrayList;
import java.util.EnumMap;
import java.util.Map;
import java.util.MissingResourceException;
import com.ibm.icu.impl.CurrencyData;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
+import com.ibm.icu.impl.PatternProps;
import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.impl.number.Modifier.Signum;
import com.ibm.icu.impl.units.MeasureUnitImpl;
import com.ibm.icu.impl.units.SingleUnitImpl;
+import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.number.NumberFormatter.UnitWidth;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.ICUException;
import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.MeasureUnit.Complexity;
+import com.ibm.icu.util.MeasureUnit.MeasurePrefix;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
private static final int GENDER_INDEX = StandardPlural.COUNT + i++;
static final int ARRAY_LENGTH = StandardPlural.COUNT + i++;
+ // Returns the array index that corresponds to the given pluralKeyword.
private static int getIndex(String pluralKeyword) {
// pluralKeyword can also be "dnam", "per" or "gender"
if (pluralKeyword.equals("dnam")) {
return result;
}
+ private enum PlaceholderPosition { NONE, BEGINNING, MIDDLE, END }
+
+ private static class ExtractCorePatternResult {
+ String coreUnit;
+ PlaceholderPosition placeholderPosition;
+ char joinerChar;
+ }
+
+ /**
+ * Returns three outputs extracted from pattern.
+ *
+ * @param coreUnit is extracted as per Extract(...) in the spec:
+ * https://unicode.org/reports/tr35/tr35-general.html#compound-units
+ * @param PlaceholderPosition indicates where in the string the placeholder
+ * was found.
+ * @param joinerChar Iff the placeholder was at the beginning or end,
+ * joinerChar contains the space character (if any) that separated the
+ * placeholder from the rest of the pattern. Otherwise, joinerChar is set
+ * to NUL. Only one space character is considered.
+ */
+ private static ExtractCorePatternResult extractCorePattern(String pattern) {
+ ExtractCorePatternResult result = new ExtractCorePatternResult();
+ result.joinerChar = 0;
+ int len = pattern.length();
+ if (pattern.startsWith("{0}")) {
+ result.placeholderPosition = PlaceholderPosition.BEGINNING;
+ if (len > 3 && Character.isSpaceChar(pattern.charAt(3))) {
+ result.joinerChar = pattern.charAt(3);
+ result.coreUnit = pattern.substring(4);
+ } else {
+ result.coreUnit = pattern.substring(3);
+ }
+ } else if (pattern.endsWith("{0}")) {
+ result.placeholderPosition = PlaceholderPosition.END;
+ if (Character.isSpaceChar(pattern.charAt(len - 4))) {
+ result.coreUnit = pattern.substring(0, len - 4);
+ result.joinerChar = pattern.charAt(len - 4);
+ } else {
+ result.coreUnit = pattern.substring(0, len - 3);
+ }
+ } else if (pattern.indexOf("{0}", 1) == -1) {
+ result.placeholderPosition = PlaceholderPosition.NONE;
+ result.coreUnit = pattern;
+ } else {
+ result.placeholderPosition = PlaceholderPosition.MIDDLE;
+ result.coreUnit = pattern;
+ }
+ return result;
+ }
+
//////////////////////////
/// BEGIN DATA LOADING ///
//////////////////////////
+ // Gets the gender of a built-in unit: unit must be a built-in. Returns an empty
+ // string both in case of unknown gender and in case of unknown unit.
+ private static String getGenderForBuiltin(ULocale locale, MeasureUnit builtinUnit) {
+ ICUResourceBundle unitsBundle = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale);
+
+ StringBuilder key = new StringBuilder();
+ key.append("units/");
+ key.append(builtinUnit.getType());
+ key.append("/");
+
+ // Map duration-year-person, duration-week-person, etc. to duration-year, duration-week, ...
+ // TODO(ICU-20400): Get duration-*-person data properly with aliases.
+ if (builtinUnit.getSubtype() != null && builtinUnit.getSubtype().endsWith("-person")) {
+ key.append(builtinUnit.getSubtype(), 0, builtinUnit.getSubtype().length() - 7);
+ } else {
+ key.append(builtinUnit.getSubtype());
+ }
+ key.append("/gender");
+
+ try {
+ ICUResourceBundle stackBundle =
+ (ICUResourceBundle)unitsBundle.getWithFallback(key.toString());
+ return stackBundle.getString();
+ } catch (Exception e) {
+ // TODO(icu-units#28): "$unitRes/gender" does not exist. Do we want to
+ // check whether the parent "$unitRes" exists? Then we could return
+ // U_MISSING_RESOURCE_ERROR for incorrect usage (e.g. builtinUnit not
+ // being a builtin).
+ return "";
+ }
+ }
+
+ // Loads data from a resource tree with paths matching
+ // $key/$pluralForm/$gender/$case, with lateral inheritance for missing cases
+ // and genders.
+ //
+ // An InflectedPluralSink is configured to load data for a specific gender and
+ // case. It loads all plural forms, because selection between plural forms is
+ // dependent upon the value being formatted.
+ //
+ // TODO(icu-units#138): Conceptually similar to PluralTableSink, however the
+ // tree structures are different. After homogenizing the structures, we may be
+ // able to unify the two classes.
+ //
+ // TODO: Spec violation: expects presence of "count" - does not fallback to an
+ // absent "count"! If this fallback were added, getCompoundValue could be
+ // superseded?
+ private static final class InflectedPluralSink extends UResource.Sink {
+ // NOTE: outArray MUST have a length of at least ARRAY_LENGTH. No bounds
+ // checking is performed.
+ public InflectedPluralSink(String gender, String caseVariant, String[] outArray) {
+ this.gender = gender;
+ this.caseVariant = caseVariant;
+ this.outArray = outArray;
+ for (int i = 0; i < ARRAY_LENGTH; i++) {
+ outArray[i] = null;
+ }
+ }
+
+ // See ResourceSink::put().
+ @Override
+ public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
+ UResource.Table pluralsTable = value.getTable();
+ for (int i = 0; pluralsTable.getKeyAndValue(i, key, value); ++i) {
+ String keyString = key.toString();
+ int pluralIndex = getIndex(keyString);
+ if (outArray[pluralIndex] != null) {
+ // We already have a pattern
+ continue;
+ }
+ UResource.Table genderTable = value.getTable();
+ if (loadForPluralForm(genderTable, value)) {
+ outArray[pluralIndex] = value.getString();
+ }
+ }
+ }
+
+ // Tries to load data for the configured gender from `genderTable`. The
+ // returned data will be for the configured gender if found, falling
+ // back to "neuter" and no-gender. If none of those are found, null is
+ // returned.
+ private boolean loadForPluralForm(UResource.Table genderTable, UResource.Value value) {
+ if (gender != null && !gender.isEmpty()) {
+ if (loadForGender(genderTable, gender, value)) {
+ return true;
+ }
+ if (gender != "neuter") {
+ if (loadForGender(genderTable, "neuter", value)) {
+ return true;
+ }
+ }
+ }
+ if (loadForGender(genderTable, "_", value)) {
+ return true;
+ }
+ return false;
+ }
+
+ // Tries to load data for the given gender from `genderTable`. Returns true
+ // if found, returning the data in `value`. The returned data will be for
+ // the configured case if found, falling back to "nominative" and no-case if
+ // not.
+ private boolean
+ loadForGender(UResource.Table genderTable, String genderVal, UResource.Value value) {
+ if (!genderTable.findValue(genderVal, value)) {
+ return false;
+ }
+ UResource.Table caseTable = value.getTable();
+ if (caseVariant != null && !caseVariant.isEmpty()) {
+ if (loadForCase(caseTable, caseVariant, value)) {
+ return true;
+ }
+ if (caseVariant != "nominative") {
+ if (loadForCase(caseTable, "nominative", value)) {
+ return true;
+ }
+ }
+ }
+ if (loadForCase(caseTable, "_", value)) {
+ return true;
+ }
+ return false;
+ }
+
+ // Tries to load data for the given case from `caseTable`. Returns null
+ // if not found.
+ private boolean loadForCase(UResource.Table caseTable, String caseValue, UResource.Value value) {
+ if (!caseTable.findValue(caseValue, value)) {
+ return false;
+ }
+ return true;
+ }
+
+ String gender;
+ String caseVariant;
+ String[] outArray;
+ }
+
+ static void getInflectedMeasureData(String subKey,
+ ULocale locale,
+ UnitWidth width,
+ String gender,
+ String caseVariant,
+ String[] outArray) {
+ InflectedPluralSink sink = new InflectedPluralSink(gender, caseVariant, outArray);
+ ICUResourceBundle unitsBundle =
+ (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale);
+
+ StringBuilder key = new StringBuilder();
+ key.append("units");
+ if (width == UnitWidth.NARROW) {
+ key.append("Narrow");
+ } else if (width == UnitWidth.SHORT) {
+ key.append("Short");
+ }
+ key.append("/");
+ key.append(subKey);
+
+ try {
+ unitsBundle.getAllItemsWithFallback(key.toString(), sink);
+ if (width == UnitWidth.SHORT) {
+ return;
+ }
+ } catch (Exception e) {
+ // Continue: fall back to short
+ }
+
+ // TODO(ICU-13353): The ICU4J fallback mechanism works differently:
+ // investigate? Without this code, unit tests do fail:
+ key.setLength(0);
+ key.append("unitsShort/");
+ key.append(subKey);
+ unitsBundle.getAllItemsWithFallback(key.toString(), sink);
+ }
+
private static final class PluralTableSink extends UResource.Sink {
String[] outArray;
+ // NOTE: outArray MUST have at least ARRAY_LENGTH entries. No bounds
+ // checking is performed.
public PluralTableSink(String[] outArray) {
this.outArray = outArray;
}
caseKey.append(unitDisplayCase);
try {
+ // TODO(icu-units#138): our fallback logic is not spec-compliant:
+ // lateral fallback should happen before locale fallback. Switch to
+ // getInflectedMeasureData after homogenizing data format? Find a unit
+ // test case that demonstrates the incorrect fallback logic (via
+ // regional variant of an inflected language?)
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
}
}
- private static String getPerUnitFormat(ULocale locale, UnitWidth width) {
+ private static String getCompoundValue(String compoundKey, ULocale locale, UnitWidth width) {
ICUResourceBundle resource;
resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME,
locale);
} else if (width == UnitWidth.SHORT) {
key.append("Short");
}
- key.append("/compound/per");
+ key.append("/compound/");
+ key.append(compoundKey);
try {
return resource.getStringWithFallback(key.toString());
- } catch (MissingResourceException e) {
- throw new IllegalArgumentException(
- "Could not find x-per-y format for " + locale + ", width " + width);
+ } catch (Exception e) {
+ if (width == UnitWidth.SHORT) {
+ return "";
+ }
+ }
+
+ // TODO(icu-units#28): this code is not exercised by unit tests yet.
+ // Also, what's the fallback mechanism mentioned in ICU-13353?
+ key.setLength(0);
+ key.append("unitsShort/compound/");
+ key.append(compoundKey);
+ try {
+ return resource.getStringWithFallback(key.toString());
+ } catch (Exception e) {
+ return "";
+ }
+ }
+
+ /**
+ * Loads and applies deriveComponent rules from CLDR's
+ * grammaticalFeatures.xml.
+ * <p>
+ * Consider a deriveComponent rule that looks like this:
+ * <pre>
+ * <deriveComponent feature="case" structure="per" value0="compound" value1="nominative"/>
+ * </pre>
+ * Instantiating an instance as follows:
+ * <pre>
+ * DerivedComponents d(loc, "case", "per");
+ * </pre>
+ * <p>
+ * Applying the rule in the XML element above, <code>d.value0("foo")</code>
+ * will be "foo", and <code>d.value1("foo")</code> will be "nominative".
+ * <p>
+ * In case of any kind of failure, value0() and value1() will simply return
+ * "".
+ */
+ private static class DerivedComponents {
+ /**
+ * Constructor.
+ */
+ DerivedComponents(ULocale locale, String feature, String structure) {
+ try {
+ 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 = null;
+ } else {
+ this.value0 = value;
+ }
+
+ value = stackBundle.getString(1);
+ if (value.compareTo("compound") == 0) {
+ this.value1 = null;
+ } else {
+ this.value1 = value;
+ }
+ } catch (Exception e) {
+ // Fall back to uninflected.
+ }
}
+
+ String value0(String compoundValue) {
+ return (this.value0 != null) ? this.value0 : compoundValue;
+ }
+
+ String value1(String compoundValue) {
+ return (this.value1 != null) ? this.value1 : compoundValue;
+ }
+
+ private String value0 = "", value1 = "";
}
+ // TODO(icu-units#28): test somehow? Associate with an ICU ticket for adding
+ // testsuite support for testing with synthetic data?
+ /**
+ * Loads and returns the value in rules that look like these:
+ *
+ * <deriveCompound feature="gender" structure="per" value="0"/>
+ * <deriveCompound feature="gender" structure="times" value="1"/>
+ *
+ * Currently a fake example, but spec compliant:
+ * <deriveCompound feature="gender" structure="power" value="feminine"/>
+ *
+ * NOTE: If U_FAILURE(status), returns an empty string.
+ */
private static String getDeriveCompoundRule(ULocale locale, String feature, String structure) {
ICUResourceBundle derivationsBundle =
(ICUResourceBundle) UResourceBundle
return stackBundle.getString(structure);
}
+ // Returns the gender string for structures following these rules:
+ //
+ // <deriveCompound feature="gender" structure="per" value="0"/>
+ // <deriveCompound feature="gender" structure="times" value="1"/>
+ //
+ // Fake example:
+ // <deriveCompound feature="gender" structure="power" value="feminine"/>
+ //
+ // data0 and data1 should be pattern arrays (UnicodeString[ARRAY_SIZE]) that
+ // correspond to value="0" and value="1".
+ //
+ // Pass a null to data1 if the structure has no concept of value="1" (e.g.
+ // "prefix" doesn't).
+ private static String
+ getDerivedGender(ULocale locale, String structure, String[] data0, String[] data1) {
+ String val = getDeriveCompoundRule(locale, "gender", structure);
+ if (val.length() == 1) {
+ switch (val.charAt(0)) {
+ case '0':
+ return data0[GENDER_INDEX];
+ case '1':
+ if (data1 == null) {
+ return null;
+ }
+ return data1[GENDER_INDEX];
+ }
+ }
+ return val;
+ }
+
////////////////////////
/// END DATA LOADING ///
////////////////////////
StandardPlural.class);
LongNameHandler result = new LongNameHandler(modifiers, rules, parent);
result.simpleFormatsToModifiers(simpleFormats, NumberFormat.Field.CURRENCY);
+ // TODO(icu-units#28): currency gender?
return result;
}
* @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 unitDisplayCase Specifies the desired grammatical case. If the
+ * specified case is not found, we fall back to nominative or no-case.
* @param rules Plural rules.
* @param parent Plural rules.
*/
String unitDisplayCase,
PluralRules rules,
MicroPropsGenerator parent) {
- if (unit.getType() == null) {
- // Not a built-in unit. Split it up, since we can already format
- // "builtin-per-builtin".
- // TODO(ICU-20941): support more generic case than builtin-per-builtin.
- MeasureUnitImpl fullUnit = unit.getCopyOfMeasureUnitImpl();
- unit = null;
- MeasureUnit perUnit = null;
- for (SingleUnitImpl subUnit : fullUnit.getSingleUnits()) {
- if (subUnit.getDimensionality() > 0) {
- if (unit == null) {
- unit = subUnit.build();
- } else {
- unit = unit.product(subUnit.build());
- }
+ // From https://unicode.org/reports/tr35/tr35-general.html#compound-units -
+ // Points 1 and 2 are mostly handled by MeasureUnit:
+ //
+ // 1. If the unitId is empty or invalid, fail
+ // 2. Put the unitId into normalized order
+ if (unit.getType() != null) {
+ String[] simpleFormats = new String[ARRAY_LENGTH];
+ 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;
+ } else {
+ assert unit.getComplexity() != Complexity.MIXED
+ : "Mixed units not supported by LongNameHandler: use MixedUnitLongNameHandler";
+ return forArbitraryUnit(locale, unit, width, unitDisplayCase, rules, parent);
+ }
+ }
+
+ private static LongNameHandler forArbitraryUnit(ULocale loc,
+ MeasureUnit unit,
+ UnitWidth width,
+ String unitDisplayCase,
+ PluralRules rules,
+ MicroPropsGenerator parent) {
+ // Numbered list items are from the algorithms at
+ // https://unicode.org/reports/tr35/tr35-general.html#compound-units:
+ //
+ // 4. Divide the unitId into numerator (the part before the "-per-") and
+ // denominator (the part after the "-per-). If both are empty, fail
+ MeasureUnitImpl fullUnit = unit.getCopyOfMeasureUnitImpl();
+ unit = null;
+ MeasureUnit perUnit = null;
+ // TODO(icu-units#28): lots of inefficiency in the handling of
+ // MeasureUnit/MeasureUnitImpl:
+ for (SingleUnitImpl subUnit : fullUnit.getSingleUnits()) {
+ if (subUnit.getDimensionality() > 0) {
+ if (unit == null) {
+ unit = subUnit.build();
} else {
- // It's okay to mutate fullUnit, we made a temporary copy:
- subUnit.setDimensionality(subUnit.getDimensionality() * -1);
- if (perUnit == null) {
- perUnit = subUnit.build();
- } else {
- perUnit = perUnit.product(subUnit.build());
- }
+ unit = unit.product(subUnit.build());
+ }
+ } else {
+ // It's okay to mutate fullUnit, we made a temporary copy:
+ subUnit.setDimensionality(subUnit.getDimensionality() * -1);
+ if (perUnit == null) {
+ perUnit = subUnit.build();
+ } else {
+ perUnit = perUnit.product(subUnit.build());
}
}
- return forCompoundUnit(locale, unit, perUnit, width, unitDisplayCase, rules, parent);
}
+ MeasureUnitImpl unitImpl = unit == null ? null : unit.getCopyOfMeasureUnitImpl();
+ MeasureUnitImpl perUnitImpl = perUnit == null ? null : perUnit.getCopyOfMeasureUnitImpl();
- String[] simpleFormats = new String[ARRAY_LENGTH];
- getMeasureData(locale, unit, width, unitDisplayCase, simpleFormats);
- // TODO(ICU4J): Reduce the number of object creations here?
+ // TODO(icu-units#28): check placeholder logic, see if it needs to be
+ // present here instead of only in processPatternTimes:
+ //
+ // 5. Set both globalPlaceholder and globalPlaceholderPosition to be empty
+
+ DerivedComponents derivedPerCases = new DerivedComponents(loc, "case", "per");
+
+ // 6. numeratorUnitString
+ String[] numeratorUnitData = new String[ARRAY_LENGTH];
+ processPatternTimes(unitImpl, loc, width, derivedPerCases.value0(unitDisplayCase),
+ numeratorUnitData);
+
+ // 7. denominatorUnitString
+ String[] denominatorUnitData = new String[ARRAY_LENGTH];
+ processPatternTimes(perUnitImpl, loc, width, derivedPerCases.value1(unitDisplayCase),
+ denominatorUnitData);
+
+ // TODO(icu-units#139):
+ // - implement DerivedComponents for "plural/times" and "plural/power":
+ // French has different rules, we'll be producing the wrong results
+ // currently. (Prove via tests!)
+ // - implement DerivedComponents for "plural/per", "plural/prefix",
+ // "case/times", "case/power", and "case/prefix" - although they're
+ // currently hardcoded. Languages with different rules are surely on the
+ // way.
+ //
+ // Currently we only use "case/per", "plural/times", "case/times", and
+ // "case/power".
+ //
+ // This may have impact on multiSimpleFormatsToModifiers(...) below too?
+ // These rules are currently (ICU 69) all the same and hard-coded below.
+ String perUnitPattern = null;
+ if (denominatorUnitData[PER_INDEX] != null) {
+ // If we have no denominator, we obtain the empty string:
+ perUnitPattern = denominatorUnitData[PER_INDEX];
+ } else {
+ StringBuilder sb = new StringBuilder();
+
+ // 8. Set perPattern to be getValue([per], locale, length)
+ String rawPerUnitFormat = getCompoundValue("per", loc, width);
+ // rawPerUnitFormat is something like "{0} per {1}"; we need to substitute in the secondary
+ // unit.
+ String perPatternFormatter =
+ SimpleFormatterImpl.compileToStringMinMaxArguments(rawPerUnitFormat, sb, 2, 2);
+ // Plural and placeholder handling for 7. denominatorUnitString:
+ // TODO(icu-units#139): hardcoded:
+ // <deriveComponent feature="plural" structure="per" value0="compound" value1="one"/>
+ String rawDenominatorFormat = getWithPlural(denominatorUnitData, StandardPlural.ONE);
+ // Some "one" pattern may not contain "{0}". For example in "ar" or "ne" locale.
+ String denominatorFormatter =
+ SimpleFormatterImpl.compileToStringMinMaxArguments(rawDenominatorFormat, sb, 0, 1);
+ String denominatorString = PatternProps.trimSpaceChar(
+ SimpleFormatterImpl.getTextWithNoArguments(denominatorFormatter));
+
+ // 9. If the denominatorString is empty, set result to
+ // [numeratorString], otherwise set result to format(perPattern,
+ // numeratorString, denominatorString)
+ //
+ // TODO(icu-units#28): Why does UnicodeString need to be explicit in the
+ // following line?
+ perUnitPattern =
+ SimpleFormatterImpl.formatCompiledPattern(perPatternFormatter, "{0}", denominatorString);
+ }
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];
+ if (perUnitPattern.length() == 0) {
+ result.simpleFormatsToModifiers(numeratorUnitData, NumberFormat.Field.MEASURE_UNIT);
+ } else {
+ result.multiSimpleFormatsToModifiers(numeratorUnitData, perUnitPattern,
+ NumberFormat.Field.MEASURE_UNIT);
}
+ // Gender
+ //
+ // TODO(icu-units#28): find out what gender to use in the absence of a first
+ // value - e.g. what's the gender of "per-second"? Mentioned in CLDR-14253.
+ //
+ // gender/per deriveCompound rules don't say:
+ // <deriveCompound feature="gender" structure="per" value="0"/> <!-- gender(gram-per-meter) ←
+ // gender(gram) -->
+ result.gender = getDerivedGender(loc, "per", numeratorUnitData, denominatorUnitData);
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 "".
+ * Roughly corresponds to patternTimes(...) in the spec:
+ * https://unicode.org/reports/tr35/tr35-general.html#compound-units
*/
- 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");
- }
+ private static void processPatternTimes(MeasureUnitImpl productUnit,
+ ULocale loc,
+ UnitWidth width,
+ String caseVariant,
+ String[] outArray) {
+ assert outArray[StandardPlural.OTHER.ordinal()] == null : "outArray must have only null values!";
+ assert outArray[PER_INDEX] == null : "outArray must have only null values!";
- stackBundle = (ICUResourceBundle) stackBundle.get("component");
- stackBundle = (ICUResourceBundle) stackBundle.get(feature);
- stackBundle = (ICUResourceBundle) stackBundle.get(structure);
+ if (productUnit == null) {
+ outArray[StandardPlural.OTHER.ordinal()] = "";
+ outArray[PER_INDEX] = "";
+ return;
+ }
+ if (productUnit.getComplexity() == Complexity.MIXED) {
+ // These are handled by MixedUnitLongNameHandler
+ throw new UnsupportedOperationException("Mixed units not supported by LongNameHandler");
+ }
+
+ if (productUnit.getIdentifier() == null) {
+ // TODO(icu-units#28): consider when serialize should be called.
+ // identifier might also be empty for MeasureUnit().
+ productUnit.serialize();
+ }
+ if (productUnit.getIdentifier().length() == 0) {
+ // MeasureUnit(): no units: return empty strings.
+ return;
+ }
+
+ MeasureUnit builtinUnit = MeasureUnit.findBySubType(productUnit.getIdentifier());
+ if (builtinUnit != null) {
+ // TODO(icu-units#145): spec doesn't cover builtin-per-builtin, it
+ // breaks them all down. Do we want to drop this?
+ // - findBySubType isn't super efficient, if we skip it and go to basic
+ // singles, we don't have to construct MeasureUnit's anymore.
+ // - Check all the existing unit tests that fail without this: is it due
+ // to incorrect fallback via getMeasureData?
+ // - Do those unit tests cover this code path representatively?
+ getMeasureData(loc, builtinUnit, width, caseVariant, outArray);
+ return;
+ }
+
+ // 2. Set timesPattern to be getValue(times, locale, length)
+ String timesPattern = getCompoundValue("times", loc, width);
+ StringBuilder sb = new StringBuilder();
+ String timesPatternFormatter = SimpleFormatterImpl.compileToStringMinMaxArguments(timesPattern, sb, 2, 2);
- String value = stackBundle.getString(0);
- if (value.compareTo("compound") == 0) {
- this.value0 = compoundValue;
+ PlaceholderPosition[] globalPlaceholder = new PlaceholderPosition[ARRAY_LENGTH];
+ char globalJoinerChar = 0;
+ // Numbered list items are from the algorithms at
+ // https://unicode.org/reports/tr35/tr35-general.html#compound-units:
+ //
+ // pattern(...) point 5:
+ // - Set both globalPlaceholder and globalPlaceholderPosition to be empty
+ //
+ // 3. Set result to be empty
+ for (StandardPlural plural : StandardPlural.values()) {
+ int pluralIndex = plural.ordinal();
+ // Initial state: empty string pattern, via all falling back to OTHER:
+ if (plural == StandardPlural.OTHER) {
+ outArray[pluralIndex] = "";
} else {
- this.value0 = value;
+ outArray[pluralIndex] = null;
}
+ globalPlaceholder[pluralIndex] = null;
+ }
+
+ // null represents "compound" (propagate the plural form).
+ String pluralCategory = null;
+ DerivedComponents derivedTimesPlurals = new DerivedComponents(loc, "plural", "times");
+ DerivedComponents derivedTimesCases = new DerivedComponents(loc, "case", "times");
+ DerivedComponents derivedPowerCases = new DerivedComponents(loc, "case", "power");
- value = stackBundle.getString(1);
- if (value.compareTo("compound") == 0) {
- this.value1 = compoundValue;
+ // 4. For each single_unit in product_unit
+ ArrayList<SingleUnitImpl> singleUnits = productUnit.getSingleUnits();
+ for (int singleUnitIndex = 0; singleUnitIndex < singleUnits.size(); singleUnitIndex++) {
+ SingleUnitImpl singleUnit = singleUnits.get(singleUnitIndex);
+ String singlePluralCategory;
+ String singleCaseVariant;
+ // TODO(icu-units#28): ensure we have unit tests that change/fail if we
+ // assign incorrect case variants here:
+ if (singleUnitIndex < singleUnits.size() - 1) {
+ // 4.1. If hasMultiple
+ singlePluralCategory = derivedTimesPlurals.value0(pluralCategory);
+ singleCaseVariant = derivedTimesCases.value0(caseVariant);
+ pluralCategory = derivedTimesPlurals.value1(pluralCategory);
+ caseVariant = derivedTimesCases.value1(caseVariant);
} else {
- this.value1 = value;
+ singlePluralCategory = derivedTimesPlurals.value1(pluralCategory);
+ singleCaseVariant = derivedTimesCases.value1(caseVariant);
}
- }
- public final String value0, value1;
- }
+ // 4.2. Get the gender of that single_unit
+ builtinUnit = MeasureUnit.findBySubType(singleUnit.getSimpleUnitID());
+ if (builtinUnit == null) {
+ // Ideally all simple units should be known, but they're not:
+ // 100-kilometer is internally treated as a simple unit, but it is
+ // not a built-in unit and does not have formatting data in CLDR 39.
+ //
+ // TODO(icu-units#28): test (desirable) invariants in unit tests.
+ throw new UnsupportedOperationException("Unsupported sinlgeUnit: " +
+ singleUnit.getSimpleUnitID());
+ }
+ String gender = getGenderForBuiltin(loc, builtinUnit);
- 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) {
- // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an
- // error code.
- throw new UnsupportedOperationException(
- "Unsanctioned units, not yet supported: " + unit.getIdentifier() + "/" +
- perUnit.getIdentifier());
- }
+ // 4.3. If singleUnit starts with a dimensionality_prefix, such as 'square-'
+ assert singleUnit.getDimensionality() > 0;
+ int dimensionality = singleUnit.getDimensionality();
+ String[] dimensionalityPrefixPatterns = new String[ARRAY_LENGTH];
+ if (dimensionality != 1) {
+ // 4.3.1. set dimensionalityPrefixPattern to be
+ // getValue(that dimensionality_prefix, locale, length, singlePluralCategory,
+ // singleCaseVariant, gender), such as "{0} kwadratowym"
+ StringBuilder dimensionalityKey = new StringBuilder("compound/power");
+ dimensionalityKey.append(dimensionality);
+ try {
+ getInflectedMeasureData(dimensionalityKey.toString(), loc, width, gender,
+ singleCaseVariant, dimensionalityPrefixPatterns);
+ } catch (MissingResourceException e) {
+ // At the time of writing, only pow2 and pow3 are supported.
+ // Attempting to format other powers results in a
+ // U_RESOURCE_TYPE_MISMATCH. We convert the error if we
+ // understand it:
+ if (dimensionality > 3) {
+ throw new UnsupportedOperationException("powerN not supported for N > 3: " +
+ productUnit.getIdentifier());
+ } else {
+ throw e;
+ }
+ }
- DerivedComponents derivedPerCases = new DerivedComponents(locale, "case", "per", unitDisplayCase);
+ // TODO(icu-units#139):
+ // 4.3.2. set singlePluralCategory to be power0(singlePluralCategory)
+ // 4.3.3. set singleCaseVariant to be power0(singleCaseVariant)
+ singleCaseVariant = derivedPowerCases.value0(singleCaseVariant);
+ // 4.3.4. remove the dimensionality_prefix from singleUnit
+ singleUnit.setDimensionality(1);
+ }
- String[] primaryData = new String[ARRAY_LENGTH];
- getMeasureData(locale, unit, width, derivedPerCases.value0, primaryData);
- String[] secondaryData = new String[ARRAY_LENGTH];
- getMeasureData(locale, perUnit, width, derivedPerCases.value1, secondaryData);
+ // 4.4. if singleUnit starts with an si_prefix, such as 'centi'
+ MeasurePrefix prefix = singleUnit.getPrefix();
+ String prefixPattern = "";
+ if (prefix != MeasurePrefix.ONE) {
+ // 4.4.1. set siPrefixPattern to be getValue(that si_prefix, locale,
+ // length), such as "centy{0}"
+ StringBuilder prefixKey = new StringBuilder();
+ // prefixKey looks like "1024p3" or "10p-2":
+ prefixKey.append(prefix.getBase());
+ prefixKey.append('p');
+ prefixKey.append(prefix.getPower());
+ // Contains a pattern like "centy{0}".
+ prefixPattern = getCompoundValue(prefixKey.toString(), loc, width);
- // 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];
- } else {
- String rawPerUnitFormat = getPerUnitFormat(locale, width);
- // rawPerUnitFormat is something like "{0}/{1}"; we need to substitute in the secondary unit.
- // TODO: Lots of thrashing. Improve?
- StringBuilder sb = new StringBuilder();
- String compiled = SimpleFormatterImpl
- .compileToStringMinMaxArguments(rawPerUnitFormat, sb, 2, 2);
- String secondaryFormat = getWithPlural(secondaryData, StandardPlural.ONE);
+ // 4.4.2. set singlePluralCategory to be prefix0(singlePluralCategory)
+ //
+ // TODO(icu-units#139): that refers to these rules:
+ // <deriveComponent feature="plural" structure="prefix" value0="one" value1="compound"/>
+ // though I'm not sure what other value they might end up having.
+ //
+ // 4.4.3. set singleCaseVariant to be prefix0(singleCaseVariant)
+ //
+ // TODO(icu-units#139): that refers to:
+ // <deriveComponent feature="case" structure="prefix" value0="nominative"
+ // value1="compound"/> but the prefix (value0) doesn't have case, the rest simply
+ // propagates.
- // Some "one" pattern may not contain "{0}". For example in "ar" or "ne" locale.
- String secondaryCompiled = SimpleFormatterImpl
- .compileToStringMinMaxArguments(secondaryFormat, sb, 0, 1);
- String secondaryFormatString = SimpleFormatterImpl.getTextWithNoArguments(secondaryCompiled);
+ // 4.4.4. remove the si_prefix from singleUnit
+ singleUnit.setPrefix(MeasurePrefix.ONE);
+ }
- // TODO(icu-units#28): do not use regular expression
- String secondaryString = secondaryFormatString.replaceAll("(^\\h*)|(\\h*$)",""); // Trim all spaces.
+ // 4.5. Set corePattern to be the getValue(singleUnit, locale, length,
+ // singlePluralCategory, singleCaseVariant), such as "{0} metrem"
+ String[] singleUnitArray = new String[ARRAY_LENGTH];
+ // At this point we are left with a Simple Unit:
+ assert singleUnit.build().getIdentifier().equals(singleUnit.getSimpleUnitID())
+ : "Should be equal: singleUnit.build().getIdentifier() produced " +
+ singleUnit.build().getIdentifier() + ", singleUnit.getSimpleUnitID() produced " +
+ singleUnit.getSimpleUnitID();
+ getMeasureData(loc, singleUnit.build(), width, singleCaseVariant, singleUnitArray);
- 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);
+ // Calculate output gender
+ if (singleUnitArray[GENDER_INDEX] != null) {
+ assert !singleUnitArray[GENDER_INDEX].isEmpty();
- // Gender
- String val = getDeriveCompoundRule(locale, "gender", "per");
+ if (prefix != MeasurePrefix.ONE) {
+ singleUnitArray[GENDER_INDEX] =
+ getDerivedGender(loc, "prefix", singleUnitArray, null);
+ }
- 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;
- }
+ // Powers use compoundUnitPattern1, dimensionalityPrefixPatterns may
+ // have a "gender" element
+ //
+ // TODO(icu-units#28): untested: no locale data uses this currently:
+ if (dimensionality != 1) {
+ singleUnitArray[GENDER_INDEX] =
+ getDerivedGender(loc, "power", singleUnitArray, dimensionalityPrefixPatterns);
+ }
- return result;
+ String timesGenderRule = getDeriveCompoundRule(loc, "gender", "times");
+ if (timesGenderRule.length() == 1) {
+ switch (timesGenderRule.charAt(0)) {
+ case '0':
+ if (singleUnitIndex == 0) {
+ assert outArray[GENDER_INDEX] == null;
+ outArray[GENDER_INDEX] = singleUnitArray[GENDER_INDEX];
+ }
+ break;
+ case '1':
+ if (singleUnitIndex == singleUnits.size() - 1) {
+ assert outArray[GENDER_INDEX] == null;
+ outArray[GENDER_INDEX] = singleUnitArray[GENDER_INDEX];
+ }
+ }
+ } else {
+ if (outArray[GENDER_INDEX] == null) {
+ outArray[GENDER_INDEX] = timesGenderRule;
+ }
+ }
+ }
+
+ // Calculate resulting patterns for each plural form
+ for (StandardPlural plural_ : StandardPlural.values()) {
+ StandardPlural plural = plural_;
+ int pluralIndex = plural.ordinal();
+
+ // singleUnitArray[pluralIndex] looks something like "{0} Meter"
+ if (outArray[pluralIndex] == null) {
+ if (singleUnitArray[pluralIndex] == null) {
+ // Let the usual plural fallback mechanism take care of this
+ // plural form
+ continue;
+ } else {
+ // Since our singleUnit can have a plural form that outArray
+ // doesn't yet have (relying on fallback to OTHER), we start
+ // by grabbing it with the normal plural fallback mechanism
+ outArray[pluralIndex] = getWithPlural(outArray, plural);
+ }
+ }
+
+ if (singlePluralCategory != null) {
+ plural = StandardPlural.fromString(singlePluralCategory);
+ }
+
+ // 4.6. Extract(corePattern, coreUnit, placeholder, placeholderPosition) from that
+ // pattern.
+ ExtractCorePatternResult r = extractCorePattern(getWithPlural(singleUnitArray, plural));
+
+ // 4.7 If the position is middle, then fail
+ if (r.placeholderPosition == PlaceholderPosition.MIDDLE) {
+ throw new UnsupportedOperationException();
+ }
+
+ // 4.8. If globalPlaceholder is empty
+ if (globalPlaceholder[pluralIndex] == null) {
+ globalPlaceholder[pluralIndex] = r.placeholderPosition;
+ globalJoinerChar = r.joinerChar;
+ } else {
+ // Expect all units involved to have the same placeholder position
+ assert globalPlaceholder[pluralIndex] == r.placeholderPosition;
+ // TODO(icu-units#28): Do we want to add a unit test that checks
+ // for consistent joiner chars? Probably not, given how
+ // inconsistent they are. File a CLDR ticket with examples?
+ }
+ // Now coreUnit would be just "Meter"
+
+ // 4.9. If siPrefixPattern is not empty
+ if (prefix != MeasurePrefix.ONE) {
+ String prefixCompiled =
+ SimpleFormatterImpl.compileToStringMinMaxArguments(prefixPattern, sb, 1, 1);
+
+ // 4.9.1. Set coreUnit to be the combineLowercasing(locale, length, siPrefixPattern,
+ // coreUnit)
+ // combineLowercasing(locale, length, prefixPattern, coreUnit)
+ //
+ // TODO(icu-units#28): run this only if prefixPattern does not
+ // contain space characters - do languages "as", "bn", "hi",
+ // "kk", etc have concepts of upper and lower case?:
+ if (width == UnitWidth.FULL_NAME) {
+ r.coreUnit = UCharacter.toLowerCase(loc, r.coreUnit);
+ }
+ r.coreUnit = SimpleFormatterImpl.formatCompiledPattern(prefixCompiled, r.coreUnit);
+ }
+
+ // 4.10. If dimensionalityPrefixPattern is not empty
+ if (dimensionality != 1) {
+ String dimensionalityCompiled = SimpleFormatterImpl.compileToStringMinMaxArguments(
+ getWithPlural(dimensionalityPrefixPatterns, plural), sb, 1, 1);
+
+ // 4.10.1. Set coreUnit to be the combineLowercasing(locale, length,
+ // dimensionalityPrefixPattern, coreUnit)
+ // combineLowercasing(locale, length, prefixPattern, coreUnit)
+ //
+ // TODO(icu-units#28): run this only if prefixPattern does not
+ // contain space characters - do languages "as", "bn", "hi",
+ // "kk", etc have concepts of upper and lower case?:
+ if (width == UnitWidth.FULL_NAME) {
+ r.coreUnit = UCharacter.toLowerCase(loc, r.coreUnit);
+ }
+ r.coreUnit =
+ SimpleFormatterImpl.formatCompiledPattern(dimensionalityCompiled, r.coreUnit);
+ }
+
+ if (outArray[pluralIndex].length() == 0) {
+ // 4.11. If the result is empty, set result to be coreUnit
+ outArray[pluralIndex] = r.coreUnit;
+ } else {
+ // 4.12. Otherwise set result to be format(timesPattern, result, coreUnit)
+ outArray[pluralIndex] = SimpleFormatterImpl.formatCompiledPattern(
+ timesPatternFormatter, outArray[pluralIndex], r.coreUnit);
+ }
+ }
+ }
+ for (StandardPlural plural : StandardPlural.values()) {
+ int pluralIndex = plural.ordinal();
+ if (globalPlaceholder[pluralIndex] == PlaceholderPosition.BEGINNING) {
+ StringBuilder tmp = new StringBuilder();
+ tmp.append("{0}");
+ if (globalJoinerChar != 0) {
+ tmp.append(globalJoinerChar);
+ }
+ tmp.append(outArray[pluralIndex]);
+ outArray[pluralIndex] = tmp.toString();
+ } else if (globalPlaceholder[pluralIndex] == PlaceholderPosition.END) {
+ if (globalJoinerChar != 0) {
+ outArray[pluralIndex] = outArray[pluralIndex] + globalJoinerChar;
+ }
+ outArray[pluralIndex] = outArray[pluralIndex] + "{0}";
+ }
+ }
}
+ /** Sets modifiers to use the patterns from simpleFormats. */
private void simpleFormatsToModifiers(
String[] simpleFormats,
NumberFormat.Field field) {
}
}
+ /**
+ * Sets modifiers to a combination of `leadFormats` (one per plural form)
+ * and `trailFormat` appended to each.
+ *
+ * With a leadFormat of "{0}m" and a trailFormat of "{0}/s", it produces a
+ * pattern of "{0}m/s" by inserting each leadFormat pattern into
+ * trailFormat.
+ */
private void multiSimpleFormatsToModifiers(
String[] leadFormats,
String trailFormat,
String trailCompiled = SimpleFormatterImpl.compileToStringMinMaxArguments(trailFormat, sb, 1, 1);
for (StandardPlural plural : StandardPlural.VALUES) {
String leadFormat = getWithPlural(leadFormats, plural);
- String compoundFormat = SimpleFormatterImpl.formatCompiledPattern(trailCompiled, leadFormat);
- String compoundCompiled = SimpleFormatterImpl
- .compileToStringMinMaxArguments(compoundFormat, sb, 0, 1);
+ String compoundFormat;
+ if (leadFormat.length() == 0) {
+ compoundFormat = trailFormat;
+ } else {
+ compoundFormat = SimpleFormatterImpl.formatCompiledPattern(trailCompiled, leadFormat);
+ }
+ String compoundCompiled =
+ SimpleFormatterImpl.compileToStringMinMaxArguments(compoundFormat, sb, 0, 1);
Modifier.Parameters parameters = new Modifier.Parameters();
parameters.obj = this;
parameters.signum = null; // Signum ignored
* @param mixedUnit The mixed measure unit to construct a
* MixedUnitLongNameHandler for.
* @param width Specifies the desired unit rendering.
- * @param unitDisplayName
+ * @param unitDisplayCase Specifies the desired grammatical case. If the
+ * specified case is not found, we fall back to nominative or no-case.
* @param rules PluralRules instance.
* @param parent MicroPropsGenerator instance.
*/
public static MixedUnitLongNameHandler forMeasureUnit(ULocale locale,
MeasureUnit mixedUnit,
NumberFormatter.UnitWidth width,
- String unitDisplayName,
+ String unitDisplayCase,
PluralRules rules,
MicroPropsGenerator parent) {
- assert (mixedUnit.getComplexity() == MeasureUnit.Complexity.MIXED);
+ assert mixedUnit.getComplexity() == MeasureUnit.Complexity.MIXED
+ : "MixedUnitLongNameHandler only supports MIXED units";
+ // In ICU4C, in addition to an assert, we return a failure status if the
+ // unit is not mixed (commented by: "Defensive, for production code").
+ // In Java, we don't have efficient access to MeasureUnitImpl, so we
+ // skip this check - relying on unit tests and the assert above to help
+ // enforce the invariant.
MixedUnitLongNameHandler result = new MixedUnitLongNameHandler(rules, parent);
List<MeasureUnit> individualUnits = mixedUnit.splitToSingleUnits();
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, unitDisplayName, unitData);
+ LongNameHandler.getMeasureData(locale, individualUnits.get(i), width, unitDisplayCase,
+ unitData);
result.fMixedUnitData.add(unitData);
}
*/
@Override
public Modifier getModifier(Modifier.Signum signum, StandardPlural plural) {
- // TODO(units): investigate this method while investigating where
+ // TODO(icu-units#28): investigate this method while investigating where
// LongNameHandler.getModifier() gets used. To be sure it remains
// unreachable:
assert false : "should be unreachable";
MeasureUnit unit = macros.unit;
if (macros.perUnit != null) {
unit = unit.product(macros.perUnit.reciprocal());
+ // This isn't strictly necessary, but was what we specced
+ // out when perUnit became a backward-compatibility thing:
+ // unit/perUnit use case is only valid if both units are
+ // built-ins, or the product is a built-in.
+ if (unit.getType() == null && (macros.unit.getType() == null || macros.perUnit.getType() == null)) {
+ throw new UnsupportedOperationException(
+ "perUnit() can only be used if unit and perUnit are both built-ins, or the combination is a built-in");
+ }
}
chain = LongNameHandler.forMeasureUnit(
macros.loc,
return MeasureUnit.addUnit(type, subType, factory);
}
- private static MeasureUnit findBySubType(String subType) {
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ public static MeasureUnit findBySubType(String subType) {
populateCache();
for (Map<String, MeasureUnit> unitsForType : cache.values()) {
if (unitsForType.containsKey(subType)) {
"0.0088 meters",
"0 meters");
- // // TODO(ICU-20941): Support formatting for not-built-in units
- // assertFormatDescending(
- // "Hectometers",
- // "measure-unit/length-hectometer",
- // "unit/hectometer",
- // NumberFormatter.with().unit(MeasureUnit.forIdentifier("hectometer")),
- // ULocale.ENGLISH,
- // "87,650 hm",
- // "8,765 ham",
- // "876.5 hm",
- // "87.65 hm",
- // "8.765 hm",
- // "0.8765 hm",
- // "0.08765 hm",
- // "0.008765 hm",
- // "0 hm");
+ assertFormatDescending(
+ "Hectometers",
+ "unit/hectometer",
+ "unit/hectometer",
+ NumberFormatter.with().unit(MeasureUnit.forIdentifier("hectometer")),
+ ULocale.ENGLISH,
+ "87,650 hm",
+ "8,765 hm",
+ "876.5 hm",
+ "87.65 hm",
+ "8.765 hm",
+ "0.8765 hm",
+ "0.08765 hm",
+ "0.008765 hm",
+ "0 hm");
assertFormatSingleMeasure(
"Meters with Measure Input",
5,
"5 a\u00F1os");
- // TODO(ICU-20941): arbitrary unit formatting
- // assertFormatSingle(
- // "Hubble Constant",
- // "unit/kilometer-per-megaparsec-second",
- // "unit/kilometer-per-megaparsec-second",
- // NumberFormatter.with()
- // .unit(MeasureUnit.forIdentifier("kilometer-per-megaparsec-second")),
- // new ULocale("en"),
- // 74, // Approximate 2019-03-18 measurement
- // "74 km/s.Mpc");
+ assertFormatSingle(
+ "Hubble Constant",
+ "unit/kilometer-per-megaparsec-second",
+ "unit/kilometer-per-megaparsec-second",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("kilometer-per-megaparsec-second")),
+ new ULocale("en"),
+ 74, // Approximate 2019-03-18 measurement
+ "74 km/Mpc⋅sec");
assertFormatSingle(
"Mixed unit",
try {
nf.format(2.4d);
- fail("Expected failure, got: " + nf.format(2.4d) + ".");
+ fail("Expected failure for unit/furlong-pascal per-unit/length-meter, got: " +
+ nf.format(2.4d) + ".");
} catch (UnsupportedOperationException e) {
// Pass
}
"2.4 m/s\u00B2");
}
+ @Test
+ public void unitArbitraryMeasureUnits() {
+ // TODO: fix after data bug is resolved? See CLDR-14510.
+ // assertFormatSingle(
+ // "Binary unit prefix: kibibyte",
+ // "unit/kibibyte",
+ // "unit/kibibyte",
+ // NumberFormatter.with().unit(MeasureUnit.forIdentifier("kibibyte")),
+ // new ULocale("en-GB"),
+ // 2.4,
+ // "2.4 KiB");
+
+ assertFormatSingle("Binary unit prefix: kibibyte full-name",
+ "unit/kibibyte unit-width-full-name", "unit/kibibyte unit-width-full-name",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("kibibyte"))
+ .unitWidth(UnitWidth.FULL_NAME),
+ new ULocale("en-GB"), 2.4, "2.4 kibibytes");
+
+ assertFormatSingle("Binary unit prefix: kibibyte full-name",
+ "unit/kibibyte unit-width-full-name", "unit/kibibyte unit-width-full-name",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("kibibyte"))
+ .unitWidth(UnitWidth.FULL_NAME),
+ new ULocale("de"), 2.4, "2,4 Kibibyte");
+
+ assertFormatSingle("Binary prefix for non-digital units: kibimeter", "unit/kibimeter",
+ "unit/kibimeter",
+ NumberFormatter.with().unit(MeasureUnit.forIdentifier("kibimeter")),
+ new ULocale("en-GB"), 2.4, "2.4 Kim");
+
+ assertFormatSingle("SI prefix falling back to root: microohm", "unit/microohm", "unit/microohm",
+ NumberFormatter.with().unit(MeasureUnit.forIdentifier("microohm")),
+ new ULocale("de-CH"), 2.4, "2.4 μΩ");
+
+ assertFormatSingle("de-CH fallback to de: microohm unit-width-full-name",
+ "unit/microohm unit-width-full-name", "unit/microohm unit-width-full-name",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("microohm"))
+ .unitWidth(UnitWidth.FULL_NAME),
+ new ULocale("de-CH"), 2.4, "2.4\u00A0Mikroohm");
+
+ assertFormatSingle("No prefixes, 'times' pattern: joule-furlong", "unit/joule-furlong",
+ "unit/joule-furlong",
+ NumberFormatter.with().unit(MeasureUnit.forIdentifier("joule-furlong")),
+ new ULocale("en"), 2.4, "2.4 J⋅fur");
+
+ assertFormatSingle("No numeratorUnitString: per-second", "unit/per-second", "unit/per-second",
+ NumberFormatter.with().unit(MeasureUnit.forIdentifier("per-second")),
+ new ULocale("de-CH"), 2.4, "2.4/s");
+
+ assertFormatSingle("No numeratorUnitString: per-second unit-width-full-name",
+ "unit/per-second unit-width-full-name",
+ "unit/per-second unit-width-full-name",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("per-second"))
+ .unitWidth(UnitWidth.FULL_NAME),
+ new ULocale("de-CH"), 2.4, "2.4 pro Sekunde");
+
+ assertFormatSingle(
+ "Prefix in the denominator: nanogram-per-picobarrel", "unit/nanogram-per-picobarrel",
+ "unit/nanogram-per-picobarrel",
+ NumberFormatter.with().unit(MeasureUnit.forIdentifier("nanogram-per-picobarrel")),
+ new ULocale("en-ZA"), 2.4, "2,4 ng/pbbl");
+
+ assertFormatSingle("Prefix in the denominator: nanogram-per-picobarrel unit-width-full-name",
+ "unit/nanogram-per-picobarrel unit-width-full-name",
+ "unit/nanogram-per-picobarrel unit-width-full-name",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("nanogram-per-picobarrel"))
+ .unitWidth(UnitWidth.FULL_NAME),
+ new ULocale("en-ZA"), 2.4, "2,4 nanograms per picobarrel");
+
+ // Valid MeasureUnit, but unformattable, because we only have patterns for
+ // pow2 and pow3 at this time:
+ LocalizedNumberFormatter lnf = NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("pow4-mile"))
+ .unitWidth(UnitWidth.FULL_NAME)
+ .locale(new ULocale("en-ZA"));
+ try {
+ lnf.format(1);
+ fail("Expected failure for pow4-mile, got: " + lnf.format(1) + ".");
+ } catch (UnsupportedOperationException e) {
+ // pass
+ }
+
+ assertFormatSingle(
+ "kibijoule-foot-per-cubic-gigafurlong-square-second unit-width-full-name",
+ "unit/kibijoule-foot-per-cubic-gigafurlong-square-second unit-width-full-name",
+ "unit/kibijoule-foot-per-cubic-gigafurlong-square-second unit-width-full-name",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("kibijoule-foot-per-cubic-gigafurlong-square-second"))
+ .unitWidth(UnitWidth.FULL_NAME),
+ new ULocale("en-ZA"), 2.4, "2,4 kibijoule-feet per cubic gigafurlong-square second");
+
+ assertFormatSingle(
+ "kibijoule-foot-per-cubic-gigafurlong-square-second unit-width-full-name",
+ "unit/kibijoule-foot-per-cubic-gigafurlong-square-second unit-width-full-name",
+ "unit/kibijoule-foot-per-cubic-gigafurlong-square-second unit-width-full-name",
+ NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("kibijoule-foot-per-cubic-gigafurlong-square-second"))
+ .unitWidth(UnitWidth.FULL_NAME),
+ new ULocale("de-CH"), 2.4, "2.4\u00A0Kibijoule⋅Fuss pro Kubikgigafurlong⋅Quadratsekunde");
+
+ // TODO(ICU-21504): We want to be able to format this, but "100-kilometer"
+ // is not yet supported when it's not part of liter-per-100-kilometer:
+ lnf = NumberFormatter.with()
+ .unit(MeasureUnit.forIdentifier("kilowatt-hour-per-100-kilometer"))
+ .unitWidth(UnitWidth.FULL_NAME)
+ .locale(new ULocale("en-ZA"));
+ try {
+ lnf.format(1);
+ fail("Expected failure for kilowatt-hour-per-100-kilometer, got: " + lnf.format(1) + ".");
+ } catch (UnsupportedOperationException e) {
+ // pass
+ }
+ }
+
// TODO: merge these tests into NumberSkeletonTest.java instead of here:
@Test
public void unitSkeletons() {
}
public static class UnitInflectionTestCase {
+ public final String unitIdentifier;
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) {
+ UnitInflectionTestCase(String unitIdentifier,
+ String locale,
+ String unitDisplayCase,
+ double value,
+ String expected) {
+ this.unitIdentifier = unitIdentifier;
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);
+ public void runTest(UnlocalizedNumberFormatter unf, String skeleton) {
+ MeasureUnit mu = MeasureUnit.forIdentifier(unitIdentifier);
+ String skel;
+ if (this.unitDisplayCase == null || this.unitDisplayCase.isEmpty()) {
+ unf = unf.unit(mu).unitDisplayCase("");
+ skel = "unit/" + unitIdentifier + " " + skeleton;
+ } else {
+ unf = unf.unit(mu).unitDisplayCase(this.unitDisplayCase);
+ // No skeleton support for unitDisplayCase yet.
+ skel = null;
}
+ assertFormatSingle("\"" + skeleton + "\", locale=\"" + this.locale + "\", case=\"" +
+ (this.unitDisplayCase != null ? this.unitDisplayCase : "") +
+ "\", value=" + this.value,
+ skel, skel, unf, new ULocale(this.locale), this.value, this.expected);
}
}
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";
+ unf = NumberFormatter.with().unitWidth(UnitWidth.FULL_NAME);
+ skeleton = "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
+ new UnitInflectionTestCase("percent", "ru", null, 10, "10 процентов"), // many
+ new UnitInflectionTestCase("percent", "ru", "genitive", 10, "10 процентов"), // many
+ new UnitInflectionTestCase("percent", "ru", null, 33, "33 процента"), // few
+ new UnitInflectionTestCase("percent", "ru", "genitive", 33, "33 процентов"), // few
+ new UnitInflectionTestCase("percent", "ru", null, 1, "1 процент"), // one
+ new UnitInflectionTestCase("percent", "ru", "genitive", 1, "1 процента"), // one
};
-
- for (UnitInflectionTestCase testCase :
- percentCases) {
- UnitInflectionTestCase.runUnitInflectionsTestCases(unf, skeleton, conciseSkeleton, percentCases);
+ for (UnitInflectionTestCase t : percentCases) {
+ t.runTest(unf, skeleton);
}
}
{
- // 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"),
+ // General testing of inflection rules
+ unf = NumberFormatter.with().unitWidth(UnitWidth.FULL_NAME);
+ skeleton = "unit-width-full-name";
+ final UnitInflectionTestCase testCases[] = {
+ // Check up on the basic values that the compound patterns below
+ // are derived from:
+ new UnitInflectionTestCase("meter", "de", null, 1, "1 Meter"),
+ new UnitInflectionTestCase("meter", "de", "genitive", 1, "1 Meters"),
+ new UnitInflectionTestCase("meter", "de", null, 2, "2 Meter"),
+ new UnitInflectionTestCase("meter", "de", "dative", 2, "2 Metern"),
+ new UnitInflectionTestCase("mile", "de", null, 1, "1 Meile"),
+ new UnitInflectionTestCase("mile", "de", null, 2, "2 Meilen"),
+ new UnitInflectionTestCase("day", "de", null, 1, "1 Tag"),
+ new UnitInflectionTestCase("day", "de", "genitive", 1, "1 Tages"),
+ new UnitInflectionTestCase("day", "de", null, 2, "2 Tage"),
+ new UnitInflectionTestCase("day", "de", "dative", 2, "2 Tagen"),
+ new UnitInflectionTestCase("decade", "de", null, 1, "1\u00A0Jahrzehnt"),
+ new UnitInflectionTestCase("decade", "de", null, 2, "2\u00A0Jahrzehnte"),
+
+ // Testing de "per" rules:
+ // <deriveComponent feature="case" structure="per" value0="compound" value1="accusative"/>
+ // <deriveComponent feature="plural" structure="per" value0="compound" value1="one"/>
+ // per-patterns use accusative, but since the accusative form
+ // matches the nominative form, we're not effectively testing value1
+ // in the "case & per" rule above.
+
+ // We have a perUnitPattern for "day" in de, so "per" rules are not
+ // applied for these:
+ new UnitInflectionTestCase("meter-per-day", "de", null, 1, "1 Meter pro Tag"),
+ new UnitInflectionTestCase("meter-per-day", "de", "genitive", 1, "1 Meters pro Tag"),
+ new UnitInflectionTestCase("meter-per-day", "de", null, 2, "2 Meter pro Tag"),
+ new UnitInflectionTestCase("meter-per-day", "de", "dative", 2, "2 Metern pro Tag"),
+
+ // testing code path that falls back to "root" grammaticalFeatures
+ // but does not inflect:
+ new UnitInflectionTestCase("meter-per-day", "af", null, 1, "1 meter per dag"),
+ new UnitInflectionTestCase("meter-per-day", "af", "dative", 1, "1 meter per dag"),
+
+ // 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:
+ // Fragile test cases: these cases will break when whitespace is more
+ // consistently applied.
+ new UnitInflectionTestCase("parsec-per-decade", "de", null, 1,
+ "1\u00A0Parsec pro Jahrzehnt"),
+ new UnitInflectionTestCase("parsec-per-decade", "de", "genitive", 1,
+ "1 Parsec pro Jahrzehnt"),
+ new UnitInflectionTestCase("parsec-per-decade", "de", null, 2,
+ "2\u00A0Parsec pro Jahrzehnt"),
+ new UnitInflectionTestCase("parsec-per-decade", "de", "dative", 2,
+ "2 Parsec pro Jahrzehnt"),
+
+ // Testing de "times", "power" and "prefix" rules:
+ //
+ // <deriveComponent feature="plural" structure="times" value0="one" value1="compound"/>
+ // <deriveComponent feature="case" structure="times" value0="nominative" value1="compound"/>
+ //
+ // <deriveComponent feature="plural" structure="prefix" value0="one" value1="compound"/>
+ // <deriveComponent feature="case" structure="prefix" value0="nominative" value1="compound"/>
+ //
+ // Prefixes in German don't change with plural or case, so these
+ // tests can't test value0 of the following two rules:
+ // <deriveComponent feature="plural" structure="power" value0="one" value1="compound"/>
+ // <deriveComponent feature="case" structure="power" value0="nominative" value1="compound"/>
+
+ new UnitInflectionTestCase("square-decimeter-dekameter", "de", null, 1,
+ "1 Quadratdezimeter⋅Dekameter"),
+ new UnitInflectionTestCase("square-decimeter-dekameter", "de", "genitive", 1,
+ "1 Quadratdezimeter⋅Dekameters"),
+ new UnitInflectionTestCase("square-decimeter-dekameter", "de", null, 2,
+ "2 Quadratdezimeter⋅Dekameter"),
+ new UnitInflectionTestCase("square-decimeter-dekameter", "de", "dative", 2,
+ "2 Quadratdezimeter⋅Dekametern"),
+ // Feminine "Meile" better demonstrates singular-vs-plural form:
+ new UnitInflectionTestCase("cubic-mile-dekamile", "de", null, 1,
+ "1 Kubikmeile⋅Dekameile"),
+ new UnitInflectionTestCase("cubic-mile-dekamile", "de", null, 2,
+ "2 Kubikmeile⋅Dekameilen"),
+
+ // French handles plural "times" and "power" structures differently:
+ // plural form impacts all "numerator" units (denominator remains
+ // singular like German), and "pow2" prefixes have different forms
+ // <deriveComponent feature="plural" structure="times" value0="compound"
+ // value1="compound"/>
+ // <deriveComponent feature="plural" structure="power" value0="compound"
+ // value1="compound"/>
+ new UnitInflectionTestCase("square-decimeter-square-second", "fr", null, 1,
+ "1\u00A0décimètre carré-seconde carrée"),
+ new UnitInflectionTestCase("square-decimeter-square-second", "fr", null, 2,
+ "2\u00A0décimètres carrés-secondes carrées"),
};
- 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);
+ for (UnitInflectionTestCase t : testCases) {
+ t.runTest(unf, skeleton);
+ }
}
{
// 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";
+ unf = NumberFormatter.with().unitWidth(UnitWidth.FULL_NAME);
+ skeleton = "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"),
+ new UnitInflectionTestCase("meter", "de", null, 1, "1 Meter"),
+ new UnitInflectionTestCase("meter", "de", "genitive", 1, "1 Meters"),
+ new UnitInflectionTestCase("meter", "de", "dative", 2, "2 Metern"),
+ new UnitInflectionTestCase("centimeter", "de", null, 1, "1 Zentimeter"),
+ new UnitInflectionTestCase("centimeter", "de", "genitive", 1, "1 Zentimeters"),
+ new UnitInflectionTestCase("centimeter", "de", "dative", 10, "10 Zentimetern"),
+ // 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("meter-and-centimeter", "de", null, 1.01,
+ "1 Meter, 1 Zentimeter"),
+ new UnitInflectionTestCase("meter-and-centimeter", "de", "genitive", 1.01,
+ "1 Meters, 1 Zentimeters"),
+ new UnitInflectionTestCase("meter-and-centimeter", "de", "genitive", 1.1,
+ "1 Meters, 10 Zentimeter"),
+ new UnitInflectionTestCase("meter-and-centimeter", "de", "dative", 1.1,
+ "1 Meter, 10 Zentimetern"),
+ new UnitInflectionTestCase("meter-and-centimeter", "de", "dative", 2.1,
+ "2 Metern, 10 Zentimetern"),
};
- UnitInflectionTestCase.runUnitInflectionsTestCases(unf, skeleton, conciseSkeleton, meterPerDayCases);
+ for (UnitInflectionTestCase t : meterPerDayCases) {
+ t.runTest(unf, skeleton);
+ }
}
// 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
}
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("de", "meter", "masculine"),
+ new TestCase("de", "second", "feminine"),
+ new TestCase("de", "minute", "feminine"),
+ new TestCase("de", "hour", "feminine"),
+ new TestCase("de", "day", "masculine"),
+ new TestCase("de", "year", "neuter"),
+ new TestCase("fr", "meter", "masculine"),
+ new TestCase("fr", "second", "feminine"),
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"),
+ new TestCase("fr", "hour", "feminine"),
+ new TestCase("fr", "day", "masculine"),
+ // grammaticalFeatures deriveCompound "per" rule takes the gender of the
+ // numerator unit:
+ new TestCase("de", "meter-per-hour", "masculine"),
+ new TestCase("fr", "meter-per-hour", "masculine"),
+ new TestCase("af", "meter-per-hour", null), // ungendered language
+ // French "times" takes gender from first value, German takes the
+ // second. Prefix and power does not have impact on gender for these
+ // languages:
+ new TestCase("de", "square-decimeter-square-second", "feminine"),
+ new TestCase("fr", "square-decimeter-square-second", "masculine"),
+ // 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
+ // // TODO(icu-units#140): make this work for more than just UnitWidth.FULL_NAME
+ // formatter = NumberFormatter.with()
+ // .unit(MeasureUnit.forIdentifier(t.unitIdentifier))
+ // .locale(new ULocale(t.locale));
+ // fn = formatter.format(1.1);
+ // assertEquals("Testing gender with default width, unit: " + t.unitIdentifier +
+ // ", locale: " + t.locale,
+ // t.expectedGender, fn.getGender());
+
formatter = NumberFormatter.with()
- .unit(MeasureUnit.forIdentifier(t.unitIdentifier))
- .unitWidth(UnitWidth.FULL_NAME)
- .locale(new ULocale(t.locale));
+ .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());
+ assertEquals("Testing gender with UnitWidth.FULL_NAME, unit: " + t.unitIdentifier +
+ ", locale: " + t.locale,
+ t.expectedGender, fn.getGender());
}
// Make sure getGender does not return garbage for genderless languages