ICU-13520 Adds compound unit support to NumberFormatter.

X-SVN-Rev: 40747
This commit is contained in:
Shane Carr 2017-12-22 00:02:01 +00:00
parent 73569e99bd
commit 4d10bf03f5
17 changed files with 511 additions and 107 deletions

View 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);
}

View 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) {

View 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);

View 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,

View 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);

View 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

View 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

View File

@ -10,17 +10,17 @@
#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

View 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.
*

View 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;

View 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",

View 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));
}
}

View 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)

View 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

View 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);
}

View 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().

View 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(