From 6ce3295e4ddcd8f655f31fea0427ec8ffbd3d899 Mon Sep 17 00:00:00 2001 From: Mihai Nita Date: Tue, 28 May 2019 15:41:00 -0700 Subject: [PATCH] ICU-20622 Fixing several MeasureFormat problems --- icu4c/source/i18n/measfmt.cpp | 218 ++++++++---------- icu4c/source/i18n/unicode/measfmt.h | 8 - icu4c/source/test/intltest/measfmttest.cpp | 135 ++++++++++- .../src/com/ibm/icu/text/MeasureFormat.java | 195 +++++++--------- .../icu/dev/test/format/MeasureUnitTest.java | 97 ++++++++ 5 files changed, 407 insertions(+), 246 deletions(-) diff --git a/icu4c/source/i18n/measfmt.cpp b/icu4c/source/i18n/measfmt.cpp index 610d298eb76..beb091b4caa 100644 --- a/icu4c/source/i18n/measfmt.cpp +++ b/icu4c/source/i18n/measfmt.cpp @@ -55,28 +55,23 @@ UOBJECT_DEFINE_RTTI_IMPLEMENTATION(MeasureFormat) class NumericDateFormatters : public UMemory { public: // Formats like H:mm - SimpleDateFormat hourMinute; + UnicodeString hourMinute; // formats like M:ss - SimpleDateFormat minuteSecond; + UnicodeString minuteSecond; // formats like H:mm:ss - SimpleDateFormat hourMinuteSecond; + UnicodeString hourMinuteSecond; // Constructor that takes the actual patterns for hour-minute, // minute-second, and hour-minute-second respectively. NumericDateFormatters( const UnicodeString &hm, const UnicodeString &ms, - const UnicodeString &hms, - UErrorCode &status) : - hourMinute(hm, status), - minuteSecond(ms, status), - hourMinuteSecond(hms, status) { - const TimeZone *gmt = TimeZone::getGMT(); - hourMinute.setTimeZone(*gmt); - minuteSecond.setTimeZone(*gmt); - hourMinuteSecond.setTimeZone(*gmt); + const UnicodeString &hms) : + hourMinute(hm), + minuteSecond(ms), + hourMinuteSecond(hms) { } private: NumericDateFormatters(const NumericDateFormatters &other); @@ -233,8 +228,7 @@ static NumericDateFormatters *loadNumericDateFormatters( NumericDateFormatters *result = new NumericDateFormatters( loadNumericDateFormatterPattern(resource, "hm", status), loadNumericDateFormatterPattern(resource, "ms", status), - loadNumericDateFormatterPattern(resource, "hms", status), - status); + loadNumericDateFormatterPattern(resource, "hms", status)); if (U_FAILURE(status)) { delete result; return NULL; @@ -706,55 +700,6 @@ UnicodeString &MeasureFormat::formatMeasure( return appendTo; } -// Formats hours-minutes-seconds as 5:37:23 or similar. -UnicodeString &MeasureFormat::formatNumeric( - const Formattable *hms, // always length 3 - int32_t bitMap, // 1=hourset, 2=minuteset, 4=secondset - UnicodeString &appendTo, - UErrorCode &status) const { - if (U_FAILURE(status)) { - return appendTo; - } - UDate millis = - (UDate) (((uprv_trunc(hms[0].getDouble(status)) * 60.0 - + uprv_trunc(hms[1].getDouble(status))) * 60.0 - + uprv_trunc(hms[2].getDouble(status))) * 1000.0); - switch (bitMap) { - case 5: // hs - case 7: // hms - return formatNumeric( - millis, - cache->getNumericDateFormatters()->hourMinuteSecond, - UDAT_SECOND_FIELD, - hms[2], - appendTo, - status); - break; - case 6: // ms - return formatNumeric( - millis, - cache->getNumericDateFormatters()->minuteSecond, - UDAT_SECOND_FIELD, - hms[2], - appendTo, - status); - break; - case 3: // hm - return formatNumeric( - millis, - cache->getNumericDateFormatters()->hourMinute, - UDAT_MINUTE_FIELD, - hms[1], - appendTo, - status); - break; - default: - status = U_INTERNAL_PROGRAM_ERROR; - return appendTo; - break; - } -} - static void appendRange( const UnicodeString &src, int32_t start, @@ -770,71 +715,112 @@ static void appendRange( dest.append(src, end, src.length() - end); } -// Formats time like 5:37:23 + +// Formats numeric time duration as 5:00:47 or 3:54. UnicodeString &MeasureFormat::formatNumeric( - UDate date, // Time since epoch 1:30:00 would be 5400000 - const DateFormat &dateFmt, // h:mm, m:ss, or h:mm:ss - UDateFormatField smallestField, // seconds in 5:37:23.5 - const Formattable &smallestAmount, // 23.5 for 5:37:23.5 + const Formattable *hms, // always length 3 + int32_t bitMap, // 1=hour set, 2=minute set, 4=second set UnicodeString &appendTo, UErrorCode &status) const { if (U_FAILURE(status)) { return appendTo; } - // Format the smallest amount with this object's NumberFormat - UnicodeString smallestAmountFormatted; - - // We keep track of the integer part of smallest amount so that - // we can replace it later so that we get '0:00:09.3' instead of - // '0:00:9.3' - FieldPosition intFieldPosition(UNUM_INTEGER_FIELD); - (*numberFormat)->format( - smallestAmount, smallestAmountFormatted, intFieldPosition, status); - if ( - intFieldPosition.getBeginIndex() == 0 && - intFieldPosition.getEndIndex() == 0) { + + UnicodeString pattern; + + double hours = hms[0].getDouble(status); + double minutes = hms[1].getDouble(status); + double seconds = hms[2].getDouble(status); + if (U_FAILURE(status)) { + return appendTo; + } + + // All possible combinations: "h", "m", "s", "hm", "hs", "ms", "hms" + if (bitMap == 5 || bitMap == 7) { // "hms" & "hs" (we add minutes if "hs") + pattern = cache->getNumericDateFormatters()->hourMinuteSecond; + hours = uprv_trunc(hours); + minutes = uprv_trunc(minutes); + } else if (bitMap == 3) { // "hm" + pattern = cache->getNumericDateFormatters()->hourMinute; + hours = uprv_trunc(hours); + } else if (bitMap == 6) { // "ms" + pattern = cache->getNumericDateFormatters()->minuteSecond; + minutes = uprv_trunc(minutes); + } else { // h m s, handled outside formatNumeric. No value is also an error. status = U_INTERNAL_PROGRAM_ERROR; return appendTo; } - // Format time. draft becomes something like '5:30:45' - // #13606: DateFormat is not thread-safe, but MeasureFormat advertises itself as thread-safe. - FieldPosition smallestFieldPosition(smallestField); - UnicodeString draft; - static UMutex dateFmtMutex; - umtx_lock(&dateFmtMutex); - dateFmt.format(date, draft, smallestFieldPosition, status); - umtx_unlock(&dateFmtMutex); - - // If we find field for smallest amount replace it with the formatted - // smallest amount from above taking care to replace the integer part - // with what is in original time. For example, If smallest amount - // is 9.35s and the formatted time is 0:00:09 then 9.35 becomes 09.35 - // and replacing yields 0:00:09.35 - if (smallestFieldPosition.getBeginIndex() != 0 || - smallestFieldPosition.getEndIndex() != 0) { - appendRange(draft, 0, smallestFieldPosition.getBeginIndex(), appendTo); - appendRange( - smallestAmountFormatted, - 0, - intFieldPosition.getBeginIndex(), - appendTo); - appendRange( - draft, - smallestFieldPosition.getBeginIndex(), - smallestFieldPosition.getEndIndex(), - appendTo); - appendRange( - smallestAmountFormatted, - intFieldPosition.getEndIndex(), - appendTo); - appendRange( - draft, - smallestFieldPosition.getEndIndex(), - appendTo); + const DecimalFormat *numberFormatter = dynamic_cast(numberFormat->get()); + if (!numberFormatter) { + status = U_INTERNAL_PROGRAM_ERROR; + return appendTo; + } + number::LocalizedNumberFormatter numberFormatter2; + if (auto* lnf = numberFormatter->toNumberFormatter(status)) { + numberFormatter2 = lnf->integerWidth(number::IntegerWidth::zeroFillTo(2)); } else { - appendTo.append(draft); + return appendTo; + } + + FormattedStringBuilder fsb; + + UBool protect = FALSE; + const int32_t patternLength = pattern.length(); + for (int32_t i = 0; i < patternLength; i++) { + char16_t c = pattern[i]; + + // Also set the proper field in this switch + // We don't use DateFormat.Field because this is not a date / time, is a duration. + double value = 0; + switch (c) { + case u'H': value = hours; break; + case u'm': value = minutes; break; + case u's': value = seconds; break; + } + + // For undefined field we use UNUM_FIELD_COUNT, for historical reasons. + // See cleanup bug: https://unicode-org.atlassian.net/browse/ICU-20665 + // But we give it a clear name, to keep "the ugly part" in one place. + constexpr UNumberFormatFields undefinedField = UNUM_FIELD_COUNT; + + // There is not enough info to add Field(s) for the unit because all we have are plain + // text patterns. For example in "21:51" there is no text for something like "hour", + // while in something like "21h51" there is ("h"). But we can't really tell... + switch (c) { + case u'H': + case u'm': + case u's': + if (protect) { + fsb.appendCodePoint(c, undefinedField, status); + } else { + UnicodeString tmp; + if ((i + 1 < patternLength) && pattern[i + 1] == c) { // doubled + tmp = numberFormatter2.formatDouble(value, status).toString(status); + i++; + } else { + numberFormatter->format(value, tmp, status); + } + // TODO: Use proper Field + fsb.append(tmp, undefinedField, status); + } + break; + case u'\'': + // '' is escaped apostrophe + if ((i + 1 < patternLength) && pattern[i + 1] == c) { + fsb.appendCodePoint(c, undefinedField, status); + i++; + } else { + protect = !protect; + } + break; + default: + fsb.appendCodePoint(c, undefinedField, status); + } } + + appendTo.append(fsb.toTempUnicodeString()); + return appendTo; } diff --git a/icu4c/source/i18n/unicode/measfmt.h b/icu4c/source/i18n/unicode/measfmt.h index fa962e1fb9c..a41aa88002b 100644 --- a/icu4c/source/i18n/unicode/measfmt.h +++ b/icu4c/source/i18n/unicode/measfmt.h @@ -384,14 +384,6 @@ class U_I18N_API MeasureFormat : public Format { int32_t bitMap, // 1=hour set, 2=minute set, 4=second set UnicodeString &appendTo, UErrorCode &status) const; - - UnicodeString &formatNumeric( - UDate date, - const DateFormat &dateFmt, - UDateFormatField smallestField, - const Formattable &smallestAmount, - UnicodeString &appendTo, - UErrorCode &status) const; }; U_NAMESPACE_END diff --git a/icu4c/source/test/intltest/measfmttest.cpp b/icu4c/source/test/intltest/measfmttest.cpp index 1b73f541559..59a1eb5035a 100644 --- a/icu4c/source/test/intltest/measfmttest.cpp +++ b/icu4c/source/test/intltest/measfmttest.cpp @@ -75,6 +75,8 @@ private: void TestUnitPerUnitResolution(); void TestIndividualPluralFallback(); void Test20332_PersonUnits(); + void TestNumericTime(); + void TestNumericTimeSomeSpecialFormats(); void verifyFormat( const char *description, const MeasureFormat &fmt, @@ -175,6 +177,8 @@ void MeasureFormatTest::runIndexedTest( TESTCASE_AUTO(TestUnitPerUnitResolution); TESTCASE_AUTO(TestIndividualPluralFallback); TESTCASE_AUTO(Test20332_PersonUnits); + TESTCASE_AUTO(TestNumericTime); + TESTCASE_AUTO(TestNumericTimeSomeSpecialFormats); TESTCASE_AUTO_END; } @@ -1837,6 +1841,32 @@ void MeasureFormatTest::TestFormatPeriodEn() { {t_6h_56_92m, UPRV_LENGTHOF(t_6h_56_92m), "6:56,92"}, {t_3h_5h, UPRV_LENGTHOF(t_3h_5h), "3 Std., 5 Std."}}; + ExpectedResult numericDataBn[] = { + {t_1m_59_9996s, UPRV_LENGTHOF(t_1m_59_9996s), "\\u09E7:\\u09EB\\u09EF.\\u09EF\\u09EF\\u09EF\\u09EC"}, + {t_19m, UPRV_LENGTHOF(t_19m), "\\u09E7\\u09EF \\u09AE\\u09BF\\u0983"}, + {t_1h_23_5s, UPRV_LENGTHOF(t_1h_23_5s), "\\u09E7:\\u09E6\\u09E6:\\u09E8\\u09E9.\\u09EB"}, + {t_1h_0m_23s, UPRV_LENGTHOF(t_1h_0m_23s), "\\u09E7:\\u09E6\\u09E6:\\u09E8\\u09E9"}, + {t_1h_23_5m, UPRV_LENGTHOF(t_1h_23_5m), "\\u09E7:\\u09E8\\u09E9.\\u09EB"}, + {t_5h_17m, UPRV_LENGTHOF(t_5h_17m), "\\u09EB:\\u09E7\\u09ED"}, + {t_19m_28s, UPRV_LENGTHOF(t_19m_28s), "\\u09E7\\u09EF:\\u09E8\\u09EE"}, + {t_2y_5M_3w_4d, UPRV_LENGTHOF(t_2y_5M_3w_4d), "\\u09E8 \\u09AC\\u099B\\u09B0, \\u09EB \\u09AE\\u09BE\\u09B8, \\u09E9 \\u09B8\\u09AA\\u09CD\\u09A4\\u09BE\\u09B9, \\u09EA \\u09A6\\u09BF\\u09A8"}, + {t_0h_0m_17s, UPRV_LENGTHOF(t_0h_0m_17s), "\\u09E6:\\u09E6\\u09E6:\\u09E7\\u09ED"}, + {t_6h_56_92m, UPRV_LENGTHOF(t_6h_56_92m), "\\u09EC:\\u09EB\\u09EC.\\u09EF\\u09E8"}, + {t_3h_5h, UPRV_LENGTHOF(t_3h_5h), "\\u09E9 \\u0998\\u0983, \\u09EB \\u0998\\u0983"}}; + + ExpectedResult numericDataBnLatn[] = { + {t_1m_59_9996s, UPRV_LENGTHOF(t_1m_59_9996s), "1:59.9996"}, + {t_19m, UPRV_LENGTHOF(t_19m), "19 \\u09AE\\u09BF\\u0983"}, + {t_1h_23_5s, UPRV_LENGTHOF(t_1h_23_5s), "1:00:23.5"}, + {t_1h_0m_23s, UPRV_LENGTHOF(t_1h_0m_23s), "1:00:23"}, + {t_1h_23_5m, UPRV_LENGTHOF(t_1h_23_5m), "1:23.5"}, + {t_5h_17m, UPRV_LENGTHOF(t_5h_17m), "5:17"}, + {t_19m_28s, UPRV_LENGTHOF(t_19m_28s), "19:28"}, + {t_2y_5M_3w_4d, UPRV_LENGTHOF(t_2y_5M_3w_4d), "2 \\u09AC\\u099B\\u09B0, 5 \\u09AE\\u09BE\\u09B8, 3 \\u09B8\\u09AA\\u09CD\\u09A4\\u09BE\\u09B9, 4 \\u09A6\\u09BF\\u09A8"}, + {t_0h_0m_17s, UPRV_LENGTHOF(t_0h_0m_17s), "0:00:17"}, + {t_6h_56_92m, UPRV_LENGTHOF(t_6h_56_92m), "6:56.92"}, + {t_3h_5h, UPRV_LENGTHOF(t_3h_5h), "3 \\u0998\\u0983, 5 \\u0998\\u0983"}}; + Locale en(Locale::getEnglish()); LocalPointer nf(NumberFormat::createInstance(en, status)); if (U_FAILURE(status)) { @@ -1893,6 +1923,30 @@ void MeasureFormatTest::TestFormatPeriodEn() { return; } verifyFormat("de NUMERIC", mf, numericDataDe, UPRV_LENGTHOF(numericDataDe)); + + Locale bengali("bn"); + nf.adoptInstead(NumberFormat::createInstance(bengali, status)); + if (!assertSuccess("Error creating number format de object", status)) { + return; + } + nf->setMaximumFractionDigits(4); + mf = MeasureFormat(bengali, UMEASFMT_WIDTH_NUMERIC, (NumberFormat *) nf->clone(), status); + if (!assertSuccess("Error creating measure format bn NUMERIC", status)) { + return; + } + verifyFormat("bn NUMERIC", mf, numericDataBn, UPRV_LENGTHOF(numericDataBn)); + + Locale bengaliLatin("bn-u-nu-latn"); + nf.adoptInstead(NumberFormat::createInstance(bengaliLatin, status)); + if (!assertSuccess("Error creating number format de object", status)) { + return; + } + nf->setMaximumFractionDigits(4); + mf = MeasureFormat(bengaliLatin, UMEASFMT_WIDTH_NUMERIC, (NumberFormat *) nf->clone(), status); + if (!assertSuccess("Error creating measure format bn-u-nu-latn NUMERIC", status)) { + return; + } + verifyFormat("bn-u-nu-latn NUMERIC", mf, numericDataBnLatn, UPRV_LENGTHOF(numericDataBnLatn)); } void MeasureFormatTest::Test10219FractionalPlurals() { @@ -1967,7 +2021,7 @@ void MeasureFormatTest::TestGreek() { "1 \\u03B7\\u03BC\\u03AD\\u03C1\\u03B1", "1 \\u03B5\\u03B2\\u03B4.", "1 \\u03BC\\u03AE\\u03BD.", - "1 \\u03AD\\u03C4.", // year (one) + "1 \\u03AD\\u03C4.", // year (one) // "el_GR" 7 wide "7 \\u03B4\\u03B5\\u03C5\\u03C4\\u03B5\\u03C1\\u03CC\\u03BB\\u03B5\\u03C0\\u03C4\\u03B1", "7 \\u03BB\\u03B5\\u03C0\\u03C4\\u03AC", @@ -1979,7 +2033,7 @@ void MeasureFormatTest::TestGreek() { // "el_GR" 7 short "7 \\u03B4\\u03B5\\u03C5\\u03C4.", "7 \\u03BB\\u03B5\\u03C0.", - "7 \\u03CE\\u03C1.", // hour (other) + "7 \\u03CE\\u03C1.", // hour (other) "7 \\u03B7\\u03BC\\u03AD\\u03C1\\u03B5\\u03C2", "7 \\u03B5\\u03B2\\u03B4.", "7 \\u03BC\\u03AE\\u03BD.", @@ -2000,7 +2054,7 @@ void MeasureFormatTest::TestGreek() { "1 \\u03B7\\u03BC\\u03AD\\u03C1\\u03B1", "1 \\u03B5\\u03B2\\u03B4.", "1 \\u03BC\\u03AE\\u03BD.", - "1 \\u03AD\\u03C4.", // year (one) + "1 \\u03AD\\u03C4.", // year (one) // "el" 7 wide "7 \\u03B4\\u03B5\\u03C5\\u03C4\\u03B5\\u03C1\\u03CC\\u03BB\\u03B5\\u03C0\\u03C4\\u03B1", "7 \\u03BB\\u03B5\\u03C0\\u03C4\\u03AC", @@ -2012,7 +2066,7 @@ void MeasureFormatTest::TestGreek() { // "el" 7 short "7 \\u03B4\\u03B5\\u03C5\\u03C4.", "7 \\u03BB\\u03B5\\u03C0.", - "7 \\u03CE\\u03C1.", // hour (other) + "7 \\u03CE\\u03C1.", // hour (other) "7 \\u03B7\\u03BC\\u03AD\\u03C1\\u03B5\\u03C2", "7 \\u03B5\\u03B2\\u03B4.", "7 \\u03BC\\u03AE\\u03BD.", @@ -2691,6 +2745,79 @@ void MeasureFormatTest::Test20332_PersonUnits() { } } +void MeasureFormatTest::TestNumericTime() { + IcuTestErrorCode status(*this, "TestNumericTime"); + + MeasureFormat fmt("en", UMEASFMT_WIDTH_NUMERIC, status); + + Measure hours(112, MeasureUnit::createHour(status), status); + Measure minutes(113, MeasureUnit::createMinute(status), status); + Measure seconds(114, MeasureUnit::createSecond(status), status); + Measure fhours(112.8765, MeasureUnit::createHour(status), status); + Measure fminutes(113.8765, MeasureUnit::createMinute(status), status); + Measure fseconds(114.8765, MeasureUnit::createSecond(status), status); + assertSuccess("", status); + + verifyFormat("hours", fmt, &hours, 1, "112h"); + verifyFormat("minutes", fmt, &minutes, 1, "113m"); + verifyFormat("seconds", fmt, &seconds, 1, "114s"); + + verifyFormat("fhours", fmt, &fhours, 1, "112.876h"); + verifyFormat("fminutes", fmt, &fminutes, 1, "113.876m"); + verifyFormat("fseconds", fmt, &fseconds, 1, "114.876s"); + + Measure hoursMinutes[2] = {hours, minutes}; + verifyFormat("hoursMinutes", fmt, hoursMinutes, 2, "112:113"); + Measure hoursSeconds[2] = {hours, seconds}; + verifyFormat("hoursSeconds", fmt, hoursSeconds, 2, "112:00:114"); + Measure minutesSeconds[2] = {minutes, seconds}; + verifyFormat("minutesSeconds", fmt, minutesSeconds, 2, "113:114"); + + Measure hoursFminutes[2] = {hours, fminutes}; + verifyFormat("hoursFminutes", fmt, hoursFminutes, 2, "112:113.876"); + Measure hoursFseconds[2] = {hours, fseconds}; + verifyFormat("hoursFseconds", fmt, hoursFseconds, 2, "112:00:114.876"); + Measure minutesFseconds[2] = {minutes, fseconds}; + verifyFormat("hoursMminutesFsecondsinutes", fmt, minutesFseconds, 2, "113:114.876"); + + Measure fhoursMinutes[2] = {fhours, minutes}; + verifyFormat("fhoursMinutes", fmt, fhoursMinutes, 2, "112:113"); + Measure fhoursSeconds[2] = {fhours, seconds}; + verifyFormat("fhoursSeconds", fmt, fhoursSeconds, 2, "112:00:114"); + Measure fminutesSeconds[2] = {fminutes, seconds}; + verifyFormat("fminutesSeconds", fmt, fminutesSeconds, 2, "113:114"); + + Measure fhoursFminutes[2] = {fhours, fminutes}; + verifyFormat("fhoursFminutes", fmt, fhoursFminutes, 2, "112:113.876"); + Measure fhoursFseconds[2] = {fhours, fseconds}; + verifyFormat("fhoursFseconds", fmt, fhoursFseconds, 2, "112:00:114.876"); + Measure fminutesFseconds[2] = {fminutes, fseconds}; + verifyFormat("fminutesFseconds", fmt, fminutesFseconds, 2, "113:114.876"); + + Measure hoursMinutesSeconds[3] = {hours, minutes, seconds}; + verifyFormat("hoursMinutesSeconds", fmt, hoursMinutesSeconds, 3, "112:113:114"); + Measure fhoursFminutesFseconds[3] = {fhours, fminutes, fseconds}; + verifyFormat("fhoursFminutesFseconds", fmt, fhoursFminutesFseconds, 3, "112:113:114.876"); +} + +void MeasureFormatTest::TestNumericTimeSomeSpecialFormats() { + IcuTestErrorCode status(*this, "TestNumericTimeSomeSpecialFormats"); + + Measure fhours(2.8765432, MeasureUnit::createHour(status), status); + Measure fminutes(3.8765432, MeasureUnit::createMinute(status), status); + assertSuccess("", status); + + Measure fhoursFminutes[2] = {fhours, fminutes}; + + // Latvian is one of the very few locales 0-padding the hour + MeasureFormat fmtLt("lt", UMEASFMT_WIDTH_NUMERIC, status); + verifyFormat("Latvian fhoursFminutes", fmtLt, fhoursFminutes, 2, "02:03,877"); + + // Danish is one of the very few locales using '.' as separator + MeasureFormat fmtDa("da", UMEASFMT_WIDTH_NUMERIC, status); + verifyFormat("Danish fhoursFminutes", fmtDa, fhoursFminutes, 2, "2.03,877"); +} + void MeasureFormatTest::verifyFieldPosition( const char *description, diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java index 93fddbcc786..d3f230678e4 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java @@ -23,7 +23,6 @@ import java.text.FieldPosition; import java.text.ParsePosition; import java.util.Arrays; import java.util.Collection; -import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -31,6 +30,7 @@ import java.util.MissingResourceException; import java.util.concurrent.ConcurrentHashMap; import com.ibm.icu.impl.DontCareFieldPosition; +import com.ibm.icu.impl.FormattedStringBuilder; import com.ibm.icu.impl.ICUData; import com.ibm.icu.impl.ICUResourceBundle; import com.ibm.icu.impl.SimpleCache; @@ -38,6 +38,7 @@ import com.ibm.icu.impl.SimpleFormatterImpl; import com.ibm.icu.impl.number.LongNameHandler; import com.ibm.icu.impl.number.RoundingUtils; import com.ibm.icu.number.FormattedNumber; +import com.ibm.icu.number.IntegerWidth; import com.ibm.icu.number.LocalizedNumberFormatter; import com.ibm.icu.number.NumberFormatter; import com.ibm.icu.number.NumberFormatter.UnitWidth; @@ -47,7 +48,6 @@ import com.ibm.icu.util.Currency; import com.ibm.icu.util.ICUUncheckedIOException; import com.ibm.icu.util.Measure; import com.ibm.icu.util.MeasureUnit; -import com.ibm.icu.util.TimeZone; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.ULocale.Category; import com.ibm.icu.util.UResourceBundle; @@ -655,28 +655,28 @@ public class MeasureFormat extends UFormat { } static class NumericFormatters { - private DateFormat hourMinute; - private DateFormat minuteSecond; - private DateFormat hourMinuteSecond; + private String hourMinute; + private String minuteSecond; + private String hourMinuteSecond; public NumericFormatters( - DateFormat hourMinute, - DateFormat minuteSecond, - DateFormat hourMinuteSecond) { + String hourMinute, + String minuteSecond, + String hourMinuteSecond) { this.hourMinute = hourMinute; this.minuteSecond = minuteSecond; this.hourMinuteSecond = hourMinuteSecond; } - public DateFormat getHourMinute() { + public String getHourMinute() { return hourMinute; } - public DateFormat getMinuteSecond() { + public String getMinuteSecond() { return minuteSecond; } - public DateFormat getHourMinuteSecond() { + public String getHourMinuteSecond() { return hourMinuteSecond; } } @@ -823,12 +823,10 @@ public class MeasureFormat extends UFormat { } // type is one of "hm", "ms" or "hms" - private static DateFormat loadNumericDurationFormat(ICUResourceBundle r, String type) { + private static String loadNumericDurationFormat(ICUResourceBundle r, String type) { r = r.getWithFallback(String.format("durationUnits/%s", type)); // We replace 'h' with 'H' because 'h' does not make sense in the context of durations. - DateFormat result = new SimpleDateFormat(r.getString().replace("h", "H")); - result.setTimeZone(TimeZone.GMT_ZONE); - return result; + return r.getString().replace("h", "H"); } // Returns hours in [0]; minutes in [1]; seconds in [2] out of measures array. If @@ -861,116 +859,77 @@ public class MeasureFormat extends UFormat { // Formats numeric time duration as 5:00:47 or 3:54. In the process, it replaces any null // values in hms with 0. private void formatNumeric(Number[] hms, Appendable appendable) { - - // find the start and end of non-nil values in hms array. We have to know if we - // have hour-minute; minute-second; or hour-minute-second. - int startIndex = -1; - int endIndex = -1; - for (int i = 0; i < hms.length; i++) { - if (hms[i] != null) { - endIndex = i; - if (startIndex == -1) { - startIndex = endIndex; - } - } else { - // Replace nil value with 0. - hms[i] = Integer.valueOf(0); - } - } - // convert hours, minutes, seconds into milliseconds. - long millis = (long) (((Math.floor(hms[0].doubleValue()) * 60.0 - + Math.floor(hms[1].doubleValue())) * 60.0 + Math.floor(hms[2].doubleValue())) * 1000.0); - Date d = new Date(millis); - if (startIndex == 0 && endIndex == 2) { - // if hour-minute-second - formatNumeric(d, - numericFormatters.getHourMinuteSecond(), - DateFormat.Field.SECOND, - hms[endIndex], - appendable); - } else if (startIndex == 1 && endIndex == 2) { - // if minute-second - formatNumeric(d, - numericFormatters.getMinuteSecond(), - DateFormat.Field.SECOND, - hms[endIndex], - appendable); - } else if (startIndex == 0 && endIndex == 1) { - // if hour-minute - formatNumeric(d, - numericFormatters.getHourMinute(), - DateFormat.Field.MINUTE, - hms[endIndex], - appendable); - } else { + String pattern; + + // All possible combinations: "h", "m", "s", "hm", "hs", "ms", "hms" + if (hms[0] != null && hms[2] != null) { // "hms" & "hs" (we add minutes if "hs") + pattern = numericFormatters.getHourMinuteSecond(); + if (hms[1] == null) + hms[1] = 0; + hms[1] = Math.floor(hms[1].doubleValue()); + hms[0] = Math.floor(hms[0].doubleValue()); + } else if (hms[0] != null && hms[1] != null) { // "hm" + pattern = numericFormatters.getHourMinute(); + hms[0] = Math.floor(hms[0].doubleValue()); + } else if (hms[1] != null && hms[2] != null) { // "ms" + pattern = numericFormatters.getMinuteSecond(); + hms[1] = Math.floor(hms[1].doubleValue()); + } else { // h m s, handled outside formatNumeric. No value is also an error. throw new IllegalStateException(); } - } - // Formats a duration as 5:00:37 or 23:59. - // duration is a particular duration after epoch. - // formatter is a hour-minute-second, hour-minute, or minute-second formatter. - // smallestField denotes what the smallest field is in duration: either - // hour, minute, or second. - // smallestAmount is the value of that smallest field. for 5:00:37.3, - // smallestAmount is 37.3. This smallest field is formatted with this object's - // NumberFormat instead of formatter. - // appendTo is where the formatted string is appended. - private void formatNumeric( - Date duration, - DateFormat formatter, - DateFormat.Field smallestField, - Number smallestAmount, - Appendable appendTo) { - // Format the smallest amount ahead of time. - String smallestAmountFormatted; - - // Format the smallest amount using this object's number format, but keep track - // of the integer portion of this formatted amount. We have to replace just the - // integer part with the corresponding value from formatting the date. Otherwise - // when formatting 0 minutes 9 seconds, we may get "00:9" instead of "00:09" - FieldPosition intFieldPosition = new FieldPosition(NumberFormat.INTEGER_FIELD); - FormattedNumber result = getNumberFormatter().format(smallestAmount); - result.nextFieldPosition(intFieldPosition); - smallestAmountFormatted = result.toString(); - // Give up if there is no integer field. - if (intFieldPosition.getBeginIndex() == 0 && intFieldPosition.getEndIndex() == 0) { - throw new IllegalStateException(); - } + // We can create it on demand, but all of the patterns (right now) have mm and ss. + // So unless it is hours only we will need a 0-padded 2 digits formatter. + LocalizedNumberFormatter numberFormatter2 = numberFormatter.integerWidth(IntegerWidth.zeroFillTo(2)); + FormattedStringBuilder fsb = new FormattedStringBuilder(); + + boolean protect = false; + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + + // Also set the proper field in this switch + // We don't use DateFormat.Field because this is not a date / time, is a duration. + Number value = 0; + switch (c) { + case 'H': value = hms[0]; break; + case 'm': value = hms[1]; break; + case 's': value = hms[2]; break; + } - // Format our duration as a date, but keep track of where the smallest field is - // so that we can use it to replace the integer portion of the smallest value. - // #13606: DateFormat is not thread-safe, but MeasureFormat advertises itself as thread-safe. - FieldPosition smallestFieldPosition = new FieldPosition(smallestField); - String draft; - synchronized (formatter) { - draft = formatter.format(duration, new StringBuffer(), smallestFieldPosition).toString(); + // There is not enough info to add Field(s) for the unit because all we have are plain + // text patterns. For example in "21:51" there is no text for something like "hour", + // while in something like "21h51" there is ("h"). But we can't really tell... + switch (c) { + case 'H': + case 'm': + case 's': + if (protect) { + fsb.appendCodePoint(c, null); + } else { + if ((i + 1 < pattern.length()) && pattern.charAt(i + 1) == c) { // doubled + fsb.append(numberFormatter2.format(value), null); // TODO: Use proper Field + i++; + } else { + fsb.append(numberFormatter.format(value), null); // TODO: Use proper Field + } + } + break; + case '\'': + // '' is escaped apostrophe + if ((i + 1 < pattern.length()) && pattern.charAt(i + 1) == c) { + fsb.appendCodePoint(c, null); + i++; + } else { + protect = !protect; + } + break; + default: + fsb.appendCodePoint(c, null); + } } try { - // If we find the smallest field - if (smallestFieldPosition.getBeginIndex() != 0 || smallestFieldPosition.getEndIndex() != 0) { - // add everything up to the start of the smallest field in duration. - appendTo.append(draft, 0, smallestFieldPosition.getBeginIndex()); - - // add everything in the smallest field up to the integer portion - appendTo.append(smallestAmountFormatted, 0, intFieldPosition.getBeginIndex()); - - // Add the smallest field in formatted duration in lieu of the integer portion - // of smallest field - appendTo.append(draft, - smallestFieldPosition.getBeginIndex(), - smallestFieldPosition.getEndIndex()); - - // Add the rest of the smallest field - appendTo.append(smallestAmountFormatted, - intFieldPosition.getEndIndex(), - smallestAmountFormatted.length()); - appendTo.append(draft, smallestFieldPosition.getEndIndex(), draft.length()); - } else { - // As fallback, just use the formatted duration. - appendTo.append(draft); - } + appendable.append(fsb); } catch (IOException e) { throw new ICUUncheckedIOException(e); } 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 3691a347715..f3c79790a38 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 @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -1623,6 +1624,30 @@ public class MeasureUnitTest extends TestFmwk { {_0h_0m_17s, "0:00:17"}, {_6h_56_92m, "6:56,92"}, {_3h_5h, "3 Std., 5 Std."}}; + Object[][] numericDataBn = { + {_1m_59_9996s, "১:৫৯.৯৯৯৬"}, + {_19m, "১৯ মিঃ"}, + {_1h_23_5s, "১:০০:২৩.৫"}, + {_1h_0m_23s, "১:০০:২৩"}, + {_1h_23_5m, "১:২৩.৫"}, + {_5h_17m, "৫:১৭"}, + {_19m_28s, "১৯:২৮"}, + {_2y_5M_3w_4d, "২ বছর, ৫ মাস, ৩ সপ্তাহ, ৪ দিন"}, + {_0h_0m_17s, "০:০০:১৭"}, + {_6h_56_92m, "৬:৫৬.৯২"}, + {_3h_5h, "৩ ঘঃ, ৫ ঘঃ"}}; + Object[][] numericDataBnLatn = { + {_1m_59_9996s, "1:59.9996"}, + {_19m, "19 মিঃ"}, + {_1h_23_5s, "1:00:23.5"}, + {_1h_0m_23s, "1:00:23"}, + {_1h_23_5m, "1:23.5"}, + {_5h_17m, "5:17"}, + {_19m_28s, "19:28"}, + {_2y_5M_3w_4d, "2 বছর, 5 মাস, 3 সপ্তাহ, 4 দিন"}, + {_0h_0m_17s, "0:00:17"}, + {_6h_56_92m, "6:56.92"}, + {_3h_5h, "3 ঘঃ, 5 ঘঃ"}}; NumberFormat nf = NumberFormat.getNumberInstance(ULocale.ENGLISH); nf.setMaximumFractionDigits(4); @@ -1650,6 +1675,17 @@ public class MeasureUnitTest extends TestFmwk { mf = MeasureFormat.getInstance(Locale.GERMAN, FormatWidth.NUMERIC, nf); verifyFormatPeriod("de NUMERIC(Java Locale)", mf, numericDataDe); + ULocale bengali = ULocale.forLanguageTag("bn"); + nf = NumberFormat.getNumberInstance(bengali); + nf.setMaximumFractionDigits(4); + mf = MeasureFormat.getInstance(bengali, FormatWidth.NUMERIC, nf); + verifyFormatPeriod("bn NUMERIC(Java Locale)", mf, numericDataBn); + + bengali = ULocale.forLanguageTag("bn-u-nu-latn"); + nf = NumberFormat.getNumberInstance(bengali); + nf.setMaximumFractionDigits(4); + mf = MeasureFormat.getInstance(bengali, FormatWidth.NUMERIC, nf); + verifyFormatPeriod("bn NUMERIC(Java Locale)", mf, numericDataBnLatn); } private void verifyFormatPeriod(String desc, MeasureFormat mf, Object[][] testData) { @@ -2979,4 +3015,65 @@ public class MeasureUnitTest extends TestFmwk { return false; } } + + @Test + public void TestNumericTimeNonLatin() { + ULocale ulocale = ULocale.forLanguageTag("bn"); + MeasureFormat fmt = MeasureFormat.getInstance(ulocale, FormatWidth.NUMERIC); + String actual = fmt.formatMeasures(new Measure(12, MeasureUnit.MINUTE), new Measure(39.12345, MeasureUnit.SECOND)); + assertEquals("Incorect digits", "১২:৩৯.১২৩", actual); + } + + @Test + public void TestNumericTime() { + MeasureFormat fmt = MeasureFormat.getInstance(ULocale.forLanguageTag("en"), FormatWidth.NUMERIC); + + Measure hours = new Measure(112, MeasureUnit.HOUR); + Measure minutes = new Measure(113, MeasureUnit.MINUTE); + Measure seconds = new Measure(114, MeasureUnit.SECOND); + Measure fhours = new Measure(112.8765, MeasureUnit.HOUR); + Measure fminutes = new Measure(113.8765, MeasureUnit.MINUTE); + Measure fseconds = new Measure(114.8765, MeasureUnit.SECOND); + + Assert.assertEquals("112h", fmt.formatMeasures(hours)); + Assert.assertEquals("113m", fmt.formatMeasures(minutes)); + Assert.assertEquals("114s", fmt.formatMeasures(seconds)); + + Assert.assertEquals("112.876h", fmt.formatMeasures(fhours)); + Assert.assertEquals("113.876m", fmt.formatMeasures(fminutes)); + Assert.assertEquals("114.876s", fmt.formatMeasures(fseconds)); + + Assert.assertEquals("112:113", fmt.formatMeasures(hours, minutes)); + Assert.assertEquals("112:00:114", fmt.formatMeasures(hours, seconds)); + Assert.assertEquals("113:114", fmt.formatMeasures(minutes, seconds)); + + Assert.assertEquals("112:113.876", fmt.formatMeasures(hours, fminutes)); + Assert.assertEquals("112:00:114.876", fmt.formatMeasures(hours, fseconds)); + Assert.assertEquals("113:114.876", fmt.formatMeasures(minutes, fseconds)); + + Assert.assertEquals("112:113", fmt.formatMeasures(fhours, minutes)); + Assert.assertEquals("112:00:114", fmt.formatMeasures(fhours, seconds)); + Assert.assertEquals("113:114", fmt.formatMeasures(fminutes, seconds)); + + Assert.assertEquals("112:113.876", fmt.formatMeasures(fhours, fminutes)); + Assert.assertEquals("112:00:114.876", fmt.formatMeasures(fhours, fseconds)); + Assert.assertEquals("113:114.876", fmt.formatMeasures(fminutes, fseconds)); + + Assert.assertEquals("112:113:114", fmt.formatMeasures(hours, minutes, seconds)); + Assert.assertEquals("112:113:114.876", fmt.formatMeasures(fhours, fminutes, fseconds)); + } + + @Test + public void TestNumericTimeSomeSpecialFormats() { + Measure fhours = new Measure(2.8765432, MeasureUnit.HOUR); + Measure fminutes = new Measure(3.8765432, MeasureUnit.MINUTE); + + // Latvian is one of the very few locales 0-padding the hour + MeasureFormat fmt = MeasureFormat.getInstance(ULocale.forLanguageTag("lt"), FormatWidth.NUMERIC); + Assert.assertEquals("02:03,877", fmt.formatMeasures(fhours, fminutes)); + + // Danish is one of the very few locales using '.' as separator + fmt = MeasureFormat.getInstance(ULocale.forLanguageTag("da"), FormatWidth.NUMERIC); + Assert.assertEquals("2.03,877", fmt.formatMeasures(fhours, fminutes)); + } } -- 2.40.0