mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-09-30 00:31:14 +00:00
LibJS: Implement Intl.DateTimeFormat.prototype.formatRange
This commit is contained in:
parent
1f35eda37b
commit
04f8fb07e1
Notes:
sideshowbarker
2024-07-17 23:01:45 +09:00
Author: https://github.com/trflynn89 Commit: https://github.com/SerenityOS/serenity/commit/04f8fb07e1a Pull-request: https://github.com/SerenityOS/serenity/pull/11199 Reviewed-by: https://github.com/linusg ✅
|
@ -179,6 +179,7 @@ namespace JS {
|
|||
P(forEach) \
|
||||
P(format) \
|
||||
P(formatMatcher) \
|
||||
P(formatRange) \
|
||||
P(formatToParts) \
|
||||
P(fractionalSecondDigits) \
|
||||
P(freeze) \
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
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(IntlStartTimeAfterEndTime, "Start time {} is after end time {}") \
|
||||
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 {}") \
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/IterationDecision.h>
|
||||
#include <AK/NumericLimits.h>
|
||||
#include <LibJS/Runtime/AbstractOperations.h>
|
||||
#include <LibJS/Runtime/Array.h>
|
||||
|
@ -716,10 +717,12 @@ struct StyleAndValue {
|
|||
i32 value { 0 };
|
||||
};
|
||||
|
||||
static Optional<StyleAndValue> find_calendar_field(StringView name, DateTimeFormat const& date_time_format, LocalTime const& local_time)
|
||||
static Optional<StyleAndValue> find_calendar_field(StringView name, Unicode::CalendarPattern const& options, Unicode::CalendarPattern const* range_options, LocalTime const& local_time)
|
||||
{
|
||||
auto make_style_and_value = [](auto name, auto style, auto value) {
|
||||
return StyleAndValue { name, style, static_cast<i32>(value) };
|
||||
auto make_style_and_value = [](auto name, auto style, auto fallback_style, auto value) {
|
||||
if (style.has_value())
|
||||
return StyleAndValue { name, *style, static_cast<i32>(value) };
|
||||
return StyleAndValue { name, fallback_style, static_cast<i32>(value) };
|
||||
};
|
||||
|
||||
constexpr auto weekday = "weekday"sv;
|
||||
|
@ -731,27 +734,42 @@ static Optional<StyleAndValue> find_calendar_field(StringView name, DateTimeForm
|
|||
constexpr auto minute = "minute"sv;
|
||||
constexpr auto second = "second"sv;
|
||||
|
||||
Optional<Unicode::CalendarPatternStyle> empty;
|
||||
|
||||
if (name == weekday)
|
||||
return make_style_and_value(weekday, date_time_format.weekday(), local_time.weekday);
|
||||
return make_style_and_value(weekday, range_options ? range_options->weekday : empty, *options.weekday, local_time.weekday);
|
||||
if (name == era)
|
||||
return make_style_and_value(era, date_time_format.era(), local_time.era);
|
||||
return make_style_and_value(era, range_options ? range_options->era : empty, *options.era, local_time.era);
|
||||
if (name == year)
|
||||
return make_style_and_value(year, date_time_format.year(), local_time.year);
|
||||
return make_style_and_value(year, range_options ? range_options->year : empty, *options.year, local_time.year);
|
||||
if (name == month)
|
||||
return make_style_and_value(month, date_time_format.month(), local_time.month);
|
||||
return make_style_and_value(month, range_options ? range_options->month : empty, *options.month, local_time.month);
|
||||
if (name == day)
|
||||
return make_style_and_value(day, date_time_format.day(), local_time.day);
|
||||
return make_style_and_value(day, range_options ? range_options->day : empty, *options.day, local_time.day);
|
||||
if (name == hour)
|
||||
return make_style_and_value(hour, date_time_format.hour(), local_time.hour);
|
||||
return make_style_and_value(hour, range_options ? range_options->hour : empty, *options.hour, local_time.hour);
|
||||
if (name == minute)
|
||||
return make_style_and_value(minute, date_time_format.minute(), local_time.minute);
|
||||
return make_style_and_value(minute, range_options ? range_options->minute : empty, *options.minute, local_time.minute);
|
||||
if (name == second)
|
||||
return make_style_and_value(second, date_time_format.second(), local_time.second);
|
||||
return make_style_and_value(second, range_options ? range_options->second : empty, *options.second, local_time.second);
|
||||
return {};
|
||||
}
|
||||
|
||||
static Optional<StringView> day_period_for_hour(StringView locale, StringView calendar, Unicode::CalendarPatternStyle style, u8 hour)
|
||||
{
|
||||
// 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 ((hour >= 6) && (hour < 12))
|
||||
return Unicode::get_calendar_day_period_symbol(locale, calendar, style, Unicode::DayPeriod::Morning);
|
||||
if ((hour >= 12) && (hour < 18))
|
||||
return Unicode::get_calendar_day_period_symbol(locale, calendar, style, Unicode::DayPeriod::Afternoon);
|
||||
if ((hour >= 18) && (hour < 21))
|
||||
return Unicode::get_calendar_day_period_symbol(locale, calendar, style, Unicode::DayPeriod::Evening);
|
||||
return Unicode::get_calendar_day_period_symbol(locale, calendar, style, Unicode::DayPeriod::Night);
|
||||
}
|
||||
|
||||
// 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)
|
||||
ThrowCompletionOr<Vector<PatternPartition>> format_date_time_pattern(GlobalObject& global_object, DateTimeFormat& date_time_format, Vector<PatternPartition> pattern_parts, Value time, Unicode::CalendarPattern const* range_format_options)
|
||||
{
|
||||
auto& vm = global_object.vm();
|
||||
|
||||
|
@ -851,24 +869,13 @@ ThrowCompletionOr<Vector<PatternPartition>> format_date_time_pattern(GlobalObjec
|
|||
|
||||
// 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);
|
||||
|
||||
auto symbol = day_period_for_hour(data_locale, date_time_format.calendar(), style, local_time.hour);
|
||||
if (symbol.has_value())
|
||||
formatted_value = *symbol;
|
||||
|
||||
|
@ -893,12 +900,12 @@ ThrowCompletionOr<Vector<PatternPartition>> format_date_time_pattern(GlobalObjec
|
|||
}
|
||||
|
||||
// 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()) {
|
||||
else if (auto style_and_value = find_calendar_field(part, date_time_format, range_format_options, 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.
|
||||
// NOTE: find_calendar_field handles resolving rangeFormatOptions and dateTimeFormat fields.
|
||||
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.
|
||||
|
@ -1052,7 +1059,7 @@ ThrowCompletionOr<Vector<PatternPartition>> partition_date_time_pattern(GlobalOb
|
|||
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()));
|
||||
auto result = TRY(format_date_time_pattern(global_object, date_time_format, move(pattern_parts), time, nullptr));
|
||||
|
||||
// 3. Return result.
|
||||
return result;
|
||||
|
@ -1113,6 +1120,290 @@ ThrowCompletionOr<Array*> format_date_time_to_parts(GlobalObject& global_object,
|
|||
return result;
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
void for_each_range_pattern_field(LocalTime const& time1, LocalTime const& time2, Callback&& callback)
|
||||
{
|
||||
// Table 6: Range pattern fields, https://tc39.es/ecma402/#table-datetimeformat-rangepatternfields
|
||||
if (callback(static_cast<u8>(time1.era), static_cast<u8>(time2.era), Unicode::CalendarRangePattern::Field::Era) == IterationDecision::Break)
|
||||
return;
|
||||
if (callback(time1.year, time2.year, Unicode::CalendarRangePattern::Field::Year) == IterationDecision::Break)
|
||||
return;
|
||||
if (callback(time1.month, time2.month, Unicode::CalendarRangePattern::Field::Month) == IterationDecision::Break)
|
||||
return;
|
||||
if (callback(time1.day, time2.day, Unicode::CalendarRangePattern::Field::Day) == IterationDecision::Break)
|
||||
return;
|
||||
if (callback(time1.hour, time2.hour, Unicode::CalendarRangePattern::Field::AmPm) == IterationDecision::Break)
|
||||
return;
|
||||
if (callback(time1.hour, time2.hour, Unicode::CalendarRangePattern::Field::DayPeriod) == IterationDecision::Break)
|
||||
return;
|
||||
if (callback(time1.hour, time2.hour, Unicode::CalendarRangePattern::Field::Hour) == IterationDecision::Break)
|
||||
return;
|
||||
if (callback(time1.minute, time2.minute, Unicode::CalendarRangePattern::Field::Minute) == IterationDecision::Break)
|
||||
return;
|
||||
if (callback(time1.second, time2.second, Unicode::CalendarRangePattern::Field::Second) == IterationDecision::Break)
|
||||
return;
|
||||
if (callback(time1.millisecond, time2.millisecond, Unicode::CalendarRangePattern::Field::FractionalSecondDigits) == IterationDecision::Break)
|
||||
return;
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
ThrowCompletionOr<void> for_each_range_pattern_with_source(Unicode::CalendarRangePattern& pattern, Callback&& callback)
|
||||
{
|
||||
TRY(callback(pattern.start_range, "startRange"sv));
|
||||
TRY(callback(pattern.separator, "shared"sv));
|
||||
TRY(callback(pattern.end_range, "endRange"sv));
|
||||
return {};
|
||||
}
|
||||
|
||||
// 11.1.11 PartitionDateTimeRangePattern ( dateTimeFormat, x, y ), https://tc39.es/ecma402/#sec-partitiondatetimerangepattern
|
||||
ThrowCompletionOr<Vector<PatternPartitionWithSource>> partition_date_time_range_pattern(GlobalObject& global_object, DateTimeFormat& date_time_format, Value start, Value end)
|
||||
{
|
||||
auto& vm = global_object.vm();
|
||||
|
||||
// 1. Let x be TimeClip(x).
|
||||
start = time_clip(global_object, start);
|
||||
|
||||
// 2. If x is NaN, throw a RangeError exception.
|
||||
if (start.is_nan())
|
||||
return vm.throw_completion<RangeError>(global_object, ErrorType::IntlInvalidTime);
|
||||
|
||||
// 3. Let y be TimeClip(y).
|
||||
end = time_clip(global_object, end);
|
||||
|
||||
// 4. If y is NaN, throw a RangeError exception.
|
||||
if (end.is_nan())
|
||||
return vm.throw_completion<RangeError>(global_object, ErrorType::IntlInvalidTime);
|
||||
|
||||
// 5. If x is greater than y, throw a RangeError exception.
|
||||
if (start.as_double() > end.as_double())
|
||||
return vm.throw_completion<RangeError>(global_object, ErrorType::IntlStartTimeAfterEndTime, start, end);
|
||||
|
||||
// 6. Let tm1 be ToLocalTime(x, dateTimeFormat.[[Calendar]], dateTimeFormat.[[TimeZone]]).
|
||||
auto start_local_time = TRY(to_local_time(global_object, start.as_double(), date_time_format.calendar(), date_time_format.time_zone()));
|
||||
|
||||
// 7. Let tm2 be ToLocalTime(y, dateTimeFormat.[[Calendar]], dateTimeFormat.[[TimeZone]]).
|
||||
auto end_local_time = TRY(to_local_time(global_object, end.as_double(), date_time_format.calendar(), date_time_format.time_zone()));
|
||||
|
||||
// 8. Let rangePatterns be dateTimeFormat.[[RangePatterns]].
|
||||
auto range_patterns = date_time_format.range_patterns();
|
||||
|
||||
// 9. Let rangePattern be undefined.
|
||||
Optional<Unicode::CalendarRangePattern> range_pattern;
|
||||
|
||||
// 10. Let dateFieldsPracticallyEqual be true.
|
||||
bool date_fields_practically_equal = true;
|
||||
|
||||
// 11. Let patternContainsLargerDateField be false.
|
||||
bool pattern_contains_larger_date_field = false;
|
||||
|
||||
// 12. While dateFieldsPracticallyEqual is true and patternContainsLargerDateField is false, repeat for each row of Table 6 in order, except the header row:
|
||||
for_each_range_pattern_field(start_local_time, end_local_time, [&](auto start_value, auto end_value, auto field_name) {
|
||||
// a. Let fieldName be the name given in the Range Pattern Field column of the row.
|
||||
|
||||
// b. If rangePatterns has a field [[<fieldName>]], let rp be rangePatterns.[[<fieldName>]]; else let rp be undefined.
|
||||
Optional<Unicode::CalendarRangePattern> pattern;
|
||||
for (auto const& range : range_patterns) {
|
||||
if (range.field == field_name) {
|
||||
pattern = range;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// c. If rangePattern is not undefined and rp is undefined, then
|
||||
if (range_pattern.has_value() && !pattern.has_value()) {
|
||||
// i. Set patternContainsLargerDateField to true.
|
||||
pattern_contains_larger_date_field = true;
|
||||
}
|
||||
// d. Else,
|
||||
else {
|
||||
// i. Let rangePattern be rp.
|
||||
range_pattern = pattern;
|
||||
|
||||
switch (field_name) {
|
||||
// ii. If fieldName is equal to [[AmPm]], then
|
||||
case Unicode::CalendarRangePattern::Field::AmPm: {
|
||||
// 1. Let v1 be tm1.[[Hour]].
|
||||
// 2. Let v2 be tm2.[[Hour]].
|
||||
|
||||
// 3. If v1 is greater than 11 and v2 less or equal than 11, or v1 is less or equal than 11 and v2 is greater than 11, then
|
||||
if ((start_value > 11 && end_value <= 11) || (start_value <= 11 && end_value > 11)) {
|
||||
// a. Set dateFieldsPracticallyEqual to false.
|
||||
date_fields_practically_equal = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// iii. Else if fieldName is equal to [[DayPeriod]], then
|
||||
case Unicode::CalendarRangePattern::Field::DayPeriod: {
|
||||
// 1. Let v1 be a String value representing the day period of tm1; the String value depends upon the implementation and the effective locale of dateTimeFormat.
|
||||
auto start_period = day_period_for_hour(date_time_format.data_locale(), date_time_format.calendar(), Unicode::CalendarPatternStyle::Short, start_value);
|
||||
|
||||
// 2. Let v2 be a String value representing the day period of tm2; the String value depends upon the implementation and the effective locale of dateTimeFormat.
|
||||
auto end_period = day_period_for_hour(date_time_format.data_locale(), date_time_format.calendar(), Unicode::CalendarPatternStyle::Short, end_value);
|
||||
|
||||
// 3. If v1 is not equal to v2, then
|
||||
if (start_period != end_period) {
|
||||
// a. Set dateFieldsPracticallyEqual to false.
|
||||
date_fields_practically_equal = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// iv. Else if fieldName is equal to [[FractionalSecondDigits]], then
|
||||
case Unicode::CalendarRangePattern::Field::FractionalSecondDigits: {
|
||||
// 1. Let fractionalSecondDigits be dateTimeFormat.[[FractionalSecondDigits]].
|
||||
Optional<u8> fractional_second_digits;
|
||||
if (date_time_format.has_fractional_second_digits())
|
||||
fractional_second_digits = date_time_format.fractional_second_digits();
|
||||
|
||||
// 2. If fractionalSecondDigits is undefined, then
|
||||
if (!fractional_second_digits.has_value()) {
|
||||
// a. Set fractionalSecondDigits to 3.
|
||||
fractional_second_digits = 3;
|
||||
}
|
||||
|
||||
// 3. Let v1 be tm1.[[Millisecond]].
|
||||
// 4. Let v2 be tm2.[[Millisecond]].
|
||||
|
||||
// 5. Let v1 be floor(v1 × 10( fractionalSecondDigits - 3 )).
|
||||
start_value = floor(start_value * pow(10, static_cast<int>(*fractional_second_digits) - 3));
|
||||
|
||||
// 6. Let v2 be floor(v2 × 10( fractionalSecondDigits - 3 )).
|
||||
end_value = floor(end_value * pow(10, static_cast<int>(*fractional_second_digits) - 3));
|
||||
|
||||
// 7. If v1 is not equal to v2, then
|
||||
if (start_value != end_value) {
|
||||
// a. Set dateFieldsPracticallyEqual to false.
|
||||
date_fields_practically_equal = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// v. Else,
|
||||
default: {
|
||||
// 1. Let v1 be tm1.[[<fieldName>]].
|
||||
// 2. Let v2 be tm2.[[<fieldName>]].
|
||||
|
||||
// 3. If v1 is not equal to v2, then
|
||||
if (start_value != end_value) {
|
||||
// a. Set dateFieldsPracticallyEqual to false.
|
||||
date_fields_practically_equal = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (date_fields_practically_equal && !pattern_contains_larger_date_field)
|
||||
return IterationDecision::Continue;
|
||||
return IterationDecision::Break;
|
||||
});
|
||||
|
||||
// 13. If dateFieldsPracticallyEqual is true, then
|
||||
if (date_fields_practically_equal) {
|
||||
// a. Let pattern be dateTimeFormat.[[Pattern]].
|
||||
auto const& pattern = date_time_format.pattern();
|
||||
|
||||
// b. Let patternParts be PartitionPattern(pattern).
|
||||
auto pattern_parts = partition_pattern(pattern);
|
||||
|
||||
// c. Let result be ? FormatDateTimePattern(dateTimeFormat, patternParts, x, undefined).
|
||||
auto raw_result = TRY(format_date_time_pattern(global_object, date_time_format, move(pattern_parts), start, nullptr));
|
||||
auto result = PatternPartitionWithSource::create_from_parent_list(move(raw_result));
|
||||
|
||||
// d. For each Record { [[Type]], [[Value]] } r in result, do
|
||||
for (auto& part : result) {
|
||||
// i. Set r.[[Source]] to "shared".
|
||||
part.source = "shared"sv;
|
||||
}
|
||||
|
||||
// e. Return result.
|
||||
return result;
|
||||
}
|
||||
|
||||
// 14. Let result be a new empty List.
|
||||
Vector<PatternPartitionWithSource> result;
|
||||
|
||||
// 15. If rangePattern is undefined, then
|
||||
if (!range_pattern.has_value()) {
|
||||
// a. Let rangePattern be rangePatterns.[[Default]].
|
||||
range_pattern = Unicode::get_calendar_default_range_format(date_time_format.data_locale(), date_time_format.calendar());
|
||||
|
||||
// Non-standard, range_pattern will be empty if Unicode data generation is disabled.
|
||||
if (!range_pattern.has_value())
|
||||
return result;
|
||||
|
||||
// Non-standard, LibUnicode leaves the CLDR's {0} and {1} partitions in the default patterns
|
||||
// to be replaced at runtime with the DateTimeFormat object's pattern.
|
||||
auto const& pattern = date_time_format.pattern();
|
||||
|
||||
if (range_pattern->start_range.contains("{0}"sv)) {
|
||||
range_pattern->start_range = range_pattern->start_range.replace("{0}"sv, pattern);
|
||||
range_pattern->end_range = range_pattern->end_range.replace("{1}"sv, pattern);
|
||||
} else {
|
||||
range_pattern->start_range = range_pattern->start_range.replace("{1}"sv, pattern);
|
||||
range_pattern->end_range = range_pattern->end_range.replace("{0}"sv, pattern);
|
||||
}
|
||||
|
||||
// FIXME: The above is not sufficient. For example, if the start date is days before the end date, and only the timeStyle
|
||||
// option is provided, the resulting range will not include the differing dates. We will likely need to implement
|
||||
// step 3 here: https://unicode.org/reports/tr35/tr35-dates.html#intervalFormats
|
||||
}
|
||||
|
||||
// 16. For each Record { [[Pattern]], [[Source]] } rangePatternPart in rangePattern.[[PatternParts]], do
|
||||
TRY(for_each_range_pattern_with_source(*range_pattern, [&](auto const& pattern, auto source) -> ThrowCompletionOr<void> {
|
||||
// a. Let pattern be rangePatternPart.[[Pattern]].
|
||||
// b. Let source be rangePatternPart.[[Source]].
|
||||
|
||||
// c. If source is "startRange" or "shared", then
|
||||
// i. Let z be x.
|
||||
// d. Else,
|
||||
// i. Let z be y.
|
||||
auto time = ((source == "startRange") || (source == "shared")) ? start : end;
|
||||
|
||||
// e. Let patternParts be PartitionPattern(pattern).
|
||||
auto pattern_parts = partition_pattern(pattern);
|
||||
|
||||
// f. Let partResult be ? FormatDateTimePattern(dateTimeFormat, patternParts, z, rangePattern).
|
||||
auto raw_part_result = TRY(format_date_time_pattern(global_object, date_time_format, move(pattern_parts), time, &range_pattern.value()));
|
||||
auto part_result = PatternPartitionWithSource::create_from_parent_list(move(raw_part_result));
|
||||
|
||||
// g. For each Record { [[Type]], [[Value]] } r in partResult, do
|
||||
for (auto& part : part_result) {
|
||||
// i. Set r.[[Source]] to source.
|
||||
part.source = source;
|
||||
}
|
||||
|
||||
// h. Add all elements in partResult to result in order.
|
||||
result.extend(move(part_result));
|
||||
return {};
|
||||
}));
|
||||
|
||||
// 17. Return result.
|
||||
return result;
|
||||
}
|
||||
|
||||
// 11.1.12 FormatDateTimeRange ( dateTimeFormat, x, y ), https://tc39.es/ecma402/#sec-formatdatetimerange
|
||||
ThrowCompletionOr<String> format_date_time_range(GlobalObject& global_object, DateTimeFormat& date_time_format, Value start, Value end)
|
||||
{
|
||||
// 1. Let parts be ? PartitionDateTimeRangePattern(dateTimeFormat, x, y).
|
||||
auto parts = TRY(partition_date_time_range_pattern(global_object, date_time_format, start, end));
|
||||
|
||||
// 2. Let result be the empty String.
|
||||
StringBuilder result;
|
||||
|
||||
// 3. For each Record { [[Type]], [[Value]], [[Source]] } 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)
|
||||
{
|
||||
|
|
|
@ -175,15 +175,36 @@ struct LocalTime {
|
|||
bool in_dst { false }; // [[InDST]]
|
||||
};
|
||||
|
||||
struct PatternPartitionWithSource : public PatternPartition {
|
||||
static Vector<PatternPartitionWithSource> create_from_parent_list(Vector<PatternPartition> partitions)
|
||||
{
|
||||
Vector<PatternPartitionWithSource> result;
|
||||
result.ensure_capacity(partitions.size());
|
||||
|
||||
for (auto& partition : partitions) {
|
||||
PatternPartitionWithSource partition_with_source {};
|
||||
partition_with_source.type = partition.type;
|
||||
partition_with_source.value = move(partition.value);
|
||||
result.append(move(partition_with_source));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
StringView source;
|
||||
};
|
||||
|
||||
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>> format_date_time_pattern(GlobalObject& global_object, DateTimeFormat& date_time_format, Vector<PatternPartition> pattern_parts, Value time, Unicode::CalendarPattern const* 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<Array*> format_date_time_to_parts(GlobalObject& global_object, DateTimeFormat& date_time_format, Value time);
|
||||
ThrowCompletionOr<Vector<PatternPartitionWithSource>> partition_date_time_range_pattern(GlobalObject& global_object, DateTimeFormat& date_time_format, Value start, Value end);
|
||||
ThrowCompletionOr<String> format_date_time_range(GlobalObject& global_object, DateTimeFormat& date_time_format, Value start, Value end);
|
||||
ThrowCompletionOr<LocalTime> to_local_time(GlobalObject& global_object, double time, StringView calendar, StringView time_zone);
|
||||
|
||||
template<typename Callback>
|
||||
|
|
|
@ -32,6 +32,7 @@ void DateTimeFormatPrototype::initialize(GlobalObject& global_object)
|
|||
|
||||
u8 attr = Attribute::Writable | Attribute::Configurable;
|
||||
define_native_function(vm.names.formatToParts, format_to_parts, 1, attr);
|
||||
define_native_function(vm.names.formatRange, format_range, 2, attr);
|
||||
define_native_function(vm.names.resolvedOptions, resolved_options, 0, attr);
|
||||
}
|
||||
|
||||
|
@ -82,6 +83,33 @@ JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatPrototype::format_to_parts)
|
|||
return TRY(format_date_time_to_parts(global_object, *date_time_format, date));
|
||||
}
|
||||
|
||||
// 11.4.5 Intl.DateTimeFormat.prototype.formatRange ( startDate, endDate ), https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.formatRange
|
||||
JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatPrototype::format_range)
|
||||
{
|
||||
auto start_date = vm.argument(0);
|
||||
auto end_date = vm.argument(1);
|
||||
|
||||
// 1. Let dtf be this value.
|
||||
// 2. Perform ? RequireInternalSlot(dtf, [[InitializedDateTimeFormat]]).
|
||||
auto* date_time_format = TRY(typed_this_object(global_object));
|
||||
|
||||
// 3. If startDate is undefined or endDate is undefined, throw a TypeError exception.
|
||||
if (start_date.is_undefined())
|
||||
return vm.throw_completion<TypeError>(global_object, ErrorType::IsUndefined, "startDate"sv);
|
||||
if (end_date.is_undefined())
|
||||
return vm.throw_completion<TypeError>(global_object, ErrorType::IsUndefined, "endDate"sv);
|
||||
|
||||
// 4. Let x be ? ToNumber(startDate).
|
||||
start_date = TRY(start_date.to_number(global_object));
|
||||
|
||||
// 5. Let y be ? ToNumber(endDate).
|
||||
end_date = TRY(end_date.to_number(global_object));
|
||||
|
||||
// 6. Return ? FormatDateTimeRange(dtf, x, y).
|
||||
auto formatted = TRY(format_date_time_range(global_object, *date_time_format, start_date, end_date));
|
||||
return js_string(vm, move(formatted));
|
||||
}
|
||||
|
||||
// 11.4.7 Intl.DateTimeFormat.prototype.resolvedOptions ( ), https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.resolvedoptions
|
||||
JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatPrototype::resolved_options)
|
||||
{
|
||||
|
|
|
@ -22,6 +22,7 @@ public:
|
|||
private:
|
||||
JS_DECLARE_NATIVE_FUNCTION(format);
|
||||
JS_DECLARE_NATIVE_FUNCTION(format_to_parts);
|
||||
JS_DECLARE_NATIVE_FUNCTION(format_range);
|
||||
JS_DECLARE_NATIVE_FUNCTION(resolved_options);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
describe("errors", () => {
|
||||
test("called on non-DateTimeFormat object", () => {
|
||||
expect(() => {
|
||||
Intl.DateTimeFormat.prototype.formatRange(1, 2);
|
||||
}).toThrowWithMessage(TypeError, "Not an object of type Intl.DateTimeFormat");
|
||||
});
|
||||
|
||||
test("called with undefined values", () => {
|
||||
expect(() => {
|
||||
Intl.DateTimeFormat().formatRange();
|
||||
}).toThrowWithMessage(TypeError, "startDate is undefined");
|
||||
|
||||
expect(() => {
|
||||
Intl.DateTimeFormat().formatRange(1);
|
||||
}).toThrowWithMessage(TypeError, "endDate is undefined");
|
||||
});
|
||||
|
||||
test("called with values that cannot be converted to numbers", () => {
|
||||
expect(() => {
|
||||
Intl.DateTimeFormat().formatRange(1, Symbol.hasInstance);
|
||||
}).toThrowWithMessage(TypeError, "Cannot convert symbol to number");
|
||||
|
||||
expect(() => {
|
||||
Intl.DateTimeFormat().formatRange(1n, 1);
|
||||
}).toThrowWithMessage(TypeError, "Cannot convert BigInt to number");
|
||||
});
|
||||
|
||||
test("time value cannot be clipped", () => {
|
||||
[NaN, -8.65e15, 8.65e15].forEach(d => {
|
||||
expect(() => {
|
||||
Intl.DateTimeFormat().formatRange(d, 1);
|
||||
}).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15");
|
||||
|
||||
expect(() => {
|
||||
Intl.DateTimeFormat().formatRange(1, d);
|
||||
}).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15");
|
||||
});
|
||||
});
|
||||
|
||||
test("called with values in bad order", () => {
|
||||
expect(() => {
|
||||
Intl.DateTimeFormat().formatRange(new Date(2021), new Date(1989));
|
||||
}).toThrowWithMessage(RangeError, "Start time 2021 is after end time 1989");
|
||||
});
|
||||
});
|
||||
|
||||
const d0 = Date.UTC(1989, 0, 23, 7, 8, 9, 45);
|
||||
const d1 = Date.UTC(2021, 11, 7, 17, 40, 50, 456);
|
||||
|
||||
describe("equal dates are squashed", () => {
|
||||
test("with date fields", () => {
|
||||
const en = new Intl.DateTimeFormat("en", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "2-digit",
|
||||
});
|
||||
expect(en.formatRange(d0, d0)).toBe("January 23, 1989");
|
||||
expect(en.formatRange(d1, d1)).toBe("December 07, 2021");
|
||||
|
||||
const ja = new Intl.DateTimeFormat("ja", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "2-digit",
|
||||
});
|
||||
expect(ja.formatRange(d0, d0)).toBe("1989/1月/23");
|
||||
expect(ja.formatRange(d1, d1)).toBe("2021/12月/07");
|
||||
});
|
||||
|
||||
test("with time fields", () => {
|
||||
const en = new Intl.DateTimeFormat("en", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
expect(en.formatRange(d0, d0)).toBe("7:08:09 AM");
|
||||
expect(en.formatRange(d1, d1)).toBe("5:40:50 PM");
|
||||
|
||||
const ja = new Intl.DateTimeFormat("ja", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
expect(ja.formatRange(d0, d0)).toBe("7:08:09");
|
||||
expect(ja.formatRange(d1, d1)).toBe("17:40:50");
|
||||
});
|
||||
|
||||
test("with mixed fields", () => {
|
||||
const en = new Intl.DateTimeFormat("en", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "2-digit",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
expect(en.formatRange(d0, d0)).toBe("January 23, 1989 at 7:08:09 AM");
|
||||
expect(en.formatRange(d1, d1)).toBe("December 07, 2021 at 5:40:50 PM");
|
||||
|
||||
const ja = new Intl.DateTimeFormat("ja", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "2-digit",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
expect(ja.formatRange(d0, d0)).toBe("1989/1月/23 7:08:09");
|
||||
expect(ja.formatRange(d1, d1)).toBe("2021/12月/07 17:40:50");
|
||||
});
|
||||
|
||||
test("with date/time style fields", () => {
|
||||
const en = new Intl.DateTimeFormat("en", {
|
||||
dateStyle: "full",
|
||||
timeStyle: "medium",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
expect(en.formatRange(d0, d0)).toBe("Monday, January 23, 1989 at 7:08:09 AM");
|
||||
expect(en.formatRange(d1, d1)).toBe("Tuesday, December 7, 2021 at 5:40:50 PM");
|
||||
|
||||
const ja = new Intl.DateTimeFormat("ja", {
|
||||
dateStyle: "full",
|
||||
timeStyle: "medium",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
expect(ja.formatRange(d0, d0)).toBe("1989年1月23日月曜日 7:08:09");
|
||||
expect(ja.formatRange(d1, d1)).toBe("2021年12月7日火曜日 17:40:50");
|
||||
});
|
||||
});
|
||||
|
||||
describe("dateStyle", () => {
|
||||
// prettier-ignore
|
||||
const data = [
|
||||
{ date: "full", en: "Monday, January 23, 1989 – Tuesday, December 7, 2021", ja: "1989年1月23日月曜日~2021年12月7日火曜日" },
|
||||
{ date: "long", en: "January 23, 1989 – December 7, 2021", ja: "1989/01/23~2021/12/07" },
|
||||
{ date: "medium", en: "Jan 23, 1989 – Dec 7, 2021", ja: "1989/01/23~2021/12/07" },
|
||||
{ date: "short", en: "1/23/89 – 12/7/21", ja: "1989/01/23~2021/12/07" },
|
||||
];
|
||||
|
||||
test("all", () => {
|
||||
data.forEach(d => {
|
||||
const en = new Intl.DateTimeFormat("en", { dateStyle: d.date });
|
||||
expect(en.formatRange(d0, d1)).toBe(d.en);
|
||||
|
||||
// If this test is to be changed, take care to note the "long" style for the ja locale is an intentionally
|
||||
// chosen complex test case. The format pattern is "y年M月d日" and its skeleton is "yMd" - note that the
|
||||
// month field has a numeric style. However, the interval patterns that match the "yMd" skeleton are all
|
||||
// "y/MM/dd~y/MM/dd" - the month field there conflicts with a 2-digit style. This exercises the step in the
|
||||
// FormatDateTimePattern AO to choose the style from rangeFormatOptions instead of dateTimeFormat (step 15.f.i
|
||||
// as of when this test was written).
|
||||
const ja = new Intl.DateTimeFormat("ja", { dateStyle: d.date });
|
||||
expect(ja.formatRange(d0, d1)).toBe(d.ja);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeStyle", () => {
|
||||
// prettier-ignore
|
||||
const data = [
|
||||
// FIXME: These results should include the date, even though it isn't requested, because the start/end dates
|
||||
// are more than just hours apart. See the FIXME in PartitionDateTimeRangePattern.
|
||||
{ time: "full", en: "7:08:09 AM Coordinated Universal Time – 5:40:50 PM Coordinated Universal Time", ja: "7時08分09秒 協定世界時~17時40分50秒 協定世界時" },
|
||||
{ time: "long", en: "7:08:09 AM UTC – 5:40:50 PM UTC", ja: "7:08:09 UTC~17:40:50 UTC" },
|
||||
{ time: "medium", en: "7:08:09 AM – 5:40:50 PM", ja: "7:08:09~17:40:50" },
|
||||
{ time: "short", en: "7:08 AM – 5:40 PM", ja: "7:08~17:40" },
|
||||
];
|
||||
|
||||
test("all", () => {
|
||||
data.forEach(d => {
|
||||
const en = new Intl.DateTimeFormat("en", { timeStyle: d.time, timeZone: "UTC" });
|
||||
expect(en.formatRange(d0, d1)).toBe(d.en);
|
||||
|
||||
const ja = new Intl.DateTimeFormat("ja", { timeStyle: d.time, timeZone: "UTC" });
|
||||
expect(ja.formatRange(d0, d1)).toBe(d.ja);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("dateStyle + timeStyle", () => {
|
||||
// prettier-ignore
|
||||
const data = [
|
||||
{ date: "full", time: "full", en: "Monday, January 23, 1989 at 7:08:09 AM Coordinated Universal Time – Tuesday, December 7, 2021 at 5:40:50 PM Coordinated Universal Time", ja: "1989年1月23日月曜日 7時08分09秒 協定世界時~2021年12月7日火曜日 17時40分50秒 協定世界時" },
|
||||
{ date: "full", time: "long", en: "Monday, January 23, 1989 at 7:08:09 AM UTC – Tuesday, December 7, 2021 at 5:40:50 PM UTC", ja: "1989年1月23日月曜日 7:08:09 UTC~2021年12月7日火曜日 17:40:50 UTC" },
|
||||
{ date: "full", time: "medium", en: "Monday, January 23, 1989 at 7:08:09 AM – Tuesday, December 7, 2021 at 5:40:50 PM", ja: "1989年1月23日月曜日 7:08:09~2021年12月7日火曜日 17:40:50" },
|
||||
{ date: "full", time: "short", en: "Monday, January 23, 1989 at 7:08 AM – Tuesday, December 7, 2021 at 5:40 PM", ja: "1989年1月23日月曜日 7:08~2021年12月7日火曜日 17:40" },
|
||||
{ date: "long", time: "full", en: "January 23, 1989 at 7:08:09 AM Coordinated Universal Time – December 7, 2021 at 5:40:50 PM Coordinated Universal Time", ja: "1989年1月23日 7時08分09秒 協定世界時~2021年12月7日 17時40分50秒 協定世界時" },
|
||||
{ date: "long", time: "long", en: "January 23, 1989 at 7:08:09 AM UTC – December 7, 2021 at 5:40:50 PM UTC", ja: "1989年1月23日 7:08:09 UTC~2021年12月7日 17:40:50 UTC" },
|
||||
{ date: "long", time: "medium", en: "January 23, 1989 at 7:08:09 AM – December 7, 2021 at 5:40:50 PM", ja: "1989年1月23日 7:08:09~2021年12月7日 17:40:50" },
|
||||
{ date: "long", time: "short", en: "January 23, 1989 at 7:08 AM – December 7, 2021 at 5:40 PM", ja: "1989年1月23日 7:08~2021年12月7日 17:40" },
|
||||
{ date: "medium", time: "full", en: "Jan 23, 1989, 7:08:09 AM Coordinated Universal Time – Dec 7, 2021, 5:40:50 PM Coordinated Universal Time", ja: "1989/01/23 7時08分09秒 協定世界時~2021/12/07 17時40分50秒 協定世界時" },
|
||||
{ date: "medium", time: "long", en: "Jan 23, 1989, 7:08:09 AM UTC – Dec 7, 2021, 5:40:50 PM UTC", ja: "1989/01/23 7:08:09 UTC~2021/12/07 17:40:50 UTC" },
|
||||
{ date: "medium", time: "medium", en: "Jan 23, 1989, 7:08:09 AM – Dec 7, 2021, 5:40:50 PM", ja: "1989/01/23 7:08:09~2021/12/07 17:40:50" },
|
||||
{ date: "medium", time: "short", en: "Jan 23, 1989, 7:08 AM – Dec 7, 2021, 5:40 PM", ja: "1989/01/23 7:08~2021/12/07 17:40" },
|
||||
{ date: "short", time: "full", en: "1/23/89, 7:08:09 AM Coordinated Universal Time – 12/7/21, 5:40:50 PM Coordinated Universal Time", ja: "1989/01/23 7時08分09秒 協定世界時~2021/12/07 17時40分50秒 協定世界時" },
|
||||
{ date: "short", time: "long", en: "1/23/89, 7:08:09 AM UTC – 12/7/21, 5:40:50 PM UTC", ja: "1989/01/23 7:08:09 UTC~2021/12/07 17:40:50 UTC" },
|
||||
{ date: "short", time: "medium", en: "1/23/89, 7:08:09 AM – 12/7/21, 5:40:50 PM", ja: "1989/01/23 7:08:09~2021/12/07 17:40:50" },
|
||||
{ date: "short", time: "short", en: "1/23/89, 7:08 AM – 12/7/21, 5:40 PM", ja: "1989/01/23 7:08~2021/12/07 17:40" },
|
||||
];
|
||||
|
||||
test("all", () => {
|
||||
data.forEach(d => {
|
||||
const en = new Intl.DateTimeFormat("en", {
|
||||
dateStyle: d.date,
|
||||
timeStyle: d.time,
|
||||
timeZone: "UTC",
|
||||
});
|
||||
expect(en.formatRange(d0, d1)).toBe(d.en);
|
||||
|
||||
const ja = new Intl.DateTimeFormat("ja", {
|
||||
dateStyle: d.date,
|
||||
timeStyle: d.time,
|
||||
timeZone: "UTC",
|
||||
});
|
||||
expect(ja.formatRange(d0, d1)).toBe(d.ja);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue