Skip to content

Commit

Permalink
[JSC] Add TimeZoneOffset format support to Intl.DateTimeFormat
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=263315
rdar://117124296

Reviewed by Ross Kirsling.

Latest spec adds offset-based timeZone option support for Intl.DateTimeFormat.
This patch implements it. The example is, "+03:00" => "GMT+0300".
See tc39.es/ecma402/#sec-createdatetimeformat steps 32-33.

* JSTests/test262/expectations.yaml:
* Source/JavaScriptCore/runtime/ISO8601.cpp:
(JSC::ISO8601::parseUTCOffsetInMinutes):
* Source/JavaScriptCore/runtime/ISO8601.h:
* Source/JavaScriptCore/runtime/IntlDateTimeFormat.cpp:
(JSC::IntlDateTimeFormat::initializeDateTimeFormat):
(JSC::IntlDateTimeFormat::createDateIntervalFormatIfNecessary):
* Source/JavaScriptCore/runtime/IntlDateTimeFormat.h:

Canonical link: https://commits.webkit.org/269497@main
  • Loading branch information
Constellation committed Oct 19, 2023
1 parent 0fb6b6e commit b999ffe
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 40 deletions.
12 changes: 0 additions & 12 deletions JSTests/test262/expectations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1096,9 +1096,6 @@ test/harness/verifyProperty-desc-is-not-object.js:
test/harness/verifyProperty-value-error.js:
default: 'Error: The error thrown did not define the specified message.'
strict mode: 'Error: The error thrown did not define the specified message.'
test/intl402/DateTimeFormat/prototype/format/offset-timezone-gmt-same.js:
default: 'RangeError: invalid time zone: +0300'
strict mode: 'RangeError: invalid time zone: +0300'
test/intl402/DateTimeFormat/prototype/format/temporal-zoneddatetime-not-supported.js:
default: "TypeError: undefined is not a constructor (evaluating 'new Temporal.ZonedDateTime(0n, timeZone)')"
strict mode: "TypeError: undefined is not a constructor (evaluating 'new Temporal.ZonedDateTime(0n, timeZone)')"
Expand All @@ -1108,21 +1105,12 @@ test/intl402/DateTimeFormat/prototype/formatRange/temporal-zoneddatetime-not-sup
test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-zoneddatetime-not-supported.js:
default: "TypeError: undefined is not a constructor (evaluating 'new Temporal.ZonedDateTime(0n, timeZone)')"
strict mode: "TypeError: undefined is not a constructor (evaluating 'new Temporal.ZonedDateTime(0n, timeZone)')"
test/intl402/DateTimeFormat/prototype/formatToParts/offset-timezone-correct.js:
default: 'RangeError: invalid time zone: +0301'
strict mode: 'RangeError: invalid time zone: +0301'
test/intl402/DateTimeFormat/prototype/formatToParts/temporal-zoneddatetime-not-supported.js:
default: "TypeError: undefined is not a constructor (evaluating 'new Temporal.ZonedDateTime(0n, timeZone)')"
strict mode: "TypeError: undefined is not a constructor (evaluating 'new Temporal.ZonedDateTime(0n, timeZone)')"
test/intl402/DateTimeFormat/prototype/resolvedOptions/hourCycle-default.js:
default: 'Test262Error: Expected SameValue(«h24», «h23») to be true'
strict mode: 'Test262Error: Expected SameValue(«h24», «h23») to be true'
test/intl402/DateTimeFormat/prototype/resolvedOptions/offset-timezone-basic.js:
default: 'RangeError: invalid time zone: +03'
strict mode: 'RangeError: invalid time zone: +03'
test/intl402/DateTimeFormat/prototype/resolvedOptions/offset-timezone-change.js:
default: 'RangeError: invalid time zone: -00'
strict mode: 'RangeError: invalid time zone: -00'
test/intl402/DateTimeFormat/timezone-case-insensitive.js:
default: 'Test262Error: Time zone created from string "America/Argentina/Buenos_Aires" Expected SameValue(«America/Buenos_Aires», «America/Argentina/Buenos_Aires») to be true'
strict mode: 'Test262Error: Time zone created from string "America/Argentina/Buenos_Aires" Expected SameValue(«America/Buenos_Aires», «America/Argentina/Buenos_Aires») to be true'
Expand Down
140 changes: 119 additions & 21 deletions Source/JavaScriptCore/runtime/ISO8601.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,20 +270,19 @@ static std::optional<PlainTime> parseTimeSpec(StringParsingBuffer<CharacterType>
if (buffer.lengthRemaining() < 2)
return std::nullopt;

unsigned hour = 0;
ASSERT(buffer.lengthRemaining() >= 2);
auto firstHourCharacter = *buffer;
if (firstHourCharacter >= '0' && firstHourCharacter <= '2') {
buffer.advance();
auto secondHourCharacter = *buffer;
if (!isASCIIDigit(secondHourCharacter))
return std::nullopt;
hour = (secondHourCharacter - '0') + 10 * (firstHourCharacter - '0');
if (hour >= 24)
return std::nullopt;
buffer.advance();
} else
if (!(firstHourCharacter >= '0' && firstHourCharacter <= '2'))
return std::nullopt;

buffer.advance();
auto secondHourCharacter = *buffer;
if (!isASCIIDigit(secondHourCharacter))
return std::nullopt;
unsigned hour = (secondHourCharacter - '0') + 10 * (firstHourCharacter - '0');
if (hour >= 24)
return std::nullopt;
buffer.advance();

if (buffer.atEnd())
return PlainTime(hour, 0, 0, 0, 0, 0);
Expand All @@ -295,21 +294,20 @@ static std::optional<PlainTime> parseTimeSpec(StringParsingBuffer<CharacterType>
} else if (!(*buffer >= '0' && *buffer <= '5'))
return PlainTime(hour, 0, 0, 0, 0, 0);

unsigned minute = 0;
if (buffer.lengthRemaining() < 2)
return std::nullopt;
auto firstMinuteCharacter = *buffer;
if (firstMinuteCharacter >= '0' && firstMinuteCharacter <= '5') {
buffer.advance();
auto secondMinuteCharacter = *buffer;
if (!isASCIIDigit(secondMinuteCharacter))
return std::nullopt;
minute = (secondMinuteCharacter - '0') + 10 * (firstMinuteCharacter - '0');
ASSERT(minute < 60);
buffer.advance();
} else
if (!(firstMinuteCharacter >= '0' && firstMinuteCharacter <= '5'))
return std::nullopt;

buffer.advance();
auto secondMinuteCharacter = *buffer;
if (!isASCIIDigit(secondMinuteCharacter))
return std::nullopt;
unsigned minute = (secondMinuteCharacter - '0') + 10 * (firstMinuteCharacter - '0');
ASSERT(minute < 60);
buffer.advance();

if (buffer.atEnd())
return PlainTime(hour, minute, 0, 0, 0, 0);

Expand Down Expand Up @@ -423,6 +421,106 @@ std::optional<int64_t> parseTimeZoneNumericUTCOffset(StringView string)
});
}

