From ea503a4f683708e5d24d725935c6a21293745be2 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 27 Nov 2024 16:09:41 -0500 Subject: [PATCH] LibJS+LibUnicode: Integrate Temporal into Intl.DateTimeFormat The gist is that we need to construct an ICU date-time formatter for each possible Temporal type. This is of course going to be expensive. So instead, we construct the configurations needed for the ICU objects in the Intl.DateTimeFormat constructor, and defer creating the actual ICU objects until they are needed. Each formatting prototype can also now accept either a number (as they already did), or any of the supported Temporal objects. These types may not be mixed, and their properties (namely, their calendar) must align with the Intl.DateTimeFormat object. --- Libraries/LibJS/Runtime/ErrorTypes.h | 4 + .../LibJS/Runtime/Intl/DateTimeFormat.cpp | 618 ++++++++++++++++-- Libraries/LibJS/Runtime/Intl/DateTimeFormat.h | 100 ++- .../Intl/DateTimeFormatConstructor.cpp | 212 +++--- .../Runtime/Intl/DateTimeFormatConstructor.h | 14 +- .../Runtime/Intl/DateTimeFormatFunction.cpp | 17 +- .../Runtime/Intl/DateTimeFormatPrototype.cpp | 55 +- .../DateTimeFormat.prototype.format.js | 75 +++ .../DateTimeFormat.prototype.formatRange.js | 115 ++++ ...TimeFormat.prototype.formatRangeToParts.js | 185 ++++++ .../DateTimeFormat.prototype.formatToParts.js | 111 ++++ Libraries/LibUnicode/DateTimeFormat.h | 52 ++ 12 files changed, 1348 insertions(+), 210 deletions(-) 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;