diff --git a/Libraries/LibJS/Runtime/ErrorTypes.h b/Libraries/LibJS/Runtime/ErrorTypes.h index 33f0a1e7a7b..510b45f1cb4 100644 --- a/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Libraries/LibJS/Runtime/ErrorTypes.h @@ -67,6 +67,10 @@ "Styles other than 'fractional', numeric', or '2-digit' may not be used in smaller units after being used in larger units") \ M(IntlNumberIsNaNOrOutOfRange, "Value {} is NaN or is not between {} and {}") \ M(IntlOptionUndefined, "Option {} must be defined when option {} is {}") \ + M(IntlTemporalFormatIsNull, "Unable to determine format for {}") \ + M(IntlTemporalFormatRangeTypeMismatch, "Cannot format a date-time range with different date-time types") \ + M(IntlTemporalInvalidCalendar, "Cannot format {} with calendar '{}' in locale with calendar '{}'") \ + M(IntlTemporalZonedDateTime, "Cannot format Temporal.ZonedDateTime, use Temporal.ZonedDateTime.prototype.toLocaleString") \ M(InvalidAssignToConst, "Invalid assignment to const variable") \ M(InvalidCodePoint, "Invalid code point {}, must be an integer no less than 0 and no greater than 0x10FFFF") \ M(InvalidEnumerationValue, "Invalid value '{}' for enumeration type '{}'") \ diff --git a/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp b/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp index ffe25e715b9..9d0cf3584a6 100644 --- a/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp +++ b/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,6 +8,14 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include namespace JS::Intl { @@ -26,52 +34,88 @@ void DateTimeFormat::visit_edges(Cell::Visitor& visitor) visitor.visit(m_bound_format); } -// 11.5.5 FormatDateTimePattern ( dateTimeFormat, patternParts, x, rangeFormatOptions ), https://tc39.es/ecma402/#sec-formatdatetimepattern -ThrowCompletionOr> format_date_time_pattern(VM& vm, DateTimeFormat& date_time_format, double time) +static Optional get_or_create_formatter(StringView locale, StringView time_zone, OwnPtr& formatter, Optional const& format) { - // 1. Let x be TimeClip(x). - time = time_clip(time); + if (formatter) + return *formatter; + if (!format.has_value()) + return {}; - // 2. If x is NaN, throw a RangeError exception. - if (isnan(time)) - return vm.throw_completion(ErrorType::IntlInvalidTime); + formatter = Unicode::DateTimeFormat::create_for_pattern_options(locale, time_zone, *format); + return *formatter; +} - return date_time_format.formatter().format_to_parts(time); +Optional DateTimeFormat::temporal_plain_date_formatter() +{ + return get_or_create_formatter(m_locale, m_temporal_time_zone, m_temporal_plain_date_formatter, m_temporal_plain_date_format); +} + +Optional DateTimeFormat::temporal_plain_year_month_formatter() +{ + return get_or_create_formatter(m_locale, m_temporal_time_zone, m_temporal_plain_year_month_formatter, m_temporal_plain_year_month_format); +} + +Optional DateTimeFormat::temporal_plain_month_day_formatter() +{ + return get_or_create_formatter(m_locale, m_temporal_time_zone, m_temporal_plain_month_day_formatter, m_temporal_plain_month_day_format); +} + +Optional DateTimeFormat::temporal_plain_time_formatter() +{ + return get_or_create_formatter(m_locale, m_temporal_time_zone, m_temporal_plain_time_formatter, m_temporal_plain_time_format); +} + +Optional DateTimeFormat::temporal_plain_date_time_formatter() +{ + return get_or_create_formatter(m_locale, m_temporal_time_zone, m_temporal_plain_date_time_formatter, m_temporal_plain_date_time_format); +} + +Optional DateTimeFormat::temporal_instant_formatter() +{ + return get_or_create_formatter(m_locale, m_temporal_time_zone, m_temporal_instant_formatter, m_temporal_instant_format); +} + +// 11.5.5 FormatDateTimePattern ( dateTimeFormat, patternParts, x, rangeFormatOptions ), https://tc39.es/ecma402/#sec-formatdatetimepattern +// 15.9.4 FormatDateTimePattern ( dateTimeFormat, format, pattern, x, epochNanoseconds ), https://tc39.es/proposal-temporal/#sec-formatdatetimepattern +Vector format_date_time_pattern(ValueFormat const& format_record) +{ + return format_record.formatter.format_to_parts(format_record.epoch_milliseconds); } // 11.5.6 PartitionDateTimePattern ( dateTimeFormat, x ), https://tc39.es/ecma402/#sec-partitiondatetimepattern -ThrowCompletionOr> partition_date_time_pattern(VM& vm, DateTimeFormat& date_time_format, double time) +// 15.9.5 PartitionDateTimePattern ( dateTimeFormat, x ), https://tc39.es/proposal-temporal/#sec-partitiondatetimepattern +ThrowCompletionOr> partition_date_time_pattern(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& time) { - // 1. Let patternParts be PartitionPattern(dateTimeFormat.[[Pattern]]). - // 2. Let result be ? FormatDateTimePattern(dateTimeFormat, patternParts, x, undefined). - return format_date_time_pattern(vm, date_time_format, time); + // 1. Let xFormatRecord be ? HandleDateTimeValue(dateTimeFormat, x). + auto format_record = TRY(handle_date_time_value(vm, date_time_format, time)); + + // 5. Let result be ? FormatDateTimePattern(dateTimeFormat, format, pattern, xFormatRecord.[[EpochNanoseconds]]). + return format_date_time_pattern(format_record); } // 11.5.7 FormatDateTime ( dateTimeFormat, x ), https://tc39.es/ecma402/#sec-formatdatetime -ThrowCompletionOr format_date_time(VM& vm, DateTimeFormat& date_time_format, double time) +// 15.9.6 FormatDateTime ( dateTimeFormat, x ), https://tc39.es/proposal-temporal/#sec-formatdatetime +ThrowCompletionOr format_date_time(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& time) { // 1. Let parts be ? PartitionDateTimePattern(dateTimeFormat, x). + // 2. Let result be the empty String. + String result; + + // NOTE: We short-circuit PartitionDateTimePattern as we do not need individual partitions. { - // NOTE: We short-circuit PartitionDateTimePattern as we do not need individual partitions. But we must still - // perform the time clip and NaN sanity checks from its call to FormatDateTimePattern. + // 1. Let xFormatRecord be ? HandleDateTimeValue(dateTimeFormat, x). + auto format_record = TRY(handle_date_time_value(vm, date_time_format, time)); - // 1. Let x be TimeClip(x). - time = time_clip(time); - - // 2. If x is NaN, throw a RangeError exception. - if (isnan(time)) - return vm.throw_completion(ErrorType::IntlInvalidTime); + result = format_record.formatter.format(format_record.epoch_milliseconds); } - // 2. Let result be the empty String. - // 3. For each Record { [[Type]], [[Value]] } part in parts, do - // a. Set result to the string-concatenation of result and part.[[Value]]. // 4. Return result. - return date_time_format.formatter().format(time); + return result; } // 11.5.8 FormatDateTimeToParts ( dateTimeFormat, x ), https://tc39.es/ecma402/#sec-formatdatetimetoparts -ThrowCompletionOr> format_date_time_to_parts(VM& vm, DateTimeFormat& date_time_format, double time) +// 15.9.7 FormatDateTimeToParts ( dateTimeFormat, x ), https://tc39.es/proposal-temporal/#sec-formatdatetimetoparts +ThrowCompletionOr> format_date_time_to_parts(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& time) { auto& realm = *vm.current_realm(); @@ -107,57 +151,58 @@ ThrowCompletionOr> format_date_time_to_parts(VM& vm, DateTimeForm } // 11.5.9 PartitionDateTimeRangePattern ( dateTimeFormat, x, y ), https://tc39.es/ecma402/#sec-partitiondatetimerangepattern -ThrowCompletionOr> partition_date_time_range_pattern(VM& vm, DateTimeFormat& date_time_format, double start, double end) +// 15.9.8 PartitionDateTimeRangePattern ( dateTimeFormat, x, y ), https://tc39.es/proposal-temporal/#sec-partitiondatetimerangepattern +ThrowCompletionOr> partition_date_time_range_pattern(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& start, FormattableDateTime const& end) { - // 1. Let x be TimeClip(x). - start = time_clip(start); + // 1. If IsTemporalObject(x) is true or IsTemporalObject(y) is true, then + if (is_temporal_object(start) || is_temporal_object(end)) { + // a. If SameTemporalType(x, y) is false, throw a TypeError exception. + if (!same_temporal_type(start, end)) + return vm.throw_completion(ErrorType::IntlTemporalFormatRangeTypeMismatch); + } - // 2. If x is NaN, throw a RangeError exception. - if (isnan(start)) - return vm.throw_completion(ErrorType::IntlInvalidTime); + // 2. Let xFormatRecord be ? HandleDateTimeValue(dateTimeFormat, x). + auto start_format_record = TRY(handle_date_time_value(vm, date_time_format, start)); - // 3. Let y be TimeClip(y). - end = time_clip(end); + // 3. Let yFormatRecord be ? HandleDateTimeValue(dateTimeFormat, y). + auto end_format_record = TRY(handle_date_time_value(vm, date_time_format, end)); - // 4. If y is NaN, throw a RangeError exception. - if (isnan(end)) - return vm.throw_completion(ErrorType::IntlInvalidTime); - - return date_time_format.formatter().format_range_to_parts(start, end); + return start_format_record.formatter.format_range_to_parts(start_format_record.epoch_milliseconds, end_format_record.epoch_milliseconds); } // 11.5.10 FormatDateTimeRange ( dateTimeFormat, x, y ), https://tc39.es/ecma402/#sec-formatdatetimerange -ThrowCompletionOr format_date_time_range(VM& vm, DateTimeFormat& date_time_format, double start, double end) +// 15.9.9 FormatDateTimeRange ( dateTimeFormat, x, y ), https://tc39.es/proposal-temporal/#sec-formatdatetimerange +ThrowCompletionOr format_date_time_range(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& start, FormattableDateTime const& end) { - { - // NOTE: We short-circuit PartitionDateTimeRangePattern as we do not need individual partitions. But we must - // still perform the time clip and NaN sanity checks from its its first steps. - - // 1. Let x be TimeClip(x). - start = time_clip(start); - - // 2. If x is NaN, throw a RangeError exception. - if (isnan(start)) - return vm.throw_completion(ErrorType::IntlInvalidTime); - - // 3. Let y be TimeClip(y). - end = time_clip(end); - - // 4. If y is NaN, throw a RangeError exception. - if (isnan(end)) - return vm.throw_completion(ErrorType::IntlInvalidTime); - } - // 1. Let parts be ? PartitionDateTimeRangePattern(dateTimeFormat, x, y). // 2. Let result be the empty String. - // 3. For each Record { [[Type]], [[Value]], [[Source]] } part in parts, do - // a. Set result to the string-concatenation of result and part.[[Value]]. + String result; + + // NOTE: We short-circuit PartitionDateTimeRangePattern as we do not need individual partitions. + { + // 1. If IsTemporalObject(x) is true or IsTemporalObject(y) is true, then + if (is_temporal_object(start) || is_temporal_object(end)) { + // a. If SameTemporalType(x, y) is false, throw a TypeError exception. + if (!same_temporal_type(start, end)) + return vm.throw_completion(ErrorType::IntlTemporalFormatRangeTypeMismatch); + } + + // 2. Let xFormatRecord be ? HandleDateTimeValue(dateTimeFormat, x). + auto start_format_record = TRY(handle_date_time_value(vm, date_time_format, start)); + + // 3. Let yFormatRecord be ? HandleDateTimeValue(dateTimeFormat, y). + auto end_format_record = TRY(handle_date_time_value(vm, date_time_format, end)); + + result = start_format_record.formatter.format_range(start_format_record.epoch_milliseconds, end_format_record.epoch_milliseconds); + } + // 4. Return result. - return date_time_format.formatter().format_range(start, end); + return result; } // 11.5.11 FormatDateTimeRangeToParts ( dateTimeFormat, x, y ), https://tc39.es/ecma402/#sec-formatdatetimerangetoparts -ThrowCompletionOr> format_date_time_range_to_parts(VM& vm, DateTimeFormat& date_time_format, double start, double end) +// 15.9.10 FormatDateTimeRangeToParts ( dateTimeFormat, x, y ), https://tc39.es/proposal-temporal/#sec-formatdatetimerangetoparts +ThrowCompletionOr> format_date_time_range_to_parts(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& start, FormattableDateTime const& end) { auto& realm = *vm.current_realm(); @@ -195,4 +240,449 @@ ThrowCompletionOr> format_date_time_range_to_parts(VM& vm, DateTi return result; } +// 15.9.1 GetDateTimeFormat ( formats, matcher, options, required, defaults, inherit ), https://tc39.es/proposal-temporal/#sec-getdatetimeformat +Optional get_date_time_format(Unicode::CalendarPattern const& options, OptionRequired required, OptionDefaults defaults, OptionInherit inherit) +{ + using enum Unicode::CalendarPattern::Field; + + auto required_options = [&]() -> ReadonlySpan { + static constexpr auto date_fields = AK::Array { Weekday, Year, Month, Day }; + static constexpr auto time_fields = AK::Array { DayPeriod, Hour, Minute, Second, FractionalSecondDigits }; + static constexpr auto year_month_fields = AK::Array { Year, Month }; + static constexpr auto month_day_fields = AK::Array { Month, Day }; + static constexpr auto any_fields = AK::Array { Weekday, Year, Month, Day, DayPeriod, Hour, Minute, Second, FractionalSecondDigits }; + + switch (required) { + // 1. If required is DATE, then + case OptionRequired::Date: + // a. Let requiredOptions be « "weekday", "year", "month", "day" ». + return date_fields; + // 2. Else if required is TIME, then + case OptionRequired::Time: + // a. Let requiredOptions be « "dayPeriod", "hour", "minute", "second", "fractionalSecondDigits" ». + return time_fields; + // 3. Else if required is YEAR-MONTH, then + case OptionRequired::YearMonth: + // a. Let requiredOptions be « "year", "month" ». + return year_month_fields; + // 4. Else if required is MONTH-DAY, then + case OptionRequired::MonthDay: + // a. Let requiredOptions be « "month", "day" ». + return month_day_fields; + // 5. Else, + case OptionRequired::Any: + // a. Assert: required is ANY. + // b. Let requiredOptions be « "weekday", "year", "month", "day", "dayPeriod", "hour", "minute", "second", "fractionalSecondDigits" ». + return any_fields; + } + VERIFY_NOT_REACHED(); + }(); + + auto default_options = [&]() -> ReadonlySpan { + static constexpr auto date_fields = AK::Array { Year, Month, Day }; + static constexpr auto time_fields = AK::Array { Hour, Minute, Second }; + static constexpr auto year_month_fields = AK::Array { Year, Month }; + static constexpr auto month_day_fields = AK::Array { Month, Day }; + static constexpr auto all_fields = AK::Array { Year, Month, Day, Hour, Minute, Second }; + + switch (defaults) { + // 6. If defaults is DATE, then + case OptionDefaults::Date: + // a. Let defaultOptions be « "year", "month", "day" ». + return date_fields; + // 7. Else if defaults is TIME, then + case OptionDefaults::Time: + // a. Let defaultOptions be « "hour", "minute", "second" ». + return time_fields; + // 8. Else if defaults is YEAR-MONTH, then + case OptionDefaults::YearMonth: + // a. Let defaultOptions be « "year", "month" ». + return year_month_fields; + // 9. Else if defaults is MONTH-DAY, then + case OptionDefaults::MonthDay: + // a. Let defaultOptions be « "month", "day" ». + return month_day_fields; + // 10. Else, + case OptionDefaults::ZonedDateTime: + case OptionDefaults::All: + // a. Assert: defaults is ZONED-DATE-TIME or ALL. + // b. Let defaultOptions be « "year", "month", "day", "hour", "minute", "second" ». + return all_fields; + } + VERIFY_NOT_REACHED(); + }(); + + Unicode::CalendarPattern format_options {}; + + // 11. If inherit is ALL, then + if (inherit == OptionInherit::All) { + // a. Let formatOptions be a copy of options. + format_options = options; + } + // 12. Else, + else { + // a. Let formatOptions be a new Record. + + // b. If required is one of DATE, YEAR-MONTH, or ANY, then + if (required == OptionRequired::Date || required == OptionRequired::YearMonth || required == OptionRequired::Any) { + // i. Set formatOptions.[[era]] to options.[[era]]. + format_options.era = options.era; + } + + // c. If required is TIME or ANY, then + if (required == OptionRequired::Time || required == OptionRequired::Any) { + // i. Set formatOptions.[[hourCycle]] to options.[[hourCycle]]. + format_options.hour_cycle = options.hour_cycle; + format_options.hour12 = options.hour12; + } + } + + // 13. Let anyPresent be false. + auto any_present = false; + + // 14. For each property name prop of « "weekday", "year", "month", "day", "era", "dayPeriod", "hour", "minute", "second", "fractionalSecondDigits" », do + static constexpr auto all_fields = AK::Array { Weekday, Year, Month, Day, Era, DayPeriod, Hour, Minute, Second, FractionalSecondDigits }; + + options.for_each_calendar_field_zipped_with(format_options, all_fields, [&](auto const& option, auto&) { + // a. If options.[[]] is not undefined, set anyPresent to true. + if (option.has_value()) { + any_present = true; + return IterationDecision::Break; + } + + return IterationDecision::Continue; + }); + + // 15. Let needDefaults be true. + auto need_defaults = true; + + // 16. For each property name prop of requiredOptions, do + options.for_each_calendar_field_zipped_with(format_options, required_options, [&](auto const& option, auto& format_option) { + // a. Let value be options.[[]]. + // b. If value is not undefined, then + if (option.has_value()) { + // i. Set formatOptions.[[]] to value. + format_option = *option; + + // ii. Set needDefaults to false. + need_defaults = false; + } + + return IterationDecision::Continue; + }); + + // 17. If anyPresent is true and needDefaults is true, return null. + if (any_present && need_defaults) { + // FIXME: Spec issue: We can hit this when setting `bestFormat`, which should never be null. Don't return for now. + // https://github.com/tc39/proposal-temporal/issues/3049 + } + + // 18. If needDefaults is true, then + if (need_defaults) { + // a. For each property name prop of defaultOptions, do + options.for_each_calendar_field_zipped_with(format_options, default_options, [&](auto const&, auto& format_option) { + using ValueType = typename RemoveCVReference::ValueType; + + if constexpr (IsSame) { + // i. Set formatOptions.[[]] to "numeric". + format_option = Unicode::CalendarPatternStyle::Numeric; + } + + return IterationDecision::Continue; + }); + + // b. If defaults is ZONED-DATE-TIME, then + if (defaults == OptionDefaults::ZonedDateTime) { + // i. Set formatOptions.[[timeZoneName]] to "short". + format_options.time_zone_name = Unicode::CalendarPatternStyle::Short; + } + } + + // 19. If matcher is "basic", then + // a. Let bestFormat be BasicFormatMatcher(formatOptions, formats). + // 20. Else, + // a. Let bestFormat be BestFitFormatMatcher(formatOptions, formats). + // 21. Return bestFormat. + return format_options; +} + +// 15.9.2 AdjustDateTimeStyleFormat ( formats, baseFormat, matcher, allowedOptions ), https://tc39.es/proposal-temporal/#sec-adjustdatetimestyleformat +Unicode::CalendarPattern adjust_date_time_style_format(Unicode::CalendarPattern const& base_format, ReadonlySpan allowed_options) +{ + // 1. Let formatOptions be a new Record. + Unicode::CalendarPattern format_options; + + // 2. For each field name fieldName of allowedOptions, do + base_format.for_each_calendar_field_zipped_with(format_options, allowed_options, [&](auto const& base_option, auto& format_option) { + // a. Set the field of formatOptions whose name is fieldName to the value of the field of baseFormat whose name is fieldName. + format_option = base_option; + return IterationDecision::Continue; + }); + + // 3. If matcher is "basic", then + // a. Let bestFormat be BasicFormatMatcher(formatOptions, formats). + // 4. Else, + // a. Let bestFormat be BestFitFormatMatcher(formatOptions, formats). + // 5. Return bestFormat. + return format_options; +} + +// 15.9.11 ToDateTimeFormattable ( value ), https://tc39.es/proposal-temporal/#sec-todatetimeformattable +ThrowCompletionOr to_date_time_formattable(VM& vm, Value value) +{ + // 1. If IsTemporalObject(value) is true, return value. + if (value.is_object()) { + auto& object = value.as_object(); + + if (is(object)) + return FormattableDateTime { static_cast(object) }; + if (is(object)) + return FormattableDateTime { static_cast(object) }; + if (is(object)) + return FormattableDateTime { static_cast(object) }; + if (is(object)) + return FormattableDateTime { static_cast(object) }; + if (is(object)) + return FormattableDateTime { static_cast(object) }; + if (is(object)) + return FormattableDateTime { static_cast(object) }; + if (is(object)) + return FormattableDateTime { static_cast(object) }; + } + + // 2. Return ? ToNumber(value). + return FormattableDateTime { TRY(value.to_number(vm)).as_double() }; +} + +// 15.9.12 IsTemporalObject ( value ), https://tc39.es/proposal-temporal/#sec-temporal-istemporalobject +bool is_temporal_object(FormattableDateTime const& value) +{ + // 1. If value is not an Object, then + // a. Return false. + // 2. If value does not have an [[InitializedTemporalDate]], [[InitializedTemporalTime]], [[InitializedTemporalDateTime]], + // [[InitializedTemporalZonedDateTime]], [[InitializedTemporalYearMonth]], [[InitializedTemporalMonthDay]], or + // [[InitializedTemporalInstant]] internal slot, then + // a. Return false. + // 3. Return true. + return !value.has(); +} + +// 15.9.13 SameTemporalType ( x, y ), https://tc39.es/proposal-temporal/#sec-temporal-istemporalobject +bool same_temporal_type(FormattableDateTime const& x, FormattableDateTime const& y) +{ + // 1. If either of IsTemporalObject(x) or IsTemporalObject(y) is false, return false. + if (!is_temporal_object(x) || !is_temporal_object(y)) + return false; + + // 2. If x has an [[InitializedTemporalDate]] internal slot and y does not, return false. + // 3. If x has an [[InitializedTemporalTime]] internal slot and y does not, return false. + // 4. If x has an [[InitializedTemporalDateTime]] internal slot and y does not, return false. + // 5. If x has an [[InitializedTemporalZonedDateTime]] internal slot and y does not, return false. + // 6. If x has an [[InitializedTemporalYearMonth]] internal slot and y does not, return false. + // 7. If x has an [[InitializedTemporalMonthDay]] internal slot and y does not, return false. + // 8. If x has an [[InitializedTemporalInstant]] internal slot and y does not, return false. + // 9. Return true. + return x.index() == y.index(); +} + +static double to_epoch_milliseconds(Crypto::SignedBigInteger const& epoch_nanoseconds) +{ + return Temporal::big_floor(epoch_nanoseconds, Temporal::NANOSECONDS_PER_MILLISECOND).to_double(); +} + +// 15.9.15 HandleDateTimeTemporalDate ( dateTimeFormat, temporalDate ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporaldate +ThrowCompletionOr handle_date_time_temporal_date(VM& vm, DateTimeFormat& date_time_format, Temporal::PlainDate const& temporal_date) +{ + // 1. If temporalDate.[[Calendar]] is not dateTimeFormat.[[Calendar]] or "iso8601", throw a RangeError exception. + if (!temporal_date.calendar().is_one_of(date_time_format.calendar(), "iso8601"sv)) + return vm.throw_completion(ErrorType::IntlTemporalInvalidCalendar, "Temporal.PlainDate"sv, temporal_date.calendar(), date_time_format.calendar()); + + // 2. Let isoDateTime be CombineISODateAndTimeRecord(temporalDate.[[ISODate]], NoonTimeRecord()). + auto iso_date_time = Temporal::combine_iso_date_and_time_record(temporal_date.iso_date(), Temporal::noon_time_record()); + + // 3. Let epochNs be ? GetEpochNanosecondsFor(dateTimeFormat.[[TimeZone]], isoDateTime, COMPATIBLE). + auto epoch_nanoseconds = TRY(Temporal::get_epoch_nanoseconds_for(vm, date_time_format.time_zone(), iso_date_time, Temporal::Disambiguation::Compatible)); + + // 4. Let format be dateTimeFormat.[[TemporalPlainDateFormat]]. + auto formatter = date_time_format.temporal_plain_date_formatter(); + + // 5. If format is null, throw a TypeError exception. + if (!formatter.has_value()) + return vm.throw_completion(ErrorType::IntlTemporalFormatIsNull, "Temporal.PlainDate"sv); + + // 6. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: epochNs }. + return ValueFormat { .formatter = *formatter, .epoch_milliseconds = to_epoch_milliseconds(epoch_nanoseconds) }; +} + +// 15.9.16 HandleDateTimeTemporalYearMonth ( dateTimeFormat, temporalYearMonth ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporalyearmonth +ThrowCompletionOr handle_date_time_temporal_year_month(VM& vm, DateTimeFormat& date_time_format, Temporal::PlainYearMonth const& temporal_year_month) +{ + // 1. If temporalYearMonth.[[Calendar]] is not equal to dateTimeFormat.[[Calendar]], then + if (temporal_year_month.calendar() != date_time_format.calendar()) { + // a. Throw a RangeError exception. + return vm.throw_completion(ErrorType::IntlTemporalInvalidCalendar, "Temporal.PlainYearMonth"sv, temporal_year_month.calendar(), date_time_format.calendar()); + } + + // 2. Let isoDateTime be CombineISODateAndTimeRecord(temporalYearMonth.[[ISODate]], NoonTimeRecord()). + auto iso_date_time = Temporal::combine_iso_date_and_time_record(temporal_year_month.iso_date(), Temporal::noon_time_record()); + + // 3. Let epochNs be ? GetEpochNanosecondsFor(dateTimeFormat.[[TimeZone]], isoDateTime, COMPATIBLE). + auto epoch_nanoseconds = TRY(Temporal::get_epoch_nanoseconds_for(vm, date_time_format.time_zone(), iso_date_time, Temporal::Disambiguation::Compatible)); + + // 4. Let format be dateTimeFormat.[[TemporalPlainYearMonthFormat]]. + auto formatter = date_time_format.temporal_plain_year_month_formatter(); + + // 5. If format is null, throw a TypeError exception. + if (!formatter.has_value()) + return vm.throw_completion(ErrorType::IntlTemporalFormatIsNull, "Temporal.PlainYearMonth"sv); + + // 6. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: epochNs }. + return ValueFormat { .formatter = *formatter, .epoch_milliseconds = to_epoch_milliseconds(epoch_nanoseconds) }; +} + +// 15.9.17 HandleDateTimeTemporalMonthDay ( dateTimeFormat, temporalMonthDay ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporalmonthday +ThrowCompletionOr handle_date_time_temporal_month_day(VM& vm, DateTimeFormat& date_time_format, Temporal::PlainMonthDay const& temporal_month_day) +{ + // 1. If temporalMonthDay.[[Calendar]] is not equal to dateTimeFormat.[[Calendar]], then + if (temporal_month_day.calendar() != date_time_format.calendar()) { + // a. Throw a RangeError exception. + return vm.throw_completion(ErrorType::IntlTemporalInvalidCalendar, "Temporal.PlainMonthDay"sv, temporal_month_day.calendar(), date_time_format.calendar()); + } + + // 2. Let isoDateTime be CombineISODateAndTimeRecord(temporalMonthDay.[[ISODate]], NoonTimeRecord()). + auto iso_date_time = Temporal::combine_iso_date_and_time_record(temporal_month_day.iso_date(), Temporal::noon_time_record()); + + // 3. Let epochNs be ? GetEpochNanosecondsFor(dateTimeFormat.[[TimeZone]], isoDateTime, COMPATIBLE). + auto epoch_nanoseconds = TRY(Temporal::get_epoch_nanoseconds_for(vm, date_time_format.time_zone(), iso_date_time, Temporal::Disambiguation::Compatible)); + + // 4. Let format be dateTimeFormat.[[TemporalPlainMonthDayFormat]]. + auto formatter = date_time_format.temporal_plain_month_day_formatter(); + + // 5. If format is null, throw a TypeError exception. + if (!formatter.has_value()) + return vm.throw_completion(ErrorType::IntlTemporalFormatIsNull, "Temporal.PlainMonthDay"sv); + + // 6. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: epochNs }. + return ValueFormat { .formatter = *formatter, .epoch_milliseconds = to_epoch_milliseconds(epoch_nanoseconds) }; +} + +// 15.9.18 HandleDateTimeTemporalTime ( dateTimeFormat, temporalTime ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporaltime +ThrowCompletionOr handle_date_time_temporal_time(VM& vm, DateTimeFormat& date_time_format, Temporal::PlainTime const& temporal_time) +{ + // 1. Let isoDate be CreateISODateRecord(1970, 1, 1). + auto iso_date = Temporal::create_iso_date_record(1970, 1, 1); + + // 2. Let isoDateTime be CombineISODateAndTimeRecord(isoDate, temporalTime.[[Time]]). + auto iso_date_time = Temporal::combine_iso_date_and_time_record(iso_date, temporal_time.time()); + + // 3. Let epochNs be ? GetEpochNanosecondsFor(dateTimeFormat.[[TimeZone]], isoDateTime, COMPATIBLE). + auto epoch_nanoseconds = TRY(Temporal::get_epoch_nanoseconds_for(vm, date_time_format.time_zone(), iso_date_time, Temporal::Disambiguation::Compatible)); + + // 4. Let format be dateTimeFormat.[[TemporalPlainTimeFormat]]. + auto formatter = date_time_format.temporal_plain_time_formatter(); + + // 5. If format is null, throw a TypeError exception. + if (!formatter.has_value()) + return vm.throw_completion(ErrorType::IntlTemporalFormatIsNull, "Temporal.PlainTime"sv); + + // 6. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: epochNs }. + return ValueFormat { .formatter = *formatter, .epoch_milliseconds = to_epoch_milliseconds(epoch_nanoseconds) }; +} + +// 15.9.19 HandleDateTimeTemporalDateTime ( dateTimeFormat, dateTime ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporaldatetime +ThrowCompletionOr handle_date_time_temporal_date_time(VM& vm, DateTimeFormat& date_time_format, Temporal::PlainDateTime const& date_time) +{ + // 1. If dateTime.[[Calendar]] is not "iso8601" and not equal to dateTimeFormat.[[Calendar]], then + if (!date_time.calendar().is_one_of(date_time_format.calendar(), "iso8601"sv)) { + // a. Throw a RangeError exception. + return vm.throw_completion(ErrorType::IntlTemporalInvalidCalendar, "Temporal.PlainDateTime"sv, date_time.calendar(), date_time_format.calendar()); + } + + // 2. Let epochNs be ? GetEpochNanosecondsFor(dateTimeFormat.[[TimeZone]], dateTime.[[ISODateTime]], COMPATIBLE). + auto epoch_nanoseconds = TRY(Temporal::get_epoch_nanoseconds_for(vm, date_time_format.time_zone(), date_time.iso_date_time(), Temporal::Disambiguation::Compatible)); + + // 3. Let format be dateTimeFormat.[[TemporalPlainDateTimeFormat]]. + auto formatter = date_time_format.temporal_plain_date_time_formatter(); + VERIFY(formatter.has_value()); + + // 4. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: epochNs }. + return ValueFormat { .formatter = *formatter, .epoch_milliseconds = to_epoch_milliseconds(epoch_nanoseconds) }; +} + +// 15.9.20 HandleDateTimeTemporalInstant ( dateTimeFormat, instant ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporalinstant +ValueFormat handle_date_time_temporal_instant(DateTimeFormat& date_time_format, Temporal::Instant const& instant) +{ + // 1. Let format be dateTimeFormat.[[TemporalInstantFormat]]. + auto formatter = date_time_format.temporal_instant_formatter(); + VERIFY(formatter.has_value()); + + // 2. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: instant.[[EpochNanoseconds]] }. + return ValueFormat { .formatter = *formatter, .epoch_milliseconds = to_epoch_milliseconds(instant.epoch_nanoseconds()->big_integer()) }; +} + +// 15.9.21 HandleDateTimeOthers ( dateTimeFormat, x ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimeothers +ThrowCompletionOr handle_date_time_others(VM& vm, DateTimeFormat& date_time_format, double time) +{ + // 1. Set x to TimeClip(x). + time = time_clip(time); + + // 2. If x is NaN, throw a RangeError exception. + if (isnan(time)) + return vm.throw_completion(ErrorType::IntlInvalidTime); + + // 3. Let epochNanoseconds be ℤ(ℝ(x) × 10**6). + + // 4. Let format be dateTimeFormat.[[DateTimeFormat]]. + auto const& formatter = date_time_format.formatter(); + + // 5. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: epochNanoseconds }. + return ValueFormat { .formatter = formatter, .epoch_milliseconds = time }; +} + +// 15.9.22 HandleDateTimeValue ( dateTimeFormat, x ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimevalue +ThrowCompletionOr handle_date_time_value(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& formattable) +{ + return formattable.visit( + // 1. If x is an Object, then + // a. If x has an [[InitializedTemporalDate]] internal slot, then + [&](GC::Ref temporal_date) { + // i. Return ? HandleDateTimeTemporalDate(dateTimeFormat, x). + return handle_date_time_temporal_date(vm, date_time_format, temporal_date); + }, + // b. If x has an [[InitializedTemporalYearMonth]] internal slot, then + [&](GC::Ref temporal_year_month) { + // i. Return ? HandleDateTimeTemporalYearMonth(dateTimeFormat, x). + return handle_date_time_temporal_year_month(vm, date_time_format, temporal_year_month); + }, + // c. If x has an [[InitializedTemporalMonthDay]] internal slot, then + [&](GC::Ref temporal_month_day) { + // i. Return ? HandleDateTimeTemporalMonthDay(dateTimeFormat, x). + return handle_date_time_temporal_month_day(vm, date_time_format, temporal_month_day); + }, + // d. If x has an [[InitializedTemporalTime]] internal slot, then + [&](GC::Ref temporal_time) { + // i. Return ? HandleDateTimeTemporalTime(dateTimeFormat, x). + return handle_date_time_temporal_time(vm, date_time_format, temporal_time); + }, + // e. If x has an [[InitializedTemporalDateTime]] internal slot, then + [&](GC::Ref date_time) { + // i. Return ? HandleDateTimeTemporalDateTime(dateTimeFormat, x). + return handle_date_time_temporal_date_time(vm, date_time_format, date_time); + }, + // f. If x has an [[InitializedTemporalInstant]] internal slot, then + [&](GC::Ref instant) -> ThrowCompletionOr { + // i. Return HandleDateTimeTemporalInstant(dateTimeFormat, x). + return handle_date_time_temporal_instant(date_time_format, instant); + }, + // g. Assert: x has an [[InitializedTemporalZonedDateTime]] internal slot. + [&](GC::Ref) -> ThrowCompletionOr { + // h. Throw a TypeError exception. + return vm.throw_completion(ErrorType::IntlTemporalZonedDateTime); + }, + // 2. Return ? HandleDateTimeOthers(dateTimeFormat, x). + [&](double time) { + return handle_date_time_others(vm, date_time_format, time); + }); +} + } diff --git a/Libraries/LibJS/Runtime/Intl/DateTimeFormat.h b/Libraries/LibJS/Runtime/Intl/DateTimeFormat.h index 0114c4b85de..d192e750e8f 100644 --- a/Libraries/LibJS/Runtime/Intl/DateTimeFormat.h +++ b/Libraries/LibJS/Runtime/Intl/DateTimeFormat.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -65,31 +66,96 @@ public: Unicode::DateTimeFormat const& formatter() const { return *m_formatter; } void set_formatter(NonnullOwnPtr formatter) { m_formatter = move(formatter); } + Optional temporal_plain_date_formatter(); + void set_temporal_plain_date_format(Optional temporal_plain_date_format) { m_temporal_plain_date_format = move(temporal_plain_date_format); } + + Optional temporal_plain_year_month_formatter(); + void set_temporal_plain_year_month_format(Optional temporal_plain_year_month_format) { m_temporal_plain_year_month_format = move(temporal_plain_year_month_format); } + + Optional temporal_plain_month_day_formatter(); + void set_temporal_plain_month_day_format(Optional temporal_plain_month_day_format) { m_temporal_plain_month_day_format = move(temporal_plain_month_day_format); } + + Optional temporal_plain_time_formatter(); + void set_temporal_plain_time_format(Optional temporal_plain_time_format) { m_temporal_plain_time_format = move(temporal_plain_time_format); } + + Optional temporal_plain_date_time_formatter(); + void set_temporal_plain_date_time_format(Optional temporal_plain_date_time_format) { m_temporal_plain_date_time_format = move(temporal_plain_date_time_format); } + + Optional temporal_instant_formatter(); + void set_temporal_instant_format(Optional temporal_instant_format) { m_temporal_instant_format = move(temporal_instant_format); } + + void set_temporal_time_zone(String temporal_time_zone) { m_temporal_time_zone = move(temporal_time_zone); } + private: DateTimeFormat(Object& prototype); virtual void visit_edges(Visitor&) override; - String m_locale; // [[Locale]] - String m_calendar; // [[Calendar]] - String m_numbering_system; // [[NumberingSystem]] - String m_time_zone; // [[TimeZone]] - Optional m_date_style; // [[DateStyle]] - Optional m_time_style; // [[TimeStyle]] - Unicode::CalendarPattern m_date_time_format; // [[DateTimeFormat]] - GC::Ptr m_bound_format; // [[BoundFormat]] + String m_locale; // [[Locale]] + String m_calendar; // [[Calendar]] + String m_numbering_system; // [[NumberingSystem]] + String m_time_zone; // [[TimeZone]] + Optional m_date_style; // [[DateStyle]] + Optional m_time_style; // [[TimeStyle]] + Unicode::CalendarPattern m_date_time_format; // [[DateTimeFormat]] + Optional m_temporal_plain_date_format; // [[TemporalPlainDateFormat]] + Optional m_temporal_plain_year_month_format; // [[TemporalPlainYearMonthFormat]] + Optional m_temporal_plain_month_day_format; // [[TemporalPlainMonthDayFormat]] + Optional m_temporal_plain_time_format; // [[TemporalPlainTimeFormat]] + Optional m_temporal_plain_date_time_format; // [[TemporalPlainDateTimeFormat]] + Optional m_temporal_instant_format; // [[TemporalInstantFormat]] + GC::Ptr m_bound_format; // [[BoundFormat]] - // Non-standard. Stores the ICU date-time formatter for the Intl object's formatting options. + // Non-standard. Stores the ICU date-time formatters for the Intl object's formatting options. OwnPtr m_formatter; + OwnPtr m_temporal_plain_date_formatter; + OwnPtr m_temporal_plain_year_month_formatter; + OwnPtr m_temporal_plain_month_day_formatter; + OwnPtr m_temporal_plain_time_formatter; + OwnPtr m_temporal_plain_date_time_formatter; + OwnPtr m_temporal_instant_formatter; + String m_temporal_time_zone; }; -ThrowCompletionOr> format_date_time_pattern(VM&, DateTimeFormat&, double time); -ThrowCompletionOr> partition_date_time_pattern(VM&, DateTimeFormat&, double time); -ThrowCompletionOr format_date_time(VM&, DateTimeFormat&, double time); -ThrowCompletionOr> format_date_time_to_parts(VM&, DateTimeFormat&, double time); -ThrowCompletionOr> partition_date_time_range_pattern(VM&, DateTimeFormat&, double start, double end); -ThrowCompletionOr format_date_time_range(VM&, DateTimeFormat&, double start, double end); -ThrowCompletionOr> format_date_time_range_to_parts(VM&, DateTimeFormat&, double start, double end); +using FormattableDateTime = Variant< + double, + GC::Ref, + GC::Ref, + GC::Ref, + GC::Ref, + GC::Ref, + GC::Ref, + GC::Ref>; + +// https://tc39.es/proposal-temporal/#datetimeformat-value-format-record +// NOTE: ICU does not support nanoseconds in its date-time formatter. Thus, we do do not store the epoch nanoseconds as +// a BigInt here. Instead, we store the epoch in milliseconds as a double. +struct ValueFormat { + Unicode::DateTimeFormat const& formatter; // [[Format]] + double epoch_milliseconds { 0 }; // [[EpochNanoseconds]] +}; + +Vector format_date_time_pattern(ValueFormat const&); +ThrowCompletionOr> partition_date_time_pattern(VM&, DateTimeFormat&, FormattableDateTime const&); +ThrowCompletionOr format_date_time(VM&, DateTimeFormat&, FormattableDateTime const&); +ThrowCompletionOr> format_date_time_to_parts(VM&, DateTimeFormat&, FormattableDateTime const&); +ThrowCompletionOr> partition_date_time_range_pattern(VM&, DateTimeFormat&, FormattableDateTime const& start, FormattableDateTime const& end); +ThrowCompletionOr format_date_time_range(VM&, DateTimeFormat&, FormattableDateTime const& start, FormattableDateTime const& end); +ThrowCompletionOr> format_date_time_range_to_parts(VM&, DateTimeFormat&, FormattableDateTime const& start, FormattableDateTime const& end); + +Optional get_date_time_format(Unicode::CalendarPattern const& options, OptionRequired, OptionDefaults, OptionInherit); +Unicode::CalendarPattern adjust_date_time_style_format(Unicode::CalendarPattern const& base_format, ReadonlySpan allowed_options); +ThrowCompletionOr to_date_time_formattable(VM&, Value); +bool is_temporal_object(FormattableDateTime const&); +bool same_temporal_type(FormattableDateTime const&, FormattableDateTime const&); +ThrowCompletionOr handle_date_time_temporal_date(VM&, DateTimeFormat&, Temporal::PlainDate const&); +ThrowCompletionOr handle_date_time_temporal_year_month(VM&, DateTimeFormat&, Temporal::PlainYearMonth const&); +ThrowCompletionOr handle_date_time_temporal_month_day(VM&, DateTimeFormat&, Temporal::PlainMonthDay const&); +ThrowCompletionOr handle_date_time_temporal_time(VM&, DateTimeFormat&, Temporal::PlainTime const&); +ThrowCompletionOr handle_date_time_temporal_date_time(VM&, DateTimeFormat&, Temporal::PlainDateTime const&); +ValueFormat handle_date_time_temporal_instant(DateTimeFormat&, Temporal::Instant const&); +ThrowCompletionOr handle_date_time_others(VM&, DateTimeFormat&, double); +ThrowCompletionOr handle_date_time_value(VM&, DateTimeFormat&, FormattableDateTime const&); template ThrowCompletionOr for_each_calendar_field(VM& vm, Unicode::CalendarPattern& pattern, Callback&& callback) diff --git a/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp b/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp index a6e946d394f..0cd116c498f 100644 --- a/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp +++ b/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp @@ -55,7 +55,7 @@ ThrowCompletionOr> DateTimeFormatConstructor::construct(Function auto locales = vm.argument(0); auto options = vm.argument(1); - // 2. Let dateTimeFormat be ? CreateDateTimeFormat(newTarget, locales, options, any, date). + // 2. Let dateTimeFormat be ? CreateDateTimeFormat(newTarget, locales, options, ANY, DATE). auto date_time_format = TRY(create_date_time_format(vm, new_target, locales, options, OptionRequired::Any, OptionDefaults::Date)); // 3. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then @@ -83,8 +83,7 @@ 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) +ThrowCompletionOr> create_date_time_format(VM& vm, FunctionObject& new_target, Value locales_value, Value options_value, OptionRequired required, OptionDefaults defaults, Optional const& to_locale_string_time_zone) { // 1. Let dateTimeFormat be ? OrdinaryCreateFromConstructor(newTarget, "%Intl.DateTimeFormat.prototype%", « [[InitializedDateTimeFormat]], [[Locale]], [[Calendar]], [[NumberingSystem]], [[TimeZone]], [[HourCycle]], [[DateStyle]], [[TimeStyle]], [[DateTimeFormat]], [[BoundFormat]] »). auto date_time_format = TRY(ordinary_create_from_constructor(vm, new_target, &Intrinsics::intl_date_time_format_prototype)); @@ -187,22 +186,37 @@ ThrowCompletionOr> create_date_time_format(VM& vm, Funct hour_cycle_value = Unicode::default_hour_cycle(date_time_format->locale()); } - // 26. Let timeZone be ? Get(options, "timeZone"). + // 26. Set dateTimeFormat.[[HourCycle]] to hc. + // NOTE: The [[HourCycle]] is stored and accessed from [[DateTimeFormat]]. + + // 27. Let timeZone be ? Get(options, "timeZone"). auto time_zone_value = TRY(options->get(vm.names.timeZone)); String time_zone; - // 27. If timeZone is undefined, then + // 28. If timeZone is undefined, then if (time_zone_value.is_undefined()) { - // a. Set timeZone to SystemTimeZoneIdentifier(). - time_zone = system_time_zone_identifier(); + // a. If toLocaleStringTimeZone is present, then + if (to_locale_string_time_zone.has_value()) { + // i. Set timeZone to toLocaleStringTimeZone. + time_zone = *to_locale_string_time_zone; + } + // b. Else, + else { + // i. Set timeZone to SystemTimeZoneIdentifier(). + time_zone = system_time_zone_identifier(); + } } - // 28. Else, + // 29. Else, else { - // a. Set timeZone to ? ToString(timeZone). + // a. If toLocaleStringTimeZone is present, throw a TypeError exception. + if (to_locale_string_time_zone.has_value()) + return vm.throw_completion(ErrorType::IntlInvalidDateTimeFormatOption, vm.names.timeZone, "a toLocaleString time zone"sv); + + // b. Set timeZone to ? ToString(timeZone). time_zone = TRY(time_zone_value.to_string(vm)); } - // 29. If IsTimeZoneOffsetString(timeZone) is true, then + // 30. If IsTimeZoneOffsetString(timeZone) is true, then bool is_time_zone_offset_string = JS::is_offset_time_zone_identifier(time_zone); if (is_time_zone_offset_string) { @@ -224,7 +238,7 @@ ThrowCompletionOr> create_date_time_format(VM& vm, Funct // f. Set timeZone to FormatOffsetTimeZoneIdentifier(offsetMinutes). time_zone = format_offset_time_zone_identifier(offset_minutes); } - // 30. Else, + // 31. Else, else { // a. Let timeZoneIdentifierRecord be GetAvailableNamedTimeZoneIdentifier(timeZone). auto time_zone_identifier_record = get_available_named_time_zone_identifier(time_zone); @@ -237,25 +251,28 @@ ThrowCompletionOr> create_date_time_format(VM& vm, Funct time_zone = time_zone_identifier_record->primary_identifier; } - // 31. Set dateTimeFormat.[[TimeZone]] to timeZone. + // 32. Set dateTimeFormat.[[TimeZone]] to timeZone. date_time_format->set_time_zone(time_zone); // NOTE: ICU requires time zone offset strings to be of the form "GMT+00:00" if (is_time_zone_offset_string) time_zone = MUST(String::formatted("GMT{}", time_zone)); - // 32. Let formatOptions be a new Record. + // AD-HOC: We must store the massaged time zone for creating ICU formatters for Temporal objects. + date_time_format->set_temporal_time_zone(time_zone); + + // 33. Let formatOptions be a new Record. Unicode::CalendarPattern format_options {}; - // 33. Set formatOptions.[[hourCycle]] to hc. + // 34. Set formatOptions.[[hourCycle]] to hc. format_options.hour_cycle = hour_cycle_value; format_options.hour12 = hour12_value; - // 34. Let hasExplicitFormatComponents be false. + // 35. Let hasExplicitFormatComponents be false. // NOTE: Instead of using a boolean, we track any explicitly provided component name for nicer exception messages. PropertyKey const* explicit_format_component = nullptr; - // 35. For each row of Table 16, except the header row, in table order, do + // 36. For each row of Table 16, except the header row, in table order, do TRY(for_each_calendar_field(vm, format_options, [&](auto& option, PropertyKey const& property, auto const& values) -> ThrowCompletionOr { using ValueType = typename RemoveReference::ValueType; @@ -294,26 +311,28 @@ ThrowCompletionOr> create_date_time_format(VM& vm, Funct return {}; })); - // 36. Let formatMatcher be ? GetOption(options, "formatMatcher", string, « "basic", "best fit" », "best fit"). + // 37. Let formatMatcher be ? GetOption(options, "formatMatcher", string, « "basic", "best fit" », "best fit"). [[maybe_unused]] auto format_matcher = TRY(get_option(vm, *options, vm.names.formatMatcher, OptionType::String, AK::Array { "basic"sv, "best fit"sv }, "best fit"sv)); - // 37. Let dateStyle be ? GetOption(options, "dateStyle", string, « "full", "long", "medium", "short" », undefined). + // 38. Let dateStyle be ? GetOption(options, "dateStyle", string, « "full", "long", "medium", "short" », undefined). auto date_style = TRY(get_option(vm, *options, vm.names.dateStyle, OptionType::String, AK::Array { "full"sv, "long"sv, "medium"sv, "short"sv }, Empty {})); - // 38. Set dateTimeFormat.[[DateStyle]] to dateStyle. + // 39. Set dateTimeFormat.[[DateStyle]] to dateStyle. if (!date_style.is_undefined()) date_time_format->set_date_style(date_style.as_string().utf8_string_view()); - // 39. Let timeStyle be ? GetOption(options, "timeStyle", string, « "full", "long", "medium", "short" », undefined). + // 40. Let timeStyle be ? GetOption(options, "timeStyle", string, « "full", "long", "medium", "short" », undefined). auto time_style = TRY(get_option(vm, *options, vm.names.timeStyle, OptionType::String, AK::Array { "full"sv, "long"sv, "medium"sv, "short"sv }, Empty {})); - // 40. Set dateTimeFormat.[[TimeStyle]] to timeStyle. + // 41. Set dateTimeFormat.[[TimeStyle]] to timeStyle. if (!time_style.is_undefined()) date_time_format->set_time_style(time_style.as_string().utf8_string_view()); + // 42. Let formats be resolvedLocaleData.[[formats]].[[]]. + OwnPtr formatter; - // 41. If dateStyle is not undefined or timeStyle is not undefined, then + // 43. If dateStyle is not undefined or timeStyle is not undefined, then if (date_time_format->has_date_style() || date_time_format->has_time_style()) { // a. If hasExplicitFormatComponents is true, then if (explicit_format_component != nullptr) { @@ -342,93 +361,100 @@ ThrowCompletionOr> create_date_time_format(VM& vm, Funct format_options.hour12, date_time_format->date_style(), date_time_format->time_style()); + + auto best_format = formatter->chosen_pattern(); + using enum Unicode::CalendarPattern::Field; + + // f. If dateStyle is not undefined, then + if (!date_style.is_undefined()) { + // i. Set dateTimeFormat.[[TemporalPlainDateFormat]] to AdjustDateTimeStyleFormat(formats, bestFormat, matcher, « [[weekday]], [[era]], [[year]], [[month]], [[day]] »). + auto temporal_plain_date_format = adjust_date_time_style_format(best_format, { { Weekday, Era, Year, Month, Day } }); + date_time_format->set_temporal_plain_date_format(move(temporal_plain_date_format)); + + // ii. Set dateTimeFormat.[[TemporalPlainYearMonthFormat]] to AdjustDateTimeStyleFormat(formats, bestFormat, matcher, « [[era]], [[year]], [[month]] »). + auto temporal_plain_year_month_format = adjust_date_time_style_format(best_format, { { Era, Year, Month } }); + date_time_format->set_temporal_plain_year_month_format(move(temporal_plain_year_month_format)); + + // iii. Set dateTimeFormat.[[TemporalPlainMonthDayFormat]] to AdjustDateTimeStyleFormat(formats, bestFormat, matcher, « [[month]], [[day]] »). + auto temporal_plain_month_day_format = adjust_date_time_style_format(best_format, { { Month, Day } }); + date_time_format->set_temporal_plain_month_day_format(move(temporal_plain_month_day_format)); + } + // g. Else, + else { + // i. Set dateTimeFormat.[[TemporalPlainDateFormat]] to null. + // ii. Set dateTimeFormat.[[TemporalPlainYearMonthFormat]] to null. + // iii. Set dateTimeFormat.[[TemporalPlainMonthDayFormat]] to null. + } + + // h. If timeStyle is not undefined, then + if (!time_style.is_undefined()) { + // i. Set dateTimeFormat.[[TemporalPlainTimeFormat]] to AdjustDateTimeStyleFormat(formats, bestFormat, matcher, « [[dayPeriod]], [[hour]], [[minute]], [[second]], [[fractionalSecondDigits]] »). + auto temporal_plain_time_format = adjust_date_time_style_format(best_format, { { DayPeriod, Hour, Minute, Second, FractionalSecondDigits } }); + date_time_format->set_temporal_plain_time_format(move(temporal_plain_time_format)); + } + // i. Else, + else { + // i. Set dateTimeFormat.[[TemporalPlainTimeFormat]] to null. + } + + // j. Set dateTimeFormat.[[TemporalPlainDateTimeFormat]] to AdjustDateTimeStyleFormat(formats, bestFormat, matcher, « [[weekday]], [[era]], [[year]], [[month]], [[day]], [[dayPeriod]], [[hour]], [[minute]], [[second]], [[fractionalSecondDigits]] »). + auto temporal_plain_date_time_format = adjust_date_time_style_format(best_format, { { Weekday, Era, Year, Month, Day, DayPeriod, Hour, Minute, Second, FractionalSecondDigits } }); + date_time_format->set_temporal_plain_date_time_format(move(temporal_plain_date_time_format)); + + // k. Set dateTimeFormat.[[TemporalInstantFormat]] to bestFormat. + date_time_format->set_temporal_instant_format(move(best_format)); } - // 42. Else, + // 44. Else, else { - // a. Let needDefaults be true. - bool needs_defaults = true; + // a. Let bestFormat be GetDateTimeFormat(formats, formatMatcher, formatOptions, required, defaults, ALL). + auto best_format = get_date_time_format(format_options, required, defaults, OptionInherit::All).release_value(); - // b. If required is date or any, then - if (required == OptionRequired::Date || required == OptionRequired::Any) { - // i. For each property name prop of « "weekday", "year", "month", "day" », do - auto check_property_value = [&](auto const& value) { - // 1. Let value be formatOptions.[[]]. - // 2. If value is not undefined, set needDefaults to false. - if (value.has_value()) - needs_defaults = false; - }; + // b. Set dateTimeFormat.[[TemporalPlainDateFormat]] to GetDateTimeFormat(formats, formatMatcher, formatOptions, DATE, DATE, RELEVANT). + auto temporal_plain_date_format = get_date_time_format(format_options, OptionRequired::Date, OptionDefaults::Date, OptionInherit::Relevant); + date_time_format->set_temporal_plain_date_format(move(temporal_plain_date_format)); - check_property_value(format_options.weekday); - check_property_value(format_options.year); - check_property_value(format_options.month); - check_property_value(format_options.day); + // c. Set dateTimeFormat.[[TemporalPlainYearMonthFormat]] to GetDateTimeFormat(formats, formatMatcher, formatOptions, YEAR-MONTH, YEAR-MONTH, RELEVANT). + auto temporal_plain_year_month_format = get_date_time_format(format_options, OptionRequired::YearMonth, OptionDefaults::YearMonth, OptionInherit::Relevant); + date_time_format->set_temporal_plain_year_month_format(move(temporal_plain_year_month_format)); + + // d. Set dateTimeFormat.[[TemporalPlainMonthDayFormat]] to GetDateTimeFormat(formats, formatMatcher, formatOptions, MONTH-DAY, MONTH-DAY, RELEVANT). + auto temporal_plain_month_day_format = get_date_time_format(format_options, OptionRequired::MonthDay, OptionDefaults::MonthDay, OptionInherit::Relevant); + date_time_format->set_temporal_plain_month_day_format(move(temporal_plain_month_day_format)); + + // e. Set dateTimeFormat.[[TemporalPlainTimeFormat]] to GetDateTimeFormat(formats, formatMatcher, formatOptions, TIME, TIME, RELEVANT). + auto temporal_plain_time_format = get_date_time_format(format_options, OptionRequired::Time, OptionDefaults::Time, OptionInherit::Relevant); + date_time_format->set_temporal_plain_time_format(move(temporal_plain_time_format)); + + // f. Set dateTimeFormat.[[TemporalPlainDateTimeFormat]] to GetDateTimeFormat(formats, formatMatcher, formatOptions, ANY, ALL, RELEVANT). + auto temporal_plain_date_time_format = get_date_time_format(format_options, OptionRequired::Any, OptionDefaults::All, OptionInherit::Relevant); + date_time_format->set_temporal_plain_date_time_format(move(temporal_plain_date_time_format)); + + // g. If toLocaleStringTimeZone is present, then + if (to_locale_string_time_zone.has_value()) { + // i. Set dateTimeFormat.[[TemporalInstantFormat]] to GetDateTimeFormat(formats, formatMatcher, formatOptions, ANY, ZONED-DATE-TIME, ALL). + auto temporal_instant_format = get_date_time_format(format_options, OptionRequired::Any, OptionDefaults::ZonedDateTime, OptionInherit::All); + date_time_format->set_temporal_instant_format(move(temporal_instant_format)); } - - // c. If required is time or any, then - if (required == OptionRequired::Time || required == OptionRequired::Any) { - // i. For each property name prop of « "dayPeriod", "hour", "minute", "second", "fractionalSecondDigits" », do - auto check_property_value = [&](auto const& value) { - // 1. Let value be formatOptions.[[]]. - // 2. If value is not undefined, set needDefaults to false. - if (value.has_value()) - needs_defaults = false; - }; - - check_property_value(format_options.day_period); - check_property_value(format_options.hour); - check_property_value(format_options.minute); - check_property_value(format_options.second); - check_property_value(format_options.fractional_second_digits); - } - - // d. If needDefaults is true and defaults is either date or all, then - if (needs_defaults && (defaults == OptionDefaults::Date || defaults == OptionDefaults::All)) { - // i. For each property name prop of « "year", "month", "day" », do - auto set_property_value = [&](auto& value) { - // 1. Set formatOptions.[[]] to "numeric". - value = Unicode::CalendarPatternStyle::Numeric; - }; - - set_property_value(format_options.year); - set_property_value(format_options.month); - set_property_value(format_options.day); - } - - // e. If needDefaults is true and defaults is either time or all, then - if (needs_defaults && (defaults == OptionDefaults::Time || defaults == OptionDefaults::All)) { - // i. For each property name prop of « "hour", "minute", "second" », do - auto set_property_value = [&](auto& value) { - // 1. Set formatOptions.[[]] to "numeric". - value = Unicode::CalendarPatternStyle::Numeric; - }; - - set_property_value(format_options.hour); - set_property_value(format_options.minute); - set_property_value(format_options.second); - } - - // f. Let formats be resolvedLocaleData.[[formats]].[[]]. - // g. If formatMatcher is "basic", then - // i. Let bestFormat be BasicFormatMatcher(formatOptions, formats). // h. Else, - // i. Let bestFormat be BestFitFormatMatcher(formatOptions, formats). + else { + // i. Set dateTimeFormat.[[TemporalInstantFormat]] to GetDateTimeFormat(formats, formatMatcher, formatOptions, ANY, ALL, ALL). + auto temporal_instant_format = get_date_time_format(format_options, OptionRequired::Any, OptionDefaults::All, OptionInherit::All); + date_time_format->set_temporal_instant_format(move(temporal_instant_format)); + } + formatter = Unicode::DateTimeFormat::create_for_pattern_options( date_time_format->locale(), time_zone, - format_options); + best_format); } - // 43. Set dateTimeFormat.[[DateTimeFormat]] to bestFormat. + // 45. Set dateTimeFormat.[[DateTimeFormat]] to bestFormat. date_time_format->set_date_time_format(formatter->chosen_pattern()); - // 44. If bestFormat has a field [[hour]], then - // a. Set dateTimeFormat.[[HourCycle]] to hc. - // NOTE: The [[HourCycle]] is stored and accessed from [[DateTimeFormat]]. - // Non-standard, create an ICU number formatter for this Intl object. date_time_format->set_formatter(formatter.release_nonnull()); - // 45. Return dateTimeFormat. + // 46. Return dateTimeFormat. return date_time_format; } diff --git a/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.h b/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.h index ea9d2cd0fb3..1890f8926bc 100644 --- a/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.h +++ b/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -33,15 +33,25 @@ enum class OptionRequired { Any, Date, Time, + YearMonth, + MonthDay, }; enum class OptionDefaults { All, Date, Time, + YearMonth, + MonthDay, + ZonedDateTime, }; -ThrowCompletionOr> create_date_time_format(VM&, FunctionObject& new_target, Value locales_value, Value options_value, OptionRequired, OptionDefaults); +enum class OptionInherit { + All, + Relevant, +}; + +ThrowCompletionOr> create_date_time_format(VM&, FunctionObject& new_target, Value locales_value, Value options_value, OptionRequired, OptionDefaults, Optional const& to_locale_string_time_zone = {}); String format_offset_time_zone_identifier(double offset_minutes); } diff --git a/Libraries/LibJS/Runtime/Intl/DateTimeFormatFunction.cpp b/Libraries/LibJS/Runtime/Intl/DateTimeFormatFunction.cpp index 1b7b9073d6a..3038206ef76 100644 --- a/Libraries/LibJS/Runtime/Intl/DateTimeFormatFunction.cpp +++ b/Libraries/LibJS/Runtime/Intl/DateTimeFormatFunction.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -17,6 +17,7 @@ namespace JS::Intl { GC_DEFINE_ALLOCATOR(DateTimeFormatFunction); // 11.5.4 DateTime Format Functions, https://tc39.es/ecma402/#sec-datetime-format-functions +// 15.9.3 DateTime Format Functions, https://tc39.es/proposal-temporal/#sec-datetime-format-functions GC::Ref DateTimeFormatFunction::create(Realm& realm, DateTimeFormat& date_time_format) { return realm.create(date_time_format, realm.intrinsics().function_prototype()); @@ -42,26 +43,26 @@ ThrowCompletionOr DateTimeFormatFunction::call() auto& vm = this->vm(); auto& realm = *vm.current_realm(); - auto date = vm.argument(0); + auto date_value = vm.argument(0); // 1. Let dtf be F.[[DateTimeFormat]]. // 2. Assert: Type(dtf) is Object and dtf has an [[InitializedDateTimeFormat]] internal slot. - double date_value; + FormattableDateTime date { 0 }; // 3. If date is not provided or is undefined, then - if (date.is_undefined()) { + if (date_value.is_undefined()) { // a. Let x be ! Call(%Date.now%, undefined). - date_value = MUST(JS::call(vm, *realm.intrinsics().date_constructor_now_function(), js_undefined())).as_double(); + date = MUST(JS::call(vm, *realm.intrinsics().date_constructor_now_function(), js_undefined())).as_double(); } // 4. Else, else { - // a. Let x be ? ToNumber(date). - date_value = TRY(date.to_number(vm)).as_double(); + // a. Let x be ? ToDateTimeFormattable(date). + date = TRY(to_date_time_formattable(vm, date_value)); } // 5. Return ? FormatDateTime(dtf, x). - auto formatted = TRY(format_date_time(vm, m_date_time_format, date_value)); + auto formatted = TRY(format_date_time(vm, m_date_time_format, date)); return PrimitiveString::create(vm, move(formatted)); } diff --git a/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.cpp b/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.cpp index 4f1c692cc9d..e8e4acc5552 100644 --- a/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.cpp +++ b/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -66,84 +66,87 @@ JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatPrototype::format) } // 11.3.4 Intl.DateTimeFormat.prototype.formatToParts ( date ), https://tc39.es/ecma402/#sec-Intl.DateTimeFormat.prototype.formatToParts +// 15.10.1 Intl.DateTimeFormat.prototype.formatToParts ( date ), https://tc39.es/proposal-temporal/#sec-Intl.DateTimeFormat.prototype.formatToParts JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatPrototype::format_to_parts) { auto& realm = *vm.current_realm(); - auto date = vm.argument(0); + auto date_value = vm.argument(0); // 1. Let dtf be the this value. // 2. Perform ? RequireInternalSlot(dtf, [[InitializedDateTimeFormat]]). auto date_time_format = TRY(typed_this_object(vm)); - double date_value; + FormattableDateTime date { 0 }; // 3. If date is undefined, then - if (date.is_undefined()) { + if (date_value.is_undefined()) { // a. Let x be ! Call(%Date.now%, undefined). - date_value = MUST(call(vm, *realm.intrinsics().date_constructor_now_function(), js_undefined())).as_double(); + date = MUST(call(vm, *realm.intrinsics().date_constructor_now_function(), js_undefined())).as_double(); } // 4. Else, else { - // a. Let x be ? ToNumber(date). - date_value = TRY(date.to_number(vm)).as_double(); + // a. Let x be ? ToDateTimeFormattable(date). + date = TRY(to_date_time_formattable(vm, date_value)); } // 5. Return ? FormatDateTimeToParts(dtf, x). - return TRY(format_date_time_to_parts(vm, date_time_format, date_value)); + return TRY(format_date_time_to_parts(vm, date_time_format, date)); } // 11.3.5 Intl.DateTimeFormat.prototype.formatRange ( startDate, endDate ), https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.formatRange +// 15.10.2 Intl.DateTimeFormat.prototype.formatRange ( startDate, endDate ), https://tc39.es/proposal-temporal/#sec-intl.datetimeformat.prototype.formatRange JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatPrototype::format_range) { - auto start_date = vm.argument(0); - auto end_date = vm.argument(1); + auto start_date_value = vm.argument(0); + auto end_date_value = vm.argument(1); // 1. Let dtf be this value. // 2. Perform ? RequireInternalSlot(dtf, [[InitializedDateTimeFormat]]). auto date_time_format = TRY(typed_this_object(vm)); // 3. If startDate is undefined or endDate is undefined, throw a TypeError exception. - if (start_date.is_undefined()) + if (start_date_value.is_undefined()) return vm.throw_completion(ErrorType::IsUndefined, "startDate"sv); - if (end_date.is_undefined()) + if (end_date_value.is_undefined()) return vm.throw_completion(ErrorType::IsUndefined, "endDate"sv); - // 4. Let x be ? ToNumber(startDate). - auto start_date_number = TRY(start_date.to_number(vm)).as_double(); + // 4. Let x be ? ToDateTimeFormattable(startDate). + auto start_date = TRY(to_date_time_formattable(vm, start_date_value)); - // 5. Let y be ? ToNumber(endDate). - auto end_date_number = TRY(end_date.to_number(vm)).as_double(); + // 5. Let y be ? ToDateTimeFormattable(endDate). + auto end_date = TRY(to_date_time_formattable(vm, end_date_value)); // 6. Return ? FormatDateTimeRange(dtf, x, y). - auto formatted = TRY(format_date_time_range(vm, date_time_format, start_date_number, end_date_number)); + auto formatted = TRY(format_date_time_range(vm, date_time_format, start_date, end_date)); return PrimitiveString::create(vm, move(formatted)); } // 11.3.6 Intl.DateTimeFormat.prototype.formatRangeToParts ( startDate, endDate ), https://tc39.es/ecma402/#sec-Intl.DateTimeFormat.prototype.formatRangeToParts +// 15.10.3 Intl.DateTimeFormat.prototype.formatRangeToParts ( startDate, endDate ), https://tc39.es/proposal-temporal/#sec-Intl.DateTimeFormat.prototype.formatRangeToParts JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatPrototype::format_range_to_parts) { - auto start_date = vm.argument(0); - auto end_date = vm.argument(1); + auto start_date_value = vm.argument(0); + auto end_date_value = vm.argument(1); // 1. Let dtf be this value. // 2. Perform ? RequireInternalSlot(dtf, [[InitializedDateTimeFormat]]). auto date_time_format = TRY(typed_this_object(vm)); // 3. If startDate is undefined or endDate is undefined, throw a TypeError exception. - if (start_date.is_undefined()) + if (start_date_value.is_undefined()) return vm.throw_completion(ErrorType::IsUndefined, "startDate"sv); - if (end_date.is_undefined()) + if (end_date_value.is_undefined()) return vm.throw_completion(ErrorType::IsUndefined, "endDate"sv); - // 4. Let x be ? ToNumber(startDate). - auto start_date_number = TRY(start_date.to_number(vm)).as_double(); + // 4. Let x be ? ToDateTimeFormattable(startDate). + auto start_date = TRY(to_date_time_formattable(vm, start_date_value)); - // 5. Let y be ? ToNumber(endDate). - auto end_date_number = TRY(end_date.to_number(vm)).as_double(); + // 5. Let y be ? ToDateTimeFormattable(endDate). + auto end_date = TRY(to_date_time_formattable(vm, end_date_value)); // 6. Return ? FormatDateTimeRangeToParts(dtf, x, y). - return TRY(format_date_time_range_to_parts(vm, date_time_format, start_date_number, end_date_number)); + return TRY(format_date_time_range_to_parts(vm, date_time_format, start_date, end_date)); } // 11.3.7 Intl.DateTimeFormat.prototype.resolvedOptions ( ), https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.resolvedoptions diff --git a/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.format.js b/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.format.js index b591cf530ab..d2ae4b249dd 100644 --- a/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.format.js +++ b/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.format.js @@ -32,6 +32,49 @@ describe("errors", () => { Intl.DateTimeFormat().format(8.65e15); }).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15"); }); + + test("Temporal object must have same calendar", () => { + const formatter = new Intl.DateTimeFormat([], { calendar: "iso8601" }); + + expect(() => { + formatter.format(new Temporal.PlainDate(1972, 1, 1, "gregory")); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainDate with calendar 'gregory' in locale with calendar 'iso8601'" + ); + + expect(() => { + formatter.format(new Temporal.PlainYearMonth(1972, 1, "gregory")); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainYearMonth with calendar 'gregory' in locale with calendar 'iso8601'" + ); + + expect(() => { + formatter.format(new Temporal.PlainMonthDay(1, 1, "gregory")); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainMonthDay with calendar 'gregory' in locale with calendar 'iso8601'" + ); + + expect(() => { + formatter.format( + new Temporal.PlainDateTime(1972, 1, 1, 8, 45, 56, 123, 345, 789, "gregory") + ); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainDateTime with calendar 'gregory' in locale with calendar 'iso8601'" + ); + }); + + test("cannot format Temporal.ZonedDateTime", () => { + expect(() => { + new Intl.DateTimeFormat().format(new Temporal.ZonedDateTime(0n, "UTC")); + }).toThrowWithMessage( + TypeError, + "Cannot format Temporal.ZonedDateTime, use Temporal.ZonedDateTime.prototype.toLocaleString" + ); + }); }); const d0 = Date.UTC(2021, 11, 7, 17, 40, 50, 456); @@ -575,3 +618,35 @@ describe("non-Gregorian calendars", () => { expect(zh.format(d1)).toBe("1988戊辰年腊月十六 UTC 07:08:09"); }); }); + +describe("Temporal objects", () => { + const formatter = new Intl.DateTimeFormat("en", { + calendar: "iso8601", + timeZone: "UTC", + }); + + test("Temporal.PlainDate", () => { + const plainDate = new Temporal.PlainDate(1989, 1, 23); + expect(formatter.format(plainDate)).toBe("1/23/1989"); + }); + + test("Temporal.PlainYearMonth", () => { + const plainYearMonth = new Temporal.PlainYearMonth(1989, 1); + expect(formatter.format(plainYearMonth)).toBe("1/1989"); + }); + + test("Temporal.PlainMonthDay", () => { + const plainMonthDay = new Temporal.PlainMonthDay(1, 23); + expect(formatter.format(plainMonthDay)).toBe("1/23"); + }); + + test("Temporal.PlainTime", () => { + const plainTime = new Temporal.PlainTime(8, 10, 51); + expect(formatter.format(plainTime)).toBe("8:10:51 AM"); + }); + + test("Temporal.Instant", () => { + const instant = new Temporal.Instant(1732740069000000000n); + expect(formatter.format(instant)).toBe("11/27/2024, 8:41:09 PM"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRange.js b/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRange.js index 30d112240c5..d007bd31bb4 100644 --- a/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRange.js +++ b/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRange.js @@ -36,6 +36,82 @@ describe("errors", () => { }).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15"); }); }); + + test("Temporal object must have same calendar", () => { + const formatter = new Intl.DateTimeFormat([], { calendar: "iso8601" }); + + expect(() => { + const plainDate = new Temporal.PlainDate(1972, 1, 1, "gregory"); + formatter.formatRange(plainDate, plainDate); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainDate with calendar 'gregory' in locale with calendar 'iso8601'" + ); + + expect(() => { + const plainYearMonth = new Temporal.PlainYearMonth(1972, 1, "gregory"); + formatter.formatRange(plainYearMonth, plainYearMonth); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainYearMonth with calendar 'gregory' in locale with calendar 'iso8601'" + ); + + expect(() => { + const plainMonthDay = new Temporal.PlainMonthDay(1, 1, "gregory"); + formatter.formatRange(plainMonthDay, plainMonthDay); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainMonthDay with calendar 'gregory' in locale with calendar 'iso8601'" + ); + + expect(() => { + const plainDateTime = new Temporal.PlainDateTime( + 1972, + 1, + 1, + 8, + 45, + 56, + 123, + 345, + 789, + "gregory" + ); + formatter.formatRange(plainDateTime, plainDateTime); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainDateTime with calendar 'gregory' in locale with calendar 'iso8601'" + ); + }); + + test("cannot format Temporal.ZonedDateTime", () => { + expect(() => { + const zonedDateTime = new Temporal.ZonedDateTime(0n, "UTC"); + new Intl.DateTimeFormat().formatRange(zonedDateTime, zonedDateTime); + }).toThrowWithMessage( + TypeError, + "Cannot format Temporal.ZonedDateTime, use Temporal.ZonedDateTime.prototype.toLocaleString" + ); + }); + + test("cannot mix Temporal object types", () => { + expect(() => { + const plainDate = new Temporal.PlainDate(1972, 1, 1, "gregory"); + new Intl.DateTimeFormat().formatRange(plainDate, 0); + }).toThrowWithMessage( + TypeError, + "Cannot format a date-time range with different date-time types" + ); + + expect(() => { + const plainYearMonth = new Temporal.PlainYearMonth(1972, 1, "gregory"); + const plainMonthDay = new Temporal.PlainMonthDay(1, 1, "gregory"); + new Intl.DateTimeFormat().formatRange(plainYearMonth, plainMonthDay); + }).toThrowWithMessage( + TypeError, + "Cannot format a date-time range with different date-time types" + ); + }); }); const d0 = Date.UTC(1989, 0, 23, 7, 8, 9, 45); @@ -220,3 +296,42 @@ describe("dateStyle + timeStyle", () => { }); }); }); + +describe("Temporal objects", () => { + const formatter = new Intl.DateTimeFormat("en", { + calendar: "iso8601", + timeZone: "UTC", + }); + + test("Temporal.PlainDate", () => { + const plainDate1 = new Temporal.PlainDate(1989, 1, 23); + const plainDate2 = new Temporal.PlainDate(2024, 11, 27); + expect(formatter.formatRange(plainDate1, plainDate2)).toBe("1/23/1989 – 11/27/2024"); + }); + + test("Temporal.PlainYearMonth", () => { + const plainYearMonth1 = new Temporal.PlainYearMonth(1989, 1); + const plainYearMonth2 = new Temporal.PlainYearMonth(2024, 11); + expect(formatter.formatRange(plainYearMonth1, plainYearMonth2)).toBe("1/1989 – 11/2024"); + }); + + test("Temporal.PlainMonthDay", () => { + const plainMonthDay1 = new Temporal.PlainMonthDay(1, 23); + const plainMonthDay2 = new Temporal.PlainMonthDay(11, 27); + expect(formatter.formatRange(plainMonthDay1, plainMonthDay2)).toBe("1/23 – 11/27"); + }); + + test("Temporal.PlainTime", () => { + const plainTime1 = new Temporal.PlainTime(8, 10, 51); + const plainTime2 = new Temporal.PlainTime(20, 41, 9); + expect(formatter.formatRange(plainTime1, plainTime2)).toBe("8:10:51 AM – 8:41:09 PM"); + }); + + test("Temporal.Instant", () => { + const instant1 = new Temporal.Instant(601546251000000000n); + const instant2 = new Temporal.Instant(1732740069000000000n); + expect(formatter.formatRange(instant1, instant2)).toBe( + "1/23/1989, 8:10:51 AM – 11/27/2024, 8:41:09 PM" + ); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRangeToParts.js b/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRangeToParts.js index 9ba8c248912..1b66a1ae4ca 100644 --- a/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRangeToParts.js +++ b/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRangeToParts.js @@ -36,6 +36,82 @@ describe("errors", () => { }).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15"); }); }); + + test("Temporal object must have same calendar", () => { + const formatter = new Intl.DateTimeFormat([], { calendar: "iso8601" }); + + expect(() => { + const plainDate = new Temporal.PlainDate(1972, 1, 1, "gregory"); + formatter.formatRangeToParts(plainDate, plainDate); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainDate with calendar 'gregory' in locale with calendar 'iso8601'" + ); + + expect(() => { + const plainYearMonth = new Temporal.PlainYearMonth(1972, 1, "gregory"); + formatter.formatRangeToParts(plainYearMonth, plainYearMonth); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainYearMonth with calendar 'gregory' in locale with calendar 'iso8601'" + ); + + expect(() => { + const plainMonthDay = new Temporal.PlainMonthDay(1, 1, "gregory"); + formatter.formatRangeToParts(plainMonthDay, plainMonthDay); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainMonthDay with calendar 'gregory' in locale with calendar 'iso8601'" + ); + + expect(() => { + const plainDateTime = new Temporal.PlainDateTime( + 1972, + 1, + 1, + 8, + 45, + 56, + 123, + 345, + 789, + "gregory" + ); + formatter.formatRangeToParts(plainDateTime, plainDateTime); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainDateTime with calendar 'gregory' in locale with calendar 'iso8601'" + ); + }); + + test("cannot format Temporal.ZonedDateTime", () => { + expect(() => { + const zonedDateTime = new Temporal.ZonedDateTime(0n, "UTC"); + new Intl.DateTimeFormat().formatRangeToParts(zonedDateTime, zonedDateTime); + }).toThrowWithMessage( + TypeError, + "Cannot format Temporal.ZonedDateTime, use Temporal.ZonedDateTime.prototype.toLocaleString" + ); + }); + + test("cannot mix Temporal object types", () => { + expect(() => { + const plainDate = new Temporal.PlainDate(1972, 1, 1, "gregory"); + new Intl.DateTimeFormat().formatRangeToParts(plainDate, 0); + }).toThrowWithMessage( + TypeError, + "Cannot format a date-time range with different date-time types" + ); + + expect(() => { + const plainYearMonth = new Temporal.PlainYearMonth(1972, 1, "gregory"); + const plainMonthDay = new Temporal.PlainMonthDay(1, 1, "gregory"); + new Intl.DateTimeFormat().formatRangeToParts(plainYearMonth, plainMonthDay); + }).toThrowWithMessage( + TypeError, + "Cannot format a date-time range with different date-time types" + ); + }); }); const d0 = Date.UTC(1989, 0, 23, 7, 8, 9, 45); @@ -633,3 +709,112 @@ describe("timeStyle", () => { ]); }); }); + +describe("Temporal objects", () => { + const formatter = new Intl.DateTimeFormat("en", { + calendar: "iso8601", + timeZone: "UTC", + }); + + test("Temporal.PlainDate", () => { + const plainDate1 = new Temporal.PlainDate(1989, 1, 23); + const plainDate2 = new Temporal.PlainDate(2024, 11, 27); + expect(formatter.formatRangeToParts(plainDate1, plainDate2)).toEqual([ + { type: "month", value: "1", source: "startRange" }, + { type: "literal", value: "/", source: "startRange" }, + { type: "day", value: "23", source: "startRange" }, + { type: "literal", value: "/", source: "startRange" }, + { type: "year", value: "1989", source: "startRange" }, + { type: "literal", value: " – ", source: "shared" }, + { type: "month", value: "11", source: "endRange" }, + { type: "literal", value: "/", source: "endRange" }, + { type: "day", value: "27", source: "endRange" }, + { type: "literal", value: "/", source: "endRange" }, + { type: "year", value: "2024", source: "endRange" }, + ]); + }); + + test("Temporal.PlainYearMonth", () => { + const plainYearMonth1 = new Temporal.PlainYearMonth(1989, 1); + const plainYearMonth2 = new Temporal.PlainYearMonth(2024, 11); + expect(formatter.formatRangeToParts(plainYearMonth1, plainYearMonth2)).toEqual([ + { type: "month", value: "1", source: "startRange" }, + { type: "literal", value: "/", source: "startRange" }, + { type: "year", value: "1989", source: "startRange" }, + { type: "literal", value: " – ", source: "shared" }, + { type: "month", value: "11", source: "endRange" }, + { type: "literal", value: "/", source: "endRange" }, + { type: "year", value: "2024", source: "endRange" }, + ]); + }); + + test("Temporal.PlainMonthDay", () => { + const plainMonthDay1 = new Temporal.PlainMonthDay(1, 23); + const plainMonthDay2 = new Temporal.PlainMonthDay(11, 27); + expect(formatter.formatRangeToParts(plainMonthDay1, plainMonthDay2)).toEqual([ + { type: "month", value: "1", source: "startRange" }, + { type: "literal", value: "/", source: "startRange" }, + { type: "day", value: "23", source: "startRange" }, + { type: "literal", value: " – ", source: "shared" }, + { type: "month", value: "11", source: "endRange" }, + { type: "literal", value: "/", source: "endRange" }, + { type: "day", value: "27", source: "endRange" }, + ]); + }); + + test("Temporal.PlainTime", () => { + const plainTime1 = new Temporal.PlainTime(8, 10, 51); + const plainTime2 = new Temporal.PlainTime(20, 41, 9); + expect(formatter.formatRangeToParts(plainTime1, plainTime2)).toEqual([ + { type: "hour", value: "8", source: "startRange" }, + { type: "literal", value: ":", source: "startRange" }, + { type: "minute", value: "10", source: "startRange" }, + { type: "literal", value: ":", source: "startRange" }, + { type: "second", value: "51", source: "startRange" }, + { type: "literal", value: " ", source: "startRange" }, + { type: "dayPeriod", value: "AM", source: "startRange" }, + { type: "literal", value: " – ", source: "shared" }, + { type: "hour", value: "8", source: "endRange" }, + { type: "literal", value: ":", source: "endRange" }, + { type: "minute", value: "41", source: "endRange" }, + { type: "literal", value: ":", source: "endRange" }, + { type: "second", value: "09", source: "endRange" }, + { type: "literal", value: " ", source: "endRange" }, + { type: "dayPeriod", value: "PM", source: "endRange" }, + ]); + }); + + test("Temporal.Instant", () => { + const instant1 = new Temporal.Instant(601546251000000000n); + const instant2 = new Temporal.Instant(1732740069000000000n); + expect(formatter.formatRangeToParts(instant1, instant2)).toEqual([ + { type: "month", value: "1", source: "startRange" }, + { type: "literal", value: "/", source: "startRange" }, + { type: "day", value: "23", source: "startRange" }, + { type: "literal", value: "/", source: "startRange" }, + { type: "year", value: "1989", source: "startRange" }, + { type: "literal", value: ", ", source: "startRange" }, + { type: "hour", value: "8", source: "startRange" }, + { type: "literal", value: ":", source: "startRange" }, + { type: "minute", value: "10", source: "startRange" }, + { type: "literal", value: ":", source: "startRange" }, + { type: "second", value: "51", source: "startRange" }, + { type: "literal", value: " ", source: "startRange" }, + { type: "dayPeriod", value: "AM", source: "startRange" }, + { type: "literal", value: " – ", source: "shared" }, + { type: "month", value: "11", source: "endRange" }, + { type: "literal", value: "/", source: "endRange" }, + { type: "day", value: "27", source: "endRange" }, + { type: "literal", value: "/", source: "endRange" }, + { type: "year", value: "2024", source: "endRange" }, + { type: "literal", value: ", ", source: "endRange" }, + { type: "hour", value: "8", source: "endRange" }, + { type: "literal", value: ":", source: "endRange" }, + { type: "minute", value: "41", source: "endRange" }, + { type: "literal", value: ":", source: "endRange" }, + { type: "second", value: "09", source: "endRange" }, + { type: "literal", value: " ", source: "endRange" }, + { type: "dayPeriod", value: "PM", source: "endRange" }, + ]); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatToParts.js b/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatToParts.js index 86d9e0d9fbd..e73ae55bf73 100644 --- a/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatToParts.js +++ b/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatToParts.js @@ -28,6 +28,49 @@ describe("errors", () => { Intl.DateTimeFormat().formatToParts(8.65e15); }).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15"); }); + + test("Temporal object must have same calendar", () => { + const formatter = new Intl.DateTimeFormat([], { calendar: "iso8601" }); + + expect(() => { + formatter.formatToParts(new Temporal.PlainDate(1972, 1, 1, "gregory")); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainDate with calendar 'gregory' in locale with calendar 'iso8601'" + ); + + expect(() => { + formatter.formatToParts(new Temporal.PlainYearMonth(1972, 1, "gregory")); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainYearMonth with calendar 'gregory' in locale with calendar 'iso8601'" + ); + + expect(() => { + formatter.formatToParts(new Temporal.PlainMonthDay(1, 1, "gregory")); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainMonthDay with calendar 'gregory' in locale with calendar 'iso8601'" + ); + + expect(() => { + formatter.formatToParts( + new Temporal.PlainDateTime(1972, 1, 1, 8, 45, 56, 123, 345, 789, "gregory") + ); + }).toThrowWithMessage( + RangeError, + "Cannot format Temporal.PlainDateTime with calendar 'gregory' in locale with calendar 'iso8601'" + ); + }); + + test("cannot format Temporal.ZonedDateTime", () => { + expect(() => { + new Intl.DateTimeFormat().formatToParts(new Temporal.ZonedDateTime(0n, "UTC")); + }).toThrowWithMessage( + TypeError, + "Cannot format Temporal.ZonedDateTime, use Temporal.ZonedDateTime.prototype.toLocaleString" + ); + }); }); const d = Date.UTC(1989, 0, 23, 7, 8, 9, 45); @@ -275,3 +318,71 @@ describe("special cases", () => { ]); }); }); + +describe("Temporal objects", () => { + const formatter = new Intl.DateTimeFormat("en", { + calendar: "iso8601", + timeZone: "UTC", + }); + + test("Temporal.PlainDate", () => { + const plainDate = new Temporal.PlainDate(1989, 1, 23); + expect(formatter.formatToParts(plainDate)).toEqual([ + { type: "month", value: "1" }, + { type: "literal", value: "/" }, + { type: "day", value: "23" }, + { type: "literal", value: "/" }, + { type: "year", value: "1989" }, + ]); + }); + + test("Temporal.PlainYearMonth", () => { + const plainYearMonth = new Temporal.PlainYearMonth(1989, 1); + expect(formatter.formatToParts(plainYearMonth)).toEqual([ + { type: "month", value: "1" }, + { type: "literal", value: "/" }, + { type: "year", value: "1989" }, + ]); + }); + + test("Temporal.PlainMonthDay", () => { + const plainMonthDay = new Temporal.PlainMonthDay(1, 23); + expect(formatter.formatToParts(plainMonthDay)).toEqual([ + { type: "month", value: "1" }, + { type: "literal", value: "/" }, + { type: "day", value: "23" }, + ]); + }); + + test("Temporal.PlainTime", () => { + const plainTime = new Temporal.PlainTime(8, 10, 51); + expect(formatter.formatToParts(plainTime)).toEqual([ + { type: "hour", value: "8" }, + { type: "literal", value: ":" }, + { type: "minute", value: "10" }, + { type: "literal", value: ":" }, + { type: "second", value: "51" }, + { type: "literal", value: " " }, + { type: "dayPeriod", value: "AM" }, + ]); + }); + + test("Temporal.Instant", () => { + const instant = new Temporal.Instant(1732740069000000000n); + expect(formatter.formatToParts(instant)).toEqual([ + { type: "month", value: "11" }, + { type: "literal", value: "/" }, + { type: "day", value: "27" }, + { type: "literal", value: "/" }, + { type: "year", value: "2024" }, + { type: "literal", value: ", " }, + { type: "hour", value: "8" }, + { type: "literal", value: ":" }, + { type: "minute", value: "41" }, + { type: "literal", value: ":" }, + { type: "second", value: "09" }, + { type: "literal", value: " " }, + { type: "dayPeriod", value: "PM" }, + ]); + }); +}); diff --git a/Libraries/LibUnicode/DateTimeFormat.h b/Libraries/LibUnicode/DateTimeFormat.h index 3a5949cef52..7ad970fc029 100644 --- a/Libraries/LibUnicode/DateTimeFormat.h +++ b/Libraries/LibUnicode/DateTimeFormat.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -60,9 +61,60 @@ CalendarPatternStyle calendar_pattern_style_from_string(StringView style); StringView calendar_pattern_style_to_string(CalendarPatternStyle style); struct CalendarPattern { + enum class Field { + Era, + Year, + Month, + Weekday, + Day, + DayPeriod, + Hour, + Minute, + Second, + FractionalSecondDigits, + TimeZoneName, + }; + static CalendarPattern create_from_pattern(StringView); String to_pattern() const; + template + void for_each_calendar_field_zipped_with(CalendarPattern& other, ReadonlySpan filter, Callback&& callback) const + { + auto invoke_callback_for_field = [&](auto field) { + switch (field) { + case Field::Era: + return callback(era, other.era); + case Field::Year: + return callback(year, other.year); + case Field::Month: + return callback(month, other.month); + case Field::Weekday: + return callback(weekday, other.weekday); + case Field::Day: + return callback(day, other.day); + case Field::DayPeriod: + return callback(day_period, other.day_period); + case Field::Hour: + return callback(hour, other.hour); + case Field::Minute: + return callback(minute, other.minute); + case Field::Second: + return callback(second, other.second); + case Field::FractionalSecondDigits: + return callback(fractional_second_digits, other.fractional_second_digits); + case Field::TimeZoneName: + return callback(time_zone_name, other.time_zone_name); + } + VERIFY_NOT_REACHED(); + }; + + for (auto field : filter) { + if (invoke_callback_for_field(field) == IterationDecision::Break) + break; + } + } + Optional hour_cycle; Optional hour12;