template<typename CharacterType>
static std::optional<int64_t> parseUTCOffsetInMinutes(StringParsingBuffer<CharacterType>& buffer)
{
// UTCOffset :::
// TemporalSign Hour
// TemporalSign Hour HourSubcomponents[+Extended]
// TemporalSign Hour HourSubcomponents[~Extended]
//
// TemporalSign :::
// ASCIISign
// <MINUS>
//
// ASCIISign ::: one of
// + -
//
// Hour :::
// 0 DecimalDigit
// 1 DecimalDigit
// 20
// 21
// 22
// 23
//
// HourSubcomponents[Extended] :::
// TimeSeparator[?Extended] MinuteSecond
//
// TimeSeparator[Extended] :::
// [+Extended] :
// [~Extended] [empty]
//
// MinuteSecond :::
// 0 DecimalDigit
// 1 DecimalDigit
// 2 DecimalDigit
// 3 DecimalDigit
// 4 DecimalDigit
// 5 DecimalDigit

// sign and hour.
if (buffer.lengthRemaining() < 3)
return std::nullopt;

int64_t factor = 1;
if (*buffer == '+')
buffer.advance();
else if (*buffer == '-' || *buffer == minusSign) {
factor = -1;
buffer.advance();
} else
return std::nullopt;

ASSERT(buffer.lengthRemaining() >= 2);
auto firstHourCharacter = *buffer;
if (!(firstHourCharacter >= '0' && firstHourCharacter <= '2'))
return std::nullopt;

buffer.advance();
auto secondHourCharacter = *buffer;
if (!isASCIIDigit(secondHourCharacter))
return std::nullopt;
unsigned hour = (secondHourCharacter - '0') + 10 * (firstHourCharacter - '0');
if (hour >= 24)
return std::nullopt;
buffer.advance();

if (buffer.atEnd())
return (hour * 60) * factor;

if (*buffer == ':')
buffer.advance();
else if (!(*buffer >= '0' && *buffer <= '5'))
return (hour * 60) * factor;

if (buffer.lengthRemaining() < 2)
return std::nullopt;
auto firstMinuteCharacter = *buffer;
if (!(firstMinuteCharacter >= '0' && firstMinuteCharacter <= '5'))
return std::nullopt;

buffer.advance();
auto secondMinuteCharacter = *buffer;
if (!isASCIIDigit(secondMinuteCharacter))
return std::nullopt;
unsigned minute = (secondMinuteCharacter - '0') + 10 * (firstMinuteCharacter - '0');
ASSERT(minute < 60);
buffer.advance();

return (hour * 60 + minute) * factor;
}

