ICU-21266 Support toSkeleton() for all functional Unit Formatters
See #1347
diff --git a/icu4c/source/i18n/measunit.cpp b/icu4c/source/i18n/measunit.cpp
index 808bf90..02624f8 100644
--- a/icu4c/source/i18n/measunit.cpp
+++ b/icu4c/source/i18n/measunit.cpp
@@ -612,21 +612,7 @@
"teaspoon"
};
-// Must be sorted by first value and then second value.
-static int32_t unitPerUnitToSingleUnit[][4] = {
- {374, 383, 12, 1},
- {374, 389, 12, 2},
- {379, 383, 12, 6},
- {379, 389, 12, 7},
- {390, 343, 19, 0},
- {392, 350, 19, 2},
- {394, 343, 19, 3},
- {394, 472, 4, 2},
- {394, 473, 4, 3},
- {416, 465, 3, 1},
- {419, 12, 18, 9},
- {476, 390, 4, 1}
-};
+// unitPerUnitToSingleUnit no longer in use! TODO: remove from code-generation code.
// Shortcuts to the base unit in order to make the default constructor fast
static const int32_t kBaseTypeIdx = 16;
@@ -2301,41 +2287,6 @@
return false;
}
-MeasureUnit MeasureUnit::resolveUnitPerUnit(
- const MeasureUnit &unit, const MeasureUnit &perUnit, bool* isResolved) {
- int32_t unitOffset = unit.getOffset();
- int32_t perUnitOffset = perUnit.getOffset();
- if (unitOffset == -1 || perUnitOffset == -1) {
- *isResolved = false;
- return MeasureUnit();
- }
-
- // binary search for (unitOffset, perUnitOffset)
- int32_t start = 0;
- int32_t end = UPRV_LENGTHOF(unitPerUnitToSingleUnit);
- while (start < end) {
- int32_t mid = (start + end) / 2;
- int32_t *midRow = unitPerUnitToSingleUnit[mid];
- if (unitOffset < midRow[0]) {
- end = mid;
- } else if (unitOffset > midRow[0]) {
- start = mid + 1;
- } else if (perUnitOffset < midRow[1]) {
- end = mid;
- } else if (perUnitOffset > midRow[1]) {
- start = mid + 1;
- } else {
- // We found a resolution for our unit / per-unit combo
- // return it.
- *isResolved = true;
- return MeasureUnit(midRow[2], midRow[3]);
- }
- }
-
- *isResolved = false;
- return MeasureUnit();
-}
-
MeasureUnit *MeasureUnit::create(int typeId, int subTypeId, UErrorCode &status) {
if (U_FAILURE(status)) {
return NULL;
diff --git a/icu4c/source/i18n/number_longnames.cpp b/icu4c/source/i18n/number_longnames.cpp
index 8b33992..3891d53 100644
--- a/icu4c/source/i18n/number_longnames.cpp
+++ b/icu4c/source/i18n/number_longnames.cpp
@@ -223,24 +223,16 @@
LongNameHandler *fillIn, UErrorCode &status) {
// Not valid for mixed units that aren't built-in units, and there should
// not be any built-in mixed units!
- U_ASSERT(uprv_strlen(unitRef.getType()) > 0 || unitRef.getComplexity(status) != UMEASURE_UNIT_MIXED);
+ U_ASSERT(uprv_strcmp(unitRef.getType(), "") != 0 ||
+ unitRef.getComplexity(status) != UMEASURE_UNIT_MIXED);
U_ASSERT(fillIn != nullptr);
- if (uprv_strlen(unitRef.getType()) == 0 || uprv_strlen(perUnit.getType()) == 0) {
- // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an
- // error code. Once we support not-built-in units here, unitRef may be
- // anything, but if not built-in, perUnit has to be "none".
- status = U_UNSUPPORTED_ERROR;
- return;
- }
MeasureUnit unit = unitRef;
if (uprv_strcmp(perUnit.getType(), "none") != 0) {
- // Compound unit: first try to simplify (e.g. "meter per second" is a
- // built-in unit).
- bool isResolved = false;
- MeasureUnit resolved = MeasureUnit::resolveUnitPerUnit(unit, perUnit, &isResolved);
- if (isResolved) {
- unit = resolved;
+ // Compound unit: first try to simplify (e.g., meters per second is its own unit).
+ MeasureUnit simplified = unit.product(perUnit.reciprocal(status), status);
+ if (uprv_strcmp(simplified.getType(), "") != 0) {
+ unit = simplified;
} else {
// No simplified form is available.
forCompoundUnit(loc, unit, perUnit, width, rules, parent, fillIn, status);
@@ -248,6 +240,14 @@
}
}
+ if (uprv_strcmp(unit.getType(), "") == 0) {
+ // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an
+ // error code. Once we support not-built-in units here, unitRef may be
+ // anything, but if not built-in, perUnit has to be "none".
+ status = U_UNSUPPORTED_ERROR;
+ return;
+ }
+
UnicodeString simpleFormats[ARRAY_LENGTH];
getMeasureData(loc, unit, width, simpleFormats, status);
if (U_FAILURE(status)) {
@@ -263,6 +263,13 @@
const MeasureUnit &perUnit, const UNumberUnitWidth &width,
const PluralRules *rules, const MicroPropsGenerator *parent,
LongNameHandler *fillIn, UErrorCode &status) {
+ if (uprv_strcmp(unit.getType(), "") == 0 || uprv_strcmp(perUnit.getType(), "") == 0) {
+ // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an
+ // error code. Once we support not-built-in units here, unitRef may be
+ // anything, but if not built-in, perUnit has to be "none".
+ status = U_UNSUPPORTED_ERROR;
+ return;
+ }
if (fillIn == nullptr) {
status = U_INTERNAL_PROGRAM_ERROR;
return;
diff --git a/icu4c/source/i18n/number_skeletons.cpp b/icu4c/source/i18n/number_skeletons.cpp
index fd7d7ca..e6d94d2 100644
--- a/icu4c/source/i18n/number_skeletons.cpp
+++ b/icu4c/source/i18n/number_skeletons.cpp
@@ -843,10 +843,6 @@
sb.append(u' ');
}
if (U_FAILURE(status)) { return; }
- if (GeneratorHelpers::perUnit(macros, sb, status)) {
- sb.append(u' ');
- }
- if (U_FAILURE(status)) { return; }
if (GeneratorHelpers::usage(macros, sb, status)) {
sb.append(u' ');
}
@@ -1025,14 +1021,6 @@
status = U_NUMBER_SKELETON_SYNTAX_ERROR;
}
-void blueprint_helpers::generateMeasureUnitOption(const MeasureUnit& measureUnit, UnicodeString& sb,
- UErrorCode&) {
- // Need to do char <-> UChar conversion...
- sb.append(UnicodeString(measureUnit.getType(), -1, US_INV));
- sb.append(u'-');
- sb.append(UnicodeString(measureUnit.getSubtype(), -1, US_INV));
-}
-
void blueprint_helpers::parseMeasurePerUnitOption(const StringSegment& segment, MacroProps& macros,
UErrorCode& status) {
// A little bit of a hack: save the current unit (numerator), call the main measure unit
@@ -1059,15 +1047,21 @@
return;
}
- // Mixed units can only be represented by a full MeasureUnit instances, so
- // we ignore macros.perUnit.
+ // Mixed units can only be represented by full MeasureUnit instances, so we
+ // don't split the denominator into macros.perUnit.
if (fullUnit.complexity == UMEASURE_UNIT_MIXED) {
macros.unit = std::move(fullUnit).build(status);
return;
}
- // TODO(ICU-20941): Clean this up (see also
- // https://github.com/icu-units/icu/issues/35).
+ // When we have a built-in unit (e.g. meter-per-second), we don't split it up
+ MeasureUnit testBuiltin = fullUnit.copy(status).build(status);
+ if (uprv_strcmp(testBuiltin.getType(), "") != 0) {
+ macros.unit = std::move(testBuiltin);
+ return;
+ }
+
+ // TODO(ICU-20941): Clean this up.
for (int32_t i = 0; i < fullUnit.units.length(); i++) {
SingleUnitImpl* subUnit = fullUnit.units[i];
if (subUnit->dimensionality > 0) {
@@ -1523,28 +1517,17 @@
} else if (utils::unitIsPermille(macros.unit)) {
sb.append(u"permille", -1);
return true;
- } else if (uprv_strcmp(macros.unit.getType(), "") != 0) {
- sb.append(u"measure-unit/", -1);
- blueprint_helpers::generateMeasureUnitOption(macros.unit, sb, status);
- return true;
} else {
- // TODO(icu-units#35): add support for not-built-in units.
- status = U_UNSUPPORTED_ERROR;
- return false;
- }
-}
-
-bool GeneratorHelpers::perUnit(const MacroProps& macros, UnicodeString& sb, UErrorCode& status) {
- // Per-units are currently expected to be only MeasureUnits.
- if (utils::unitIsBaseUnit(macros.perUnit)) {
- // Default value: ok to ignore
- return false;
- } else if (utils::unitIsCurrency(macros.perUnit)) {
- status = U_UNSUPPORTED_ERROR;
- return false;
- } else {
- sb.append(u"per-measure-unit/", -1);
- blueprint_helpers::generateMeasureUnitOption(macros.perUnit, sb, status);
+ MeasureUnit unit = macros.unit;
+ if (utils::unitIsCurrency(macros.perUnit)) {
+ status = U_UNSUPPORTED_ERROR;
+ return false;
+ }
+ if (!utils::unitIsBaseUnit(macros.perUnit)) {
+ unit = unit.product(macros.perUnit.reciprocal(status), status);
+ }
+ sb.append(u"unit/", -1);
+ sb.append(unit.getIdentifier());
return true;
}
}
diff --git a/icu4c/source/i18n/number_skeletons.h b/icu4c/source/i18n/number_skeletons.h
index 38dfa19..201267e 100644
--- a/icu4c/source/i18n/number_skeletons.h
+++ b/icu4c/source/i18n/number_skeletons.h
@@ -240,10 +240,10 @@
void generateCurrencyOption(const CurrencyUnit& currency, UnicodeString& sb, UErrorCode& status);
+// "measure-unit/" is deprecated in favour of "unit/".
void parseMeasureUnitOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
-void generateMeasureUnitOption(const MeasureUnit& measureUnit, UnicodeString& sb, UErrorCode& status);
-
+// "per-measure-unit/" is deprecated in favour of "unit/".
void parseMeasurePerUnitOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
/**
@@ -314,8 +314,6 @@
static bool unit(const MacroProps& macros, UnicodeString& sb, UErrorCode& status);
- static bool perUnit(const MacroProps& macros, UnicodeString& sb, UErrorCode& status);
-
static bool usage(const MacroProps& macros, UnicodeString& sb, UErrorCode& status);
static bool precision(const MacroProps& macros, UnicodeString& sb, UErrorCode& status);
diff --git a/icu4c/source/i18n/unicode/measunit.h b/icu4c/source/i18n/unicode/measunit.h
index f38c400..d86bab3 100644
--- a/icu4c/source/i18n/unicode/measunit.h
+++ b/icu4c/source/i18n/unicode/measunit.h
@@ -541,13 +541,6 @@
* @internal
*/
int32_t getOffset() const;
-
- /**
- * ICU use only.
- * @internal
- */
- 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
diff --git a/icu4c/source/test/intltest/measfmttest.cpp b/icu4c/source/test/intltest/measfmttest.cpp
index 8690b54..deada9a 100644
--- a/icu4c/source/test/intltest/measfmttest.cpp
+++ b/icu4c/source/test/intltest/measfmttest.cpp
@@ -3494,7 +3494,7 @@
UErrorCode status = U_ZERO_ERROR;
Locale en("en");
MeasureFormat fmt("en", UMEASFMT_WIDTH_SHORT, status);
- Measure measure(50.0, MeasureUnit::createPound(status), status);
+ Measure measure(50.0, MeasureUnit::createPoundForce(status), status);
LocalPointer<MeasureUnit> sqInch(MeasureUnit::createSquareInch(status));
if (!assertSuccess("Create of format unit and per unit", status)) {
return;
diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h
index 802b8bb..7603263 100644
--- a/icu4c/source/test/intltest/numbertest.h
+++ b/icu4c/source/test/intltest/numbertest.h
@@ -54,6 +54,7 @@
void notationCompact();
void unitMeasure();
void unitCompoundMeasure();
+ void unitSkeletons();
void unitUsage();
void unitUsageErrorCodes();
void unitUsageSkeletons();
@@ -104,12 +105,15 @@
CurrencyUnit CNY;
MeasureUnit METER;
+ MeasureUnit METER_PER_SECOND;
MeasureUnit DAY;
MeasureUnit SQUARE_METER;
MeasureUnit FAHRENHEIT;
MeasureUnit SECOND;
MeasureUnit POUND;
+ MeasureUnit POUND_FORCE;
MeasureUnit SQUARE_MILE;
+ MeasureUnit SQUARE_INCH;
MeasureUnit JOULE;
MeasureUnit FURLONG;
MeasureUnit KELVIN;
diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp
index 43323fb..4580564 100644
--- a/icu4c/source/test/intltest/numbertest_api.cpp
+++ b/icu4c/source/test/intltest/numbertest_api.cpp
@@ -53,12 +53,15 @@
}
METER = *unit;
+ METER_PER_SECOND = *LocalPointer<MeasureUnit>(MeasureUnit::createMeterPerSecond(status));
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));
+ POUND_FORCE = *LocalPointer<MeasureUnit>(MeasureUnit::createPoundForce(status));
SQUARE_MILE = *LocalPointer<MeasureUnit>(MeasureUnit::createSquareMile(status));
+ SQUARE_INCH = *LocalPointer<MeasureUnit>(MeasureUnit::createSquareInch(status));
JOULE = *LocalPointer<MeasureUnit>(MeasureUnit::createJoule(status));
FURLONG = *LocalPointer<MeasureUnit>(MeasureUnit::createFurlong(status));
KELVIN = *LocalPointer<MeasureUnit>(MeasureUnit::createKelvin(status));
@@ -77,6 +80,7 @@
TESTCASE_AUTO(notationCompact);
TESTCASE_AUTO(unitMeasure);
TESTCASE_AUTO(unitCompoundMeasure);
+ TESTCASE_AUTO(unitSkeletons);
TESTCASE_AUTO(unitUsage);
TESTCASE_AUTO(unitUsageErrorCodes);
TESTCASE_AUTO(unitUsageSkeletons);
@@ -578,6 +582,23 @@
u"0.0088 meters",
u"0 meters");
+// // TODO(ICU-20941): Support formatting for not-built-in units
+// assertFormatDescending(
+// u"Hectometers",
+// u"measure-unit/length-hectometer",
+// u"unit/hectometer",
+// NumberFormatter::with().unit(MeasureUnit::forIdentifier("hectometer", status)),
+// Locale::getEnglish(),
+// u"87,650 hm",
+// u"8,765 hm",
+// u"876.5 hm",
+// u"87.65 hm",
+// u"8.765 hm",
+// u"0.8765 hm",
+// u"0.08765 hm",
+// u"0.008765 hm",
+// u"0 hm");
+
// TODO: Implement Measure in C++
// assertFormatSingleMeasure(
// u"Meters with Measure Input",
@@ -694,10 +715,9 @@
5,
u"5 a\u00F1os");
- // TODO(icu-units#35): skeleton generation.
assertFormatSingle(
u"Mixed unit",
- nullptr,
+ u"unit/yard-and-foot-and-inch",
u"unit/yard-and-foot-and-inch",
NumberFormatter::with()
.unit(MeasureUnit::forIdentifier("yard-and-foot-and-inch", status)),
@@ -705,10 +725,9 @@
3.65,
"3 yd, 1 ft, 11.4 in");
- // TODO(icu-units#35): skeleton generation.
assertFormatSingle(
u"Mixed unit, Scientific",
- nullptr,
+ u"unit/yard-and-foot-and-inch E0",
u"unit/yard-and-foot-and-inch E0",
NumberFormatter::with()
.unit(MeasureUnit::forIdentifier("yard-and-foot-and-inch", status))
@@ -717,10 +736,9 @@
3.65,
"3 yd, 1 ft, 1.14E1 in");
- // TODO(icu-units#35): skeleton generation.
assertFormatSingle(
u"Mixed Unit (Narrow Version)",
- nullptr,
+ u"unit/metric-ton-and-kilogram-and-gram unit-width-narrow",
u"unit/metric-ton-and-kilogram-and-gram unit-width-narrow",
NumberFormatter::with()
.unit(MeasureUnit::forIdentifier("metric-ton-and-kilogram-and-gram", status))
@@ -729,10 +747,9 @@
4.28571,
u"4t 285kg 710g");
- // TODO(icu-units#35): skeleton generation.
assertFormatSingle(
u"Mixed Unit (Short Version)",
- nullptr,
+ u"unit/metric-ton-and-kilogram-and-gram unit-width-short",
u"unit/metric-ton-and-kilogram-and-gram unit-width-short",
NumberFormatter::with()
.unit(MeasureUnit::forIdentifier("metric-ton-and-kilogram-and-gram", status))
@@ -741,10 +758,9 @@
4.28571,
u"4 t, 285 kg, 710 g");
- // TODO(icu-units#35): skeleton generation.
assertFormatSingle(
u"Mixed Unit (Full Name Version)",
- nullptr,
+ u"unit/metric-ton-and-kilogram-and-gram unit-width-full-name",
u"unit/metric-ton-and-kilogram-and-gram unit-width-full-name",
NumberFormatter::with()
.unit(MeasureUnit::forIdentifier("metric-ton-and-kilogram-and-gram", status))
@@ -755,8 +771,8 @@
assertFormatSingle(
u"Testing \"1 foot 12 inches\"",
- nullptr,
- u"unit/foot-and-inch",
+ u"unit/foot-and-inch @### unit-width-full-name",
+ u"unit/foot-and-inch @### unit-width-full-name",
NumberFormatter::with()
.unit(MeasureUnit::forIdentifier("foot-and-inch", status))
.precision(Precision::maxSignificantDigits(4))
@@ -776,7 +792,7 @@
assertFormatSingle(
u"Negative numbers: time",
- nullptr, // submitting after TODO(icu-units#35) is fixed: fill in skeleton!
+ u"unit/hour-and-minute-and-second",
u"unit/hour-and-minute-and-second",
NumberFormatter::with().unit(MeasureUnit::forIdentifier("hour-and-minute-and-second", status)),
Locale("de-DE"),
@@ -803,16 +819,21 @@
u"0.008765 m/s",
u"0 m/s");
- // TODO(icu-units#35): does not normalize as desired: while "unit/*" does
- // get split into unit/perUnit, ".unit(*)" and "measure-unit/*" don't:
- assertFormatSingle(
- u"Built-in unit, meter-per-second",
- u"measure-unit/speed-meter-per-second",
- u"~unit/meter-per-second",
- NumberFormatter::with().unit(MeasureUnit::getMeterPerSecond()),
- Locale("en-GB"),
- 2.4,
- u"2.4 m/s");
+ assertFormatDescending(
+ u"Meters Per Second Short, built-in m/s",
+ u"measure-unit/speed-meter-per-second",
+ u"unit/meter-per-second",
+ NumberFormatter::with().unit(METER_PER_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",
@@ -863,29 +884,51 @@
// u"0.008765 J/fur",
// u"0 J/fur");
- // TODO(icu-units#59): THIS UNIT TEST DEMONSTRATES UNDESIREABLE BEHAVIOUR!
- // When specifying built-in types, one can give both a unit and a perUnit.
- // Resolving to a built-in unit does not always work.
- //
- // (Unit-testing philosophy: do we leave this enabled to demonstrate current
- // behaviour, and changing behaviour in the future? Or comment it out to
- // avoid asserting this is "correct"?)
+ assertFormatDescending(
+ u"Pounds per Square Inch: composed",
+ u"measure-unit/force-pound-force per-measure-unit/area-square-inch",
+ u"unit/pound-force-per-square-inch",
+ NumberFormatter::with().unit(POUND_FORCE).perUnit(SQUARE_INCH),
+ Locale::getEnglish(),
+ u"87,650 psi",
+ u"8,765 psi",
+ u"876.5 psi",
+ u"87.65 psi",
+ u"8.765 psi",
+ u"0.8765 psi",
+ u"0.08765 psi",
+ u"0.008765 psi",
+ u"0 psi");
+
+ assertFormatDescending(
+ u"Pounds per Square Inch: built-in",
+ u"measure-unit/force-pound-force per-measure-unit/area-square-inch",
+ u"unit/pound-force-per-square-inch",
+ NumberFormatter::with().unit(MeasureUnit::getPoundPerSquareInch()),
+ Locale::getEnglish(),
+ u"87,650 psi",
+ u"8,765 psi",
+ u"876.5 psi",
+ u"87.65 psi",
+ u"8.765 psi",
+ u"0.8765 psi",
+ u"0.08765 psi",
+ u"0.008765 psi",
+ u"0 psi");
+
assertFormatSingle(
- u"DEMONSTRATING BAD BEHAVIOUR, TODO(icu-units#59)",
+ u"m/s/s simplifies to m/s^2",
u"measure-unit/speed-meter-per-second per-measure-unit/duration-second",
- u"measure-unit/speed-meter-per-second per-measure-unit/duration-second",
- NumberFormatter::with()
- .unit(MeasureUnit::getMeterPerSecond())
- .perUnit(MeasureUnit::getSecond()),
+ u"unit/meter-per-square-second",
+ NumberFormatter::with().unit(METER_PER_SECOND).perUnit(SECOND),
Locale("en-GB"),
2.4,
- "2.4 m/s/s");
+ u"2.4 m/s\u00B2");
assertFormatSingle(
u"Negative numbers: acceleration",
u"measure-unit/acceleration-meter-per-square-second",
- // TODO: when other PRs are merged, try: u"unit/meter-per-second-second" instead:
- u"measure-unit/acceleration-meter-per-square-second",
+ u"unit/meter-per-second-second",
NumberFormatter::with().unit(MeasureUnit::forIdentifier("meter-per-pow2-second", status)),
Locale("af-ZA"),
-9.81,
@@ -908,12 +951,10 @@
status.assertSuccess();
}
- // .perUnit() may only be passed a built-in type, "square-second" is not a
- // built-in type.
- nf = NumberFormatter::with()
- .unit(MeasureUnit::getMeter())
- .perUnit(MeasureUnit::forIdentifier("square-second", status))
- .locale("en-GB");
+ // .perUnit() may only be passed a built-in type, or something that combines
+ // to a built-in type together with .unit().
+ MeasureUnit SQUARE_SECOND = MeasureUnit::forIdentifier("square-second", status);
+ nf = NumberFormatter::with().unit(FURLONG).perUnit(SQUARE_SECOND).locale("en-GB");
status.assertSuccess(); // Error is only returned once we try to format.
num = nf.formatDouble(2.4, status);
if (!status.expectErrorAndReset(U_UNSUPPORTED_ERROR)) {
@@ -921,6 +962,128 @@
nf.formatDouble(2.4, status).toString(status) + "\".");
status.assertSuccess();
}
+ // As above, "square-second" is not a built-in type, however this time,
+ // meter-per-square-second is a built-in type.
+ assertFormatSingle(
+ u"meter per square-second works as a composed unit",
+ u"measure-unit/speed-meter-per-second per-measure-unit/duration-second",
+ u"unit/meter-per-square-second",
+ NumberFormatter::with().unit(METER).perUnit(SQUARE_SECOND),
+ Locale("en-GB"),
+ 2.4,
+ u"2.4 m/s\u00B2");
+}
+
+void NumberFormatterApiTest::unitSkeletons() {
+ const struct TestCase {
+ const char *msg;
+ const char16_t *inputSkeleton;
+ const char16_t *normalizedSkeleton;
+ } cases[] = {
+ {"old-form built-in compound unit", //
+ u"measure-unit/speed-meter-per-second", //
+ u"unit/meter-per-second"},
+
+ {"old-form compound construction, converts to built-in", //
+ u"measure-unit/length-meter per-measure-unit/duration-second", //
+ u"unit/meter-per-second"},
+
+ {"old-form compound construction which does not simplify to a built-in", //
+ u"measure-unit/energy-joule per-measure-unit/length-meter", //
+ u"unit/joule-per-meter"},
+
+ {"old-form compound-compound ugliness resolves neatly", //
+ u"measure-unit/speed-meter-per-second per-measure-unit/duration-second", //
+ u"unit/meter-per-square-second"},
+
+ {"short-form built-in units stick with the built-in", //
+ u"unit/meter-per-second", //
+ u"unit/meter-per-second"},
+
+ {"short-form compound units stay as is", //
+ u"unit/square-meter-per-square-meter", //
+ u"unit/square-meter-per-square-meter"},
+
+ {"short-form compound units stay as is", //
+ u"unit/joule-per-furlong", //
+ u"unit/joule-per-furlong"},
+
+ {"short-form that doesn't consist of built-in units", //
+ u"unit/hectometer-per-second", //
+ u"unit/hectometer-per-second"},
+
+ {"short-form that doesn't consist of built-in units", //
+ u"unit/meter-per-hectosecond", //
+ u"unit/meter-per-hectosecond"},
+
+ // // TODO: binary prefixes not supported yet!
+ // {"Round-trip example from icu-units#35", //
+ // u"unit/kibijoule-per-furlong", //
+ // u"unit/kibijoule-per-furlong"},
+ };
+ for (auto &cas : cases) {
+ IcuTestErrorCode status(*this, cas.msg);
+ auto nf = NumberFormatter::forSkeleton(cas.inputSkeleton, status);
+ if (status.errIfFailureAndReset("NumberFormatter::forSkeleton failed")) {
+ continue;
+ }
+ assertEquals( //
+ UnicodeString(TRUE, cas.inputSkeleton, -1) + u" normalization", //
+ cas.normalizedSkeleton, //
+ nf.toSkeleton(status));
+ status.errIfFailureAndReset("NumberFormatter::toSkeleton failed");
+ }
+
+ const struct FailCase {
+ const char *msg;
+ const char16_t *inputSkeleton;
+ UErrorCode expectedForSkelStatus;
+ UErrorCode expectedToSkelStatus;
+ } failCases[] = {
+ {"Parsing measure-unit/* results in failure if not built-in unit",
+ u"measure-unit/hectometer", //
+ U_NUMBER_SKELETON_SYNTAX_ERROR, //
+ U_ZERO_ERROR},
+
+ {"Parsing per-measure-unit/* results in failure if not built-in unit",
+ u"measure-unit/meter per-measure-unit/hectosecond", //
+ U_NUMBER_SKELETON_SYNTAX_ERROR, //
+ U_ZERO_ERROR},
+ };
+ for (auto &cas : failCases) {
+ IcuTestErrorCode status(*this, cas.msg);
+ auto nf = NumberFormatter::forSkeleton(cas.inputSkeleton, status);
+ if (status.expectErrorAndReset(cas.expectedForSkelStatus, cas.msg)) {
+ continue;
+ }
+ nf.toSkeleton(status);
+ status.expectErrorAndReset(cas.expectedToSkelStatus, cas.msg);
+ }
+
+ IcuTestErrorCode status(*this, "unitSkeletons");
+ MeasureUnit METER_PER_SECOND = MeasureUnit::forIdentifier("meter-per-second", status);
+
+ assertEquals( //
+ ".unit(METER_PER_SECOND) normalization", //
+ u"unit/meter-per-second", //
+ NumberFormatter::with().unit(METER_PER_SECOND).toSkeleton(status));
+ assertEquals( //
+ ".unit(METER).perUnit(SECOND) normalization", //
+ u"unit/meter-per-second",
+ NumberFormatter::with().unit(METER).perUnit(SECOND).toSkeleton(status));
+ assertEquals( //
+ ".unit(MeasureUnit::forIdentifier(\"hectometer\", status)) normalization", //
+ u"unit/hectometer",
+ NumberFormatter::with()
+ .unit(MeasureUnit::forIdentifier("hectometer", status))
+ .toSkeleton(status));
+ assertEquals( //
+ ".unit(MeasureUnit::forIdentifier(\"hectometer\", status)) normalization", //
+ u"unit/meter-per-hectosecond",
+ NumberFormatter::with()
+ .unit(METER)
+ .perUnit(MeasureUnit::forIdentifier("hectosecond", status))
+ .toSkeleton(status));
}
void NumberFormatterApiTest::unitUsage() {
@@ -3532,11 +3695,11 @@
}
{
- const char16_t* message = u"Measure unit field position with prefix and suffix";
+ const char16_t* message = u"Measure unit field position with prefix and suffix, composed m/s";
FormattedNumber result = assertFormatSingle(
message,
u"measure-unit/length-meter per-measure-unit/duration-second unit-width-full-name",
- u"unit/meter-per-second unit-width-full-name",
+ u"measure-unit/length-meter per-measure-unit/duration-second unit-width-full-name",
NumberFormatter::with().unit(METER).perUnit(SECOND).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME),
"ky", // locale with the interesting data
68,
@@ -3554,6 +3717,28 @@
}
{
+ const char16_t* message = u"Measure unit field position with prefix and suffix, built-in m/s";
+ FormattedNumber result = assertFormatSingle(
+ message,
+ u"measure-unit/speed-meter-per-second unit-width-full-name",
+ u"unit/meter-per-second unit-width-full-name",
+ NumberFormatter::with().unit(METER_PER_SECOND).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME),
+ "ky", // locale with the interesting data
+ 68,
+ u"секундасына 68 метр");
+ static const UFieldPosition expectedFieldPositions[] = {
+ // field, begin index, end index
+ {UNUM_MEASURE_UNIT_FIELD, 0, 11},
+ {UNUM_INTEGER_FIELD, 12, 14},
+ {UNUM_MEASURE_UNIT_FIELD, 15, 19}};
+ assertNumberFieldPositions(
+ message,
+ result,
+ expectedFieldPositions,
+ UPRV_LENGTHOF(expectedFieldPositions));
+ }
+
+ {
const char16_t* message = u"Measure unit field position with inner spaces";
FormattedNumber result = assertFormatSingle(
message,
@@ -4141,6 +4326,15 @@
assertEquals("Copy Assigned capacity", 4, copyAssigned.mixedMeasures.getCapacity());
}
+/* For skeleton comparisons: this checks the toSkeleton output for `f` and for
+ * `conciseSkeleton` against the normalized version of `uskeleton` - this does
+ * not round-trip uskeleton itself.
+ *
+ * If `conciseSkeleton` starts with a "~", its round-trip check is skipped.
+ *
+ * If `uskeleton` is nullptr, toSkeleton is expected to return an
+ * U_UNSUPPORTED_ERROR.
+ */
void NumberFormatterApiTest::assertFormatDescending(
const char16_t* umessage,
const char16_t* uskeleton,
@@ -4202,6 +4396,15 @@
}
}
+/* For skeleton comparisons: this checks the toSkeleton output for `f` and for
+ * `conciseSkeleton` against the normalized version of `uskeleton` - this does
+ * not round-trip uskeleton itself.
+ *
+ * If `conciseSkeleton` starts with a "~", its round-trip check is skipped.
+ *
+ * If `uskeleton` is nullptr, toSkeleton is expected to return an
+ * U_UNSUPPORTED_ERROR.
+ */
void NumberFormatterApiTest::assertFormatDescendingBig(
const char16_t* umessage,
const char16_t* uskeleton,
@@ -4263,6 +4466,15 @@
}
}
+/* For skeleton comparisons: this checks the toSkeleton output for `f` and for
+ * `conciseSkeleton` against the normalized version of `uskeleton` - this does
+ * not round-trip uskeleton itself.
+ *
+ * If `conciseSkeleton` starts with a "~", its round-trip check is skipped.
+ *
+ * If `uskeleton` is nullptr, toSkeleton is expected to return an
+ * U_UNSUPPORTED_ERROR.
+ */
FormattedNumber
NumberFormatterApiTest::assertFormatSingle(
const char16_t* umessage,
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java
index e320575..9d972e4 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java
@@ -215,16 +215,10 @@
UnitWidth width,
PluralRules rules,
MicroPropsGenerator parent) {
- if (unit.getType() == null || (perUnit != null && perUnit.getType() == null)) {
- // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an
- // error code. Once we support not-built-in units here, unitRef may be
- // anything, but if not built-in, perUnit has to be "none".
- throw new UnsupportedOperationException("Unsanctioned units, not yet supported");
- }
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) {
+ MeasureUnit simplified = unit.product(perUnit.reciprocal());
+ if (simplified.getType() != null) {
unit = simplified;
} else {
// No simplified form is available.
@@ -232,6 +226,12 @@
}
}
+ if (unit.getType() == null) {
+ // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported.
+ throw new UnsupportedOperationException("Unsanctioned unit, not yet supported: " +
+ unit.getIdentifier());
+ }
+
String[] simpleFormats = new String[ARRAY_LENGTH];
getMeasureData(locale, unit, width, simpleFormats);
// TODO(ICU4J): Reduce the number of object creations here?
@@ -249,6 +249,13 @@
UnitWidth width,
PluralRules rules,
MicroPropsGenerator parent) {
+ if (unit.getType() == null || perUnit.getType() == null) {
+ // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an
+ // error code.
+ throw new UnsupportedOperationException(
+ "Unsanctioned units, not yet supported: " + unit.getIdentifier() + "/" +
+ perUnit.getIdentifier());
+ }
String[] primaryData = new String[ARRAY_LENGTH];
getMeasureData(locale, unit, width, primaryData);
String[] secondaryData = new String[ARRAY_LENGTH];
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
index 6325e22..9dc64d8 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
@@ -70,7 +70,9 @@
MeasureUnitImpl result = new MeasureUnitImpl();
result.complexity = this.complexity;
result.identifier = this.identifier;
- result.singleUnits = new ArrayList<>(this.singleUnits);
+ for (SingleUnitImpl single : this.singleUnits) {
+ result.singleUnits.add(single.copy());
+ }
return result;
}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
index 07e81c4..2071e33 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
@@ -12,6 +12,8 @@
import com.ibm.icu.impl.StringSegment;
import com.ibm.icu.impl.number.MacroProps;
import com.ibm.icu.impl.number.RoundingUtils;
+import com.ibm.icu.impl.units.MeasureUnitImpl;
+import com.ibm.icu.impl.units.SingleUnitImpl;
import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
import com.ibm.icu.number.NumberFormatter.SignDisplay;
@@ -904,9 +906,6 @@
if (macros.unit != null && GeneratorHelpers.unit(macros, sb)) {
sb.append(' ');
}
- if (macros.perUnit != null && GeneratorHelpers.perUnit(macros, sb)) {
- sb.append(' ');
- }
if (macros.usage != null && GeneratorHelpers.usage(macros, sb)) {
sb.append(' ');
}
@@ -1026,6 +1025,7 @@
sb.append(currency.getCurrencyCode());
}
+ // "measure-unit/" is deprecated in favour of "unit/".
private static void parseMeasureUnitOption(StringSegment segment, MacroProps macros) {
// NOTE: The category (type) of the unit is guaranteed to be a valid subtag (alphanumeric)
// http://unicode.org/reports/tr35/#Validity_Data
@@ -1048,12 +1048,7 @@
throw new SkeletonSyntaxException("Unknown measure unit", segment);
}
- private static void generateMeasureUnitOption(MeasureUnit unit, StringBuilder sb) {
- sb.append(unit.getType());
- sb.append("-");
- sb.append(unit.getSubtype());
- }
-
+ // "per-measure-unit/" is deprecated in favour of "unit/".
private static void parseMeasurePerUnitOption(StringSegment segment, MacroProps macros) {
// A little bit of a hack: save the current unit (numerator), call the main measure unit
// parsing code, put back the numerator unit, and put the new unit into per-unit.
@@ -1068,13 +1063,44 @@
* specified via a "unit/" concise skeleton.
*/
private static void parseIdentifierUnitOption(StringSegment segment, MacroProps macros) {
- MeasureUnit[] units = MeasureUnit.parseCoreUnitIdentifier(segment.asString());
- if (units == null) {
- throw new SkeletonSyntaxException("Invalid core unit identifier", segment);
+ MeasureUnitImpl fullUnit;
+ try {
+ fullUnit = MeasureUnitImpl.forIdentifier(segment.asString());
+ } catch (IllegalArgumentException e) {
+ throw new SkeletonSyntaxException("Invalid unit stem", segment);
}
- macros.unit = units[0];
- if (units.length == 2) {
- macros.perUnit = units[1];
+
+ // Mixed units can only be represented by full MeasureUnit instances, so we
+ // don't split the denominator into macros.perUnit.
+ if (fullUnit.getComplexity() == MeasureUnit.Complexity.MIXED) {
+ macros.unit = fullUnit.build();
+ return;
+ }
+
+ // When we have a built-in unit (e.g. meter-per-second), we don't split it up
+ MeasureUnit testBuiltin = fullUnit.build();
+ if (testBuiltin.getType() != null) {
+ macros.unit = testBuiltin;
+ return;
+ }
+
+ // TODO(ICU-20941): Clean this up.
+ for (SingleUnitImpl subUnit : fullUnit.getSingleUnits()) {
+ if (subUnit.getDimensionality() > 0) {
+ if (macros.unit == null) {
+ macros.unit = subUnit.build();
+ } else {
+ macros.unit = macros.unit.product(subUnit.build());
+ }
+ } else {
+ // It's okay to mutate fullUnit, we're throwing it away after this:
+ subUnit.setDimensionality(subUnit.getDimensionality() * -1);
+ if (macros.perUnit == null) {
+ macros.perUnit = subUnit.build();
+ } else {
+ macros.perUnit = macros.perUnit.product(subUnit.build());
+ }
+ }
}
}
@@ -1468,24 +1494,17 @@
} else if (macros.unit == MeasureUnit.PERMILLE) {
sb.append("permille");
return true;
- } else if (macros.unit.getType() != null) {
- sb.append("measure-unit/");
- BlueprintHelpers.generateMeasureUnitOption(macros.unit, sb);
- return true;
} else {
- // TODO(icu-units#35): add support for not-built-in units.
- throw new UnsupportedOperationException();
- }
- }
-
- private static boolean perUnit(MacroProps macros, StringBuilder sb) {
- // Per-units are currently expected to be only MeasureUnits.
- if (macros.perUnit instanceof Currency) {
- throw new UnsupportedOperationException(
- "Cannot generate number skeleton with per-unit that is not a standard measure unit");
- } else {
- sb.append("per-measure-unit/");
- BlueprintHelpers.generateMeasureUnitOption(macros.perUnit, sb);
+ MeasureUnit unit = macros.unit;
+ if (macros.perUnit != null) {
+ if (macros.perUnit instanceof Currency) {
+ throw new UnsupportedOperationException(
+ "Cannot generate number skeleton with per-unit that is not a standard measure unit");
+ }
+ unit = unit.product(macros.perUnit.reciprocal());
+ }
+ sb.append("unit/");
+ sb.append(unit.getIdentifier());
return true;
}
}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java b/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
index 15ee506..a32b0b7 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
@@ -381,7 +381,7 @@
/**
- * Get the type, such as "length"
+ * Get the type, such as "length". May return null.
*
* @stable ICU 53
*/
@@ -391,7 +391,7 @@
/**
- * Get the subType, such as “foot”.
+ * Get the subType, such as “foot”. May return null.
*
* @stable ICU 53
*/
@@ -702,47 +702,6 @@
return null;
}
- /**
- * For ICU use only.
- * @internal
- * @deprecated This API is ICU internal only.
- */
- @Deprecated
- public static MeasureUnit[] parseCoreUnitIdentifier(String coreUnitIdentifier) {
- // First search for the whole code unit identifier as a subType
- MeasureUnit whole = findBySubType(coreUnitIdentifier);
- if (whole != null) {
- return new MeasureUnit[] { whole }; // found a numerator but not denominator
- }
-
- // If not found, try breaking apart numerator and denominator
- int perIdx = coreUnitIdentifier.indexOf("-per-");
- if (perIdx == -1) {
- // String does not contain "-per-"
- return null;
- }
- String numeratorStr = coreUnitIdentifier.substring(0, perIdx);
- String denominatorStr = coreUnitIdentifier.substring(perIdx + 5);
- MeasureUnit numerator = findBySubType(numeratorStr);
- MeasureUnit denominator = findBySubType(denominatorStr);
- if (numerator != null && denominator != null) {
- return new MeasureUnit[] { numerator, denominator }; // found both a numerator and denominator
- }
-
- // The numerator or denominator were invalid
- return null;
- }
-
- /**
- * For ICU use only.
- * @internal
- * @deprecated This API is ICU internal only.
- */
- @Deprecated
- public static MeasureUnit resolveUnitPerUnit(MeasureUnit unit, MeasureUnit perUnit) {
- return unitPerUnitToSingleUnit.get(Pair.of(unit, perUnit));
- }
-
static final UnicodeSet ASCII = new UnicodeSet('a', 'z').freeze();
static final UnicodeSet ASCII_HYPHEN_DIGITS = new UnicodeSet('-', '-', '0', '9', 'a', 'z').freeze();
@@ -2010,23 +1969,7 @@
*/
public static final MeasureUnit TEASPOON = MeasureUnit.internalGetInstance("volume", "teaspoon");
- private static HashMap<Pair<MeasureUnit, MeasureUnit>, MeasureUnit>unitPerUnitToSingleUnit =
- new HashMap<>();
-
- static {
- unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.LITER, MeasureUnit.KILOMETER), MeasureUnit.LITER_PER_KILOMETER);
- unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.POUND, MeasureUnit.SQUARE_INCH), MeasureUnit.POUND_PER_SQUARE_INCH);
- unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.PIXEL, MeasureUnit.CENTIMETER), MeasureUnit.PIXEL_PER_CENTIMETER);
- unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.DOT, MeasureUnit.CENTIMETER), MeasureUnit.DOT_PER_CENTIMETER);
- unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.MILE, MeasureUnit.HOUR), MeasureUnit.MILE_PER_HOUR);
- unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.MILLIGRAM, MeasureUnit.DECILITER), MeasureUnit.MILLIGRAM_PER_DECILITER);
- unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.MILE, MeasureUnit.GALLON_IMPERIAL), MeasureUnit.MILE_PER_GALLON_IMPERIAL);
- unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.KILOMETER, MeasureUnit.HOUR), MeasureUnit.KILOMETER_PER_HOUR);
- unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.MILE, MeasureUnit.GALLON), MeasureUnit.MILE_PER_GALLON);
- unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.PIXEL, MeasureUnit.INCH), MeasureUnit.PIXEL_PER_INCH);
- unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.DOT, MeasureUnit.INCH), MeasureUnit.DOT_PER_INCH);
- unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.METER, MeasureUnit.SECOND), MeasureUnit.METER_PER_SECOND);
- }
+ // unitPerUnitToSingleUnit no longer in use! TODO: remove from code-generation code.
// End generated MeasureUnit constants
/* Private */
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java
index d5a4efd..a7f706c 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java
@@ -2652,7 +2652,7 @@
// This fails unless we resolve to MeasureUnit.POUND_PER_SQUARE_INCH
assertEquals("", "50 psi",
fmt.formatMeasurePerUnit(
- new Measure(50, MeasureUnit.POUND),
+ new Measure(50, MeasureUnit.POUND_FORCE),
MeasureUnit.SQUARE_INCH,
new StringBuilder(),
new FieldPosition(0)).toString());
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
index 89afcf3..c25fd6d 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
@@ -536,6 +536,23 @@
"0.0088 meters",
"0 meters");
+ // // TODO(ICU-20941): Support formatting for not-built-in units
+ // assertFormatDescending(
+ // "Hectometers",
+ // "measure-unit/length-hectometer",
+ // "unit/hectometer",
+ // NumberFormatter.with().unit(MeasureUnit.forIdentifier("hectometer")),
+ // ULocale.ENGLISH,
+ // "87,650 hm",
+ // "8,765 ham",
+ // "876.5 hm",
+ // "87.65 hm",
+ // "8.765 hm",
+ // "0.8765 hm",
+ // "0.08765 hm",
+ // "0.008765 hm",
+ // "0 hm");
+
assertFormatSingleMeasure(
"Meters with Measure Input",
"unit-width-full-name",
@@ -656,10 +673,9 @@
5,
"5 a\u00F1os");
- // TODO(icu-units#35): skeleton generation.
assertFormatSingle(
"Mixed unit",
- null,
+ "unit/yard-and-foot-and-inch",
"unit/yard-and-foot-and-inch",
NumberFormatter.with()
.unit(MeasureUnit.forIdentifier("yard-and-foot-and-inch")),
@@ -667,10 +683,9 @@
3.65,
"3 yd, 1 ft, 11.4 in");
- // TODO(icu-units#35): skeleton generation.
assertFormatSingle(
"Mixed unit, Scientific",
- null,
+ "unit/yard-and-foot-and-inch E0",
"unit/yard-and-foot-and-inch E0",
NumberFormatter.with()
.unit(MeasureUnit.forIdentifier("yard-and-foot-and-inch"))
@@ -679,10 +694,9 @@
3.65,
"3 yd, 1 ft, 1.14E1 in");
- // TODO(icu-units#35): skeleton generation.
assertFormatSingle(
"Mixed Unit (Narrow Version)",
- null,
+ "unit/metric-ton-and-kilogram-and-gram unit-width-narrow",
"unit/metric-ton-and-kilogram-and-gram unit-width-narrow",
NumberFormatter.with()
.unit(MeasureUnit.forIdentifier("metric-ton-and-kilogram-and-gram"))
@@ -691,10 +705,9 @@
4.28571,
"4t 285kg 710g");
- // TODO(icu-units#35): skeleton generation.
assertFormatSingle(
"Mixed Unit (Short Version)",
- null,
+ "unit/metric-ton-and-kilogram-and-gram unit-width-short",
"unit/metric-ton-and-kilogram-and-gram unit-width-short",
NumberFormatter.with()
.unit(MeasureUnit.forIdentifier("metric-ton-and-kilogram-and-gram"))
@@ -703,10 +716,9 @@
4.28571,
"4 t, 285 kg, 710 g");
- // TODO(icu-units#35): skeleton generation.
assertFormatSingle(
"Mixed Unit (Full Name Version)",
- null,
+ "unit/metric-ton-and-kilogram-and-gram unit-width-full-name",
"unit/metric-ton-and-kilogram-and-gram unit-width-full-name",
NumberFormatter.with()
.unit(MeasureUnit.forIdentifier("metric-ton-and-kilogram-and-gram"))
@@ -717,8 +729,8 @@
assertFormatSingle(
"Testing \"1 foot 12 inches\"",
- null,
- "unit/foot-and-inch",
+ "unit/foot-and-inch @### unit-width-full-name",
+ "unit/foot-and-inch @### unit-width-full-name",
NumberFormatter.with()
.unit(MeasureUnit.forIdentifier("foot-and-inch"))
.precision(Precision.maxSignificantDigits(4))
@@ -738,7 +750,7 @@
assertFormatSingle(
"Negative numbers: time",
- null, // submitting after TODO(icu-units#35) is fixed: fill in skeleton!
+ "unit/hour-and-minute-and-second",
"unit/hour-and-minute-and-second",
NumberFormatter.with().unit(MeasureUnit.forIdentifier("hour-and-minute-and-second")),
new ULocale("de-DE"),
@@ -751,7 +763,7 @@
assertFormatDescending(
"Meters Per Second Short (unit that simplifies) and perUnit method",
"measure-unit/length-meter per-measure-unit/duration-second",
- "~unit/meter-per-second", // does not round-trip to the full skeleton above
+ "unit/meter-per-second",
NumberFormatter.with().unit(MeasureUnit.METER).perUnit(MeasureUnit.SECOND),
ULocale.ENGLISH,
"87,650 m/s",
@@ -764,16 +776,21 @@
"0.008765 m/s",
"0 m/s");
- // TODO(icu-units#35): does not normalize as desired: while "unit/*" does
- // get split into unit/perUnit, ".unit(*)" and "measure-unit/*" don't:
- assertFormatSingle(
- "Built-in unit, meter-per-second",
+ assertFormatDescending(
+ "Meters Per Second Short, built-in m/s",
"measure-unit/speed-meter-per-second",
- "~unit/meter-per-second",
+ "unit/meter-per-second",
NumberFormatter.with().unit(MeasureUnit.METER_PER_SECOND),
- new ULocale("en-GB"),
- 2.4,
- "2.4 m/s");
+ 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)",
@@ -824,29 +841,53 @@
// "0.008765 J/fur",
// "0 J/fur");
- // TODO(icu-units#59): THIS UNIT TEST DEMONSTRATES UNDESIRABLE BEHAVIOUR!
- // When specifying built-in types, one can give both a unit and a perUnit.
- // Resolving to a built-in unit does not always work.
- //
- // (Unit-testing philosophy: do we leave this enabled to demonstrate current
- // behaviour, and changing behaviour in the future? Or comment it out to
- // avoid asserting this is "correct"?)
+ assertFormatDescending(
+ "Pounds per Square Inch: composed",
+ "measure-unit/force-pound-force per-measure-unit/area-square-inch",
+ "unit/pound-force-per-square-inch",
+ NumberFormatter.with().unit(MeasureUnit.POUND_FORCE).perUnit(MeasureUnit.SQUARE_INCH),
+ ULocale.ENGLISH,
+ "87,650 psi",
+ "8,765 psi",
+ "876.5 psi",
+ "87.65 psi",
+ "8.765 psi",
+ "0.8765 psi",
+ "0.08765 psi",
+ "0.008765 psi",
+ "0 psi");
+
+ assertFormatDescending(
+ "Pounds per Square Inch: built-in",
+ "measure-unit/force-pound-force per-measure-unit/area-square-inch",
+ "unit/pound-force-per-square-inch",
+ NumberFormatter.with().unit(MeasureUnit.POUND_PER_SQUARE_INCH),
+ ULocale.ENGLISH,
+ "87,650 psi",
+ "8,765 psi",
+ "876.5 psi",
+ "87.65 psi",
+ "8.765 psi",
+ "0.8765 psi",
+ "0.08765 psi",
+ "0.008765 psi",
+ "0 psi");
+
assertFormatSingle(
- "DEMONSTRATING BAD BEHAVIOUR, TODO(icu-units#59)",
+ "m/s/s simplifies to m/s^2",
"measure-unit/speed-meter-per-second per-measure-unit/duration-second",
- "measure-unit/speed-meter-per-second per-measure-unit/duration-second",
+ "unit/meter-per-square-second",
NumberFormatter.with()
.unit(MeasureUnit.METER_PER_SECOND)
.perUnit(MeasureUnit.SECOND),
new ULocale("en-GB"),
2.4,
- "2.4 m/s/s");
+ "2.4 m/s\u00B2");
assertFormatSingle(
"Negative numbers: acceleration",
"measure-unit/acceleration-meter-per-square-second",
- // TODO: when other PRs are merged, try: u"unit/meter-per-second-second" instead:
- "measure-unit/acceleration-meter-per-square-second",
+ "unit/meter-per-second-second",
NumberFormatter.with().unit(MeasureUnit.forIdentifier("meter-per-pow2-second")),
new ULocale("af-ZA"),
-9.81,
@@ -869,21 +910,125 @@
// Pass
}
- // .perUnit() may only be passed a built-in type, "square-second" is not a
- // built-in type.
+ // .perUnit() may only be passed a built-in type, or something that
+ // combines to a built-in type together with .unit().
nf = NumberFormatter.with()
- .unit(MeasureUnit.METER)
+ .unit(MeasureUnit.FURLONG)
.perUnit(MeasureUnit.forIdentifier("square-second"))
.locale(new ULocale("en-GB"));
-
try {
nf.format(2.4d);
fail("Expected failure, got: " + nf.format(2.4d) + ".");
} catch (UnsupportedOperationException e) {
// pass
}
+ // As above, "square-second" is not a built-in type, however this time,
+ // meter-per-square-second is a built-in type.
+ assertFormatSingle(
+ "meter per square-second works as a composed unit",
+ "measure-unit/speed-meter-per-second per-measure-unit/duration-second",
+ "unit/meter-per-square-second",
+ NumberFormatter.with()
+ .unit(MeasureUnit.METER)
+ .perUnit(MeasureUnit.forIdentifier("square-second")),
+ new ULocale("en-GB"),
+ 2.4,
+ "2.4 m/s\u00B2");
}
+ @Test
+ public void unitSkeletons() {
+ Object[][] cases = {
+ {"old-form built-in compound unit", //
+ "measure-unit/speed-meter-per-second", //
+ "unit/meter-per-second"},
+
+ {"old-form compound construction, converts to built-in", //
+ "measure-unit/length-meter per-measure-unit/duration-second", //
+ "unit/meter-per-second"},
+
+ {"old-form compound construction which does not simplify to a built-in", //
+ "measure-unit/energy-joule per-measure-unit/length-meter", //
+ "unit/joule-per-meter"},
+
+ {"old-form compound-compound ugliness resolves neatly", //
+ "measure-unit/speed-meter-per-second per-measure-unit/duration-second", //
+ "unit/meter-per-square-second"},
+
+ {"short-form built-in units stick with the built-in", //
+ "unit/meter-per-second", //
+ "unit/meter-per-second"},
+
+ {"short-form compound units stay as is", //
+ "unit/square-meter-per-square-meter", //
+ "unit/square-meter-per-square-meter"},
+
+ {"short-form compound units stay as is", //
+ "unit/joule-per-furlong", //
+ "unit/joule-per-furlong"},
+
+ {"short-form that doesn't consist of built-in units", //
+ "unit/hectometer-per-second", //
+ "unit/hectometer-per-second"},
+
+ {"short-form that doesn't consist of built-in units", //
+ "unit/meter-per-hectosecond", //
+ "unit/meter-per-hectosecond"},
+
+ // // TODO: binary prefixes not supported yet!
+ // {"Round-trip example from icu-units#35", //
+ // "unit/kibijoule-per-furlong", //
+ // "unit/kibijoule-per-furlong"},
+ };
+ for (Object[] cas : cases) {
+ String msg = (String)cas[0];
+ String inputSkeleton = (String)cas[1];
+ String normalizedSkeleton = (String)cas[2];
+ UnlocalizedNumberFormatter nf = NumberFormatter.forSkeleton(inputSkeleton);
+ assertEquals(msg, normalizedSkeleton, nf.toSkeleton());
+ }
+
+ Object NoException = new Object();
+ Object[][] failCases = {
+ {"Parsing measure-unit/* results in failure if not built-in unit",
+ "measure-unit/hectometer", //
+ true, //
+ false},
+
+ {"Parsing per-measure-unit/* results in failure if not built-in unit",
+ "measure-unit/meter per-measure-unit/hectosecond", //
+ true, //
+ false},
+ };
+ for (Object[] cas : failCases) {
+ String msg = (String)cas[0];
+ String inputSkeleton = (String)cas[1];
+ boolean forSkeletonExpectFailure = (boolean)cas[2];
+ boolean toSkeletonExpectFailure = (boolean)cas[3];
+ UnlocalizedNumberFormatter nf = null;
+ try {
+ nf = NumberFormatter.forSkeleton(inputSkeleton);
+ if (forSkeletonExpectFailure) {
+ fail("forSkeleton() should have failed: " + msg);
+ }
+ } catch (Exception e) {
+ if (!forSkeletonExpectFailure) {
+ fail("forSkeleton() should not have failed: " + msg);
+ }
+ continue;
+ }
+ try {
+ nf.toSkeleton();
+ if (toSkeletonExpectFailure) {
+ fail("toSkeleton() should have failed: " + msg);
+ }
+ } catch (Exception e) {
+ if (!toSkeletonExpectFailure) {
+ fail("toSkeleton() should not have failed: " + msg);
+ }
+ }
+ }
+ }
@Test
public void unitUsage() {
@@ -1157,7 +1302,6 @@
// to see divide-by-zero behaviour.
}
-
@Test
public void unitUsageErrorCodes() {
UnlocalizedNumberFormatter unloc_formatter;
@@ -3409,11 +3553,11 @@
}
{
- String message = "Measure unit field position with prefix and suffix";
+ String message = "Measure unit field position with prefix and suffix, composed m/s";
FormattedNumber result = assertFormatSingle(
message,
"measure-unit/length-meter per-measure-unit/duration-second unit-width-full-name",
- "~unit/meter-per-second unit-width-full-name", // does not round-trip to the full skeleton above
+ "measure-unit/length-meter per-measure-unit/duration-second unit-width-full-name",
NumberFormatter.with().unit(MeasureUnit.METER).perUnit(MeasureUnit.SECOND).unitWidth(UnitWidth.FULL_NAME),
new ULocale("ky"), // locale with the interesting data
68,
@@ -3430,6 +3574,27 @@
}
{
+ String message = "Measure unit field position with prefix and suffix, built-in m/s";
+ FormattedNumber result = assertFormatSingle(
+ message,
+ "measure-unit/speed-meter-per-second unit-width-full-name",
+ "unit/meter-per-second unit-width-full-name",
+ NumberFormatter.with().unit(MeasureUnit.METER_PER_SECOND).unitWidth(UnitWidth.FULL_NAME),
+ new ULocale("ky"), // locale with the interesting data
+ 68,
+ "секундасына 68 метр");
+ Object[][] expectedFieldPositions = new Object[][] {
+ // field, begin index, end index
+ {NumberFormat.Field.MEASURE_UNIT, 0, 11},
+ {NumberFormat.Field.INTEGER, 12, 14},
+ {NumberFormat.Field.MEASURE_UNIT, 15, 19}};
+ assertNumberFieldPositions(
+ message,
+ result,
+ expectedFieldPositions);
+ }
+
+ {
String message = "Measure unit field position with inner spaces";
FormattedNumber result = assertFormatSingle(
message,