diff options
author | Timothy Flynn <trflynn89@pm.me> | 2021-12-06 12:26:49 -0500 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2021-12-08 11:29:36 +0000 |
commit | adaf5985a4915261b8eb366b272eb879edfd4d88 (patch) | |
tree | 96290b765b47a33f2fe06fd5ce0c4d0a68c7c425 | |
parent | d010ba10c3987fc2a1beaf5c83767fdab87833b0 (diff) | |
download | serenity-adaf5985a4915261b8eb366b272eb879edfd4d88.zip |
LibJS: Implement (most of) Intl.DateTimeFormat.prototype.format
There are a few FIXMEs that will need to be addressed, but this
implements most of the prototype method. The FIXMEs are mostly related
to range formatting, which has been entirely ignored so far. But other
than that, the following will need to be addressed:
* Determining flexible day periods must be made locale-aware.
* DST will need to be determined and acted upon.
* Time zones other than UTC and calendars other than Gregorian are
ignored.
* Some of our results differ from other engines as they have some
format patterns we do not. For example, they seem to have a lonely
{dayPeriod} pattern, whereas our closest pattern is
"{hour} {dayPeriod}".
9 files changed, 977 insertions, 8 deletions
diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index 086fee3fe4..1f8b0684da 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -88,6 +88,7 @@ set(SOURCES Runtime/Intl/AbstractOperations.cpp Runtime/Intl/DateTimeFormat.cpp Runtime/Intl/DateTimeFormatConstructor.cpp + Runtime/Intl/DateTimeFormatFunction.cpp Runtime/Intl/DateTimeFormatPrototype.cpp Runtime/Intl/DisplayNames.cpp Runtime/Intl/DisplayNamesConstructor.cpp diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index fadc7bfca6..1d6febbcbc 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -37,6 +37,7 @@ M(InstanceOfOperatorBadPrototype, "'prototype' property of {} is not an object") \ M(IntlInvalidDateTimeFormatOption, "Option {} cannot be set when also providing {}") \ M(IntlInvalidLanguageTag, "{} is not a structurally valid language tag") \ + M(IntlInvalidTime, "Time value must be between -8.64E15 and 8.64E15") \ M(IntlMinimumExceedsMaximum, "Minimum value {} is larger than maximum value {}") \ M(IntlNumberIsNaNOrOutOfRange, "Value {} is NaN or is not between {} and {}") \ M(IntlOptionUndefined, "Option {} must be defined when option {} is {}") \ diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp index f88e5a7bb8..79b3432444 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp @@ -5,10 +5,18 @@ */ #include <AK/NumericLimits.h> -#include <LibJS/Runtime/Intl/AbstractOperations.h> +#include <LibJS/Runtime/AbstractOperations.h> +#include <LibJS/Runtime/Date.h> #include <LibJS/Runtime/Intl/DateTimeFormat.h> +#include <LibJS/Runtime/Intl/NumberFormat.h> +#include <LibJS/Runtime/Intl/NumberFormatConstructor.h> +#include <LibJS/Runtime/MarkedValueList.h> +#include <LibJS/Runtime/NativeFunction.h> #include <LibJS/Runtime/Temporal/TimeZone.h> +#include <LibJS/Runtime/Utf16String.h> #include <LibUnicode/Locale.h> +#include <LibUnicode/NumberFormat.h> +#include <math.h> namespace JS::Intl { @@ -18,6 +26,13 @@ DateTimeFormat::DateTimeFormat(Object& prototype) { } +void DateTimeFormat::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + if (m_bound_format) + visitor.visit(m_bound_format); +} + DateTimeFormat::Style DateTimeFormat::style_from_string(StringView style) { if (style == "full"sv) @@ -694,4 +709,413 @@ Optional<Unicode::CalendarPattern> best_fit_format_matcher(Unicode::CalendarPatt return basic_format_matcher(options, move(formats)); } +struct StyleAndValue { + Unicode::CalendarPatternStyle style {}; + i32 value { 0 }; +}; + +static Optional<StyleAndValue> find_calendar_field(StringView name, DateTimeFormat const& date_time_format, LocalTime const& local_time) +{ + auto make_style_and_value = [](auto style, auto value) { + return StyleAndValue { style, static_cast<i32>(value) }; + }; + + if (name == "weekday"sv) + return make_style_and_value(date_time_format.weekday(), local_time.weekday); + if (name == "era"sv) + return make_style_and_value(date_time_format.era(), local_time.era); + if (name == "year"sv) + return make_style_and_value(date_time_format.year(), local_time.year); + if (name == "month"sv) + return make_style_and_value(date_time_format.month(), local_time.month); + if (name == "day"sv) + return make_style_and_value(date_time_format.day(), local_time.day); + if (name == "hour"sv) + return make_style_and_value(date_time_format.hour(), local_time.hour); + if (name == "minute"sv) + return make_style_and_value(date_time_format.minute(), local_time.minute); + if (name == "second"sv) + return make_style_and_value(date_time_format.second(), local_time.second); + return {}; +} + +// 11.1.7 FormatDateTimePattern ( dateTimeFormat, patternParts, x, rangeFormatOptions ), https://tc39.es/ecma402/#sec-formatdatetimepattern +ThrowCompletionOr<Vector<PatternPartition>> format_date_time_pattern(GlobalObject& global_object, DateTimeFormat& date_time_format, Vector<PatternPartition> pattern_parts, Value time, [[maybe_unused]] Value range_format_options) +{ + auto& vm = global_object.vm(); + + // 1. Let x be TimeClip(x). + time = time_clip(global_object, time); + + // 2. If x is NaN, throw a RangeError exception. + if (time.is_nan()) + return vm.throw_completion<RangeError>(global_object, ErrorType::IntlInvalidTime); + + // 3. Let locale be dateTimeFormat.[[Locale]]. + auto const& locale = date_time_format.locale(); + auto const& data_locale = date_time_format.data_locale(); + + auto construct_number_format = [&](auto* options) -> ThrowCompletionOr<NumberFormat*> { + MarkedValueList arguments { vm.heap() }; + arguments.append(js_string(vm, locale)); + arguments.append(options); + + auto* number_format = TRY(construct(global_object, *global_object.intl_number_format_constructor(), move(arguments))); + return static_cast<NumberFormat*>(number_format); + }; + + // 4. Let nfOptions be OrdinaryObjectCreate(null). + auto* number_format_options = Object::create(global_object, nullptr); + + // 5. Perform ! CreateDataPropertyOrThrow(nfOptions, "useGrouping", false). + MUST(number_format_options->create_data_property_or_throw(vm.names.useGrouping, Value(false))); + + // 6. Let nf be ? Construct(%NumberFormat%, « locale, nfOptions »). + auto* number_format = TRY(construct_number_format(number_format_options)); + + // 7. Let nf2Options be OrdinaryObjectCreate(null). + auto* number_format_options2 = Object::create(global_object, nullptr); + + // 8. Perform ! CreateDataPropertyOrThrow(nf2Options, "minimumIntegerDigits", 2). + MUST(number_format_options2->create_data_property_or_throw(vm.names.minimumIntegerDigits, Value(2))); + + // 9. Perform ! CreateDataPropertyOrThrow(nf2Options, "useGrouping", false). + MUST(number_format_options2->create_data_property_or_throw(vm.names.useGrouping, Value(false))); + + // 10. Let nf2 be ? Construct(%NumberFormat%, « locale, nf2Options »). + auto* number_format2 = TRY(construct_number_format(number_format_options2)); + + // 11. Let fractionalSecondDigits be dateTimeFormat.[[FractionalSecondDigits]]. + Optional<u8> fractional_second_digits; + NumberFormat* number_format3 = nullptr; + + // 12. If fractionalSecondDigits is not undefined, then + if (date_time_format.has_fractional_second_digits()) { + fractional_second_digits = date_time_format.fractional_second_digits(); + + // a. Let nf3Options be OrdinaryObjectCreate(null). + auto* number_format_options3 = Object::create(global_object, nullptr); + + // b. Perform ! CreateDataPropertyOrThrow(nf3Options, "minimumIntegerDigits", fractionalSecondDigits). + MUST(number_format_options3->create_data_property_or_throw(vm.names.minimumIntegerDigits, Value(*fractional_second_digits))); + + // c. Perform ! CreateDataPropertyOrThrow(nf3Options, "useGrouping", false). + MUST(number_format_options3->create_data_property_or_throw(vm.names.useGrouping, Value(false))); + + // d. Let nf3 be ? Construct(%NumberFormat%, « locale, nf3Options »). + number_format3 = TRY(construct_number_format(number_format_options3)); + } + + // 13. Let tm be ToLocalTime(x, dateTimeFormat.[[Calendar]], dateTimeFormat.[[TimeZone]]). + auto local_time = TRY(to_local_time(global_object, time.as_double(), date_time_format.calendar(), date_time_format.time_zone())); + + // 14. Let result be a new empty List. + Vector<PatternPartition> result; + + // 15. For each Record { [[Type]], [[Value]] } patternPart in patternParts, do + for (auto& pattern_part : pattern_parts) { + // a. Let p be patternPart.[[Type]]. + auto part = pattern_part.type; + + // b. If p is "literal", then + if (part == "literal"sv) { + // i. Append a new Record { [[Type]]: "literal", [[Value]]: patternPart.[[Value]] } as the last element of the list result. + result.append({ "literal"sv, move(pattern_part.value) }); + } + + // c. Else if p is equal to "fractionalSecondDigits", then + else if (part == "fractionalSecondDigits"sv) { + // i. Let v be tm.[[Millisecond]]. + auto value = local_time.millisecond; + + // ii. Let v be floor(v × 10^(fractionalSecondDigits - 3)). + value = floor(value * pow(10, static_cast<int>(*fractional_second_digits) - 3)); + + // iii. Let fv be FormatNumeric(nf3, v). + auto formatted_value = format_numeric(*number_format3, value); + + // iv. Append a new Record { [[Type]]: "fractionalSecond", [[Value]]: fv } as the last element of result. + result.append({ "fractionalSecond"sv, move(formatted_value) }); + } + + // d. Else if p is equal to "dayPeriod", then + else if (part == "dayPeriod"sv) { + Optional<StringView> symbol; + String formatted_value; + + // i. Let f be the value of dateTimeFormat's internal slot whose name is the Internal Slot column of the matching row. + auto style = date_time_format.day_period(); + + // ii. Let fv be a String value representing the day period of tm in the form given by f; the String value depends upon the implementation and the effective locale of dateTimeFormat. + // FIXME: This isn't locale-aware. We should parse the CLDR's cldr-core/supplemental/dayPeriods.json file to acquire day periods + // per-locale. For now, these are hard-coded to the en locale's values. + if ((local_time.hour >= 6) && (local_time.hour < 12)) + symbol = Unicode::get_calendar_day_period_symbol(data_locale, date_time_format.calendar(), style, Unicode::DayPeriod::Morning); + else if ((local_time.hour >= 12) && (local_time.hour < 18)) + symbol = Unicode::get_calendar_day_period_symbol(data_locale, date_time_format.calendar(), style, Unicode::DayPeriod::Afternoon); + else if ((local_time.hour >= 18) && (local_time.hour < 21)) + symbol = Unicode::get_calendar_day_period_symbol(data_locale, date_time_format.calendar(), style, Unicode::DayPeriod::Evening); + else + symbol = Unicode::get_calendar_day_period_symbol(data_locale, date_time_format.calendar(), style, Unicode::DayPeriod::Night); + + if (symbol.has_value()) + formatted_value = *symbol; + + // iii. Append a new Record { [[Type]]: p, [[Value]]: fv } as the last element of the list result. + result.append({ "dayPeriod"sv, move(formatted_value) }); + } + + // e. Else if p is equal to "timeZoneName", then + else if (part == "timeZoneName"sv) { + // i. Let f be dateTimeFormat.[[TimeZoneName]]. + auto style = date_time_format.time_zone_name(); + + // ii. Let v be dateTimeFormat.[[TimeZone]]. + auto const& value = date_time_format.time_zone(); + + // iii. Let fv be a String value representing v in the form given by f; the String value depends upon the implementation and the effective locale. The String value may also depend on the value of the [[InDST]] field of tm. If the implementation does not have a localized representation of f, then use the String value of v itself. + // FIXME: This should take [[InDST]] into account. + auto formatted_value = Unicode::get_time_zone_name(data_locale, value, style).value_or(value); + + // iv. Append a new Record { [[Type]]: p, [[Value]]: fv } as the last element of the list result. + result.append({ "timeZoneName"sv, move(formatted_value) }); + } + + // f. Else if p matches a Property column of the row in Table 4, then + else if (auto style_and_value = find_calendar_field(part, date_time_format, local_time); style_and_value.has_value()) { + String formatted_value; + + // i. If rangeFormatOptions is not undefined, let f be the value of rangeFormatOptions's field whose name matches p. + // ii. Else, let f be the value of dateTimeFormat's internal slot whose name is the Internal Slot column of the matching row. + // FIXME: Implement step i when range format is supported. + auto style = style_and_value->style; + + // iii. Let v be the value of tm's field whose name is the Internal Slot column of the matching row. + auto value = style_and_value->value; + + // iv. If p is "year" and v ≤ 0, let v be 1 - v. + if ((part == "year"sv) && (value <= 0)) + value = 1 - value; + + // v. If p is "month", increase v by 1. + if (part == "month"sv) + ++value; + + if (part == "hour"sv) { + auto hour_cycle = date_time_format.hour_cycle(); + + // vi. If p is "hour" and dateTimeFormat.[[HourCycle]] is "h11" or "h12", then + if ((hour_cycle == Unicode::HourCycle::H11) || (hour_cycle == Unicode::HourCycle::H12)) { + // 1. Let v be v modulo 12. + value = value % 12; + + // 2. If v is 0 and dateTimeFormat.[[HourCycle]] is "h12", let v be 12. + if ((value == 0) && (hour_cycle == Unicode::HourCycle::H12)) + value = 12; + } + + // vii. If p is "hour" and dateTimeFormat.[[HourCycle]] is "h24", then + if (hour_cycle == Unicode::HourCycle::H24) { + // 1. If v is 0, let v be 24. + if (value == 0) + value = 24; + } + } + + switch (style) { + // viii. If f is "numeric", then + case Unicode::CalendarPatternStyle::Numeric: + // 1. Let fv be FormatNumeric(nf, v). + formatted_value = format_numeric(*number_format, value); + break; + + // ix. Else if f is "2-digit", then + case Unicode::CalendarPatternStyle::TwoDigit: + // 1. Let fv be FormatNumeric(nf2, v). + formatted_value = format_numeric(*number_format2, value); + + // 2. If the "length" property of fv is greater than 2, let fv be the substring of fv containing the last two characters. + // NOTE: The first length check here isn't enough, but lets us avoid UTF-16 transcoding when the formatted value is ASCII. + if (formatted_value.length() > 2) { + Utf16String utf16_formatted_value { formatted_value }; + if (utf16_formatted_value.length_in_code_units() > 2) + formatted_value = utf16_formatted_value.substring_view(utf16_formatted_value.length_in_code_units() - 2).to_utf8(); + } + + break; + + // x. Else if f is "narrow", "short", or "long", then let fv be a String value representing v in the form given by f; the String value depends upon the implementation and the effective locale and calendar of dateTimeFormat. + // If p is "month" and rangeFormatOptions is undefined, then the String value may also depend on whether dateTimeFormat.[[Day]] is undefined. + // If p is "month" and rangeFormatOptions is not undefined, then the String value may also depend on whether rangeFormatOptions.[[day]] is undefined. + // If p is "era" and rangeFormatOptions is undefined, then the String value may also depend on whether dateTimeFormat.[[Era]] is undefined. + // If p is "era" and rangeFormatOptions is not undefined, then the String value may also depend on whether rangeFormatOptions.[[era]] is undefined. + // If the implementation does not have a localized representation of f, then use the String value of v itself. + case Unicode::CalendarPatternStyle::Narrow: + case Unicode::CalendarPatternStyle::Short: + case Unicode::CalendarPatternStyle::Long: { + Optional<StringView> symbol; + + if (part == "era"sv) + symbol = Unicode::get_calendar_era_symbol(data_locale, date_time_format.calendar(), style, static_cast<Unicode::Era>(value)); + else if (part == "month"sv) + symbol = Unicode::get_calendar_month_symbol(data_locale, date_time_format.calendar(), style, static_cast<Unicode::Month>(value - 1)); + else if (part == "weekday"sv) + symbol = Unicode::get_calendar_weekday_symbol(data_locale, date_time_format.calendar(), style, static_cast<Unicode::Weekday>(value)); + + formatted_value = symbol.value_or(String::number(value)); + break; + } + } + + // xi. Append a new Record { [[Type]]: p, [[Value]]: fv } as the last element of the list result. + result.append({ part, move(formatted_value) }); + } + + // g. Else if p is equal to "ampm", then + else if (part == "ampm"sv) { + String formatted_value; + + // i. Let v be tm.[[Hour]]. + auto value = local_time.hour; + + // ii. If v is greater than 11, then + if (value > 11) { + // 1. Let fv be an implementation and locale dependent String value representing "post meridiem". + auto symbol = Unicode::get_calendar_day_period_symbol(data_locale, date_time_format.calendar(), Unicode::CalendarPatternStyle::Short, Unicode::DayPeriod::PM); + formatted_value = symbol.value_or("PM"sv); + } + // iii. Else, + else { + // 1. Let fv be an implementation and locale dependent String value representing "ante meridiem". + auto symbol = Unicode::get_calendar_day_period_symbol(data_locale, date_time_format.calendar(), Unicode::CalendarPatternStyle::Short, Unicode::DayPeriod::AM); + formatted_value = symbol.value_or("AM"sv); + } + + // iv. Append a new Record { [[Type]]: "dayPeriod", [[Value]]: fv } as the last element of the list result. + result.append({ "dayPeriod"sv, move(formatted_value) }); + } + + // h. Else if p is equal to "relatedYear", then + else if (part == "relatedYear"sv) { + // i. Let v be tm.[[RelatedYear]]. + // ii. Let fv be FormatNumeric(nf, v). + // iii. Append a new Record { [[Type]]: "relatedYear", [[Value]]: fv } as the last element of the list result. + + // FIXME: Implement this when relatedYear is supported. + } + + // i. Else if p is equal to "yearName", then + else if (part == "yearName"sv) { + // i. Let v be tm.[[YearName]]. + // ii. Let fv be an implementation and locale dependent String value representing v. + // iii. Append a new Record { [[Type]]: "yearName", [[Value]]: fv } as the last element of the list result. + + // FIXME: Implement this when yearName is supported. + } + + // Non-standard, TR-35 requires the decimal separator before injected {fractionalSecondDigits} partitions + // to adhere to the selected locale. This depends on other generated data, so it is deferred to here. + else if (part == "decimal"sv) { + auto decimal_symbol = Unicode::get_number_system_symbol(data_locale, date_time_format.numbering_system(), "decimal"sv).value_or("."sv); + result.append({ "literal"sv, decimal_symbol }); + } + + // j. Else, + else { + // i. Let unknown be an implementation-, locale-, and numbering system-dependent String based on x and p. + // ii. Append a new Record { [[Type]]: "unknown", [[Value]]: unknown } as the last element of result. + + // LibUnicode doesn't generate any "unknown" patterns. + VERIFY_NOT_REACHED(); + } + } + + // 16. Return result. + return result; +} + +// 11.1.8 PartitionDateTimePattern ( dateTimeFormat, x ), https://tc39.es/ecma402/#sec-partitiondatetimepattern +ThrowCompletionOr<Vector<PatternPartition>> partition_date_time_pattern(GlobalObject& global_object, DateTimeFormat& date_time_format, Value time) +{ + // 1. Let patternParts be PartitionPattern(dateTimeFormat.[[Pattern]]). + auto pattern_parts = partition_pattern(date_time_format.pattern()); + + // 2. Let result be ? FormatDateTimePattern(dateTimeFormat, patternParts, x, undefined). + auto result = TRY(format_date_time_pattern(global_object, date_time_format, move(pattern_parts), time, js_undefined())); + + // 3. Return result. + return result; +} + +// 11.1.9 FormatDateTime ( dateTimeFormat, x ), https://tc39.es/ecma402/#sec-formatdatetime +ThrowCompletionOr<String> format_date_time(GlobalObject& global_object, DateTimeFormat& date_time_format, Value time) +{ + // 1. Let parts be ? PartitionDateTimePattern(dateTimeFormat, x). + auto parts = TRY(partition_date_time_pattern(global_object, date_time_format, time)); + + // 2. Let result be the empty String. + StringBuilder result; + + // 3. For each Record { [[Type]], [[Value]] } part in parts, do + for (auto& part : parts) { + // a. Set result to the string-concatenation of result and part.[[Value]]. + result.append(move(part.value)); + } + + // 4. Return result. + return result.build(); +} + +// 11.1.14 ToLocalTime ( t, calendar, timeZone ), https://tc39.es/ecma402/#sec-tolocaltime +ThrowCompletionOr<LocalTime> to_local_time(GlobalObject& global_object, double time, StringView calendar, [[maybe_unused]] StringView time_zone) +{ + // 1. Assert: Type(t) is Number. + + // 2. If calendar is "gregory", then + if (calendar == "gregory"sv) { + // a. Let timeZoneOffset be the value calculated according to LocalTZA(t, true) where the local time zone is replaced with timezone timeZone. + // FIXME: Implement LocalTZA when timezones other than UTC are supported. + double time_zone_offset = 0; + + // b. Let tz be the time value t + timeZoneOffset. + double zoned_time = time + time_zone_offset; + + auto year = year_from_time(zoned_time); + + // c. Return a record with fields calculated from tz according to Table 5. + return LocalTime { + // WeekDay(tz) specified in es2022's Week Day. + .weekday = week_day(zoned_time), + // Let year be YearFromTime(tz) specified in es2022's Year Number. If year is less than 0, return 'BC', else, return 'AD'. + .era = year < 0 ? Unicode::Era::BC : Unicode::Era::AD, + // YearFromTime(tz) specified in es2022's Year Number. + .year = year, + // undefined. + .related_year = js_undefined(), + // undefined. + .year_name = js_undefined(), + // MonthFromTime(tz) specified in es2022's Month Number. + .month = month_from_time(zoned_time), + // DateFromTime(tz) specified in es2022's Date Number. + .day = date_from_time(zoned_time), + // HourFromTime(tz) specified in es2022's Hours, Minutes, Second, and Milliseconds. + .hour = hour_from_time(zoned_time), + // MinFromTime(tz) specified in es2022's Hours, Minutes, Second, and Milliseconds. + .minute = min_from_time(zoned_time), + // SecFromTime(tz) specified in es2022's Hours, Minutes, Second, and Milliseconds. + .second = sec_from_time(zoned_time), + // msFromTime(tz) specified in es2022's Hours, Minutes, Second, and Milliseconds. + .millisecond = ms_from_time(zoned_time), + // Calculate true or false using the best available information about the specified calendar and timeZone, including current and historical information about time zone offsets from UTC and daylight saving time rules. + // FIXME: Implement this. + .in_dst = false, + }; + } + + // 3. Else, + // a. Return a record with the fields of Column 1 of Table 5 calculated from t for the given calendar and timeZone. The calculations should use best available information about the specified calendar and timeZone, including current and historical information about time zone offsets from UTC and daylight saving time rules. + // FIXME: Implement this when non-Gregorian calendars are supported by LibUnicode. + return global_object.vm().throw_completion<InternalError>(global_object, ErrorType::NotImplemented, "Non-Gregorian calendars"sv); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.h b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.h index ece9ef225d..9134d6493c 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.h @@ -13,6 +13,7 @@ #include <AK/Vector.h> #include <LibJS/Runtime/Completion.h> #include <LibJS/Runtime/GlobalObject.h> +#include <LibJS/Runtime/Intl/AbstractOperations.h> #include <LibJS/Runtime/Object.h> #include <LibUnicode/DateTimeFormat.h> @@ -121,17 +122,23 @@ public: Unicode::CalendarPatternStyle time_zone_name() const { return *Patterns::time_zone_name; }; StringView time_zone_name_string() const { return Unicode::calendar_pattern_style_to_string(*Patterns::time_zone_name); } + NativeFunction* bound_format() const { return m_bound_format; } + void set_bound_format(NativeFunction* bound_format) { m_bound_format = bound_format; } + private: static Style style_from_string(StringView style); static StringView style_to_string(Style style); - String m_locale; // [[Locale]] - String m_calendar; // [[Calendar]] - String m_numbering_system; // [[NumberingSystem]] - Optional<Unicode::HourCycle> m_hour_cycle; // [[HourCycle]] - String m_time_zone; // [[TimeZone]] - Optional<Style> m_date_style; // [[DateStyle]] - Optional<Style> m_time_style; // [[TimeStyle]] + virtual void visit_edges(Visitor&) override; + + String m_locale; // [[Locale]] + String m_calendar; // [[Calendar]] + String m_numbering_system; // [[NumberingSystem]] + Optional<Unicode::HourCycle> m_hour_cycle; // [[HourCycle]] + String m_time_zone; // [[TimeZone]] + Optional<Style> m_date_style; // [[DateStyle]] + Optional<Style> m_time_style; // [[TimeStyle]] + NativeFunction* m_bound_format { nullptr }; // [[BoundFormat]] String m_data_locale; }; @@ -148,11 +155,31 @@ enum class OptionDefaults { Time, }; +// Table 5: Record returned by ToLocalTime, https://tc39.es/ecma402/#table-datetimeformat-tolocaltime-record +struct LocalTime { + int weekday { 0 }; // [[Weekday]] + Unicode::Era era {}; // [[Era]] + i32 year { 0 }; // [[Year]] + Value related_year {}; // [[RelatedYear]] + Value year_name {}; // [[YearName]] + u8 month { 0 }; // [[Month]] + u8 day { 0 }; // [[Day]] + u8 hour { 0 }; // [[Hour]] + u8 minute { 0 }; // [[Minute]] + u8 second { 0 }; // [[Second]] + u16 millisecond { 0 }; // [[Millisecond]] + bool in_dst { false }; // [[InDST]] +}; + ThrowCompletionOr<DateTimeFormat*> initialize_date_time_format(GlobalObject& global_object, DateTimeFormat& date_time_format, Value locales_value, Value options_value); ThrowCompletionOr<Object*> to_date_time_options(GlobalObject& global_object, Value options_value, OptionRequired, OptionDefaults); Optional<Unicode::CalendarPattern> date_time_style_format(StringView data_locale, DateTimeFormat& date_time_format); Optional<Unicode::CalendarPattern> basic_format_matcher(Unicode::CalendarPattern const& options, Vector<Unicode::CalendarPattern> formats); Optional<Unicode::CalendarPattern> best_fit_format_matcher(Unicode::CalendarPattern const& options, Vector<Unicode::CalendarPattern> formats); +ThrowCompletionOr<Vector<PatternPartition>> format_date_time_pattern(GlobalObject& global_object, DateTimeFormat& date_time_format, Vector<PatternPartition> pattern_parts, Value time, Value range_format_options); +ThrowCompletionOr<Vector<PatternPartition>> partition_date_time_pattern(GlobalObject& global_object, DateTimeFormat& date_time_format, Value time); +ThrowCompletionOr<String> format_date_time(GlobalObject& global_object, DateTimeFormat& date_time_format, Value time); +ThrowCompletionOr<LocalTime> to_local_time(GlobalObject& global_object, double time, StringView calendar, StringView time_zone); template<typename Callback> ThrowCompletionOr<void> for_each_calendar_field(GlobalObject& global_object, Unicode::CalendarPattern& pattern, Callback&& callback) diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatFunction.cpp b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatFunction.cpp new file mode 100644 index 0000000000..568d988e8d --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatFunction.cpp @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2021, Tim Flynn <trflynn89@pm.me> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <LibJS/Runtime/AbstractOperations.h> +#include <LibJS/Runtime/Date.h> +#include <LibJS/Runtime/DateConstructor.h> +#include <LibJS/Runtime/GlobalObject.h> +#include <LibJS/Runtime/Intl/DateTimeFormat.h> +#include <LibJS/Runtime/Intl/DateTimeFormatFunction.h> + +namespace JS::Intl { + +// 11.1.6 DateTime Format Functions, https://tc39.es/ecma402/#sec-datetime-format-functions +DateTimeFormatFunction* DateTimeFormatFunction::create(GlobalObject& global_object, DateTimeFormat& date_time_format) +{ + return global_object.heap().allocate<DateTimeFormatFunction>(global_object, date_time_format, *global_object.function_prototype()); +} + +DateTimeFormatFunction::DateTimeFormatFunction(DateTimeFormat& date_time_format, Object& prototype) + : NativeFunction(prototype) + , m_date_time_format(date_time_format) +{ +} + +void DateTimeFormatFunction::initialize(GlobalObject& global_object) +{ + auto& vm = this->vm(); + + Base::initialize(global_object); + define_direct_property(vm.names.length, Value(1), Attribute::Configurable); + define_direct_property(vm.names.name, js_string(vm, String::empty()), Attribute::Configurable); +} + +ThrowCompletionOr<Value> DateTimeFormatFunction::call() +{ + auto& global_object = this->global_object(); + auto& vm = global_object.vm(); + + auto date = vm.argument(0); + + // 1. Let dtf be F.[[DateTimeFormat]]. + // 2. Assert: Type(dtf) is Object and dtf has an [[InitializedDateTimeFormat]] internal slot. + + // 3. If date is not provided or is undefined, then + if (date.is_undefined()) { + // a. Let x be Call(%Date.now%, undefined). + date = MUST(JS::call(global_object, global_object.date_constructor_now_function(), js_undefined())); + } + // 4. Else, + else { + // a. Let x be ? ToNumber(date). + date = TRY(date.to_number(global_object)); + } + + // 5. Return ? FormatDateTime(dtf, x). + auto formatted = TRY(format_date_time(global_object, m_date_time_format, date)); + return js_string(vm, move(formatted)); +} + +void DateTimeFormatFunction::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(&m_date_time_format); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatFunction.h b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatFunction.h new file mode 100644 index 0000000000..aa39e223ba --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatFunction.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021, Tim Flynn <trflynn89@pm.me> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <LibJS/Forward.h> +#include <LibJS/Runtime/Completion.h> +#include <LibJS/Runtime/NativeFunction.h> + +namespace JS::Intl { + +class DateTimeFormatFunction final : public NativeFunction { + JS_OBJECT(DateTimeFormatFunction, NativeFunction); + +public: + static DateTimeFormatFunction* create(GlobalObject&, DateTimeFormat&); + + explicit DateTimeFormatFunction(DateTimeFormat&, Object& prototype); + virtual ~DateTimeFormatFunction() override = default; + virtual void initialize(GlobalObject&) override; + + virtual ThrowCompletionOr<Value> call() override; + +private: + virtual void visit_edges(Visitor&) override; + + DateTimeFormat& m_date_time_format; // [[DateTimeFormat]] +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.cpp index 48dfd1b775..6e76e9db86 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.cpp @@ -5,6 +5,7 @@ */ #include <LibJS/Runtime/GlobalObject.h> +#include <LibJS/Runtime/Intl/DateTimeFormatFunction.h> #include <LibJS/Runtime/Intl/DateTimeFormatPrototype.h> #include <LibUnicode/DateTimeFormat.h> @@ -25,10 +26,35 @@ void DateTimeFormatPrototype::initialize(GlobalObject& global_object) // 11.4.2 Intl.DateTimeFormat.prototype [ @@toStringTag ], https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype-@@tostringtag define_direct_property(*vm.well_known_symbol_to_string_tag(), js_string(vm, "Intl.DateTimeFormat"), Attribute::Configurable); + define_native_accessor(vm.names.format, format, nullptr, Attribute::Configurable); + u8 attr = Attribute::Writable | Attribute::Configurable; define_native_function(vm.names.resolvedOptions, resolved_options, 0, attr); } +// 11.4.3 get Intl.DateTimeFormat.prototype.format, https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.format +JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatPrototype::format) +{ + // 1. Let dtf be the this value. + // 2. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then + // a. Set dtf to ? UnwrapDateTimeFormat(dtf). + // 3. Perform ? RequireInternalSlot(dtf, [[InitializedDateTimeFormat]]). + auto* date_time_format = TRY(typed_this_object(global_object)); + + // 4. If dtf.[[BoundFormat]] is undefined, then + if (!date_time_format->bound_format()) { + // a. Let F be a new built-in function object as defined in DateTime Format Functions (11.1.6). + // b. Set F.[[DateTimeFormat]] to dtf. + auto* bound_format = DateTimeFormatFunction::create(global_object, *date_time_format); + + // c. Set dtf.[[BoundFormat]] to F. + date_time_format->set_bound_format(bound_format); + } + + // 5. Return dtf.[[BoundFormat]]. + return date_time_format->bound_format(); +} + // 11.4.7 Intl.DateTimeFormat.prototype.resolvedOptions ( ), https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.resolvedoptions JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatPrototype::resolved_options) { diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.h b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.h index c141d700e7..1da64348c5 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.h @@ -20,6 +20,7 @@ public: virtual ~DateTimeFormatPrototype() override = default; private: + JS_DECLARE_NATIVE_FUNCTION(format); JS_DECLARE_NATIVE_FUNCTION(resolved_options); }; diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.format.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.format.js new file mode 100644 index 0000000000..b15db71704 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.format.js @@ -0,0 +1,387 @@ +describe("errors", () => { + test("called on non-DateTimeFormat object", () => { + expect(() => { + Intl.DateTimeFormat.prototype.format; + }).toThrowWithMessage(TypeError, "Not an object of type Intl.DateTimeFormat"); + + expect(() => { + Intl.DateTimeFormat.prototype.format(1); + }).toThrowWithMessage(TypeError, "Not an object of type Intl.DateTimeFormat"); + }); + + test("called with value that cannot be converted to a number", () => { + expect(() => { + Intl.DateTimeFormat().format(Symbol.hasInstance); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to number"); + + expect(() => { + Intl.DateTimeFormat().format(1n); + }).toThrowWithMessage(TypeError, "Cannot convert BigInt to number"); + }); + + test("time value cannot be clipped", () => { + expect(() => { + Intl.DateTimeFormat().format(NaN); + }).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15"); + + expect(() => { + Intl.DateTimeFormat().format(-8.65e15); + }).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15"); + + expect(() => { + Intl.DateTimeFormat().format(8.65e15); + }).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15"); + }); +}); + +const d0 = Date.UTC(2021, 11, 7, 17, 40, 50, 456); +const d1 = Date.UTC(1989, 0, 23, 7, 8, 9, 45); + +describe("dateStyle", () => { + // prettier-ignore + const data = [ + { date: "full", en0: "Tuesday, December 7, 2021", en1: "Monday, January 23, 1989", ar0: "الثلاثاء، ٧ ديسمبر ٢٠٢١", ar1: "الاثنين، ٢٣ يناير ١٩٨٩" }, + { date: "long", en0: "December 7, 2021", en1: "January 23, 1989", ar0: "٧ ديسمبر ٢٠٢١", ar1: "٢٣ يناير ١٩٨٩" }, + { date: "medium", en0: "Dec 7, 2021", en1: "Jan 23, 1989", ar0: "٠٧/١٢/٢٠٢١", ar1: "٢٣/٠١/١٩٨٩" }, + { date: "short", en0: "12/7/21", en1: "1/23/89", ar0: "٧/١٢/٢٠٢١", ar1: "٢٣/١/١٩٨٩" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { dateStyle: d.date }); + expect(en.format(d0)).toBe(d.en0); + expect(en.format(d1)).toBe(d.en1); + + const ar = new Intl.DateTimeFormat("ar", { dateStyle: d.date }); + expect(ar.format(d0)).toBe(d.ar0); + expect(ar.format(d1)).toBe(d.ar1); + }); + }); +}); + +describe("timeStyle", () => { + // prettier-ignore + const data = [ + { time: "full", en0: "5:40:50 PM Coordinated Universal Time", en1: "7:08:09 AM Coordinated Universal Time", ar0: "٥:٤٠:٥٠ م التوقيت العالمي المنسق", ar1: "٧:٠٨:٠٩ ص التوقيت العالمي المنسق" }, + { time: "long", en0: "5:40:50 PM UTC", en1: "7:08:09 AM UTC", ar0: "٥:٤٠:٥٠ م UTC", ar1: "٧:٠٨:٠٩ ص UTC" }, + { time: "medium", en0: "5:40:50 PM", en1: "7:08:09 AM", ar0: "٥:٤٠:٥٠ م", ar1: "٧:٠٨:٠٩ ص" }, + { time: "short", en0: "5:40 PM", en1: "7:08 AM", ar0: "٥:٤٠ م", ar1: "٧:٠٨ ص" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { timeStyle: d.time, timeZone: "UTC" }); + expect(en.format(d0)).toBe(d.en0); + expect(en.format(d1)).toBe(d.en1); + + const ar = new Intl.DateTimeFormat("ar", { timeStyle: d.time, timeZone: "UTC" }); + expect(ar.format(d0)).toBe(d.ar0); + expect(ar.format(d1)).toBe(d.ar1); + }); + }); +}); + +describe("dateStyle + timeStyle", () => { + // prettier-ignore + const data = [ + { date: "full", time: "full", en: "Tuesday, December 7, 2021 at 5:40:50 PM Coordinated Universal Time", ar: "الثلاثاء، ٧ ديسمبر ٢٠٢١ في ٥:٤٠:٥٠ م التوقيت العالمي المنسق" }, + { date: "full", time: "long", en: "Tuesday, December 7, 2021 at 5:40:50 PM UTC", ar: "الثلاثاء، ٧ ديسمبر ٢٠٢١ في ٥:٤٠:٥٠ م UTC" }, + { date: "full", time: "medium", en: "Tuesday, December 7, 2021 at 5:40:50 PM", ar: "الثلاثاء، ٧ ديسمبر ٢٠٢١ في ٥:٤٠:٥٠ م" }, + { date: "full", time: "short", en: "Tuesday, December 7, 2021 at 5:40 PM", ar: "الثلاثاء، ٧ ديسمبر ٢٠٢١ في ٥:٤٠ م" }, + { date: "long", time: "full", en: "December 7, 2021 at 5:40:50 PM Coordinated Universal Time", ar: "٧ ديسمبر ٢٠٢١ في ٥:٤٠:٥٠ م التوقيت العالمي المنسق" }, + { date: "long", time: "long", en: "December 7, 2021 at 5:40:50 PM UTC", ar: "٧ ديسمبر ٢٠٢١ في ٥:٤٠:٥٠ م UTC" }, + { date: "long", time: "medium", en: "December 7, 2021 at 5:40:50 PM", ar: "٧ ديسمبر ٢٠٢١ في ٥:٤٠:٥٠ م" }, + { date: "long", time: "short", en: "December 7, 2021 at 5:40 PM", ar: "٧ ديسمبر ٢٠٢١ في ٥:٤٠ م" }, + { date: "medium", time: "full", en: "Dec 7, 2021, 5:40:50 PM Coordinated Universal Time", ar: "٠٧/١٢/٢٠٢١, ٥:٤٠:٥٠ م التوقيت العالمي المنسق" }, + { date: "medium", time: "long", en: "Dec 7, 2021, 5:40:50 PM UTC", ar: "٠٧/١٢/٢٠٢١, ٥:٤٠:٥٠ م UTC" }, + { date: "medium", time: "medium", en: "Dec 7, 2021, 5:40:50 PM", ar: "٠٧/١٢/٢٠٢١, ٥:٤٠:٥٠ م" }, + { date: "medium", time: "short", en: "Dec 7, 2021, 5:40 PM", ar: "٠٧/١٢/٢٠٢١, ٥:٤٠ م" }, + { date: "short", time: "full", en: "12/7/21, 5:40:50 PM Coordinated Universal Time", ar: "٧/١٢/٢٠٢١, ٥:٤٠:٥٠ م التوقيت العالمي المنسق" }, + { date: "short", time: "long", en: "12/7/21, 5:40:50 PM UTC", ar: "٧/١٢/٢٠٢١, ٥:٤٠:٥٠ م UTC" }, + { date: "short", time: "medium", en: "12/7/21, 5:40:50 PM", ar: "٧/١٢/٢٠٢١, ٥:٤٠:٥٠ م" }, + { date: "short", time: "short", en: "12/7/21, 5:40 PM", ar: "٧/١٢/٢٠٢١, ٥:٤٠ م" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { + dateStyle: d.date, + timeStyle: d.time, + timeZone: "UTC", + }); + expect(en.format(d0)).toBe(d.en); + + const ar = new Intl.DateTimeFormat("ar", { + dateStyle: d.date, + timeStyle: d.time, + timeZone: "UTC", + }); + expect(ar.format(d0)).toBe(d.ar); + }); + }); +}); + +describe("weekday", () => { + // prettier-ignore + const data = [ + { weekday: "narrow", en0: "T", en1: "M", ar0: "ث", ar1: "ن" }, + { weekday: "short", en0: "Tue", en1: "Mon", ar0: "الثلاثاء", ar1: "الاثنين" }, + { weekday: "long", en0: "Tuesday", en1: "Monday", ar0: "الثلاثاء", ar1: "الاثنين" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { weekday: d.weekday }); + expect(en.format(d0)).toBe(d.en0); + expect(en.format(d1)).toBe(d.en1); + + const ar = new Intl.DateTimeFormat("ar", { weekday: d.weekday }); + expect(ar.format(d0)).toBe(d.ar0); + expect(ar.format(d1)).toBe(d.ar1); + }); + }); +}); + +describe("era", () => { + // prettier-ignore + const data = [ + { era: "narrow", en0: "12/7/2021 A", en1: "1/23/1989 A", ar0: "٧ ١٢ ٢٠٢١ م", ar1: "٢٣ ١ ١٩٨٩ م" }, + { era: "short", en0: "12/7/2021 AD", en1: "1/23/1989 AD", ar0: "٧ ١٢ ٢٠٢١ م", ar1: "٢٣ ١ ١٩٨٩ م" }, + { era: "long", en0: "12/7/2021 Anno Domini", en1: "1/23/1989 Anno Domini", ar0: "٧ ١٢ ٢٠٢١ ميلادي", ar1: "٢٣ ١ ١٩٨٩ ميلادي" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { era: d.era }); + expect(en.format(d0)).toBe(d.en0); + expect(en.format(d1)).toBe(d.en1); + + const ar = new Intl.DateTimeFormat("ar", { era: d.era }); + expect(ar.format(d0)).toBe(d.ar0); + expect(ar.format(d1)).toBe(d.ar1); + }); + }); +}); + +describe("year", () => { + // prettier-ignore + const data = [ + { year: "2-digit", en0: "21", en1: "89", ar0: "٢١", ar1: "٨٩" }, + { year: "numeric", en0: "2021", en1: "1989", ar0: "٢٠٢١", ar1: "١٩٨٩" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { year: d.year }); + expect(en.format(d0)).toBe(d.en0); + expect(en.format(d1)).toBe(d.en1); + + const ar = new Intl.DateTimeFormat("ar", { year: d.year }); + expect(ar.format(d0)).toBe(d.ar0); + expect(ar.format(d1)).toBe(d.ar1); + }); + }); +}); + +describe("month", () => { + // prettier-ignore + const data = [ + { month: "2-digit", en0: "12", en1: "01", ar0: "١٢", ar1: "٠١" }, + { month: "numeric", en0: "12", en1: "1", ar0: "١٢", ar1: "١" }, + { month: "narrow", en0: "D", en1: "J", ar0: "د", ar1: "ي" }, + { month: "short", en0: "Dec", en1: "Jan", ar0: "ديسمبر", ar1: "يناير" }, + { month: "long", en0: "December", en1: "January", ar0: "ديسمبر", ar1: "يناير" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { month: d.month }); + expect(en.format(d0)).toBe(d.en0); + expect(en.format(d1)).toBe(d.en1); + + const ar = new Intl.DateTimeFormat("ar", { month: d.month }); + expect(ar.format(d0)).toBe(d.ar0); + expect(ar.format(d1)).toBe(d.ar1); + }); + }); +}); + +describe("day", () => { + // prettier-ignore + const data = [ + { day: "2-digit", en0: "07", en1: "23", ar0: "٠٧", ar1: "٢٣" }, + { day: "numeric", en0: "7", en1: "23", ar0: "٧", ar1: "٢٣" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { day: d.day }); + expect(en.format(d0)).toBe(d.en0); + expect(en.format(d1)).toBe(d.en1); + + const ar = new Intl.DateTimeFormat("ar", { day: d.day }); + expect(ar.format(d0)).toBe(d.ar0); + expect(ar.format(d1)).toBe(d.ar1); + }); + }); +}); + +describe("dayPeriod", () => { + // prettier-ignore + // FIXME: The ar formats aren't entirely correct. LibUnicode is only parsing e.g. "morning1" in the "dayPeriods" + // CLDR object. It will need to parse "morning2", and figure out how to apply it. + const data = [ + { dayPeriod: "narrow", en0: "5 in the afternoon", en1: "7 in the morning", ar0: "٥ ظهرًا", ar1: "٧ فجرًا" }, + { dayPeriod: "short", en0: "5 in the afternoon", en1: "7 in the morning", ar0: "٥ ظهرًا", ar1: "٧ فجرًا" }, + { dayPeriod: "long", en0: "5 in the afternoon", en1: "7 in the morning", ar0: "٥ ظهرًا", ar1: "٧ في الصباح" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { + dayPeriod: d.dayPeriod, + hour: "numeric", + timeZone: "UTC", + }); + expect(en.format(d0)).toBe(d.en0); + expect(en.format(d1)).toBe(d.en1); + + const ar = new Intl.DateTimeFormat("ar", { + dayPeriod: d.dayPeriod, + hour: "numeric", + timeZone: "UTC", + }); + expect(ar.format(d0)).toBe(d.ar0); + expect(ar.format(d1)).toBe(d.ar1); + }); + }); +}); + +describe("hour", () => { + // prettier-ignore + // FIXME: The 2-digit results are supposed to include {ampm}. These results are acheived from the "HH" + // pattern, which should only be applied to 24-hour cycles. + const data = [ + { hour: "2-digit", en0: "05", en1: "07", ar0: "٠٥", ar1: "٠٧" }, + { hour: "numeric", en0: "5 PM", en1: "7 AM", ar0: "٥ م", ar1: "٧ ص" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { hour: d.hour, timeZone: "UTC" }); + expect(en.format(d0)).toBe(d.en0); + expect(en.format(d1)).toBe(d.en1); + + const ar = new Intl.DateTimeFormat("ar", { hour: d.hour, timeZone: "UTC" }); + expect(ar.format(d0)).toBe(d.ar0); + expect(ar.format(d1)).toBe(d.ar1); + }); + }); +}); + +describe("minute", () => { + // prettier-ignore + const data = [ + { minute: "2-digit", en0: "5:40 PM", en1: "7:08 AM", ar0: "٥:٤٠ م", ar1: "٧:٠٨ ص" }, + { minute: "numeric", en0: "5:40 PM", en1: "7:08 AM", ar0: "٥:٤٠ م", ar1: "٧:٠٨ ص" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { + minute: d.minute, + hour: "numeric", + timeZone: "UTC", + }); + expect(en.format(d0)).toBe(d.en0); + expect(en.format(d1)).toBe(d.en1); + + const ar = new Intl.DateTimeFormat("ar", { + minute: d.minute, + hour: "numeric", + timeZone: "UTC", + }); + expect(ar.format(d0)).toBe(d.ar0); + expect(ar.format(d1)).toBe(d.ar1); + }); + }); +}); + +describe("second", () => { + // prettier-ignore + const data = [ + { second: "2-digit", en0: "40:50", en1: "08:09", ar0: "٤٠:٥٠", ar1: "٠٨:٠٩" }, + { second: "numeric", en0: "40:50", en1: "08:09", ar0: "٤٠:٥٠", ar1: "٠٨:٠٩" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { + second: d.second, + minute: "numeric", + timeZone: "UTC", + }); + expect(en.format(d0)).toBe(d.en0); + expect(en.format(d1)).toBe(d.en1); + + const ar = new Intl.DateTimeFormat("ar", { + second: d.second, + minute: "numeric", + timeZone: "UTC", + }); + expect(ar.format(d0)).toBe(d.ar0); + expect(ar.format(d1)).toBe(d.ar1); + }); + }); +}); + +describe("fractionalSecondDigits", () => { + // prettier-ignore + const data = [ + { fractionalSecondDigits: 1, en0: "40:50.4", en1: "08:09.0", ar0: "٤٠:٥٠٫٤", ar1: "٠٨:٠٩٫٠" }, + { fractionalSecondDigits: 2, en0: "40:50.45", en1: "08:09.04", ar0: "٤٠:٥٠٫٤٥", ar1: "٠٨:٠٩٫٠٤" }, + { fractionalSecondDigits: 3, en0: "40:50.456", en1: "08:09.045", ar0: "٤٠:٥٠٫٤٥٦", ar1: "٠٨:٠٩٫٠٤٥" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { + fractionalSecondDigits: d.fractionalSecondDigits, + second: "numeric", + minute: "numeric", + timeZone: "UTC", + }); + expect(en.format(d0)).toBe(d.en0); + expect(en.format(d1)).toBe(d.en1); + + const ar = new Intl.DateTimeFormat("ar", { + fractionalSecondDigits: d.fractionalSecondDigits, + second: "numeric", + minute: "numeric", + timeZone: "UTC", + }); + expect(ar.format(d0)).toBe(d.ar0); + expect(ar.format(d1)).toBe(d.ar1); + }); + }); +}); + +describe("timeZoneName", () => { + // prettier-ignore + const data = [ + { timeZoneName: "short", en0: "12/7/2021, 5:40 PM UTC", en1: "1/23/1989, 7:08 AM UTC", ar0: "٧/١٢/٢٠٢١, ٥:٤٠ م UTC", ar1: "٢٣/١/١٩٨٩, ٧:٠٨ ص UTC" }, + { timeZoneName: "long", en0: "12/7/2021, 5:40 PM Coordinated Universal Time", en1: "1/23/1989, 7:08 AM Coordinated Universal Time", ar0: "٧/١٢/٢٠٢١, ٥:٤٠ م التوقيت العالمي المنسق", ar1: "٢٣/١/١٩٨٩, ٧:٠٨ ص التوقيت العالمي المنسق" }, + ]; + + test("all", () => { + data.forEach(d => { + const en = new Intl.DateTimeFormat("en", { timeZoneName: d.timeZoneName }); + expect(en.format(d0)).toBe(d.en0); + expect(en.format(d1)).toBe(d.en1); + + const ar = new Intl.DateTimeFormat("ar", { timeZoneName: d.timeZoneName }); + expect(ar.format(d0)).toBe(d.ar0); + expect(ar.format(d1)).toBe(d.ar1); + }); + }); +}); |