1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 18:17:44 +00:00

LibJS/Temporal: Allow annotations after YYYY-MM and MM-DD

This is a normative change in the Temporal spec.

See: 160e836
This commit is contained in:
Luke Wilde 2023-02-11 01:05:28 +00:00 committed by Linus Groh
parent 421b1eee49
commit 588dae8aa6
8 changed files with 290 additions and 136 deletions

View file

@ -271,6 +271,8 @@
M(TemporalInvalidYearMonthStringUTCDesignator, "Invalid year month string '{}': must not contain a UTC designator") \
M(TemporalInvalidZonedDateTimeOffset, "Invalid offset for the provided date and time in the current time zone") \
M(TemporalInvalidZonedDateTimeString, "Invalid zoned date time string '{}'") \
M(TemporalOnlyISO8601WithMonthDayString, "MM-DD string format can only be used with the iso8601 calendar") \
M(TemporalOnlyISO8601WithYearMonthString, "YYYY-MM string format can only be used with the iso8601 calendar") \
M(TemporalMissingOptionsObject, "Required options object is missing or undefined") \
M(TemporalMissingStartingPoint, "A starting point is required for balancing {}") \
M(TemporalMissingUnits, "One or both of smallestUnit or largestUnit is required") \

View file

@ -1187,24 +1187,60 @@ ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, StringView iso_string
// 1. Let parseResult be empty.
Optional<ParseResult> parse_result;
static constexpr auto productions = AK::Array {
static constexpr auto productions_valid_with_any_calendar = AK::Array {
Production::TemporalDateTimeString,
Production::TemporalInstantString,
Production::TemporalMonthDayString,
Production::TemporalTimeString,
Production::TemporalYearMonthString,
Production::TemporalZonedDateTimeString,
};
// 2. For each nonterminal goal of « TemporalDateTimeString, TemporalInstantString, TemporalMonthDayString, TemporalTimeString, TemporalYearMonthString, TemporalZonedDateTimeString », do
for (auto goal : productions) {
// 2. For each nonterminal goal of « TemporalDateTimeString, TemporalInstantString, TemporalTimeString, TemporalZonedDateTimeString », do
for (auto goal : productions_valid_with_any_calendar) {
// a. If parseResult is not a Parse Node, set parseResult to ParseText(StringToCodePoints(isoString), goal).
parse_result = parse_iso8601(goal, iso_string);
if (parse_result.has_value())
break;
}
// 3. If parseResult is not a Parse Node, throw a RangeError exception.
static constexpr auto productions_valid_only_with_iso8601_calendar = AK::Array {
Production::TemporalMonthDayString,
Production::TemporalYearMonthString,
};
// 3. For each nonterminal goal of « TemporalMonthDayString, TemporalYearMonthString », do
for (auto goal : productions_valid_only_with_iso8601_calendar) {
// a. If parseResult is not a Parse Node, then
if (!parse_result.has_value()) {
// i. Set parseResult to ParseText(StringToCodePoints(isoString), goal).
parse_result = parse_iso8601(goal, iso_string);
// NOTE: This is not done in parse_iso_date_time(VM, ParseResult) below because MonthDay and YearMonth must re-parse their strings,
// as the string could actually be a superset string above in `productions_valid_with_any_calendar` and thus not hit this code path at all.
// All other users of parse_iso_date_time(VM, ParseResult) pass in a ParseResult resulting from a production in `productions_valid_with_any_calendar`,
// and thus cannot hit this code path as they would first parse in step 2 and not step 3.
// ii. If parseResult is a Parse Node, then
if (parse_result.has_value()) {
// 1. For each Annotation Parse Node annotation contained within parseResult, do
for (auto const& annotation : parse_result->annotations) {
// a. Let key be the source text matched by the AnnotationKey Parse Node contained within annotation.
auto const& key = annotation.key;
// b. Let value be the source text matched by the AnnotationValue Parse Node contained within annotation.
auto const& value = annotation.value;
// c. If CodePointsToString(key) is "u-ca" and the ASCII-lowercase of CodePointsToString(value) is not "iso8601", throw a RangeError exception.
if (key == "u-ca"sv && value.to_lowercase_string() != "iso8601"sv) {
if (goal == Production::TemporalMonthDayString)
return vm.throw_completion<RangeError>(ErrorType::TemporalOnlyISO8601WithMonthDayString);
else
return vm.throw_completion<RangeError>(ErrorType::TemporalOnlyISO8601WithYearMonthString);
}
}
}
}
}
// 4. If parseResult is not a Parse Node, throw a RangeError exception.
if (!parse_result.has_value())
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidISODateTime);
@ -1214,7 +1250,8 @@ ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, StringView iso_string
// 13.28 ParseISODateTime ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parseisodatetime
ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, ParseResult const& parse_result)
{
// 4. Let each of year, month, day, hour, minute, second, and fSeconds be the source text matched by the respective DateYear, DateMonth, DateDay, TimeHour, TimeMinute, TimeSecond, and TimeFraction Parse Node contained within parseResult, or an empty sequence of code points if not present.
// NOTE: Steps 1-4 is handled in parse_iso_date_time(VM, StringView) above.
// 5. Let each of year, month, day, hour, minute, second, and fSeconds be the source text matched by the respective DateYear, DateMonth, DateDay, TimeHour, TimeMinute, TimeSecond, and TimeFraction Parse Node contained within parseResult, or an empty sequence of code points if not present.
auto year = parse_result.date_year;
auto month = parse_result.date_month;
auto day = parse_result.date_day;
@ -1223,7 +1260,7 @@ ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, ParseResult const& pa
auto second = parse_result.time_second;
auto f_seconds = parse_result.time_fraction;
// 5. If the first code point of year is U+2212 (MINUS SIGN), replace the first code point with U+002D (HYPHEN-MINUS).
// 6. If the first code point of year is U+2212 (MINUS SIGN), replace the first code point with U+002D (HYPHEN-MINUS).
Optional<String> normalized_year;
if (year.has_value()) {
normalized_year = year->starts_with("\xE2\x88\x92"sv)
@ -1231,31 +1268,31 @@ ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, ParseResult const& pa
: TRY_OR_THROW_OOM(vm, String::from_utf8(*year));
}
// 6. Let yearMV be ! ToIntegerOrInfinity(CodePointsToString(year)).
// 7. Let yearMV be ! ToIntegerOrInfinity(CodePointsToString(year)).
auto year_mv = *normalized_year.value_or(String::from_utf8_short_string("0"sv)).to_number<i32>();
// 7. If month is empty, then
// 8. If month is empty, then
// a. Let monthMV be 1.
// 8. Else,
// 9. Else,
// a. Let monthMV be ! ToIntegerOrInfinity(CodePointsToString(month)).
auto month_mv = *month.value_or("1"sv).to_uint<u8>();
// 9. If day is empty, then
// 10. If day is empty, then
// a. Let dayMV be 1.
// 10. Else,
// 11. Else,
// a. Let dayMV be ! ToIntegerOrInfinity(CodePointsToString(day)).
auto day_mv = *day.value_or("1"sv).to_uint<u8>();
// 11. Let hourMV be ! ToIntegerOrInfinity(CodePointsToString(hour)).
// 12. Let hourMV be ! ToIntegerOrInfinity(CodePointsToString(hour)).
auto hour_mv = *hour.value_or("0"sv).to_uint<u8>();
// 12. Let minuteMV be ! ToIntegerOrInfinity(CodePointsToString(minute)).
// 13. Let minuteMV be ! ToIntegerOrInfinity(CodePointsToString(minute)).
auto minute_mv = *minute.value_or("0"sv).to_uint<u8>();
// 13. Let secondMV be ! ToIntegerOrInfinity(CodePointsToString(second)).
// 14. Let secondMV be ! ToIntegerOrInfinity(CodePointsToString(second)).
auto second_mv = *second.value_or("0"sv).to_uint<u8>();
// 14. If secondMV is 60, then
// 15. If secondMV is 60, then
if (second_mv == 60) {
// a. Set secondMV to 59.
second_mv = 59;
@ -1265,7 +1302,7 @@ ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, ParseResult const& pa
u16 microsecond_mv;
u16 nanosecond_mv;
// 15. If fSeconds is not empty, then
// 16. If fSeconds is not empty, then
if (f_seconds.has_value()) {
// a. Let fSecondsDigits be the substring of CodePointsToString(fSeconds) from 1.
auto f_seconds_digits = f_seconds->substring_view(1);
@ -1291,7 +1328,7 @@ ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, ParseResult const& pa
// h. Let nanosecondMV be ! ToIntegerOrInfinity(nanosecond).
nanosecond_mv = *nanosecond.to_number<u16>();
}
// 16. Else,
// 17. Else,
else {
// a. Let millisecondMV be 0.
millisecond_mv = 0;
@ -1303,18 +1340,18 @@ ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, ParseResult const& pa
nanosecond_mv = 0;
}
// 17. If IsValidISODate(yearMV, monthMV, dayMV) is false, throw a RangeError exception.
// 18. If IsValidISODate(yearMV, monthMV, dayMV) is false, throw a RangeError exception.
if (!is_valid_iso_date(year_mv, month_mv, day_mv))
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidISODate);
// 18. If IsValidTime(hourMV, minuteMV, secondMV, millisecondMV, microsecondMV, nanosecondMV) is false, throw a RangeError exception.
// 19. If IsValidTime(hourMV, minuteMV, secondMV, millisecondMV, microsecondMV, nanosecondMV) is false, throw a RangeError exception.
if (!is_valid_time(hour_mv, minute_mv, second_mv, millisecond_mv, microsecond_mv, nanosecond_mv))
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidTime);
// 19. Let timeZoneResult be the Record { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: undefined }.
// 20. Let timeZoneResult be the Record { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: undefined }.
auto time_zone_result = TemporalTimeZone { .z = false, .offset_string = {}, .name = {} };
// 20. If parseResult contains a TimeZoneIdentifier Parse Node, then
// 21. If parseResult contains a TimeZoneIdentifier Parse Node, then
if (parse_result.time_zone_identifier.has_value()) {
// a. Let name be the source text matched by the TimeZoneIdentifier Parse Node contained within parseResult.
auto name = parse_result.time_zone_identifier;
@ -1323,12 +1360,12 @@ ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, ParseResult const& pa
time_zone_result.name = TRY_OR_THROW_OOM(vm, String::from_utf8(*name));
}
// 21. If parseResult contains a UTCDesignator Parse Node, then
// 22. If parseResult contains a UTCDesignator Parse Node, then
if (parse_result.utc_designator.has_value()) {
// a. Set timeZoneResult.[[Z]] to true.
time_zone_result.z = true;
}
// 22. Else,
// 23. Else,
else {
// a. If parseResult contains a TimeZoneNumericUTCOffset Parse Node, then
if (parse_result.time_zone_numeric_utc_offset.has_value()) {
@ -1343,7 +1380,7 @@ ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, ParseResult const& pa
// 23. Let calendar be undefined.
Optional<String> calendar;
// 24. For each Annotation Parse Node annotation contained within parseResult, do
// 25. For each Annotation Parse Node annotation contained within parseResult, do
for (auto const& annotation : parse_result.annotations) {
// a. Let key be the source text matched by the AnnotationKey Parse Node contained within annotation.
auto const& key = annotation.key;
@ -1367,7 +1404,7 @@ ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, ParseResult const& pa
}
}
// 25. Return the Record { [[Year]]: yearMV, [[Month]]: monthMV, [[Day]]: dayMV, [[Hour]]: hourMV, [[Minute]]: minuteMV, [[Second]]: secondMV, [[Millisecond]]: millisecondMV, [[Microsecond]]: microsecondMV, [[Nanosecond]]: nanosecondMV, [[TimeZone]]: timeZoneResult, [[Calendar]]: calendar }.
// 26. Return the Record { [[Year]]: yearMV, [[Month]]: monthMV, [[Day]]: dayMV, [[Hour]]: hourMV, [[Minute]]: minuteMV, [[Second]]: secondMV, [[Millisecond]]: millisecondMV, [[Microsecond]]: microsecondMV, [[Nanosecond]]: nanosecondMV, [[TimeZone]]: timeZoneResult, [[Calendar]]: calendar }.
return ISODateTime { .year = year_mv, .month = month_mv, .day = day_mv, .hour = hour_mv, .minute = minute_mv, .second = second_mv, .millisecond = millisecond_mv, .microsecond = microsecond_mv, .nanosecond = nanosecond_mv, .time_zone = move(time_zone_result), .calendar = move(calendar) };
}
@ -1621,7 +1658,9 @@ ThrowCompletionOr<TemporalMonthDay> parse_temporal_month_day_string(VM& vm, Stri
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidMonthDayStringUTCDesignator, iso_string);
// 4. Let result be ? ParseISODateTime(isoString).
auto result = TRY(parse_iso_date_time(vm, *parse_result));
// NOTE: We must re-parse the string, as MonthDay strings with non-iso8601 calendars are invalid and will cause parse_iso_date_time to throw.
// However, the string could be "2022-12-29[u-ca=gregorian]" for example, which is not a MonthDay string but instead a DateTime string and thus should not throw.
auto result = TRY(parse_iso_date_time(vm, iso_string));
// 5. Let year be result.[[Year]].
Optional<i32> year = result.year;
@ -1646,8 +1685,8 @@ ThrowCompletionOr<ISODateTime> parse_temporal_relative_to_string(VM& vm, StringV
if (!parse_result.has_value())
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidDateTimeString, iso_string);
// 3. If parseResult contains a UTCDesignator ParseNode but no TimeZoneBracketedAnnotation Parse Node, throw a RangeError exception.
if (parse_result->utc_designator.has_value() && !parse_result->time_zone_bracketed_annotation.has_value())
// 3. If parseResult contains a UTCDesignator ParseNode but no TimeZoneAnnotation Parse Node, throw a RangeError exception.
if (parse_result->utc_designator.has_value() && !parse_result->time_zone_annotation.has_value())
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidRelativeToStringUTCDesignatorWithoutBracketedTimeZone, iso_string);
// 4. Return ? ParseISODateTime(isoString).
@ -1716,7 +1755,9 @@ ThrowCompletionOr<TemporalYearMonth> parse_temporal_year_month_string(VM& vm, St
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidYearMonthStringUTCDesignator, iso_string);
// 4. Let result be ? ParseISODateTime(isoString).
auto result = TRY(parse_iso_date_time(vm, *parse_result));
// NOTE: We must re-parse the string, as YearMonth strings with non-iso8601 calendars are invalid and will cause parse_iso_date_time to throw.
// However, the string could be "2022-12-29[u-ca=invalid]" for example, which is not a YearMonth string but instead a DateTime string and thus should not throw.
auto result = TRY(parse_iso_date_time(vm, iso_string));
// 5. Return the Record { [[Year]]: result.[[Year]], [[Month]]: result.[[Month]], [[Day]]: result.[[Day]], [[Calendar]]: result.[[Calendar]] }.
return TemporalYearMonth { .year = result.year, .month = result.month, .day = result.day, .calendar = move(result.calendar) };

