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;
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) {
}
}
} else { // Lookup failed, twice
- fDefaultHourFormatChar = CAP_H;
+ if (!fDefaultHourFormatChar) {
+ fDefaultHourFormatChar = CAP_H;
+ }
fAllowedHourFormats[0] = ALLOWED_HOUR_FORMAT_H;
fAllowedHourFormats[1] = ALLOWED_HOUR_FORMAT_UNKNOWN;
}
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
&& (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;
TESTCASE(9, testTicket12065);
TESTCASE(10, testFormattedDateInterval);
TESTCASE(11, testCreateInstanceForAllLocales);
+ TESTCASE(12, testTicket20707);
default: name = ""; break;
}
}
}
}
+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 */
void testFormattedDateInterval();
void testCreateInstanceForAllLocales();
+ void testTicket20707();
+
private:
/**
* Test formatting against expected result
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 {
}
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);
}
}
// - "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
&& (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);
}
}
}
+
+ @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++;
+ }
+ }
}