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);
}
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();
} 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) {
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);
LongNameHandler::forMeasureUnit(
macros.locale,
macros.unit,
+ macros.perUnit,
unitWidth,
resolvePluralRules(macros.rules, macros.locale, status),
chain,
#if !UCONFIG_NO_FORMATTING && !UPRV_INCOMPLETE_CPP11_SUPPORT
+#include "unicode/simpleformatter.h"
#include "unicode/ures.h"
#include "ureslocs.h"
#include "charstr.h"
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 ///
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();
}
}
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; }
}
}
}
}
+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 ///
////////////////////////
} // 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?
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 ¤cy,
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);
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 µs,
UErrorCode &status) const {
parent->processQuantity(quantity, micros, status);
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 µs, UErrorCode &status) const U_OVERRIDE;
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
* 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
#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
/** @internal */
MeasureUnit unit; // = NoUnit::base();
+ /** @internal */
+ MeasureUnit perUnit; // = NoUnit::base();
+
/** @internal */
Rounder rounder; // = Rounder(); (bogus)
* <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
* @see MeasureUnit
* @see Currency
* @see NoUnit
+ * @see #perUnit
* @draft ICU 60
*/
Derived unit(const icu::MeasureUnit &unit) const;
* 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
*/
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.
*
void notationScientific();
void notationCompact();
void unitMeasure();
+ void unitCompoundMeasure();
void unitCurrency();
void unitPercent();
void roundingFraction();
MeasureUnit DAY;
MeasureUnit SQUARE_METER;
MeasureUnit FAHRENHEIT;
+ MeasureUnit SECOND;
+ MeasureUnit POUND;
+ MeasureUnit SQUARE_MILE;
+ MeasureUnit JOULE;
+ MeasureUnit FURLONG;
NumberingSystem MATHSANB;
NumberingSystem LATN;
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 *) {
TESTCASE_AUTO(notationScientific);
TESTCASE_AUTO(notationCompact);
TESTCASE_AUTO(unitMeasure);
+ TESTCASE_AUTO(unitCompoundMeasure);
TESTCASE_AUTO(unitCurrency);
TESTCASE_AUTO(unitPercent);
TESTCASE_AUTO(roundingFraction);
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",
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(),
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",
// 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");
// 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,
// 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,
// 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,
// 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,
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",
import java.util.EnumMap;
import java.util.Map;
+import java.util.MissingResourceException;
import com.ibm.icu.impl.CurrencyData;
import com.ibm.icu.impl.ICUData;
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();
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}"
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);
}
}
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>(
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?
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));
}
}
public class MacroProps implements Cloneable {
public Notation notation;
public MeasureUnit unit;
+ public MeasureUnit perUnit;
public Rounder rounder;
public Grouper grouper;
public Padder padder;
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;
return Utility.hash(
notation,
unit,
+ perUnit,
rounder,
grouper,
padder,
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)
// 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
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;
* 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
* @see MeasureUnit
* @see Currency
* @see NoUnit
+ * @see #perUnit
* @draft ICU 60
* @provisional This API might change or be removed in a future release.
*/
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.
*
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);
}
}
/**
- * 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().
"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(