View file

@ -826,10 +826,10 @@ bool ISO8601Parser::parse_time_zone_identifier()
return true;
}
// https://tc39.es/proposal-temporal/#prod-TimeZoneBracketedAnnotation
bool ISO8601Parser::parse_time_zone_bracketed_annotation()
// https://tc39.es/proposal-temporal/#prod-TimeZoneAnnotation
bool ISO8601Parser::parse_time_zone_annotation()
{
// TimeZoneBracketedAnnotation :
// TimeZoneAnnotation :
// [ AnnotationCriticalFlag[opt] TimeZoneIdentifier ]
StateTransaction transaction { *this };
if (!m_state.lexer.consume_specific('['))
@ -839,48 +839,7 @@ bool ISO8601Parser::parse_time_zone_bracketed_annotation()
return false;
if (!m_state.lexer.consume_specific(']'))
return false;
m_state.parse_result.time_zone_bracketed_annotation = transaction.parsed_string_view();
transaction.commit();
return true;
}
// https://tc39.es/proposal-temporal/#prod-TimeZoneOffsetRequired
bool ISO8601Parser::parse_time_zone_offset_required()
{
// TimeZoneOffsetRequired :
// TimeZoneUTCOffset TimeZoneBracketedAnnotation[opt]
StateTransaction transaction { *this };
if (!parse_time_zone_utc_offset())
return false;
(void)parse_time_zone_bracketed_annotation();
transaction.commit();
return true;
}
// https://tc39.es/proposal-temporal/#prod-TimeZoneNameRequired
bool ISO8601Parser::parse_time_zone_name_required()
{
// TimeZoneNameRequired :
// TimeZoneUTCOffset[opt] TimeZoneBracketedAnnotation
StateTransaction transaction { *this };
(void)parse_time_zone_utc_offset();
if (!parse_time_zone_bracketed_annotation())
return false;
transaction.commit();
return true;
}
// https://tc39.es/proposal-temporal/#prod-TimeZone
bool ISO8601Parser::parse_time_zone()
{
// TimeZone :
// TimeZoneUTCOffset TimeZoneBracketedAnnotation[opt]
// TimeZoneBracketedAnnotation
StateTransaction transaction { *this };
if (parse_time_zone_utc_offset())
(void)parse_time_zone_bracketed_annotation();
else if (!parse_time_zone_bracketed_annotation())
return false;
m_state.parse_result.time_zone_annotation = transaction.parsed_string_view();
transaction.commit();
return true;
}
@ -1073,11 +1032,11 @@ bool ISO8601Parser::parse_time_spec()
return true;
}
// https://tc39.es/proposal-temporal/#prod-TimeSpecWithOptionalTimeZoneNotAmbiguous
bool ISO8601Parser::parse_time_spec_with_optional_time_zone_not_ambiguous()
// https://tc39.es/proposal-temporal/#prod-TimeSpecWithOptionalOffsetNotAmbiguous
bool ISO8601Parser::parse_time_spec_with_optional_offset_not_ambiguous()
{
// TimeSpecWithOptionalTimeZoneNotAmbiguous :
// TimeSpec TimeZone[opt] but not one of ValidMonthDay or DateSpecYearMonth
// TimeSpecWithOptionalOffsetNotAmbiguous :
// TimeSpec TimeZoneUTCOffset[opt] but not one of ValidMonthDay or DateSpecYearMonth
{
StateTransaction transaction { *this };
if (parse_valid_month_day() || parse_date_spec_year_month())
@ -1086,21 +1045,7 @@ bool ISO8601Parser::parse_time_spec_with_optional_time_zone_not_ambiguous()
StateTransaction transaction { *this };
if (!parse_time_spec())
return false;
(void)parse_time_zone();
transaction.commit();
return true;
}
// https://tc39.es/proposal-temporal/#prod-TimeSpecSeparator
bool ISO8601Parser::parse_time_spec_separator()
{
// TimeSpecSeparator :
// DateTimeSeparator TimeSpec
StateTransaction transaction { *this };
if (!parse_date_time_separator())
return false;
if (!parse_time_spec())
return false;
(void)parse_time_zone_utc_offset();
transaction.commit();
return true;
}
@ -1109,11 +1054,17 @@ bool ISO8601Parser::parse_time_spec_separator()
bool ISO8601Parser::parse_date_time()
{
// DateTime :
// Date TimeSpecSeparator[opt] TimeZone[opt]
// Date
// Date DateTimeSeparator TimeSpec TimeZoneUTCOffset[opt]
StateTransaction transaction { *this };
if (!parse_date())
return false;
(void)parse_time_spec_separator();
(void)parse_time_zone();
if (parse_date_time_separator()) {
if (!parse_time_spec())
return false;
(void)parse_time_zone_utc_offset();
}
transaction.commit();
return true;
}
@ -1121,20 +1072,22 @@ bool ISO8601Parser::parse_date_time()
bool ISO8601Parser::parse_annotated_time()
{
// AnnotatedTime :
// TimeDesignator TimeSpec TimeZone[opt] Annotations[opt]
// TimeSpecWithOptionalTimeZoneNotAmbiguous Annotations[opt]
// TimeDesignator TimeSpec TimeZoneUTCOffset[opt] TimeZoneAnnotation[opt] Annotations[opt]
// TimeSpecWithOptionalOffsetNotAmbiguous TimeZoneAnnotation[opt] Annotations[opt]
{
StateTransaction transaction { *this };
if (parse_time_designator() && parse_time_spec()) {
(void)parse_time_zone();
(void)parse_time_zone_utc_offset();
(void)parse_time_zone_annotation();
(void)parse_annotations();
transaction.commit();
return true;
}
}
StateTransaction transaction { *this };
if (!parse_time_spec_with_optional_time_zone_not_ambiguous())
if (!parse_time_spec_with_optional_offset_not_ambiguous())
return false;
(void)parse_time_zone_annotation();
(void)parse_annotations();
transaction.commit();
return true;
@ -1144,9 +1097,10 @@ bool ISO8601Parser::parse_annotated_time()
bool ISO8601Parser::parse_annotated_date_time()
{
// AnnotatedDateTime :
// DateTime Annotations[opt]
// DateTime TimeZoneAnnotation[opt] Annotations[opt]
if (!parse_date_time())
return false;
(void)parse_time_zone_annotation();
(void)parse_annotations();
return true;
}
@ -1155,18 +1109,45 @@ bool ISO8601Parser::parse_annotated_date_time()
bool ISO8601Parser::parse_annotated_date_time_time_required()
{
// AnnotatedDateTimeTimeRequired :
// Date TimeSpecSeparator TimeZone[opt] Annotations[opt]
// Date DateTimeSeparator TimeSpec TimeZoneUTCOffset[opt] TimeZoneAnnotation[opt] Annotations[opt]
StateTransaction transaction { *this };
if (!parse_date())
return false;
if (!parse_time_spec_separator())
if (!parse_date_time_separator())
return false;
(void)parse_time_zone();
if (!parse_time_spec())
return false;
(void)parse_time_zone_utc_offset();
(void)parse_time_zone_annotation();
(void)parse_annotations();
transaction.commit();
return true;
}
// https://tc39.es/proposal-temporal/#prod-AnnotatedYearMonth
bool ISO8601Parser::parse_annotated_year_month()
{
// AnnotatedYearMonth :
// DateSpecYearMonth TimeZoneAnnotation[opt] Annotations[opt]
if (!parse_date_spec_year_month())
return false;
(void)parse_time_zone_annotation();
(void)parse_annotations();
return true;
}
// https://tc39.es/proposal-temporal/#prod-AnnotatedMonthDay
bool ISO8601Parser::parse_annotated_month_day()
{
// AnnotatedMonthDay :
// DateSpecMonthDay TimeZoneAnnotation[opt] Annotations[opt]
if (!parse_date_spec_month_day())
return false;
(void)parse_time_zone_annotation();
(void)parse_annotations();
return true;
}
// https://tc39.es/proposal-temporal/#prod-DurationWholeSeconds
bool ISO8601Parser::parse_duration_whole_seconds()
{
@ -1470,13 +1451,17 @@ bool ISO8601Parser::parse_duration()
bool ISO8601Parser::parse_temporal_instant_string()
{
// TemporalInstantString :
// Date TimeSpecSeparator[opt] TimeZoneOffsetRequired Annotations[opt]
// Date DateTimeSeparator TimeSpec TimeZoneUTCOffset TimeZoneAnnotation[opt] Annotations[opt]
StateTransaction transaction { *this };
if (!parse_date())
return false;
(void)parse_time_spec_separator();
if (!parse_time_zone_offset_required())
if (!parse_date_time_separator())
return false;
if (!parse_time_spec())
return false;
if (!parse_time_zone_utc_offset())
return false;
(void)parse_time_zone_annotation();
(void)parse_annotations();
transaction.commit();
return true;
@ -1502,12 +1487,12 @@ bool ISO8601Parser::parse_temporal_duration_string()
bool ISO8601Parser::parse_temporal_month_day_string()
{
// TemporalMonthDayString :
// DateSpecMonthDay
// AnnotatedMonthDay
// AnnotatedDateTime
// NOTE: Reverse order here because `DateSpecMonthDay` can be a subset of `AnnotatedDateTime`,
// NOTE: Reverse order here because `AnnotatedMonthDay` can be a subset of `AnnotatedDateTime`,
// so we'd not attempt to parse that but may not exhaust the input string.
return parse_annotated_date_time()
|| parse_date_spec_month_day();
|| parse_annotated_month_day();
}
// https://tc39.es/proposal-temporal/#prod-TemporalTimeString
@ -1526,24 +1511,23 @@ bool ISO8601Parser::parse_temporal_time_string()
bool ISO8601Parser::parse_temporal_year_month_string()
{
// TemporalYearMonthString :
// DateSpecYearMonth
// AnnotatedYearMonth
// AnnotatedDateTime
// NOTE: Reverse order here because `DateSpecYearMonth` can be a subset of `AnnotatedDateTime`,
// NOTE: Reverse order here because `AnnotatedYearMonth` can be a subset of `AnnotatedDateTime`,
// so we'd not attempt to parse that but may not exhaust the input string.
return parse_annotated_date_time()
|| parse_date_spec_year_month();
|| parse_annotated_year_month();
}
// https://tc39.es/proposal-temporal/#prod-TemporalZonedDateTimeString
bool ISO8601Parser::parse_temporal_zoned_date_time_string()
{
// TemporalZonedDateTimeString :
// Date TimeSpecSeparator[opt] TimeZoneNameRequired Annotations[opt]
// DateTime TimeZoneAnnotation Annotations[opt]
StateTransaction transaction { *this };
if (!parse_date())
if (!parse_date_time())
return false;
(void)parse_time_spec_separator();
if (!parse_time_zone_name_required())
if (!parse_time_zone_annotation())
return false;
(void)parse_annotations();
transaction.commit();

View file

@ -29,7 +29,7 @@ struct ParseResult {
Optional<StringView> time_second;
Optional<StringView> time_fraction;
Optional<StringView> utc_designator;
Optional<StringView> time_zone_bracketed_annotation;
Optional<StringView> time_zone_annotation;
Optional<StringView> time_zone_numeric_utc_offset;
Optional<StringView> time_zone_utc_offset_sign;
Optional<StringView> time_zone_utc_offset_hour;
@ -136,10 +136,7 @@ public:
[[nodiscard]] bool parse_time_zone_iana_legacy_name();
[[nodiscard]] bool parse_time_zone_iana_name();
[[nodiscard]] bool parse_time_zone_identifier();
[[nodiscard]] bool parse_time_zone_bracketed_annotation();
[[nodiscard]] bool parse_time_zone_offset_required();
[[nodiscard]] bool parse_time_zone_name_required();
[[nodiscard]] bool parse_time_zone();
[[nodiscard]] bool parse_time_zone_annotation();
[[nodiscard]] bool parse_a_key_leading_char();
[[nodiscard]] bool parse_a_key_char();
[[nodiscard]] bool parse_a_val_char();
@ -151,12 +148,13 @@ public:
[[nodiscard]] bool parse_annotation();
[[nodiscard]] bool parse_annotations();
[[nodiscard]] bool parse_time_spec();
[[nodiscard]] bool parse_time_spec_with_optional_time_zone_not_ambiguous();
[[nodiscard]] bool parse_time_spec_separator();
[[nodiscard]] bool parse_time_spec_with_optional_offset_not_ambiguous();
[[nodiscard]] bool parse_date_time();
[[nodiscard]] bool parse_annotated_time();
[[nodiscard]] bool parse_annotated_date_time();
[[nodiscard]] bool parse_annotated_date_time_time_required();
[[nodiscard]] bool parse_annotated_year_month();
[[nodiscard]] bool parse_annotated_month_day();
[[nodiscard]] bool parse_duration_whole_seconds();
[[nodiscard]] bool parse_duration_seconds_fraction();
[[nodiscard]] bool parse_duration_seconds_part();

View file

@ -54,4 +54,20 @@ describe("errors", () => {
"Got unexpected TimeZone object in conversion to Calendar"
);
});
test("yyyy-mm and mm-dd strings can only use the iso8601 calendar", () => {
// FIXME: The error message doesn't really indicate this is the case.
const values = [
"02-10[u-ca=iso8602]",
"02-10[u-ca=SerenityOS]",
"2023-02[u-ca=iso8602]",
"2023-02[u-ca=SerenityOS]",
];
for (const value of values) {
expect(() => {
Temporal.Calendar.from(value);
}).toThrowWithMessage(RangeError, `Invalid calendar string '${value}'`);
}
});
});

View file

@ -46,6 +46,28 @@ describe("correct behavior", () => {
expect(plainMonthDay.monthCode).toBe("M07");
expect(plainMonthDay.day).toBe(6);
});
test("compares calendar name in month day string in lowercase", () => {
const values = [
"02-10[u-ca=iso8601]",
"02-10[u-ca=isO8601]",
"02-10[u-ca=iSo8601]",
"02-10[u-ca=iSO8601]",
"02-10[u-ca=Iso8601]",
"02-10[u-ca=IsO8601]",
"02-10[u-ca=ISo8601]",
"02-10[u-ca=ISO8601]",
];
for (const value of values) {
expect(() => {
Temporal.PlainMonthDay.from(value);
}).not.toThrowWithMessage(
RangeError,
"MM-DD string format can only be used with the iso8601 calendar"
);
}
});
});
describe("errors", () => {
@ -84,4 +106,33 @@ describe("errors", () => {
Temporal.PlainMonthDay.from("000000-01-01"); // U+2212
}).toThrowWithMessage(RangeError, "Invalid month day string '000000-01-01'");
});
test("can only use iso8601 calendar with month day strings", () => {
expect(() => {
Temporal.PlainMonthDay.from("02-10[u-ca=iso8602]");
}).toThrowWithMessage(
RangeError,
"MM-DD string format can only be used with the iso8601 calendar"
);
expect(() => {
Temporal.PlainMonthDay.from("02-10[u-ca=SerenityOS]");
}).toThrowWithMessage(
RangeError,
"MM-DD string format can only be used with the iso8601 calendar"
);
});
test("doesn't throw non-iso8601 calendar error when using a superset format string such as DateTime", () => {
// NOTE: This will still throw, but only because "serenity" is not a recognised calendar, not because of the string format restriction.
try {
Temporal.PlainMonthDay.from("2023-02-10T22:56[u-ca=serenity]");
} catch (e) {
expect(e).toBeInstanceOf(RangeError);
expect(e.message).not.toBe(
"MM-DD string format can only be used with the iso8601 calendar"
);
expect(e.message).toBe("Invalid calendar identifier 'serenity'");
}
});
});

