Changeset 271224 in webkit


Ignore:
Timestamp:
Jan 6, 2021 10:11:42 PM (19 months ago)
Author:
ysuzuki@apple.com
Message:

[JSC] DateTimeFormat#formatRange should generate the same output to DateTimeFormat#format if startDate and endDate are "practically-equal"
https://bugs.webkit.org/show_bug.cgi?id=220395

Reviewed by Ross Kirsling.

JSTests:

Imported some test262 tests. Updated, fixed some issues (formatToParts test was using format, we should upstream it to test262), and added more tests.

  • stress/intl-datetimeformat-format-range-should-check-practically-equal.js: Added.

(shouldBe):
(vm.icuVersion):

  • stress/intl-datetimeformat-format-range-to-parts-should-check-practically-equal.js: Added.

(shouldBe):
(zip):
(compare):
(vm.icuVersion):

Source/JavaScriptCore:

Intl.DateTimeFormat.formatRange(startDate, endDate) also needs to generate the same formatted string to the Intl.DateTimeFormat.format
if startDate and endDate are *practically-equal* (spec term). However, due to CLDR, just using udtitvfmt_format generates different
formatted string to udat_format's result even though startDate and endDate are the same.

new Intl.DateTimeFormat("en", { dateStyle: "long", timeStyle: "short" }).format(new Date())
"December 12, 2019 at 11:48 AM"
new Intl.DateTimeFormat("en", { dateStyle: "long", timeStyle: "short" }).formatRange(new Date(), new Date())
"December 12, 2019, 11:48 AM"

In Intl.DateTimeFormat#formatRangeToParts, we deploys *practically-equal* checking to avoid this issue. The same thing should be done in
Intl.DateTimeFormat#formatRange too.

In this patch, we stop using udtitvfmt_format if ICU version is 64 or later to perform *practically-equal* checking.

[1]: https://github.com/tc39/proposal-intl-DateTimeFormat-formatRange/issues/19

  • runtime/IntlDateTimeFormat.cpp:

(JSC::formattedValueFromDateRange):
(JSC::dateFieldsPracticallyEqual):
(JSC::IntlDateTimeFormat::formatRange):
(JSC::IntlDateTimeFormat::formatRangeToParts):
(JSC::definitelyAfterGregorianCalendarChangeDate): Deleted.

