]> granicus.if.org Git - icu/commitdiff
ICU-13520 Adds compound unit support to NumberFormatter.
authorShane Carr <shane@unicode.org>
Fri, 22 Dec 2017 00:02:01 +0000 (00:02 +0000)
committerShane Carr <shane@unicode.org>
Fri, 22 Dec 2017 00:02:01 +0000 (00:02 +0000)
X-SVN-Rev: 40747

17 files changed:
icu4c/source/i18n/measfmt.cpp
icu4c/source/i18n/measunit.cpp
icu4c/source/i18n/number_fluent.cpp
icu4c/source/i18n/number_formatimpl.cpp
icu4c/source/i18n/number_longnames.cpp
icu4c/source/i18n/number_longnames.h
icu4c/source/i18n/unicode/measunit.h
icu4c/source/i18n/unicode/nounit.h
icu4c/source/i18n/unicode/numberformatter.h
icu4c/source/test/intltest/numbertest.h
icu4c/source/test/intltest/numbertest_api.cpp
icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/MacroProps.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/util/MeasureUnit.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java

index 2ef5329545812969032207197205824805f4e889..0d9d2f8b8e00b532304f54c44357199583e202b4 100644 (file)
@@ -764,10 +764,11 @@ UnicodeString &MeasureFormat::formatMeasurePerUnit(
     if (U_FAILURE(status)) {
         return appendTo;
     }
-    MeasureUnit *resolvedUnit =
-            MeasureUnit::resolveUnitPerUnit(measure.getUnit(), perUnit);
-    if (resolvedUnit != NULL) {
-        Measure newMeasure(measure.getNumber(), resolvedUnit, status);
+    bool isResolved = false;
+    MeasureUnit resolvedUnit =
+        MeasureUnit::resolveUnitPerUnit(measure.getUnit(), perUnit, &isResolved);
+    if (isResolved) {
+        Measure newMeasure(measure.getNumber(), new MeasureUnit(resolvedUnit), status);
         return formatMeasure(
                 newMeasure, **numberFormat, appendTo, pos, status);
     }
index e9e141789ea757e4f7f0488b818693d7727132f4..43221345424820b9ffef882dc1106298ca97085e 100644 (file)
@@ -1211,8 +1211,8 @@ int32_t MeasureUnit::internalGetIndexForTypeAndSubtype(const char *type, const c
     return gIndexes[t] + st - gOffsets[t];
 }
 
-MeasureUnit *MeasureUnit::resolveUnitPerUnit(
-        const MeasureUnit &unit, const MeasureUnit &perUnit) {
+MeasureUnit MeasureUnit::resolveUnitPerUnit(
+        const MeasureUnit &unit, const MeasureUnit &perUnit, bool* isResolved) {
     int32_t unitOffset = unit.getOffset();
     int32_t perUnitOffset = perUnit.getOffset();
 
@@ -1233,10 +1233,13 @@ MeasureUnit *MeasureUnit::resolveUnitPerUnit(
         } else {
             // We found a resolution for our unit / per-unit combo
             // return it.
-            return new MeasureUnit(midRow[2], midRow[3]);
+            *isResolved = true;
+            return MeasureUnit(midRow[2], midRow[3]);
         }
     }
-    return NULL;
+
+    *isResolved = false;
+    return MeasureUnit();
 }
 
 MeasureUnit *MeasureUnit::create(int typeId, int subTypeId, UErrorCode &status) {
index 76c3a7ce5c5d16047f388950a221a2caaafc1c32..5e59c4c14e7a84bffa2de9f0c639220af700c655 100644 (file)
@@ -45,6 +45,25 @@ Derived NumberFormatterSettings<Derived>::adoptUnit(const icu::MeasureUnit *unit
     return copy;
 }
 
+template<typename Derived>
+Derived NumberFormatterSettings<Derived>::perUnit(const icu::MeasureUnit &perUnit) const {
+    Derived copy(*this);
+    // See comments above about slicing.
+    copy.fMacros.perUnit = perUnit;
+    return copy;
+}
+
+template<typename Derived>
+Derived NumberFormatterSettings<Derived>::adoptPerUnit(const icu::MeasureUnit *perUnit) const {
+    Derived copy(*this);
+    // See comments above about slicing and ownership.
+    if (perUnit != nullptr) {
+        copy.fMacros.perUnit = *perUnit;
+        delete perUnit;
+    }
+    return copy;
+}
+
 template<typename Derived>
 Derived NumberFormatterSettings<Derived>::rounding(const Rounder &rounder) const {
     Derived copy(*this);
index 9986ce6d8c606bcc7a5e5aec8ee4eb102368d942..fcabdcd83c93d00ba2ceb807d0acbcea08f2cfb3 100644 (file)
@@ -308,6 +308,7 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps &macros, bool safe,
                         LongNameHandler::forMeasureUnit(
                                 macros.locale,
                                 macros.unit,
+                                macros.perUnit,
                                 unitWidth,
                                 resolvePluralRules(macros.rules, macros.locale, status),
                                 chain,
index 88b3413585a0f736bf6c4fa3f24e7145bc7afac6..215df1a8d1c47252666e68563d8dd5008b7b1950 100644 (file)
@@ -5,6 +5,7 @@
 
 #if !UCONFIG_NO_FORMATTING && !UPRV_INCOMPLETE_CPP11_SUPPORT
 
+#include "unicode/simpleformatter.h"
 #include "unicode/ures.h"
 #include "ureslocs.h"
 #include "charstr.h"
@@ -19,6 +20,37 @@ using namespace icu::number::impl;
 
 namespace {
 
+constexpr int32_t DNAM_INDEX = StandardPlural::Form::COUNT;
+constexpr int32_t PER_INDEX = StandardPlural::Form::COUNT + 1;
+constexpr int32_t ARRAY_LENGTH = StandardPlural::Form::COUNT + 2;
+
+static int32_t getIndex(const char* pluralKeyword, UErrorCode& status) {
+    // pluralKeyword can also be "dnam" or "per"
+    if (uprv_strcmp(pluralKeyword, "dnam") == 0) {
+        return DNAM_INDEX;
+    } else if (uprv_strcmp(pluralKeyword, "per") == 0) {
+        return PER_INDEX;
+    } else {
+        StandardPlural::Form plural = StandardPlural::fromString(pluralKeyword, status);
+        return plural;
+    }
+}
+
+static UnicodeString getWithPlural(
+        const UnicodeString* strings,
+        int32_t plural,
+        UErrorCode& status) {
+    UnicodeString result = strings[plural];
+    if (result.isBogus()) {
+        result = strings[StandardPlural::Form::OTHER];
+    }
+    if (result.isBogus()) {
+        // There should always be data in the "other" plural variant.
+        status = U_INTERNAL_PROGRAM_ERROR;
+    }
+    return result;
+}
+
 
 //////////////////////////
 /// BEGIN DATA LOADING ///
@@ -28,7 +60,7 @@ class PluralTableSink : public ResourceSink {
   public:
     explicit PluralTableSink(UnicodeString *outArray) : outArray(outArray) {
         // Initialize the array to bogus strings.
-        for (int32_t i = 0; i < StandardPlural::Form::COUNT; i++) {
+        for (int32_t i = 0; i < ARRAY_LENGTH; i++) {
             outArray[i].setToBogus();
         }
     }
@@ -36,17 +68,13 @@ class PluralTableSink : public ResourceSink {
     void put(const char *key, ResourceValue &value, UBool /*noFallback*/, UErrorCode &status) U_OVERRIDE {
         ResourceTable pluralsTable = value.getTable(status);
         if (U_FAILURE(status)) { return; }
-        for (int i = 0; pluralsTable.getKeyAndValue(i, key, value); ++i) {
-            // In MeasureUnit data, ignore dnam and per units for now.
-            if (uprv_strcmp(key, "dnam") == 0 || uprv_strcmp(key, "per") == 0) {
-                continue;
-            }
-            StandardPlural::Form plural = StandardPlural::fromString(key, status);
+        for (int32_t i = 0; pluralsTable.getKeyAndValue(i, key, value); ++i) {
+            int32_t index = getIndex(key, status);
             if (U_FAILURE(status)) { return; }
-            if (!outArray[plural].isBogus()) {
+            if (!outArray[index].isBogus()) {
                 continue;
             }
-            outArray[plural] = value.getUnicodeString(status);
+            outArray[index] = value.getUnicodeString(status);
             if (U_FAILURE(status)) { return; }
         }
     }
@@ -105,6 +133,22 @@ void getCurrencyLongNameData(const Locale &locale, const CurrencyUnit &currency,
     }
 }
 
+UnicodeString getPerUnitFormat(const Locale& locale, const UNumberUnitWidth &width, UErrorCode& status) {
+    LocalUResourceBundlePointer unitsBundle(ures_open(U_ICUDATA_UNIT, locale.getName(), &status));
+    if (U_FAILURE(status)) { return {}; }
+    CharString key;
+    key.append("units", status);
+    if (width == UNUM_UNIT_WIDTH_NARROW) {
+        key.append("Narrow", status);
+    } else if (width == UNUM_UNIT_WIDTH_SHORT) {
+        key.append("Short", status);
+    }
+    key.append("/compound/per", status);
+    int32_t len = 0;
+    const UChar* ptr = ures_getStringByKeyWithFallback(unitsBundle.getAlias(), key.data(), &len, &status);
+    return UnicodeString(ptr, len);
+}
+
 ////////////////////////
 /// END DATA LOADING ///
 ////////////////////////
@@ -112,11 +156,24 @@ void getCurrencyLongNameData(const Locale &locale, const CurrencyUnit &currency,
 } // namespace
 
 LongNameHandler
-LongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &unit, const UNumberUnitWidth &width,
-                                const PluralRules *rules, const MicroPropsGenerator *parent,
-                                UErrorCode &status) {
+LongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &unitRef, const MeasureUnit &perUnit,
+                                const UNumberUnitWidth &width, const PluralRules *rules,
+                                const MicroPropsGenerator *parent, UErrorCode &status) {
+    MeasureUnit unit = unitRef;
+    if (uprv_strcmp(perUnit.getType(), "none") != 0) {
+        // Compound unit: first try to simplify (e.g., meters per second is its own unit).
+        bool isResolved = false;
+        MeasureUnit resolved = MeasureUnit::resolveUnitPerUnit(unit, perUnit, &isResolved);
+        if (isResolved) {
+            unit = resolved;
+        } else {
+            // No simplified form is available.
+            return forCompoundUnit(loc, unit, perUnit, width, rules, parent, status);
+        }
+    }
+
     LongNameHandler result(rules, parent);
-    UnicodeString simpleFormats[StandardPlural::Form::COUNT];
+    UnicodeString simpleFormats[ARRAY_LENGTH];
     getMeasureData(loc, unit, width, simpleFormats, status);
     if (U_FAILURE(status)) { return result; }
     // TODO: What field to use for units?
@@ -124,12 +181,47 @@ LongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &unit, cons
     return result;
 }
 
+LongNameHandler
+LongNameHandler::forCompoundUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit,
+                                 const UNumberUnitWidth &width, const PluralRules *rules,
+                                 const MicroPropsGenerator *parent, UErrorCode &status) {
+    LongNameHandler result(rules, parent);
+    UnicodeString primaryData[ARRAY_LENGTH];
+    getMeasureData(loc, unit, width, primaryData, status);
+    if (U_FAILURE(status)) { return result; }
+    UnicodeString secondaryData[ARRAY_LENGTH];
+    getMeasureData(loc, perUnit, width, secondaryData, status);
+    if (U_FAILURE(status)) { return result; }
+
+    UnicodeString perUnitFormat;
+    if (!secondaryData[PER_INDEX].isBogus()) {
+        perUnitFormat = secondaryData[PER_INDEX];
+    } else {
+        UnicodeString rawPerUnitFormat = getPerUnitFormat(loc, width, status);
+        if (U_FAILURE(status)) { return result; }
+        // rawPerUnitFormat is something like "{0}/{1}"; we need to substitute in the secondary unit.
+        SimpleFormatter compiled(rawPerUnitFormat, 2, 2, status);
+        if (U_FAILURE(status)) { return result; }
+        UnicodeString secondaryFormat = getWithPlural(secondaryData, StandardPlural::Form::ONE, status);
+        if (U_FAILURE(status)) { return result; }
+        SimpleFormatter secondaryCompiled(secondaryFormat, 1, 1, status);
+        if (U_FAILURE(status)) { return result; }
+        UnicodeString secondaryString = secondaryCompiled.getTextWithNoArguments().trim();
+        // TODO: Why does UnicodeString need to be explicit in the following line?
+        compiled.format(UnicodeString(u"{0}"), secondaryString, perUnitFormat, status);
+        if (U_FAILURE(status)) { return result; }
+    }
+    // TODO: What field to use for units?
+    multiSimpleFormatsToModifiers(primaryData, perUnitFormat, UNUM_FIELD_COUNT, result.fModifiers, status);
+    return result;
+}
+
 LongNameHandler LongNameHandler::forCurrencyLongNames(const Locale &loc, const CurrencyUnit &currency,
                                                       const PluralRules *rules,
                                                       const MicroPropsGenerator *parent,
                                                       UErrorCode &status) {
     LongNameHandler result(rules, parent);
-    UnicodeString simpleFormats[StandardPlural::Form::COUNT];
+    UnicodeString simpleFormats[ARRAY_LENGTH];
     getCurrencyLongNameData(loc, currency, simpleFormats, status);
     if (U_FAILURE(status)) { return result; }
     simpleFormatsToModifiers(simpleFormats, UNUM_CURRENCY_FIELD, result.fModifiers, status);
@@ -139,20 +231,30 @@ LongNameHandler LongNameHandler::forCurrencyLongNames(const Locale &loc, const C
 void LongNameHandler::simpleFormatsToModifiers(const UnicodeString *simpleFormats, Field field,
                                                SimpleModifier *output, UErrorCode &status) {
     for (int32_t i = 0; i < StandardPlural::Form::COUNT; i++) {
-        UnicodeString simpleFormat = simpleFormats[i];
-        if (simpleFormat.isBogus()) {
-            simpleFormat = simpleFormats[StandardPlural::Form::OTHER];
-        }
-        if (simpleFormat.isBogus()) {
-            // There should always be data in the "other" plural variant.
-            status = U_INTERNAL_PROGRAM_ERROR;
-            return;
-        }
+        UnicodeString simpleFormat = getWithPlural(simpleFormats, i, status);
+        if (U_FAILURE(status)) { return; }
         SimpleFormatter compiledFormatter(simpleFormat, 1, 1, status);
+        if (U_FAILURE(status)) { return; }
         output[i] = SimpleModifier(compiledFormatter, field, false);
     }
 }
 
+void LongNameHandler::multiSimpleFormatsToModifiers(const UnicodeString *leadFormats, UnicodeString trailFormat,
+                                                    Field field, SimpleModifier *output, UErrorCode &status) {
+    SimpleFormatter trailCompiled(trailFormat, 1, 1, status);
+    if (U_FAILURE(status)) { return; }
+    for (int32_t i = 0; i < StandardPlural::Form::COUNT; i++) {
+        UnicodeString leadFormat = getWithPlural(leadFormats, i, status);
+        if (U_FAILURE(status)) { return; }
+        UnicodeString compoundFormat;
+        trailCompiled.format(leadFormat, compoundFormat, status);
+        if (U_FAILURE(status)) { return; }
+        SimpleFormatter compoundCompiled(compoundFormat, 1, 1, status);
+        if (U_FAILURE(status)) { return; }
+        output[i] = SimpleModifier(compoundCompiled, field, false);
+    }
+}
+
 void LongNameHandler::processQuantity(DecimalQuantity &quantity, MicroProps &micros,
                                       UErrorCode &status) const {
     parent->processQuantity(quantity, micros, status);
index 22ecbac30e1ebc0fba697a0c1cfae0c026f5818d..8738bb99e7d2e64d4f90c3b5420f70b16fce186a 100644 (file)
@@ -21,8 +21,9 @@ class LongNameHandler : public MicroPropsGenerator, public UMemory {
                          const MicroPropsGenerator *parent, UErrorCode &status);
 
     static LongNameHandler
-    forMeasureUnit(const Locale &loc, const MeasureUnit &unit, const UNumberUnitWidth &width,
-                   const PluralRules *rules, const MicroPropsGenerator *parent, UErrorCode &status);
+    forMeasureUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit,
+                   const UNumberUnitWidth &width, const PluralRules *rules,
+                   const MicroPropsGenerator *parent, UErrorCode &status);
 
     void
     processQuantity(DecimalQuantity &quantity, MicroProps &micros, UErrorCode &status) const U_OVERRIDE;
@@ -35,8 +36,15 @@ class LongNameHandler : public MicroPropsGenerator, public UMemory {
     LongNameHandler(const PluralRules *rules, const MicroPropsGenerator *parent)
             : rules(rules), parent(parent) {}
 
+    static LongNameHandler
+    forCompoundUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit,
+                    const UNumberUnitWidth &width, const PluralRules *rules,
+                    const MicroPropsGenerator *parent, UErrorCode &status);
+
     static void simpleFormatsToModifiers(const UnicodeString *simpleFormats, Field field,
                                          SimpleModifier *output, UErrorCode &status);
+    static void multiSimpleFormatsToModifiers(const UnicodeString *leadFormats, UnicodeString trailFormat,
+                                         Field field, SimpleModifier *output, UErrorCode &status);
 };
 
 }  // namespace impl
index 4140ae3679f94e9ff7a29eed41d259e046847f2f..5e8b65310c71a84e65c9bfff14bcc9ef03647206 100644 (file)
@@ -196,8 +196,8 @@ class U_I18N_API MeasureUnit: public UObject {
      * ICU use only.
      * @internal
      */
-    static MeasureUnit *resolveUnitPerUnit(
-            const MeasureUnit &unit, const MeasureUnit &perUnit);
+    static MeasureUnit resolveUnitPerUnit(
+            const MeasureUnit &unit, const MeasureUnit &perUnit, bool* isResolved);
 #endif /* U_HIDE_INTERNAL_API */
 
 // All code between the "Start generated createXXX methods" comment and
index 04fc84b33aa338ffb26092a413871aebd2ac20c3..290e77e8806040a9ccce261db5131d33f307ddfe 100644 (file)
 #ifndef __NOUNIT_H__
 #define __NOUNIT_H__
 
+#include "unicode/utypes.h"
+
+#if !UCONFIG_NO_FORMATTING
+
+#include "unicode/measunit.h"
 
 /**
  * \file
  * \brief C++ API: units for percent and permille
  */
 
-
-#include "unicode/measunit.h"
-
-#if !UCONFIG_NO_FORMATTING
-
 U_NAMESPACE_BEGIN
 
 #ifndef U_HIDE_DRAFT_API
index 546b1875ff4baff49c0578279746ed89d5417272..2215d096d7f3ce6bac2f1fd77db19323a597fa35 100644 (file)
@@ -1297,6 +1297,9 @@ struct U_I18N_API MacroProps : public UMemory {
     /** @internal */
     MeasureUnit unit; // = NoUnit::base();
 
+    /** @internal */
+    MeasureUnit perUnit; // = NoUnit::base();
+
     /** @internal */
     Rounder rounder;  // = Rounder();  (bogus)
 
@@ -1389,29 +1392,29 @@ class U_I18N_API NumberFormatterSettings {
      * <li>Percent: "12.3%"
      * </ul>
      *
-     * <p>
      * All units will be properly localized with locale data, and all units are compatible with notation styles,
      * rounding strategies, and other number formatter settings.
      *
-     * <p>
      * Pass this method any instance of {@link MeasureUnit}. For units of measure:
      *
      * <pre>
-     * NumberFormatter.with().adoptUnit(MeasureUnit::createMeter(status))
+     * NumberFormatter::with().adoptUnit(MeasureUnit::createMeter(status))
      * </pre>
      *
      * Currency:
      *
      * <pre>
-     * NumberFormatter.with()::unit(CurrencyUnit(u"USD", status))
+     * NumberFormatter::with().unit(CurrencyUnit(u"USD", status))
      * </pre>
      *
      * Percent:
      *
      * <pre>
-     * NumberFormatter.with()::unit(NoUnit.percent())
+     * NumberFormatter::with().unit(NoUnit.percent())
      * </pre>
      *
+     * See {@link #perUnit} for information on how to format strings like "5 meters per second".
+     *
      * The default is to render without units (equivalent to NoUnit.base()).
      *
      * @param unit
@@ -1420,6 +1423,7 @@ class U_I18N_API NumberFormatterSettings {
      * @see MeasureUnit
      * @see Currency
      * @see NoUnit
+     * @see #perUnit
      * @draft ICU 60
      */
     Derived unit(const icu::MeasureUnit &unit) const;
@@ -1429,7 +1433,7 @@ class U_I18N_API NumberFormatterSettings {
      * methods, which return pointers that need ownership.
      *
      * @param unit
-     * The unit to render.
+     *            The unit to render.
      * @return The fluent chain.
      * @see #unit
      * @see MeasureUnit
@@ -1437,6 +1441,43 @@ class U_I18N_API NumberFormatterSettings {
      */
     Derived adoptUnit(const icu::MeasureUnit *unit) const;
 
+    /**
+     * Sets a unit to be used in the denominator. For example, to format "3 m/s", pass METER to the unit and SECOND to
+     * the perUnit.
+     *
+     * Pass this method any instance of {@link MeasureUnit}. For example:
+     *
+     * <pre>
+     * NumberFormatter::with()
+     *      .adoptUnit(MeasureUnit::createMeter(status))
+     *      .adoptPerUnit(MeasureUnit::createSecond(status))
+     * </pre>
+     *
+     * The default is not to display any unit in the denominator.
+     *
+     * If a per-unit is specified without a primary unit via {@link #unit}, the behavior is undefined.
+     *
+     * @param perUnit
+     *            The unit to render in the denominator.
+     * @return The fluent chain
+     * @see #unit
+     * @draft ICU 61
+     */
+    Derived perUnit(const icu::MeasureUnit &perUnit) const;
+
+    /**
+     * Like perUnit(), but takes ownership of a pointer.  Convenient for use with the MeasureFormat factory
+     * methods, which return pointers that need ownership.
+     *
+     * @param perUnit
+     *            The unit to render in the denominator.
+     * @return The fluent chain.
+     * @see #perUnit
+     * @see MeasureUnit
+     * @draft ICU 61
+     */
+    Derived adoptPerUnit(const icu::MeasureUnit *perUnit) const;
+
     /**
      * Specifies the rounding strategy to use when formatting numbers.
      *
index 3e600318339c3a7a468c3a3aebd862049f058575..3e77da703eb146066e148076c1c0672f24cce0c3 100644 (file)
@@ -45,6 +45,7 @@ class NumberFormatterApiTest : public IntlTest {
     void notationScientific();
     void notationCompact();
     void unitMeasure();
+    void unitCompoundMeasure();
     void unitCurrency();
     void unitPercent();
     void roundingFraction();
@@ -75,6 +76,11 @@ class NumberFormatterApiTest : public IntlTest {
     MeasureUnit DAY;
     MeasureUnit SQUARE_METER;
     MeasureUnit FAHRENHEIT;
+    MeasureUnit SECOND;
+    MeasureUnit POUND;
+    MeasureUnit SQUARE_MILE;
+    MeasureUnit JOULE;
+    MeasureUnit FURLONG;
 
     NumberingSystem MATHSANB;
     NumberingSystem LATN;
index 1f5cfaa3ff5b41148fccc21f720fc94a39f430d9..ffa4858f34314564445b9826562090e61a80aaa6 100644 (file)
@@ -26,29 +26,25 @@ NumberFormatterApiTest::NumberFormatterApiTest(UErrorCode &status)
                 SWISS_SYMBOLS(Locale("de-CH"), status),
                 MYANMAR_SYMBOLS(Locale("my"), status) {
 
-    MeasureUnit *unit = MeasureUnit::createMeter(status);
+    // Check for error on the first MeasureUnit in case there is no data
+    LocalPointer<MeasureUnit> unit(MeasureUnit::createMeter(status));
     if (U_FAILURE(status)) {
         dataerrln("%s %d status = %s", __FILE__, __LINE__, u_errorName(status));
         return;
     }
     METER = *unit;
-    delete unit;
-    unit = MeasureUnit::createDay(status);
-    DAY = *unit;
-    delete unit;
-    unit = MeasureUnit::createSquareMeter(status);
-    SQUARE_METER = *unit;
-    delete unit;
-    unit = MeasureUnit::createFahrenheit(status);
-    FAHRENHEIT = *unit;
-    delete unit;
-
-    NumberingSystem *ns = NumberingSystem::createInstanceByName("mathsanb", status);
-    MATHSANB = *ns;
-    delete ns;
-    ns = NumberingSystem::createInstanceByName("latn", status);
-    LATN = *ns;
-    delete ns;
+
+    DAY = *LocalPointer<MeasureUnit>(MeasureUnit::createDay(status));
+    SQUARE_METER = *LocalPointer<MeasureUnit>(MeasureUnit::createSquareMeter(status));
+    FAHRENHEIT = *LocalPointer<MeasureUnit>(MeasureUnit::createFahrenheit(status));
+    SECOND = *LocalPointer<MeasureUnit>(MeasureUnit::createSecond(status));
+    POUND = *LocalPointer<MeasureUnit>(MeasureUnit::createPound(status));
+    SQUARE_MILE = *LocalPointer<MeasureUnit>(MeasureUnit::createSquareMile(status));
+    JOULE = *LocalPointer<MeasureUnit>(MeasureUnit::createJoule(status));
+    FURLONG = *LocalPointer<MeasureUnit>(MeasureUnit::createFurlong(status));
+
+    MATHSANB = *LocalPointer<NumberingSystem>(NumberingSystem::createInstanceByName("mathsanb", status));
+    LATN = *LocalPointer<NumberingSystem>(NumberingSystem::createInstanceByName("latn", status));
 }
 
 void NumberFormatterApiTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char *) {
@@ -60,6 +56,7 @@ void NumberFormatterApiTest::runIndexedTest(int32_t index, UBool exec, const cha
         TESTCASE_AUTO(notationScientific);
         TESTCASE_AUTO(notationCompact);
         TESTCASE_AUTO(unitMeasure);
+        TESTCASE_AUTO(unitCompoundMeasure);
         TESTCASE_AUTO(unitCurrency);
         TESTCASE_AUTO(unitPercent);
         TESTCASE_AUTO(roundingFraction);
@@ -355,8 +352,8 @@ void NumberFormatterApiTest::notationCompact() {
 
 void NumberFormatterApiTest::unitMeasure() {
     assertFormatDescending(
-            u"Meters Short",
-            NumberFormatter::with().adoptUnit(new MeasureUnit(METER)),
+            u"Meters Short and unit() method",
+            NumberFormatter::with().unit(METER),
             Locale::getEnglish(),
             u"87,650 m",
             u"8,765 m",
@@ -369,7 +366,7 @@ void NumberFormatterApiTest::unitMeasure() {
             u"0 m");
 
     assertFormatDescending(
-            u"Meters Long",
+            u"Meters Long and adoptUnit() method",
             NumberFormatter::with().adoptUnit(new MeasureUnit(METER))
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME),
             Locale::getEnglish(),
@@ -386,7 +383,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatDescending(
             u"Compact Meters Long",
             NumberFormatter::with().notation(Notation::compactLong())
-                    .adoptUnit(new MeasureUnit(METER))
+                    .unit(METER)
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME),
             Locale::getEnglish(),
             u"88 thousand meters",
@@ -410,14 +407,14 @@ void NumberFormatterApiTest::unitMeasure() {
 //    TODO: Implement Measure in C++
 //    assertFormatSingleMeasure(
 //            u"Measure format method takes precedence over fluent chain",
-//            NumberFormatter::with().adoptUnit(new MeasureUnit(METER)),
+//            NumberFormatter::with().unit(METER),
 //            Locale::getEnglish(),
 //            new Measure(5.43, USD),
 //            u"$5.43");
 
     assertFormatSingle(
             u"Meters with Negative Sign",
-            NumberFormatter::with().adoptUnit(new MeasureUnit(METER)),
+            NumberFormatter::with().unit(METER),
             Locale::getEnglish(),
             -9876543.21,
             u"-9,876,543.21 m");
@@ -425,7 +422,7 @@ void NumberFormatterApiTest::unitMeasure() {
     // The locale string "सान" appears only in brx.txt:
     assertFormatSingle(
             u"Interesting Data Fallback 1",
-            NumberFormatter::with().adoptUnit(new MeasureUnit(DAY))
+            NumberFormatter::with().unit(DAY)
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME),
             Locale::createFromName("brx"),
             5.43,
@@ -434,7 +431,7 @@ void NumberFormatterApiTest::unitMeasure() {
     // Requires following the alias from unitsNarrow to unitsShort:
     assertFormatSingle(
             u"Interesting Data Fallback 2",
-            NumberFormatter::with().adoptUnit(new MeasureUnit(DAY))
+            NumberFormatter::with().unit(DAY)
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_NARROW),
             Locale::createFromName("brx"),
             5.43,
@@ -444,7 +441,7 @@ void NumberFormatterApiTest::unitMeasure() {
     // requiring fallback to the root.
     assertFormatSingle(
             u"Interesting Data Fallback 3",
-            NumberFormatter::with().adoptUnit(new MeasureUnit(SQUARE_METER))
+            NumberFormatter::with().unit(SQUARE_METER)
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_NARROW),
             Locale::createFromName("en-GB"),
             5.43,
@@ -454,7 +451,7 @@ void NumberFormatterApiTest::unitMeasure() {
     // NOTE: This example is in the documentation.
     assertFormatSingle(
             u"Difference between Narrow and Short (Narrow Version)",
-            NumberFormatter::with().adoptUnit(new MeasureUnit(FAHRENHEIT))
+            NumberFormatter::with().unit(FAHRENHEIT)
                     .unitWidth(UNUM_UNIT_WIDTH_NARROW),
             Locale("es-US"),
             5.43,
@@ -462,13 +459,57 @@ void NumberFormatterApiTest::unitMeasure() {
 
     assertFormatSingle(
             u"Difference between Narrow and Short (Short Version)",
-            NumberFormatter::with().adoptUnit(new MeasureUnit(FAHRENHEIT))
+            NumberFormatter::with().unit(FAHRENHEIT)
                     .unitWidth(UNUM_UNIT_WIDTH_SHORT),
             Locale("es-US"),
             5.43,
             u"5.43 °F");
 }
 
+void NumberFormatterApiTest::unitCompoundMeasure() {
+    assertFormatDescending(
+            u"Meters Per Second Short (unit that simplifies) and perUnit method",
+            NumberFormatter::with().unit(METER).perUnit(SECOND),
+            Locale::getEnglish(),
+            u"87,650 m/s",
+            u"8,765 m/s",
+            u"876.5 m/s",
+            u"87.65 m/s",
+            u"8.765 m/s",
+            u"0.8765 m/s",
+            u"0.08765 m/s",
+            u"0.008765 m/s",
+            u"0 m/s");
+
+    assertFormatDescending(
+            u"Pounds Per Square Mile Short (secondary unit has per-format) and adoptPerUnit method",
+            NumberFormatter::with().unit(POUND).adoptPerUnit(new MeasureUnit(SQUARE_MILE)),
+            Locale::getEnglish(),
+            u"87,650 lb/mi²",
+            u"8,765 lb/mi²",
+            u"876.5 lb/mi²",
+            u"87.65 lb/mi²",
+            u"8.765 lb/mi²",
+            u"0.8765 lb/mi²",
+            u"0.08765 lb/mi²",
+            u"0.008765 lb/mi²",
+            u"0 lb/mi²");
+
+    assertFormatDescending(
+            u"Joules Per Furlong Short (unit with no simplifications or special patterns)",
+            NumberFormatter::with().unit(JOULE).perUnit(FURLONG),
+            Locale::getEnglish(),
+            u"87,650 J/fur",
+            u"8,765 J/fur",
+            u"876.5 J/fur",
+            u"87.65 J/fur",
+            u"8.765 J/fur",
+            u"0.8765 J/fur",
+            u"0.08765 J/fur",
+            u"0.008765 J/fur",
+            u"0 J/fur");
+}
+
 void NumberFormatterApiTest::unitCurrency() {
     assertFormatDescending(
             u"Currency",
index 2ed9f4d63cd5441a1685e254d5cf706b381eade6..f807c0bbbb203d14837e23910555e3a71916210b 100644 (file)
@@ -4,6 +4,7 @@ package com.ibm.icu.impl.number;
 
 import java.util.EnumMap;
 import java.util.Map;
+import java.util.MissingResourceException;
 
 import com.ibm.icu.impl.CurrencyData;
 import com.ibm.icu.impl.ICUData;
@@ -22,38 +23,63 @@ import com.ibm.icu.util.UResourceBundle;
 
 public class LongNameHandler implements MicroPropsGenerator {
 
+    private static final int DNAM_INDEX = StandardPlural.COUNT;
+    private static final int PER_INDEX = StandardPlural.COUNT + 1;
+    private static final int ARRAY_LENGTH = StandardPlural.COUNT + 2;
+
+    private static int getIndex(String pluralKeyword) {
+        // pluralKeyword can also be "dnam" or "per"
+        if (pluralKeyword.equals("dnam")) {
+            return DNAM_INDEX;
+        } else if (pluralKeyword.equals("per")) {
+            return PER_INDEX;
+        } else {
+            return StandardPlural.fromString(pluralKeyword).ordinal();
+        }
+    }
+
+    private static String getWithPlural(String[] strings, StandardPlural plural) {
+        String result = strings[plural.ordinal()];
+        if (result == null) {
+            result = strings[StandardPlural.OTHER.ordinal()];
+        }
+        if (result == null) {
+            // There should always be data in the "other" plural variant.
+            throw new ICUException("Could not find data in 'other' plural variant");
+        }
+        return result;
+    }
+
     //////////////////////////
     /// BEGIN DATA LOADING ///
     //////////////////////////
 
     private static final class PluralTableSink extends UResource.Sink {
 
-        Map<StandardPlural, String> output;
+        String[] outArray;
 
-        public PluralTableSink(Map<StandardPlural, String> output) {
-            this.output = output;
+        public PluralTableSink(String[] outArray) {
+            this.outArray = outArray;
         }
 
         @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) {
-                if (key.contentEquals("dnam") || key.contentEquals("per")) {
-                    continue;
-                }
-                StandardPlural plural = StandardPlural.fromString(key);
-                if (output.containsKey(plural)) {
+                int index = getIndex(key.toString());
+                if (outArray[index] != null) {
                     continue;
                 }
                 String formatString = value.getString();
-                output.put(plural, formatString);
+                outArray[index] = formatString;
             }
         }
     }
 
-    private static void getMeasureData(ULocale locale, MeasureUnit unit, UnitWidth width,
-            Map<StandardPlural, String> output) {
-        PluralTableSink sink = new PluralTableSink(output);
+    // NOTE: outArray MUST have at least ARRAY_LENGTH entries. No bounds checking is performed.
+
+    private static void getMeasureData(ULocale locale, MeasureUnit unit, UnitWidth width, String[] outArray) {
+        PluralTableSink sink = new PluralTableSink(outArray);
         ICUResourceBundle resource;
         resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale);
         StringBuilder key = new StringBuilder();
@@ -67,16 +93,20 @@ public class LongNameHandler implements MicroPropsGenerator {
         key.append(unit.getType());
         key.append("/");
         key.append(unit.getSubtype());
-        resource.getAllItemsWithFallback(key.toString(), sink);
+        try {
+            resource.getAllItemsWithFallback(key.toString(), sink);
+        } catch (MissingResourceException e) {
+            throw new IllegalArgumentException("No data for unit " + unit + ", width " + width, e);
+        }
     }
 
-    private static void getCurrencyLongNameData(ULocale locale, Currency currency, Map<StandardPlural, String> output) {
+    private static void getCurrencyLongNameData(ULocale locale, Currency currency, String[] outArray) {
         // In ICU4J, this method gets a CurrencyData from CurrencyData.provider.
         // TODO(ICU4J): Implement this without going through CurrencyData, like in ICU4C?
         Map<String, String> data = CurrencyData.provider.getInstance(locale, true).getUnitPatterns();
         for (Map.Entry<String, String> e : data.entrySet()) {
             String pluralKeyword = e.getKey();
-            StandardPlural plural = StandardPlural.fromString(e.getKey());
+            int index = getIndex(pluralKeyword);
             String longName = currency.getName(locale, Currency.PLURAL_LONG_NAME, pluralKeyword, null);
             String simpleFormat = e.getValue();
             // Example pattern from data: "{0} {1}"
@@ -84,7 +114,25 @@ public class LongNameHandler implements MicroPropsGenerator {
             simpleFormat = simpleFormat.replace("{1}", longName);
             // String compiled = SimpleFormatterImpl.compileToStringMinMaxArguments(simpleFormat, sb, 1, 1);
             // SimpleModifier mod = new SimpleModifier(compiled, Field.CURRENCY, false);
-            output.put(plural, simpleFormat);
+            outArray[index] = simpleFormat;
+        }
+    }
+
+    private static String getPerUnitFormat(ULocale locale, UnitWidth width) {
+        ICUResourceBundle resource;
+        resource = (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("/compound/per");
+        try {
+            return resource.getStringWithFallback(key.toString());
+        } catch (MissingResourceException e) {
+            throw new IllegalArgumentException("Could not find x-per-y format for " + locale + ", width " + width);
         }
     }
 
@@ -105,7 +153,7 @@ public class LongNameHandler implements MicroPropsGenerator {
 
     public static LongNameHandler forCurrencyLongNames(ULocale locale, Currency currency, PluralRules rules,
             MicroPropsGenerator parent) {
-        Map<StandardPlural, String> simpleFormats = new EnumMap<StandardPlural, String>(StandardPlural.class);
+        String[] simpleFormats = new String[ARRAY_LENGTH];
         getCurrencyLongNameData(locale, currency, simpleFormats);
         // TODO(ICU4J): Reduce the number of object creations here?
         Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<StandardPlural, SimpleModifier>(
@@ -114,9 +162,20 @@ public class LongNameHandler implements MicroPropsGenerator {
         return new LongNameHandler(modifiers, rules, parent);
     }
 
-    public static LongNameHandler forMeasureUnit(ULocale locale, MeasureUnit unit, UnitWidth width, PluralRules rules,
-            MicroPropsGenerator parent) {
-        Map<StandardPlural, String> simpleFormats = new EnumMap<StandardPlural, String>(StandardPlural.class);
+    public static LongNameHandler forMeasureUnit(ULocale locale, MeasureUnit unit, MeasureUnit perUnit, UnitWidth width,
+            PluralRules rules, MicroPropsGenerator parent) {
+        if (perUnit != null) {
+            // Compound unit: first try to simplify (e.g., meters per second is its own unit).
+            MeasureUnit simplified = MeasureUnit.resolveUnitPerUnit(unit, perUnit);
+            if (simplified != null) {
+                unit = simplified;
+            } else {
+                // No simplified form is available.
+                return forCompoundUnit(locale, unit, perUnit, width, rules, parent);
+            }
+        }
+
+        String[] simpleFormats = new String[ARRAY_LENGTH];
         getMeasureData(locale, unit, width, simpleFormats);
         // TODO: What field to use for units?
         // TODO(ICU4J): Reduce the number of object creations here?
@@ -126,20 +185,52 @@ public class LongNameHandler implements MicroPropsGenerator {
         return new LongNameHandler(modifiers, rules, parent);
     }
 
-    private static void simpleFormatsToModifiers(Map<StandardPlural, String> simpleFormats, NumberFormat.Field field,
+    private static LongNameHandler forCompoundUnit(ULocale locale, MeasureUnit unit, MeasureUnit perUnit,
+            UnitWidth width, PluralRules rules, MicroPropsGenerator parent) {
+        String[] primaryData = new String[ARRAY_LENGTH];
+        getMeasureData(locale, unit, width, primaryData);
+        String[] secondaryData = new String[ARRAY_LENGTH];
+        getMeasureData(locale, perUnit, width, secondaryData);
+        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);
+            String secondaryCompiled = SimpleFormatterImpl.compileToStringMinMaxArguments(secondaryFormat, sb, 1, 1);
+            String secondaryString = SimpleFormatterImpl.getTextWithNoArguments(secondaryCompiled).trim();
+            perUnitFormat = SimpleFormatterImpl.formatCompiledPattern(compiled, "{0}", secondaryString);
+        }
+        // TODO: What field to use for units?
+        Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<StandardPlural, SimpleModifier>(
+                StandardPlural.class);
+        multiSimpleFormatsToModifiers(primaryData, perUnitFormat, null, modifiers);
+        return new LongNameHandler(modifiers, rules, parent);
+    }
+
+    private static void simpleFormatsToModifiers(String[] simpleFormats, NumberFormat.Field field,
             Map<StandardPlural, SimpleModifier> output) {
         StringBuilder sb = new StringBuilder();
         for (StandardPlural plural : StandardPlural.VALUES) {
-            String simpleFormat = simpleFormats.get(plural);
-            if (simpleFormat == null) {
-                simpleFormat = simpleFormats.get(StandardPlural.OTHER);
-            }
-            if (simpleFormat == null) {
-                // There should always be data in the "other" plural variant.
-                throw new ICUException("Could not find data in 'other' plural variant with field " + field);
-            }
+            String simpleFormat = getWithPlural(simpleFormats, plural);
             String compiled = SimpleFormatterImpl.compileToStringMinMaxArguments(simpleFormat, sb, 1, 1);
-            output.put(plural, new SimpleModifier(compiled, null, false));
+            output.put(plural, new SimpleModifier(compiled, field, false));
+        }
+    }
+
+    private static void multiSimpleFormatsToModifiers(String[] leadFormats, String trailFormat,
+            NumberFormat.Field field, Map<StandardPlural, SimpleModifier> output) {
+        StringBuilder sb = new StringBuilder();
+        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, 1, 1);
+            output.put(plural, new SimpleModifier(compoundCompiled, field, false));
         }
     }
 
index 711b4e2f0a1f54b691d377f1cd8b1a80c07fbfdc..43bde7b4de176ed5299a6cbe73c9fd7c689c2be8 100644 (file)
@@ -17,6 +17,7 @@ import com.ibm.icu.util.ULocale;
 public class MacroProps implements Cloneable {
   public Notation notation;
   public MeasureUnit unit;
+  public MeasureUnit perUnit;
   public Rounder rounder;
   public Grouper grouper;
   public Padder padder;
@@ -39,6 +40,7 @@ public class MacroProps implements Cloneable {
   public void fallback(MacroProps fallback) {
     if (notation == null) notation = fallback.notation;
     if (unit == null) unit = fallback.unit;
+    if (perUnit == null) perUnit = fallback.perUnit;
     if (rounder == null) rounder = fallback.rounder;
     if (grouper == null) grouper = fallback.grouper;
     if (padder == null) padder = fallback.padder;
@@ -58,6 +60,7 @@ public class MacroProps implements Cloneable {
     return Utility.hash(
         notation,
         unit,
+        perUnit,
         rounder,
         grouper,
         padder,
@@ -80,6 +83,7 @@ public class MacroProps implements Cloneable {
     MacroProps other = (MacroProps) _other;
     return Utility.equals(notation, other.notation)
         && Utility.equals(unit, other.unit)
+        && Utility.equals(perUnit, other.perUnit)
         && Utility.equals(rounder, other.rounder)
         && Utility.equals(grouper, other.grouper)
         && Utility.equals(padder, other.padder)
index 74b49426027cae012c82116dd39b5122f0e3cb38..bb07017ecd1aa396ca3cf290e66a836c1855383d 100644 (file)
@@ -247,7 +247,7 @@ class NumberFormatterImpl {
                 // Lazily create PluralRules
                 rules = PluralRules.forLocale(macros.loc);
             }
-            chain = LongNameHandler.forMeasureUnit(macros.loc, macros.unit, unitWidth, rules, chain);
+            chain = LongNameHandler.forMeasureUnit(macros.loc, macros.unit, macros.perUnit, unitWidth, rules, chain);
         } else if (isCurrency && unitWidth == UnitWidth.FULL_NAME) {
             if (rules == null) {
                 // Lazily create PluralRules
index 884fb64c8961493f07cfddc3a770efbf18d8289f..05adfa7ce35ec8880e407a082852f4200d144f5b 100644 (file)
@@ -39,7 +39,8 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
     static final int KEY_SIGN = 10;
     static final int KEY_DECIMAL = 11;
     static final int KEY_THRESHOLD = 12;
-    static final int KEY_MAX = 13;
+    static final int KEY_PER_UNIT = 13;
+    static final int KEY_MAX = 14;
 
     final NumberFormatterSettings<?> parent;
     final int key;
@@ -123,6 +124,10 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
      * NumberFormatter.with().unit(NoUnit.PERCENT)
      * </pre>
      *
+     * <p>
+     * See {@link #perUnit} for information on how to format strings like "5 meters per second".
+     *
+     * <p>
      * The default is to render without units (equivalent to {@link NoUnit#BASE}).
      *
      * @param unit
@@ -131,6 +136,7 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
      * @see MeasureUnit
      * @see Currency
      * @see NoUnit
+     * @see #perUnit
      * @draft ICU 60
      * @provisional This API might change or be removed in a future release.
      */
@@ -138,6 +144,34 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
         return create(KEY_UNIT, unit);
     }
 
+    /**
+     * Sets a unit to be used in the denominator. For example, to format "3 m/s", pass METER to the unit and SECOND to
+     * the perUnit.
+     *
+     * <p>
+     * Pass this method any instance of {@link MeasureUnit}. For example:
+     *
+     * <pre>
+     * NumberFormatter.with().unit(MeasureUnit.METER).perUnit(MeasureUnit.SECOND)
+     * </pre>
+     *
+     * <p>
+     * The default is not to display any unit in the denominator.
+     *
+     * <p>
+     * If a per-unit is specified without a primary unit via {@link #unit}, the behavior is undefined.
+     *
+     * @param perUnit
+     *            The unit to render in the denominator.
+     * @return The fluent chain
+     * @see #unit
+     * @draft ICU 61
+     * @provisional This API might change or be removed in a future release.
+     */
+    public T perUnit(MeasureUnit perUnit) {
+        return create(KEY_PER_UNIT, perUnit);
+    }
+
     /**
      * Specifies the rounding strategy to use when formatting numbers.
      *
@@ -518,6 +552,11 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
                     macros.threshold = (Long) current.value;
                 }
                 break;
+            case KEY_PER_UNIT:
+                if (macros.perUnit == null) {
+                    macros.perUnit = (MeasureUnit) current.value;
+                }
+                break;
             default:
                 throw new AssertionError("Unknown key: " + current.key);
             }
index b1c3fa641abe5915b5c0d1f36a96e5b2e0ea3655..50ebcc1e6300bcf771b1b6235f28222e903ef0da 100644 (file)
@@ -171,7 +171,7 @@ public class MeasureUnit implements Serializable {
     }
 
     /**
-     * Create a MeasureUnit instance (creates a singleton instance).
+     * Creates a MeasureUnit instance (creates a singleton instance) or returns one from the cache.
      * <p>
      * Normally this method should not be used, since there will be no formatting data
      * available for it, and it may not be returned by getAvailable().
index af6df82eeac5295ea452a521eae4efa7a10568f0..3af4044a463bbfc7168a13c178a625460d16e3d1 100644 (file)
@@ -475,6 +475,54 @@ public class NumberFormatterApiTest {
                 "5.43 °F");
     }
 
+    @Test
+    public void unitCompoundMeasure() {
+        assertFormatDescending(
+                "Meters Per Second Short (unit that simplifies)",
+                "",
+                NumberFormatter.with().unit(MeasureUnit.METER).perUnit(MeasureUnit.SECOND),
+                ULocale.ENGLISH,
+                "87,650 m/s",
+                "8,765 m/s",
+                "876.5 m/s",
+                "87.65 m/s",
+                "8.765 m/s",
+                "0.8765 m/s",
+                "0.08765 m/s",
+                "0.008765 m/s",
+                "0 m/s");
+
+        assertFormatDescending(
+                "Pounds Per Square Mile Short (secondary unit has per-format)",
+                "",
+                NumberFormatter.with().unit(MeasureUnit.POUND).perUnit(MeasureUnit.SQUARE_MILE),
+                ULocale.ENGLISH,
+                "87,650 lb/mi²",
+                "8,765 lb/mi²",
+                "876.5 lb/mi²",
+                "87.65 lb/mi²",
+                "8.765 lb/mi²",
+                "0.8765 lb/mi²",
+                "0.08765 lb/mi²",
+                "0.008765 lb/mi²",
+                "0 lb/mi²");
+
+        assertFormatDescending(
+                "Joules Per Furlong Short (unit with no simplifications or special patterns)",
+                "",
+                NumberFormatter.with().unit(MeasureUnit.JOULE).perUnit(MeasureUnit.FURLONG),
+                ULocale.ENGLISH,
+                "87,650 J/fur",
+                "8,765 J/fur",
+                "876.5 J/fur",
+                "87.65 J/fur",
+                "8.765 J/fur",
+                "0.8765 J/fur",
+                "0.08765 J/fur",
+                "0.008765 J/fur",
+                "0 J/fur");
+    }
+
     @Test
     public void unitCurrency() {
         assertFormatDescending(