From 8af2f3ab2a70b46635bd380557ad6d6853d42d45 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 19 Nov 2024 15:15:13 -0500 Subject: [PATCH 1/6] LibJS: Implement several more ISO8601 productions This started with implementing TemporalMonthDayString. It turns out that the facilities needed to parse that production includes nearly all the helpers to parse each of: TemporalDateTimeString TemporalInstantString TemporalMonthDayString TemporalTimeString TemporalYearMonthString TemporalZonedDateTimeString As most of these invoke the same helpers with different options. So, all 6 of those productions are implemented here. --- Libraries/LibJS/Runtime/Temporal/ISO8601.cpp | 1000 ++++++++++++++++-- Libraries/LibJS/Runtime/Temporal/ISO8601.h | 49 + 2 files changed, 987 insertions(+), 62 deletions(-) diff --git a/Libraries/LibJS/Runtime/Temporal/ISO8601.cpp b/Libraries/LibJS/Runtime/Temporal/ISO8601.cpp index 7526486fc3c..60cbd93b55d 100644 --- a/Libraries/LibJS/Runtime/Temporal/ISO8601.cpp +++ b/Libraries/LibJS/Runtime/Temporal/ISO8601.cpp @@ -7,10 +7,70 @@ #include #include +#include #include +#include namespace JS::Temporal { +enum class Extended { + No, + Yes, +}; + +enum class Separator { + No, + Yes, +}; + +enum class TimeRequired { + No, + Yes, +}; + +enum class ZDesignator { + No, + Yes, +}; + +enum class Zoned { + No, + Yes, +}; + +// 13.30.1 Static Semantics: IsValidMonthDay, https://tc39.es/proposal-temporal/#sec-temporal-iso8601grammar-static-semantics-isvalidmonthday +static bool is_valid_month_day(ParseResult const& result) +{ + // 1. If DateDay is "31" and DateMonth is "02", "04", "06", "09", "11", return false. + if (result.date_day == "31"sv && result.date_month->is_one_of("02"sv, "04"sv, "06"sv, "09"sv, "11"sv)) + return false; + + // 2. If DateMonth is "02" and DateDay is "30", return false. + if (result.date_month == "02"sv && result.date_day == "30"sv) + return false; + + // 3. Return true. + return true; +} + +// 13.30.2 Static Semantics: IsValidDate, https://tc39.es/proposal-temporal/#sec-temporal-iso8601grammar-static-semantics-isvaliddate +static bool is_valid_date(ParseResult const& result) +{ + // 1. If IsValidMonthDay of DateSpec is false, return false. + if (!is_valid_month_day(result)) + return false; + + // 2. Let year be ℝ(StringToNumber(CodePointsToString(DateYear))). + auto year = string_to_number(*result.date_year); + + // 3. If DateMonth is "02" and DateDay is "29" and MathematicalInLeapYear(EpochTimeForYear(year)) = 0, return false. + if (result.date_month == "02"sv && result.date_day == "29"sv && mathematical_in_leap_year(epoch_time_for_year(year)) == 0) + return false; + + // 4. Return true. + return true; +} + // 13.30 ISO 8601 grammar, https://tc39.es/proposal-temporal/#sec-temporal-iso8601grammar class ISO8601Parser { public: @@ -26,23 +86,718 @@ public: [[nodiscard]] GenericLexer const& lexer() const { return m_state.lexer; } [[nodiscard]] ParseResult const& parse_result() const { return m_state.parse_result; } - enum class Separator { - No, - Yes, - }; + // https://tc39.es/proposal-temporal/#prod-TemporalDateTimeString + [[nodiscard]] bool parse_temporal_date_time_string() + { + // TemporalDateTimeString[Zoned] ::: + // AnnotatedDateTime[?Zoned, ~TimeRequired] + return parse_annotated_date_time(Zoned::No, TimeRequired::No); + } + + // https://tc39.es/proposal-temporal/#prod-TemporalDateTimeString + [[nodiscard]] bool parse_temporal_zoned_date_time_string() + { + // TemporalDateTimeString[Zoned] ::: + // AnnotatedDateTime[?Zoned, ~TimeRequired] + return parse_annotated_date_time(Zoned::Yes, TimeRequired::No); + } // https://tc39.es/proposal-temporal/#prod-TemporalDurationString [[nodiscard]] bool parse_temporal_duration_string() { - // TemporalDurationString : + // TemporalDurationString ::: // Duration return parse_duration(); } + // https://tc39.es/proposal-temporal/#prod-TemporalInstantString + [[nodiscard]] bool parse_temporal_instant_string() + { + // TemporalInstantString ::: + // Date DateTimeSeparator Time DateTimeUTCOffset[+Z] TimeZoneAnnotation[opt] Annotations[opt] + if (!parse_date()) + return false; + if (!parse_date_time_separator()) + return false; + if (!parse_time()) + return false; + if (!parse_date_time_utc_offset(ZDesignator::Yes)) + return false; + + (void)parse_time_zone_annotation(); + (void)parse_annotations(); + + return true; + } + + // https://tc39.es/proposal-temporal/#prod-TemporalMonthDayString + [[nodiscard]] bool parse_temporal_month_day_string() + { + // TemporalMonthDayString ::: + // AnnotatedMonthDay + // AnnotatedDateTime[~Zoned, ~TimeRequired] + // NOTE: Reverse order here because `AnnotatedMonthDay` can be a subset of `AnnotatedDateTime`. + return parse_annotated_date_time(Zoned::No, TimeRequired::No) || parse_annotated_month_day(); + } + + // https://tc39.es/proposal-temporal/#prod-TemporalTimeString + [[nodiscard]] bool parse_temporal_time_string() + { + // TemporalTimeString ::: + // AnnotatedTime + // AnnotatedDateTime[~Zoned, +TimeRequired] + // NOTE: Reverse order here because `AnnotatedTime` can be a subset of `AnnotatedDateTime`. + return parse_annotated_date_time(Zoned::No, TimeRequired::Yes) || parse_annotated_time(); + } + + // https://tc39.es/proposal-temporal/#prod-TemporalYearMonthString + [[nodiscard]] bool parse_temporal_year_month_string() + { + // TemporalYearMonthString ::: + // AnnotatedYearMonth + // AnnotatedDateTime[~Zoned, ~TimeRequired] + // NOTE: Reverse order here because `AnnotatedYearMonth` can be a subset of `AnnotatedDateTime`. + return parse_annotated_date_time(Zoned::No, TimeRequired::No) || parse_annotated_year_month(); + } + + // https://tc39.es/proposal-temporal/#prod-AnnotatedDateTime + [[nodiscard]] bool parse_annotated_date_time(Zoned zoned, TimeRequired time_required) + { + // AnnotatedDateTime[Zoned, TimeRequired] ::: + // [~Zoned] DateTime[~Z, ?TimeRequired] TimeZoneAnnotation[opt] Annotations[opt] + // [+Zoned] DateTime[+Z, ?TimeRequired] TimeZoneAnnotation Annotations[opt] + if (!parse_date_time(zoned == Zoned::Yes ? ZDesignator::Yes : ZDesignator::No, time_required)) + return false; + + if (!parse_time_zone_annotation()) { + if (zoned == Zoned::Yes) + return false; + } + + (void)parse_annotations(); + + return true; + } + + // https://tc39.es/proposal-temporal/#prod-AnnotatedMonthDay + [[nodiscard]] bool 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-AnnotatedTime + [[nodiscard]] bool parse_annotated_time() + { + // AnnotatedTime ::: + // TimeDesignator Time DateTimeUTCOffset[~Z][opt] TimeZoneAnnotation[opt] Annotations[opt] + // Time DateTimeUTCOffset[~Z][opt] TimeZoneAnnotation[opt] Annotations[opt] + (void)parse_time_designator(); + + if (!parse_time()) + return false; + + (void)parse_date_time_utc_offset(ZDesignator::No); + (void)parse_time_zone_annotation(); + (void)parse_annotations(); + + return true; + } + + // https://tc39.es/proposal-temporal/#prod-AnnotatedYearMonth + [[nodiscard]] bool 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-DateTime + [[nodiscard]] bool parse_date_time(ZDesignator z_designator, TimeRequired time_required) + { + StateTransaction transaction { *this }; + + // DateTime[Z, TimeRequired] ::: + // [~TimeRequired] Date + // Date DateTimeSeparator Time DateTimeUTCOffset[?Z][opt] + if (!parse_date()) + return false; + + if (parse_date_time_separator()) { + if (!parse_time()) + return false; + + (void)parse_date_time_utc_offset(z_designator); + } else if (time_required == TimeRequired::Yes) { + return false; + } + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-Date + [[nodiscard]] bool parse_date() + { + // Date ::: + // DateSpec[+Extended] + // DateSpec[~Extended] + return parse_date_spec(Extended::Yes) || parse_date_spec(Extended::No); + } + + // https://tc39.es/proposal-temporal/#prod-DateSpec + [[nodiscard]] bool parse_date_spec(Extended extended) + { + StateTransaction transaction { *this }; + + // DateSpec[Extended] ::: + // DateYear DateSeparator[?Extended] DateMonth DateSeparator[?Extended] DateDay + if (!parse_date_year()) + return false; + if (!parse_date_separator(extended)) + return false; + if (!parse_date_month()) + return false; + if (!parse_date_separator(extended)) + return false; + if (!parse_date_day()) + return false; + + // It is a Syntax Error if IsValidDate of DateSpec is false. + if (!is_valid_date(m_state.parse_result)) + return false; + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DateSpecMonthDay + [[nodiscard]] bool parse_date_spec_month_day() + { + StateTransaction transaction { *this }; + + // DateSpecMonthDay ::: + // --[opt] DateMonth DateSeparator[+Extended] DateDay + // --[opt] DateMonth DateSeparator[~Extended] DateDay + (void)m_state.lexer.consume_specific("--"sv); + + if (!parse_date_month()) + return false; + if (!parse_date_separator(Extended::Yes) && !parse_date_separator(Extended::No)) + return false; + if (!parse_date_day()) + return false; + + // It is a Syntax Error if IsValidMonthDay of DateSpecMonthDay is false. + if (!is_valid_month_day(m_state.parse_result)) + return false; + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DateSpecYearMonth + [[nodiscard]] bool parse_date_spec_year_month() + { + StateTransaction transaction { *this }; + + // DateSpecYearMonth ::: + // DateYear DateSeparator[+Extended] DateMonth + // DateYear DateSeparator[~Extended] DateMonth + if (!parse_date_year()) + return false; + if (!parse_date_separator(Extended::Yes) && !parse_date_separator(Extended::No)) + return false; + if (!parse_date_month()) + return false; + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DateYear + [[nodiscard]] bool parse_date_year() + { + StateTransaction transaction { *this }; + + // DateYear ::: + // DecimalDigit DecimalDigit DecimalDigit DecimalDigit + // ASCIISign DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit + size_t digit_count = parse_ascii_sign() ? 6 : 4; + + for (size_t i = 0; i < digit_count; ++i) { + if (!parse_decimal_digit()) + return false; + } + + // It is a Syntax Error if DateYear is "-000000". + if (transaction.parsed_string_view() == "-000000"sv) + return false; + + m_state.parse_result.date_year = transaction.parsed_string_view(); + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DateMonth + [[nodiscard]] bool parse_date_month() + { + StateTransaction transaction { *this }; + + // DateMonth ::: + // 0 NonZeroDigit + // 10 + // 11 + // 12 + if (m_state.lexer.consume_specific('0')) { + if (!parse_non_zero_digit()) + return false; + } else { + auto success = m_state.lexer.consume_specific("10"sv) || m_state.lexer.consume_specific("11"sv) || m_state.lexer.consume_specific("12"sv); + if (!success) + return false; + } + + m_state.parse_result.date_month = transaction.parsed_string_view(); + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DateDay + [[nodiscard]] bool parse_date_day() + { + StateTransaction transaction { *this }; + + // DateDay ::: + // 0 NonZeroDigit + // 1 DecimalDigit + // 2 DecimalDigit + // 30 + // 31 + if (m_state.lexer.consume_specific('0')) { + if (!parse_non_zero_digit()) + return false; + } else if (m_state.lexer.consume_specific('1') || m_state.lexer.consume_specific('2')) { + if (!parse_decimal_digit()) + return false; + } else { + auto success = m_state.lexer.consume_specific("30"sv) || m_state.lexer.consume_specific("31"sv); + if (!success) + return false; + } + + m_state.parse_result.date_day = transaction.parsed_string_view(); + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DateTimeUTCOffset + [[nodiscard]] bool parse_date_time_utc_offset(ZDesignator z_designator) + { + // DateTimeUTCOffset[Z] ::: + // [+Z] UTCDesignator + // UTCOffset[+SubMinutePrecision] + if (z_designator == ZDesignator::Yes) { + if (parse_utc_designator()) + return true; + } + + return parse_utc_offset(SubMinutePrecision::Yes, m_state.parse_result.date_time_offset); + } + + // https://tc39.es/proposal-temporal/#prod-Time + [[nodiscard]] bool parse_time() + { + // Time ::: + // TimeSpec[+Extended] + // TimeSpec[~Extended] + return parse_time_spec(); + } + + // https://tc39.es/proposal-temporal/#prod-TimeSpec + [[nodiscard]] bool parse_time_spec() + { + StateTransaction transaction { *this }; + + auto parse_time_hour = [&]() { + return scoped_parse(m_state.parse_result.time_hour, [&]() { return parse_hour(); }); + }; + auto parse_time_minute = [&]() { + return scoped_parse(m_state.parse_result.time_minute, [&]() { return parse_minute_second(); }); + }; + auto parse_time_fraction = [&]() { + return scoped_parse(m_state.parse_result.time_fraction, [&]() { return parse_temporal_decimal_fraction(); }); + }; + + // TimeSpec[Extended] ::: + // Hour + // Hour TimeSeparator[?Extended] MinuteSecond + // Hour TimeSeparator[?Extended] MinuteSecond TimeSeparator[?Extended] TimeSecond TemporalDecimalFraction[opt] + if (!parse_time_hour()) + return false; + + if (parse_time_separator(Extended::Yes)) { + if (!parse_time_minute()) + return false; + + if (parse_time_separator(Extended::Yes)) { + if (!parse_time_second()) + return false; + + (void)parse_time_fraction(); + } + } else if (parse_time_minute() && parse_time_second()) { + (void)parse_time_fraction(); + } + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-TimeSecond + [[nodiscard]] bool parse_time_second() + { + StateTransaction transaction { *this }; + + // TimeSecond ::: + // MinuteSecond + // 60 + auto success = parse_minute_second() || m_state.lexer.consume_specific("60"sv); + if (!success) + return false; + + m_state.parse_result.time_second = transaction.parsed_string_view(); + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-TimeZoneAnnotation + [[nodiscard]] bool parse_time_zone_annotation() + { + StateTransaction transaction { *this }; + + // TimeZoneAnnotation ::: + // [ AnnotationCriticalFlag[opt] TimeZoneIdentifier ] + if (!m_state.lexer.consume_specific('[')) + return false; + + (void)parse_annotation_critical_flag(); + if (!parse_time_zone_identifier()) + return false; + + if (!m_state.lexer.consume_specific(']')) + return false; + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-TimeZoneIdentifier + [[nodiscard]] bool parse_time_zone_identifier() + { + StateTransaction transaction { *this }; + + // TimeZoneIdentifier ::: + // UTCOffset[~SubMinutePrecision] + // TimeZoneIANAName + auto success = parse_utc_offset(SubMinutePrecision::No, m_state.parse_result.time_zone_offset) || parse_time_zone_iana_name(); + if (!success) + return false; + + m_state.parse_result.time_zone_identifier = transaction.parsed_string_view(); + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-TimeZoneIANAName + [[nodiscard]] bool parse_time_zone_iana_name() + { + StateTransaction transaction { *this }; + + // TimeZoneIANAName ::: + // TimeZoneIANANameComponent + // TimeZoneIANAName / TimeZoneIANANameComponent + if (!parse_time_zone_iana_name_component()) + return false; + + while (m_state.lexer.consume_specific('/')) { + if (!parse_time_zone_iana_name_component()) + return false; + } + + m_state.parse_result.time_zone_iana_name = transaction.parsed_string_view(); + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-TimeZoneIANANameComponent + [[nodiscard]] bool parse_time_zone_iana_name_component() + { + // TimeZoneIANANameComponent ::: + // TZLeadingChar + // TimeZoneIANANameComponent TZChar + if (!parse_tz_leading_char()) + return false; + while (parse_tz_leading_char()) + ; + while (parse_tz_char()) + ; + + return true; + } + + // https://tc39.es/proposal-temporal/#prod-TZLeadingChar + [[nodiscard]] bool parse_tz_leading_char() + { + // TZLeadingChar ::: + // Alpha + // . + // _ + return parse_alpha() || m_state.lexer.consume_specific('.') || m_state.lexer.consume_specific('_'); + } + + // https://tc39.es/proposal-temporal/#prod-TZChar + [[nodiscard]] bool parse_tz_char() + { + // TZChar ::: + // TZLeadingChar + // DecimalDigit + // - + // + + return parse_tz_leading_char() || parse_decimal_digit() || m_state.lexer.consume_specific('.') || m_state.lexer.consume_specific('+'); + } + + // https://tc39.es/proposal-temporal/#prod-Annotations + [[nodiscard]] bool parse_annotations() + { + // Annotations ::: + // Annotation Annotationsopt + if (!parse_annotation()) + return false; + while (parse_annotation()) + ; + return true; + } + + // https://tc39.es/proposal-temporal/#prod-Annotation + [[nodiscard]] bool parse_annotation() + { + StateTransaction transaction { *this }; + + Optional key; + Optional value; + + // Annotation ::: + // [ AnnotationCriticalFlag[opt] AnnotationKey = AnnotationValue ] + if (!m_state.lexer.consume_specific('[')) + return false; + + auto critical = parse_annotation_critical_flag(); + + if (!scoped_parse(key, [&]() { return parse_annotation_key(); })) + return false; + if (!m_state.lexer.consume_specific('=')) + return false; + if (!scoped_parse(value, [&]() { return parse_annotation_value(); })) + return false; + + if (!m_state.lexer.consume_specific(']')) + return false; + + m_state.parse_result.annotations.empend(critical, *key, *value); + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-AnnotationKey + [[nodiscard]] bool parse_annotation_key() + { + // AnnotationKey ::: + // AKeyLeadingChar + // AnnotationKey AKeyChar + if (!parse_annotation_key_leading_char()) + return false; + while (parse_annotation_key_leading_char()) + ; + while (parse_annotation_key_char()) + ; + + return true; + } + + // https://tc39.es/proposal-temporal/#prod-AKeyLeadingChar + [[nodiscard]] bool parse_annotation_key_leading_char() + { + // AKeyLeadingChar ::: + // LowercaseAlpha + // _ + return parse_lowercase_alpha() || m_state.lexer.consume_specific('_'); + } + + // https://tc39.es/proposal-temporal/#prod-AKeyChar + [[nodiscard]] bool parse_annotation_key_char() + { + // AKeyChar ::: + // AKeyLeadingChar + // DecimalDigit + // - + return parse_annotation_key_leading_char() || parse_decimal_digit() || m_state.lexer.consume_specific('-'); + } + + // https://tc39.es/proposal-temporal/#prod-AnnotationValue + [[nodiscard]] bool parse_annotation_value() + { + // AnnotationValue ::: + // AnnotationValueComponent + // AnnotationValueComponent - AnnotationValue + if (!parse_annotation_value_component()) + return false; + + while (m_state.lexer.consume_specific('-')) { + if (!parse_annotation_value_component()) + return false; + } + + return true; + } + + // https://tc39.es/proposal-temporal/#prod-AnnotationValueComponent + [[nodiscard]] bool parse_annotation_value_component() + { + // AnnotationValueComponent ::: + // Alpha AnnotationValueComponent[opt] + // DecimalDigit AnnotationValueComponent[opt] + auto parse_component = [&]() { return parse_alpha() || parse_decimal_digit(); }; + + if (!parse_component()) + return false; + while (parse_component()) + ; + + return true; + } + + // https://tc39.es/proposal-temporal/#prod-UTCOffset + [[nodiscard]] bool parse_utc_offset(SubMinutePrecision sub_minute_precision, Optional& result) + { + StateTransaction transaction { *this }; + TimeZoneOffset time_zone_offset; + + auto parse_utc_sign = [&]() { + return scoped_parse(time_zone_offset.sign, [&]() { return parse_ascii_sign(); }); + }; + auto parse_utc_hours = [&]() { + return scoped_parse(time_zone_offset.hours, [&]() { return parse_hour(); }); + }; + auto parse_utc_minutes = [&]() { + return scoped_parse(time_zone_offset.minutes, [&]() { return parse_minute_second(); }); + }; + auto parse_utc_seconds = [&]() { + return scoped_parse(time_zone_offset.seconds, [&]() { return parse_minute_second(); }); + }; + auto parse_utc_fraction = [&]() { + return scoped_parse(time_zone_offset.fraction, [&]() { return parse_temporal_decimal_fraction(); }); + }; + + // UTCOffset[SubMinutePrecision] ::: + // ASCIISign Hour + // ASCIISign Hour TimeSeparator[+Extended] MinuteSecond + // ASCIISign Hour TimeSeparator[~Extended] MinuteSecond + // [+SubMinutePrecision] ASCIISign Hour TimeSeparator[+Extended] MinuteSecond TimeSeparator[+Extended] MinuteSecond TemporalDecimalFraction[opt] + // [+SubMinutePrecision] ASCIISign Hour TimeSeparator[~Extended] MinuteSecond TimeSeparator[~Extended] MinuteSecond TemporalDecimalFraction[opt] + if (!parse_utc_sign()) + return false; + if (!parse_utc_hours()) + return false; + + if (parse_time_separator(Extended::Yes)) { + if (!parse_utc_minutes()) + return false; + + if (sub_minute_precision == SubMinutePrecision::Yes && parse_time_separator(Extended::Yes)) { + if (!parse_utc_seconds()) + return false; + + (void)parse_utc_fraction(); + } + } else if (parse_utc_minutes()) { + if (sub_minute_precision == SubMinutePrecision::Yes && parse_utc_seconds()) + (void)parse_utc_fraction(); + } + + time_zone_offset.source_text = transaction.parsed_string_view(); + result = move(time_zone_offset); + + transaction.commit(); + return true; + } + + // https://tc39.es/ecma262/#prod-Hour + [[nodiscard]] bool parse_hour() + { + // Hour ::: + // 0 DecimalDigit + // 1 DecimalDigit + // 20 + // 21 + // 22 + // 23 + if (m_state.lexer.consume_specific('0') || m_state.lexer.consume_specific('1')) { + if (!parse_decimal_digit()) + return false; + } else { + auto success = m_state.lexer.consume_specific("20"sv) + || m_state.lexer.consume_specific("21"sv) + || m_state.lexer.consume_specific("22"sv) + || m_state.lexer.consume_specific("23"sv); + if (!success) + return false; + } + + return true; + } + + // https://tc39.es/ecma262/#prod-MinuteSecond + [[nodiscard]] bool parse_minute_second() + { + // MinuteSecond ::: + // 0 DecimalDigit + // 1 DecimalDigit + // 2 DecimalDigit + // 3 DecimalDigit + // 4 DecimalDigit + // 5 DecimalDigit + auto success = m_state.lexer.consume_specific('0') + || m_state.lexer.consume_specific('1') + || m_state.lexer.consume_specific('2') + || m_state.lexer.consume_specific('3') + || m_state.lexer.consume_specific('4') + || m_state.lexer.consume_specific('5'); + if (!success) + return false; + if (!parse_decimal_digit()) + return false; + + return true; + } + // https://tc39.es/proposal-temporal/#prod-DurationDate [[nodiscard]] bool parse_duration_date() { - // DurationDate : + // DurationDate ::: // DurationYearsPart DurationTime[opt] // DurationMonthsPart DurationTime[opt] // DurationWeeksPart DurationTime[opt] @@ -63,7 +818,7 @@ public: // Duration ::: // ASCIISign[opt] DurationDesignator DurationDate // ASCIISign[opt] DurationDesignator DurationTime - (void)parse_ascii_sign(); + (void)scoped_parse(m_state.parse_result.sign, [&]() { return parse_ascii_sign(); }); if (!parse_duration_designator()) return false; @@ -81,7 +836,7 @@ public: { StateTransaction transaction { *this }; - // DurationYearsPart : + // DurationYearsPart ::: // DecimalDigits[~Sep] YearsDesignator DurationMonthsPart // DecimalDigits[~Sep] YearsDesignator DurationWeeksPart // DecimalDigits[~Sep] YearsDesignator DurationDaysPart[opt] @@ -102,7 +857,7 @@ public: { StateTransaction transaction { *this }; - // DurationMonthsPart : + // DurationMonthsPart ::: // DecimalDigits[~Sep] MonthsDesignator DurationWeeksPart // DecimalDigits[~Sep] MonthsDesignator DurationDaysPart[opt] if (!parse_decimal_digits(Separator::No, m_state.parse_result.duration_months)) @@ -122,7 +877,7 @@ public: { StateTransaction transaction { *this }; - // DurationWeeksPart : + // DurationWeeksPart ::: // DecimalDigits[~Sep] WeeksDesignator DurationDaysPart[opt] if (!parse_decimal_digits(Separator::No, m_state.parse_result.duration_weeks)) return false; @@ -141,7 +896,7 @@ public: { StateTransaction transaction { *this }; - // DurationDaysPart : + // DurationDaysPart ::: // DecimalDigits[~Sep] DaysDesignator if (!parse_decimal_digits(Separator::No, m_state.parse_result.duration_days)) return false; @@ -158,7 +913,7 @@ public: { StateTransaction transaction { *this }; - // DurationTime : + // DurationTime ::: // TimeDesignator DurationHoursPart // TimeDesignator DurationMinutesPart // TimeDesignator DurationSecondsPart @@ -178,14 +933,14 @@ public: { StateTransaction transaction { *this }; - // DurationHoursPart : + // DurationHoursPart ::: // DecimalDigits[~Sep] TemporalDecimalFraction HoursDesignator // DecimalDigits[~Sep] HoursDesignator DurationMinutesPart // DecimalDigits[~Sep] HoursDesignator DurationSecondsPart[opt] if (!parse_decimal_digits(Separator::No, m_state.parse_result.duration_hours)) return false; - auto is_fractional = parse_temporal_decimal_fraction(m_state.parse_result.duration_hours_fraction); + auto is_fractional = scoped_parse(m_state.parse_result.duration_hours_fraction, [&]() { return parse_temporal_decimal_fraction(); }); if (!parse_hours_designator()) return false; @@ -201,13 +956,13 @@ public: { StateTransaction transaction { *this }; - // DurationMinutesPart : + // DurationMinutesPart ::: // DecimalDigits[~Sep] TemporalDecimalFraction MinutesDesignator // DecimalDigits[~Sep] MinutesDesignator DurationSecondsPart[opt] if (!parse_decimal_digits(Separator::No, m_state.parse_result.duration_minutes)) return false; - auto is_fractional = parse_temporal_decimal_fraction(m_state.parse_result.duration_minutes_fraction); + auto is_fractional = scoped_parse(m_state.parse_result.duration_minutes_fraction, [&]() { return parse_temporal_decimal_fraction(); }); if (!parse_minutes_designator()) return false; @@ -223,12 +978,12 @@ public: { StateTransaction transaction { *this }; - // DurationSecondsPart : + // DurationSecondsPart ::: // DecimalDigits[~Sep] TemporalDecimalFraction[opt] SecondsDesignator if (!parse_decimal_digits(Separator::No, m_state.parse_result.duration_seconds)) return false; - (void)parse_temporal_decimal_fraction(m_state.parse_result.duration_seconds_fraction); + (void)scoped_parse(m_state.parse_result.duration_seconds_fraction, [&]() { return parse_temporal_decimal_fraction(); }); if (!parse_seconds_designator()) return false; @@ -237,6 +992,56 @@ public: return true; } + // https://tc39.es/ecma262/#prod-TemporalDecimalFraction + [[nodiscard]] bool parse_temporal_decimal_fraction() + { + // TemporalDecimalFraction ::: + // TemporalDecimalSeparator DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit + if (!parse_temporal_decimal_separator()) + return false; + if (!parse_decimal_digit()) + return false; + + for (size_t i = 0; i < 8; ++i) { + if (!parse_decimal_digit()) + break; + } + + return true; + } + + // https://tc39.es/proposal-temporal/#prod-Alpha + [[nodiscard]] bool parse_alpha() + { + // Alpha ::: one of + // A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z + if (m_state.lexer.next_is(is_ascii_alpha)) { + m_state.lexer.consume(); + return true; + } + return false; + } + + // https://tc39.es/proposal-temporal/#prod-LowercaseAlpha + [[nodiscard]] bool parse_lowercase_alpha() + { + // LowercaseAlpha ::: one of + // a b c d e f g h i j k l m n o p q r s t u v w x y z + if (m_state.lexer.next_is(is_ascii_lower_alpha)) { + m_state.lexer.consume(); + return true; + } + return false; + } + // https://tc39.es/ecma262/#prod-DecimalDigit [[nodiscard]] bool parse_decimal_digit() { @@ -272,53 +1077,66 @@ public: return true; } - // https://tc39.es/ecma262/#prod-TemporalDecimalFraction - [[nodiscard]] bool parse_temporal_decimal_fraction(Optional& result) + // https://tc39.es/ecma262/#prod-NonZeroDigit + [[nodiscard]] bool parse_non_zero_digit() { - StateTransaction transaction { *this }; - - // TemporalDecimalFraction ::: - // TemporalDecimalSeparator DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - if (!parse_temporal_decimal_separator()) - return false; - if (!parse_decimal_digit()) - return false; - - for (size_t i = 0; i < 8; ++i) { - if (!parse_decimal_digit()) - break; + // NonZeroDigit : one of + // 1 2 3 4 5 6 7 8 9 + if (m_state.lexer.next_is(is_ascii_digit) && !m_state.lexer.next_is('0')) { + m_state.lexer.consume(); + return true; } - - result = transaction.parsed_string_view(); - - transaction.commit(); - return true; + return false; } // https://tc39.es/ecma262/#prod-ASCIISign [[nodiscard]] bool parse_ascii_sign() { - StateTransaction transaction { *this }; - // ASCIISign : one of // + - - if (!m_state.lexer.next_is(is_any_of("+-"sv))) - return false; + return m_state.lexer.consume_specific('+') || m_state.lexer.consume_specific('-'); + } - m_state.parse_result.sign = m_state.lexer.consume(); - - transaction.commit(); + // https://tc39.es/proposal-temporal/#prod-DateSeparator + [[nodiscard]] bool parse_date_separator(Extended extended) + { + // DateSeparator[Extended] ::: + // [+Extended] - + // [~Extended] [empty] + if (extended == Extended::Yes) + return m_state.lexer.consume_specific('-'); return true; } + // https://tc39.es/ecma262/#prod-TimeSeparator + [[nodiscard]] bool parse_time_separator(Extended extended) + { + // TimeSeparator[Extended] ::: + // [+Extended] : + // [~Extended] [empty] + if (extended == Extended::Yes) + return m_state.lexer.consume_specific(':'); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-TimeDesignator + [[nodiscard]] bool parse_time_designator() + { + // TimeDesignator : one of + // T t + return m_state.lexer.consume_specific('T') || m_state.lexer.consume_specific('t'); + } + + // https://tc39.es/proposal-temporal/#prod-DateTimeSeparator + [[nodiscard]] bool parse_date_time_separator() + { + // DateTimeSeparator ::: + // + // T + // t + return m_state.lexer.consume_specific(' ') || m_state.lexer.consume_specific('T') || m_state.lexer.consume_specific('t'); + } + // https://tc39.es/ecma262/#prod-TemporalDecimalSeparator [[nodiscard]] bool parse_temporal_decimal_separator() { @@ -335,14 +1153,6 @@ public: return m_state.lexer.consume_specific('P') || m_state.lexer.consume_specific('p'); } - // https://tc39.es/proposal-temporal/#prod-TimeDesignator - [[nodiscard]] bool parse_time_designator() - { - // TimeDesignator : one of - // T t - return m_state.lexer.consume_specific('T') || m_state.lexer.consume_specific('t'); - } - // https://tc39.es/proposal-temporal/#prod-YearsDesignator [[nodiscard]] bool parse_years_designator() { @@ -399,7 +1209,48 @@ public: return m_state.lexer.consume_specific('S') || m_state.lexer.consume_specific('s'); } + // https://tc39.es/proposal-temporal/#prod-UTCDesignator + [[nodiscard]] bool parse_utc_designator() + { + StateTransaction transaction { *this }; + + // UTCDesignator : one of + // Z z + auto success = m_state.lexer.consume_specific('Z') || m_state.lexer.consume_specific('z'); + if (!success) + return false; + + m_state.parse_result.utc_designator = transaction.parsed_string_view(); + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-AnnotationCriticalFlag + [[nodiscard]] bool parse_annotation_critical_flag() + { + // AnnotationCriticalFlag ::: + // ! + return m_state.lexer.consume_specific('!'); + } + private: + template + [[nodiscard]] bool scoped_parse(Optional& storage, Parser&& parser) + { + StateTransaction transaction { *this }; + + if (!parser()) + return false; + + if constexpr (IsSame) + storage = transaction.parsed_string_view()[0]; + else + storage = transaction.parsed_string_view(); + + transaction.commit(); + return true; + } + struct State { GenericLexer lexer; ParseResult parse_result; @@ -436,8 +1287,17 @@ private: State m_state; }; -#define JS_ENUMERATE_ISO8601_PRODUCTION_PARSERS \ - __JS_ENUMERATE(TemporalDurationString, parse_temporal_duration_string) +#define JS_ENUMERATE_ISO8601_PRODUCTION_PARSERS \ + __JS_ENUMERATE(AnnotationValue, parse_annotation_value) \ + __JS_ENUMERATE(DateMonth, parse_date_month) \ + __JS_ENUMERATE(TemporalDateTimeString, parse_temporal_date_time_string) \ + __JS_ENUMERATE(TemporalDurationString, parse_temporal_duration_string) \ + __JS_ENUMERATE(TemporalInstantString, parse_temporal_instant_string) \ + __JS_ENUMERATE(TemporalMonthDayString, parse_temporal_month_day_string) \ + __JS_ENUMERATE(TemporalTimeString, parse_temporal_time_string) \ + __JS_ENUMERATE(TemporalYearMonthString, parse_temporal_year_month_string) \ + __JS_ENUMERATE(TemporalZonedDateTimeString, parse_temporal_zoned_date_time_string) \ + __JS_ENUMERATE(TimeZoneIdentifier, parse_time_zone_identifier) Optional parse_iso8601(Production production, StringView input) { @@ -462,4 +1322,20 @@ Optional parse_iso8601(Production production, StringView input) return parser.parse_result(); } +Optional parse_utc_offset(StringView input, SubMinutePrecision sub_minute_precision) +{ + ISO8601Parser parser { input }; + + Optional utc_offset; + + if (!parser.parse_utc_offset(sub_minute_precision, utc_offset)) + return {}; + + // If we parsed successfully but didn't reach the end, the string doesn't match the given production. + if (!parser.lexer().is_eof()) + return {}; + + return utc_offset; +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/ISO8601.h b/Libraries/LibJS/Runtime/Temporal/ISO8601.h index 0396a8661aa..298732868e9 100644 --- a/Libraries/LibJS/Runtime/Temporal/ISO8601.h +++ b/Libraries/LibJS/Runtime/Temporal/ISO8601.h @@ -9,11 +9,42 @@ #include #include +#include namespace JS::Temporal { +struct Annotation { + bool critical { false }; + StringView key; + StringView value; +}; + +struct TimeZoneOffset { + Optional sign; + Optional hours; + Optional minutes; + Optional seconds; + Optional fraction; + StringView source_text; +}; + struct ParseResult { Optional sign; + + Optional date_year; + Optional date_month; + Optional date_day; + Optional time_hour; + Optional time_minute; + Optional time_second; + Optional time_fraction; + Optional date_time_offset; + + Optional utc_designator; + Optional time_zone_identifier; + Optional time_zone_iana_name; + Optional time_zone_offset; + Optional duration_years; Optional duration_months; Optional duration_weeks; @@ -24,12 +55,30 @@ struct ParseResult { Optional duration_minutes_fraction; Optional duration_seconds; Optional duration_seconds_fraction; + + Vector annotations; }; enum class Production { + AnnotationValue, + DateMonth, + TemporalDateTimeString, TemporalDurationString, + TemporalInstantString, + TemporalMonthDayString, + TemporalTimeString, + TemporalYearMonthString, + TemporalZonedDateTimeString, + TimeZoneIdentifier, }; Optional parse_iso8601(Production, StringView); +enum class SubMinutePrecision { + No, + Yes, +}; + +Optional parse_utc_offset(StringView, SubMinutePrecision); + } From bd6545f93e54573fe96b43fbb382ac586642304f Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 20 Nov 2024 12:59:15 -0500 Subject: [PATCH 2/6] LibJS: Implement the Temporal.PlainMonthDay constructor And the simple Temporal.PlainMonthDay.prototype getters, so that the constructed Temporal.PlainMonthDay may actually be validated. --- Libraries/LibJS/CMakeLists.txt | 9 + Libraries/LibJS/Forward.h | 17 +- Libraries/LibJS/Print.cpp | 12 + Libraries/LibJS/Runtime/ErrorTypes.h | 2 + Libraries/LibJS/Runtime/Intrinsics.cpp | 2 + .../Runtime/Temporal/AbstractOperations.cpp | 479 +++++++++++++- .../Runtime/Temporal/AbstractOperations.h | 72 ++- Libraries/LibJS/Runtime/Temporal/Calendar.cpp | 586 ++++++++++++++++++ Libraries/LibJS/Runtime/Temporal/Calendar.h | 114 ++++ .../LibJS/Runtime/Temporal/DateEquations.cpp | 89 +++ .../LibJS/Runtime/Temporal/DateEquations.h | 24 + .../LibJS/Runtime/Temporal/PlainDate.cpp | 97 +++ Libraries/LibJS/Runtime/Temporal/PlainDate.h | 30 + .../LibJS/Runtime/Temporal/PlainDateTime.cpp | 55 ++ .../LibJS/Runtime/Temporal/PlainDateTime.h | 25 + .../LibJS/Runtime/Temporal/PlainMonthDay.cpp | 144 +++++ .../LibJS/Runtime/Temporal/PlainMonthDay.h | 37 ++ .../Temporal/PlainMonthDayConstructor.cpp | 102 +++ .../Temporal/PlainMonthDayConstructor.h | 33 + .../Temporal/PlainMonthDayPrototype.cpp | 70 +++ .../Runtime/Temporal/PlainMonthDayPrototype.h | 31 + .../LibJS/Runtime/Temporal/PlainTime.cpp | 83 +++ Libraries/LibJS/Runtime/Temporal/PlainTime.h | 30 + Libraries/LibJS/Runtime/Temporal/Temporal.cpp | 2 + Libraries/LibJS/Runtime/Temporal/TimeZone.cpp | 132 ++++ Libraries/LibJS/Runtime/Temporal/TimeZone.h | 28 + .../PlainMonthDay/PlainMonthDay.from.js | 104 ++++ .../Temporal/PlainMonthDay/PlainMonthDay.js | 66 ++ .../PlainMonthDay.prototype.calendarId.js | 14 + .../PlainMonthDay.prototype.day.js | 14 + .../PlainMonthDay.prototype.monthCode.js | 14 + 31 files changed, 2513 insertions(+), 4 deletions(-) create mode 100644 Libraries/LibJS/Runtime/Temporal/Calendar.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/Calendar.h create mode 100644 Libraries/LibJS/Runtime/Temporal/DateEquations.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/DateEquations.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainDate.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainDate.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainDateTime.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainTime.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainTime.h create mode 100644 Libraries/LibJS/Runtime/Temporal/TimeZone.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/TimeZone.h create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.from.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.calendarId.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.day.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.monthCode.js diff --git a/Libraries/LibJS/CMakeLists.txt b/Libraries/LibJS/CMakeLists.txt index ae7f6450493..c010b88da9b 100644 --- a/Libraries/LibJS/CMakeLists.txt +++ b/Libraries/LibJS/CMakeLists.txt @@ -205,12 +205,21 @@ set(SOURCES Runtime/SymbolObject.cpp Runtime/SymbolPrototype.cpp Runtime/Temporal/AbstractOperations.cpp + Runtime/Temporal/Calendar.cpp + Runtime/Temporal/DateEquations.cpp Runtime/Temporal/Duration.cpp Runtime/Temporal/DurationConstructor.cpp Runtime/Temporal/DurationPrototype.cpp Runtime/Temporal/Instant.cpp Runtime/Temporal/ISO8601.cpp + Runtime/Temporal/PlainDate.cpp + Runtime/Temporal/PlainDateTime.cpp + Runtime/Temporal/PlainMonthDay.cpp + Runtime/Temporal/PlainMonthDayConstructor.cpp + Runtime/Temporal/PlainMonthDayPrototype.cpp + Runtime/Temporal/PlainTime.cpp Runtime/Temporal/Temporal.cpp + Runtime/Temporal/TimeZone.cpp Runtime/TypedArray.cpp Runtime/TypedArrayConstructor.cpp Runtime/TypedArrayPrototype.cpp diff --git a/Libraries/LibJS/Forward.h b/Libraries/LibJS/Forward.h index a09c4e16cd6..e73e9051da4 100644 --- a/Libraries/LibJS/Forward.h +++ b/Libraries/LibJS/Forward.h @@ -87,8 +87,9 @@ __JS_ENUMERATE(RelativeTimeFormat, relative_time_format, RelativeTimeFormatPrototype, RelativeTimeFormatConstructor) \ __JS_ENUMERATE(Segmenter, segmenter, SegmenterPrototype, SegmenterConstructor) -#define JS_ENUMERATE_TEMPORAL_OBJECTS \ - __JS_ENUMERATE(Duration, duration, DurationPrototype, DurationConstructor) +#define JS_ENUMERATE_TEMPORAL_OBJECTS \ + __JS_ENUMERATE(Duration, duration, DurationPrototype, DurationConstructor) \ + __JS_ENUMERATE(PlainMonthDay, plain_month_day, PlainMonthDayPrototype, PlainMonthDayConstructor) #define JS_ENUMERATE_BUILTIN_NAMESPACE_OBJECTS \ __JS_ENUMERATE(AtomicsObject, atomics) \ @@ -277,6 +278,18 @@ JS_ENUMERATE_TEMPORAL_OBJECTS #undef __JS_ENUMERATE class Temporal; + +struct CalendarDate; +struct CalendarFields; +struct DateDuration; +struct InternalDuration; +struct ISODate; +struct ISODateTime; +struct ParseResult; +struct PartialDuration; +struct Time; +struct TimeZone; +struct TimeZoneOffset; }; template diff --git a/Libraries/LibJS/Print.cpp b/Libraries/LibJS/Print.cpp index bfda483c4d9..6a894526049 100644 --- a/Libraries/LibJS/Print.cpp +++ b/Libraries/LibJS/Print.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -835,6 +836,15 @@ ErrorOr print_temporal_duration(JS::PrintContext& print_context, JS::Tempo return {}; } +ErrorOr print_temporal_plain_month_day(JS::PrintContext& print_context, JS::Temporal::PlainMonthDay const& plain_month_day, HashTable& seen_objects) +{ + TRY(print_type(print_context, "Temporal.PlainMonthDay"sv)); + TRY(js_out(print_context, " \033[34;1m{:02}-{:02}\033[0m", plain_month_day.iso_date().month, plain_month_day.iso_date().day)); + TRY(js_out(print_context, "\n calendar: ")); + TRY(print_value(print_context, JS::PrimitiveString::create(plain_month_day.vm(), plain_month_day.calendar()), seen_objects)); + return {}; +} + ErrorOr print_boolean_object(JS::PrintContext& print_context, JS::BooleanObject const& boolean_object, HashTable& seen_objects) { TRY(print_type(print_context, "Boolean"sv)); @@ -952,6 +962,8 @@ ErrorOr print_value(JS::PrintContext& print_context, JS::Value value, Hash return print_intl_duration_format(print_context, static_cast(object), seen_objects); if (is(object)) return print_temporal_duration(print_context, static_cast(object), seen_objects); + if (is(object)) + return print_temporal_plain_month_day(print_context, static_cast(object), seen_objects); return print_object(print_context, object, seen_objects); } diff --git a/Libraries/LibJS/Runtime/ErrorTypes.h b/Libraries/LibJS/Runtime/ErrorTypes.h index 83442c3a602..e5301f763f1 100644 --- a/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Libraries/LibJS/Runtime/ErrorTypes.h @@ -249,6 +249,7 @@ M(TemporalInvalidCalendarFunctionResult, "Invalid calendar, {}() function returned {}") \ M(TemporalInvalidCalendarIdentifier, "Invalid calendar identifier '{}'") \ M(TemporalInvalidCalendarString, "Invalid calendar string '{}'") \ + M(TemporalInvalidCriticalAnnotation, "Invalid critical annotation: '{}={}'") \ M(TemporalInvalidDateTimeString, "Invalid date time string '{}'") \ M(TemporalInvalidDateTimeStringUTCDesignator, "Invalid date time string '{}': must not contain a UTC designator") \ M(TemporalInvalidDuration, "Invalid duration") \ @@ -295,6 +296,7 @@ "nanoseconds with the opposite sign") \ M(TemporalNanosecondsConvertedToRemainderOfNanosecondsLongerThanDayLength, "Time zone or calendar ended up with a remainder of " \ "nanoseconds longer than the day length") \ + M(TemporalObjectMustBePartialTemporalObject, "Object must be a partial Temporal object") \ M(TemporalObjectMustHaveOneOf, "Object must have at least one of the following properties: {}") \ M(TemporalObjectMustNotHave, "Object must not have a defined {} property") \ M(TemporalPropertyMustBeFinite, "Property must not be Infinity") \ diff --git a/Libraries/LibJS/Runtime/Intrinsics.cpp b/Libraries/LibJS/Runtime/Intrinsics.cpp index 9d929d3f494..f511939245c 100644 --- a/Libraries/LibJS/Runtime/Intrinsics.cpp +++ b/Libraries/LibJS/Runtime/Intrinsics.cpp @@ -101,6 +101,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 47f32ab13cb..f84c1c9fa76 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -9,11 +9,15 @@ #include #include +#include #include #include +#include #include -#include #include +#include +#include +#include namespace JS::Temporal { @@ -43,6 +47,43 @@ StringView temporal_unit_to_string(Unit unit) return temporal_units[to_underlying(unit)].singular_property_name; } +// 13.1 ISODateToEpochDays ( year, month, date ), https://tc39.es/proposal-temporal/#sec-isodatetoepochdays +double iso_date_to_epoch_days(double year, double month, double date) +{ + // 1. Let resolvedYear be year + floor(month / 12). + // 2. Let resolvedMonth be month modulo 12. + // 3. Find a time t such that EpochTimeToEpochYear(t) = resolvedYear, EpochTimeToMonthInYear(t) = resolvedMonth, and EpochTimeToDate(t) = 1. + // 4. Return EpochTimeToDayNumber(t) + date - 1. + + // EDITOR'S NOTE: This operation corresponds to ECMA-262 operation MakeDay(year, month, date). It calculates the + // result in mathematical values instead of Number values. These two operations would be unified when + // https://github.com/tc39/ecma262/issues/1087 is fixed. + + // Since we don't have a real MV type to work with, let's defer to MakeDay. + return JS::make_day(year, month, date); +} + +// 13.2 EpochDaysToEpochMs ( day, time ), https://tc39.es/proposal-temporal/#sec-epochdaystoepochms +double epoch_days_to_epoch_ms(double day, double time) +{ + // 1. Return day × ℝ(msPerDay) + time. + return day * JS::ms_per_day + time; +} + +// 13.6 GetTemporalOverflowOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporaloverflowoption +ThrowCompletionOr get_temporal_overflow_option(VM& vm, Object const& options) +{ + // 1. Let stringValue be ? GetOption(options, "overflow", STRING, « "constrain", "reject" », "constrain"). + auto string_value = TRY(get_option(vm, options, vm.names.overflow, OptionType::String, { "constrain"sv, "reject"sv }, "constrain"sv)); + + // 2. If stringValue is "constrain", return CONSTRAIN. + if (string_value.as_string().utf8_string() == "constrain"sv) + return Overflow::Constrain; + + // 3. Return REJECT. + return Overflow::Reject; +} + // 13.14 ValidateTemporalRoundingIncrement ( increment, dividend, inclusive ), https://tc39.es/proposal-temporal/#sec-validatetemporalroundingincrement ThrowCompletionOr validate_temporal_rounding_increment(VM& vm, u64 increment, u64 dividend, bool inclusive) { @@ -422,6 +463,27 @@ String format_fractional_seconds(u64 sub_second_nanoseconds, Precision precision return MUST(String::formatted(".{}", fraction_string)); } +// 13.25 FormatTimeString ( hour, minute, second, subSecondNanoseconds, precision [ , style ] ), https://tc39.es/proposal-temporal/#sec-temporal-formattimestring +String format_time_string(u8 hour, u8 minute, u8 second, u16 sub_second_nanoseconds, SecondsStringPrecision::Precision precision, Optional style) +{ + // 1. If style is present and style is UNSEPARATED, let separator be the empty String; otherwise, let separator be ":". + auto separator = style == TimeStyle::Unseparated ? ""sv : ":"sv; + + // 2. Let hh be ToZeroPaddedDecimalString(hour, 2). + // 3. Let mm be ToZeroPaddedDecimalString(minute, 2). + + // 4. If precision is minute, return the string-concatenation of hh, separator, and mm. + if (precision.has()) + return MUST(String::formatted("{:02}{}{:02}", hour, separator, minute)); + + // 5. Let ss be ToZeroPaddedDecimalString(second, 2). + // 6. Let subSecondsPart be FormatFractionalSeconds(subSecondNanoseconds, precision). + auto sub_seconds_part = format_fractional_seconds(sub_second_nanoseconds, precision.downcast()); + + // 7. Return the string-concatenation of hh, separator, mm, separator, ss, and subSecondsPart. + return MUST(String::formatted("{:02}{}{:02}{}{:02}{}", hour, separator, minute, separator, second, sub_seconds_part)); +} + // 13.26 GetUnsignedRoundingMode ( roundingMode, sign ), https://tc39.es/proposal-temporal/#sec-getunsignedroundingmode UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode rounding_mode, Sign sign) { @@ -664,6 +726,260 @@ Crypto::SignedBigInteger round_number_to_increment(Crypto::SignedBigInteger cons return rounded.multiplied_by(increment); } +// 13.33 ParseISODateTime ( isoString, allowedFormats ), https://tc39.es/proposal-temporal/#sec-temporal-parseisodatetime +ThrowCompletionOr parse_iso_date_time(VM& vm, StringView iso_string, ReadonlySpan allowed_formats) +{ + // 1. Let parseResult be EMPTY. + Optional parse_result; + + // 2. Let calendar be EMPTY. + Optional calendar; + + // 3. Let yearAbsent be false. + auto year_absent = false; + + // 4. For each nonterminal goal of allowedFormats, do + for (auto goal : allowed_formats) { + // a. If parseResult is not a Parse Node, then + if (parse_result.has_value()) + break; + + // i. Set parseResult to ParseText(StringToCodePoints(isoString), goal). + parse_result = parse_iso8601(goal, iso_string); + + // ii. If parseResult is a Parse Node, then + if (parse_result.has_value()) { + // 1. Let calendarWasCritical be false. + auto calendar_was_critical = false; + + // 2. 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", then + if (key == "u-ca"sv) { + // i. If calendar is EMPTY, then + if (!calendar.has_value()) { + // i. Set calendar to CodePointsToString(value). + calendar = String::from_utf8_without_validation(value.bytes()); + + // ii. If annotation contains an AnnotationCriticalFlag Parse Node, set calendarWasCritical to true. + if (annotation.critical) + calendar_was_critical = true; + } + // ii. Else, + else { + // i. If annotation contains an AnnotationCriticalFlag Parse Node, or calendarWasCritical is true, + // throw a RangeError exception. + if (annotation.critical || calendar_was_critical) + return vm.throw_completion(ErrorType::TemporalInvalidCriticalAnnotation, key, value); + } + } + // d. Else, + else { + // i. If annotation contains an AnnotationCriticalFlag Parse Node, throw a RangeError exception. + if (annotation.critical) + return vm.throw_completion(ErrorType::TemporalInvalidCriticalAnnotation, key, value); + } + } + + // 3. If goal is TemporalMonthDayString or TemporalYearMonthString, calendar is not EMPTY, and the + // ASCII-lowercase of calendar is not "iso8601", throw a RangeError exception. + if (goal == Production::TemporalMonthDayString || goal == Production::TemporalYearMonthString) { + if (calendar.has_value() && !calendar->equals_ignoring_ascii_case("iso8601"sv)) + return vm.throw_completion(ErrorType::TemporalInvalidCalendarIdentifier, *calendar); + } + + // 4. If goal is TemporalMonthDayString and parseResult does not contain a DateYear Parse Node, then + if (goal == Production::TemporalMonthDayString && !parse_result->date_year.has_value()) { + // a. Assert: goal is the last element of allowedFormats. + VERIFY(goal == allowed_formats.last()); + + // b. Set yearAbsent to true. + year_absent = true; + } + } + } + + // 5. If parseResult is not a Parse Node, throw a RangeError exception. + if (!parse_result.has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidISODateTime); + + // 6. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed values is guaranteed + // to be a sufficiently short string of decimal digits. + + // 7. Let each of year, month, day, hour, minute, second, and fSeconds be the source text matched by the respective + // DateYear, DateMonth, DateDay, the first Hour, the first MinuteSecond, TimeSecond, and the first + // TemporalDecimalFraction Parse Node contained within parseResult, or an empty sequence of code points if not present. + auto year = parse_result->date_year.value_or({}); + auto month = parse_result->date_month.value_or({}); + auto day = parse_result->date_day.value_or({}); + auto hour = parse_result->time_hour.value_or({}); + auto minute = parse_result->time_minute.value_or({}); + auto second = parse_result->time_second.value_or({}); + auto fractional_seconds = parse_result->time_fraction.value_or({}); + + // 8. Let yearMV be ℝ(StringToNumber(CodePointsToString(year))). + auto year_value = string_to_number(year); + + // 9. If month is empty, then + // a. Let monthMV be 1. + // 10. Else, + // a. Let monthMV be ℝ(StringToNumber(CodePointsToString(month))). + auto month_value = month.is_empty() ? 1 : string_to_number(month); + + // 11. If day is empty, then + // a. Let dayMV be 1. + // 12. Else, + // a. Let dayMV be ℝ(StringToNumber(CodePointsToString(day))). + auto day_value = day.is_empty() ? 1 : string_to_number(day); + + // 13. If hour is empty, then + // a. Let hourMV be 0. + // 14. Else, + // a. Let hourMV be ℝ(StringToNumber(CodePointsToString(hour))). + auto hour_value = hour.is_empty() ? 0 : string_to_number(hour); + + // 15. If minute is empty, then + // a. Let minuteMV be 0. + // 16. Else, + // a. Let minuteMV be ℝ(StringToNumber(CodePointsToString(minute))). + auto minute_value = minute.is_empty() ? 0 : string_to_number(minute); + + // 17. If second is empty, then + // a. Let secondMV be 0. + // 18. Else, + // a. Let secondMV be ℝ(StringToNumber(CodePointsToString(second))). + // b. If secondMV = 60, then + // i. Set secondMV to 59. + auto second_value = second.is_empty() ? 0 : min(string_to_number(second), 59.0); + + double millisecond_value = 0; + double microsecond_value = 0; + double nanosecond_value = 0; + + // 19. If fSeconds is not empty, then + if (!fractional_seconds.is_empty()) { + // a. Let fSecondsDigits be the substring of CodePointsToString(fSeconds) from 1. + auto fractional_seconds_digits = fractional_seconds.substring_view(1); + + // b. Let fSecondsDigitsExtended be the string-concatenation of fSecondsDigits and "000000000". + auto fractional_seconds_extended = MUST(String::formatted("{}000000000", fractional_seconds_digits)); + + // c. Let millisecond be the substring of fSecondsDigitsExtended from 0 to 3. + auto millisecond = fractional_seconds_extended.bytes_as_string_view().substring_view(0, 3); + + // d. Let microsecond be the substring of fSecondsDigitsExtended from 3 to 6. + auto microsecond = fractional_seconds_extended.bytes_as_string_view().substring_view(3, 3); + + // e. Let nanosecond be the substring of fSecondsDigitsExtended from 6 to 9. + auto nanosecond = fractional_seconds_extended.bytes_as_string_view().substring_view(6, 3); + + // f. Let millisecondMV be ℝ(StringToNumber(millisecond)). + millisecond_value = string_to_number(millisecond); + + // g. Let microsecondMV be ℝ(StringToNumber(microsecond)). + microsecond_value = string_to_number(microsecond); + + // h. Let nanosecondMV be ℝ(StringToNumber(nanosecond)). + nanosecond_value = string_to_number(nanosecond); + } + // 20. Else, + else { + // a. Let millisecondMV be 0. + // b. Let microsecondMV be 0. + // c. Let nanosecondMV be 0. + } + + // 21. Assert: IsValidISODate(yearMV, monthMV, dayMV) is true. + VERIFY(is_valid_iso_date(year_value, month_value, day_value)); + + Variant time { ParsedISODateTime::StartOfDay {} }; + + // 22. If hour is empty, then + if (hour.is_empty()) { + // a. Let time be START-OF-DAY. + } + // 23. Else, + else { + // a. Let time be CreateTimeRecord(hourMV, minuteMV, secondMV, millisecondMV, microsecondMV, nanosecondMV). + time = create_time_record(hour_value, minute_value, second_value, millisecond_value, microsecond_value, nanosecond_value); + } + + // 24. Let timeZoneResult be ISO String Time Zone Parse Record { [[Z]]: false, [[OffsetString]]: EMPTY, [[TimeZoneAnnotation]]: EMPTY }. + ParsedISOTimeZone time_zone_result; + + // 25. If parseResult contains a TimeZoneIdentifier Parse Node, then + if (parse_result->time_zone_identifier.has_value()) { + // a. Let identifier be the source text matched by the TimeZoneIdentifier Parse Node contained within parseResult. + // b. Set timeZoneResult.[[TimeZoneAnnotation]] to CodePointsToString(identifier). + time_zone_result.time_zone_annotation = String::from_utf8_without_validation(parse_result->time_zone_identifier->bytes()); + } + + // 26. 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_designator = true; + } + // 27. Else if parseResult contains a UTCOffset[+SubMinutePrecision] Parse Node, then + else if (parse_result->date_time_offset.has_value()) { + // a. Let offset be the source text matched by the UTCOffset[+SubMinutePrecision] Parse Node contained within parseResult. + // b. Set timeZoneResult.[[OffsetString]] to CodePointsToString(offset). + time_zone_result.offset_string = String::from_utf8_without_validation(parse_result->date_time_offset->source_text.bytes()); + } + + // 28. If yearAbsent is true, let yearReturn be EMPTY; else let yearReturn be yearMV. + Optional year_return; + if (!year_absent) + year_return = static_cast(year_value); + + // 29. Return ISO Date-Time Parse Record { [[Year]]: yearReturn, [[Month]]: monthMV, [[Day]]: dayMV, [[Time]]: time, [[TimeZone]]: timeZoneResult, [[Calendar]]: calendar }. + return ParsedISODateTime { .year = year_return, .month = static_cast(month_value), .day = static_cast(day_value), .time = move(time), .time_zone = move(time_zone_result), .calendar = move(calendar) }; +} + +// 13.34 ParseTemporalCalendarString ( string ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporalcalendarstring +ThrowCompletionOr parse_temporal_calendar_string(VM& vm, String const& string) +{ + // 1. Let parseResult be Completion(ParseISODateTime(string, « TemporalDateTimeString[+Zoned], TemporalDateTimeString[~Zoned], + // TemporalInstantString, TemporalTimeString, TemporalMonthDayString, TemporalYearMonthString »)). + static constexpr auto productions = to_array({ + Production::TemporalZonedDateTimeString, + Production::TemporalDateTimeString, + Production::TemporalInstantString, + Production::TemporalTimeString, + Production::TemporalMonthDayString, + Production::TemporalYearMonthString, + }); + + auto parse_result = parse_iso_date_time(vm, string, productions); + + // 2. If parseResult is a normal completion, then + if (!parse_result.is_error()) { + // a. Let calendar be parseResult.[[Value]].[[Calendar]]. + auto calendar = parse_result.value().calendar; + + // b. If calendar is empty, return "iso8601". + // c. Else, return calendar. + return calendar.value_or("iso8601"_string); + } + // 3. Else, + else { + // a. Set parseResult to ParseText(StringToCodePoints(string), AnnotationValue). + auto annotation_parse_result = parse_iso8601(Production::AnnotationValue, string); + + // b. If parseResult is a List of errors, throw a RangeError exception. + if (!annotation_parse_result.has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidCalendarString, string); + + // c. Else, return string. + return string; + } +} + // 13.35 ParseTemporalDurationString ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporaldurationstring ThrowCompletionOr> parse_temporal_duration_string(VM& vm, StringView iso_string) { @@ -884,6 +1200,152 @@ ThrowCompletionOr> parse_temporal_duration_string(VM& vm, Stri return TRY(create_temporal_duration(vm, years_value, months_value, weeks_value, days_value, hours_value, factored_minutes_value, factored_seconds_value, factored_milliseconds_value, factored_microseconds_value, factored_nanoseconds_value)); } +// 13.36 ParseTemporalTimeZoneString ( timeZoneString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporaltimezonestring +ThrowCompletionOr parse_temporal_time_zone_string(VM& vm, StringView time_zone_string) +{ + // 1. Let parseResult be ParseText(StringToCodePoints(timeZoneString), TimeZoneIdentifier). + auto parse_result = parse_iso8601(Production::TimeZoneIdentifier, time_zone_string); + + // 2. If parseResult is a Parse Node, then + if (parse_result.has_value()) { + // a. Return ! ParseTimeZoneIdentifier(timeZoneString). + return parse_time_zone_identifier(parse_result.release_value()); + } + + // 3. Let result be ? ParseISODateTime(timeZoneString, « TemporalDateTimeString[+Zoned], TemporalDateTimeString[~Zoned], + // TemporalInstantString, TemporalTimeString, TemporalMonthDayString, TemporalYearMonthString »). + static constexpr auto productions = to_array({ + Production::TemporalZonedDateTimeString, + Production::TemporalDateTimeString, + Production::TemporalInstantString, + Production::TemporalTimeString, + Production::TemporalMonthDayString, + Production::TemporalYearMonthString, + }); + + auto result = TRY(parse_iso_date_time(vm, time_zone_string, productions)); + + // 4. Let timeZoneResult be result.[[TimeZone]]. + auto time_zone_result = move(result.time_zone); + + // 5. If timeZoneResult.[[TimeZoneAnnotation]] is not empty, then + if (time_zone_result.time_zone_annotation.has_value()) { + // a. Return ! ParseTimeZoneIdentifier(timeZoneResult.[[TimeZoneAnnotation]]). + return MUST(parse_time_zone_identifier(vm, *time_zone_result.time_zone_annotation)); + } + + // 6. If timeZoneResult.[[Z]] is true, then + if (time_zone_result.z_designator) { + // a. Return ! ParseTimeZoneIdentifier("UTC"). + return MUST(parse_time_zone_identifier(vm, "UTC"sv)); + } + + // 7. If timeZoneResult.[[OffsetString]] is not empty, then + if (time_zone_result.offset_string.has_value()) { + // a. Return ? ParseTimeZoneIdentifier(timeZoneResult.[[OffsetString]]). + return TRY(parse_time_zone_identifier(vm, *time_zone_result.offset_string)); + } + + // 8. Throw a RangeError exception. + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, time_zone_string); +} + +// 13.40 ToMonthCode ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-tomonthcode +ThrowCompletionOr to_month_code(VM& vm, Value argument) +{ + // 1. Let monthCode be ? ToPrimitive(argument, STRING). + auto month_code = TRY(argument.to_primitive(vm, Value::PreferredType::String)); + + // 2. If monthCode is not a String, throw a TypeError exception. + if (!month_code.is_string()) + return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); + auto month_code_string = month_code.as_string().utf8_string_view(); + + // 3. If the length of monthCode is not 3 or 4, throw a RangeError exception. + if (month_code_string.length() != 3 && month_code_string.length() != 4) + return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); + + // 4. If the first code unit of monthCode is not 0x004D (LATIN CAPITAL LETTER M), throw a RangeError exception. + if (month_code_string[0] != 'M') + return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); + + // 5. If the second code unit of monthCode is not in the inclusive interval from 0x0030 (DIGIT ZERO) to 0x0039 (DIGIT NINE), + // throw a RangeError exception. + if (!is_ascii_digit(month_code_string[1]) || parse_ascii_digit(month_code_string[1]) > 9) + return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); + + // 6. If the third code unit of monthCode is not in the inclusive interval from 0x0030 (DIGIT ZERO) to 0x0039 (DIGIT NINE), + // throw a RangeError exception. + if (!is_ascii_digit(month_code_string[2]) || parse_ascii_digit(month_code_string[2]) > 9) + return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); + + // 7. If the length of monthCode is 4 and the fourth code unit of monthCode is not 0x004C (LATIN CAPITAL LETTER L), + // throw a RangeError exception. + if (month_code_string.length() == 4 && month_code_string[3] != 'L') + return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); + + // 8. Let monthCodeDigits be the substring of monthCode from 1 to 3. + auto month_code_digits = month_code_string.substring_view(1, 2); + + // 9. Let monthCodeInteger be ℝ(StringToNumber(monthCodeDigits)). + auto month_code_integer = month_code_digits.to_number().value(); + + // 10. If monthCodeInteger is 0 and the length of monthCode is not 4, throw a RangeError exception. + if (month_code_integer == 0 && month_code_string.length() != 4) + return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); + + // 11. Return monthCode. + return month_code.as_string().utf8_string(); +} + +// 13.41 ToOffsetString ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-tooffsetstring +ThrowCompletionOr to_offset_string(VM& vm, Value argument) +{ + // 1. Let offset be ? ToPrimitive(argument, STRING). + auto offset = TRY(argument.to_primitive(vm, Value::PreferredType::String)); + + // 2. If offset is not a String, throw a TypeError exception. + if (!offset.is_string()) + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, offset); + + // 3. Perform ? ParseDateTimeUTCOffset(offset). + // FIXME: ParseTimeZoneOffsetString should be renamed to ParseDateTimeUTCOffset and updated for Temporal. For now, we + // can just check with the ISO8601 parser directly. + if (!parse_utc_offset(argument.as_string().utf8_string_view(), SubMinutePrecision::Yes).has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, offset); + + // 4. Return offset. + return offset.as_string().utf8_string(); +} + +// 13.42 ISODateToFields ( calendar, isoDate, type ), https://tc39.es/proposal-temporal/#sec-temporal-isodatetofields +CalendarFields iso_date_to_fields(StringView calendar, ISODate const& iso_date, DateType type) +{ + // 1. Let fields be an empty Calendar Fields Record with all fields set to unset. + auto fields = CalendarFields::unset(); + + // 2. Let calendarDate be CalendarISOToDate(calendar, isoDate). + auto calendar_date = calendar_iso_to_date(calendar, iso_date); + + // 3. Set fields.[[MonthCode]] to calendarDate.[[MonthCode]]. + fields.month_code = calendar_date.month_code; + + // 4. If type is MONTH-DAY or DATE, then + if (type == DateType::MonthDay || type == DateType::Date) { + // a. Set fields.[[Day]] to calendarDate.[[Day]]. + fields.day = calendar_date.day; + } + + // 5. If type is YEAR-MONTH or DATE, then + if (type == DateType::YearMonth || type == DateType::Date) { + // a. Set fields.[[Year]] to calendarDate.[[Year]]. + fields.year = calendar_date.year; + } + + // 6. Return fields. + return fields; +} + // 14.4.1.1 GetOptionsObject ( options ), https://tc39.es/proposal-temporal/#sec-getoptionsobject ThrowCompletionOr> get_options_object(VM& vm, Value options) { @@ -992,4 +1454,19 @@ ThrowCompletionOr get_rounding_increment_option(VM& vm, Object const& optio return static_cast(integer_increment); } +// 14.5.1 GetUTCEpochNanoseconds ( isoDateTime ), https://tc39.es/proposal-temporal/#sec-getutcepochnanoseconds +Crypto::SignedBigInteger get_utc_epoch_nanoseconds(ISODateTime const& iso_date_time) +{ + return JS::get_utc_epoch_nanoseconds( + iso_date_time.iso_date.year, + iso_date_time.iso_date.month, + iso_date_time.iso_date.day, + iso_date_time.time.hour, + iso_date_time.time.minute, + iso_date_time.time.second, + iso_date_time.time.millisecond, + iso_date_time.time.microsecond, + iso_date_time.time.nanosecond); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index 7e4fa8ab79e..101f757206c 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include #include @@ -25,6 +27,22 @@ enum class ArithmeticOperation { Subtract, }; +enum class DateType { + Date, + MonthDay, + YearMonth, +}; + +enum class Overflow { + Constrain, + Reject, +}; + +enum class TimeStyle { + Separated, + Unseparated, +}; + // https://tc39.es/proposal-temporal/#sec-temporal-units enum class Unit { Year, @@ -91,8 +109,9 @@ using UnitValue = Variant; struct SecondsStringPrecision { struct Minute { }; + using Precision = Variant; - Variant precision; + Precision precision; Unit unit; u8 increment { 0 }; }; @@ -103,6 +122,28 @@ struct RelativeTo { GC::Ptr zoned_relative_to; // [[ZonedRelativeTo]] }; +// 13.31 ISO String Time Zone Parse Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-string-time-zone-parse-records +struct ParsedISOTimeZone { + bool z_designator { false }; + Optional offset_string; + Optional time_zone_annotation; +}; + +// 13.32 ISO Date-Time Parse Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-date-time-parse-records +struct ParsedISODateTime { + struct StartOfDay { }; + + Optional year { 0 }; + u8 month { 0 }; + u8 day { 0 }; + Variant time; + ParsedISOTimeZone time_zone; + Optional calendar; +}; + +double iso_date_to_epoch_days(double year, double month, double date); +double epoch_days_to_epoch_ms(double day, double time); +ThrowCompletionOr get_temporal_overflow_option(VM&, Object const& options); ThrowCompletionOr validate_temporal_rounding_increment(VM&, u64 increment, u64 dividend, bool inclusive); ThrowCompletionOr get_temporal_fractional_second_digits_option(VM&, Object const& options); SecondsStringPrecision to_seconds_string_precision_record(UnitValue, Precision); @@ -114,12 +155,19 @@ UnitCategory temporal_unit_category(Unit); RoundingIncrement maximum_temporal_duration_rounding_increment(Unit); Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit); String format_fractional_seconds(u64, Precision); +String format_time_string(u8 hour, u8 minute, u8 second, u16 sub_second_nanoseconds, SecondsStringPrecision::Precision, Optional = {}); UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode, Sign); double apply_unsigned_rounding_mode(double, double r1, double r2, UnsignedRoundingMode); Crypto::SignedBigInteger apply_unsigned_rounding_mode(Crypto::SignedDivisionResult const&, Crypto::SignedBigInteger const& r1, Crypto::SignedBigInteger const& r2, UnsignedRoundingMode, Crypto::UnsignedBigInteger const& increment); double round_number_to_increment(double, u64 increment, RoundingMode); Crypto::SignedBigInteger round_number_to_increment(Crypto::SignedBigInteger const&, Crypto::UnsignedBigInteger const& increment, RoundingMode); +ThrowCompletionOr parse_iso_date_time(VM&, StringView iso_string, ReadonlySpan allowed_formats); +ThrowCompletionOr parse_temporal_calendar_string(VM&, String const&); ThrowCompletionOr> parse_temporal_duration_string(VM&, StringView iso_string); +ThrowCompletionOr parse_temporal_time_zone_string(VM& vm, StringView time_zone_string); +ThrowCompletionOr to_month_code(VM&, Value argument); +ThrowCompletionOr to_offset_string(VM&, Value argument); +CalendarFields iso_date_to_fields(StringView calendar, ISODate const&, DateType); // 13.38 ToIntegerWithTruncation ( argument ), https://tc39.es/proposal-temporal/#sec-tointegerwithtruncation template @@ -153,6 +201,21 @@ ThrowCompletionOr to_integer_with_truncation(VM& vm, StringView argument return trunc(number); } +// 13.37 ToPositiveIntegerWithTruncation ( argument ), https://tc39.es/proposal-temporal/#sec-topositiveintegerwithtruncation +template +ThrowCompletionOr to_positive_integer_with_truncation(VM& vm, Value argument, ErrorType error_type, Args&&... args) +{ + // 1. Let integer be ? ToIntegerWithTruncation(argument). + auto integer = TRY(to_integer_with_truncation(vm, argument, error_type, args...)); + + // 2. If integer ≤ 0, throw a RangeError exception. + if (integer <= 0) + return vm.throw_completion(error_type, args...); + + // 3. Return integer. + return integer; +} + // 13.39 ToIntegerIfIntegral ( argument ), https://tc39.es/proposal-temporal/#sec-tointegerifintegral template ThrowCompletionOr to_integer_if_integral(VM& vm, Value argument, ErrorType error_type, Args&&... args) @@ -168,6 +231,12 @@ ThrowCompletionOr to_integer_if_integral(VM& vm, Value argument, ErrorTy return number.as_double(); } +// 14.2 The Year-Week Record Specification Type, https://tc39.es/proposal-temporal/#sec-year-week-record-specification-type +struct YearWeek { + Optional week; + Optional year; +}; + enum class OptionType { Boolean, String, @@ -186,5 +255,6 @@ ThrowCompletionOr get_option(VM& vm, Object const& options, PropertyKey c ThrowCompletionOr get_rounding_mode_option(VM&, Object const& options, RoundingMode fallback); ThrowCompletionOr get_rounding_increment_option(VM&, Object const& options); +Crypto::SignedBigInteger get_utc_epoch_nanoseconds(ISODateTime const&); } diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp new file mode 100644 index 00000000000..93fb7f8a96e --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -0,0 +1,586 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2023-2024, Shannon Booth + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +enum class CalendarFieldConversion { + ToIntegerWithTruncation, + ToMonthCode, + ToOffsetString, + ToPositiveIntegerWithTruncation, + ToString, + ToTemporalTimeZoneIdentifier, +}; + +// https://tc39.es/proposal-temporal/#table-temporal-calendar-fields-record-fields +#define JS_ENUMERATE_CALENDAR_FIELDS \ + __JS_ENUMERATE(CalendarField::Era, era, vm.names.era, CalendarFieldConversion::ToString) \ + __JS_ENUMERATE(CalendarField::EraYear, era_year, vm.names.eraYear, CalendarFieldConversion::ToIntegerWithTruncation) \ + __JS_ENUMERATE(CalendarField::Year, year, vm.names.year, CalendarFieldConversion::ToIntegerWithTruncation) \ + __JS_ENUMERATE(CalendarField::Month, month, vm.names.month, CalendarFieldConversion::ToPositiveIntegerWithTruncation) \ + __JS_ENUMERATE(CalendarField::MonthCode, month_code, vm.names.monthCode, CalendarFieldConversion::ToMonthCode) \ + __JS_ENUMERATE(CalendarField::Day, day, vm.names.day, CalendarFieldConversion::ToPositiveIntegerWithTruncation) \ + __JS_ENUMERATE(CalendarField::Hour, hour, vm.names.hour, CalendarFieldConversion::ToIntegerWithTruncation) \ + __JS_ENUMERATE(CalendarField::Minute, minute, vm.names.minute, CalendarFieldConversion::ToIntegerWithTruncation) \ + __JS_ENUMERATE(CalendarField::Second, second, vm.names.second, CalendarFieldConversion::ToIntegerWithTruncation) \ + __JS_ENUMERATE(CalendarField::Millisecond, millisecond, vm.names.millisecond, CalendarFieldConversion::ToIntegerWithTruncation) \ + __JS_ENUMERATE(CalendarField::Microsecond, microsecond, vm.names.microsecond, CalendarFieldConversion::ToIntegerWithTruncation) \ + __JS_ENUMERATE(CalendarField::Nanosecond, nanosecond, vm.names.nanosecond, CalendarFieldConversion::ToIntegerWithTruncation) \ + __JS_ENUMERATE(CalendarField::Offset, offset, vm.names.offset, CalendarFieldConversion::ToOffsetString) \ + __JS_ENUMERATE(CalendarField::TimeZone, time_zone, vm.names.timeZone, CalendarFieldConversion::ToTemporalTimeZoneIdentifier) + +struct CalendarFieldData { + CalendarField key; + NonnullRawPtr property; + CalendarFieldConversion conversion; +}; +static Vector sorted_calendar_fields(VM& vm, CalendarFieldList fields) +{ + auto data_for_field = [&](auto field) -> CalendarFieldData { + switch (field) { +#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \ + case enumeration: \ + return { enumeration, property_key, conversion }; + JS_ENUMERATE_CALENDAR_FIELDS +#undef __JS_ENUMERATE + } + + VERIFY_NOT_REACHED(); + }; + + Vector result; + result.ensure_capacity(fields.size()); + + for (auto field : fields) + result.unchecked_append(data_for_field(field)); + + quick_sort(result, [](auto const& lhs, auto const& rhs) { + return StringView { lhs.property->as_string() } < StringView { rhs.property->as_string() }; + }); + + return result; +} + +template +static void set_field_value(CalendarField field, CalendarFields& fields, T&& value) +{ + switch (field) { +#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \ + case enumeration: \ + if constexpr (IsAssignable>) \ + fields.field_name = value; \ + return; + JS_ENUMERATE_CALENDAR_FIELDS +#undef __JS_ENUMERATE + } + + VERIFY_NOT_REACHED(); +} + +static void set_default_field_value(CalendarField field, CalendarFields& fields) +{ + CalendarFields default_ {}; + + switch (field) { +#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \ + case enumeration: \ + fields.field_name = default_.field_name; \ + return; + JS_ENUMERATE_CALENDAR_FIELDS +#undef __JS_ENUMERATE + } + + VERIFY_NOT_REACHED(); +} + +// 12.1.1 CanonicalizeCalendar ( id ), https://tc39.es/proposal-temporal/#sec-temporal-canonicalizecalendar +ThrowCompletionOr canonicalize_calendar(VM& vm, StringView id) +{ + // 1. Let calendars be AvailableCalendars(). + auto const& calendars = available_calendars(); + + // 2. If calendars does not contain the ASCII-lowercase of id, throw a RangeError exception. + for (auto const& calendar : calendars) { + if (calendar.equals_ignoring_ascii_case(id)) { + // 3. Return CanonicalizeUValue("ca", id). + return Unicode::canonicalize_unicode_extension_values("ca"sv, id); + } + } + + return vm.throw_completion(ErrorType::TemporalInvalidCalendarIdentifier, id); +} + +// 12.1.2 AvailableCalendars ( ), https://tc39.es/proposal-temporal/#sec-availablecalendars +Vector const& available_calendars() +{ + // The implementation-defined abstract operation AvailableCalendars takes no arguments and returns a List of calendar + // types. The returned List is sorted according to lexicographic code unit order, and contains unique calendar types + // in canonical form (12.1) identifying the calendars for which the implementation provides the functionality of + // Intl.DateTimeFormat objects, including their aliases (e.g., either both or neither of "islamicc" and + // "islamic-civil"). The List must include "iso8601". + return Unicode::available_calendars(); +} + +// 12.2.3 PrepareCalendarFields ( calendar, fields, calendarFieldNames, nonCalendarFieldNames, requiredFieldNames ), https://tc39.es/proposal-temporal/#sec-temporal-preparecalendarfields +ThrowCompletionOr prepare_calendar_fields(VM& vm, StringView calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names) +{ + // 1. Assert: If requiredFieldNames is a List, requiredFieldNames contains zero or one of each of the elements of + // calendarFieldNames and nonCalendarFieldNames. + + // 2. Let fieldNames be the list-concatenation of calendarFieldNames and nonCalendarFieldNames. + Vector field_names; + field_names.append(calendar_field_names.data(), calendar_field_names.size()); + field_names.append(non_calendar_field_names.data(), non_calendar_field_names.size()); + + // 3. Let extraFieldNames be CalendarExtraFields(calendar, calendarFieldNames). + auto extra_field_names = calendar_extra_fields(calendar, calendar_field_names); + + // 4. Set fieldNames to the list-concatenation of fieldNames and extraFieldNames. + field_names.extend(move(extra_field_names)); + + // 5. Assert: fieldNames contains no duplicate elements. + + // 6. Let result be a Calendar Fields Record with all fields equal to UNSET. + auto result = CalendarFields::unset(); + + // 7. Let any be false. + auto any = false; + + // 8. Let sortedPropertyNames be a List whose elements are the values in the Property Key column of Table 19 + // corresponding to the elements of fieldNames, sorted according to lexicographic code unit order. + auto sorted_property_names = sorted_calendar_fields(vm, field_names); + + // 9. For each property name property of sortedPropertyNames, do + for (auto const& [key, property, conversion] : sorted_property_names) { + // a. Let key be the value in the Enumeration Key column of Table 19 corresponding to the row whose Property Key value is property. + + // b. Let value be ? Get(fields, property). + auto value = TRY(fields.get(property)); + + // c. If value is not undefined, then + if (!value.is_undefined()) { + // i. Set any to true. + any = true; + + // ii. Let Conversion be the Conversion value of the same row. + switch (conversion) { + // iii. If Conversion is TO-INTEGER-WITH-TRUNCATION, then + case CalendarFieldConversion::ToIntegerWithTruncation: + // 1. Set value to ? ToIntegerWithTruncation(value). + // 2. Set value to 𝔽(value). + set_field_value(key, result, TRY(to_integer_with_truncation(vm, value, ErrorType::TemporalInvalidCalendarFieldName, *property))); + break; + // iv. Else if Conversion is TO-POSITIVE-INTEGER-WITH-TRUNCATION, then + case CalendarFieldConversion::ToPositiveIntegerWithTruncation: + // 1. Set value to ? ToPositiveIntegerWithTruncation(value). + // 2. Set value to 𝔽(value). + set_field_value(key, result, TRY(to_positive_integer_with_truncation(vm, value, ErrorType::TemporalInvalidCalendarFieldName, *property))); + break; + // v. Else if Conversion is TO-STRING, then + case CalendarFieldConversion::ToString: + // 1. Set value to ? ToString(value). + set_field_value(key, result, TRY(value.to_string(vm))); + break; + // vi. Else if Conversion is TO-TEMPORAL-TIME-ZONE-IDENTIFIER, then + case CalendarFieldConversion::ToTemporalTimeZoneIdentifier: + // 1. Set value to ? ToTemporalTimeZoneIdentifier(value). + set_field_value(key, result, TRY(to_temporal_time_zone_identifier(vm, value))); + break; + // vii. Else if Conversion is TO-MONTH-CODE, then + case CalendarFieldConversion::ToMonthCode: + // 1. Set value to ? ToMonthCode(value). + set_field_value(key, result, TRY(to_month_code(vm, value))); + break; + // viii. Else, + case CalendarFieldConversion::ToOffsetString: + // 1. Assert: Conversion is TO-OFFSET-STRING. + // 2. Set value to ? ToOffsetString(value). + set_field_value(key, result, TRY(to_offset_string(vm, value))); + break; + } + + // ix. Set result's field whose name is given in the Field Name column of the same row to value. + } + // d. Else if requiredFieldNames is a List, then + else if (auto const* required = required_field_names.get_pointer()) { + // i. If requiredFieldNames contains key, then + if (required->contains_slow(key)) { + // 1. Throw a TypeError exception. + return vm.throw_completion(ErrorType::MissingRequiredProperty, *property); + } + + // ii. Set result's field whose name is given in the Field Name column of the same row to the corresponding + // Default value of the same row. + set_default_field_value(key, result); + } + } + + // 10. If requiredFieldNames is PARTIAL and any is false, then + if (required_field_names.has() && !any) { + // a. Throw a TypeError exception. + return vm.throw_completion(ErrorType::TemporalObjectMustBePartialTemporalObject); + } + + // 11. Return result. + return result; +} + +// 12.2.8 ToTemporalCalendarIdentifier ( temporalCalendarLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalcalendaridentifier +ThrowCompletionOr to_temporal_calendar_identifier(VM& vm, Value temporal_calendar_like) +{ + // 1. If temporalCalendarLike is an Object, then + if (temporal_calendar_like.is_object()) { + auto const& temporal_calendar_object = temporal_calendar_like.as_object(); + + // a. If temporalCalendarLike has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], + // [[InitializedTemporalMonthDay]], [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] + // internal slot, then + // i. Return temporalCalendarLike.[[Calendar]]. + // FIXME: Add the other calendar-holding types as we define them. + if (is(temporal_calendar_object)) + return static_cast(temporal_calendar_object).calendar(); + } + + // 2. If temporalCalendarLike is not a String, throw a TypeError exception. + if (!temporal_calendar_like.is_string()) + return vm.throw_completion(ErrorType::TemporalInvalidCalendar); + + // 3. Let identifier be ? ParseTemporalCalendarString(temporalCalendarLike). + auto identifier = TRY(parse_temporal_calendar_string(vm, temporal_calendar_like.as_string().utf8_string())); + + // 4. Return ? CanonicalizeCalendar(identifier). + return TRY(canonicalize_calendar(vm, identifier)); +} + +// 12.2.9 GetTemporalCalendarIdentifierWithISODefault ( item ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalcalendarslotvaluewithisodefault +ThrowCompletionOr get_temporal_calendar_identifier_with_iso_default(VM& vm, Object const& item) +{ + // 1. If item has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], [[InitializedTemporalMonthDay]], + // [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal slot, then + // a. Return item.[[Calendar]]. + // FIXME: Add the other calendar-holding types as we define them. + if (is(item)) + return static_cast(item).calendar(); + + // 2. Let calendarLike be ? Get(item, "calendar"). + auto calendar_like = TRY(item.get(vm.names.calendar)); + + // 3. If calendarLike is undefined, then + if (calendar_like.is_undefined()) { + // a. Return "iso8601". + return "iso8601"_string; + } + + // 4. Return ? ToTemporalCalendarIdentifier(calendarLike). + return TRY(to_temporal_calendar_identifier(vm, calendar_like)); +} + +// 12.2.12 CalendarMonthDayFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdayfromfields +ThrowCompletionOr calendar_month_day_from_fields(VM& vm, StringView calendar, CalendarFields fields, Overflow overflow) +{ + // 1. Perform ? CalendarResolveFields(calendar, fields, MONTH-DAY). + TRY(calendar_resolve_fields(vm, calendar, fields, DateType::MonthDay)); + + // 2. Let result be ? CalendarMonthDayToISOReferenceDate(calendar, fields, overflow). + auto result = TRY(calendar_month_day_to_iso_reference_date(vm, calendar, fields, overflow)); + + // 3. If ISODateWithinLimits(result) is false, throw a RangeError exception. + if (!iso_date_within_limits(result)) + return vm.throw_completion(ErrorType::TemporalInvalidISODate); + + // 4. Return result. + return result; +} + +// 12.2.15 ISODaysInMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-isodaysinmonth +u8 iso_days_in_month(double year, double month) +{ + // 1. If month is 1, 3, 5, 7, 8, 10, or 12, return 31. + if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) + return 31; + + // 2. If month is 4, 6, 9, or 11, return 30. + if (month == 4 || month == 6 || month == 9 || month == 11) + return 30; + + // 3. Assert: month is 2. + VERIFY(month == 2); + + // 4. Return 28 + MathematicalInLeapYear(EpochTimeForYear(year)). + return 28 + mathematical_in_leap_year(epoch_time_for_year(year)); +} + +// 12.2.16 ISOWeekOfYear ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isoweekofyear +YearWeek iso_week_of_year(ISODate const& iso_date) +{ + // 1. Let year be isoDate.[[Year]]. + auto year = iso_date.year; + + // 2. Let wednesday be 3. + static constexpr auto wednesday = 3; + + // 3. Let thursday be 4. + static constexpr auto thursday = 4; + + // 4. Let friday be 5. + static constexpr auto friday = 5; + + // 5. Let saturday be 6. + static constexpr auto saturday = 6; + + // 6. Let daysInWeek be 7. + static constexpr auto days_in_week = 7; + + // 7. Let maxWeekNumber be 53. + static constexpr auto max_week_number = 53; + + // 8. Let dayOfYear be ISODayOfYear(isoDate). + auto day_of_year = iso_day_of_year(iso_date); + + // 9. Let dayOfWeek be ISODayOfWeek(isoDate). + auto day_of_week = iso_day_of_week(iso_date); + + // 10. Let week be floor((dayOfYear + daysInWeek - dayOfWeek + wednesday) / daysInWeek). + auto week = floor(static_cast(day_of_year + days_in_week - day_of_week + wednesday) / static_cast(days_in_week)); + + // 11. If week < 1, then + if (week < 1) { + // a. NOTE: This is the last week of the previous year. + + // b. Let jan1st be CreateISODateRecord(year, 1, 1). + auto jan1st = create_iso_date_record(year, 1, 1); + + // c. Let dayOfJan1st be ISODayOfWeek(jan1st). + auto day_of_jan1st = iso_day_of_week(jan1st); + + // d. If dayOfJan1st = friday, then + if (day_of_jan1st == friday) { + // i. Return Year-Week Record { [[Week]]: maxWeekNumber, [[Year]]: year - 1 }. + return { .week = max_week_number, .year = year - 1 }; + } + + // e. If dayOfJan1st = saturday, and MathematicalInLeapYear(EpochTimeForYear(year - 1)) = 1, then + if (day_of_jan1st == saturday && mathematical_in_leap_year(epoch_time_for_year(year - 1)) == 1) { + // i. Return Year-Week Record { [[Week]]: maxWeekNumber. [[Year]]: year - 1 }. + return { .week = max_week_number, .year = year - 1 }; + } + + // f. Return Year-Week Record { [[Week]]: maxWeekNumber - 1, [[Year]]: year - 1 }. + return { .week = max_week_number - 1, .year = year - 1 }; + } + + // 12. If week = maxWeekNumber, then + if (week == max_week_number) { + // a. Let daysInYear be MathematicalDaysInYear(year). + auto days_in_year = mathematical_days_in_year(year); + + // b. Let daysLaterInYear be daysInYear - dayOfYear. + auto days_later_in_year = days_in_year - day_of_year; + + // c. Let daysAfterThursday be thursday - dayOfWeek. + auto days_after_thursday = thursday - day_of_week; + + // d. If daysLaterInYear < daysAfterThursday, then + if (days_later_in_year < days_after_thursday) { + // i. Return Year-Week Record { [[Week]]: 1, [[Year]]: year + 1 }. + return { .week = 1, .year = year + 1 }; + } + } + + // 13. Return Year-Week Record { [[Week]]: week, [[Year]]: year }. + return { .week = week, .year = year }; +} + +// 12.2.17 ISODayOfYear ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodayofyear +u16 iso_day_of_year(ISODate const& iso_date) +{ + // 1. Let epochDays be ISODateToEpochDays(isoDate.[[Year]], isoDate.[[Month]] - 1, isoDate.[[Day]]). + auto epoch_days = iso_date_to_epoch_days(iso_date.year, iso_date.month - 1, iso_date.day); + + // 2. Return EpochTimeToDayInYear(EpochDaysToEpochMs(epochDays, 0)) + 1. + return epoch_time_to_day_in_year(epoch_days_to_epoch_ms(epoch_days, 0)) + 1; +} + +// 12.2.18 ISODayOfWeek ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodayofweek +u8 iso_day_of_week(ISODate const& iso_date) +{ + // 1. Let epochDays be ISODateToEpochDays(isoDate.[[Year]], isoDate.[[Month]] - 1, isoDate.[[Day]]). + auto epoch_days = iso_date_to_epoch_days(iso_date.year, iso_date.month - 1, iso_date.day); + + // 2. Let dayOfWeek be EpochTimeToWeekDay(EpochDaysToEpochMs(epochDays, 0)). + auto day_of_week = epoch_time_to_week_day(epoch_days_to_epoch_ms(epoch_days, 0)); + + // 3. If dayOfWeek = 0, return 7. + if (day_of_week == 0) + return 7; + + // 4. Return dayOfWeek. + return day_of_week; +} + +// 12.2.20 CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdaytoisoreferencedate +ThrowCompletionOr calendar_month_day_to_iso_reference_date(VM& vm, StringView calendar, CalendarFields const& fields, Overflow overflow) +{ + // 1. If calendar is "iso8601", then + if (calendar == "iso8601"sv) { + // a. Assert: fields.[[Month]] and fields.[[Day]] are not UNSET. + VERIFY(fields.month.has_value()); + VERIFY(fields.day.has_value()); + + // b. Let referenceISOYear be 1972 (the first ISO 8601 leap year after the epoch). + static constexpr i32 reference_iso_year = 1972; + + // c. If fields.[[Year]] is UNSET, let year be referenceISOYear; else let year be fields.[[Year]]. + auto year = !fields.year.has_value() ? reference_iso_year : *fields.year; + + // d. Let result be ? RegulateISODate(year, fields.[[Month]], fields.[[Day]], overflow). + auto result = TRY(regulate_iso_date(vm, year, *fields.month, *fields.day, overflow)); + + // e. Return CreateISODateRecord(referenceISOYear, result.[[Month]], result.[[Day]]). + return create_iso_date_record(reference_iso_year, result.month, result.day); + } + + // 2. Return an implementation-defined ISO Date Record, or throw a RangeError exception, as described below. + // FIXME: Create an ISODateRecord based on an ISO8601 calendar for now. See also: CalendarResolveFields. + return calendar_month_day_to_iso_reference_date(vm, "iso8601"sv, fields, overflow); +} + +// 12.2.21 CalendarISOToDate ( calendar, isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-calendarisotodate +CalendarDate calendar_iso_to_date(StringView calendar, ISODate const& iso_date) +{ + // 1. If calendar is "iso8601", then + if (calendar == "iso8601"sv) { + // a. Let monthNumberPart be ToZeroPaddedDecimalString(isoDate.[[Month]], 2). + // b. Let monthCode be the string-concatenation of "M" and monthNumberPart. + auto month_code = MUST(String::formatted("M{:02}", iso_date.month)); + + // c. If MathematicalInLeapYear(EpochTimeForYear(isoDate.[[Year]])) = 1, let inLeapYear be true; else let inLeapYear be false. + auto in_leap_year = mathematical_in_leap_year(epoch_time_for_year(iso_date.year)) == 1; + + // d. Return Calendar Date Record { [[Era]]: undefined, [[EraYear]]: undefined, [[Year]]: isoDate.[[Year]], + // [[Month]]: isoDate.[[Month]], [[MonthCode]]: monthCode, [[Day]]: isoDate.[[Day]], [[DayOfWeek]]: ISODayOfWeek(isoDate), + // [[DayOfYear]]: ISODayOfYear(isoDate), [[WeekOfYear]]: ISOWeekOfYear(isoDate), [[DaysInWeek]]: 7, + // [[DaysInMonth]]: ISODaysInMonth(isoDate.[[Year]], isoDate.[[Month]]), [[DaysInYear]]: MathematicalDaysInYear(isoDate.[[Year]]), + // [[MonthsInYear]]: 12, [[InLeapYear]]: inLeapYear }. + return CalendarDate { + .era = {}, + .era_year = {}, + .year = iso_date.year, + .month = iso_date.month, + .month_code = move(month_code), + .day = iso_date.day, + .day_of_week = iso_day_of_week(iso_date), + .day_of_year = iso_day_of_year(iso_date), + .week_of_year = iso_week_of_year(iso_date), + .days_in_week = 7, + .days_in_month = iso_days_in_month(iso_date.year, iso_date.month), + .days_in_year = mathematical_days_in_year(iso_date.year), + .months_in_year = 12, + .in_leap_year = in_leap_year, + }; + } + + // 2. Return an implementation-defined Calendar Date Record with fields as described in Table 18. + // FIXME: Return an ISO8601 calendar date for now. + return calendar_iso_to_date("iso8601"sv, iso_date); +} + +// 12.2.22 CalendarExtraFields ( calendar, fields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarextrafields +Vector calendar_extra_fields(StringView calendar, CalendarFieldList) +{ + // 1. If calendar is "iso8601", return an empty List. + if (calendar == "iso8601"sv) + return {}; + + // FIXME: 2. Return an implementation-defined List as described above. + return {}; +} + +// 12.2.24 CalendarResolveFields ( calendar, fields, type ), https://tc39.es/proposal-temporal/#sec-temporal-calendarresolvefields +ThrowCompletionOr calendar_resolve_fields(VM& vm, StringView calendar, CalendarFields& fields, DateType type) +{ + // 1. If calendar is "iso8601", then + if (calendar == "iso8601"sv) { + // a. If type is DATE or YEAR-MONTH and fields.[[Year]] is UNSET, throw a TypeError exception. + if ((type == DateType::Date || type == DateType::YearMonth) && !fields.year.has_value()) + return vm.throw_completion(ErrorType::MissingRequiredProperty, "year"sv); + + // b. If type is DATE or MONTH-DAY and fields.[[Day]] is UNSET, throw a TypeError exception. + if ((type == DateType::Date || type == DateType::MonthDay) && !fields.day.has_value()) + return vm.throw_completion(ErrorType::MissingRequiredProperty, "day"sv); + + // c. Let month be fields.[[Month]]. + auto const& month = fields.month; + + // d. Let monthCode be fields.[[MonthCode]]. + auto const& month_code = fields.month_code; + + // e. If monthCode is UNSET, then + if (!month_code.has_value()) { + // i. If month is UNSET, throw a TypeError exception. + if (!month.has_value()) + return vm.throw_completion(ErrorType::MissingRequiredProperty, "month"sv); + + // ii. Return UNUSED. + return {}; + } + + // f. Assert: monthCode is a String. + VERIFY(month_code.has_value()); + + // g. NOTE: The ISO 8601 calendar does not include leap months. + // h. If the length of monthCode is not 3, throw a RangeError exception. + if (month_code->byte_count() != 3) + return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv); + + // i. If the first code unit of monthCode is not 0x004D (LATIN CAPITAL LETTER M), throw a RangeError exception. + if (month_code->bytes_as_string_view()[0] != 'M') + return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv); + + // j. Let monthCodeDigits be the substring of monthCode from 1. + auto month_code_digits = month_code->bytes_as_string_view().substring_view(1); + + // k. If ParseText(StringToCodePoints(monthCodeDigits), DateMonth) is a List of errors, throw a RangeError exception. + if (!parse_iso8601(Production::DateMonth, month_code_digits).has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv); + + // l. Let monthCodeInteger be ℝ(StringToNumber(monthCodeDigits)). + auto month_code_integer = month_code_digits.to_number().value(); + + // m. If month is not UNSET and month ≠ monthCodeInteger, throw a RangeError exception. + if (month.has_value() && month != month_code_integer) + return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "month"sv); + + // n. Set fields.[[Month]] to monthCodeInteger. + fields.month = month_code_integer; + } + // 2. Else, + else { + // a. Perform implementation-defined processing to mutate fields, or throw a TypeError or RangeError exception, as described below. + // FIXME: Resolve fields as an ISO8601 calendar for now. See also: CalendarMonthDayToISOReferenceDate. + TRY(calendar_resolve_fields(vm, "iso8601"sv, fields, type)); + } + + // 3. Return UNUSED. + return {}; +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Libraries/LibJS/Runtime/Temporal/Calendar.h new file mode 100644 index 00000000000..7289d1e3c78 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2023-2024, Shannon Booth + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +// 12.2.1 Calendar Date Records, https://tc39.es/proposal-temporal/#sec-temporal-calendar-date-records +struct CalendarDate { + Optional era; + Optional era_year; + i32 year { 0 }; + u8 month { 0 }; + String month_code; + u8 day { 0 }; + u8 day_of_week { 0 }; + u16 day_of_year { 0 }; + YearWeek week_of_year; + u8 days_in_week { 0 }; + u8 days_in_month { 0 }; + u16 days_in_year { 0 }; + u8 months_in_year { 0 }; + bool in_leap_year { false }; +}; + +// https://tc39.es/proposal-temporal/#table-temporal-calendar-fields-record-fields +enum class CalendarField { + Era, + EraYear, + Year, + Month, + MonthCode, + Day, + Hour, + Minute, + Second, + Millisecond, + Microsecond, + Nanosecond, + Offset, + TimeZone, +}; + +// https://tc39.es/proposal-temporal/#table-temporal-calendar-fields-record-fields +struct CalendarFields { + static CalendarFields unset() + { + return { + .era = {}, + .era_year = {}, + .year = {}, + .month = {}, + .month_code = {}, + .day = {}, + .hour = {}, + .minute = {}, + .second = {}, + .millisecond = {}, + .microsecond = {}, + .nanosecond = {}, + .offset = {}, + .time_zone = {}, + }; + } + + Optional era; + Optional era_year; + Optional year; + Optional month; + Optional month_code; + Optional day; + Optional hour { 0 }; + Optional minute { 0 }; + Optional second { 0 }; + Optional millisecond { 0 }; + Optional microsecond { 0 }; + Optional nanosecond { 0 }; + Optional offset; + Optional time_zone; +}; + +struct Partial { }; +using CalendarFieldList = ReadonlySpan; +using CalendarFieldListOrPartial = Variant; + +ThrowCompletionOr canonicalize_calendar(VM&, StringView id); +Vector const& available_calendars(); +ThrowCompletionOr prepare_calendar_fields(VM&, StringView calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names); +ThrowCompletionOr calendar_month_day_from_fields(VM&, StringView calendar, CalendarFields, Overflow); +u8 iso_days_in_month(double year, double month); +YearWeek iso_week_of_year(ISODate const&); +u16 iso_day_of_year(ISODate const&); +u8 iso_day_of_week(ISODate const&); +ThrowCompletionOr to_temporal_calendar_identifier(VM&, Value temporal_calendar_like); +ThrowCompletionOr get_temporal_calendar_identifier_with_iso_default(VM&, Object const& item); +ThrowCompletionOr calendar_month_day_to_iso_reference_date(VM&, StringView calendar, CalendarFields const&, Overflow); +CalendarDate calendar_iso_to_date(StringView calendar, ISODate const&); +Vector calendar_extra_fields(StringView calendar, CalendarFieldList); +ThrowCompletionOr calendar_resolve_fields(VM&, StringView calendar, CalendarFields&, DateType); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp b/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp new file mode 100644 index 00000000000..fc499e183f0 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace JS::Temporal { + +// https://tc39.es/proposal-temporal/#eqn-mathematicaldaysinyear +u16 mathematical_days_in_year(i32 year) +{ + // MathematicalDaysInYear(y) + // = 365 if ((y) modulo 4) ≠ 0 + // = 366 if ((y) modulo 4) = 0 and ((y) modulo 100) ≠ 0 + // = 365 if ((y) modulo 100) = 0 and ((y) modulo 400) ≠ 0 + // = 366 if ((y) modulo 400) = 0 + if (modulo(year, 4) != 0) + return 365; + if (modulo(year, 4) == 0 && modulo(year, 100) != 0) + return 366; + if (modulo(year, 100) == 0 && modulo(year, 400) != 0) + return 365; + if (modulo(year, 400) == 0) + return 366; + VERIFY_NOT_REACHED(); +} + +// https://tc39.es/proposal-temporal/#eqn-mathematicalinleapyear +u8 mathematical_in_leap_year(double time) +{ + // MathematicalInLeapYear(t) + // = 0 if MathematicalDaysInYear(EpochTimeToEpochYear(t)) = 365 + // = 1 if MathematicalDaysInYear(EpochTimeToEpochYear(t)) = 366 + auto days_in_year = mathematical_days_in_year(epoch_time_to_epoch_year(time)); + + if (days_in_year == 365) + return 0; + if (days_in_year == 366) + return 1; + VERIFY_NOT_REACHED(); +} + +// https://tc39.es/proposal-temporal/#eqn-EpochTimeToDayNumber +double epoch_time_to_day_number(double time) +{ + // EpochTimeToDayNumber(t) = floor(t / ℝ(msPerDay)) + return floor(time / JS::ms_per_day); +} + +// https://tc39.es/proposal-temporal/#eqn-epochdaynumberforyear +double epoch_day_number_for_year(double year) +{ + // EpochDayNumberForYear(y) = 365 × (y - 1970) + floor((y - 1969) / 4) - floor((y - 1901) / 100) + floor((y - 1601) / 400) + return 365.0 * (year - 1970.0) + floor((year - 1969.0) / 4.0) - floor((year - 1901.0) / 100.0) + floor((year - 1601.0) / 400.0); +} + +// https://tc39.es/proposal-temporal/#eqn-epochtimeforyear +double epoch_time_for_year(double year) +{ + // EpochTimeForYear(y) = ℝ(msPerDay) × EpochDayNumberForYear(y) + return ms_per_day * epoch_day_number_for_year(year); +} + +// https://tc39.es/proposal-temporal/#eqn-epochtimetoepochyear +i32 epoch_time_to_epoch_year(double time) +{ + // EpochTimeToEpochYear(t) = the largest integral Number y (closest to +∞) such that EpochTimeForYear(y) ≤ t + return JS::year_from_time(time); +} + +// https://tc39.es/proposal-temporal/#eqn-epochtimetodayinyear +u16 epoch_time_to_day_in_year(double time) +{ + // EpochTimeToDayInYear(t) = EpochTimeToDayNumber(t) - EpochDayNumberForYear(EpochTimeToEpochYear(t)) + return static_cast(epoch_time_to_day_number(time) - epoch_day_number_for_year(epoch_time_to_epoch_year(time))); +} + +// https://tc39.es/proposal-temporal/#eqn-epochtimetoweekday +u8 epoch_time_to_week_day(double time) +{ + // EpochTimeToWeekDay(t) = (EpochTimeToDayNumber(t) + 4) modulo 7 + return static_cast(modulo(epoch_time_to_day_number(time) + 4, 7.0)); +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/DateEquations.h b/Libraries/LibJS/Runtime/Temporal/DateEquations.h new file mode 100644 index 00000000000..0dcd6033014 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/DateEquations.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS::Temporal { + +// 13.3 Date Equations, https://tc39.es/proposal-temporal/#sec-date-equations + +u16 mathematical_days_in_year(i32 year); +u8 mathematical_in_leap_year(double time); +double epoch_time_to_day_number(double time); +double epoch_day_number_for_year(double year); +double epoch_time_for_year(double year); +i32 epoch_time_to_epoch_year(double time); +u16 epoch_time_to_day_in_year(double time); +u8 epoch_time_to_week_day(double time); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp new file mode 100644 index 00000000000..f523d6539ab --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Shannon Booth + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +// 3.5.2 CreateISODateRecord ( year, month, day ), https://tc39.es/proposal-temporal/#sec-temporal-create-iso-date-record +ISODate create_iso_date_record(double year, double month, double day) +{ + // 1. Assert: IsValidISODate(year, month, day) is true. + VERIFY(is_valid_iso_date(year, month, day)); + + // 2. Return ISO Date Record { [[Year]]: year, [[Month]]: month, [[Day]]: day }. + return { .year = static_cast(year), .month = static_cast(month), .day = static_cast(day) }; +} + +// 3.5.6 RegulateISODate ( year, month, day, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-regulateisodate +ThrowCompletionOr regulate_iso_date(VM& vm, double year, double month, double day, Overflow overflow) +{ + switch (overflow) { + // 1. If overflow is CONSTRAIN, then + case Overflow::Constrain: + // a. Set month to the result of clamping month between 1 and 12. + month = clamp(month, 1, 12); + + // b. Let daysInMonth be ISODaysInMonth(year, month). + // c. Set day to the result of clamping day between 1 and daysInMonth. + day = clamp(day, 1, iso_days_in_month(year, month)); + + // AD-HOC: We perform the IsValidISODate check even for CONSTRAIN to ensure the year may be treated as a normal + // integer. See the note in IsValidISODate for more details. + [[fallthrough]]; + + // 2. Else, + case Overflow::Reject: + // a. Assert: overflow is REJECT. + // b. If IsValidISODate(year, month, day) is false, throw a RangeError exception. + if (!is_valid_iso_date(year, month, day)) + return vm.throw_completion(ErrorType::TemporalInvalidISODate); + break; + } + + // 3. Return CreateISODateRecord(year, month, day). + return create_iso_date_record(year, month, day); +} + +// 3.5.7 IsValidISODate ( year, month, day ), https://tc39.es/proposal-temporal/#sec-temporal-isvalidisodate +bool is_valid_iso_date(double year, double month, double day) +{ + // AD-HOC: This is an optimization that allows us to treat these doubles as normal integers from this point onwards. + // This does not change the exposed behavior as the call to CreateISODateRecord will immediately check that + // these values are valid ISO values (for years: -273975 - 273975, for months: 1 - 12, for days: 1 - 31), + // all of which are subsets of this check. + if (!AK::is_within_range(year) || !AK::is_within_range(month) || !AK::is_within_range(day)) + return false; + + // 1. If month < 1 or month > 12, then + if (month < 1 || month > 12) { + // a. Return false. + return false; + } + + // 2. Let daysInMonth be ISODaysInMonth(year, month). + auto days_in_month = iso_days_in_month(year, month); + + // 3. If day < 1 or day > daysInMonth, then + if (day < 1 || day > days_in_month) { + // a. Return false. + return false; + } + + // 4. Return true. + return true; +} + +// 3.5.11 ISODateWithinLimits ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodatewithinlimits +bool iso_date_within_limits(ISODate iso_date) +{ + // 1. Let isoDateTime be CombineISODateAndTimeRecord(isoDate, NoonTimeRecord()). + auto iso_date_time = combine_iso_date_and_time_record(iso_date, noon_time_record()); + + // 2. Return ISODateTimeWithinLimits(isoDateTime). + return iso_date_time_within_limits(iso_date_time); +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.h b/Libraries/LibJS/Runtime/Temporal/PlainDate.h new file mode 100644 index 00000000000..67d38b33753 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Shannon Booth + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace JS::Temporal { + +// 3.5.1 ISO Date Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-date-records +struct ISODate { + i32 year { 0 }; + u8 month { 0 }; + u8 day { 0 }; +}; + +ISODate create_iso_date_record(double year, double month, double day); +ThrowCompletionOr regulate_iso_date(VM& vm, double year, double month, double day, Overflow overflow); +bool is_valid_iso_date(double year, double month, double day); +bool iso_date_within_limits(ISODate); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp new file mode 100644 index 00000000000..84c21661e02 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace JS::Temporal { + +// 5.5.3 CombineISODateAndTimeRecord ( isoDate, time ), https://tc39.es/proposal-temporal/#sec-temporal-combineisodateandtimerecord +ISODateTime combine_iso_date_and_time_record(ISODate iso_date, Time time) +{ + // 1. NOTE: time.[[Days]] is ignored. + // 2. Return ISO Date-Time Record { [[ISODate]]: isoDate, [[Time]]: time }. + return { .iso_date = iso_date, .time = time }; +} + +// nsMinInstant - nsPerDay +static auto const DATETIME_NANOSECONDS_MIN = "-8640000086400000000000"_sbigint; + +// nsMaxInstant + nsPerDay +static auto const DATETIME_NANOSECONDS_MAX = "8640000086400000000000"_sbigint; + +// 5.5.4 ISODateTimeWithinLimits ( isoDateTime ), https://tc39.es/proposal-temporal/#sec-temporal-isodatetimewithinlimits +bool iso_date_time_within_limits(ISODateTime iso_date_time) +{ + // 1. If abs(ISODateToEpochDays(isoDateTime.[[ISODate]].[[Year]], isoDateTime.[[ISODate]].[[Month]] - 1, isoDateTime.[[ISODate]].[[Day]])) > 10**8 + 1, return false. + if (fabs(iso_date_to_epoch_days(iso_date_time.iso_date.year, iso_date_time.iso_date.month - 1, iso_date_time.iso_date.day)) > 100000001) + return false; + + // 2. Let ns be ℝ(GetUTCEpochNanoseconds(isoDateTime)). + auto nanoseconds = get_utc_epoch_nanoseconds(iso_date_time); + + // 3. If ns ≤ nsMinInstant - nsPerDay, then + if (nanoseconds <= DATETIME_NANOSECONDS_MIN) { + // a. Return false. + return false; + } + + // 4. If ns ≥ nsMaxInstant + nsPerDay, then + if (nanoseconds >= DATETIME_NANOSECONDS_MAX) { + // a. Return false. + return false; + } + + // 5. Return true. + return true; +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h new file mode 100644 index 00000000000..31260cca3ae --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS::Temporal { + +// 5.5.1 ISO Date-Time Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-date-time-records +struct ISODateTime { + ISODate iso_date; + Time time; +}; + +ISODateTime combine_iso_date_and_time_record(ISODate, Time); +bool iso_date_time_within_limits(ISODateTime); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp new file mode 100644 index 00000000000..e5efdc04c35 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2021, Luke Wilde + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +GC_DEFINE_ALLOCATOR(PlainMonthDay); + +// 10 Temporal.PlainMonthDay Objects, https://tc39.es/proposal-temporal/#sec-temporal-plainmonthday-objects +PlainMonthDay::PlainMonthDay(ISODate iso_date, String calendar, Object& prototype) + : Object(ConstructWithPrototypeTag::Tag, prototype) + , m_iso_date(iso_date) + , m_calendar(move(calendar)) +{ +} + +// 10.5.1 ToTemporalMonthDay ( item [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalmonthday +ThrowCompletionOr> to_temporal_month_day(VM& vm, Value item, Value options) +{ + // 1. If options is not present, set options to undefined. + + // 2. If item is a Object, then + if (item.is_object()) { + auto const& object = item.as_object(); + + // a. If item has an [[InitializedTemporalMonthDay]] internal slot, then + if (is(object)) { + auto const& plain_month_day = static_cast(object); + + // i. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // ii. Perform ? GetTemporalOverflowOption(resolvedOptions). + TRY(get_temporal_overflow_option(vm, resolved_options)); + + // iii. Return ! CreateTemporalMonthDay(item.[[ISODate]], item.[[Calendar]]). + return MUST(create_temporal_month_day(vm, plain_month_day.iso_date(), plain_month_day.calendar())); + } + + // b. Let calendar be ? GetTemporalCalendarIdentifierWithISODefault(item). + auto calendar = TRY(get_temporal_calendar_identifier_with_iso_default(vm, object)); + + // c. Let fields be ? PrepareCalendarFields(calendar, item, « YEAR, MONTH, MONTH-CODE, DAY », «», «»). + auto fields = TRY(prepare_calendar_fields(vm, calendar, object, { { CalendarField::Year, CalendarField::Month, CalendarField::MonthCode, CalendarField::Day } }, {}, CalendarFieldList {})); + + // d. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // e. Let overflow be ? GetTemporalOverflowOption(resolvedOptions). + auto overflow = TRY(get_temporal_overflow_option(vm, resolved_options)); + + // f. Let isoDate be ? CalendarMonthDayFromFields(calendar, fields, overflow). + auto iso_date = TRY(calendar_month_day_from_fields(vm, calendar, move(fields), overflow)); + + // g. Return ! CreateTemporalMonthDay(isoDate, calendar). + return MUST(create_temporal_month_day(vm, iso_date, move(calendar))); + } + + // 3. If item is not a String, throw a TypeError exception. + if (!item.is_string()) + return vm.throw_completion(ErrorType::TemporalInvalidPlainMonthDay); + + // 4. Let result be ? ParseISODateTime(item, « TemporalMonthDayString »). + auto parse_result = TRY(parse_iso_date_time(vm, item.as_string().utf8_string_view(), { { Production::TemporalMonthDayString } })); + + // 5. Let calendar be result.[[Calendar]]. + // 6. If calendar is empty, set calendar to "iso8601". + auto calendar = parse_result.calendar.value_or("iso8601"_string); + + // 7. Set calendar to ? CanonicalizeCalendar(calendar). + calendar = TRY(canonicalize_calendar(vm, calendar)); + + // 8. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // 9. Perform ? GetTemporalOverflowOption(resolvedOptions). + TRY(get_temporal_overflow_option(vm, resolved_options)); + + // 10. If result.[[Year]] is empty, then + if (!parse_result.year.has_value()) { + // a. Assert: calendar is "iso8601". + VERIFY(calendar == "iso8601"sv); + + // b. Let referenceISOYear be 1972 (the first ISO 8601 leap year after the epoch). + static constexpr i32 reference_iso_year = 1972; + + // c. Let isoDate be CreateISODateRecord(referenceISOYear, result.[[Month]], result.[[Day]]). + auto iso_date = create_iso_date_record(reference_iso_year, parse_result.month, parse_result.day); + + // d. Return ! CreateTemporalMonthDay(isoDate, calendar). + return MUST(create_temporal_month_day(vm, iso_date, move(calendar))); + } + + // 11. Let isoDate be CreateISODateRecord(result.[[Year]], result.[[Month]], result.[[Day]]). + auto iso_date = create_iso_date_record(*parse_result.year, parse_result.month, parse_result.day); + + // 12. Set result to ISODateToFields(calendar, isoDate, MONTH-DAY). + auto result = iso_date_to_fields(calendar, iso_date, DateType::MonthDay); + + // 13. NOTE: The following operation is called with CONSTRAIN regardless of the value of overflow, in order for the + // calendar to store a canonical value in the [[Year]] field of the [[ISODate]] internal slot of the result. + // 14. Set isoDate to ? CalendarMonthDayFromFields(calendar, result, CONSTRAIN). + iso_date = TRY(calendar_month_day_from_fields(vm, calendar, result, Overflow::Constrain)); + + // 15. Return ! CreateTemporalMonthDay(isoDate, calendar). + return MUST(create_temporal_month_day(vm, iso_date, move(calendar))); +} + +// 10.5.2 CreateTemporalMonthDay ( isoDate, calendar [ , newTarget ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtemporalmonthday +ThrowCompletionOr> create_temporal_month_day(VM& vm, ISODate iso_date, String calendar, GC::Ptr new_target) +{ + auto& realm = *vm.current_realm(); + + // 1. If ISODateWithinLimits(isoDate) is false, throw a RangeError exception. + if (!iso_date_within_limits(iso_date)) + return vm.throw_completion(ErrorType::TemporalInvalidPlainMonthDay); + + // 2. If newTarget is not present, set newTarget to %Temporal.PlainMonthDay%. + if (!new_target) + new_target = realm.intrinsics().temporal_plain_month_day_constructor(); + + // 3. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainMonthDay.prototype%", « [[InitializedTemporalMonthDay]], [[ISODate]], [[Calendar]] »). + // 4. Set object.[[ISODate]] to isoDate. + // 5. Set object.[[Calendar]] to calendar. + auto object = TRY(ordinary_create_from_constructor(vm, *new_target, &Intrinsics::temporal_plain_month_day_prototype, iso_date, move(calendar))); + + // 6. Return object. + return object; +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h new file mode 100644 index 00000000000..3a6eeb6ab80 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace JS::Temporal { + +class PlainMonthDay final : public Object { + JS_OBJECT(PlainMonthDay, Object); + GC_DECLARE_ALLOCATOR(PlainMonthDay); + +public: + virtual ~PlainMonthDay() override = default; + + [[nodiscard]] ISODate iso_date() const { return m_iso_date; } + [[nodiscard]] String const& calendar() const { return m_calendar; } + +private: + PlainMonthDay(ISODate, String calendar, Object& prototype); + + ISODate m_iso_date; // [[ISODate]] + String m_calendar; // [[Calendar]] +}; + +ThrowCompletionOr> to_temporal_month_day(VM&, Value item, Value options = js_undefined()); +ThrowCompletionOr> create_temporal_month_day(VM&, ISODate, String calendar, GC::Ptr new_target = {}); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.cpp b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.cpp new file mode 100644 index 00000000000..f5634aa4019 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +GC_DEFINE_ALLOCATOR(PlainMonthDayConstructor); + +// 10.1 The Temporal.PlainMonthDay Constructor, https://tc39.es/proposal-temporal/#sec-temporal-plainmonthday-constructor +PlainMonthDayConstructor::PlainMonthDayConstructor(Realm& realm) + : NativeFunction(realm.vm().names.PlainMonthDay.as_string(), realm.intrinsics().function_prototype()) +{ +} + +void PlainMonthDayConstructor::initialize(Realm& realm) +{ + Base::initialize(realm); + + auto& vm = this->vm(); + + // 10.2.1 Temporal.PlainMonthDay.prototype, https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype + define_direct_property(vm.names.prototype, realm.intrinsics().temporal_plain_month_day_prototype(), 0); + + define_direct_property(vm.names.length, Value(2), Attribute::Configurable); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.from, from, 1, attr); +} + +// 10.1.1 Temporal.PlainMonthDay ( isoMonth, isoDay [ , calendar [ , referenceISOYear ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday +ThrowCompletionOr PlainMonthDayConstructor::call() +{ + auto& vm = this->vm(); + + // 1. If NewTarget is undefined, throw a TypeError exception. + return vm.throw_completion(ErrorType::ConstructorWithoutNew, "Temporal.PlainMonthDay"); +} + +// 10.1.1 Temporal.PlainMonthDay ( isoMonth, isoDay [ , calendar [ , referenceISOYear ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday +ThrowCompletionOr> PlainMonthDayConstructor::construct(FunctionObject& new_target) +{ + auto& vm = this->vm(); + + auto iso_month = vm.argument(0); + auto iso_day = vm.argument(1); + auto calendar_value = vm.argument(2); + auto reference_iso_year = vm.argument(3); + + // 2. If referenceISOYear is undefined, then + if (reference_iso_year.is_undefined()) { + // a. Set referenceISOYear to 1972𝔽 (the first ISO 8601 leap year after the epoch). + reference_iso_year = Value { 1972 }; + } + + // 3. Let m be ? ToIntegerWithTruncation(isoMonth). + auto month = TRY(to_integer_with_truncation(vm, iso_month, ErrorType::TemporalInvalidPlainMonthDay)); + + // 4. Let d be ? ToIntegerWithTruncation(isoDay). + auto day = TRY(to_integer_with_truncation(vm, iso_day, ErrorType::TemporalInvalidPlainMonthDay)); + + // 5. If calendar is undefined, set calendar to "iso8601". + if (calendar_value.is_undefined()) + calendar_value = PrimitiveString::create(vm, "iso8601"_string); + + // 6. If calendar is not a String, throw a TypeError exception. + if (!calendar_value.is_string()) + return vm.throw_completion(ErrorType::NotAString, calendar_value); + + // 7. Set calendar to ? CanonicalizeCalendar(calendar). + auto calendar = TRY(canonicalize_calendar(vm, calendar_value.as_string().utf8_string_view())); + + // 8. Let y be ? ToIntegerWithTruncation(referenceISOYear). + auto year = TRY(to_integer_with_truncation(vm, reference_iso_year, ErrorType::TemporalInvalidPlainMonthDay)); + + // 9. If IsValidISODate(y, m, d) is false, throw a RangeError exception. + if (!is_valid_iso_date(year, month, day)) + return vm.throw_completion(ErrorType::TemporalInvalidPlainMonthDay); + + // 10. Let isoDate be CreateISODateRecord(y, m, d). + auto iso_date = create_iso_date_record(year, month, day); + + // 11. Return ? CreateTemporalMonthDay(isoDate, calendar, NewTarget). + return TRY(create_temporal_month_day(vm, iso_date, move(calendar), &new_target)); +} + +// 10.2.2 Temporal.PlainMonthDay.from ( item [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.from +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayConstructor::from) +{ + // 1. Return ? ToTemporalMonthDay(item, options). + return TRY(to_temporal_month_day(vm, vm.argument(0), vm.argument(1))); +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.h b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.h new file mode 100644 index 00000000000..03b81c0ceb2 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021-2022, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS::Temporal { + +class PlainMonthDayConstructor final : public NativeFunction { + JS_OBJECT(PlainMonthDayConstructor, NativeFunction); + GC_DECLARE_ALLOCATOR(PlainMonthDayConstructor); + +public: + virtual void initialize(Realm&) override; + virtual ~PlainMonthDayConstructor() override = default; + + virtual ThrowCompletionOr call() override; + virtual ThrowCompletionOr> construct(FunctionObject& new_target) override; + +private: + explicit PlainMonthDayConstructor(Realm&); + + virtual bool has_constructor() const override { return true; } + + JS_DECLARE_NATIVE_FUNCTION(from); +}; + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp new file mode 100644 index 00000000000..4987eb706af --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace JS::Temporal { + +GC_DEFINE_ALLOCATOR(PlainMonthDayPrototype); + +// 10.3 Properties of the Temporal.PlainMonthDay Prototype Object, https://tc39.es/proposal-temporal/#sec-properties-of-the-temporal-plainmonthday-prototype-object +PlainMonthDayPrototype::PlainMonthDayPrototype(Realm& realm) + : PrototypeObject(realm.intrinsics().object_prototype()) +{ +} + +void PlainMonthDayPrototype::initialize(Realm& realm) +{ + Base::initialize(realm); + + auto& vm = this->vm(); + + // 10.3.2 Temporal.PlainMonthDay.prototype[ %Symbol.toStringTag% ], https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype-%symbol.tostringtag% + define_direct_property(vm.well_known_symbol_to_string_tag(), PrimitiveString::create(vm, "Temporal.PlainMonthDay"_string), Attribute::Configurable); + + define_native_accessor(realm, vm.names.calendarId, calendar_id_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.monthCode, month_code_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.day, day_getter, {}, Attribute::Configurable); +} + +// 10.3.3 get Temporal.PlainMonthDay.prototype.calendarId, https://tc39.es/proposal-temporal/#sec-get-temporal.plainmonthday.prototype.calendarid +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::calendar_id_getter) +{ + // 1. Let monthDay be the this value + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. Return monthDay.[[Calendar]]. + return PrimitiveString::create(vm, month_day->calendar()); +} + +// 10.3.4 get Temporal.PlainMonthDay.prototype.monthCode, https://tc39.es/proposal-temporal/#sec-get-temporal.plainmonthday.prototype.monthcode +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::month_code_getter) +{ + // 1. Let monthDay be the this value + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. Return CalendarISOToDate(monthDay.[[Calendar]], monthDay.[[ISODate]]).[[MonthCode]]. + return PrimitiveString::create(vm, calendar_iso_to_date(month_day->calendar(), month_day->iso_date()).month_code); +} + +// 10.3.5 get Temporal.PlainMonthDay.prototype.day, https://tc39.es/proposal-temporal/#sec-get-temporal.plainmonthday.prototype.day +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::day_getter) +{ + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. Return 𝔽(CalendarISOToDate(monthDay.[[Calendar]], monthDay.[[ISODate]]).[[Day]]). + return calendar_iso_to_date(month_day->calendar(), month_day->iso_date()).day; +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h new file mode 100644 index 00000000000..33a00aedee9 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS::Temporal { + +class PlainMonthDayPrototype final : public PrototypeObject { + JS_PROTOTYPE_OBJECT(PlainMonthDayPrototype, PlainMonthDay, Temporal.PlainMonthDay); + GC_DECLARE_ALLOCATOR(PlainMonthDayPrototype); + +public: + virtual void initialize(Realm&) override; + virtual ~PlainMonthDayPrototype() override = default; + +private: + explicit PlainMonthDayPrototype(Realm&); + + JS_DECLARE_NATIVE_FUNCTION(calendar_id_getter); + JS_DECLARE_NATIVE_FUNCTION(month_code_getter); + JS_DECLARE_NATIVE_FUNCTION(day_getter); +}; + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp b/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp new file mode 100644 index 00000000000..85f6a39a3b0 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace JS::Temporal { + +// 4.5.2 CreateTimeRecord ( hour, minute, second, millisecond, microsecond, nanosecond [ , deltaDays ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtimerecord +Time create_time_record(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond, double delta_days) +{ + // 1. If deltaDays is not present, set deltaDays to 0. + // 2. Assert: IsValidTime(hour, minute, second, millisecond, microsecond, nanosecond). + VERIFY(is_valid_time(hour, minute, second, millisecond, microsecond, nanosecond)); + + // 3. Return Time Record { [[Days]]: deltaDays, [[Hour]]: hour, [[Minute]]: minute, [[Second]]: second, [[Millisecond]]: millisecond, [[Microsecond]]: microsecond, [[Nanosecond]]: nanosecond }. + return { + .days = delta_days, + .hour = static_cast(hour), + .minute = static_cast(minute), + .second = static_cast(second), + .millisecond = static_cast(millisecond), + .microsecond = static_cast(microsecond), + .nanosecond = static_cast(nanosecond), + }; +} + +// 4.5.4 NoonTimeRecord ( ), https://tc39.es/proposal-temporal/#sec-temporal-noontimerecord +Time noon_time_record() +{ + // 1. Return Time Record { [[Days]]: 0, [[Hour]]: 12, [[Minute]]: 0, [[Second]]: 0, [[Millisecond]]: 0, [[Microsecond]]: 0, [[Nanosecond]]: 0 }. + return { .days = 0, .hour = 12, .minute = 0, .second = 0, .millisecond = 0, .microsecond = 0, .nanosecond = 0 }; +} + +// 4.5.9 IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ), https://tc39.es/proposal-temporal/#sec-temporal-isvalidtime +bool is_valid_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond) +{ + // 1. If hour < 0 or hour > 23, then + if (hour < 0 || hour > 23) { + // a. Return false. + return false; + } + + // 2. If minute < 0 or minute > 59, then + if (minute < 0 || minute > 59) { + // a. Return false. + return false; + } + + // 3. If second < 0 or second > 59, then + if (second < 0 || second > 59) { + // a. Return false. + return false; + } + + // 4. If millisecond < 0 or millisecond > 999, then + if (millisecond < 0 || millisecond > 999) { + // a. Return false. + return false; + } + + // 5. If microsecond < 0 or microsecond > 999, then + if (microsecond < 0 || microsecond > 999) { + // a. Return false. + return false; + } + + // 6. If nanosecond < 0 or nanosecond > 999, then + if (nanosecond < 0 || nanosecond > 999) { + // a. Return false. + return false; + } + + // 7. Return true. + return true; +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTime.h b/Libraries/LibJS/Runtime/Temporal/PlainTime.h new file mode 100644 index 00000000000..40eefc37a11 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainTime.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS::Temporal { + +// 4.5.1 Time Records, https://tc39.es/proposal-temporal/#sec-temporal-time-records +struct Time { + double days { 0 }; + u8 hour { 0 }; + u8 minute { 0 }; + u8 second { 0 }; + u16 millisecond { 0 }; + u16 microsecond { 0 }; + u16 nanosecond { 0 }; +}; + +Time create_time_record(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond, double delta_days = 0); +Time noon_time_record(); +bool is_valid_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/Temporal.cpp b/Libraries/LibJS/Runtime/Temporal/Temporal.cpp index 1fa7a43ea9e..e8d067e3de5 100644 --- a/Libraries/LibJS/Runtime/Temporal/Temporal.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Temporal.cpp @@ -7,6 +7,7 @@ #include #include +#include #include namespace JS::Temporal { @@ -30,6 +31,7 @@ void Temporal::initialize(Realm& realm) u8 attr = Attribute::Writable | Attribute::Configurable; define_intrinsic_accessor(vm.names.Duration, attr, [](auto& realm) -> Value { return realm.intrinsics().temporal_duration_constructor(); }); + define_intrinsic_accessor(vm.names.PlainMonthDay, attr, [](auto& realm) -> Value { return realm.intrinsics().temporal_plain_month_day_constructor(); }); } } diff --git a/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp b/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp new file mode 100644 index 00000000000..33cc50020ac --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Shannon Booth + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +// 11.1.5 FormatOffsetTimeZoneIdentifier ( offsetMinutes [ , style ] ), https://tc39.es/proposal-temporal/#sec-temporal-formatoffsettimezoneidentifier +String format_offset_time_zone_identifier(i64 offset_minutes, Optional style) +{ + // 1. If offsetMinutes ≥ 0, let sign be the code unit 0x002B (PLUS SIGN); otherwise, let sign be the code unit 0x002D (HYPHEN-MINUS). + auto sign = offset_minutes >= 0 ? '+' : '-'; + + // 2. Let absoluteMinutes be abs(offsetMinutes). + auto absolute_minutes = abs(offset_minutes); + + // 3. Let hour be floor(absoluteMinutes / 60). + auto hour = static_cast(floor(static_cast(absolute_minutes) / 60.0)); + + // 4. Let minute be absoluteMinutes modulo 60. + auto minute = static_cast(modulo(static_cast(absolute_minutes), 60.0)); + + // 5. Let timeString be FormatTimeString(hour, minute, 0, 0, MINUTE, style). + auto time_string = format_time_string(hour, minute, 0, 0, SecondsStringPrecision::Minute {}, style); + + // 6. Return the string-concatenation of sign and timeString. + return MUST(String::formatted("{}{}", sign, time_string)); +} + +// 11.1.8 ToTemporalTimeZoneIdentifier ( temporalTimeZoneLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporaltimezoneidentifier +ThrowCompletionOr to_temporal_time_zone_identifier(VM& vm, Value temporal_time_zone_like) +{ + // 1. If temporalTimeZoneLike is an Object, then + if (temporal_time_zone_like.is_object()) { + // FIXME: a. If temporalTimeZoneLike has an [[InitializedTemporalZonedDateTime]] internal slot, then + // FIXME: i. Return temporalTimeZoneLike.[[TimeZone]]. + } + + // 2. If temporalTimeZoneLike is not a String, throw a TypeError exception. + if (!temporal_time_zone_like.is_string()) + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneName, temporal_time_zone_like); + + // 3. Let parseResult be ? ParseTemporalTimeZoneString(temporalTimeZoneLike). + auto parse_result = TRY(parse_temporal_time_zone_string(vm, temporal_time_zone_like.as_string().utf8_string_view())); + + // 4. Let offsetMinutes be parseResult.[[OffsetMinutes]]. + // 5. If offsetMinutes is not empty, return FormatOffsetTimeZoneIdentifier(offsetMinutes). + if (parse_result.offset_minutes.has_value()) + return format_offset_time_zone_identifier(*parse_result.offset_minutes); + + // 6. Let name be parseResult.[[Name]]. + // 7. Let timeZoneIdentifierRecord be GetAvailableNamedTimeZoneIdentifier(name). + auto time_zone_identifier_record = Intl::get_available_named_time_zone_identifier(*parse_result.name); + + // 8. If timeZoneIdentifierRecord is empty, throw a RangeError exception. + if (!time_zone_identifier_record.has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneName, temporal_time_zone_like); + + // 9. Return timeZoneIdentifierRecord.[[Identifier]]. + return time_zone_identifier_record->identifier; +} + +// 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier +ThrowCompletionOr parse_time_zone_identifier(VM& vm, StringView identifier) +{ + // 1. Let parseResult be ParseText(StringToCodePoints(identifier), TimeZoneIdentifier). + auto parse_result = parse_iso8601(Production::TimeZoneIdentifier, identifier); + + // 2. If parseResult is a List of errors, throw a RangeError exception. + if (!parse_result.has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, identifier); + + return parse_time_zone_identifier(*parse_result); +} + +// 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier +TimeZone parse_time_zone_identifier(StringView identifier) +{ + // OPTIMIZATION: Some callers can assume that parsing will succeed. + + // 1. Let parseResult be ParseText(StringToCodePoints(identifier), TimeZoneIdentifier). + auto parse_result = parse_iso8601(Production::TimeZoneIdentifier, identifier); + VERIFY(parse_result.has_value()); + + return parse_time_zone_identifier(*parse_result); +} + +// 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier +TimeZone parse_time_zone_identifier(ParseResult const& parse_result) +{ + // OPTIMIZATION: Some callers will have already parsed and validated the time zone identifier. + + // 3. If parseResult contains a TimeZoneIANAName Parse Node, then + if (parse_result.time_zone_iana_name.has_value()) { + // a. Let name be the source text matched by the TimeZoneIANAName Parse Node contained within parseResult. + // b. NOTE: name is syntactically valid, but does not necessarily conform to IANA Time Zone Database naming + // guidelines or correspond with an available named time zone identifier. + // c. Return the Record { [[Name]]: CodePointsToString(name), [[OffsetMinutes]]: empty }. + return TimeZone { .name = String::from_utf8_without_validation(parse_result.time_zone_iana_name->bytes()), .offset_minutes = {} }; + } + // 4. Else, + else { + // a. Assert: parseResult contains a UTCOffset[~SubMinutePrecision] Parse Node. + VERIFY(parse_result.time_zone_offset.has_value()); + + // b. Let offsetString be the source text matched by the UTCOffset[~SubMinutePrecision] Parse Node contained within parseResult. + // c. Let offsetNanoseconds be ! ParseDateTimeUTCOffset(offsetString). + // FIXME: ParseTimeZoneOffsetString should be renamed to ParseDateTimeUTCOffset and updated for Temporal. + auto offset_nanoseconds = parse_time_zone_offset_string(parse_result.time_zone_offset->source_text); + + // d. Let offsetMinutes be offsetNanoseconds / (60 × 10**9). + auto offset_minutes = offset_nanoseconds / 60'000'000'000; + + // e. Assert: offsetMinutes is an integer. + VERIFY(trunc(offset_minutes) == offset_minutes); + + // f. Return the Record { [[Name]]: empty, [[OffsetMinutes]]: offsetMinutes }. + return TimeZone { .name = {}, .offset_minutes = static_cast(offset_minutes) }; + } +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/TimeZone.h b/Libraries/LibJS/Runtime/Temporal/TimeZone.h new file mode 100644 index 00000000000..97972ab0c88 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/TimeZone.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace JS::Temporal { + +struct TimeZone { + Optional name; + Optional offset_minutes; +}; + +String format_offset_time_zone_identifier(i64 offset_minutes, Optional = {}); +ThrowCompletionOr to_temporal_time_zone_identifier(VM&, Value temporal_time_zone_like); +ThrowCompletionOr parse_time_zone_identifier(VM&, StringView identifier); +TimeZone parse_time_zone_identifier(StringView identifier); +TimeZone parse_time_zone_identifier(ParseResult const&); + +} diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.from.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.from.js new file mode 100644 index 00000000000..2a52adad8be --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.from.js @@ -0,0 +1,104 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainMonthDay.from).toHaveLength(1); + }); + + test("fields object argument", () => { + const object = { + month: 7, + day: 6, + }; + const plainMonthDay = Temporal.PlainMonthDay.from(object); + expect(plainMonthDay.monthCode).toBe("M07"); + expect(plainMonthDay.day).toBe(6); + }); + + test("from month day string", () => { + const plainMonthDay = Temporal.PlainMonthDay.from("--07-06"); + expect(plainMonthDay.monthCode).toBe("M07"); + expect(plainMonthDay.day).toBe(6); + }); + + test("from date time string", () => { + const plainMonthDay = Temporal.PlainMonthDay.from("2021-07-06T23:42:01"); + 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", () => { + test("missing fields", () => { + expect(() => { + Temporal.PlainMonthDay.from({}); + }).toThrowWithMessage(TypeError, "Required property day is missing or undefined"); + expect(() => { + Temporal.PlainMonthDay.from({ month: 1 }); + }).toThrowWithMessage(TypeError, "Required property day is missing or undefined"); + expect(() => { + Temporal.PlainMonthDay.from({ day: 1 }); + }).toThrowWithMessage(TypeError, "Required property month is missing or undefined"); + }); + + test("invalid month day string", () => { + expect(() => { + Temporal.PlainMonthDay.from("foo"); + }).toThrowWithMessage(RangeError, "Invalid ISO date time"); + }); + + test("string must not contain a UTC designator", () => { + expect(() => { + Temporal.PlainMonthDay.from("2021-07-06T23:42:01Z"); + }).toThrowWithMessage(RangeError, "Invalid ISO date time"); + }); + + test("extended year must not be negative zero", () => { + expect(() => { + Temporal.PlainMonthDay.from("-000000-01-01"); + }).toThrowWithMessage(RangeError, "Invalid ISO date time"); + }); + + test("can only use iso8601 calendar with month day strings", () => { + expect(() => { + Temporal.PlainMonthDay.from("02-10[u-ca=iso8602]"); + }).toThrowWithMessage(RangeError, "Invalid calendar identifier 'iso8602'"); + + expect(() => { + Temporal.PlainMonthDay.from("02-10[u-ca=ladybird]"); + }).toThrowWithMessage(RangeError, "Invalid calendar identifier 'ladybird'"); + }); + + 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 "ladybird" is not a recognised calendar, not because of the string format restriction. + try { + Temporal.PlainMonthDay.from("2023-02-10T22:56[u-ca=ladybird]"); + } 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 'ladybird'"); + } + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.js new file mode 100644 index 00000000000..e7216ee163b --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.js @@ -0,0 +1,66 @@ +describe("errors", () => { + test("called without new", () => { + expect(() => { + Temporal.PlainMonthDay(); + }).toThrowWithMessage( + TypeError, + "Temporal.PlainMonthDay constructor must be called with 'new'" + ); + }); + + test("cannot pass Infinity", () => { + expect(() => { + new Temporal.PlainMonthDay(Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + expect(() => { + new Temporal.PlainMonthDay(1, Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + expect(() => { + new Temporal.PlainMonthDay(1, 1, undefined, Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + expect(() => { + new Temporal.PlainMonthDay(-Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + expect(() => { + new Temporal.PlainMonthDay(1, -Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + expect(() => { + new Temporal.PlainMonthDay(1, 1, undefined, -Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + }); + + test("cannot pass invalid ISO month/day", () => { + expect(() => { + new Temporal.PlainMonthDay(0, 1); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + expect(() => { + new Temporal.PlainMonthDay(1, 0); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + }); + + test("not within iso date time limit", () => { + expect(() => { + new Temporal.PlainMonthDay(9, 30, "iso8601", 999_999_999_999_999); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + }); +}); + +describe("normal behavior", () => { + test("length is 2", () => { + expect(Temporal.PlainMonthDay).toHaveLength(2); + }); + + test("basic functionality", () => { + const plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(typeof plainMonthDay).toBe("object"); + expect(plainMonthDay).toBeInstanceOf(Temporal.PlainMonthDay); + expect(Object.getPrototypeOf(plainMonthDay)).toBe(Temporal.PlainMonthDay.prototype); + }); + + // FIXME: Re-implement this test with Temporal.PlainMonthDay.prototype.toString({ calendarName: "always" }). + // test("default reference year is 1972", () => { + // const plainMonthDay = new Temporal.PlainMonthDay(7, 6); + // const fields = plainMonthDay.getISOFields(); + // expect(fields.isoYear).toBe(1972); + // }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.calendarId.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.calendarId.js new file mode 100644 index 00000000000..8f9177df267 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.calendarId.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("calendarId basic functionality", () => { + const plainMonthDay = new Temporal.PlainMonthDay(5, 1, "iso8601"); + expect(plainMonthDay.calendarId).toBe("iso8601"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Reflect.get(Temporal.PlainMonthDay.prototype, "calendarId", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.day.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.day.js new file mode 100644 index 00000000000..e9807f1286f --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.day.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(plainMonthDay.day).toBe(6); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Reflect.get(Temporal.PlainMonthDay.prototype, "day", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.monthCode.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.monthCode.js new file mode 100644 index 00000000000..2943794e495 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.monthCode.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(plainMonthDay.monthCode).toBe("M07"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Reflect.get(Temporal.PlainMonthDay.prototype, "monthCode", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); +}); From 2487c26e1bec99d810fd98f09eb71378199a0c1e Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 20 Nov 2024 14:57:18 -0500 Subject: [PATCH 3/6] LibJS: Implement stringification Temporal.PlainMonthDay prototypes --- .../Runtime/Temporal/AbstractOperations.cpp | 22 +++++++++ .../Runtime/Temporal/AbstractOperations.h | 8 ++++ Libraries/LibJS/Runtime/Temporal/Calendar.cpp | 18 ++++++++ Libraries/LibJS/Runtime/Temporal/Calendar.h | 1 + .../LibJS/Runtime/Temporal/PlainDate.cpp | 17 +++++++ Libraries/LibJS/Runtime/Temporal/PlainDate.h | 1 + .../LibJS/Runtime/Temporal/PlainMonthDay.cpp | 28 +++++++++++- .../LibJS/Runtime/Temporal/PlainMonthDay.h | 2 + .../Temporal/PlainMonthDayPrototype.cpp | 45 +++++++++++++++++++ .../Runtime/Temporal/PlainMonthDayPrototype.h | 3 ++ .../PlainMonthDay.prototype.toJSON.js | 23 ++++++++++ .../PlainMonthDay.prototype.toLocaleString.js | 23 ++++++++++ .../PlainMonthDay.prototype.toString.js | 42 +++++++++++++++++ 13 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toJSON.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toLocaleString.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toString.js diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index f84c1c9fa76..e41c31f82e6 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -84,6 +84,28 @@ ThrowCompletionOr get_temporal_overflow_option(VM& vm, Object const& o return Overflow::Reject; } +// 13.10 GetTemporalShowCalendarNameOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalshowcalendarnameoption +ThrowCompletionOr get_temporal_show_calendar_name_option(VM& vm, Object const& options) +{ + // 1. Let stringValue be ? GetOption(options, "calendarName", STRING, « "auto", "always", "never", "critical" », "auto"). + auto string_value = TRY(get_option(vm, options, vm.names.calendarName, OptionType::String, { "auto"sv, "always"sv, "never"sv, "critical"sv }, "auto"sv)); + + // 2. If stringValue is "always", return ALWAYS. + if (string_value.as_string().utf8_string_view() == "always"sv) + return ShowCalendar::Always; + + // 3. If stringValue is "never", return NEVER. + if (string_value.as_string().utf8_string_view() == "never"sv) + return ShowCalendar::Never; + + // 4. If stringValue is "critical", return CRITICAL. + if (string_value.as_string().utf8_string_view() == "critical"sv) + return ShowCalendar::Critical; + + // 5. Return AUTO. + return ShowCalendar::Auto; +} + // 13.14 ValidateTemporalRoundingIncrement ( increment, dividend, inclusive ), https://tc39.es/proposal-temporal/#sec-validatetemporalroundingincrement ThrowCompletionOr validate_temporal_rounding_increment(VM& vm, u64 increment, u64 dividend, bool inclusive) { diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index 101f757206c..16a1df2aaca 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -38,6 +38,13 @@ enum class Overflow { Reject, }; +enum class ShowCalendar { + Auto, + Always, + Never, + Critical, +}; + enum class TimeStyle { Separated, Unseparated, @@ -144,6 +151,7 @@ struct ParsedISODateTime { double iso_date_to_epoch_days(double year, double month, double date); double epoch_days_to_epoch_ms(double day, double time); ThrowCompletionOr get_temporal_overflow_option(VM&, Object const& options); +ThrowCompletionOr get_temporal_show_calendar_name_option(VM&, Object const& options); ThrowCompletionOr validate_temporal_rounding_increment(VM&, u64 increment, u64 dividend, bool inclusive); ThrowCompletionOr get_temporal_fractional_second_digits_option(VM&, Object const& options); SecondsStringPrecision to_seconds_string_precision_record(UnitValue, Precision); diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp index 93fb7f8a96e..fcb5a6a4cfe 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -310,6 +310,24 @@ ThrowCompletionOr calendar_month_day_from_fields(VM& vm, StringView cal return result; } +// 12.2.13 FormatCalendarAnnotation ( id, showCalendar ), https://tc39.es/proposal-temporal/#sec-temporal-formatcalendarannotation +String format_calendar_annotation(StringView id, ShowCalendar show_calendar) +{ + // 1. If showCalendar is NEVER, return the empty String. + if (show_calendar == ShowCalendar::Never) + return String {}; + + // 2. If showCalendar is AUTO and id is "iso8601", return the empty String. + if (show_calendar == ShowCalendar::Auto && id == "iso8601"sv) + return String {}; + + // 3. If showCalendar is CRITICAL, let flag be "!"; else, let flag be the empty String. + auto flag = show_calendar == ShowCalendar::Critical ? "!"sv : ""sv; + + // 4. Return the string-concatenation of "[", flag, "u-ca=", id, and "]". + return MUST(String::formatted("[{}u-ca={}]", flag, id)); +} + // 12.2.15 ISODaysInMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-isodaysinmonth u8 iso_days_in_month(double year, double month) { diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Libraries/LibJS/Runtime/Temporal/Calendar.h index 7289d1e3c78..c54b5b22d29 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.h +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -100,6 +100,7 @@ ThrowCompletionOr canonicalize_calendar(VM&, StringView id); Vector const& available_calendars(); ThrowCompletionOr prepare_calendar_fields(VM&, StringView calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names); ThrowCompletionOr calendar_month_day_from_fields(VM&, StringView calendar, CalendarFields, Overflow); +String format_calendar_annotation(StringView id, ShowCalendar); u8 iso_days_in_month(double year, double month); YearWeek iso_week_of_year(ISODate const&); u16 iso_day_of_year(ISODate const&); diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp index f523d6539ab..5fe270d0169 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp @@ -84,6 +84,23 @@ bool is_valid_iso_date(double year, double month, double day) return true; } +// 3.5.9 PadISOYear ( y ), https://tc39.es/proposal-temporal/#sec-temporal-padisoyear +String pad_iso_year(i32 year) +{ + // 1. If y ≥ 0 and y ≤ 9999, then + if (year >= 0 && year <= 9999) { + // a. Return ToZeroPaddedDecimalString(y, 4). + return MUST(String::formatted("{:04}", year)); + } + + // 2. If y > 0, let yearSign be "+"; otherwise, let yearSign be "-". + auto year_sign = year > 0 ? '+' : '-'; + + // 3. Let year be ToZeroPaddedDecimalString(abs(y), 6). + // 4. Return the string-concatenation of yearSign and year. + return MUST(String::formatted("{}{:06}", year_sign, abs(year))); +} + // 3.5.11 ISODateWithinLimits ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodatewithinlimits bool iso_date_within_limits(ISODate iso_date) { diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.h b/Libraries/LibJS/Runtime/Temporal/PlainDate.h index 67d38b33753..310510fc23d 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDate.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.h @@ -25,6 +25,7 @@ struct ISODate { ISODate create_iso_date_record(double year, double month, double day); ThrowCompletionOr regulate_iso_date(VM& vm, double year, double month, double day, Overflow overflow); bool is_valid_iso_date(double year, double month, double day); +String pad_iso_year(i32 year); bool iso_date_within_limits(ISODate); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp index e5efdc04c35..1cdf1225633 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -141,4 +140,31 @@ ThrowCompletionOr> create_temporal_month_day(VM& vm, ISOD return object; } +// 10.5.3 TemporalMonthDayToString ( monthDay, showCalendar ), https://tc39.es/proposal-temporal/#sec-temporal-temporalmonthdaytostring +String temporal_month_day_to_string(PlainMonthDay const& month_day, ShowCalendar show_calendar) +{ + // 1. Let month be ToZeroPaddedDecimalString(monthDay.[[ISODate]].[[Month]], 2). + // 2. Let day be ToZeroPaddedDecimalString(monthDay.[[ISODate]].[[Day]], 2). + // 3. Let result be the string-concatenation of month, the code unit 0x002D (HYPHEN-MINUS), and day. + auto result = MUST(String::formatted("{:02}-{:02}", month_day.iso_date().month, month_day.iso_date().day)); + + // 4. If showCalendar is one of ALWAYS or CRITICAL, or if monthDay.[[Calendar]] is not "iso8601", then + if (show_calendar == ShowCalendar::Always || show_calendar == ShowCalendar::Critical || month_day.calendar() != "iso8601"sv) { + // a. Let year be PadISOYear(monthDay.[[ISODate]].[[Year]]). + auto year = pad_iso_year(month_day.iso_date().year); + + // b. Set result to the string-concatenation of year, the code unit 0x002D (HYPHEN-MINUS), and result. + result = MUST(String::formatted("{}-{}", year, result)); + } + + // 5. Let calendarString be FormatCalendarAnnotation(monthDay.[[Calendar]], showCalendar). + auto calendar_string = format_calendar_annotation(month_day.calendar(), show_calendar); + + // 6. Set result to the string-concatenation of result and calendarString. + result = MUST(String::formatted("{}{}", result, calendar_string)); + + // 7. Return result. + return result; +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h index 3a6eeb6ab80..0f2592240bc 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace JS::Temporal { @@ -33,5 +34,6 @@ private: ThrowCompletionOr> to_temporal_month_day(VM&, Value item, Value options = js_undefined()); ThrowCompletionOr> create_temporal_month_day(VM&, ISODate, String calendar, GC::Ptr new_target = {}); +String temporal_month_day_to_string(PlainMonthDay const&, ShowCalendar); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp index 4987eb706af..2d4a68a5b59 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp @@ -32,6 +32,11 @@ void PlainMonthDayPrototype::initialize(Realm& realm) define_native_accessor(realm, vm.names.calendarId, calendar_id_getter, {}, Attribute::Configurable); define_native_accessor(realm, vm.names.monthCode, month_code_getter, {}, Attribute::Configurable); define_native_accessor(realm, vm.names.day, day_getter, {}, Attribute::Configurable); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.toString, to_string, 0, attr); + define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr); + define_native_function(realm, vm.names.toJSON, to_json, 0, attr); } // 10.3.3 get Temporal.PlainMonthDay.prototype.calendarId, https://tc39.es/proposal-temporal/#sec-get-temporal.plainmonthday.prototype.calendarid @@ -67,4 +72,44 @@ JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::day_getter) return calendar_iso_to_date(month_day->calendar(), month_day->iso_date()).day; } +// 10.3.8 Temporal.PlainMonthDay.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.tostring +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::to_string) +{ + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, vm.argument(0))); + + // 4. Let showCalendar be ? GetTemporalShowCalendarNameOption(resolvedOptions). + auto show_calendar = TRY(get_temporal_show_calendar_name_option(vm, resolved_options)); + + // 5. Return TemporalMonthDayToString(monthDay, showCalendar). + return PrimitiveString::create(vm, temporal_month_day_to_string(month_day, show_calendar)); +} + +// 10.3.9 Temporal.PlainMonthDay.prototype.toLocaleString ( [ locales [ , options ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.tolocalestring +// NOTE: This is the minimum toLocaleString implementation for engines without ECMA-402. +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::to_locale_string) +{ + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. Return TemporalMonthDayToString(monthDay, auto). + return PrimitiveString::create(vm, temporal_month_day_to_string(month_day, ShowCalendar::Auto)); +} + +// 10.3.10 Temporal.PlainMonthDay.prototype.toJSON ( ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.tolocalestring +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::to_json) +{ + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. Return TemporalMonthDayToString(monthDay, auto). + return PrimitiveString::create(vm, temporal_month_day_to_string(month_day, ShowCalendar::Auto)); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h index 33a00aedee9..6eb48a84078 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h @@ -26,6 +26,9 @@ private: JS_DECLARE_NATIVE_FUNCTION(calendar_id_getter); JS_DECLARE_NATIVE_FUNCTION(month_code_getter); JS_DECLARE_NATIVE_FUNCTION(day_getter); + JS_DECLARE_NATIVE_FUNCTION(to_string); + JS_DECLARE_NATIVE_FUNCTION(to_locale_string); + JS_DECLARE_NATIVE_FUNCTION(to_json); }; } diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toJSON.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toJSON.js new file mode 100644 index 00000000000..0e9ac4e3a2e --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toJSON.js @@ -0,0 +1,23 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainMonthDay.prototype.toJSON).toHaveLength(0); + }); + + test("basic functionality", () => { + let plainMonthDay; + + plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(plainMonthDay.toJSON()).toBe("07-06"); + + plainMonthDay = new Temporal.PlainMonthDay(7, 6, "gregory", 2021); + expect(plainMonthDay.toJSON()).toBe("2021-07-06[u-ca=gregory]"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Temporal.PlainMonthDay.prototype.toJSON.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toLocaleString.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toLocaleString.js new file mode 100644 index 00000000000..f5ea5e9cc2f --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toLocaleString.js @@ -0,0 +1,23 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainMonthDay.prototype.toLocaleString).toHaveLength(0); + }); + + test("basic functionality", () => { + let plainMonthDay; + + plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(plainMonthDay.toLocaleString()).toBe("07-06"); + + plainMonthDay = new Temporal.PlainMonthDay(7, 6, "gregory", 2021); + expect(plainMonthDay.toLocaleString()).toBe("2021-07-06[u-ca=gregory]"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Temporal.PlainMonthDay.prototype.toLocaleString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toString.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toString.js new file mode 100644 index 00000000000..b296f8b0716 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toString.js @@ -0,0 +1,42 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainMonthDay.prototype.toString).toHaveLength(0); + }); + + test("basic functionality", () => { + let plainMonthDay; + + plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(plainMonthDay.toString()).toBe("07-06"); + expect(plainMonthDay.toString({ calendarName: "auto" })).toBe("07-06"); + expect(plainMonthDay.toString({ calendarName: "always" })).toBe("1972-07-06[u-ca=iso8601]"); + expect(plainMonthDay.toString({ calendarName: "never" })).toBe("07-06"); + expect(plainMonthDay.toString({ calendarName: "critical" })).toBe( + "1972-07-06[!u-ca=iso8601]" + ); + + plainMonthDay = new Temporal.PlainMonthDay(7, 6, "gregory", 2021); + expect(plainMonthDay.toString()).toBe("2021-07-06[u-ca=gregory]"); + expect(plainMonthDay.toString({ calendarName: "auto" })).toBe("2021-07-06[u-ca=gregory]"); + expect(plainMonthDay.toString({ calendarName: "always" })).toBe("2021-07-06[u-ca=gregory]"); + expect(plainMonthDay.toString({ calendarName: "never" })).toBe("2021-07-06"); + expect(plainMonthDay.toString({ calendarName: "critical" })).toBe( + "2021-07-06[!u-ca=gregory]" + ); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Temporal.PlainMonthDay.prototype.toString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); + + test("calendarName option must be one of 'auto', 'always', 'never', 'critical'", () => { + const plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(() => { + plainMonthDay.toString({ calendarName: "foo" }); + }).toThrowWithMessage(RangeError, "foo is not a valid value for option calendarName"); + }); +}); From 9823f4382787510cbc2c4019f8c5fff34fae55dc Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 20 Nov 2024 18:08:07 -0500 Subject: [PATCH 4/6] LibJS: Implement Temporal.PlainMonthDay.prototype.with/equals --- .../Runtime/Temporal/AbstractOperations.cpp | 35 ++++++ .../Runtime/Temporal/AbstractOperations.h | 1 + Libraries/LibJS/Runtime/Temporal/Calendar.cpp | 109 ++++++++++++++++++ Libraries/LibJS/Runtime/Temporal/Calendar.h | 4 + .../LibJS/Runtime/Temporal/PlainDate.cpp | 31 +++++ Libraries/LibJS/Runtime/Temporal/PlainDate.h | 1 + .../Temporal/PlainMonthDayPrototype.cpp | 60 ++++++++++ .../Runtime/Temporal/PlainMonthDayPrototype.h | 2 + .../PlainMonthDay.prototype.equals.js | 14 +++ .../PlainMonthDay.prototype.with.js | 57 +++++++++ 10 files changed, 314 insertions(+) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.equals.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.with.js diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index e41c31f82e6..e838f97d469 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include namespace JS::Temporal { @@ -451,6 +452,40 @@ Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit unit) } } +// 13.23 IsPartialTemporalObject ( value ), https://tc39.es/proposal-temporal/#sec-temporal-ispartialtemporalobject +ThrowCompletionOr is_partial_temporal_object(VM& vm, Value value) +{ + // 1. If value is not an Object, return false. + if (!value.is_object()) + return false; + + auto const& object = value.as_object(); + + // 2. If value has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], [[InitializedTemporalMonthDay]], + // [[InitializedTemporalTime]], [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal + // slot, return false. + // FIXME: Add the other types as we define them. + if (is(object)) + return false; + + // 3. Let calendarProperty be ? Get(value, "calendar"). + auto calendar_property = TRY(object.get(vm.names.calendar)); + + // 4. If calendarProperty is not undefined, return false. + if (!calendar_property.is_undefined()) + return false; + + // 5. Let timeZoneProperty be ? Get(value, "timeZone"). + auto time_zone_property = TRY(object.get(vm.names.timeZone)); + + // 6. If timeZoneProperty is not undefined, return false. + if (!time_zone_property.is_undefined()) + return false; + + // 7. Return true. + return true; +} + // 13.24 FormatFractionalSeconds ( subSecondNanoseconds, precision ), https://tc39.es/proposal-temporal/#sec-temporal-formatfractionalseconds String format_fractional_seconds(u64 sub_second_nanoseconds, Precision precision) { diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index 16a1df2aaca..f31a269e55f 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -162,6 +162,7 @@ bool is_calendar_unit(Unit); UnitCategory temporal_unit_category(Unit); RoundingIncrement maximum_temporal_duration_rounding_increment(Unit); Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit); +ThrowCompletionOr is_partial_temporal_object(VM&, Value); String format_fractional_seconds(u64, Precision); String format_time_string(u8 hour, u8 minute, u8 second, u16 sub_second_nanoseconds, SecondsStringPrecision::Precision, Optional = {}); UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode, Sign); diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp index fcb5a6a4cfe..529e588bc93 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -243,6 +243,73 @@ ThrowCompletionOr prepare_calendar_fields(VM& vm, StringView cal return result; } +// 12.2.4 CalendarFieldKeysPresent ( fields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeyspresent +Vector calendar_field_keys_present(CalendarFields const& fields) +{ + // 1. Let list be « ». + Vector list; + + auto handle_field = [&](auto enumeration_key, auto const& value) { + // a. Let value be fields' field whose name is given in the Field Name column of the row. + // b. Let enumerationKey be the value in the Enumeration Key column of the row. + // c. If value is not unset, append enumerationKey to list. + if (value.has_value()) + list.append(enumeration_key); + }; + + // 2. For each row of Table 19, except the header row, do +#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \ + handle_field(enumeration, fields.field_name); + JS_ENUMERATE_CALENDAR_FIELDS +#undef __JS_ENUMERATE + + // 3. Return list. + return list; +} + +// 12.2.5 CalendarMergeFields ( calendar, fields, additionalFields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmergefields +CalendarFields calendar_merge_fields(StringView calendar, CalendarFields const& fields, CalendarFields const& additional_fields) +{ + // 1. Let additionalKeys be CalendarFieldKeysPresent(additionalFields). + auto additional_keys = calendar_field_keys_present(additional_fields); + + // 2. Let overriddenKeys be CalendarFieldKeysToIgnore(calendar, additionalKeys). + auto overridden_keys = calendar_field_keys_to_ignore(calendar, additional_keys); + + // 3. Let merged be a Calendar Fields Record with all fields set to unset. + auto merged = CalendarFields::unset(); + + // 4. Let fieldsKeys be CalendarFieldKeysPresent(fields). + auto fields_keys = calendar_field_keys_present(fields); + + auto merge_field = [&](auto key, auto& merged_field, auto const& fields_field, auto const& additional_fields_field) { + // a. Let key be the value in the Enumeration Key column of the row. + + // b. If fieldsKeys contains key and overriddenKeys does not contain key, then + if (fields_keys.contains_slow(key) && !overridden_keys.contains_slow(key)) { + // i. Let propValue be fields' field whose name is given in the Field Name column of the row. + // ii. Set merged's field whose name is given in the Field Name column of the row to propValue. + merged_field = fields_field; + } + + // c. If additionalKeys contains key, then + if (additional_keys.contains_slow(key)) { + // i. Let propValue be additionalFields' field whose name is given in the Field Name column of the row. + // ii. Set merged's field whose name is given in the Field Name column of the row to propValue. + merged_field = additional_fields_field; + } + }; + + // 5. For each row of Table 19, except the header row, do +#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \ + merge_field(enumeration, merged.field_name, fields.field_name, additional_fields.field_name); + JS_ENUMERATE_CALENDAR_FIELDS +#undef __JS_ENUMERATE + + // 6. Return merged. + return merged; +} + // 12.2.8 ToTemporalCalendarIdentifier ( temporalCalendarLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalcalendaridentifier ThrowCompletionOr to_temporal_calendar_identifier(VM& vm, Value temporal_calendar_like) { @@ -328,6 +395,15 @@ String format_calendar_annotation(StringView id, ShowCalendar show_calendar) return MUST(String::formatted("[{}u-ca={}]", flag, id)); } +// 12.2.14 CalendarEquals ( one, two ), https://tc39.es/proposal-temporal/#sec-temporal-calendarequals +bool calendar_equals(StringView one, StringView two) +{ + // 1. If CanonicalizeUValue("ca", one) is CanonicalizeUValue("ca", two), return true. + // 2. Return false. + return Unicode::canonicalize_unicode_extension_values("ca"sv, one) + == Unicode::canonicalize_unicode_extension_values("ca"sv, two); +} + // 12.2.15 ISODaysInMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-isodaysinmonth u8 iso_days_in_month(double year, double month) { @@ -532,6 +608,39 @@ Vector calendar_extra_fields(StringView calendar, CalendarFieldLi return {}; } +// 12.2.23 CalendarFieldKeysToIgnore ( calendar, keys ), https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeystoignore +Vector calendar_field_keys_to_ignore(StringView calendar, ReadonlySpan keys) +{ + // 1. If calendar is "iso8601", then + if (calendar == "iso8601"sv) { + // a. Let ignoredKeys be an empty List. + Vector ignored_keys; + + // b. For each element key of keys, do + for (auto key : keys) { + // i. Append key to ignoredKeys. + ignored_keys.append(key); + + // ii. If key is MONTH, append MONTH-CODE to ignoredKeys. + if (key == CalendarField::Month) + ignored_keys.append(CalendarField::MonthCode); + // iii. Else if key is MONTH-CODE, append MONTH to ignoredKeys. + else if (key == CalendarField::MonthCode) + ignored_keys.append(CalendarField::Month); + } + + // c. NOTE: While ignoredKeys can have duplicate elements, this is not intended to be meaningful. This specification + // only checks whether particular keys are or are not members of the list. + + // d. Return ignoredKeys. + return ignored_keys; + } + + // 2. Return an implementation-defined List as described below. + // FIXME: Return keys for an ISO8601 calendar for now. + return calendar_field_keys_to_ignore("iso8601"sv, keys); +} + // 12.2.24 CalendarResolveFields ( calendar, fields, type ), https://tc39.es/proposal-temporal/#sec-temporal-calendarresolvefields ThrowCompletionOr calendar_resolve_fields(VM& vm, StringView calendar, CalendarFields& fields, DateType type) { diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Libraries/LibJS/Runtime/Temporal/Calendar.h index c54b5b22d29..b7a620ce354 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.h +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -101,15 +101,19 @@ Vector const& available_calendars(); ThrowCompletionOr prepare_calendar_fields(VM&, StringView calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names); ThrowCompletionOr calendar_month_day_from_fields(VM&, StringView calendar, CalendarFields, Overflow); String format_calendar_annotation(StringView id, ShowCalendar); +bool calendar_equals(StringView one, StringView two); u8 iso_days_in_month(double year, double month); YearWeek iso_week_of_year(ISODate const&); u16 iso_day_of_year(ISODate const&); u8 iso_day_of_week(ISODate const&); +Vector calendar_field_keys_present(CalendarFields const&); +CalendarFields calendar_merge_fields(StringView calendar, CalendarFields const& fields, CalendarFields const& additional_fields); ThrowCompletionOr to_temporal_calendar_identifier(VM&, Value temporal_calendar_like); ThrowCompletionOr get_temporal_calendar_identifier_with_iso_default(VM&, Object const& item); ThrowCompletionOr calendar_month_day_to_iso_reference_date(VM&, StringView calendar, CalendarFields const&, Overflow); CalendarDate calendar_iso_to_date(StringView calendar, ISODate const&); Vector calendar_extra_fields(StringView calendar, CalendarFieldList); +Vector calendar_field_keys_to_ignore(StringView calendar, ReadonlySpan); ThrowCompletionOr calendar_resolve_fields(VM&, StringView calendar, CalendarFields&, DateType); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp index 5fe270d0169..71eafb59115 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp @@ -111,4 +111,35 @@ bool iso_date_within_limits(ISODate iso_date) return iso_date_time_within_limits(iso_date_time); } +// 3.5.12 CompareISODate ( isoDate1, isoDate2 ), https://tc39.es/proposal-temporal/#sec-temporal-compareisodate +i8 compare_iso_date(ISODate iso_date1, ISODate iso_date2) +{ + // 1. If isoDate1.[[Year]] > isoDate2.[[Year]], return 1. + if (iso_date1.year > iso_date2.year) + return 1; + + // 2. If isoDate1.[[Year]] < isoDate2.[[Year]], return -1. + if (iso_date1.year < iso_date2.year) + return -1; + + // 3. If isoDate1.[[Month]] > isoDate2.[[Month]], return 1. + if (iso_date1.month > iso_date2.month) + return 1; + + // 4. If isoDate1.[[Month]] < isoDate2.[[Month]], return -1. + if (iso_date1.month < iso_date2.month) + return -1; + + // 5. If isoDate1.[[Day]] > isoDate2.[[Day]], return 1. + if (iso_date1.day > iso_date2.day) + return 1; + + // 6. If isoDate1.[[Day]] < isoDate2.[[Day]], return -1. + if (iso_date1.day < iso_date2.day) + return -1; + + // 7. Return 0. + return 0; +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.h b/Libraries/LibJS/Runtime/Temporal/PlainDate.h index 310510fc23d..7a235c9d7fa 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDate.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.h @@ -27,5 +27,6 @@ ThrowCompletionOr regulate_iso_date(VM& vm, double year, double month, bool is_valid_iso_date(double year, double month, double day); String pad_iso_year(i32 year); bool iso_date_within_limits(ISODate); +i8 compare_iso_date(ISODate, ISODate); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp index 2d4a68a5b59..f8f92146ac6 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp @@ -6,6 +6,7 @@ */ #include +#include #include #include #include @@ -34,6 +35,8 @@ void PlainMonthDayPrototype::initialize(Realm& realm) define_native_accessor(realm, vm.names.day, day_getter, {}, Attribute::Configurable); u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.with, with, 1, attr); + define_native_function(realm, vm.names.equals, equals, 1, attr); define_native_function(realm, vm.names.toString, to_string, 0, attr); define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr); define_native_function(realm, vm.names.toJSON, to_json, 0, attr); @@ -72,6 +75,63 @@ JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::day_getter) return calendar_iso_to_date(month_day->calendar(), month_day->iso_date()).day; } +// 10.3.6 Temporal.PlainMonthDay.prototype.with ( temporalMonthDayLike [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.with +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::with) +{ + auto temporal_month_day_like = vm.argument(0); + auto options = vm.argument(1); + + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. If ? IsPartialTemporalObject(temporalMonthDayLike) is false, throw a TypeError exception. + if (!TRY(is_partial_temporal_object(vm, temporal_month_day_like))) + return vm.throw_completion(ErrorType::TemporalObjectMustBePartialTemporalObject); + + // 4. Let calendar be monthDay.[[Calendar]]. + auto const& calendar = month_day->calendar(); + + // 5. Let fields be ISODateToFields(calendar, monthDay.[[ISODate]], MONTH-DAY). + auto fields = iso_date_to_fields(calendar, month_day->iso_date(), DateType::MonthDay); + + // 6. Let partialMonthDay be ? PrepareCalendarFields(calendar, temporalMonthDayLike, « YEAR, MONTH, MONTH-CODE, DAY », « », PARTIAL). + auto partial_month_day = TRY(prepare_calendar_fields(vm, calendar, temporal_month_day_like.as_object(), { { CalendarField::Year, CalendarField::Month, CalendarField::MonthCode, CalendarField::Day } }, {}, Partial {})); + + // 7. Set fields to CalendarMergeFields(calendar, fields, partialMonthDay). + fields = calendar_merge_fields(calendar, fields, partial_month_day); + + // 8. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // 9. Let overflow be ? GetTemporalOverflowOption(resolvedOptions). + auto overflow = TRY(get_temporal_overflow_option(vm, resolved_options)); + + // 10. Let isoDate be ? CalendarMonthDayFromFields(calendar, fields, overflow). + auto iso_date = TRY(calendar_month_day_from_fields(vm, calendar, fields, overflow)); + + // 11. Return ! CreateTemporalMonthDay(isoDate, calendar). + return MUST(create_temporal_month_day(vm, iso_date, calendar)); +} + +// 10.3.7 Temporal.PlainMonthDay.prototype.equals ( other ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.equals +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::equals) +{ + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. Set other to ? ToTemporalMonthDay(other). + auto other = TRY(to_temporal_month_day(vm, vm.argument(0))); + + // 4. If CompareISODate(monthDay.[[ISODate]], other.[[ISODate]]) ≠ 0, return false. + if (compare_iso_date(month_day->iso_date(), other->iso_date()) != 0) + return false; + + // 5. Return CalendarEquals(monthDay.[[Calendar]], other.[[Calendar]]). + return calendar_equals(month_day->calendar(), other->calendar()); +} + // 10.3.8 Temporal.PlainMonthDay.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.tostring JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::to_string) { diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h index 6eb48a84078..ace7fbc8005 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h @@ -26,6 +26,8 @@ private: JS_DECLARE_NATIVE_FUNCTION(calendar_id_getter); JS_DECLARE_NATIVE_FUNCTION(month_code_getter); JS_DECLARE_NATIVE_FUNCTION(day_getter); + JS_DECLARE_NATIVE_FUNCTION(with); + JS_DECLARE_NATIVE_FUNCTION(equals); JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(to_locale_string); JS_DECLARE_NATIVE_FUNCTION(to_json); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.equals.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.equals.js new file mode 100644 index 00000000000..cff03493229 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.equals.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainMonthDay.prototype.equals).toHaveLength(1); + }); + + test("basic functionality", () => { + const firstPlainMonthDay = new Temporal.PlainMonthDay(2, 1, "iso8601"); + const secondPlainMonthDay = new Temporal.PlainMonthDay(1, 1, "iso8601"); + expect(firstPlainMonthDay.equals(firstPlainMonthDay)).toBeTrue(); + expect(secondPlainMonthDay.equals(secondPlainMonthDay)).toBeTrue(); + expect(firstPlainMonthDay.equals(secondPlainMonthDay)).toBeFalse(); + expect(secondPlainMonthDay.equals(firstPlainMonthDay)).toBeFalse(); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.with.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.with.js new file mode 100644 index 00000000000..596dc0cf3bc --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.with.js @@ -0,0 +1,57 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainMonthDay.prototype.with).toHaveLength(1); + }); + + test("basic functionality", () => { + const plainMonthDay = new Temporal.PlainMonthDay(1, 1); + const values = [ + [{ monthCode: "M07" }, new Temporal.PlainMonthDay(7, 1)], + [{ monthCode: "M07", day: 6 }, new Temporal.PlainMonthDay(7, 6)], + [{ year: 0, month: 7, day: 6 }, new Temporal.PlainMonthDay(7, 6)], + ]; + for (const [arg, expected] of values) { + expect(plainMonthDay.with(arg).equals(expected)).toBeTrue(); + } + + // Supplying the same values doesn't change the month/day, but still creates a new object + const plainMonthDayLike = { + month: plainMonthDay.month, + day: plainMonthDay.day, + }; + expect(plainMonthDay.with(plainMonthDayLike)).not.toBe(plainMonthDay); + expect(plainMonthDay.with(plainMonthDayLike).equals(plainMonthDay)).toBeTrue(); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Temporal.PlainMonthDay.prototype.with.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); + + test("argument must be an object", () => { + expect(() => { + new Temporal.PlainMonthDay(1, 1).with("foo"); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + expect(() => { + new Temporal.PlainMonthDay(1, 1).with(42); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + }); + + test("argument must have at least one Temporal property", () => { + expect(() => { + new Temporal.PlainMonthDay(1, 1).with({}); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + }); + + test("argument must not have 'calendar' or 'timeZone'", () => { + expect(() => { + new Temporal.PlainMonthDay(1, 1).with({ calendar: {} }); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + expect(() => { + new Temporal.PlainMonthDay(1, 1).with({ timeZone: {} }); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + }); +}); From d1a988930f22af9b4c5cbdee4fc53de79507dc91 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 20 Nov 2024 18:12:23 -0500 Subject: [PATCH 5/6] LibJS: Implement Temporal.PlainMonthDay.prototype.valueOf --- .../LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp | 8 ++++++++ .../LibJS/Runtime/Temporal/PlainMonthDayPrototype.h | 1 + .../PlainMonthDay/PlainMonthDay.prototype.valueOf.js | 11 +++++++++++ 3 files changed, 20 insertions(+) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.valueOf.js diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp index f8f92146ac6..325dc5e7745 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp @@ -40,6 +40,7 @@ void PlainMonthDayPrototype::initialize(Realm& realm) define_native_function(realm, vm.names.toString, to_string, 0, attr); define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr); define_native_function(realm, vm.names.toJSON, to_json, 0, attr); + define_native_function(realm, vm.names.valueOf, value_of, 0, attr); } // 10.3.3 get Temporal.PlainMonthDay.prototype.calendarId, https://tc39.es/proposal-temporal/#sec-get-temporal.plainmonthday.prototype.calendarid @@ -172,4 +173,11 @@ JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::to_json) return PrimitiveString::create(vm, temporal_month_day_to_string(month_day, ShowCalendar::Auto)); } +// 10.3.11 Temporal.PlainMonthDay.prototype.valueOf ( ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.valueof +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::value_of) +{ + // 1. Throw a TypeError exception. + return vm.throw_completion(ErrorType::Convert, "Temporal.PlainMonthDay", "a primitive value"); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h index ace7fbc8005..2780f77aa8c 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h @@ -31,6 +31,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(to_locale_string); JS_DECLARE_NATIVE_FUNCTION(to_json); + JS_DECLARE_NATIVE_FUNCTION(value_of); }; } diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.valueOf.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.valueOf.js new file mode 100644 index 00000000000..f8f48e07fc3 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.valueOf.js @@ -0,0 +1,11 @@ +describe("errors", () => { + test("throws TypeError", () => { + const plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(() => { + plainMonthDay.valueOf(); + }).toThrowWithMessage( + TypeError, + "Cannot convert Temporal.PlainMonthDay to a primitive value" + ); + }); +}); From 91e25ca5888004fa5a45acd60d643b6ee44ad489 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 20 Nov 2024 20:06:42 -0500 Subject: [PATCH 6/6] LibJS: Use Temporal's ISO8601 parser to parse UTC offset strings We now have the Temporal facilities to implement the Date AOs which parse UTC offset strings using the ISO8601 parser. This patch updates those AOs and their callers in accordance with the Temporal spec. --- Libraries/LibJS/Runtime/Date.cpp | 276 +++++------------- Libraries/LibJS/Runtime/Date.h | 17 +- Libraries/LibJS/Runtime/DatePrototype.cpp | 71 ++--- .../Intl/DateTimeFormatConstructor.cpp | 25 +- .../Runtime/Temporal/AbstractOperations.cpp | 5 +- Libraries/LibJS/Runtime/Temporal/TimeZone.cpp | 3 +- 6 files changed, 113 insertions(+), 284 deletions(-) diff --git a/Libraries/LibJS/Runtime/Date.cpp b/Libraries/LibJS/Runtime/Date.cpp index 8a2a45d359b..48e571a5c60 100644 --- a/Libraries/LibJS/Runtime/Date.cpp +++ b/Libraries/LibJS/Runtime/Date.cpp @@ -1,20 +1,19 @@ /* * Copyright (c) 2020-2023, Linus Groh - * Copyright (c) 2022-2024, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ -#include -#include #include -#include #include #include #include #include #include #include +#include +#include #include namespace JS { @@ -457,7 +456,7 @@ String system_time_zone_identifier() // time zone identifier or an offset time zone identifier. auto system_time_zone_string = Unicode::current_time_zone(); - if (!is_time_zone_offset_string(system_time_zone_string)) { + if (!is_offset_time_zone_identifier(system_time_zone_string)) { auto time_zone_identifier = Intl::get_available_named_time_zone_identifier(system_time_zone_string); if (!time_zone_identifier.has_value()) return "UTC"_string; @@ -476,46 +475,55 @@ void clear_system_time_zone_cache() } // 21.4.1.25 LocalTime ( t ), https://tc39.es/ecma262/#sec-localtime +// 14.5.6 LocalTime ( t ), https://tc39.es/proposal-temporal/#sec-localtime double local_time(double time) { // 1. Let systemTimeZoneIdentifier be SystemTimeZoneIdentifier(). auto system_time_zone_identifier = JS::system_time_zone_identifier(); + // 2. Let parseResult be ! ParseTimeZoneIdentifier(systemTimeZoneIdentifier). + auto parse_result = Temporal::parse_time_zone_identifier(system_time_zone_identifier); + double offset_nanoseconds { 0 }; - // 2. If IsTimeZoneOffsetString(systemTimeZoneIdentifier) is true, then - if (is_time_zone_offset_string(system_time_zone_identifier)) { - // a. Let offsetNs be ParseTimeZoneOffsetString(systemTimeZoneIdentifier). - offset_nanoseconds = parse_time_zone_offset_string(system_time_zone_identifier); + // 3. If parseResult.[[OffsetMinutes]] is not EMPTY, then + if (parse_result.offset_minutes.has_value()) { + // a. Let offsetNs be parseResult.[[OffsetMinutes]] × (60 × 10**9). + offset_nanoseconds = static_cast(*parse_result.offset_minutes) * 60'000'000'000; } - // 3. Else, + // 4. Else, else { // a. Let offsetNs be GetNamedTimeZoneOffsetNanoseconds(systemTimeZoneIdentifier, ℤ(ℝ(t) × 10^6)). auto offset = get_named_time_zone_offset_milliseconds(system_time_zone_identifier, time); offset_nanoseconds = static_cast(offset.offset.to_nanoseconds()); } - // 4. Let offsetMs be truncate(offsetNs / 10^6). + // 5. Let offsetMs be truncate(offsetNs / 10^6). auto offset_milliseconds = trunc(offset_nanoseconds / 1e6); - // 5. Return t + 𝔽(offsetMs). + // 6. Return t + 𝔽(offsetMs). return time + offset_milliseconds; } // 21.4.1.26 UTC ( t ), https://tc39.es/ecma262/#sec-utc-t +// 14.5.7 UTC ( t ), https://tc39.es/proposal-temporal/#sec-localtime +// FIXME: Update the rest of this AO for Temporal once we have the required Temporal objects. double utc_time(double time) { // 1. Let systemTimeZoneIdentifier be SystemTimeZoneIdentifier(). auto system_time_zone_identifier = JS::system_time_zone_identifier(); + // 2. Let parseResult be ! ParseTimeZoneIdentifier(systemTimeZoneIdentifier). + auto parse_result = Temporal::parse_time_zone_identifier(system_time_zone_identifier); + double offset_nanoseconds { 0 }; - // 2. If IsTimeZoneOffsetString(systemTimeZoneIdentifier) is true, then - if (is_time_zone_offset_string(system_time_zone_identifier)) { - // a. Let offsetNs be ParseTimeZoneOffsetString(systemTimeZoneIdentifier). - offset_nanoseconds = parse_time_zone_offset_string(system_time_zone_identifier); + // 3. If parseResult.[[OffsetMinutes]] is not EMPTY, then + if (parse_result.offset_minutes.has_value()) { + // a. Let offsetNs be parseResult.[[OffsetMinutes]] × (60 × 10**9). + offset_nanoseconds = static_cast(*parse_result.offset_minutes) * 60'000'000'000; } - // 3. Else, + // 4. Else, else { // a. Let possibleInstants be GetNamedTimeZoneEpochNanoseconds(systemTimeZoneIdentifier, ℝ(YearFromTime(t)), ℝ(MonthFromTime(t)) + 1, ℝ(DateFromTime(t)), ℝ(HourFromTime(t)), ℝ(MinFromTime(t)), ℝ(SecFromTime(t)), ℝ(msFromTime(t)), 0, 0). auto possible_instants = get_named_time_zone_epoch_nanoseconds(system_time_zone_identifier, year_from_time(time), month_from_time(time) + 1, date_from_time(time), hour_from_time(time), min_from_time(time), sec_from_time(time), ms_from_time(time), 0, 0); @@ -544,10 +552,10 @@ double utc_time(double time) offset_nanoseconds = static_cast(offset.offset.to_nanoseconds()); } - // 4. Let offsetMs be truncate(offsetNs / 10^6). + // 5. Let offsetMs be truncate(offsetNs / 10^6). auto offset_milliseconds = trunc(offset_nanoseconds / 1e6); - // 5. Return t - 𝔽(offsetMs). + // 6. Return t - 𝔽(offsetMs). return time - offset_milliseconds; } @@ -636,171 +644,12 @@ double time_clip(double time) return to_integer_or_infinity(time); } -// 21.4.1.33 Time Zone Offset String Format, https://tc39.es/ecma262/#sec-time-zone-offset-strings -Optional parse_utc_offset(StringView offset_string) -{ - GenericLexer lexer { offset_string }; - UTCOffset parse_result; - - // https://tc39.es/ecma262/#prod-ASCIISign - auto parse_ascii_sign = [&]() { - // ASCIISign ::: one of - // + - - if (lexer.next_is(is_any_of("+-"sv))) { - parse_result.sign = lexer.consume(); - return true; - } - - return false; - }; - - auto parse_two_digits = [&](size_t max_value) -> Optional { - if (auto digits = lexer.peek_string(2); digits.has_value()) { - auto number = digits->to_number(TrimWhitespace::No); - - if (number.has_value() && *number <= max_value) { - lexer.ignore(2); - return *number; - } - } - - return {}; - }; - - // https://tc39.es/ecma262/#prod-Hour - auto parse_hour = [&]() { - // Hour ::: - // 0 DecimalDigit - // 1 DecimalDigit - // 20 - // 21 - // 22 - // 23 - parse_result.hour = parse_two_digits(23); - return parse_result.hour.has_value(); - }; - - // https://tc39.es/ecma262/#prod-TimeSeparator - auto parse_time_separator = [&](auto extended) { - // TimeSeparator[Extended] ::: - // [+Extended] : - // [~Extended] [empty] - if (extended) - return lexer.consume_specific(':'); - return true; - }; - - // https://tc39.es/ecma262/#prod-MinuteSecond - auto parse_minute_second = [&](auto& result) { - // MinuteSecond ::: - // 0 DecimalDigit - // 1 DecimalDigit - // 2 DecimalDigit - // 3 DecimalDigit - // 4 DecimalDigit - // 5 DecimalDigit - result = parse_two_digits(59); - return result.has_value(); - }; - - // https://tc39.es/ecma262/#prod-TemporalDecimalSeparator - auto parse_temporal_decimal_separator = [&]() { - // TemporalDecimalSeparator ::: one of - // . , - return lexer.consume_specific('.') || lexer.consume_specific(','); - }; - - // https://tc39.es/ecma262/#prod-TemporalDecimalFraction - auto parse_temporal_decimal_fraction = [&]() { - // TemporalDecimalFraction ::: - // TemporalDecimalSeparator DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - auto position = lexer.tell(); - - if (!parse_temporal_decimal_separator()) - return false; - - for (size_t i = 0; i < 9; ++i) { - if (!lexer.next_is(is_ascii_digit)) - break; - lexer.ignore(); - } - - if (auto fraction = lexer.input().substring_view(position, lexer.tell() - position); fraction.length() > 1) { - parse_result.fraction = fraction; - return true; - } - - return false; - }; - - // https://tc39.es/ecma262/#prod-HourSubcomponents - auto parse_hour_subcomponents = [&](auto extended) { - // HourSubcomponents[Extended] ::: - // TimeSeparator[?Extended] MinuteSecond - // TimeSeparator[?Extended] MinuteSecond TimeSeparator[?Extended] MinuteSecond TemporalDecimalFraction[opt] - ArmedScopeGuard guard { [&, position = lexer.tell()]() { lexer.retreat(lexer.tell() - position); } }; - - if (!parse_time_separator(extended)) - return false; - if (!parse_minute_second(parse_result.minute)) - return false; - - if (lexer.is_eof()) { - guard.disarm(); - return true; - } - - if (!parse_time_separator(extended)) - return false; - if (!parse_minute_second(parse_result.second)) - return false; - - if (lexer.is_eof()) { - guard.disarm(); - return true; - } - - if (!parse_temporal_decimal_fraction()) - return false; - - guard.disarm(); - return true; - }; - - // https://tc39.es/ecma262/#prod-UTCOffset - // UTCOffset ::: - // ASCIISign Hour - // ASCIISign Hour HourSubcomponents[+Extended] - // ASCIISign Hour HourSubcomponents[~Extended] - if (!parse_ascii_sign()) - return {}; - if (!parse_hour()) - return {}; - - if (lexer.is_eof()) - return parse_result; - - if (!parse_hour_subcomponents(true) && !parse_hour_subcomponents(false)) - return {}; - if (lexer.is_eof()) - return parse_result; - - return {}; -} - // 21.4.1.33.1 IsTimeZoneOffsetString ( offsetString ), https://tc39.es/ecma262/#sec-istimezoneoffsetstring -bool is_time_zone_offset_string(StringView offset_string) +// 14.5.10 IsOffsetTimeZoneIdentifier ( offsetString ), https://tc39.es/proposal-temporal/#sec-isoffsettimezoneidentifier +bool is_offset_time_zone_identifier(StringView offset_string) { - // 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset). - auto parse_result = parse_utc_offset(offset_string); + // 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset[~SubMinutePrecision]). + auto parse_result = Temporal::parse_utc_offset(offset_string, Temporal::SubMinutePrecision::No); // 2. If parseResult is a List of errors, return false. // 3. Return true. @@ -808,66 +657,83 @@ bool is_time_zone_offset_string(StringView offset_string) } // 21.4.1.33.2 ParseTimeZoneOffsetString ( offsetString ), https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring -double parse_time_zone_offset_string(StringView offset_string) +// 14.5.11 ParseDateTimeUTCOffset ( offsetString ), https://tc39.es/proposal-temporal/#sec-parsedatetimeutcoffset +ThrowCompletionOr parse_date_time_utc_offset(VM& vm, StringView offset_string) { - // 1. Let parseResult be ParseText(offsetString, UTCOffset). - auto parse_result = parse_utc_offset(offset_string); + // 1. Let parseResult be ParseText(offsetString, UTCOffset[+SubMinutePrecision]). + auto parse_result = Temporal::parse_utc_offset(offset_string, Temporal::SubMinutePrecision::Yes); - // 2. Assert: parseResult is not a List of errors. + // 2. If parseResult is a List of errors, throw a RangeError exception. + if (!parse_result.has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, offset_string); + + return parse_date_time_utc_offset(*parse_result); +} + +// 21.4.1.33.2 ParseTimeZoneOffsetString ( offsetString ), https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring +// 14.5.11 ParseDateTimeUTCOffset ( offsetString ), https://tc39.es/proposal-temporal/#sec-parsedatetimeutcoffset +double parse_date_time_utc_offset(StringView offset_string) +{ + // OPTIMIZATION: Some callers can assume that parsing will succeed. + + // 1. Let parseResult be ParseText(offsetString, UTCOffset[+SubMinutePrecision]). + auto parse_result = Temporal::parse_utc_offset(offset_string, Temporal::SubMinutePrecision::Yes); VERIFY(parse_result.has_value()); + return parse_date_time_utc_offset(*parse_result); +} + +// 21.4.1.33.2 ParseTimeZoneOffsetString ( offsetString ), https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring +// 14.5.11 ParseDateTimeUTCOffset ( offsetString ), https://tc39.es/proposal-temporal/#sec-parsedatetimeutcoffset +double parse_date_time_utc_offset(Temporal::TimeZoneOffset const& parse_result) +{ + // OPTIMIZATION: Some callers will have already parsed and validated the time zone identifier. + // 3. Assert: parseResult contains a ASCIISign Parse Node. - VERIFY(parse_result->sign.has_value()); + VERIFY(parse_result.sign.has_value()); // 4. Let parsedSign be the source text matched by the ASCIISign Parse Node contained within parseResult. - auto parsed_sign = *parse_result->sign; - i8 sign { 0 }; - // 5. If parsedSign is the single code point U+002D (HYPHEN-MINUS), then - if (parsed_sign == '-') { - // a. Let sign be -1. - sign = -1; - } + // a. Let sign be -1. // 6. Else, - else { - // a. Let sign be 1. - sign = 1; - } + // a. Let sign be 1. + auto sign = parse_result.sign == '-' ? -1 : 1; - // 7. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed values is guaranteed to be a sufficiently short string of decimal digits. + // 7. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed values is guaranteed + // to be a sufficiently short string of decimal digits. // 8. Assert: parseResult contains an Hour Parse Node. - VERIFY(parse_result->hour.has_value()); + VERIFY(parse_result.hours.has_value()); // 9. Let parsedHours be the source text matched by the Hour Parse Node contained within parseResult. // 10. Let hours be ℝ(StringToNumber(CodePointsToString(parsedHours))). - auto hours = *parse_result->hour; + auto hours = parse_result.hours->to_number().value(); // 11. If parseResult does not contain a MinuteSecond Parse Node, then // a. Let minutes be 0. // 12. Else, // a. Let parsedMinutes be the source text matched by the first MinuteSecond Parse Node contained within parseResult. // b. Let minutes be ℝ(StringToNumber(CodePointsToString(parsedMinutes))). - double minutes = parse_result->minute.value_or(0); + double minutes = parse_result.minutes.has_value() ? parse_result.minutes->to_number().value() : 0; // 13. If parseResult does not contain two MinuteSecond Parse Nodes, then // a. Let seconds be 0. // 14. Else, // a. Let parsedSeconds be the source text matched by the second secondSecond Parse Node contained within parseResult. // b. Let seconds be ℝ(StringToNumber(CodePointsToString(parsedSeconds))). - double seconds = parse_result->second.value_or(0); + double seconds = parse_result.seconds.has_value() ? parse_result.seconds->to_number().value() : 0; double nanoseconds = 0; // 15. If parseResult does not contain a TemporalDecimalFraction Parse Node, then - if (!parse_result->fraction.has_value()) { + if (!parse_result.fraction.has_value()) { // a. Let nanoseconds be 0. nanoseconds = 0; } // 16. Else, else { // a. Let parsedFraction be the source text matched by the TemporalDecimalFraction Parse Node contained within parseResult. - auto parsed_fraction = *parse_result->fraction; + auto parsed_fraction = *parse_result.fraction; // b. Let fraction be the string-concatenation of CodePointsToString(parsedFraction) and "000000000". auto fraction = ByteString::formatted("{}000000000", parsed_fraction); diff --git a/Libraries/LibJS/Runtime/Date.h b/Libraries/LibJS/Runtime/Date.h index 91036948e83..dd522773e3c 100644 --- a/Libraries/LibJS/Runtime/Date.h +++ b/Libraries/LibJS/Runtime/Date.h @@ -1,6 +1,6 @@ /* * Copyright (c) 2020-2022, Linus Groh - * Copyright (c) 2022-2024, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -58,14 +58,6 @@ constexpr inline double ms_per_day = 86'400'000; constexpr inline double ns_per_day = 86'400'000'000'000; extern Crypto::SignedBigInteger const ns_per_day_bigint; -struct UTCOffset { - Optional sign; - Optional hour; - Optional minute; - Optional second; - Optional fraction; -}; - double day(double); double time_within_day(double); u16 days_in_year(i32); @@ -93,8 +85,9 @@ double make_time(double hour, double min, double sec, double ms); double make_day(double year, double month, double date); double make_date(double day, double time); double time_clip(double time); -Optional parse_utc_offset(StringView); -bool is_time_zone_offset_string(StringView offset_string); -double parse_time_zone_offset_string(StringView offset_string); +bool is_offset_time_zone_identifier(StringView offset_string); +ThrowCompletionOr parse_date_time_utc_offset(VM&, StringView offset_string); +double parse_date_time_utc_offset(StringView offset_string); +double parse_date_time_utc_offset(Temporal::TimeZoneOffset const&); } diff --git a/Libraries/LibJS/Runtime/DatePrototype.cpp b/Libraries/LibJS/Runtime/DatePrototype.cpp index 9f12a403535..14740241eb8 100644 --- a/Libraries/LibJS/Runtime/DatePrototype.cpp +++ b/Libraries/LibJS/Runtime/DatePrototype.cpp @@ -2,7 +2,7 @@ * Copyright (c) 2020-2023, Linus Groh * Copyright (c) 2021, Petróczi Zoltán * Copyright (c) 2021, Idan Horowitz - * Copyright (c) 2022, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include #include #include @@ -1066,19 +1068,14 @@ JS_DEFINE_NATIVE_FUNCTION(DatePrototype::to_string) } // 21.4.4.41.1 TimeString ( tv ), https://tc39.es/ecma262/#sec-timestring +// 14.5.8 TimeString ( tv ), https://tc39.es/proposal-temporal/#sec-timestring ByteString time_string(double time) { - // 1. Let hour be ToZeroPaddedDecimalString(ℝ(HourFromTime(tv)), 2). - auto hour = hour_from_time(time); + // 1. Let timeString be FormatTimeString(ℝ(HourFromTime(tv)), ℝ(MinFromTime(tv)), ℝ(SecFromTime(tv)), 0, 0). + auto time_string = Temporal::format_time_string(hour_from_time(time), min_from_time(time), sec_from_time(time), 0, 0); - // 2. Let minute be ToZeroPaddedDecimalString(ℝ(MinFromTime(tv)), 2). - auto minute = min_from_time(time); - - // 3. Let second be ToZeroPaddedDecimalString(ℝ(SecFromTime(tv)), 2). - auto second = sec_from_time(time); - - // 4. Return the string-concatenation of hour, ":", minute, ":", second, the code unit 0x0020 (SPACE), and "GMT". - return ByteString::formatted("{:02}:{:02}:{:02} GMT", hour, minute, second); + // 4. Return the string-concatenation of timeString, the code unit 0x0020 (SPACE), and "GMT". + return ByteString::formatted("{} GMT", time_string); } // 21.4.4.41.2 DateString ( tv ), https://tc39.es/ecma262/#sec-datestring @@ -1105,62 +1102,40 @@ ByteString date_string(double time) } // 21.4.4.41.3 TimeZoneString ( tv ), https://tc39.es/ecma262/#sec-timezoneestring +// 14.5.9 TimeZoneString ( tv ), https://tc39.es/proposal-temporal/#sec-timezoneestring ByteString time_zone_string(double time) { // 1. Let systemTimeZoneIdentifier be SystemTimeZoneIdentifier(). auto system_time_zone_identifier = JS::system_time_zone_identifier(); - double offset_nanoseconds { 0 }; + // 2. Let offsetMinutes be ! ParseTimeZoneIdentifier(systemTimeZoneIdentifier).[[OffsetMinutes]]. + auto offset_minutes = Temporal::parse_time_zone_identifier(system_time_zone_identifier).offset_minutes; auto in_dst = Unicode::TimeZoneOffset::InDST::No; - // 2. If IsTimeZoneOffsetString(systemTimeZoneIdentifier) is true, then - if (is_time_zone_offset_string(system_time_zone_identifier)) { - // a. Let offsetNs be ParseTimeZoneOffsetString(systemTimeZoneIdentifier). - offset_nanoseconds = parse_time_zone_offset_string(system_time_zone_identifier); - } - // 3. Else, - else { + // 2. If offsetMinutes is EMPTY, then + if (!offset_minutes.has_value()) { // a. Let offsetNs be GetNamedTimeZoneOffsetNanoseconds(systemTimeZoneIdentifier, ℤ(ℝ(tv) × 10^6)). auto offset = get_named_time_zone_offset_milliseconds(system_time_zone_identifier, time); - - offset_nanoseconds = static_cast(offset.offset.to_nanoseconds()); in_dst = offset.in_dst; + + // b. Set offsetMinutes to truncate(offsetNs / (60 × 10**9)). + offset_minutes = trunc(static_cast(offset.offset.to_nanoseconds()) / 60'000'000'000.0); } - // 4. Let offset be 𝔽(truncate(offsetNs / 106)). - auto offset = trunc(offset_nanoseconds / 1e6); + // 3. Let offsetString be FormatOffsetTimeZoneIdentifier(offsetMinutes, UNSEPARATED). + auto offset_string = Temporal::format_offset_time_zone_identifier(*offset_minutes, Temporal::TimeStyle::Unseparated); - StringView offset_sign; - - // 5. If offset is +0𝔽 or offset > +0𝔽, then - if (offset >= 0) { - // a. Let offsetSign be "+". - offset_sign = "+"sv; - // b. Let absOffset be offset. - } - // 6. Else, - else { - // a. Let offsetSign be "-". - offset_sign = "-"sv; - // b. Let absOffset be -offset. - offset *= -1; - } - - // 7. Let offsetMin be ToZeroPaddedDecimalString(ℝ(MinFromTime(absOffset)), 2). - auto offset_min = min_from_time(offset); - - // 8. Let offsetHour be ToZeroPaddedDecimalString(ℝ(HourFromTime(absOffset)), 2). - auto offset_hour = hour_from_time(offset); - - // 9. Let tzName be an implementation-defined string that is either the empty String or the string-concatenation of the code unit 0x0020 (SPACE), the code unit 0x0028 (LEFT PARENTHESIS), an implementation-defined timezone name, and the code unit 0x0029 (RIGHT PARENTHESIS). + // 5. Let tzName be an implementation-defined string that is either the empty String or the string-concatenation of + // the code unit 0x0020 (SPACE), the code unit 0x0028 (LEFT PARENTHESIS), an implementation-defined timezone name, + // and the code unit 0x0029 (RIGHT PARENTHESIS). auto tz_name = Unicode::current_time_zone(); // Most implementations seem to prefer the long-form display name of the time zone. Not super important, but we may as well match that behavior. if (auto name = Unicode::time_zone_display_name(Unicode::default_locale(), tz_name, in_dst, time); name.has_value()) tz_name = name.release_value(); - // 10. Return the string-concatenation of offsetSign, offsetHour, offsetMin, and tzName. - return ByteString::formatted("{}{:02}{:02} ({})", offset_sign, offset_hour, offset_min, tz_name); + // 10. Return the string-concatenation of offsetString and tzName. + return ByteString::formatted("{} ({})", offset_string, tz_name); } // 21.4.4.41.4 ToDateString ( tv ), https://tc39.es/ecma262/#sec-todatestring diff --git a/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp b/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp index 64a9187ae6f..a6e946d394f 100644 --- a/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp +++ b/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -81,6 +82,8 @@ JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatConstructor::supported_locales_of) } // 11.1.2 CreateDateTimeFormat ( newTarget, locales, options, required, defaults ), https://tc39.es/ecma402/#sec-createdatetimeformat +// 15.7.1 CreateDateTimeFormat ( newTarget, locales, options, required, defaults [ , toLocaleStringTimeZone ] ), https://tc39.es/proposal-temporal/#sec-createdatetimeformat +// FIXME: Update the rest of this AO for Temporal once we have the required Temporal objects. ThrowCompletionOr> create_date_time_format(VM& vm, FunctionObject& new_target, Value locales_value, Value options_value, OptionRequired required, OptionDefaults defaults) { // 1. Let dateTimeFormat be ? OrdinaryCreateFromConstructor(newTarget, "%Intl.DateTimeFormat.prototype%", « [[InitializedDateTimeFormat]], [[Locale]], [[Calendar]], [[NumberingSystem]], [[TimeZone]], [[HourCycle]], [[DateStyle]], [[TimeStyle]], [[DateTimeFormat]], [[BoundFormat]] »). @@ -200,29 +203,25 @@ ThrowCompletionOr> create_date_time_format(VM& vm, Funct } // 29. If IsTimeZoneOffsetString(timeZone) is true, then - bool is_time_zone_offset_string = JS::is_time_zone_offset_string(time_zone); + bool is_time_zone_offset_string = JS::is_offset_time_zone_identifier(time_zone); if (is_time_zone_offset_string) { - // a. Let parseResult be ParseText(StringToCodePoints(timeZone), UTCOffset). - auto parse_result = parse_utc_offset(time_zone); + // a. Let parseResult be ParseText(StringToCodePoints(timeZone), UTCOffset[~SubMinutePrecision]). + auto parse_result = Temporal::parse_utc_offset(time_zone, Temporal::SubMinutePrecision::No); // b. Assert: parseResult is a Parse Node. VERIFY(parse_result.has_value()); - // c. If parseResult contains more than one MinuteSecond Parse Node, throw a RangeError exception. - if (parse_result->second.has_value()) - return vm.throw_completion(ErrorType::OptionIsNotValidValue, time_zone, vm.names.timeZone); + // c. Let offsetNanoseconds be ? ParseDateTimeUTCOffset(timeZone). + auto offset_nanoseconds = TRY(parse_date_time_utc_offset(vm, time_zone)); - // d. Let offsetNanoseconds be ParseTimeZoneOffsetString(timeZone). - auto offset_nanoseconds = parse_time_zone_offset_string(time_zone); - - // e. Let offsetMinutes be offsetNanoseconds / (6 × 10**10). + // d. Let offsetMinutes be offsetNanoseconds / (6 × 10**10). auto offset_minutes = offset_nanoseconds / 60'000'000'000; - // f. Assert: offsetMinutes is an integer. + // e. Assert: offsetMinutes is an integer. VERIFY(trunc(offset_minutes) == offset_minutes); - // g. Set timeZone to FormatOffsetTimeZoneIdentifier(offsetMinutes). + // f. Set timeZone to FormatOffsetTimeZoneIdentifier(offsetMinutes). time_zone = format_offset_time_zone_identifier(offset_minutes); } // 30. Else, diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index e838f97d469..f1ac2c6e7a6 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -1366,10 +1366,7 @@ ThrowCompletionOr to_offset_string(VM& vm, Value argument) return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, offset); // 3. Perform ? ParseDateTimeUTCOffset(offset). - // FIXME: ParseTimeZoneOffsetString should be renamed to ParseDateTimeUTCOffset and updated for Temporal. For now, we - // can just check with the ISO8601 parser directly. - if (!parse_utc_offset(argument.as_string().utf8_string_view(), SubMinutePrecision::Yes).has_value()) - return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, offset); + TRY(parse_date_time_utc_offset(vm, offset.as_string().utf8_string_view())); // 4. Return offset. return offset.as_string().utf8_string(); diff --git a/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp b/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp index 33cc50020ac..31bc53655af 100644 --- a/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp +++ b/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp @@ -115,8 +115,7 @@ TimeZone parse_time_zone_identifier(ParseResult const& parse_result) // b. Let offsetString be the source text matched by the UTCOffset[~SubMinutePrecision] Parse Node contained within parseResult. // c. Let offsetNanoseconds be ! ParseDateTimeUTCOffset(offsetString). - // FIXME: ParseTimeZoneOffsetString should be renamed to ParseDateTimeUTCOffset and updated for Temporal. - auto offset_nanoseconds = parse_time_zone_offset_string(parse_result.time_zone_offset->source_text); + auto offset_nanoseconds = parse_date_time_utc_offset(parse_result.time_zone_offset->source_text); // d. Let offsetMinutes be offsetNanoseconds / (60 × 10**9). auto offset_minutes = offset_nanoseconds / 60'000'000'000;