diff --git a/icu4c/source/i18n/dtptngen.cpp b/icu4c/source/i18n/dtptngen.cpp index 09faf5bb6cc..54a2e02b892 100644 --- a/icu4c/source/i18n/dtptngen.cpp +++ b/icu4c/source/i18n/dtptngen.cpp @@ -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; diff --git a/icu4c/source/test/intltest/dtifmtts.cpp b/icu4c/source/test/intltest/dtifmtts.cpp index 7edb6a43a1c..2976558dcf5 100644 --- a/icu4c/source/test/intltest/dtifmtts.cpp +++ b/icu4c/source/test/intltest/dtifmtts.cpp @@ -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 dtifmt(DateIntervalFormat::createInstance(skeleton, locale, status)); + FieldPosition fposition; + UnicodeString result; + LocalPointer 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 */ diff --git a/icu4c/source/test/intltest/dtifmtts.h b/icu4c/source/test/intltest/dtifmtts.h index b05a57a8d08..592996e9b39 100644 --- a/icu4c/source/test/intltest/dtifmtts.h +++ b/icu4c/source/test/intltest/dtifmtts.h @@ -67,6 +67,8 @@ public: void testFormattedDateInterval(); void testCreateInstanceForAllLocales(); + void testTicket20707(); + private: /** * Test formatting against expected result diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DateTimePatternGenerator.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DateTimePatternGenerator.java index 96becb5c770..c3da8dda04f 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/DateTimePatternGenerator.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DateTimePatternGenerator.java @@ -368,6 +368,26 @@ public class DateTimePatternGenerator implements Freezable 0; --i) fieldBuilder.append(c); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateIntervalFormatTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateIntervalFormatTest.java index ff442f8b094..4ec5d5e8211 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateIntervalFormatTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateIntervalFormatTest.java @@ -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++; + } + } }