Changeset 267108 in webkit


Ignore:
Timestamp:
Sep 15, 2020 3:56:32 PM (4 years ago)
Author:
ysuzuki@apple.com
Message:

[JSC] Apply Intl.DateTimeFormat hour-cycle correctly when timeStyle is used
https://bugs.webkit.org/show_bug.cgi?id=216521

Reviewed by Ross Kirsling.

JSTests:

JSTests/stress/intl-date-time-format-date-time-style-hour-cycle.js result is now matching against V8 and SpiderMonkey,
and this new behavior is more reasonable.

  • stress/intl-date-time-format-date-time-style-hour-cycle.js:

(shouldBe.o.format):
(shouldBe):

  • stress/intl-datetimeformat-hour-cycle.js: Added.

(shouldBe):
(throw.new.Error):

  • test262/expectations.yaml:

Source/JavaScriptCore:

When specifying timeStyle in Intl.DateTimeFormat, we need to check that the generated format also follows to the hourCycle / hour12 options
specified in the constructor. Because dayPeriod can be included automatically, just replacing symbols after generating a pattern can dump strange result.
For example, the generated one is something like "02:12:47 PM Coordinated Universal Time". And we adjust the pattern to make it "14:12:47 PM Coordinated Universal Time"
when hourCycle H23 / H24 is specified. But this looks strange since dayPeriod "PM" should not exist when using H23 / H24.

In this patch, we revise our hour-cycle handling in Intl.DateTimeFormat. We align our behavior to SpiderMonkey's one[1] rather than the spec's one: when hour12 is specified,
we will just use 'H' or 'h' skeleton and do not enforce hour-cycle after generating pattern in hour12 case. If hour12 is not specified, then we use 'h' or 'H' skeleton
symbols based on hour-cycle, and later we modify the pattern based on hour-cycle. If both are not offered, we use 'j' which allows ICU to pick preferable one.
This is slightly different behavior to the spec (hcDefault etc.) but the spec's behavior can cause a bit surprising result[2,3], and SpiderMonkey like behavior will be
integrated into the spec eventually[4].

[1]: https://github.com/tc39/ecma402/issues/402#issuecomment-623628320
[2]: https://github.com/tc39/ecma402/issues/402
[3]: https://bugs.chromium.org/p/chromium/issues/detail?id=1045791
[4]: https://github.com/tc39/ecma402/pull/436

  • runtime/IntlDateTimeFormat.cpp:

(JSC::IntlDateTimeFormat::setFormatsFromPattern):
(JSC::IntlDateTimeFormat::parseHourCycle):
(JSC::IntlDateTimeFormat::hourCycleFromPattern):
(JSC::IntlDateTimeFormat::replaceHourCycleInSkeleton):
(JSC::IntlDateTimeFormat::replaceHourCycleInPattern):
(JSC::IntlDateTimeFormat::initializeDateTimeFormat):
(JSC::IntlDateTimeFormat::hourCycleString):
(JSC::IntlDateTimeFormat::resolvedOptions const):
(JSC::IntlDateTimeFormat::createDateIntervalFormatIfNecessary):

  • runtime/IntlDateTimeFormat.h:
Location:
trunk
Files:
1 added
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/JSTests/ChangeLog

    r267102 r267108  
     12020-09-15  Yusuke Suzuki  <ysuzuki@apple.com>
     2
     3        [JSC] Apply Intl.DateTimeFormat hour-cycle correctly when timeStyle is used
     4        https://bugs.webkit.org/show_bug.cgi?id=216521
     5
     6        Reviewed by Ross Kirsling.
     7
     8        JSTests/stress/intl-date-time-format-date-time-style-hour-cycle.js result is now matching against V8 and SpiderMonkey,
     9        and this new behavior is more reasonable.
     10
     11        * stress/intl-date-time-format-date-time-style-hour-cycle.js:
     12        (shouldBe.o.format):
     13        (shouldBe):
     14        * stress/intl-datetimeformat-hour-cycle.js: Added.
     15        (shouldBe):
     16        (throw.new.Error):
     17        * test262/expectations.yaml:
     18
    1192020-09-14  Yusuke Suzuki  <ysuzuki@apple.com>
    220
  • trunk/JSTests/stress/intl-date-time-format-date-time-style-hour-cycle.js

    r266035 r267108  
    66let now1 = 1592870440000;
    77let now2 = 1592827240000;
     8
     9{
     10    let o = new Intl.DateTimeFormat("en" , {
     11        timeStyle: "short",
     12        timeZone: "UTC",
     13    });
     14    shouldBe(o.format(now1), `12:00 AM`);
     15}
     16
    817{
    918    let o = new Intl.DateTimeFormat("en" , {
     
    1221        hourCycle: "h23",
    1322    });
    14     shouldBe(o.format(now1), `0:00 AM`);
     23    shouldBe(o.format(now1), `00:00`);
    1524}
    1625
     
    2130        hourCycle: "h24",
    2231    });
    23     shouldBe(o.format(now1), `24:00 AM`);
     32    shouldBe(o.format(now1), `24:00`);
    2433}
    2534
     
    4655        timeStyle: "short",
    4756        timeZone: "UTC",
    48         hourCycle: "h23",
     57        hour12: true,
     58    });
     59    shouldBe(o.format(now1), `12:00 AM`);
     60}
     61
     62{
     63    let o = new Intl.DateTimeFormat("en" , {
     64        timeStyle: "short",
     65        timeZone: "UTC",
     66        hour12: false,
     67    });
     68    shouldBe(o.format(now1), `00:00`);
     69}
     70
     71{
     72    let o = new Intl.DateTimeFormat("en" , {
     73        timeStyle: "short",
     74        timeZone: "UTC",
    4975    });
    5076    shouldBe(o.format(now2), `12:00 PM`);
     
    5581        timeStyle: "short",
    5682        timeZone: "UTC",
     83        hourCycle: "h23",
     84    });
     85    shouldBe(o.format(now2), `12:00`);
     86}
     87
     88{
     89    let o = new Intl.DateTimeFormat("en" , {
     90        timeStyle: "short",
     91        timeZone: "UTC",
    5792        hourCycle: "h24",
    5893    });
    59     shouldBe(o.format(now2), `12:00 PM`);
     94    shouldBe(o.format(now2), `12:00`);
    6095}
    6196
     
    77112    shouldBe(o.format(now2), `12:00 PM`);
    78113}
     114
     115{
     116    let o = new Intl.DateTimeFormat("en" , {
     117        timeStyle: "short",
     118        timeZone: "UTC",
     119        hour12: true,
     120    });
     121    shouldBe(o.format(now2), `12:00 PM`);
     122}
     123
     124{
     125    let o = new Intl.DateTimeFormat("en" , {
     126        timeStyle: "short",
     127        timeZone: "UTC",
     128        hour12: false,
     129    });
     130    shouldBe(o.format(now2), `12:00`);
     131}
  • trunk/JSTests/test262/expectations.yaml

    r267040 r267108  
    14091409  strict mode: 'TypeError: TypedArray.of requires its this argument to subclass a TypedArray constructor (Testing with Float64Array.)'
    14101410test/intl402/DateTimeFormat/prototype/format/timedatestyle-en.js:
    1411   default: 'Test262Error: Result for full with {} Expected SameValue(«14:12:47 PM Coordinated Universal Time», «14:12:47 Coordinated Universal Time») to be true'
    1412   strict mode: 'Test262Error: Result for full with {} Expected SameValue(«14:12:47 PM Coordinated Universal Time», «14:12:47 Coordinated Universal Time») to be true'
     1411  default: 'Test262Error: Result for date=medium and time=full Expected SameValue(«May 1, 1886 at 2:12:47 PM Coordinated Universal Time», «May 1, 1886, 2:12:47 PM Coordinated Universal Time») to be true'
     1412  strict mode: 'Test262Error: Result for date=medium and time=full Expected SameValue(«May 1, 1886 at 2:12:47 PM Coordinated Universal Time», «May 1, 1886, 2:12:47 PM Coordinated Universal Time») to be true'
    14131413test/intl402/DateTimeFormat/prototype/formatRange/en-US.js:
    14141414  default: 'Test262Error: Expected SameValue(«1/3/2019 – 1/5/2019», «1/3/2019 – 1/5/2019») to be true'
  • trunk/Source/JavaScriptCore/ChangeLog

    r267102 r267108  
     12020-09-15  Yusuke Suzuki  <ysuzuki@apple.com>
     2
     3        [JSC] Apply Intl.DateTimeFormat hour-cycle correctly when timeStyle is used
     4        https://bugs.webkit.org/show_bug.cgi?id=216521
     5
     6        Reviewed by Ross Kirsling.
     7
     8        When specifying timeStyle in Intl.DateTimeFormat, we need to check that the generated format also follows to the hourCycle / hour12 options
     9        specified in the constructor. Because dayPeriod can be included automatically, just replacing symbols after generating a pattern can dump strange result.
     10        For example, the generated one is something like "02:12:47 PM Coordinated Universal Time". And we adjust the pattern to make it "14:12:47 PM Coordinated Universal Time"
     11        when hourCycle H23 / H24 is specified. But this looks strange since dayPeriod "PM" should not exist when using H23 / H24.
     12
     13        In this patch, we revise our hour-cycle handling in Intl.DateTimeFormat. We align our behavior to SpiderMonkey's one[1] rather than the spec's one: when hour12 is specified,
     14        we will just use 'H' or 'h' skeleton and do not enforce hour-cycle after generating pattern in hour12 case. If hour12 is not specified, then we use 'h' or 'H' skeleton
     15        symbols based on hour-cycle, and later we modify the pattern based on hour-cycle. If both are not offered, we use 'j' which allows ICU to pick preferable one.
     16        This is slightly different behavior to the spec (hcDefault etc.) but the spec's behavior can cause a bit surprising result[2,3], and SpiderMonkey like behavior will be
     17        integrated into the spec eventually[4].
     18
     19        [1]: https://github.com/tc39/ecma402/issues/402#issuecomment-623628320
     20        [2]: https://github.com/tc39/ecma402/issues/402
     21        [3]: https://bugs.chromium.org/p/chromium/issues/detail?id=1045791
     22        [4]: https://github.com/tc39/ecma402/pull/436
     23
     24        * runtime/IntlDateTimeFormat.cpp:
     25        (JSC::IntlDateTimeFormat::setFormatsFromPattern):
     26        (JSC::IntlDateTimeFormat::parseHourCycle):
     27        (JSC::IntlDateTimeFormat::hourCycleFromPattern):
     28        (JSC::IntlDateTimeFormat::replaceHourCycleInSkeleton):
     29        (JSC::IntlDateTimeFormat::replaceHourCycleInPattern):
     30        (JSC::IntlDateTimeFormat::initializeDateTimeFormat):
     31        (JSC::IntlDateTimeFormat::hourCycleString):
     32        (JSC::IntlDateTimeFormat::resolvedOptions const):
     33        (JSC::IntlDateTimeFormat::createDateIntervalFormatIfNecessary):
     34        * runtime/IntlDateTimeFormat.h:
     35
    1362020-09-14  Yusuke Suzuki  <ysuzuki@apple.com>
    237
  • trunk/Source/JavaScriptCore/runtime/IntlDateTimeFormat.cpp

    r266323 r267108  
    330330        }
    331331
    332         // If hourCycle was null, this sets it to the locale default.
    333         if (m_hourCycle.isNull()) {
    334             if (currentCharacter == 'h')
    335                 m_hourCycle = "h12"_s;
    336             else if (currentCharacter == 'H')
    337                 m_hourCycle = "h23"_s;
    338             else if (currentCharacter == 'k')
    339                 m_hourCycle = "h24"_s;
    340             else if (currentCharacter == 'K')
    341                 m_hourCycle = "h11"_s;
    342         }
    343 
    344332        switch (currentCharacter) {
    345333        case 'G':
     
    399387        case 'H':
    400388        case 'k':
    401         case 'K':
     389        case 'K': {
     390            // Populate hourCycle from actually generated patterns. It is possible that locale or option is specifying hourCycle explicitly,
     391            // but the generated pattern does not include related part since the pattern does not include hours.
     392            // This is tested in test262/test/intl402/DateTimeFormat/prototype/resolvedOptions/hourCycle-dateStyle.js and our stress tests.
     393            // Example:
     394            //     new Intl.DateTimeFormat(`de-u-hc-h11`, {
     395            //         dateStyle: "full"
     396            //     }).resolvedOptions().hourCycle === undefined
     397            m_hourCycle = hourCycleFromSymbol(currentCharacter);
    402398            if (count == 1)
    403399                m_hour = Hour::Numeric;
     
    405401                m_hour = Hour::TwoDigit;
    406402            break;
     403        }
    407404        case 'm':
    408405            if (count == 1)
     
    432429}
    433430
     431IntlDateTimeFormat::HourCycle IntlDateTimeFormat::parseHourCycle(const String& hourCycle)
     432{
     433    if (hourCycle == "h11"_s)
     434        return HourCycle::H11;
     435    if (hourCycle == "h12"_s)
     436        return HourCycle::H12;
     437    if (hourCycle == "h23"_s)
     438        return HourCycle::H23;
     439    if (hourCycle == "h24"_s)
     440        return HourCycle::H24;
     441    return HourCycle::None;
     442}
     443
     444inline IntlDateTimeFormat::HourCycle IntlDateTimeFormat::hourCycleFromSymbol(UChar symbol)
     445{
     446    switch (symbol) {
     447    case 'K':
     448        return HourCycle::H11;
     449    case 'h':
     450        return HourCycle::H12;
     451    case 'H':
     452        return HourCycle::H23;
     453    case 'k':
     454        return HourCycle::H24;
     455    }
     456    return HourCycle::None;
     457}
     458
     459inline IntlDateTimeFormat::HourCycle IntlDateTimeFormat::hourCycleFromPattern(const Vector<UChar, 32>& pattern)
     460{
     461    for (auto character : pattern) {
     462        switch (character) {
     463        case 'K':
     464        case 'h':
     465        case 'H':
     466        case 'k':
     467            return hourCycleFromSymbol(character);
     468        }
     469    }
     470    return HourCycle::None;
     471}
     472
     473inline void IntlDateTimeFormat::replaceHourCycleInSkeleton(Vector<UChar, 32>& skeleton, bool isHour12)
     474{
     475    UChar skeletonCharacter = 'H';
     476    if (isHour12)
     477        skeletonCharacter = 'h';
     478    for (auto& character : skeleton) {
     479        switch (character) {
     480        case 'h':
     481        case 'H':
     482        case 'j':
     483            character = skeletonCharacter;
     484            break;
     485        }
     486    }
     487}
     488
     489inline void IntlDateTimeFormat::replaceHourCycleInPattern(Vector<UChar, 32>& pattern, HourCycle hourCycle)
     490{
     491    UChar hourFromHourCycle = 'H';
     492    switch (hourCycle) {
     493    case HourCycle::H11:
     494        hourFromHourCycle = 'K';
     495        break;
     496    case HourCycle::H12:
     497        hourFromHourCycle = 'h';
     498        break;
     499    case HourCycle::H23:
     500        hourFromHourCycle = 'H';
     501        break;
     502    case HourCycle::H24:
     503        hourFromHourCycle = 'k';
     504        break;
     505    case HourCycle::None:
     506        return;
     507    }
     508
     509    for (auto& character : pattern) {
     510        switch (character) {
     511        case 'K':
     512        case 'h':
     513        case 'H':
     514        case 'k':
     515            character = hourFromHourCycle;
     516            break;
     517        }
     518    }
     519}
     520
    434521// https://tc39.github.io/ecma402/#sec-initializedatetimeformat
    435522void IntlDateTimeFormat::initializeDateTimeFormat(JSGlobalObject* globalObject, JSValue locales, JSValue originalOptions)
     
    471558    TriState hour12 = intlBooleanOption(globalObject, options, vm.propertyNames->hour12);
    472559    RETURN_IF_EXCEPTION(scope, void());
    473     bool isHour12Undefined = (hour12 == TriState::Indeterminate);
    474 
    475     String hourCycle = intlStringOption(globalObject, options, vm.propertyNames->hourCycle, { "h11", "h12", "h23", "h24" }, "hourCycle must be \"h11\", \"h12\", \"h23\", or \"h24\"", nullptr);
    476     RETURN_IF_EXCEPTION(scope, void());
    477     if (isHour12Undefined) {
    478         // Set hour12 here to simplify hour logic later.
    479         hour12 = triState(hourCycle == "h11" || hourCycle == "h12");
    480         if (!hourCycle.isNull())
    481             localeOptions[static_cast<unsigned>(RelevantExtensionKey::Hc)] = hourCycle;
    482     } else
     560
     561    HourCycle hourCycle = intlOption<HourCycle>(globalObject, options, vm.propertyNames->hourCycle, { { "h11"_s, HourCycle::H11 }, { "h12"_s, HourCycle::H12 }, { "h23"_s, HourCycle::H23 }, { "h24"_s, HourCycle::H24 } }, "hourCycle must be \"h11\", \"h12\", \"h23\", or \"h24\""_s, HourCycle::None);
     562    RETURN_IF_EXCEPTION(scope, void());
     563    if (hour12 == TriState::Indeterminate) {
     564        if (hourCycle != HourCycle::None)
     565            localeOptions[static_cast<unsigned>(RelevantExtensionKey::Hc)] = String(hourCycleString(hourCycle));
     566    } else {
     567        // If there is hour12, hourCycle is ignored.
     568        // We are setting null String explicitly here (localeOptions' entries are Optional<String>). This leads us to use HourCycle::None later.
    483569        localeOptions[static_cast<unsigned>(RelevantExtensionKey::Hc)] = String();
     570    }
    484571
    485572    const HashSet<String>& availableLocales = intlDateTimeFormatAvailableLocales();
     
    500587        m_calendar = "ethiopic-amete-alem"_s;
    501588
    502     m_hourCycle = resolved.extensions[static_cast<unsigned>(RelevantExtensionKey::Hc)];
     589    hourCycle = parseHourCycle(resolved.extensions[static_cast<unsigned>(RelevantExtensionKey::Hc)]);
    503590    m_numberingSystem = resolved.extensions[static_cast<unsigned>(RelevantExtensionKey::Nu)];
    504591    m_dataLocale = resolved.dataLocale;
     
    610697    Hour hour = intlOption<Hour>(globalObject, options, vm.propertyNames->hour, { { "2-digit"_s, Hour::TwoDigit }, { "numeric"_s, Hour::Numeric } }, "hour must be \"2-digit\" or \"numeric\""_s, Hour::None);
    611698    RETURN_IF_EXCEPTION(scope, void());
    612     switch (hour) {
    613     case Hour::TwoDigit:
    614         if (isHour12Undefined && m_hourCycle.isNull())
    615             skeletonBuilder.appendLiteral("jj");
    616         else if (hour12 == TriState::True)
    617             skeletonBuilder.appendLiteral("hh");
    618         else
    619             skeletonBuilder.appendLiteral("HH");
    620         break;
    621     case Hour::Numeric:
    622         if (isHour12Undefined && m_hourCycle.isNull())
    623             skeletonBuilder.append('j');
    624         else if (hour12 == TriState::True)
    625             skeletonBuilder.append('h');
    626         else
    627             skeletonBuilder.append('H');
    628         break;
    629     case Hour::None:
    630         break;
     699    {
     700        // Specifically, this hour-cycle / hour12 behavior is slightly different from the spec.
     701        // But the spec behavior is known to cause surprising behaviors, and the spec change is ongoing.
     702        // We implement SpiderMonkey's behavior.
     703        //
     704        //     > No option present: "j"
     705        //     > hour12 = true: "h"
     706        //     > hour12 = false: "H"
     707        //     > hourCycle = h11: "h", plus modifying the resolved pattern to use the hour symbol "K".
     708        //     > hourCycle = h12: "h", plus modifying the resolved pattern to use the hour symbol "h".
     709        //     > hourCycle = h23: "H", plus modifying the resolved pattern to use the hour symbol "H".
     710        //     > hourCycle = h24: "H", plus modifying the resolved pattern to use the hour symbol "k".
     711        //
     712        UChar skeletonCharacter = 'j';
     713        if (hour12 == TriState::Indeterminate) {
     714            switch (hourCycle) {
     715            case HourCycle::None:
     716                break;
     717            case HourCycle::H11:
     718            case HourCycle::H12:
     719                skeletonCharacter = 'h';
     720                break;
     721            case HourCycle::H23:
     722            case HourCycle::H24:
     723                skeletonCharacter = 'H';
     724                break;
     725            }
     726        } else {
     727            if (hour12 == TriState::True)
     728                skeletonCharacter = 'h';
     729            else
     730                skeletonCharacter = 'H';
     731        }
     732
     733        switch (hour) {
     734        case Hour::TwoDigit:
     735            skeletonBuilder.append(skeletonCharacter);
     736            skeletonBuilder.append(skeletonCharacter);
     737            break;
     738        case Hour::Numeric:
     739            skeletonBuilder.append(skeletonCharacter);
     740            break;
     741        case Hour::None:
     742            break;
     743        }
    631744    }
    632745
     
    702815    RETURN_IF_EXCEPTION(scope, void());
    703816
    704     bool canIncludeHour = true;
     817
     818    auto patternFromSkeleton = [&](const UChar* skeleton, unsigned skeletonSize, Vector<UChar, 32>& patternBuffer, UErrorCode& status) {
     819        // Always use ICU date format generator, rather than our own pattern list and matcher.
     820        auto generator = std::unique_ptr<UDateTimePatternGenerator, ICUDeleter<udatpg_close>>(udatpg_open(dataLocaleWithExtensions.data(), &status));
     821        if (U_FAILURE(status))
     822            return;
     823
     824        status = callBufferProducingFunction(udatpg_getBestPatternWithOptions, generator.get(), skeleton, skeletonSize, UDATPG_MATCH_HOUR_FIELD_LENGTH, patternBuffer);
     825        if (U_FAILURE(status))
     826            return;
     827    };
     828
    705829    Vector<UChar, 32> patternBuffer;
    706830    if (m_dateStyle != DateTimeStyle::None || m_timeStyle != DateTimeStyle::None) {
     
    713837            throwTypeError(globalObject, scope, "dateStyle and timeStyle may not be used with other DateTimeFormat options"_s);
    714838            return;
    715         }
    716 
    717         // If timeStyle is not specified, m_hourCycle does not matter. Let's skip enforcing step.
    718         if (m_timeStyle == DateTimeStyle::None) {
    719             canIncludeHour = false;
    720             m_hourCycle = String();
    721839        }
    722840
     
    739857        // We cannot use this UDateFormat directly yet because we need to enforce specified hourCycle.
    740858        // First, we create UDateFormat via dateStyle and timeStyle. And then convert it to pattern string.
    741         // After updating this pattern string with m_hourCycle, we create a final UDateFormat with the updated pattern string.
     859        // After updating this pattern string with hourCycle, we create a final UDateFormat with the updated pattern string.
    742860        UErrorCode status = U_ZERO_ERROR;
    743861        StringView timeZoneView(m_timeZone);
     
    753871            return;
    754872        }
     873
     874        // It is possible that timeStyle includes dayPeriod, which is sensitive to hour-cycle.
     875        // If dayPeriod is included, just replacing hour based on hourCycle / hour12 produces strange results.
     876        // Let's consider about the example. The formatted result looks like "02:12:47 PM Coordinated Universal Time"
     877        // If we simply replace 02 to 14, this becomes "14:12:47 PM Coordinated Universal Time", this looks strange since "PM" is unnecessary!
     878        //
     879        // If the generated pattern's hour12 does not match against the option's one, we retrieve skeleton from the pattern, enforcing hour-cycle,
     880        // and re-generating the best pattern from the modified skeleton. ICU will look into the generated skeleton, and pick the best format for the request.
     881        // We do not care about h11 vs. h12 and h23 vs. h24 difference here since this will be later adjusted by replaceHourCycleInPattern.
     882        //
     883        // test262/test/intl402/DateTimeFormat/prototype/format/timedatestyle-en.js includes the test for this behavior.
     884        if (m_timeStyle != DateTimeStyle::None && (hourCycle != HourCycle::None || hour12 != TriState::Indeterminate)) {
     885            auto isHour12 = [](HourCycle hourCycle) {
     886                return hourCycle == HourCycle::H11 || hourCycle == HourCycle::H12;
     887            };
     888            bool specifiedHour12 = false;
     889            // If hour12 is specified, we prefer it and ignore hourCycle.
     890            if (hour12 != TriState::Indeterminate)
     891                specifiedHour12 = hour12 == TriState::True;
     892            else
     893                specifiedHour12 = isHour12(hourCycle);
     894            HourCycle extractedHourCycle = hourCycleFromPattern(patternBuffer);
     895            if (extractedHourCycle != HourCycle::None && isHour12(extractedHourCycle) != specifiedHour12) {
     896                Vector<UChar, 32> skeleton;
     897                auto status = callBufferProducingFunction(udatpg_getSkeleton, nullptr, patternBuffer.data(), patternBuffer.size(), skeleton);
     898                if (U_FAILURE(status)) {
     899                    throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
     900                    return;
     901                }
     902                replaceHourCycleInSkeleton(skeleton, specifiedHour12);
     903                dataLogLnIf(IntlDateTimeFormatInternal::verbose, "replaced:(", StringView(skeleton.data(), skeleton.size()), ")");
     904
     905                patternBuffer.clear();
     906                patternFromSkeleton(skeleton.data(), skeleton.size(), patternBuffer, status);
     907                if (U_FAILURE(status)) {
     908                    throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
     909                    return;
     910                }
     911            }
     912        }
    755913    } else {
    756         // If hour is not specified, m_hourCycle does not matter. Let's skip enforcing step.
    757         if (hour == Hour::None) {
    758             canIncludeHour = false;
    759             m_hourCycle = String();
    760         }
    761 
    762         // Always use ICU date format generator, rather than our own pattern list and matcher.
    763914        UErrorCode status = U_ZERO_ERROR;
    764         auto* generator = udatpg_open(dataLocaleWithExtensions.data(), &status);
     915        String skeleton = skeletonBuilder.toString();
     916        patternFromSkeleton(StringView(skeleton).upconvertedCharacters().get(), skeleton.length(), patternBuffer, status);
    765917        if (U_FAILURE(status)) {
    766918            throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
    767919            return;
    768920        }
    769 
    770         String skeleton = skeletonBuilder.toString();
    771         StringView skeletonView(skeleton);
    772         status = callBufferProducingFunction(udatpg_getBestPatternWithOptions, generator, skeletonView.upconvertedCharacters().get(), skeletonView.length(), UDATPG_MATCH_HOUR_FIELD_LENGTH, patternBuffer);
    773         udatpg_close(generator);
    774         if (U_FAILURE(status)) {
    775             throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
    776             return;
    777         }
    778     }
    779 
    780     // Enforce our hourCycle, replacing hour characters in pattern. ICU can pick format which does not agree with specified hourCycle.
    781     // This step modifies generated pattern to use specified hourCycle, and it is specified in 1.1.1 InitializeDateTimeFormat step 33.
    782     // FIXME: We should cache `hourCycleDefault` value obtained by generating "jjmm" pattern from UDateTimePatternGenerator.
    783     // https://bugs.webkit.org/show_bug.cgi?id=213459
    784     if (canIncludeHour) {
    785         bool hasHour = false;
    786         if (!m_hourCycle.isNull() || !isHour12Undefined) {
    787             UChar hourFromHourCycle = 'H'; // H23
    788             if (m_hourCycle == "h11")
    789                 hourFromHourCycle = 'K';
    790             else if (m_hourCycle == "h12")
    791                 hourFromHourCycle = 'h';
    792             else if (m_hourCycle == "h24")
    793                 hourFromHourCycle = 'k';
    794 
    795             bool isEscaped = false;
    796             for (auto& c : patternBuffer) {
    797                 if (c == '\'')
    798                     isEscaped = !isEscaped;
    799                 else if (!isEscaped && (c == 'h' || c == 'H' || c == 'k' || c == 'K')) {
    800                     switch (c) {
    801                     case 'K':
    802                     case 'H': {
    803                         if (isHour12Undefined)
    804                             c = hourFromHourCycle;
    805                         else if (hour12 == TriState::True)
    806                             c = 'K';
    807                         else
    808                             c = 'H';
    809                         break;
    810                     }
    811                     case 'h':
    812                     case 'k': {
    813                         if (isHour12Undefined)
    814                             c = hourFromHourCycle;
    815                         else if (hour12 == TriState::True)
    816                             c = 'h';
    817                         else
    818                             c = 'k';
    819                         break;
    820                     }
    821                     default:
    822                         ASSERT_NOT_REACHED();
    823                     }
    824                     hasHour = true;
    825                 }
    826             }
    827         }
    828         if (!hasHour)
    829             m_hourCycle = String();
    830     }
     921    }
     922
     923    // After generating pattern from skeleton, we need to change h11 vs. h12 and h23 vs. h24 if hourCycle is specified.
     924    if (hourCycle != HourCycle::None)
     925        replaceHourCycleInPattern(patternBuffer, hourCycle);
    831926
    832927    StringView pattern(patternBuffer.data(), patternBuffer.size());
     
    847942    UCalendar* cal = const_cast<UCalendar*>(udat_getCalendar(m_dateFormat.get()));
    848943    ucal_setGregorianChange(cal, minECMAScriptTime, &status);
     944}
     945
     946ASCIILiteral IntlDateTimeFormat::hourCycleString(HourCycle hourCycle)
     947{
     948    switch (hourCycle) {
     949    case HourCycle::H11:
     950        return "h11"_s;
     951    case HourCycle::H12:
     952        return "h12"_s;
     953    case HourCycle::H23:
     954        return "h23"_s;
     955    case HourCycle::H24:
     956        return "h24"_s;
     957    case HourCycle::None:
     958        ASSERT_NOT_REACHED();
     959        return ASCIILiteral::null();
     960    }
     961    ASSERT_NOT_REACHED();
     962    return ASCIILiteral::null();
    849963}
    850964
     
    10411155    options->putDirect(vm, vm.propertyNames->timeZone, jsNontrivialString(vm, m_timeZone));
    10421156
    1043     if (!m_hourCycle.isNull()) {
    1044         options->putDirect(vm, vm.propertyNames->hourCycle, jsNontrivialString(vm, m_hourCycle));
    1045         options->putDirect(vm, vm.propertyNames->hour12, jsBoolean(m_hourCycle == "h11" || m_hourCycle == "h12"));
     1157    if (m_hourCycle != HourCycle::None) {
     1158        options->putDirect(vm, vm.propertyNames->hourCycle, jsNontrivialString(vm, hourCycleString(m_hourCycle)));
     1159        options->putDirect(vm, vm.propertyNames->hour12, jsBoolean(m_hourCycle == HourCycle::H11 || m_hourCycle == HourCycle::H12));
    10461160    }
    10471161
     
    12681382    StringBuilder localeBuilder;
    12691383    localeBuilder.append(m_dataLocale, "-u-ca-", m_calendar, "-nu-", m_numberingSystem);
    1270     if (!m_hourCycle.isNull())
    1271         localeBuilder.append("-hc-", m_hourCycle);
     1384    if (m_hourCycle != HourCycle::None)
     1385        localeBuilder.append("-hc-", hourCycleString(m_hourCycle));
    12721386    CString dataLocaleWithExtensions = localeBuilder.toString().utf8();
    12731387
  • trunk/Source/JavaScriptCore/runtime/IntlDateTimeFormat.h

    r266655 r267108  
    7979    UDateIntervalFormat* createDateIntervalFormatIfNecessary(JSGlobalObject*);
    8080
     81    enum class HourCycle : uint8_t { None, H11, H12, H23, H24 };
    8182    enum class Weekday : uint8_t { None, Narrow, Short, Long };
    8283    enum class Era : uint8_t { None, Narrow, Short, Long };
     
    9293
    9394    void setFormatsFromPattern(const StringView&);
     95    static ASCIILiteral hourCycleString(HourCycle);
    9496    static ASCIILiteral weekdayString(Weekday);
    9597    static ASCIILiteral eraString(Era);
     
    103105    static ASCIILiteral timeZoneNameString(TimeZoneName);
    104106    static ASCIILiteral formatStyleString(DateTimeStyle);
     107
     108    static HourCycle hourCycleFromSymbol(UChar);
     109    static HourCycle parseHourCycle(const String&);
     110    static HourCycle hourCycleFromPattern(const Vector<UChar, 32>&);
     111    static void replaceHourCycleInSkeleton(Vector<UChar, 32>&, bool hour12);
     112    static void replaceHourCycleInPattern(Vector<UChar, 32>&, HourCycle);
    105113
    106114    using UDateFormatDeleter = ICUDeleter<udat_close>;
     
    116124    String m_numberingSystem;
    117125    String m_timeZone;
    118     String m_hourCycle;
     126    HourCycle m_hourCycle { HourCycle::None };
    119127    Weekday m_weekday { Weekday::None };
    120128    Era m_era { Era::None };
Note: See TracChangeset for help on using the changeset viewer.