Location:
trunk
Files:
2 added
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/JSTests/ChangeLog

    r271168 r271224  
     12021-01-06  Yusuke Suzuki  <ysuzuki@apple.com>
     2
     3        [JSC] DateTimeFormat#formatRange should generate the same output to DateTimeFormat#format if startDate and endDate are "practically-equal"
     4        https://bugs.webkit.org/show_bug.cgi?id=220395
     5
     6        Reviewed by Ross Kirsling.
     7
     8        Imported some test262 tests. Updated, fixed some issues (`formatToParts` test was using `format`, we should upstream it to test262), and added more tests.
     9
     10        * stress/intl-datetimeformat-format-range-should-check-practically-equal.js: Added.
     11        (shouldBe):
     12        (vm.icuVersion):
     13        * stress/intl-datetimeformat-format-range-to-parts-should-check-practically-equal.js: Added.
     14        (shouldBe):
     15        (zip):
     16        (compare):
     17        (vm.icuVersion):
     18
    1192021-01-05  Yusuke Suzuki  <ysuzuki@apple.com>
    220
  • trunk/Source/JavaScriptCore/ChangeLog

    r271217 r271224  
     12021-01-06  Yusuke Suzuki  <ysuzuki@apple.com>
     2
     3        [JSC] DateTimeFormat#formatRange should generate the same output to DateTimeFormat#format if startDate and endDate are "practically-equal"
     4        https://bugs.webkit.org/show_bug.cgi?id=220395
     5
     6        Reviewed by Ross Kirsling.
     7
     8        Intl.DateTimeFormat.formatRange(startDate, endDate) also needs to generate the same formatted string to the Intl.DateTimeFormat.format
     9        if startDate and endDate are *practically-equal* (spec term). However, due to CLDR, just using udtitvfmt_format generates different
     10        formatted string to udat_format's result even though startDate and endDate are the same.
     11
     12            new Intl.DateTimeFormat("en", { dateStyle: "long", timeStyle: "short" }).format(new Date())
     13            // "December 12, 2019 at 11:48 AM"
     14            new Intl.DateTimeFormat("en", { dateStyle: "long", timeStyle: "short" }).formatRange(new Date(), new Date())
     15            // "December 12, 2019, 11:48 AM"
     16
     17        In Intl.DateTimeFormat#formatRangeToParts, we deploys *practically-equal* checking to avoid this issue. The same thing should be done in
     18        Intl.DateTimeFormat#formatRange too.
     19
     20        In this patch, we stop using udtitvfmt_format if ICU version is 64 or later to perform *practically-equal* checking.
     21
     22        [1]: https://github.com/tc39/proposal-intl-DateTimeFormat-formatRange/issues/19
     23
     24        * runtime/IntlDateTimeFormat.cpp:
     25        (JSC::formattedValueFromDateRange):
     26        (JSC::dateFieldsPracticallyEqual):
     27        (JSC::IntlDateTimeFormat::formatRange):
     28        (JSC::IntlDateTimeFormat::formatRangeToParts):
     29        (JSC::definitelyAfterGregorianCalendarChangeDate): Deleted.
     30
    1312021-01-06  Yusuke Suzuki  <ysuzuki@apple.com>
    232
  • trunk/Source/JavaScriptCore/runtime/IntlDateTimeFormat.cpp

    r270861 r271224  
    13831383#if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
    13841384
    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.
    1389 static inline bool definitelyAfterGregorianCalendarChangeDate(double millisecondsFromEpoch)
    1390 {
    1391     constexpr double gregorianCalendarReformDateInUTC = -12219292800000.0;
    1392     return millisecondsFromEpoch >= (gregorianCalendarReformDateInUTC + msPerDay);
    1393 }
    1394 
    13951385static std::unique_ptr<UFormattedDateInterval, ICUDeleter<udtitvfmt_closeResult>> formattedValueFromDateRange(UDateIntervalFormat& dateIntervalFormat, UDateFormat& dateFormat, double startDate, double endDate, UErrorCode& status)
    13961386{
     
    14011391    // After ICU 67, udtitvfmt_formatToResult's signature is changed.
    14021392#if U_ICU_VERSION_MAJOR_NUM >= 67
     1393    // If a date is after Oct 15, 1582, the configuration of gregorian calendar change date in UCalendar does not affect
     1394    // on the formatted string. To ensure that it is after Oct 15 in all timezones, we add one day to gregorian calendar
     1395    // change date in UTC, so that this check can conservatively answer whether the date is definitely after gregorian
     1396    // calendar change date.
     1397    auto definitelyAfterGregorianCalendarChangeDate = [](double millisecondsFromEpoch) {
     1398        constexpr double gregorianCalendarReformDateInUTC = -12219292800000.0;
     1399        return millisecondsFromEpoch >= (gregorianCalendarReformDateInUTC + msPerDay);
     1400    };
     1401
    14031402    // UFormattedDateInterval does not have a way to configure gregorian calendar change date while ECMAScript requires that
    14041403    // gregorian calendar change should not have effect (we are setting ucal_setGregorianChange(cal, minECMAScriptTime, &status) explicitly).
     
    14441443}
    14451444
     1445static bool dateFieldsPracticallyEqual(const UFormattedValue* formattedValue, UErrorCode& status)
     1446{
     1447    auto iterator = std::unique_ptr<UConstrainedFieldPosition, ICUDeleter<ucfpos_close>>(ucfpos_open(&status));
     1448    if (U_FAILURE(status))
     1449        return false;
     1450
     1451    // We only care about UFIELD_CATEGORY_DATE_INTERVAL_SPAN category.
     1452    ucfpos_constrainCategory(iterator.get(), UFIELD_CATEGORY_DATE_INTERVAL_SPAN, &status);
     1453    if (U_FAILURE(status))
     1454        return false;
     1455
     1456    bool hasSpan = ufmtval_nextPosition(formattedValue, iterator.get(), &status);
     1457    if (U_FAILURE(status))
     1458        return false;
     1459
     1460    return !hasSpan;
     1461}
     1462
    14461463#endif // HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS)
    14471464
     
    14651482
    14661483#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 
     1484    UErrorCode status = U_ZERO_ERROR;
     1485    auto result = formattedValueFromDateRange(*dateIntervalFormat, *m_dateFormat, startDate, endDate, status);
     1486    if (U_FAILURE(status)) {
     1487        throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1488        return { };
     1489    }
     1490
     1491    // UFormattedValue is owned by UFormattedDateInterval. We do not need to close it.
     1492    auto formattedValue = udtitvfmt_resultAsValue(result.get(), &status);
     1493    if (U_FAILURE(status)) {
     1494        throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1495        return { };
     1496    }
     1497
     1498    // If the formatted parts of startDate and endDate are the same, it is possible that the resulted string does not look like range.
     1499    // For example, if the requested format only includes "year" and startDate and endDate are the same year, the result just contains one year.
     1500    // In that case, startDate and endDate are *practically-equal* (spec term), and we generate parts as we call `formatToParts(startDate)` with
     1501    // `source: "shared"` additional fields.
     1502    bool equal = dateFieldsPracticallyEqual(formattedValue, status);
     1503    if (U_FAILURE(status)) {
     1504        throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1505        return { };
     1506    }
     1507
     1508    if (equal)
     1509        RELEASE_AND_RETURN(scope, format(globalObject, startDate));
     1510
     1511    int32_t formattedStringLength = 0;
     1512    const UChar* formattedStringPointer = ufmtval_getString(formattedValue, &formattedStringLength, &status);
     1513    if (U_FAILURE(status)) {
     1514        throwTypeError(globalObject, scope, "Failed to format date interval"_s);
     1515        return { };
     1516    }
     1517
     1518    return jsString(vm, String(formattedStringPointer, formattedStringLength));
     1519#else
    14961520    Vector<UChar, 32> buffer;
    14971521    auto status = callBufferProducingFunction(udtitvfmt_format, dateIntervalFormat, startDate, endDate, buffer, nullptr);
     
    15021526
    15031527    return jsString(vm, String(buffer));
     1528#endif
    15041529}
    15051530
     
    15221547    auto* dateIntervalFormat = createDateIntervalFormatIfNecessary(globalObject);
    15231548    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     };
    15411549
    15421550    UErrorCode status = U_ZERO_ERROR;
Note: See TracChangeset for help on using the changeset viewer.