View file

@ -72,6 +72,28 @@ describe("correct behavior", () => {
expect(plainYearMonth.monthsInYear).toBe(12);
expect(plainYearMonth.inLeapYear).toBeFalse();
});
test("compares calendar name in year month string in lowercase", () => {
const values = [
"2023-02[u-ca=iso8601]",
"2023-02[u-ca=isO8601]",
"2023-02[u-ca=iSo8601]",
"2023-02[u-ca=iSO8601]",
"2023-02[u-ca=Iso8601]",
"2023-02[u-ca=IsO8601]",
"2023-02[u-ca=ISo8601]",
"2023-02[u-ca=ISO8601]",
];
for (const value of values) {
expect(() => {
Temporal.PlainYearMonth.from(value);
}).not.toThrowWithMessage(
RangeError,
"YYYY-MM string format can only be used with the iso8601 calendar"
);
}
});
});
describe("errors", () => {
@ -110,4 +132,33 @@ describe("errors", () => {
Temporal.PlainYearMonth.from("000000-01"); // U+2212
}).toThrowWithMessage(RangeError, "Invalid year month string '000000-01'");
});
test("can only use iso8601 calendar with year month strings", () => {
expect(() => {
Temporal.PlainYearMonth.from("2023-02[u-ca=iso8602]");
}).toThrowWithMessage(
RangeError,
"YYYY-MM string format can only be used with the iso8601 calendar"
);
expect(() => {
Temporal.PlainYearMonth.from("2023-02[u-ca=SerenityOS]");
}).toThrowWithMessage(
RangeError,
"YYYY-MM string format can only be used with the iso8601 calendar"
);
});
test("doesn't throw non-iso8601 calendar error when using a superset format string such as DateTime", () => {
// NOTE: This will still throw, but only because "serenity" is not a recognised calendar, not because of the string format restriction.
try {
Temporal.PlainYearMonth.from("2023-02-10T22:57[u-ca=serenity]");
} catch (e) {
expect(e).toBeInstanceOf(RangeError);
expect(e.message).not.toBe(
"MM-DD string format can only be used with the iso8601 calendar"
);
expect(e.message).toBe("Invalid calendar identifier 'serenity'");
}
});
});