std::optional<int64_t> parseUTCOffsetInMinutes(StringView string)
{
return readCharactersForParsing(string, [](auto buffer) -> std::optional<int64_t> {
auto result = parseUTCOffsetInMinutes(buffer);
if (!buffer.atEnd())
return std::nullopt;
return result;
});
}

template<typename CharacterType>
static bool canBeCalendar(const StringParsingBuffer<CharacterType>& buffer)
{
Expand Down
1 change: 1 addition & 0 deletions Source/JavaScriptCore/runtime/ISO8601.h
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ struct CalendarRecord {
std::optional<TimeZoneID> parseTimeZoneName(StringView);
std::optional<Duration> parseDuration(StringView);
std::optional<int64_t> parseTimeZoneNumericUTCOffset(StringView);
std::optional<int64_t> parseUTCOffsetInMinutes(StringView);
enum class ValidateTimeZoneID : bool { No, Yes };
std::optional<std::tuple<PlainTime, std::optional<TimeZoneRecord>>> parseTime(StringView);
std::optional<std::tuple<PlainTime, std::optional<TimeZoneRecord>, std::optional<CalendarRecord>>> parseCalendarTime(StringView);
Expand Down
27 changes: 20 additions & 7 deletions Source/JavaScriptCore/runtime/IntlDateTimeFormat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "config.h"
#include "IntlDateTimeFormat.h"

#include "ISO8601.h"
#include "IntlCache.h"
#include "IntlObjectInlines.h"
#include "JSBoundFunction.h"
Expand Down Expand Up @@ -734,17 +735,29 @@ void IntlDateTimeFormat::initializeDateTimeFormat(JSGlobalObject* globalObject,
RETURN_IF_EXCEPTION(scope, void());
}
String tz;
String timeZoneForICU;
if (!tzValue.isUndefined()) {
String originalTz = tzValue.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, void());
tz = canonicalizeTimeZoneName(originalTz);
if (tz.isNull()) {
throwRangeError(globalObject, scope, "invalid time zone: "_s + originalTz);
return;
if (auto minutesValue = ISO8601::parseUTCOffsetInMinutes(originalTz)) {
int64_t minutes = minutesValue.value();
int64_t absMinutes = std::abs(minutes);
tz = makeString(minutes < 0 ? '-' : '+', pad('0', 2, absMinutes / 60), ':', pad('0', 2, absMinutes % 60));
timeZoneForICU = makeString("GMT", minutes < 0 ? '-' : '+', pad('0', 2, absMinutes / 60), pad('0', 2, absMinutes % 60));
} else {
tz = canonicalizeTimeZoneName(originalTz);
if (tz.isNull()) {
throwRangeError(globalObject, scope, "invalid time zone: "_s + originalTz);
return;
}
}
} else
tz = vm.dateCache.defaultTimeZone();
m_timeZone = tz;
if (!timeZoneForICU.isNull())
m_timeZoneForICU = WTFMove(timeZoneForICU);
else
m_timeZoneForICU = tz;

Weekday weekday = intlOption<Weekday>(globalObject, options, vm.propertyNames->weekday, { { "narrow"_s, Weekday::Narrow }, { "short"_s, Weekday::Short }, { "long"_s, Weekday::Long } }, "weekday must be \"narrow\", \"short\", or \"long\""_s, Weekday::None);
RETURN_IF_EXCEPTION(scope, void());
Expand Down Expand Up @@ -830,7 +843,7 @@ void IntlDateTimeFormat::initializeDateTimeFormat(JSGlobalObject* globalObject,
// First, we create UDateFormat via dateStyle and timeStyle. And then convert it to pattern string.
// After updating this pattern string with hourCycle, we create a final UDateFormat with the updated pattern string.
UErrorCode status = U_ZERO_ERROR;
StringView timeZoneView(m_timeZone);
StringView timeZoneView(m_timeZoneForICU);
auto dateFormatFromStyle = std::unique_ptr<UDateFormat, UDateFormatDeleter>(udat_open(parseUDateFormatStyle(m_timeStyle), parseUDateFormatStyle(m_dateStyle), dataLocaleWithExtensions.data(), timeZoneView.upconvertedCharacters(), timeZoneView.length(), nullptr, -1, &status));
if (U_FAILURE(status)) {
throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
Expand Down Expand Up @@ -930,7 +943,7 @@ void IntlDateTimeFormat::initializeDateTimeFormat(JSGlobalObject* globalObject,
dataLogLnIf(IntlDateTimeFormatInternal::verbose, "locale:(", m_locale, "),dataLocale:(", dataLocaleWithExtensions, "),pattern:(", pattern, ")");

UErrorCode status = U_ZERO_ERROR;
StringView timeZoneView(m_timeZone);
StringView timeZoneView(m_timeZoneForICU);
m_dateFormat = std::unique_ptr<UDateFormat, UDateFormatDeleter>(udat_open(UDAT_PATTERN, UDAT_PATTERN, dataLocaleWithExtensions.data(), timeZoneView.upconvertedCharacters(), timeZoneView.length(), pattern.upconvertedCharacters(), pattern.length(), &status));
if (U_FAILURE(status)) {
throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
Expand Down Expand Up @@ -1419,7 +1432,7 @@ UDateIntervalFormat* IntlDateTimeFormat::createDateIntervalFormatIfNecessary(JSG
CString dataLocaleWithExtensions = localeBuilder.toString().utf8();

UErrorCode status = U_ZERO_ERROR;
StringView timeZoneView(m_timeZone);
StringView timeZoneView(m_timeZoneForICU);
m_dateIntervalFormat = std::unique_ptr<UDateIntervalFormat, UDateIntervalFormatDeleter>(udtitvfmt_open(dataLocaleWithExtensions.data(), skeleton.data(), skeleton.size(), timeZoneView.upconvertedCharacters(), timeZoneView.length(), &status));
if (U_FAILURE(status)) {
throwTypeError(globalObject, scope, "failed to initialize DateIntervalFormat"_s);
Expand Down
1 change: 1 addition & 0 deletions Source/JavaScriptCore/runtime/IntlDateTimeFormat.h
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ class IntlDateTimeFormat final : public JSNonFinalObject {
String m_calendar;
String m_numberingSystem;
String m_timeZone;
String m_timeZoneForICU;
HourCycle m_hourCycle { HourCycle::None };
Weekday m_weekday { Weekday::None };
Era m_era { Era::None };
Expand Down

0 comments on commit b999ffe

Please sign in to comment.