]> granicus.if.org Git - icu/commitdiff
ICU-20622 Fixing several MeasureFormat problems
authorMihai Nita <nmihai_2000@yahoo.com>
Tue, 28 May 2019 22:41:00 +0000 (15:41 -0700)
committerShane F. Carr <shane@unicode.org>
Mon, 17 Jun 2019 20:44:38 +0000 (13:44 -0700)
icu4c/source/i18n/measfmt.cpp
icu4c/source/i18n/unicode/measfmt.h
icu4c/source/test/intltest/measfmttest.cpp
icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java

index 610d298eb766dae382bf0586d2a1b5213a09a8dd..beb091b4caa84cc747b2117e32f59af88f92e293 100644 (file)
@@ -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<const DecimalFormat*>(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;
 }
 
index fa962e1fb9cc413316b0c186d19aaec11ef58c71..a41aa88002b69e6b156d6ed3d69522c679c019aa 100644 (file)
@@ -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
index 1b73f541559ad048f8658e49e7bd414efcf481db..59a1eb5035ac5112ba716bcfd4b70d5ff3801855 100644 (file)
@@ -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<NumberFormat> 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,
index 93fddbcc786b8d9284a55e52078d86253ff9ee89..d3f230678e4be4ca715fcfb9933f1991e8b7a810 100644 (file)
@@ -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);
         }
index 3691a347715768fce464a679b980e2e27a5e07e7..f3c79790a38647c9ce35645d9beda973d71dae02 100644 (file)
@@ -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));
+    }
 }