View file

@ -27,19 +27,9 @@ describe("normal behavior", () => {
["Etc/GMT-12", "Etc/GMT-12"],
["Europe/London", "Europe/London"],
["Europe/Isle_of_Man", "Europe/London"],
["1970-01-01+01", "+01:00"],
["1970-01-01+01[-12:34]", "-12:34"],
["1970-01-01T00:00:00+01", "+01:00"],
["1970-01-01T00:00:00.000000000+01", "+01:00"],
["1970-01-01T00:00:00.000000000+01:00:00", "+01:00"],
["1970-01-01+12:34", "+12:34"],
["1970-01-01+12:34:56", "+12:34:56"],
["1970-01-01+12:34:56.789", "+12:34:56.789"],
["1970-01-01+12:34:56.789[-01:00]", "-01:00"],
["1970-01-01-12:34", "-12:34"],
["1970-01-01-12:34:56", "-12:34:56"],
["1970-01-01-12:34:56.789", "-12:34:56.789"],
["1970-01-01-12:34:56.789[+01:00]", "+01:00"],
];
for (const [arg, expected] of values) {
expect(Temporal.TimeZone.from(arg).id).toBe(expected);
@ -75,4 +65,25 @@ describe("errors", () => {
"Got unexpected Calendar object in conversion to TimeZone"
);
});
test("invalid time zone strings", () => {
const values = [
"1970-01-01+01",
"1970-01-01+01[-12:34]",
"1970-01-01+12:34",
"1970-01-01+12:34:56",
"1970-01-01+12:34:56.789",
"1970-01-01+12:34:56.789[-01:00]",
"1970-01-01-12:34",
"1970-01-01-12:34:56",
"1970-01-01-12:34:56.789",
"1970-01-01-12:34:56.789[+01:00]",
];
for (const value of values) {
expect(() => {
Temporal.TimeZone.from(value);
}).toThrowWithMessage(RangeError, "Invalid ISO date time");
}
});
});