Changeset 269706 in webkit


Ignore:
Timestamp:
Nov 11, 2020 4:25:17 PM (21 months ago)
Author:
ysuzuki@apple.com
Message:

[JSC] Implement Intl.DateTimeFormat.formatRangeToParts
https://bugs.webkit.org/show_bug.cgi?id=213822
<rdar://problem/69328711>

Reviewed by Ross Kirsling.

JSTests:

  • stress/intl-datetimeformat-formatrange-relevant-extensions.js:
  • stress/intl-datetimeformat-formatrange-should-not-handle-gregorian-change-date.js: Added.

(shouldBe):
(vm.icuHeaderVersion):

  • stress/intl-datetimeformat-formatrange.js:

(shouldThrow):
(test):

  • stress/intl-datetimeformat-formatrangetoparts-relevant-extensions-ja.js: Added.

(shouldBe):
(compareParts):
(shouldThrow):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt1.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt2.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt3.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt4.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt5.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.vm.icuVersion):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt6.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt7.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt8.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt9.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt10.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt11.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt12.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt13.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt14.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt15.formatRangeToParts):
(Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt16.formatRangeToParts):

  • stress/intl-datetimeformat-formatrangetoparts-relevant-extensions.js: Added.

(shouldBe):
(compareParts):
(shouldThrow):

  • stress/intl-datetimeformat-formatrangetoparts-should-not-handle-gregorian-change-date.js: Added.

(shouldBe):
(compareParts):
(vm.icuHeaderVersion):

  • stress/intl-datetimeformat-formatrangetoparts.js: Added.

(shouldBe):
(compareParts):
(shouldThrow):

  • test262/config.yaml:
  • test262/expectations.yaml: This failure is because of CLDR data inside ICU. ICU 67 will fix them.

Source/JavaScriptCore:

This patch implements Intl.DateTimeFormat.formatRangeToParts. It is already stage-4 (included in the spec).
The inputs are date interval, and this function generates array of parts of formatted string of date interval.
Currently, required ICU APIs are draft status. So, for now, we track ABI changes, and use APIs with careful version checks.

However, currently, OpenSource macOS WebKit is built with specific ICU header (ICU 62 headers). So for now, we disable it
in OpenSource macOS WebKit build. But we enable it for Apple Internal SDK WebKit build. We can enable it if we include
multiple ICU header sets and select appropriate one against the linked ICU version. In the other platforms, they are using
corresponding ICU headers so that we can just enable it.

There are two interesting implementation topics.

  1. From ICU 67, the signature of udtitvfmt_formatToResult is changed. We need to switch the implementation with fine grained ICU version checks.
  2. udtitvfmt_formatToResult does not have an ability to configure gregorian calendar change date: before that date, the calendar is julian. In ECMAScript spec, we need to ignore this gregorian calendar change date, and we should handle all gregorian calendar dates as is even if the dates are older than gregorian calendar change date. However, since udtitvfmt_formatToResult does not offer the above ability, ICU automatically switches the calendar between gregorian and julian. To fix this issue, ICU 67 introduced udtitvfmt_formatCalendarToResult, which can take an explicit calendar for each input date so that we configure gregorian calendar change date. But this only exists after ICU 67. In the implementations using ICU 64-66, we just use udtitvfmt_formatToResult.
  • JavaScriptCore.xcodeproj/project.pbxproj:
  • Sources.txt:
  • runtime/IntlDateTimeFormat.cpp:

(JSC::UDateIntervalFormatDeleter::operator()):
(JSC::IntlDateTimeFormat::formatToParts const):
(JSC::definitelyAfterGregorianCalendarChangeDate):
(JSC::formattedValueFromDateRange):
(JSC::IntlDateTimeFormat::formatRange):
(JSC::IntlDateTimeFormat::formatRangeToParts):

  • runtime/IntlDateTimeFormat.h:
  • runtime/IntlDateTimeFormatPrototype.cpp:

(JSC::IntlDateTimeFormatPrototype::create):
(JSC::IntlDateTimeFormatPrototype::finishCreation):
(JSC::JSC_DEFINE_HOST_FUNCTION):

  • runtime/IntlDateTimeFormatPrototype.h:
  • runtime/OptionsList.h:
Location:
trunk
Files:
5 added
13 edited

Legend:

