ICU-20622 Fixing several MeasureFormat problems

This commit is contained in:
Mihai Nita 2019-05-28 15:41:00 -07:00 committed by Shane F. Carr
parent 506c935bf5
commit 6ce3295e4d
5 changed files with 408 additions and 247 deletions
icu4c/source
i18n
test/intltest
icu4j/main
classes/core/src/com/ibm/icu/text
tests/core/src/com/ibm/icu/dev/test/format

View 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);
} else {
appendTo.append(draft);
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 {
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;
}

View 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

View 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,

View 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) {
String pattern;
// 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);
// 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();
}
// 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;
}
}
// 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 {
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();
}
// 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);
}

View 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));
}
}