ICU-20442 Adding support for hour-cycle on DateTimePatternGenerator

DateTimePatternGenerator needs to consider the hour-cycle preferred by
Locale. This means that we need to to override the hour-cycle when a
locale contains "hc" keyword. This patch is adding such functionality.
In addition, "DateTimePatternGenerator::adjustFieldTypes" should adjust
hour field to properly follow tr35
spec(https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-hour).
This commit is contained in:
Caio Lima 2019-12-12 19:14:28 -08:00 committed by Shane F. Carr
parent d88fd11a34
commit 09d409f5f4
5 changed files with 187 additions and 19 deletions

View file

@ -654,6 +654,23 @@ void DateTimePatternGenerator::getAllowedHourFormats(const Locale &locale, UErro
int32_t* allowedFormats = getAllowedHourFormatsLangCountry(language, country, status);
// We need to check if there is an hour cycle on locale
char buffer[8];
int32_t count = locale.getKeywordValue("hours", buffer, sizeof(buffer), status);
fDefaultHourFormatChar = 0;
if (U_SUCCESS(status) && count > 0) {
if(uprv_strcmp(buffer, "h24") == 0) {
fDefaultHourFormatChar = LOW_K;
} else if(uprv_strcmp(buffer, "h23") == 0) {
fDefaultHourFormatChar = CAP_H;
} else if(uprv_strcmp(buffer, "h12") == 0) {
fDefaultHourFormatChar = LOW_H;
} else if(uprv_strcmp(buffer, "h11") == 0) {
fDefaultHourFormatChar = CAP_K;
}
}
// Check if the region has an alias
if (allowedFormats == nullptr) {
UErrorCode localStatus = U_ZERO_ERROR;
@ -667,13 +684,16 @@ void DateTimePatternGenerator::getAllowedHourFormats(const Locale &locale, UErro
if (allowedFormats != nullptr) { // Lookup is successful
// Here allowedFormats points to a list consisting of key for preferredFormat,
// followed by one or more keys for allowedFormats, then followed by ALLOWED_HOUR_FORMAT_UNKNOWN.
switch (allowedFormats[0]) {
case ALLOWED_HOUR_FORMAT_h: fDefaultHourFormatChar = LOW_H; break;
case ALLOWED_HOUR_FORMAT_H: fDefaultHourFormatChar = CAP_H; break;
case ALLOWED_HOUR_FORMAT_K: fDefaultHourFormatChar = CAP_K; break;
case ALLOWED_HOUR_FORMAT_k: fDefaultHourFormatChar = LOW_K; break;
default: fDefaultHourFormatChar = CAP_H; break;
if (!fDefaultHourFormatChar) {
switch (allowedFormats[0]) {
case ALLOWED_HOUR_FORMAT_h: fDefaultHourFormatChar = LOW_H; break;
case ALLOWED_HOUR_FORMAT_H: fDefaultHourFormatChar = CAP_H; break;
case ALLOWED_HOUR_FORMAT_K: fDefaultHourFormatChar = CAP_K; break;
case ALLOWED_HOUR_FORMAT_k: fDefaultHourFormatChar = LOW_K; break;
default: fDefaultHourFormatChar = CAP_H; break;
}
}
for (int32_t i = 0; i < UPRV_LENGTHOF(fAllowedHourFormats); ++i) {
fAllowedHourFormats[i] = allowedFormats[i + 1];
if (fAllowedHourFormats[i] == ALLOWED_HOUR_FORMAT_UNKNOWN) {
@ -681,7 +701,9 @@ void DateTimePatternGenerator::getAllowedHourFormats(const Locale &locale, UErro
}
}
} else { // Lookup failed, twice
fDefaultHourFormatChar = CAP_H;
if (!fDefaultHourFormatChar) {
fDefaultHourFormatChar = CAP_H;
}
fAllowedHourFormats[0] = ALLOWED_HOUR_FORMAT_H;
fAllowedHourFormats[1] = ALLOWED_HOUR_FORMAT_UNKNOWN;
}
@ -1562,14 +1584,16 @@ DateTimePatternGenerator::adjustFieldTypes(const UnicodeString& pattern,
dtMatcher->skeleton.original.appendFieldTo(UDATPG_FRACTIONAL_SECOND_FIELD, field);
} else if (dtMatcher->skeleton.type[typeValue]!=0) {
// Here:
// - "reqField" is the field from the originally requested skeleton, with length
// "reqFieldLen".
// - "reqField" is the field from the originally requested skeleton after replacement
// of metacharacters 'j', 'C' and 'J', with length "reqFieldLen".
// - "field" is the field from the found pattern.
//
// The adjusted field should consist of characters from the originally requested
// skeleton, except in the case of UDATPG_HOUR_FIELD or UDATPG_MONTH_FIELD or
// skeleton, except in the case of UDATPG_MONTH_FIELD or
// UDATPG_WEEKDAY_FIELD or UDATPG_YEAR_FIELD, in which case it should consist
// of characters from the found pattern.
// of characters from the found pattern. In some cases of UDATPG_HOUR_FIELD,
// there is adjustment following the "defaultHourFormatChar". There is explanation
// how it is done below.
//
// The length of the adjusted field (adjFieldLen) should match that in the originally
// requested skeleton, except that in the following cases the length of the adjusted field
@ -1607,9 +1631,28 @@ DateTimePatternGenerator::adjustFieldTypes(const UnicodeString& pattern,
&& (typeValue!= UDATPG_YEAR_FIELD || reqFieldChar==CAP_Y))
? reqFieldChar
: field.charAt(0);
if (typeValue == UDATPG_HOUR_FIELD && (flags & kDTPGSkeletonUsesCapJ) != 0) {
c = fDefaultHourFormatChar;
if (typeValue == UDATPG_HOUR_FIELD) {
// The adjustment here is required to match spec (https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-hour).
// It is necessary to match the hour-cycle preferred by the Locale.
// Given that, we need to do the following adjustments:
// 1. When hour-cycle is h11 it should replace 'h' by 'K'.
// 2. When hour-cycle is h23 it should replace 'H' by 'k'.
// 3. When hour-cycle is h24 it should replace 'k' by 'H'.
// 4. When hour-cycle is h12 it should replace 'K' by 'h'.
if ((flags & kDTPGSkeletonUsesCapJ) != 0 || reqFieldChar == fDefaultHourFormatChar) {
c = fDefaultHourFormatChar;
} else if (reqFieldChar == LOW_H && fDefaultHourFormatChar == CAP_K) {
c = CAP_K;
} else if (reqFieldChar == CAP_H && fDefaultHourFormatChar == LOW_K) {
c = LOW_K;
} else if (reqFieldChar == LOW_K && fDefaultHourFormatChar == CAP_H) {
c = CAP_H;
} else if (reqFieldChar == CAP_K && fDefaultHourFormatChar == LOW_H) {
c = LOW_H;
}
}
field.remove();
for (int32_t j=adjFieldLen; j>0; --j) {
field += c;

View file

@ -58,6 +58,7 @@ void DateIntervalFormatTest::runIndexedTest( int32_t index, UBool exec, const ch
TESTCASE(9, testTicket12065);
TESTCASE(10, testFormattedDateInterval);
TESTCASE(11, testCreateInstanceForAllLocales);
TESTCASE(12, testTicket20707);
default: name = ""; break;
}
}
@ -1805,4 +1806,43 @@ void DateIntervalFormatTest::testCreateInstanceForAllLocales() {
}
}
void DateIntervalFormatTest::testTicket20707() {
IcuTestErrorCode status(*this, "testTicket20707");
const char16_t timeZone[] = u"UTC";
Locale locales[] = {"en-u-hc-h24", "en-u-hc-h23", "en-u-hc-h12", "en-u-hc-h11", "en", "en-u-hc-h25", "hi-IN-u-hc-h11"};
// Clomuns: hh, HH, kk, KK, jj, JJs, CC
UnicodeString expected[][7] = {
// Hour-cycle: k
{u"12 AM", u"24", u"24", u"12 AM", u"24", u"0 (hour: 24)", u"12 AM"},
// Hour-cycle: H
{u"12 AM", u"00", u"00", u"12 AM", u"00", u"0 (hour: 00)", u"12 AM"},
// Hour-cycle: h
{u"12 AM", u"00", u"00", u"12 AM", u"12 AM", u"0 (hour: 12)", u"12 AM"},
// Hour-cycle: K
{u"0 AM", u"00", u"00", u"0 AM", u"0 AM", u"0 (hour: 00)", u"0 AM"},
{u"12 AM", u"00", u"00", u"12 AM", u"12 AM", u"0 (hour: 12)", u"12 AM"},
{u"12 AM", u"00", u"00", u"12 AM", u"12 AM", u"0 (hour: 12)", u"12 AM"},
// Hour-cycle: K
{u"0 am", u"00", u"00", u"0 am", u"0 am", u"0 (\u0918\u0902\u091F\u093E: 00)", u"\u0930\u093E\u0924 0"}
};
int32_t i = 0;
for (Locale locale : locales) {
int32_t j = 0;
for (const UnicodeString skeleton : {u"hh", u"HH", u"kk", u"KK", u"jj", u"JJs", u"CC"}) {
LocalPointer<DateIntervalFormat> dtifmt(DateIntervalFormat::createInstance(skeleton, locale, status));
FieldPosition fposition;
UnicodeString result;
LocalPointer<Calendar> calendar(Calendar::createInstance(TimeZone::createTimeZone(timeZone), status));
calendar->setTime(UDate(1563235200000), status);
dtifmt->format(*calendar, *calendar, result, fposition, status);
assertEquals("Formatted result", expected[i][j++], result);
}
i++;
}
}
#endif /* #if !UCONFIG_NO_FORMATTING */

View file

@ -67,6 +67,8 @@ public:
void testFormattedDateInterval();
void testCreateInstanceForAllLocales();
void testTicket20707();
private:
/**
* Test formatting against expected result

View file

@ -368,6 +368,26 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera
String[] list = getAllowedHourFormatsLangCountry(language, country);
// We need to check if there is an hour cycle on locale
Character defaultCharFromLocale = null;
String hourCycle = uLocale.getKeywordValue("hours");
if (hourCycle != null) {
switch(hourCycle) {
case "h24":
defaultCharFromLocale = 'k';
break;
case "h23":
defaultCharFromLocale = 'H';
break;
case "h12":
defaultCharFromLocale = 'h';
break;
case "h11":
defaultCharFromLocale = 'K';
break;
}
}
// Check if the region has an alias
if (list == null) {
try {
@ -380,11 +400,11 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera
}
if (list != null) {
defaultHourFormatChar = list[0].charAt(0);
defaultHourFormatChar = defaultCharFromLocale != null ? defaultCharFromLocale : list[0].charAt(0);
allowedHourFormats = Arrays.copyOfRange(list, 1, list.length - 1);
} else {
allowedHourFormats = LAST_RESORT_ALLOWED_HOUR_FORMAT;
defaultHourFormatChar = allowedHourFormats[0].charAt(0);
defaultHourFormatChar = (defaultCharFromLocale != null) ? defaultCharFromLocale : allowedHourFormats[0].charAt(0);
}
}
@ -2117,8 +2137,10 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera
// - "field" is the field from the found pattern.
//
// The adjusted field should consist of characters from the originally requested
// skeleton, except in the case of HOUR or MONTH or WEEKDAY or YEAR, in which case it
// should consist of characters from the found pattern.
// skeleton, except in the case of MONTH or WEEKDAY or YEAR, in which case it
// should consist of characters from the found pattern. There is some adjustment
// in some cases of HOUR to "defaultHourFormatChar". There is explanation
// how it is done below.
//
// The length of the adjusted field (adjFieldLen) should match that in the originally
// requested skeleton, except that in the following cases the length of the adjusted field
@ -2163,8 +2185,25 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera
&& (type != YEAR || reqFieldChar=='Y'))
? reqFieldChar
: fieldBuilder.charAt(0);
if (type == HOUR && flags.contains(DTPGflags.SKELETON_USES_CAP_J)) {
c = defaultHourFormatChar;
if (type == HOUR) {
// The adjustment here is required to match spec (https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-hour).
// It is necessary to match the hour-cycle preferred by the Locale.
// Given that, we need to do the following adjustments:
// 1. When hour-cycle is h11 it should replace 'h' by 'K'.
// 2. When hour-cycle is h23 it should replace 'H' by 'k'.
// 3. When hour-cycle is h24 it should replace 'k' by 'H'.
// 4. When hour-cycle is h12 it should replace 'K' by 'h'.
if (flags.contains(DTPGflags.SKELETON_USES_CAP_J) || reqFieldChar == defaultHourFormatChar) {
c = defaultHourFormatChar;
} else if (reqFieldChar == 'h' && defaultHourFormatChar == 'K') {
c = 'K';
} else if (reqFieldChar == 'H' && defaultHourFormatChar == 'k') {
c = 'k';
} else if (reqFieldChar == 'k' && defaultHourFormatChar == 'H') {
c = 'H';
} else if (reqFieldChar == 'K' && defaultHourFormatChar == 'h') {
c = 'h';
}
}
fieldBuilder = new StringBuilder();
for (int i = adjFieldLen; i > 0; --i) fieldBuilder.append(c);

View file

@ -2050,4 +2050,48 @@ public class DateIntervalFormatTest extends TestFmwk {
}
}
}
@Test
public void testTicket20707() {
TimeZone tz = TimeZone.getTimeZone("UTC");
Locale locales[] = {
new Locale("en-u-hc-h24"),
new Locale("en-u-hc-h23"),
new Locale("en-u-hc-h12"),
new Locale("en-u-hc-h11"),
new Locale("en"),
new Locale("en-u-hc-h25"),
new Locale("hi-IN-u-hc-h11")
};
// Clomuns: hh, HH, kk, KK, jj, JJs, CC
String expected[][] = {
// Hour-cycle: k
{"12 AM", "24", "24", "12 AM", "24", "0 (hour: 24)", "12 AM"},
// Hour-cycle: H
{"12 AM", "00", "00", "12 AM", "00", "0 (hour: 00)", "12 AM"},
// Hour-cycle: h
{"12 AM", "00", "00", "12 AM", "12 AM", "0 (hour: 12)", "12 AM"},
// Hour-cycle: K
{"0 AM", "00", "00", "0 AM", "0 AM", "0 (hour: 00)", "0 AM"},
{"12 AM", "00", "00", "12 AM", "12 AM", "0 (hour: 12)", "12 AM"},
{"12 AM", "00", "00", "12 AM", "12 AM", "0 (hour: 12)", "12 AM"},
{"0 am", "00", "00", "0 am", "0 am", "0 (\u0918\u0902\u091F\u093E: 00)", "\u0930\u093E\u0924 0"}
};
int i = 0;
for (Locale locale : locales) {
int j = 0;
String skeletons[] = {"hh", "HH", "kk", "KK", "jj", "JJs", "CC"};
for (String skeleton : skeletons) {
DateIntervalFormat dateFormat = DateIntervalFormat.getInstance(skeleton, locale);
Calendar calendar = Calendar.getInstance(tz);
calendar.setTime(new Date(1563235200000L));
StringBuffer resultBuffer = dateFormat.format(calendar, calendar, new StringBuffer(""), new FieldPosition(0));
assertEquals("Formatted result for " + skeleton + " locale: " + locale.getDisplayName(), expected[i][j++], resultBuffer.toString());
}
i++;
}
}
}