ICU-21266 Support toSkeleton() for all functional Unit Formatters

See #1347
This commit is contained in:
Hugo van der Merwe 2020-09-30 22:52:30 +00:00
parent a08ac00c67
commit a84fdd0e90
14 changed files with 586 additions and 302 deletions

View file

@ -612,21 +612,7 @@ static const char * const gSubTypes[] = {
"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 @@ bool MeasureUnit::findBySubType(StringPiece subType, MeasureUnit* output) {
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;

View file

@ -223,24 +223,16 @@ void LongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &unitR
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 @@ void LongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &unitR
}
}
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 @@ void LongNameHandler::forCompoundUnit(const Locale &loc, const MeasureUnit &unit
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;

View file

@ -843,10 +843,6 @@ void GeneratorHelpers::generateSkeleton(const MacroProps& macros, UnicodeString&
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 @@ void blueprint_helpers::parseMeasureUnitOption(const StringSegment& segment, Mac
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 @@ void blueprint_helpers::parseIdentifierUnitOption(const StringSegment& segment,
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 @@ bool GeneratorHelpers::unit(const MacroProps& macros, UnicodeString& sb, UErrorC
} 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;
}
}

View file

@ -240,10 +240,10 @@ void parseCurrencyOption(const StringSegment& segment, MacroProps& macros, UErro
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 @@ class GeneratorHelpers {
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);

View file

@ -541,13 +541,6 @@ class U_I18N_API MeasureUnit: public UObject {
* @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

View file

@ -3494,7 +3494,7 @@ void MeasureFormatTest::TestUnitPerUnitResolution() {
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;

View file

@ -54,6 +54,7 @@ class NumberFormatterApiTest : public IntlTestWithFieldPosition {
void notationCompact();
void unitMeasure();
void unitCompoundMeasure();
void unitSkeletons();
void unitUsage();
void unitUsageErrorCodes();
void unitUsageSkeletons();
@ -104,12 +105,15 @@ class NumberFormatterApiTest : public IntlTestWithFieldPosition {
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;

View file

@ -53,12 +53,15 @@ NumberFormatterApiTest::NumberFormatterApiTest(UErrorCode& status)
}
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 @@ void NumberFormatterApiTest::runIndexedTest(int32_t index, UBool exec, const cha
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 @@ void NumberFormatterApiTest::unitMeasure() {
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 @@ void NumberFormatterApiTest::unitMeasure() {
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 @@ void NumberFormatterApiTest::unitMeasure() {
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 @@ void NumberFormatterApiTest::unitMeasure() {
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 @@ void NumberFormatterApiTest::unitMeasure() {
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 @@ void NumberFormatterApiTest::unitMeasure() {
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 @@ void NumberFormatterApiTest::unitMeasure() {
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 @@ void NumberFormatterApiTest::unitMeasure() {
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 @@ void NumberFormatterApiTest::unitCompoundMeasure() {
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 @@ void NumberFormatterApiTest::unitCompoundMeasure() {
// 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 @@ void NumberFormatterApiTest::unitCompoundMeasure() {
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 @@ void NumberFormatterApiTest::unitCompoundMeasure() {
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 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
}
{
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,
@ -3553,6 +3716,28 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
UPRV_LENGTHOF(expectedFieldPositions));
}
{
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(
@ -4141,6 +4326,15 @@ void NumberFormatterApiTest::microPropsInternals() {
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 @@ void NumberFormatterApiTest::assertFormatDescending(
}
}
/* 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 @@ void NumberFormatterApiTest::assertFormatDescendingBig(
}
}
/* 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,

View file

@ -215,16 +215,10 @@ public class LongNameHandler
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 @@ public class LongNameHandler
}
}
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 @@ public class LongNameHandler
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];

View file

@ -70,7 +70,9 @@ public class MeasureUnitImpl {
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;
}

View file

@ -12,6 +12,8 @@ import com.ibm.icu.impl.SoftCache;
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 @@ class NumberSkeletonImpl {
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 @@ class NumberSkeletonImpl {
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 @@ class NumberSkeletonImpl {
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 @@ class NumberSkeletonImpl {
* 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 @@ class NumberSkeletonImpl {
} 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;
}
}

View file

@ -381,7 +381,7 @@ public class MeasureUnit implements Serializable {
/**
* Get the type, such as "length"
* Get the type, such as "length". May return null.
*
* @stable ICU 53
*/
@ -391,7 +391,7 @@ public class MeasureUnit implements Serializable {
/**
* Get the subType, such as foot.
* Get the subType, such as foot. May return null.
*
* @stable ICU 53
*/
@ -702,47 +702,6 @@ public class MeasureUnit implements Serializable {
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 class MeasureUnit implements Serializable {
*/
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 */

View file

@ -2652,7 +2652,7 @@ public class MeasureUnitTest extends TestFmwk {
// 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());

View file

@ -536,6 +536,23 @@ public class NumberFormatterApiTest extends TestFmwk {
"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 @@ public class NumberFormatterApiTest extends TestFmwk {
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 @@ public class NumberFormatterApiTest extends TestFmwk {
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 @@ public class NumberFormatterApiTest extends TestFmwk {
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 @@ public class NumberFormatterApiTest extends TestFmwk {
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 @@ public class NumberFormatterApiTest extends TestFmwk {
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 @@ public class NumberFormatterApiTest extends TestFmwk {
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 @@ public class NumberFormatterApiTest extends TestFmwk {
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 @@ public class NumberFormatterApiTest extends TestFmwk {
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 @@ public class NumberFormatterApiTest extends TestFmwk {
"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 @@ public class NumberFormatterApiTest extends TestFmwk {
// "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)",
"measure-unit/speed-meter-per-second per-measure-unit/duration-second",
"m/s/s simplifies to m/s^2",
"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 @@ public class NumberFormatterApiTest extends TestFmwk {
// 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 @@ public class NumberFormatterApiTest extends TestFmwk {
// to see divide-by-zero behaviour.
}
@Test
public void unitUsageErrorCodes() {
UnlocalizedNumberFormatter unloc_formatter;
@ -3409,11 +3553,11 @@ public class NumberFormatterApiTest extends TestFmwk {
}
{
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,
@ -3429,6 +3573,27 @@ public class NumberFormatterApiTest extends TestFmwk {
expectedFieldPositions);
}
{
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(