Unmodified
Added
Removed
  • trunk/JSTests/ChangeLog

    r269670 r269706  
     12020-11-11  Yusuke Suzuki  <ysuzuki@apple.com>
     2
     3        [JSC] Implement Intl.DateTimeFormat.formatRangeToParts
     4        https://bugs.webkit.org/show_bug.cgi?id=213822
     5        <rdar://problem/69328711>
     6
     7        Reviewed by Ross Kirsling.
     8
     9        * stress/intl-datetimeformat-formatrange-relevant-extensions.js:
     10        * stress/intl-datetimeformat-formatrange-should-not-handle-gregorian-change-date.js: Added.
     11        (shouldBe):
     12        (vm.icuHeaderVersion):
     13        * stress/intl-datetimeformat-formatrange.js:
     14        (shouldThrow):
     15        (test):
     16        * stress/intl-datetimeformat-formatrangetoparts-relevant-extensions-ja.js: Added.
     17        (shouldBe):
     18        (compareParts):
     19        (shouldThrow):
     20        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt1.formatRangeToParts):
     21        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt2.formatRangeToParts):
     22        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt3.formatRangeToParts):
     23        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt4.formatRangeToParts):
     24        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt5.formatRangeToParts):
     25        (Intl.DateTimeFormat.formatRangeToParts.vm.icuVersion):
     26        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt6.formatRangeToParts):
     27        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt7.formatRangeToParts):
     28        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt8.formatRangeToParts):
     29        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt9.formatRangeToParts):
     30        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt10.formatRangeToParts):
     31        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt11.formatRangeToParts):
     32        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt12.formatRangeToParts):
     33        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt13.formatRangeToParts):
     34        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt14.formatRangeToParts):
     35        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt15.formatRangeToParts):
     36        (Intl.DateTimeFormat.formatRangeToParts.compareParts.fmt16.formatRangeToParts):
     37        * stress/intl-datetimeformat-formatrangetoparts-relevant-extensions.js: Added.
     38        (shouldBe):
     39        (compareParts):
     40        (shouldThrow):
     41        * stress/intl-datetimeformat-formatrangetoparts-should-not-handle-gregorian-change-date.js: Added.
     42        (shouldBe):
     43        (compareParts):
     44        (vm.icuHeaderVersion):
     45        * stress/intl-datetimeformat-formatrangetoparts.js: Added.
     46        (shouldBe):
     47        (compareParts):
     48        (shouldThrow):
     49        * test262/config.yaml:
     50        * test262/expectations.yaml: This failure is because of CLDR data inside ICU. ICU 67 will fix them.
     51
    1522020-11-10  Ross Kirsling  <ross.kirsling@sony.com>
    253
  • trunk/JSTests/stress/intl-datetimeformat-formatrange-relevant-extensions.js

    r269667 r269706  
    2121function test() {
    2222    let range = " – "; // This is not usual space unfortuantely in older ICU versions.
    23     if ($vm.icuVersion() >= 65)
     23    if ($vm.icuVersion() >= 67)
    2424        range = " – ";
    2525
  • trunk/JSTests/stress/intl-datetimeformat-formatrange.js

    r269667 r269706  
    3232}, `TypeError: startDate or endDate is undefined`);
    3333
     34shouldThrow(() => {
     35    fmt0.formatRange(new Date(Date.UTC(2008, 0, 20, 10, 0, 0)), new Date(Date.UTC(2007, 0, 10, 10, 0, 0)));
     36}, `RangeError: startDate is larger than endDate`);
    3437
    3538function test() {
    36     let range = " – "; // This is not usual space unfortuantely in older ICU versions.
    37     if ($vm.icuVersion() >= 65)
     39    let range = " – ";
     40    if ($vm.icuVersion() >= 67)
    3841        range = " – ";
    3942
     
    5356        });
    5457        shouldBe(fmt1.format(date1), `1/10/07, 2:00 AM`);
     58        shouldBe(fmt1.formatRange(date1, date1), `1/10/07, 2:00 AM`);
    5559        shouldBe(fmt1.formatRange(date1, date2), `1/10/07, 2:00${range}3:00 AM`);
    5660        shouldBe(fmt1.formatRange(date1, date3), `1/10/07, 2:00 AM${range}1/20/07, 2:00 AM`);
     
    6569        });
    6670        shouldBe(fmt1.format(date1), `1/10/07`);
     71        shouldBe(fmt1.formatRange(date1, date1), `1/10/07`);
    6772        shouldBe(fmt1.formatRange(date1, date2), `1/10/07`);
    6873        shouldBe(fmt1.formatRange(date1, date3), `1/10/07${range}1/20/07`);
     
    7681        });
    7782        shouldBe(fmt1.format(date1), `2007`);
     83        shouldBe(fmt1.formatRange(date1, date1), `2007`);
    7884        shouldBe(fmt1.formatRange(date1, date2), `2007`);
    7985        shouldBe(fmt1.formatRange(date1, date4), `2007${range}2008`);
     
    9096        });
    9197        shouldBe(fmt1.format(date1), `1/10/07, 10:00 AM`);
     98        shouldBe(fmt1.formatRange(date1, date1), `1/10/07, 10:00 AM`);
    9299        shouldBe(fmt1.formatRange(date1, date2), `1/10/07, 10:00${range}11:00 AM`);
    93100        shouldBe(fmt1.formatRange(date1, date3), `1/10/07, 10:00 AM${range}1/20/07, 10:00 AM`);
     
    102109        });
    103110        shouldBe(fmt2.format(date1), `Jan 10, 2007`);
     111        shouldBe(fmt2.formatRange(date1, date1), `Jan 10, 2007`);
    104112        shouldBe(fmt2.formatRange(date1, date2), `Jan 10, 2007`);
    105113        shouldBe(fmt2.formatRange(date1, date3), `Jan 10${range}20, 2007`);
     
    131139}
    132140
    133 if ($vm.icuVersion() >= 64)
     141if (Intl.DateTimeFormat.formatRange)
    134142    test();
  • trunk/JSTests/test262/config.yaml

    r269531 r269706  
    9595    - test/built-ins/Atomics/wait/bigint
    9696    - test/built-ins/Atomics/xor/bigint
    97 
    98     # requires ICU APIs implementation
    99     # https://bugs.webkit.org/show_bug.cgi?id=213822
    100     - test/intl402/DateTimeFormat/prototype/formatRangeToParts
    10197  files:
    10298    # https://bugs.webkit.org/show_bug.cgi?id=190800
  • trunk/JSTests/test262/expectations.yaml

    r269670 r269706  
    956956  default: 'Test262Error: no fractionalSecondDigits Expected SameValue(«02:03 – 02:13», «02:03 – 02:13») to be true'
    957957  strict mode: 'Test262Error: no fractionalSecondDigits Expected SameValue(«02:03 – 02:13», «02:03 – 02:13») to be true'
     958test/intl402/DateTimeFormat/prototype/formatRangeToParts/en-US.js:
     959  default: 'Test262Error: value for entry 5 Expected SameValue(« – », « – ») to be true'
     960  strict mode: 'Test262Error: value for entry 5 Expected SameValue(« – », « – ») to be true'
     961test/intl402/DateTimeFormat/prototype/formatRangeToParts/fractionalSecondDigits.js:
     962  default: 'Test262Error: value for entry 3 Expected SameValue(« – », « – ») to be true'
     963  strict mode: 'Test262Error: value for entry 3 Expected SameValue(« – », « – ») to be true'
    958964test/intl402/DateTimeFormat/prototype/resolvedOptions/hourCycle-default.js:
    959965  default: 'Test262Error: Expected SameValue(«h24», «h23») to be true'
  • trunk/Source/JavaScriptCore/ChangeLog

    r269694 r269706  
     12020-11-11  Yusuke Suzuki  <ysuzuki@apple.com>
     2
     3        [JSC] Implement Intl.DateTimeFormat.formatRangeToParts
     4        https://bugs.webkit.org/show_bug.cgi?id=213822
     5        <rdar://problem/69328711>
     6
     7        Reviewed by Ross Kirsling.
     8
     9        This patch implements Intl.DateTimeFormat.formatRangeToParts. It is already stage-4 (included in the spec).
     10        The inputs are date interval, and this function generates array of parts of formatted string of date interval.
     11        Currently, required ICU APIs are draft status. So, for now, we track ABI changes, and use APIs with careful version checks.
     12
     13        However, currently, OpenSource macOS WebKit is built with specific ICU header (ICU 62 headers). So for now, we disable it
     14        in OpenSource macOS WebKit build. But we enable it for Apple Internal SDK WebKit build. We can enable it if we include
     15        multiple ICU header sets and select appropriate one against the linked ICU version. In the other platforms, they are using
     16        corresponding ICU headers so that we can just enable it.
     17
     18        There are two interesting implementation topics.
     19
     20        1. From ICU 67, the signature of udtitvfmt_formatToResult is changed. We need to switch the implementation with fine grained ICU version checks.
     21        2. udtitvfmt_formatToResult does not have an ability to configure gregorian calendar change date: before that date, the calendar is julian.
     22           In ECMAScript spec, we need to ignore this gregorian calendar change date, and we should handle all gregorian calendar dates as is even if
     23           the dates are older than gregorian calendar change date. However, since udtitvfmt_formatToResult does not offer the above ability,
     24           ICU automatically switches the calendar between gregorian and julian. To fix this issue, ICU 67 introduced udtitvfmt_formatCalendarToResult,
     25           which can take an explicit calendar for each input date so that we configure gregorian calendar change date. But this only exists after ICU 67.
     26           In the implementations using ICU 64-66, we just use udtitvfmt_formatToResult.
     27
     28        * JavaScriptCore.xcodeproj/project.pbxproj:
     29        * Sources.txt:
     30        * runtime/IntlDateTimeFormat.cpp:
     31        (JSC::UDateIntervalFormatDeleter::operator()):
     32        (JSC::IntlDateTimeFormat::formatToParts const):
     33        (JSC::definitelyAfterGregorianCalendarChangeDate):
     34        (JSC::formattedValueFromDateRange):
     35        (JSC::IntlDateTimeFormat::formatRange):
     36        (JSC::IntlDateTimeFormat::formatRangeToParts):
     37        * runtime/IntlDateTimeFormat.h:
     38        * runtime/IntlDateTimeFormatPrototype.cpp:
     39        (JSC::IntlDateTimeFormatPrototype::create):
     40        (JSC::IntlDateTimeFormatPrototype::finishCreation):
     41        (JSC::JSC_DEFINE_HOST_FUNCTION):
     42        * runtime/IntlDateTimeFormatPrototype.h:
     43        * runtime/OptionsList.h:
     44
    1452020-11-11  Yusuke Suzuki  <ysuzuki@apple.com>
    246
  • trunk/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj

    r269380 r269706  
    18661866                E392E6F924D25FA900B20767 /* B3BottomTupleValue.h in Headers */ = {isa = PBXBuildFile; fileRef = E392E6F724D25FA600B20767 /* B3BottomTupleValue.h */; };
    18671867                E393ADD81FE702D00022D681 /* WeakMapImplInlines.h in Headers */ = {isa = PBXBuildFile; fileRef = E393ADD71FE702CC0022D681 /* WeakMapImplInlines.h */; };
     1868                E399AEC32559457F00B78485 /* IntlDateTimeFormat.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A1587D671B4DC14100D69849 /* IntlDateTimeFormat.cpp */; };
    18681869                E39BF39922A2288B00BD183E /* SymbolTableInlines.h in Headers */ = {isa = PBXBuildFile; fileRef = E39BF39822A2288B00BD183E /* SymbolTableInlines.h */; };
    18691870                E39D45F51D39005600B3B377 /* InterpreterInlines.h in Headers */ = {isa = PBXBuildFile; fileRef = E39D9D841D39000600667282 /* InterpreterInlines.h */; settings = {ATTRIBUTES = (Private, ); }; };
     
    1139311394                                33B2A548226543BF005A0F79 /* FTLLowerDFGToB3.cpp in Sources */,
    1139411395                                5C4196622270E0000047B7CD /* InspectorBackendDispatcherCompatibility.cpp in Sources */,
     11396                                E399AEC32559457F00B78485 /* IntlDateTimeFormat.cpp in Sources */,
    1139511397                                E366441E254409B30001876F /* IntlListFormat.cpp in Sources */,
    1139611398                                E38E8790254B978400F6F9E4 /* JSDateMath.cpp in Sources */,
  • trunk/Source/JavaScriptCore/Sources.txt

    r269320 r269706  
    814814runtime/IntlCollatorConstructor.cpp
    815815runtime/IntlCollatorPrototype.cpp
    816 runtime/IntlDateTimeFormat.cpp
     816runtime/IntlDateTimeFormat.cpp @no-unify // Confine U_HIDE_DRAFT_API's effect to this file.
    817817runtime/IntlDateTimeFormatConstructor.cpp
    818818runtime/IntlDateTimeFormatPrototype.cpp
     
    820820runtime/IntlDisplayNamesConstructor.cpp
    821821runtime/IntlDisplayNamesPrototype.cpp
    822 // Confine U_HIDE_DRAFT_API's effect in this file.
    823 runtime/IntlListFormat.cpp @no-unify
     822runtime/IntlListFormat.cpp @no-unify // Confine U_HIDE_DRAFT_API's effect in this file.
    824823runtime/IntlListFormatConstructor.cpp
    825824runtime/IntlListFormatPrototype.cpp
  • trunk/Source/JavaScriptCore/runtime/IntlDateTimeFormat.cpp

    r269341 r269706  
    3535#include <unicode/ucal.h>
    3636#include <unicode/uenum.h>
     37#include <wtf/Range.h>
    3738#include <wtf/text/StringBuilder.h>
    3839#include <wtf/unicode/icu/ICUHelpers.h>
    3940
     41#if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
     42#include <unicode/uformattedvalue.h>
     43#ifdef U_HIDE_DRAFT_API
     44#undef U_HIDE_DRAFT_API
     45#endif
     46#endif // HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
     47#include <unicode/udateintervalformat.h>
     48#if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
     49#define U_HIDE_DRAFT_API 1
     50#endif // HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
     51
    4052namespace JSC {
    4153
    42 static const double minECMAScriptTime = -8.64E15;
     54// We do not use ICUDeleter<udtitvfmt_close> because we do not want to include udateintervalformat.h in IntlDateTimeFormat.h.
     55// udateintervalformat.h needs to be included with #undef U_HIDE_DRAFT_API, and we would like to minimize this effect in IntlDateTimeFormat.cpp.
     56void UDateIntervalFormatDeleter::operator()(UDateIntervalFormat* formatter)
     57{
     58    if (formatter)
     59        udtitvfmt_close(formatter);
     60}
     61
     62static constexpr double minECMAScriptTime = -8.64E15;
    4363
    4464const ClassInfo IntlDateTimeFormat::s_info = { "Object", &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(IntlDateTimeFormat) };
     
    12491269
    12501270// https://tc39.es/ecma402/#sec-formatdatetimetoparts
    1251 JSValue IntlDateTimeFormat::formatToParts(JSGlobalObject* globalObject, double value) const
     1271JSValue IntlDateTimeFormat::formatToParts(JSGlobalObject* globalObject, double value, JSString* sourceType) const
    12521272{
    12531273    ASSERT(m_dateFormat);
     
    12901310            part->putDirect(vm, vm.propertyNames->type, literalString);
    12911311            part->putDirect(vm, vm.propertyNames->value, value);
     1312            if (sourceType)
     1313                part->putDirect(vm, vm.propertyNames->source, sourceType);
    12921314            parts->push(globalObject, part);
    12931315            RETURN_IF_EXCEPTION(scope, { });
     
    13011323            part->putDirect(vm, vm.propertyNames->type, type);
    13021324            part->putDirect(vm, vm.propertyNames->value, value);
     1325            if (sourceType)
     1326                part->putDirect(vm, vm.propertyNames->source, sourceType);
    13031327            parts->push(globalObject, part);
    13041328            RETURN_IF_EXCEPTION(scope, { });
    13051329        }
    13061330    }
    1307 
    13081331
    13091332    return parts;
     
    13581381}
    13591382
     1383#if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
     1384
     1385// If a date is after Oct 15, 1582, the configuration of gregorian calendar change date in UCalendar does not affect
     1386// on the formatted string. To ensure that it is after Oct 15 in all timezones, we add one day to gregorian calendar
     1387// change date in UTC, so that this check can conservatively answer whether the date is definitely after gregorian
     1388// calendar change date.
     1389static inline bool definitelyAfterGregorianCalendarChangeDate(double millisecondsFromEpoch)
     1390{
     1391    constexpr double gregorianCalendarReformDateInUTC = -12219292800000.0;
     1392    return millisecondsFromEpoch >= (gregorianCalendarReformDateInUTC + msPerDay);
     1393}
     1394
     1395static std::unique_ptr<UFormattedDateInterval, ICUDeleter<udtitvfmt_closeResult>> formattedValueFromDateRange(UDateIntervalFormat& dateIntervalFormat, UDateFormat& dateFormat, double startDate, double endDate, UErrorCode& status)
     1396{
     1397    auto result = std::unique_ptr<UFormattedDateInterval, ICUDeleter<udtitvfmt_closeResult>>(udtitvfmt_openResult(&status));
     1398    if (U_FAILURE(status))
     1399        return nullptr;
     1400
     1401    // After ICU 67, udtitvfmt_formatToResult's signature is changed.
     1402#if U_ICU_VERSION_MAJOR_NUM >= 67
     1403    // UFormattedDateInterval does not have a way to configure gregorian calendar change date while ECMAScript requires that
     1404    // gregorian calendar change should not have effect (we are setting ucal_setGregorianChange(cal, minECMAScriptTime, &status) explicitly).
     1405    // As a result, if the input date is older than gregorian calendar change date (Oct 15, 1582), the formatted string becomes
     1406    // julian calendar date.
     1407    // udtitvfmt_formatCalendarToResult API offers the way to set calendar to each date of the input, so that we can use UDateFormat's
     1408    // calendar which is already configured to meet ECMAScript's requirement (effectively clearing gregorian calendar change date).
     1409    //
     1410    // If we can ensure that startDate is after gregorian calendar change date, we can just use udtitvfmt_formatToResult since gregorian
     1411    // calendar change date does not affect on the formatted string.
     1412    //
     1413    // https://unicode-org.atlassian.net/browse/ICU-20705
     1414    if (definitelyAfterGregorianCalendarChangeDate(startDate))
     1415        udtitvfmt_formatToResult(&dateIntervalFormat, startDate, endDate, result.get(), &status);
     1416    else {
     1417        auto createCalendarForDate = [](const UCalendar* calendar, double date, UErrorCode& status) -> std::unique_ptr<UCalendar, ICUDeleter<ucal_close>> {
     1418            auto result = std::unique_ptr<UCalendar, ICUDeleter<ucal_close>>(ucal_clone(calendar, &status));
     1419            if (U_FAILURE(status))
     1420                return nullptr;
     1421            ucal_setMillis(result.get(), date, &status);
     1422            if (U_FAILURE(status))
     1423                return nullptr;
     1424            return result;
     1425        };
     1426
     1427        auto calendar = udat_getCalendar(&dateFormat);
     1428
     1429        auto startCalendar = createCalendarForDate(calendar, startDate, status);
     1430        if (U_FAILURE(status))
     1431            return nullptr;
     1432
     1433        auto endCalendar = createCalendarForDate(calendar, endDate, status);
     1434        if (U_FAILURE(status))
     1435            return nullptr;
     1436
     1437        udtitvfmt_formatCalendarToResult(&dateIntervalFormat, startCalendar.get(), endCalendar.get(), result.get(), &status);
     1438    }
     1439#else
     1440    UNUSED_PARAM(dateFormat);
     1441    udtitvfmt_formatToResult(&dateIntervalFormat, result.get(), startDate, endDate, &status);
     1442#endif
     1443    return result;
     1444}
     1445
     1446#endif // HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
     1447
    13601448JSValue IntlDateTimeFormat::formatRange(JSGlobalObject* globalObject, double startDate, double endDate)
    13611449{
     
    13761464    RETURN_IF_EXCEPTION(scope, { });
    13771465
     1466#if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
     1467    // If the date is older than gregorian calendar change date, we need to explicitly pass configured UCalendar to
     1468    // udtitvfmt_formatCalendarToResult to generate a correct formatted string.
     1469    // The comment in formattedValueFromDateRange describes the details.
     1470    if (!definitelyAfterGregorianCalendarChangeDate(startDate)) {
     1471        UErrorCode status = U_ZERO_ERROR;
     1472        auto result = formattedValueFromDateRange(*dateIntervalFormat, *m_dateFormat, startDate, endDate, status);
     1473        if (U_FAILURE(status)) {
     1474            throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1475            return { };
     1476        }
     1477
     1478        // UFormattedValue is owned by UFormattedDateInterval. We do not need to close it.
     1479        auto formattedValue = udtitvfmt_resultAsValue(result.get(), &status);
     1480        if (U_FAILURE(status)) {
     1481            throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1482            return { };
     1483        }
     1484
     1485        int32_t formattedStringLength = 0;
     1486        const UChar* formattedStringPointer = ufmtval_getString(formattedValue, &formattedStringLength, &status);
     1487        if (U_FAILURE(status)) {
     1488            throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1489            return { };
     1490        }
     1491
     1492        return jsString(vm, String(formattedStringPointer, formattedStringLength));
     1493    }
     1494#endif
     1495
    13781496    Vector<UChar, 32> buffer;
    13791497    auto status = callBufferProducingFunction(udtitvfmt_format, dateIntervalFormat, startDate, endDate, buffer, nullptr);
     
    13861504}
    13871505
     1506JSValue IntlDateTimeFormat::formatRangeToParts(JSGlobalObject* globalObject, double startDate, double endDate)
     1507{
     1508    ASSERT(m_dateFormat);
     1509
     1510    VM& vm = globalObject->vm();
     1511    auto scope = DECLARE_THROW_SCOPE(vm);
     1512
     1513#if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
     1514    // http://tc39.es/proposal-intl-DateTimeFormat-formatRange/#sec-partitiondatetimerangepattern
     1515    startDate = timeClip(startDate);
     1516    endDate = timeClip(endDate);
     1517    if (std::isnan(startDate) || std::isnan(endDate)) {
     1518        throwRangeError(globalObject, scope, "Passed date is out of range"_s);
     1519        return { };
     1520    }
     1521
     1522    auto* dateIntervalFormat = createDateIntervalFormatIfNecessary(globalObject);
     1523    RETURN_IF_EXCEPTION(scope, { });
     1524
     1525    auto dateFieldsPracticallyEqual = [](const UFormattedValue* formattedValue, UErrorCode& status) {
     1526        auto iterator = std::unique_ptr<UConstrainedFieldPosition, ICUDeleter<ucfpos_close>>(ucfpos_open(&status));
     1527        if (U_FAILURE(status))
     1528            return false;
     1529
     1530        // We only care about UFIELD_CATEGORY_DATE_INTERVAL_SPAN category.
     1531        ucfpos_constrainCategory(iterator.get(), UFIELD_CATEGORY_DATE_INTERVAL_SPAN, &status);
     1532        if (U_FAILURE(status))
     1533            return false;
     1534
     1535        bool hasSpan = ufmtval_nextPosition(formattedValue, iterator.get(), &status);
     1536        if (U_FAILURE(status))
     1537            return false;
     1538
     1539        return !hasSpan;
     1540    };
     1541
     1542    UErrorCode status = U_ZERO_ERROR;
     1543    auto result = formattedValueFromDateRange(*dateIntervalFormat, *m_dateFormat, startDate, endDate, status);
     1544    if (U_FAILURE(status)) {
     1545        throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1546        return { };
     1547    }
     1548
     1549    // UFormattedValue is owned by UFormattedDateInterval. We do not need to close it.
     1550    auto formattedValue = udtitvfmt_resultAsValue(result.get(), &status);
     1551    if (U_FAILURE(status)) {
     1552        throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1553        return { };
     1554    }
     1555
     1556    auto sharedString = jsNontrivialString(vm, "shared"_s);
     1557
     1558    // If the formatted parts of startDate and endDate are the same, it is possible that the resulted string does not look like range.
     1559    // For example, if the requested format only includes "year" and startDate and endDate are the same year, the result just contains one year.
     1560    // In that case, startDate and endDate are *practically-equal* (spec term), and we generate parts as we call `formatToParts(startDate)` with
     1561    // `source: "shared"` additional fields.
     1562    bool equal = dateFieldsPracticallyEqual(formattedValue, status);
     1563    if (U_FAILURE(status)) {
     1564        throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1565        return { };
     1566    }
     1567
     1568    if (equal)
     1569        RELEASE_AND_RETURN(scope, formatToParts(globalObject, startDate, sharedString));
     1570
     1571    // ICU produces ranges for the formatted string, and we construct parts array from that.
     1572    // For example, startDate = Jan 3, 2019, endDate = Jan 5, 2019 with en-US locale is,
     1573    //
     1574    // Formatted string: "1/3/2019 – 1/5/2019"
     1575    //                    | | |  |   | | |  |
     1576    //                    B C |  |   F G |  |
     1577    //                    |   +-D+   |   +-H+
     1578    //                    |      |   |      |
     1579    //                    +--A---+   +--E---+
     1580    //
     1581    // Ranges ICU generates:
     1582    //     A:    (0, 8)   UFIELD_CATEGORY_DATE_INTERVAL_SPAN startRange
     1583    //     B:    (0, 1)   UFIELD_CATEGORY_DATE month
     1584    //     C:    (2, 3)   UFIELD_CATEGORY_DATE day
     1585    //     D:    (4, 8)   UFIELD_CATEGORY_DATE year
     1586    //     E:    (11, 19) UFIELD_CATEGORY_DATE_INTERVAL_SPAN endRange
     1587    //     F:    (11, 12) UFIELD_CATEGORY_DATE month
     1588    //     G:    (13, 14) UFIELD_CATEGORY_DATE day
     1589    //     H:    (15, 19) UFIELD_CATEGORY_DATE year
     1590    //
     1591    //  We use UFIELD_CATEGORY_DATE_INTERVAL_SPAN range to determine each part is either "startRange", "endRange", or "shared".
     1592    //  It is gurarnteed that UFIELD_CATEGORY_DATE_INTERVAL_SPAN comes first before any other parts including that range.
     1593    //  For example, in the above formatted string, " – " is "shared" part. For UFIELD_CATEGORY_DATE ranges, we generate corresponding
     1594    //  part object with types such as "month". And non populated parts (e.g. "/") become "literal" parts.
     1595    //  In the above case, expected parts are,
     1596    //
     1597    //     { type: "month", value: "1", source: "startRange" },
     1598    //     { type: "literal", value: "/", source: "startRange" },
     1599    //     { type: "day", value: "3", source: "startRange" },
     1600    //     { type: "literal", value: "/", source: "startRange" },
     1601    //     { type: "year", value: "2019", source: "startRange" },
     1602    //     { type: "literal", value: " - ", source: "shared" },
     1603    //     { type: "month", value: "1", source: "endRange" },
     1604    //     { type: "literal", value: "/", source: "endRange" },
     1605    //     { type: "day", value: "5", source: "endRange" },
     1606    //     { type: "literal", value: "/", source: "endRange" },
     1607    //     { type: "year", value: "2019", source: "endRange" },
     1608    //
     1609
     1610    JSArray* parts = JSArray::tryCreate(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 0);
     1611    if (!parts) {
     1612        throwOutOfMemoryError(globalObject, scope);
     1613        return { };
     1614    }
     1615
     1616    int32_t formattedStringLength = 0;
     1617    const UChar* formattedStringPointer = ufmtval_getString(formattedValue, &formattedStringLength, &status);
     1618    if (U_FAILURE(status)) {
     1619        throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1620        return { };
     1621    }
     1622    String resultString(formattedStringPointer, formattedStringLength);
     1623
     1624    // We care multiple categories (UFIELD_CATEGORY_DATE and UFIELD_CATEGORY_DATE_INTERVAL_SPAN).
     1625    // So we do not constraint iterator.
     1626    auto iterator = std::unique_ptr<UConstrainedFieldPosition, ICUDeleter<ucfpos_close>>(ucfpos_open(&status));
     1627    if (U_FAILURE(status)) {
     1628        throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1629        return { };
     1630    }
     1631
     1632    auto startRangeString = jsNontrivialString(vm, "startRange"_s);
     1633    auto endRangeString = jsNontrivialString(vm, "endRange"_s);
     1634    auto literalString = jsNontrivialString(vm, "literal"_s);
     1635
     1636    WTF::Range<int32_t> startRange { -1, -1 };
     1637    WTF::Range<int32_t> endRange { -1, -1 };
     1638
     1639    auto createPart = [&] (JSString* type, int32_t beginIndex, int32_t length) {
     1640        auto sourceType = [&](int32_t index) -> JSString* {
     1641            if (startRange.contains(index))
     1642                return startRangeString;
     1643            if (endRange.contains(index))
     1644                return endRangeString;
     1645            return sharedString;
     1646        };
     1647
     1648        auto value = jsString(vm, resultString.substring(beginIndex, length));
     1649        JSObject* part = constructEmptyObject(globalObject);
     1650        part->putDirect(vm, vm.propertyNames->type, type);
     1651        part->putDirect(vm, vm.propertyNames->value, value);
     1652        part->putDirect(vm, vm.propertyNames->source, sourceType(beginIndex));
     1653        return part;
     1654    };
     1655
     1656    int32_t resultLength = resultString.length();
     1657    int32_t previousEndIndex = 0;
     1658    while (true) {
     1659        bool next = ufmtval_nextPosition(formattedValue, iterator.get(), &status);
     1660        if (U_FAILURE(status)) {
     1661            throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1662            return { };
     1663        }
     1664        if (!next)
     1665            break;
     1666
     1667        int32_t category = ucfpos_getCategory(iterator.get(), &status);
     1668        if (U_FAILURE(status)) {
     1669            throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1670            return { };
     1671        }
     1672
     1673        int32_t fieldType = ucfpos_getField(iterator.get(), &status);
     1674        if (U_FAILURE(status)) {
     1675            throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1676            return { };
     1677        }
     1678
     1679        int32_t beginIndex = 0;
     1680        int32_t endIndex = 0;
     1681        ucfpos_getIndexes(iterator.get(), &beginIndex, &endIndex, &status);
     1682        if (U_FAILURE(status)) {
     1683            throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1684            return { };
     1685        }
     1686
     1687        dataLogLnIf(IntlDateTimeFormatInternal::verbose, category, " ", fieldType, " (", beginIndex, ", ", endIndex, ")");
     1688
     1689        if (category != UFIELD_CATEGORY_DATE && category != UFIELD_CATEGORY_DATE_INTERVAL_SPAN)
     1690            continue;
     1691        if (category == UFIELD_CATEGORY_DATE && fieldType < 0)
     1692            continue;
     1693
     1694        if (previousEndIndex < beginIndex) {
     1695            JSObject* part = createPart(literalString, previousEndIndex, beginIndex - previousEndIndex);
     1696            parts->push(globalObject, part);
     1697            RETURN_IF_EXCEPTION(scope, { });
     1698            previousEndIndex = beginIndex;
     1699        }
     1700
     1701        if (category == UFIELD_CATEGORY_DATE_INTERVAL_SPAN) {
     1702            // > The special field category UFIELD_CATEGORY_DATE_INTERVAL_SPAN is used to indicate which datetime
     1703            // > primitives came from which arguments: 0 means fromCalendar, and 1 means toCalendar. The span category
     1704            // > will always occur before the corresponding fields in UFIELD_CATEGORY_DATE in the nextPosition() iterator.
     1705            // from ICU comment. So, field 0 is startRange, field 1 is endRange.
     1706            if (!fieldType)
     1707                startRange = WTF::Range<int32_t>(beginIndex, endIndex);
     1708            else {
     1709                ASSERT(fieldType == 1);
     1710                endRange = WTF::Range<int32_t>(beginIndex, endIndex);
     1711            }
     1712            continue;
     1713        }
     1714
     1715        ASSERT(category == UFIELD_CATEGORY_DATE);
     1716
     1717        auto type = jsString(vm, partTypeString(UDateFormatField(fieldType)));
     1718        JSObject* part = createPart(type, beginIndex, endIndex - beginIndex);
     1719        parts->push(globalObject, part);
     1720        RETURN_IF_EXCEPTION(scope, { });
     1721        previousEndIndex = endIndex;
     1722    }
     1723
     1724    if (previousEndIndex < resultLength) {
     1725        JSObject* part = createPart(literalString, previousEndIndex, resultLength - previousEndIndex);
     1726        parts->push(globalObject, part);
     1727        RETURN_IF_EXCEPTION(scope, { });
     1728    }
     1729
     1730    return parts;
     1731#else
     1732    UNUSED_PARAM(startDate);
     1733    UNUSED_PARAM(endDate);
     1734    throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1735    return { };
     1736#endif
     1737}
     1738
     1739
    13881740} // namespace JSC
  • trunk/Source/JavaScriptCore/runtime/IntlDateTimeFormat.h

    r267108 r269706  
    2828#include "JSObject.h"
    2929#include <unicode/udat.h>
    30 #include <unicode/udateintervalformat.h>
    3130#include <wtf/unicode/icu/ICUHelpers.h>
     31
     32struct UDateIntervalFormat;
     33
     34#if !defined(HAVE_ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
     35// ICU header is up-to-date if the build is non-Darwin or using Apple Internal SDK.
     36#if (USE(APPLE_INTERNAL_SDK) || !OS(DARWIN)) && U_ICU_VERSION_MAJOR_NUM >= 64
     37#define HAVE_ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS 1
     38#endif
     39#endif
    3240
    3341namespace JSC {
     
    3644
    3745class JSBoundFunction;
     46
     47struct UDateIntervalFormatDeleter {
     48    JS_EXPORT_PRIVATE void operator()(UDateIntervalFormat*);
     49};
    3850
    3951class IntlDateTimeFormat final : public JSNonFinalObject {
     
    6173    void initializeDateTimeFormat(JSGlobalObject*, JSValue locales, JSValue options);
    6274    JSValue format(JSGlobalObject*, double value) const;
    63     JSValue formatToParts(JSGlobalObject*, double value) const;
     75    JSValue formatToParts(JSGlobalObject*, double value, JSString* sourceType = nullptr) const;
    6476    JSValue formatRange(JSGlobalObject*, double startDate, double endDate);
     77    JSValue formatRangeToParts(JSGlobalObject*, double startDate, double endDate);
    6578    JSObject* resolvedOptions(JSGlobalObject*) const;
    6679
     
    113126
    114127    using UDateFormatDeleter = ICUDeleter<udat_close>;
    115     using UDateIntervalFormatDeleter = ICUDeleter<udtitvfmt_close>;
    116128
    117129    WriteBarrier<JSBoundFunction> m_boundFormat;
  • trunk/Source/JavaScriptCore/runtime/IntlDateTimeFormatPrototype.cpp

    r267594 r269706  
    3939static JSC_DECLARE_HOST_FUNCTION(IntlDateTimeFormatPrototypeGetterFormat);
    4040static JSC_DECLARE_HOST_FUNCTION(IntlDateTimeFormatPrototypeFuncFormatRange);
     41static JSC_DECLARE_HOST_FUNCTION(IntlDateTimeFormatPrototypeFuncFormatRangeToParts);
    4142static JSC_DECLARE_HOST_FUNCTION(IntlDateTimeFormatPrototypeFuncFormatToParts);
    4243static JSC_DECLARE_HOST_FUNCTION(IntlDateTimeFormatPrototypeFuncResolvedOptions);
     
    6061*/
    6162
    62 IntlDateTimeFormatPrototype* IntlDateTimeFormatPrototype::create(VM& vm, JSGlobalObject*, Structure* structure)
     63IntlDateTimeFormatPrototype* IntlDateTimeFormatPrototype::create(VM& vm, JSGlobalObject* globalObject, Structure* structure)
    6364{
    6465    IntlDateTimeFormatPrototype* object = new (NotNull, allocateCell<IntlDateTimeFormatPrototype>(vm.heap)) IntlDateTimeFormatPrototype(vm, structure);
    65     object->finishCreation(vm);
     66    object->finishCreation(vm, globalObject);
    6667    return object;
    6768}
     
    7778}
    7879
    79 void IntlDateTimeFormatPrototype::finishCreation(VM& vm)
     80void IntlDateTimeFormatPrototype::finishCreation(VM& vm, JSGlobalObject* globalObject)
    8081{
    8182    Base::finishCreation(vm);
    8283    ASSERT(inherits(vm, info()));
     84#if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
     85    if (Options::useIntlDateTimeFormatRangeToParts())
     86        JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("formatRangeToParts", IntlDateTimeFormatPrototypeFuncFormatRangeToParts, static_cast<unsigned>(PropertyAttribute::DontEnum), 2);
     87#else
     88    UNUSED_PARAM(globalObject);
     89    UNUSED_PARAM(&IntlDateTimeFormatPrototypeFuncFormatRangeToParts);
     90#endif
    8391    JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
    8492}
     
    189197}
    190198
     199// http://tc39.es/proposal-intl-DateTimeFormat-formatRange/#sec-intl.datetimeformat.prototype.formatRangeToParts
     200JSC_DEFINE_HOST_FUNCTION(IntlDateTimeFormatPrototypeFuncFormatRangeToParts, (JSGlobalObject* globalObject, CallFrame* callFrame))
     201{
     202    VM& vm = globalObject->vm();
     203    auto scope = DECLARE_THROW_SCOPE(vm);
     204
     205    // Do not use unwrapForOldFunctions.
     206    auto* dateTimeFormat = jsDynamicCast<IntlDateTimeFormat*>(vm, callFrame->thisValue());
     207    if (UNLIKELY(!dateTimeFormat))
     208        return JSValue::encode(throwTypeError(globalObject, scope, "Intl.DateTimeFormat.prototype.formatRangeToParts called on value that's not an object initialized as a DateTimeFormat"_s));
     209
     210    JSValue startDateValue = callFrame->argument(0);
     211    JSValue endDateValue = callFrame->argument(1);
     212
     213    if (startDateValue.isUndefined() || endDateValue.isUndefined())
     214        return throwVMTypeError(globalObject, scope, "startDate or endDate is undefined"_s);
     215
     216    double startDate = startDateValue.toNumber(globalObject);
     217    RETURN_IF_EXCEPTION(scope, { });
     218    double endDate = endDateValue.toNumber(globalObject);
     219    RETURN_IF_EXCEPTION(scope, { });
     220    if (startDate > endDate)
     221        return throwVMRangeError(globalObject, scope, "startDate is larger than endDate"_s);
     222
     223    RELEASE_AND_RETURN(scope, JSValue::encode(dateTimeFormat->formatRangeToParts(globalObject, startDate, endDate)));
     224}
     225
    191226JSC_DEFINE_HOST_FUNCTION(IntlDateTimeFormatPrototypeFuncResolvedOptions, (JSGlobalObject* globalObject, CallFrame* callFrame))
    192227{
  • trunk/Source/JavaScriptCore/runtime/IntlDateTimeFormatPrototype.h

    r260415 r269706  
    5050private:
    5151    IntlDateTimeFormatPrototype(VM&, Structure*);
    52     void finishCreation(VM&);
     52    void finishCreation(VM&, JSGlobalObject*);
    5353};
    5454
  • trunk/Source/JavaScriptCore/runtime/OptionsList.h

    r269531 r269706  
    494494    v(Bool, useWeakRefs, true, Normal, "Expose the WeakRef constructor.") \
    495495    v(Bool, useIntlDateTimeFormatDayPeriod, true, Normal, "Expose the Intl.DateTimeFormat dayPeriod feature.") \
     496    v(Bool, useIntlDateTimeFormatRangeToParts, true, Normal, "Expose the Intl.DateTimeFormat#formatRangeToParts feature.") \
    496497    v(Bool, useAtMethod, false, Normal, "Expose the at() method on Array and %TypedArray%.") \
    497498    v(Bool, useSharedArrayBuffer, false, Normal, nullptr) \
Note: See TracChangeset for help on using the changeset viewer.