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);
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;
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,
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;
}
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
void TestUnitPerUnitResolution();
void TestIndividualPluralFallback();
void Test20332_PersonUnits();
+ void TestNumericTime();
+ void TestNumericTimeSomeSpecialFormats();
void verifyFormat(
const char *description,
const MeasureFormat &fmt,
TESTCASE_AUTO(TestUnitPerUnitResolution);
TESTCASE_AUTO(TestIndividualPluralFallback);
TESTCASE_AUTO(Test20332_PersonUnits);
+ TESTCASE_AUTO(TestNumericTime);
+ TESTCASE_AUTO(TestNumericTimeSomeSpecialFormats);
TESTCASE_AUTO_END;
}
{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)) {
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() {
"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",
// "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.",
"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",
// "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.",
}
}
+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,
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;
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;
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;
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;
}
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;
}
}
}
// 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
// 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);
}
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;
{_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);
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) {
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));
+ }
}