]> granicus.if.org Git - icu/commitdiff
ICU-20442 Adding support for hour-cycle on DateTimePatternGenerator
authorCaio Lima <ticaiolima@gmail.com>
Fri, 13 Dec 2019 03:14:28 +0000 (19:14 -0800)
committerShane F. Carr <shane@unicode.org>
Thu, 9 Jan 2020 15:45:56 +0000 (16:45 +0100)
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).

icu4c/source/i18n/dtptngen.cpp
icu4c/source/test/intltest/dtifmtts.cpp
icu4c/source/test/intltest/dtifmtts.h
icu4j/main/classes/core/src/com/ibm/icu/text/DateTimePatternGenerator.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateIntervalFormatTest.java

index 09faf5bb6ccd585f31ee3091b1509f8c889a8b66..54a2e02b892da3fbbadabee1c59a0fe5391ac3f2 100644 (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;
index 7edb6a43a1c2754b7b8e03c8fee66efd1ccb30dc..2976558dcf5368140ee72714ecdb59dcf306976a 100644 (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 */
index b05a57a8d0806b619a24451fcb53836f3cc47635..592996e9b390bd2a00451b1a580a4757c1a8d7d3 100644 (file)
@@ -67,6 +67,8 @@ public:
     void testFormattedDateInterval();
     void testCreateInstanceForAllLocales();
 
+    void testTicket20707();
+
 private:
     /**
      * Test formatting against expected result
index 96becb5c770c4c752c36f4dbf15646e0b5dbf6fd..c3da8dda04fa9cd9e20bdb3651fbffaaa92ecdb4 100644 (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);
index ff442f8b094725ab15395a83fbd670b644d613c6..4ec5d5e82119fd87cc9699c9b4c64481d14c681d 100644 (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++;
+        }
+    }
 }