LibJS+LibLocale: Replace date-time formatting with ICU

This uses ICU for the Intl.DateTimeFormat `format` `formatToParts`,
`formatRange`, and `formatRangeToParts`.

This lets us remove most data from our date-time format generator. All
that remains are time zone data and locale week info, which are relied
upon still for other interfaces. So they will be removed in a future
patch.

Note: All of the changes to the test files in this patch are now aligned
with other browsers. This includes:

* Some very incorrect formatting of Japanese symbols. (Looking at the
  old results now, it's very obvious they were wrong.)
* Old FIXMEs regarding range formatting not including the start/end date
  when only time fields were requested, but the dates differ.
* Day period inconsistencies.
This commit is contained in:
Timothy Flynn 2024-06-12 10:47:20 -04:00 committed by Andreas Kling
parent 2f5cf8ac20
commit 273694d8de
Notes: sideshowbarker 2024-07-17 04:01:41 +09:00
17 changed files with 1139 additions and 3468 deletions

View File

@ -1,11 +1,8 @@
set(TEST_SOURCES
TestDateTimeFormat.cpp
TestDisplayNames.cpp
TestLocale.cpp
)
foreach(source IN LISTS TEST_SOURCES)
serenity_test("${source}" LibLocale LIBS LibLocale)
get_filename_component(target "${source}" NAME_WLE)
endforeach()

View File

@ -1,188 +0,0 @@
/*
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibTest/TestCase.h>
#include <AK/Array.h>
#include <AK/StringView.h>
#include <AK/Time.h>
#include <LibLocale/DateTimeFormat.h>
TEST_CASE(time_zone_name)
{
struct TestData {
StringView locale;
Locale::CalendarPatternStyle style;
StringView time_zone;
StringView expected_result;
};
constexpr auto test_data = Array {
TestData { "en"sv, Locale::CalendarPatternStyle::Long, "UTC"sv, "Coordinated Universal Time"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Short, "UTC"sv, "UTC"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::LongGeneric, "UTC"sv, "GMT"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::ShortGeneric, "UTC"sv, "GMT"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "UTC"sv, "التوقيت العالمي المنسق"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "UTC"sv, "UTC"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::LongGeneric, "UTC"sv, "غرينتش"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::ShortGeneric, "UTC"sv, "غرينتش"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Long, "America/Los_Angeles"sv, "Pacific Standard Time"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Short, "America/Los_Angeles"sv, "PST"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::LongGeneric, "America/Los_Angeles"sv, "Pacific Time"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::ShortGeneric, "America/Los_Angeles"sv, "PT"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "America/Los_Angeles"sv, "توقيت المحيط الهادي الرسمي"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "America/Los_Angeles"sv, "غرينتش-٨"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::LongGeneric, "America/Los_Angeles"sv, "توقيت المحيط الهادي"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::ShortGeneric, "America/Los_Angeles"sv, "غرينتش-٨"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Long, "America/Vancouver"sv, "Pacific Standard Time"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Short, "America/Vancouver"sv, "PST"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::LongGeneric, "America/Vancouver"sv, "Pacific Time"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::ShortGeneric, "America/Vancouver"sv, "PT"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "America/Vancouver"sv, "توقيت المحيط الهادي الرسمي"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "America/Vancouver"sv, "غرينتش-٨"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::LongGeneric, "America/Vancouver"sv, "توقيت المحيط الهادي"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::ShortGeneric, "America/Vancouver"sv, "غرينتش-٨"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Long, "Europe/London"sv, "Greenwich Mean Time"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Short, "Europe/London"sv, "GMT"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::LongGeneric, "Europe/London"sv, "GMT"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::ShortGeneric, "Europe/London"sv, "GMT"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "Europe/London"sv, "توقيت غرينتش"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "Europe/London"sv, "غرينتش"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::LongGeneric, "Europe/London"sv, "غرينتش"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::ShortGeneric, "Europe/London"sv, "غرينتش"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Long, "Africa/Accra"sv, "Greenwich Mean Time"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Short, "Africa/Accra"sv, "GMT"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::LongGeneric, "Africa/Accra"sv, "GMT"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::ShortGeneric, "Africa/Accra"sv, "GMT"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "Africa/Accra"sv, "توقيت غرينتش"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "Africa/Accra"sv, "غرينتش"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::LongGeneric, "Africa/Accra"sv, "غرينتش"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::ShortGeneric, "Africa/Accra"sv, "غرينتش"sv },
};
constexpr auto jan_1_2022 = AK::UnixDateTime::from_seconds_since_epoch(1640995200); // Saturday, January 1, 2022 12:00:00 AM
for (auto const& test : test_data) {
auto time_zone = Locale::format_time_zone(test.locale, test.time_zone, test.style, jan_1_2022);
EXPECT_EQ(time_zone, test.expected_result);
}
}
TEST_CASE(time_zone_name_dst)
{
struct TestData {
StringView locale;
Locale::CalendarPatternStyle style;
StringView time_zone;
StringView expected_result;
};
constexpr auto test_data = Array {
TestData { "en"sv, Locale::CalendarPatternStyle::Long, "UTC"sv, "Coordinated Universal Time"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Short, "UTC"sv, "UTC"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "UTC"sv, "التوقيت العالمي المنسق"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "UTC"sv, "UTC"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Long, "America/Los_Angeles"sv, "Pacific Daylight Time"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Short, "America/Los_Angeles"sv, "PDT"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "America/Los_Angeles"sv, "توقيت المحيط الهادي الصيفي"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "America/Los_Angeles"sv, "غرينتش-٧"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Long, "America/Vancouver"sv, "Pacific Daylight Time"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Short, "America/Vancouver"sv, "PDT"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "America/Vancouver"sv, "توقيت المحيط الهادي الصيفي"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "America/Vancouver"sv, "غرينتش-٧"sv },
// FIXME: This should be "British Summer Time", but the CLDR puts that one name in a section we aren't parsing.
TestData { "en"sv, Locale::CalendarPatternStyle::Long, "Europe/London"sv, "GMT+01:00"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Short, "Europe/London"sv, "GMT+1"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "Europe/London"sv, "غرينتش+٠١:٠٠"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "Europe/London"sv, "غرينتش+١"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Long, "Africa/Accra"sv, "Greenwich Mean Time"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::Short, "Africa/Accra"sv, "GMT"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "Africa/Accra"sv, "توقيت غرينتش"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "Africa/Accra"sv, "غرينتش"sv },
};
constexpr auto sep_19_2022 = AK::UnixDateTime::from_seconds_since_epoch(1663553728); // Monday, September 19, 2022 2:15:28 AM
for (auto const& test : test_data) {
auto time_zone = Locale::format_time_zone(test.locale, test.time_zone, test.style, sep_19_2022);
EXPECT_EQ(time_zone, test.expected_result);
}
}
TEST_CASE(format_time_zone_offset)
{
constexpr auto jan_1_1833 = AK::UnixDateTime::from_seconds_since_epoch(-4323283200); // Tuesday, January 1, 1833 12:00:00 AM
constexpr auto jan_1_2022 = AK::UnixDateTime::from_seconds_since_epoch(1640995200); // Saturday, January 1, 2022 12:00:00 AM
struct TestData {
StringView locale;
Locale::CalendarPatternStyle style;
AK::UnixDateTime time;
StringView time_zone;
StringView expected_result;
};
constexpr auto test_data = Array {
TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, {}, "UTC"sv, "GMT"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, {}, "UTC"sv, "GMT"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, {}, "UTC"sv, "غرينتش"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, {}, "UTC"sv, "غرينتش"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_1833, "America/Los_Angeles"sv, "GMT-7:52:58"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_2022, "America/Los_Angeles"sv, "GMT-8"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_1833, "America/Los_Angeles"sv, "GMT-07:52:58"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_2022, "America/Los_Angeles"sv, "GMT-08:00"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_1833, "America/Los_Angeles"sv, "غرينتش-٧:٥٢:٥٨"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_2022, "America/Los_Angeles"sv, "غرينتش-٨"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_1833, "America/Los_Angeles"sv, "غرينتش-٠٧:٥٢:٥٨"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_2022, "America/Los_Angeles"sv, "غرينتش-٠٨:٠٠"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_1833, "Europe/London"sv, "GMT-0:01:15"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_2022, "Europe/London"sv, "GMT"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_1833, "Europe/London"sv, "GMT-00:01:15"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_2022, "Europe/London"sv, "GMT"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_1833, "Europe/London"sv, "غرينتش-٠:٠١:١٥"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_2022, "Europe/London"sv, "غرينتش"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_1833, "Europe/London"sv, "غرينتش-٠٠:٠١:١٥"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_2022, "Europe/London"sv, "غرينتش"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_1833, "Asia/Kathmandu"sv, "GMT+5:41:16"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_2022, "Asia/Kathmandu"sv, "GMT+5:45"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_1833, "Asia/Kathmandu"sv, "GMT+05:41:16"sv },
TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_2022, "Asia/Kathmandu"sv, "GMT+05:45"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_1833, "Asia/Kathmandu"sv, "غرينتش+٥:٤١:١٦"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_2022, "Asia/Kathmandu"sv, "غرينتش+٥:٤٥"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_1833, "Asia/Kathmandu"sv, "غرينتش+٠٥:٤١:١٦"sv },
TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_2022, "Asia/Kathmandu"sv, "غرينتش+٠٥:٤٥"sv },
};
for (auto const& test : test_data) {
auto time_zone = Locale::format_time_zone(test.locale, test.time_zone, test.style, test.time);
EXPECT_EQ(time_zone, test.expected_result);
}
}

View File

@ -736,15 +736,13 @@ ErrorOr<void> print_intl_date_time_format(JS::PrintContext& print_context, JS::I
TRY(print_type(print_context, "Intl.DateTimeFormat"sv));
TRY(js_out(print_context, "\n locale: "));
TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), date_time_format.locale()), seen_objects));
TRY(js_out(print_context, "\n pattern: "));
TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), date_time_format.pattern()), seen_objects));
TRY(js_out(print_context, "\n calendar: "));
TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), date_time_format.calendar()), seen_objects));
TRY(js_out(print_context, "\n numberingSystem: "));
TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), date_time_format.numbering_system()), seen_objects));
if (date_time_format.has_hour_cycle()) {
if (date_time_format.hour_cycle.has_value()) {
TRY(js_out(print_context, "\n hourCycle: "));
TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), date_time_format.hour_cycle_string()), seen_objects));
TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), ::Locale::hour_cycle_to_string(*date_time_format.hour_cycle)), seen_objects));
}
TRY(js_out(print_context, "\n timeZone: "));
TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), date_time_format.time_zone()), seen_objects));

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021-2023, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2021-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -9,12 +9,11 @@
#include <AK/Array.h>
#include <AK/String.h>
#include <AK/StringView.h>
#include <AK/Time.h>
#include <AK/Types.h>
#include <AK/Vector.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Intl/AbstractOperations.h>
#include <LibJS/Runtime/Object.h>
#include <LibJS/Runtime/VM.h>
#include <LibLocale/DateTimeFormat.h>
namespace JS::Intl {
@ -28,13 +27,6 @@ class DateTimeFormat final
using Patterns = ::Locale::CalendarPattern;
public:
enum class Style {
Full,
Long,
Medium,
Short,
};
static constexpr auto relevant_extension_keys()
{
// 11.2.3 Internal slots, https://tc39.es/ecma402/#sec-intl.datetimeformat-internal-slots
@ -56,130 +48,51 @@ public:
String const& numbering_system() const { return m_numbering_system; }
void set_numbering_system(String numbering_system) { m_numbering_system = move(numbering_system); }
bool has_hour_cycle() const { return m_hour_cycle.has_value(); }
::Locale::HourCycle hour_cycle() const { return *m_hour_cycle; }
StringView hour_cycle_string() const { return ::Locale::hour_cycle_to_string(*m_hour_cycle); }
void set_hour_cycle(::Locale::HourCycle hour_cycle) { m_hour_cycle = hour_cycle; }
void clear_hour_cycle() { m_hour_cycle.clear(); }
String const& time_zone() const { return m_time_zone; }
void set_time_zone(String time_zone) { m_time_zone = move(time_zone); }
bool has_date_style() const { return m_date_style.has_value(); }
Style date_style() const { return *m_date_style; }
StringView date_style_string() const { return style_to_string(*m_date_style); }
void set_date_style(StringView style) { m_date_style = style_from_string(style); }
Optional<::Locale::DateTimeStyle> const& date_style() const { return m_date_style; }
StringView date_style_string() const { return ::Locale::date_time_style_to_string(*m_date_style); }
void set_date_style(StringView style) { m_date_style = ::Locale::date_time_style_from_string(style); }
bool has_time_style() const { return m_time_style.has_value(); }
Style time_style() const { return *m_time_style; }
StringView time_style_string() const { return style_to_string(*m_time_style); }
void set_time_style(StringView style) { m_time_style = style_from_string(style); }
String const& pattern() const { return Patterns::pattern; }
void set_pattern(String pattern) { Patterns::pattern = move(pattern); }
ReadonlySpan<::Locale::CalendarRangePattern> range_patterns() const { return m_range_patterns.span(); }
void set_range_patterns(Vector<::Locale::CalendarRangePattern> range_patterns) { m_range_patterns = move(range_patterns); }
bool has_era() const { return Patterns::era.has_value(); }
::Locale::CalendarPatternStyle era() const { return *Patterns::era; }
StringView era_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::era); }
bool has_year() const { return Patterns::year.has_value(); }
::Locale::CalendarPatternStyle year() const { return *Patterns::year; }
StringView year_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::year); }
bool has_month() const { return Patterns::month.has_value(); }
::Locale::CalendarPatternStyle month() const { return *Patterns::month; }
StringView month_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::month); }
bool has_weekday() const { return Patterns::weekday.has_value(); }
::Locale::CalendarPatternStyle weekday() const { return *Patterns::weekday; }
StringView weekday_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::weekday); }
bool has_day() const { return Patterns::day.has_value(); }
::Locale::CalendarPatternStyle day() const { return *Patterns::day; }
StringView day_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::day); }
bool has_day_period() const { return Patterns::day_period.has_value(); }
::Locale::CalendarPatternStyle day_period() const { return *Patterns::day_period; }
StringView day_period_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::day_period); }
bool has_hour() const { return Patterns::hour.has_value(); }
::Locale::CalendarPatternStyle hour() const { return *Patterns::hour; }
StringView hour_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::hour); }
bool has_minute() const { return Patterns::minute.has_value(); }
::Locale::CalendarPatternStyle minute() const { return *Patterns::minute; }
StringView minute_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::minute); }
bool has_second() const { return Patterns::second.has_value(); }
::Locale::CalendarPatternStyle second() const { return *Patterns::second; }
StringView second_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::second); }
bool has_fractional_second_digits() const { return Patterns::fractional_second_digits.has_value(); }
u8 fractional_second_digits() const { return *Patterns::fractional_second_digits; }
bool has_time_zone_name() const { return Patterns::time_zone_name.has_value(); }
::Locale::CalendarPatternStyle time_zone_name() const { return *Patterns::time_zone_name; }
StringView time_zone_name_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::time_zone_name); }
Optional<::Locale::DateTimeStyle> const& time_style() const { return m_time_style; }
StringView time_style_string() const { return ::Locale::date_time_style_to_string(*m_time_style); }
void set_time_style(StringView style) { m_time_style = ::Locale::date_time_style_from_string(style); }
NativeFunction* bound_format() const { return m_bound_format; }
void set_bound_format(NativeFunction* bound_format) { m_bound_format = bound_format; }
::Locale::DateTimeFormat const& formatter() const { return *m_formatter; }
void set_formatter(NonnullOwnPtr<::Locale::DateTimeFormat> formatter) { m_formatter = move(formatter); }
private:
DateTimeFormat(Object& prototype);
static Style style_from_string(StringView style);
static StringView style_to_string(Style style);
virtual void visit_edges(Visitor&) override;
String m_locale; // [[Locale]]
String m_calendar; // [[Calendar]]
String m_numbering_system; // [[NumberingSystem]]
Optional<::Locale::HourCycle> m_hour_cycle; // [[HourCycle]]
String m_time_zone; // [[TimeZone]]
Optional<Style> m_date_style; // [[DateStyle]]
Optional<Style> m_time_style; // [[TimeStyle]]
Vector<::Locale::CalendarRangePattern> m_range_patterns; // [[RangePatterns]]
GCPtr<NativeFunction> m_bound_format; // [[BoundFormat]]
String m_locale; // [[Locale]]
String m_calendar; // [[Calendar]]
String m_numbering_system; // [[NumberingSystem]]
String m_time_zone; // [[TimeZone]]
Optional<::Locale::DateTimeStyle> m_date_style; // [[DateStyle]]
Optional<::Locale::DateTimeStyle> m_time_style; // [[TimeStyle]]
GCPtr<NativeFunction> m_bound_format; // [[BoundFormat]]
String m_data_locale;
// Non-standard. Stores the ICU date-time formatter for the Intl object's formatting options.
OwnPtr<::Locale::DateTimeFormat> m_formatter;
};
// Table 8: Record returned by ToLocalTime, https://tc39.es/ecma402/#table-datetimeformat-tolocaltime-record
// Note: [[InDST]] is not included here - it is handled by LibUnicode / LibTimeZone.
struct LocalTime {
AK::UnixDateTime time_since_epoch() const
{
return AK::UnixDateTime::from_unix_time_parts(year, month + 1, day + 1, hour, minute, second, millisecond);
}
int weekday { 0 }; // [[Weekday]]
::Locale::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]]
};
Optional<::Locale::CalendarPattern> date_time_style_format(StringView data_locale, DateTimeFormat& date_time_format);
Optional<::Locale::CalendarPattern> basic_format_matcher(::Locale::CalendarPattern const& options, Vector<::Locale::CalendarPattern> formats);
Optional<::Locale::CalendarPattern> best_fit_format_matcher(::Locale::CalendarPattern const& options, Vector<::Locale::CalendarPattern> formats);
ThrowCompletionOr<Vector<PatternPartition>> format_date_time_pattern(VM&, DateTimeFormat&, Vector<PatternPartition> pattern_parts, double time, ::Locale::CalendarPattern const* range_format_options);
ThrowCompletionOr<Vector<PatternPartition>> partition_date_time_pattern(VM&, DateTimeFormat&, double time);
ThrowCompletionOr<Vector<::Locale::DateTimeFormat::Partition>> format_date_time_pattern(VM&, DateTimeFormat&, double time);
ThrowCompletionOr<Vector<::Locale::DateTimeFormat::Partition>> partition_date_time_pattern(VM&, DateTimeFormat&, double time);
ThrowCompletionOr<String> format_date_time(VM&, DateTimeFormat&, double time);
ThrowCompletionOr<NonnullGCPtr<Array>> format_date_time_to_parts(VM&, DateTimeFormat&, double time);
ThrowCompletionOr<Vector<PatternPartitionWithSource>> partition_date_time_range_pattern(VM&, DateTimeFormat&, double start, double end);
ThrowCompletionOr<Vector<::Locale::DateTimeFormat::Partition>> partition_date_time_range_pattern(VM&, DateTimeFormat&, double start, double end);
ThrowCompletionOr<String> format_date_time_range(VM&, DateTimeFormat&, double start, double end);
ThrowCompletionOr<NonnullGCPtr<Array>> format_date_time_range_to_parts(VM&, DateTimeFormat&, double start, double end);
ThrowCompletionOr<LocalTime> to_local_time(VM&, Crypto::SignedBigInteger const& epoch_ns, StringView calendar, StringView time_zone);
template<typename Callback>
ThrowCompletionOr<void> for_each_calendar_field(VM& vm, ::Locale::CalendarPattern& pattern, Callback&& callback)

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021-2023, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2021-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -168,30 +168,15 @@ ThrowCompletionOr<NonnullGCPtr<DateTimeFormat>> create_date_time_format(VM& vm,
// 24. Let dataLocaleData be localeData.[[<dataLocale>]].
Optional<::Locale::HourCycle> hour_cycle_value;
auto set_locale_hour_cycle = [&](auto... candidate_hour_cycles) {
// Locale hour cycles (parsed from timeData.json) are stored in preference order. Use the
// first hour cycle that matches any of the provided candidates. There may be no matches if
// e.g. Unicode data generation is disabled.
auto locale_hour_cycles = ::Locale::get_locale_hour_cycles(data_locale);
auto index = locale_hour_cycles.find_first_index_if([&](auto hour_cycle) {
return ((hour_cycle == candidate_hour_cycles) || ...);
});
if (index.has_value())
hour_cycle_value = locale_hour_cycles[*index];
};
Optional<bool> hour12_value;
// 25. If hour12 is true, then
if (hour12.is_boolean() && hour12.as_bool()) {
// a. Let hc be dataLocaleData.[[hourCycle12]].
set_locale_hour_cycle(::Locale::HourCycle::H11, ::Locale::HourCycle::H12);
}
// a. Let hc be dataLocaleData.[[hourCycle12]].
// 26. Else if hour12 is false, then
else if (hour12.is_boolean() && !hour12.as_bool()) {
// a. Let hc be dataLocaleData.[[hourCycle24]].
set_locale_hour_cycle(::Locale::HourCycle::H23, ::Locale::HourCycle::H24);
// a. Let hc be dataLocaleData.[[hourCycle24]].
if (hour12.is_boolean()) {
// NOTE: We let LibLocale figure out the appropriate hour cycle.
hour12_value = hour12.as_bool();
}
// 27. Else,
else {
@ -208,8 +193,7 @@ ThrowCompletionOr<NonnullGCPtr<DateTimeFormat>> create_date_time_format(VM& vm,
}
// 28. Set dateTimeFormat.[[HourCycle]] to hc.
if (hour_cycle_value.has_value())
date_time_format->set_hour_cycle(*hour_cycle_value);
date_time_format->hour_cycle = hour_cycle_value;
// 29. Let timeZone be ? Get(options, "timeZone").
auto time_zone_value = TRY(options->get(vm.names.timeZone));
@ -227,7 +211,9 @@ ThrowCompletionOr<NonnullGCPtr<DateTimeFormat>> create_date_time_format(VM& vm,
}
// 32. If IsTimeZoneOffsetString(timeZone) is true, then
if (is_time_zone_offset_string(time_zone)) {
bool is_time_zone_offset_string = JS::is_time_zone_offset_string(time_zone);
if (is_time_zone_offset_string) {
// a. Let parseResult be ParseText(StringToCodePoints(timeZone), UTCOffset).
auto parse_result = Temporal::parse_iso8601(Temporal::Production::TimeZoneNumericUTCOffset, time_zone);
@ -262,13 +248,18 @@ ThrowCompletionOr<NonnullGCPtr<DateTimeFormat>> create_date_time_format(VM& vm,
}
// 35. Set dateTimeFormat.[[TimeZone]] to timeZone.
date_time_format->set_time_zone(move(time_zone));
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));
// 36. Let formatOptions be a new Record.
::Locale::CalendarPattern format_options {};
// 37. Set formatOptions.[[hourCycle]] to hc.
format_options.hour_cycle = hour_cycle_value;
format_options.hour12 = hour12_value;
// 38. Let hasExplicitFormatComponents be false.
// NOTE: Instead of using a boolean, we track any explicitly provided component name for nicer exception messages.
@ -330,8 +321,6 @@ ThrowCompletionOr<NonnullGCPtr<DateTimeFormat>> create_date_time_format(VM& vm,
if (!time_style.is_undefined())
date_time_format->set_time_style(time_style.as_string().utf8_string_view());
Optional<::Locale::CalendarPattern> best_format {};
// 45. 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
@ -354,7 +343,14 @@ ThrowCompletionOr<NonnullGCPtr<DateTimeFormat>> create_date_time_format(VM& vm,
// d. Let styles be dataLocaleData.[[styles]].[[<resolvedCalendar>]].
// e. Let bestFormat be DateTimeStyleFormat(dateStyle, timeStyle, styles).
best_format = date_time_style_format(data_locale, date_time_format);
auto formatter = ::Locale::DateTimeFormat::create_for_date_and_time_style(
date_time_format->data_locale(),
time_zone,
format_options.hour_cycle,
format_options.hour12,
date_time_format->date_style(),
date_time_format->time_style());
date_time_format->set_formatter(move(formatter));
}
// 46. Else,
else {
@ -421,22 +417,19 @@ ThrowCompletionOr<NonnullGCPtr<DateTimeFormat>> create_date_time_format(VM& vm,
}
// f. Let formats be dataLocaleData.[[formats]].[[<resolvedCalendar>]].
auto formats = ::Locale::get_calendar_available_formats(data_locale, date_time_format->calendar());
// g. If matcher is "basic", then
if (matcher.as_string().utf8_string_view() == "basic"sv) {
// i. Let bestFormat be BasicFormatMatcher(formatOptions, formats).
best_format = basic_format_matcher(format_options, move(formats));
}
// i. Let bestFormat be BasicFormatMatcher(formatOptions, formats).
// h. Else,
else {
// i. Let bestFormat be BestFitFormatMatcher(formatOptions, formats).
best_format = best_fit_format_matcher(format_options, move(formats));
}
// i. Let bestFormat be BestFitFormatMatcher(formatOptions, formats).
auto formatter = ::Locale::DateTimeFormat::create_for_pattern_options(
date_time_format->data_locale(),
time_zone,
format_options);
date_time_format->set_formatter(move(formatter));
}
// 47. For each row in Table 6, except the header row, in table order, do
date_time_format->for_each_calendar_field_zipped_with(*best_format, [&](auto& date_time_format_field, auto const& best_format_field, auto) {
date_time_format->for_each_calendar_field_zipped_with(date_time_format->formatter().chosen_pattern(), [&](auto& date_time_format_field, auto const& best_format_field) {
// a. Let prop be the name given in the Property column of the row.
// b. If bestFormat has a field [[<prop>]], then
if (best_format_field.has_value()) {
@ -446,43 +439,20 @@ ThrowCompletionOr<NonnullGCPtr<DateTimeFormat>> create_date_time_format(VM& vm,
}
});
String pattern;
Vector<::Locale::CalendarRangePattern> range_patterns;
// 48. If dateTimeFormat.[[Hour]] is undefined, then
if (!date_time_format->has_hour()) {
if (!date_time_format->hour.has_value()) {
// a. Set dateTimeFormat.[[HourCycle]] to undefined.
date_time_format->clear_hour_cycle();
date_time_format->hour_cycle.clear();
}
// 49. If dateTimeFormat.[[HourCycle]] is "h11" or "h12", then
if ((hour_cycle_value == ::Locale::HourCycle::H11) || (hour_cycle_value == ::Locale::HourCycle::H12)) {
// a. Let pattern be bestFormat.[[pattern12]].
if (best_format->pattern12.has_value()) {
pattern = best_format->pattern12.release_value();
} else {
// Non-standard, LibUnicode only provides [[pattern12]] when [[pattern]] has a day
// period. Other implementations provide [[pattern12]] as a copy of [[pattern]].
pattern = move(best_format->pattern);
}
// b. Let rangePatterns be bestFormat.[[rangePatterns12]].
range_patterns = ::Locale::get_calendar_range12_formats(data_locale, date_time_format->calendar(), best_format->skeleton);
}
// a. Let pattern be bestFormat.[[pattern12]].
// b. Let rangePatterns be bestFormat.[[rangePatterns12]].
// 50. Else,
else {
// a. Let pattern be bestFormat.[[pattern]].
pattern = move(best_format->pattern);
// b. Let rangePatterns be bestFormat.[[rangePatterns]].
range_patterns = ::Locale::get_calendar_range_formats(data_locale, date_time_format->calendar(), best_format->skeleton);
}
// a. Let pattern be bestFormat.[[pattern]].
// b. Let rangePatterns be bestFormat.[[rangePatterns]].
// 51. Set dateTimeFormat.[[Pattern]] to pattern.
date_time_format->set_pattern(move(pattern));
// 52. Set dateTimeFormat.[[RangePatterns]] to rangePatterns.
date_time_format->set_range_patterns(move(range_patterns));
// 53. Return dateTimeFormat.
return date_time_format;

View File

@ -179,10 +179,10 @@ JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatPrototype::resolved_options)
MUST(options->create_data_property_or_throw(vm.names.numberingSystem, PrimitiveString::create(vm, date_time_format->numbering_system())));
MUST(options->create_data_property_or_throw(vm.names.timeZone, PrimitiveString::create(vm, date_time_format->time_zone())));
if (date_time_format->has_hour_cycle()) {
MUST(options->create_data_property_or_throw(vm.names.hourCycle, PrimitiveString::create(vm, date_time_format->hour_cycle_string())));
if (date_time_format->hour_cycle.has_value()) {
MUST(options->create_data_property_or_throw(vm.names.hourCycle, PrimitiveString::create(vm, ::Locale::hour_cycle_to_string(*date_time_format->hour_cycle))));
switch (date_time_format->hour_cycle()) {
switch (*date_time_format->hour_cycle) {
case ::Locale::HourCycle::H11:
case ::Locale::HourCycle::H12:
MUST(options->create_data_property_or_throw(vm.names.hour12, Value(true)));

View File

@ -145,9 +145,9 @@ describe("weekday", () => {
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: "٢٣ ١ ١٩٨٩ ميلادي" },
{ 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", () => {
@ -170,7 +170,7 @@ describe("era", () => {
expect(en.format(year1BC)).toBe("1/1/1 BC");
const ar = new Intl.DateTimeFormat("ar", { era: "short", timeZone: "UTC" });
expect(ar.format(year1BC)).toBe("١ ١ ١ ق.م");
expect(ar.format(year1BC)).toBe("٠١-٠١-١ ق.م");
});
});
@ -300,7 +300,7 @@ describe("dayPeriod", () => {
{ minute: undefined, second: undefined, fractionalSecondDigits: undefined, en0: "12 noon", en1: "12 noon", en2: "12 noon", en3: "12 noon", ar0: "١٢ ظهرًا", ar1: "١٢ ظهرًا", ar2: "١٢ ظهرًا", ar3: "١٢ ظهرًا" },
{ minute: "numeric", second: undefined, fractionalSecondDigits: undefined, en0: "12:00 noon", en1: "12:01 in the afternoon", en2: "12:00 noon", en3: "12:00 noon", ar0: "١٢:٠٠ ظهرًا", ar1: "١٢:٠١ ظهرًا", ar2: "١٢:٠٠ ظهرًا", ar3: "١٢:٠٠ ظهرًا" },
{ minute: "numeric", second: "numeric", fractionalSecondDigits: undefined, en0: "12:00:00 noon", en1: "12:01:00 in the afternoon", en2: "12:00:01 in the afternoon", en3: "12:00:00 noon", ar0: "١٢:٠٠:٠٠ ظهرًا", ar1: "١٢:٠١:٠٠ ظهرًا", ar2: "١٢:٠٠:٠١ ظهرًا", ar3: "١٢:٠٠:٠٠ ظهرًا" },
{ minute: "numeric", second: "numeric", fractionalSecondDigits: 1, en0: "12:00:00.0 noon", en1: "12:01:00.0 in the afternoon", en2: "12:00:01.0 in the afternoon", en3: "12:00:00.5 in the afternoon", ar0: "١٢:٠٠:٠٠٫٠ ظهرًا", ar1: "١٢:٠١:٠٠٫٠ ظهرًا", ar2: "١٢:٠٠:٠١٫٠ ظهرًا", ar3: "١٢:٠٠:٠٠٫٥ ظهرًا" },
{ minute: "numeric", second: "numeric", fractionalSecondDigits: 1, en0: "12:00:00.0 noon", en1: "12:01:00.0 in the afternoon", en2: "12:00:01.0 in the afternoon", en3: "12:00:00.5 noon", ar0: "١٢:٠٠:٠٠٫٠ ظهرًا", ar1: "١٢:٠١:٠٠٫٠ ظهرًا", ar2: "١٢:٠٠:٠١٫٠ ظهرًا", ar3: "١٢:٠٠:٠٠٫٥ ظهرًا" },
];
// The en locale includes the "noon" fixed day period, whereas the ar locale does not.
@ -368,10 +368,8 @@ describe("dayPeriod", () => {
describe("hour", () => {
// prettier-ignore
// FIXME: The 2-digit results are supposed to include {ampm}. These results are achieved 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: "2-digit", en0: "05\u202fPM", en1: "07\u202fAM", ar0: "٠٥ م", ar1: "٠٧ ص" },
{ hour: "numeric", en0: "5\u202fPM", en1: "7\u202fAM", ar0: "٥ م", ar1: "٧ ص" },
];
@ -478,47 +476,47 @@ describe("fractionalSecondDigits", () => {
describe("timeZoneName", () => {
// prettier-ignore
const data = [
{ timeZone: "UTC", timeZoneName: "short", en0: "12/7/2021, 5:40\u202fPM UTC", en1: "1/23/1989, 7:08\u202fAM UTC", ar0: "٧/١٢‏/٢٠٢١، ٥:٤٠ م UTC", ar1: "٢٣‏/١/١٩٨٩، ٧:٠٨ ص UTC" },
{ timeZone: "UTC", timeZoneName: "long", en0: "12/7/2021, 5:40\u202fPM Coordinated Universal Time", en1: "1/23/1989, 7:08\u202fAM Coordinated Universal Time", ar0: "٧/١٢‏/٢٠٢١، ٥:٤٠ م التوقيت العالمي المنسق", ar1: "٢٣‏/١/١٩٨٩، ٧:٠٨ ص التوقيت العالمي المنسق" },
{ timeZone: "UTC", timeZoneName: "shortOffset", en0: "12/7/2021, 5:40\u202fPM GMT", en1: "1/23/1989, 7:08\u202fAM GMT", ar0: "٧/١٢‏/٢٠٢١، ٥:٤٠ م غرينتش", ar1: "٢٣‏/١/١٩٨٩، ٧:٠٨ ص غرينتش" },
{ timeZone: "UTC", timeZoneName: "longOffset", en0: "12/7/2021, 5:40\u202fPM GMT", en1: "1/23/1989, 7:08\u202fAM GMT", ar0: "٧/١٢‏/٢٠٢١، ٥:٤٠ م غرينتش", ar1: "٢٣‏/١/١٩٨٩، ٧:٠٨ ص غرينتش" },
{ timeZone: "UTC", timeZoneName: "shortGeneric", en0: "12/7/2021, 5:40\u202fPM GMT", en1: "1/23/1989, 7:08\u202fAM GMT", ar0: "٧/١٢‏/٢٠٢١، ٥:٤٠ م غرينتش", ar1: "٢٣‏/١/١٩٨٩، ٧:٠٨ ص غرينتش" },
{ timeZone: "UTC", timeZoneName: "longGeneric", en0: "12/7/2021, 5:40\u202fPM GMT", en1: "1/23/1989, 7:08\u202fAM GMT", ar0: "٧/١٢‏/٢٠٢١، ٥:٤٠ م غرينتش", ar1: "٢٣‏/١/١٩٨٩، ٧:٠٨ ص غرينتش" },
{ timeZone: "UTC", timeZoneName: "short", en0: "12/7/2021, UTC", en1: "1/23/1989, UTC", ar0: "٧/١٢‏/٢٠٢١، UTC", ar1: "٢٣‏/١/١٩٨٩، UTC" },
{ timeZone: "UTC", timeZoneName: "long", en0: "12/7/2021, Coordinated Universal Time", en1: "1/23/1989, Coordinated Universal Time", ar0: "٧/١٢‏/٢٠٢١، التوقيت العالمي المنسق", ar1: "٢٣‏/١/١٩٨٩، التوقيت العالمي المنسق" },
{ timeZone: "UTC", timeZoneName: "shortOffset", en0: "12/7/2021, GMT", en1: "1/23/1989, GMT", ar0: "٧/١٢‏/٢٠٢١، غرينتش", ar1: "٢٣‏/١/١٩٨٩، غرينتش" },
{ timeZone: "UTC", timeZoneName: "longOffset", en0: "12/7/2021, GMT", en1: "1/23/1989, GMT", ar0: "٧/١٢‏/٢٠٢١، غرينتش", ar1: "٢٣‏/١/١٩٨٩، غرينتش" },
{ timeZone: "UTC", timeZoneName: "shortGeneric", en0: "12/7/2021, GMT", en1: "1/23/1989, GMT", ar0: "٧/١٢‏/٢٠٢١، غرينتش", ar1: "٢٣‏/١/١٩٨٩، غرينتش" },
{ timeZone: "UTC", timeZoneName: "longGeneric", en0: "12/7/2021, GMT", en1: "1/23/1989, GMT", ar0: "٧/١٢‏/٢٠٢١، غرينتش", ar1: "٢٣‏/١/١٩٨٩، غرينتش" },
{ timeZone: "America/New_York", timeZoneName: "short", en0: "12/7/2021, 12:40\u202fPM EST", en1: "1/23/1989, 2:08\u202fAM EST", ar0: "٧/١٢‏/٢٠٢١، ١٢:٤٠ م غرينتش-٥", ar1: "٢٣‏/١/١٩٨٩، ٢:٠٨ ص غرينتش-٥" },
{ timeZone: "America/New_York", timeZoneName: "long", en0: "12/7/2021, 12:40\u202fPM Eastern Standard Time", en1: "1/23/1989, 2:08\u202fAM Eastern Standard Time", ar0: "٧/١٢‏/٢٠٢١، ١٢:٤٠ م التوقيت الرسمي الشرقي لأمريكا الشمالية", ar1: "٢٣‏/١/١٩٨٩، ٢:٠٨ ص التوقيت الرسمي الشرقي لأمريكا الشمالية" },
{ timeZone: "America/New_York", timeZoneName: "shortOffset", en0: "12/7/2021, 12:40\u202fPM GMT-5", en1: "1/23/1989, 2:08\u202fAM GMT-5", ar0: "٧/١٢‏/٢٠٢١، ١٢:٤٠ م غرينتش-٥", ar1: "٢٣‏/١/١٩٨٩، ٢:٠٨ ص غرينتش-٥" },
{ timeZone: "America/New_York", timeZoneName: "longOffset", en0: "12/7/2021, 12:40\u202fPM GMT-05:00", en1: "1/23/1989, 2:08\u202fAM GMT-05:00", ar0: "٧/١٢‏/٢٠٢١، ١٢:٤٠ م غرينتش-٠٥:٠٠", ar1: "٢٣‏/١/١٩٨٩، ٢:٠٨ ص غرينتش-٠٥:٠٠" },
{ timeZone: "America/New_York", timeZoneName: "shortGeneric", en0: "12/7/2021, 12:40\u202fPM ET", en1: "1/23/1989, 2:08\u202fAM ET", ar0: "٧/١٢‏/٢٠٢١، ١٢:٤٠ م غرينتش-٥", ar1: "٢٣‏/١/١٩٨٩، ٢:٠٨ ص غرينتش-٥" },
{ timeZone: "America/New_York", timeZoneName: "longGeneric", en0: "12/7/2021, 12:40\u202fPM Eastern Time", en1: "1/23/1989, 2:08\u202fAM Eastern Time", ar0: "٧/١٢‏/٢٠٢١، ١٢:٤٠ م التوقيت الشرقي لأمريكا الشمالية", ar1: "٢٣‏/١/١٩٨٩، ٢:٠٨ ص التوقيت الشرقي لأمريكا الشمالية" },
{ timeZone: "America/New_York", timeZoneName: "short", en0: "12/7/2021, EST", en1: "1/23/1989, EST", ar0: "٧/١٢‏/٢٠٢١، غرينتش-٥", ar1: "٢٣‏/١/١٩٨٩، غرينتش-٥" },
{ timeZone: "America/New_York", timeZoneName: "long", en0: "12/7/2021, Eastern Standard Time", en1: "1/23/1989, Eastern Standard Time", ar0: "٧/١٢‏/٢٠٢١، التوقيت الرسمي الشرقي لأمريكا الشمالية", ar1: "٢٣‏/١/١٩٨٩، التوقيت الرسمي الشرقي لأمريكا الشمالية" },
{ timeZone: "America/New_York", timeZoneName: "shortOffset", en0: "12/7/2021, GMT-5", en1: "1/23/1989, GMT-5", ar0: "٧/١٢‏/٢٠٢١، غرينتش-٥", ar1: "٢٣‏/١/١٩٨٩، غرينتش-٥" },
{ timeZone: "America/New_York", timeZoneName: "longOffset", en0: "12/7/2021, GMT-05:00", en1: "1/23/1989, GMT-05:00", ar0: "٧/١٢‏/٢٠٢١، غرينتش-٠٥:٠٠", ar1: "٢٣‏/١/١٩٨٩، غرينتش-٠٥:٠٠" },
{ timeZone: "America/New_York", timeZoneName: "shortGeneric", en0: "12/7/2021, ET", en1: "1/23/1989, ET", ar0: "٧/١٢‏/٢٠٢١، توقيت نيويورك", ar1: "٢٣‏/١/١٩٨٩، توقيت نيويورك" },
{ timeZone: "America/New_York", timeZoneName: "longGeneric", en0: "12/7/2021, Eastern Time", en1: "1/23/1989, Eastern Time", ar0: "٧/١٢‏/٢٠٢١، التوقيت الشرقي لأمريكا الشمالية", ar1: "٢٣‏/١/١٩٨٩، التوقيت الشرقي لأمريكا الشمالية" },
{ timeZone: "Europe/London", timeZoneName: "short", en0: "12/7/2021, 5:40\u202fPM GMT", en1: "1/23/1989, 7:08\u202fAM GMT", ar0: "٧/١٢‏/٢٠٢١، ٥:٤٠ م غرينتش", ar1: "٢٣‏/١/١٩٨٩، ٧:٠٨ ص غرينتش" },
{ timeZone: "Europe/London", timeZoneName: "long", en0: "12/7/2021, 5:40\u202fPM Greenwich Mean Time", en1: "1/23/1989, 7:08\u202fAM Greenwich Mean Time", ar0: "٧/١٢‏/٢٠٢١، ٥:٤٠ م توقيت غرينتش", ar1: "٢٣‏/١/١٩٨٩، ٧:٠٨ ص توقيت غرينتش" },
{ timeZone: "Europe/London", timeZoneName: "shortOffset", en0: "12/7/2021, 5:40\u202fPM GMT", en1: "1/23/1989, 7:08\u202fAM GMT", ar0: "٧/١٢‏/٢٠٢١، ٥:٤٠ م غرينتش", ar1: "٢٣‏/١/١٩٨٩، ٧:٠٨ ص غرينتش" },
{ timeZone: "Europe/London", timeZoneName: "longOffset", en0: "12/7/2021, 5:40\u202fPM GMT", en1: "1/23/1989, 7:08\u202fAM GMT", ar0: "٧/١٢‏/٢٠٢١، ٥:٤٠ م غرينتش", ar1: "٢٣‏/١/١٩٨٩، ٧:٠٨ ص غرينتش" },
{ timeZone: "Europe/London", timeZoneName: "shortGeneric", en0: "12/7/2021, 5:40\u202fPM GMT", en1: "1/23/1989, 7:08\u202fAM GMT", ar0: "٧/١٢‏/٢٠٢١، ٥:٤٠ م غرينتش", ar1: "٢٣‏/١/١٩٨٩، ٧:٠٨ ص غرينتش" },
{ timeZone: "Europe/London", timeZoneName: "longGeneric", en0: "12/7/2021, 5:40\u202fPM GMT", en1: "1/23/1989, 7:08\u202fAM GMT", ar0: "٧/١٢‏/٢٠٢١، ٥:٤٠ م غرينتش", ar1: "٢٣‏/١/١٩٨٩، ٧:٠٨ ص غرينتش" },
{ timeZone: "Europe/London", timeZoneName: "short", en0: "12/7/2021, GMT", en1: "1/23/1989, GMT", ar0: "٧/١٢‏/٢٠٢١، غرينتش", ar1: "٢٣‏/١/١٩٨٩، غرينتش" },
{ timeZone: "Europe/London", timeZoneName: "long", en0: "12/7/2021, Greenwich Mean Time", en1: "1/23/1989, Greenwich Mean Time", ar0: "٧/١٢‏/٢٠٢١، توقيت غرينتش", ar1: "٢٣‏/١/١٩٨٩، توقيت غرينتش" },
{ timeZone: "Europe/London", timeZoneName: "shortOffset", en0: "12/7/2021, GMT", en1: "1/23/1989, GMT", ar0: "٧/١٢‏/٢٠٢١، غرينتش", ar1: "٢٣‏/١/١٩٨٩، غرينتش" },
{ timeZone: "Europe/London", timeZoneName: "longOffset", en0: "12/7/2021, GMT", en1: "1/23/1989, GMT", ar0: "٧/١٢‏/٢٠٢١، غرينتش", ar1: "٢٣‏/١/١٩٨٩، غرينتش" },
{ timeZone: "Europe/London", timeZoneName: "shortGeneric", en0: "12/7/2021, United Kingdom Time", en1: "1/23/1989, United Kingdom Time", ar0: "٧/١٢‏/٢٠٢١، توقيت المملكة المتحدة", ar1: "٢٣‏/١/١٩٨٩، توقيت المملكة المتحدة" },
{ timeZone: "Europe/London", timeZoneName: "longGeneric", en0: "12/7/2021, United Kingdom Time", en1: "1/23/1989, United Kingdom Time", ar0: "٧/١٢‏/٢٠٢١، توقيت المملكة المتحدة", ar1: "٢٣‏/١/١٩٨٩، توقيت المملكة المتحدة" },
{ timeZone: "America/Los_Angeles", timeZoneName: "short", en0: "12/7/2021, 9:40\u202fAM PST", en1: "1/22/1989, 11:08\u202fPM PST", ar0: "٧/١٢‏/٢٠٢١، ٩:٤٠ ص غرينتش-٨", ar1: "٢٢‏/١/١٩٨٩، ١١:٠٨ م غرينتش-٨" },
{ timeZone: "America/Los_Angeles", timeZoneName: "long", en0: "12/7/2021, 9:40\u202fAM Pacific Standard Time", en1: "1/22/1989, 11:08\u202fPM Pacific Standard Time", ar0: "٧/١٢‏/٢٠٢١، ٩:٤٠ ص توقيت المحيط الهادي الرسمي", ar1: "٢٢‏/١/١٩٨٩، ١١:٠٨ م توقيت المحيط الهادي الرسمي" },
{ timeZone: "America/Los_Angeles", timeZoneName: "shortOffset", en0: "12/7/2021, 9:40\u202fAM GMT-8", en1: "1/22/1989, 11:08\u202fPM GMT-8", ar0: "٧/١٢‏/٢٠٢١، ٩:٤٠ ص غرينتش-٨", ar1: "٢٢‏/١/١٩٨٩، ١١:٠٨ م غرينتش-٨" },
{ timeZone: "America/Los_Angeles", timeZoneName: "longOffset", en0: "12/7/2021, 9:40\u202fAM GMT-08:00", en1: "1/22/1989, 11:08\u202fPM GMT-08:00", ar0: "٧/١٢‏/٢٠٢١، ٩:٤٠ ص غرينتش-٠٨:٠٠", ar1: "٢٢‏/١/١٩٨٩، ١١:٠٨ م غرينتش-٠٨:٠٠" },
{ timeZone: "America/Los_Angeles", timeZoneName: "shortGeneric", en0: "12/7/2021, 9:40\u202fAM PT", en1: "1/22/1989, 11:08\u202fPM PT", ar0: "٧/١٢‏/٢٠٢١، ٩:٤٠ ص غرينتش-٨", ar1: "٢٢‏/١/١٩٨٩، ١١:٠٨ م غرينتش-٨" },
{ timeZone: "America/Los_Angeles", timeZoneName: "longGeneric", en0: "12/7/2021, 9:40\u202fAM Pacific Time", en1: "1/22/1989, 11:08\u202fPM Pacific Time", ar0: "٧/١٢‏/٢٠٢١، ٩:٤٠ ص توقيت المحيط الهادي", ar1: "٢٢‏/١/١٩٨٩، ١١:٠٨ م توقيت المحيط الهادي" },
{ timeZone: "America/Los_Angeles", timeZoneName: "short", en0: "12/7/2021, PST", en1: "1/22/1989, PST", ar0: "٧/١٢‏/٢٠٢١، غرينتش-٨", ar1: "٢٢‏/١/١٩٨٩، غرينتش-٨" },
{ timeZone: "America/Los_Angeles", timeZoneName: "long", en0: "12/7/2021, Pacific Standard Time", en1: "1/22/1989, Pacific Standard Time", ar0: "٧/١٢‏/٢٠٢١، توقيت المحيط الهادي الرسمي", ar1: "٢٢‏/١/١٩٨٩، توقيت المحيط الهادي الرسمي" },
{ timeZone: "America/Los_Angeles", timeZoneName: "shortOffset", en0: "12/7/2021, GMT-8", en1: "1/22/1989, GMT-8", ar0: "٧/١٢‏/٢٠٢١، غرينتش-٨", ar1: "٢٢‏/١/١٩٨٩، غرينتش-٨" },
{ timeZone: "America/Los_Angeles", timeZoneName: "longOffset", en0: "12/7/2021, GMT-08:00", en1: "1/22/1989, GMT-08:00", ar0: "٧/١٢‏/٢٠٢١، غرينتش-٠٨:٠٠", ar1: "٢٢‏/١/١٩٨٩، غرينتش-٠٨:٠٠" },
{ timeZone: "America/Los_Angeles", timeZoneName: "shortGeneric", en0: "12/7/2021, PT", en1: "1/22/1989, PT", ar0: "٧/١٢‏/٢٠٢١، توقيت لوس انجلوس", ar1: "٢٢‏/١/١٩٨٩، توقيت لوس انجلوس" },
{ timeZone: "America/Los_Angeles", timeZoneName: "longGeneric", en0: "12/7/2021, Pacific Time", en1: "1/22/1989, Pacific Time", ar0: "٧/١٢‏/٢٠٢١، توقيت المحيط الهادي", ar1: "٢٢‏/١/١٩٨٩، توقيت المحيط الهادي" },
{ timeZone: "Asia/Kathmandu", timeZoneName: "short", en0: "12/7/2021, 11:25\u202fPM GMT+5:45", en1: "1/23/1989, 12:53\u202fPM GMT+5:45", ar0: "٧/١٢‏/٢٠٢١، ١١:٢٥ م غرينتش+٥:٤٥", ar1: "٢٣‏/١/١٩٨٩، ١٢:٥٣ م غرينتش+٥:٤٥" },
{ timeZone: "Asia/Kathmandu", timeZoneName: "long", en0: "12/7/2021, 11:25\u202fPM Nepal Time", en1: "1/23/1989, 12:53\u202fPM Nepal Time", ar0: "٧/١٢‏/٢٠٢١، ١١:٢٥ م توقيت نيبال", ar1: "٢٣‏/١/١٩٨٩، ١٢:٥٣ م توقيت نيبال" },
{ timeZone: "Asia/Kathmandu", timeZoneName: "shortOffset", en0: "12/7/2021, 11:25\u202fPM GMT+5:45", en1: "1/23/1989, 12:53\u202fPM GMT+5:45", ar0: "٧/١٢‏/٢٠٢١، ١١:٢٥ م غرينتش+٥:٤٥", ar1: "٢٣‏/١/١٩٨٩، ١٢:٥٣ م غرينتش+٥:٤٥" },
{ timeZone: "Asia/Kathmandu", timeZoneName: "longOffset", en0: "12/7/2021, 11:25\u202fPM GMT+05:45", en1: "1/23/1989, 12:53\u202fPM GMT+05:45", ar0: "٧/١٢‏/٢٠٢١، ١١:٢٥ م غرينتش+٠٥:٤٥", ar1: "٢٣‏/١/١٩٨٩، ١٢:٥٣ م غرينتش+٠٥:٤٥" },
{ timeZone: "Asia/Kathmandu", timeZoneName: "shortGeneric", en0: "12/7/2021, 11:25\u202fPM GMT+5:45", en1: "1/23/1989, 12:53\u202fPM GMT+5:45", ar0: "٧/١٢‏/٢٠٢١، ١١:٢٥ م غرينتش+٥:٤٥", ar1: "٢٣‏/١/١٩٨٩، ١٢:٥٣ م غرينتش+٥:٤٥" },
{ timeZone: "Asia/Kathmandu", timeZoneName: "longGeneric", en0: "12/7/2021, 11:25\u202fPM GMT+05:45", en1: "1/23/1989, 12:53\u202fPM GMT+05:45", ar0: "٧/١٢‏/٢٠٢١، ١١:٢٥ م غرينتش+٠٥:٤٥", ar1: "٢٣‏/١/١٩٨٩، ١٢:٥٣ م غرينتش+٠٥:٤٥" },
{ timeZone: "Asia/Kathmandu", timeZoneName: "short", en0: "12/7/2021, GMT+5:45", en1: "1/23/1989, GMT+5:45", ar0: "٧/١٢‏/٢٠٢١، غرينتش+٥:٤٥", ar1: "٢٣‏/١/١٩٨٩، غرينتش+٥:٤٥" },
{ timeZone: "Asia/Kathmandu", timeZoneName: "long", en0: "12/7/2021, Nepal Time", en1: "1/23/1989, Nepal Time", ar0: "٧/١٢‏/٢٠٢١، توقيت نيبال", ar1: "٢٣‏/١/١٩٨٩، توقيت نيبال" },
{ timeZone: "Asia/Kathmandu", timeZoneName: "shortOffset", en0: "12/7/2021, GMT+5:45", en1: "1/23/1989, GMT+5:45", ar0: "٧/١٢‏/٢٠٢١، غرينتش+٥:٤٥", ar1: "٢٣‏/١/١٩٨٩، غرينتش+٥:٤٥" },
{ timeZone: "Asia/Kathmandu", timeZoneName: "longOffset", en0: "12/7/2021, GMT+05:45", en1: "1/23/1989, GMT+05:45", ar0: "٧/١٢‏/٢٠٢١، غرينتش+٠٥:٤٥", ar1: "٢٣‏/١/١٩٨٩، غرينتش+٠٥:٤٥" },
{ timeZone: "Asia/Kathmandu", timeZoneName: "shortGeneric", en0: "12/7/2021, Nepal Time", en1: "1/23/1989, Nepal Time", ar0: "٧/١٢‏/٢٠٢١، توقيت نيبال", ar1: "٢٣‏/١/١٩٨٩، توقيت نيبال" },
{ timeZone: "Asia/Kathmandu", timeZoneName: "longGeneric", en0: "12/7/2021, Nepal Time", en1: "1/23/1989, Nepal Time", ar0: "٧/١٢‏/٢٠٢١، توقيت نيبال", ar1: "٢٣‏/١/١٩٨٩، توقيت نيبال" },
{ timeZone: "+04:15", timeZoneName: "short", en0: "12/7/2021, 9:55\u202fPM +04:15", en1: "1/23/1989, 11:23\u202fAM +04:15", ar0: "٧/١٢‏/٢٠٢١، ٩:٥٥ م +04:15", ar1: "٢٣‏/١/١٩٨٩، ١١:٢٣ ص +04:15" },
{ timeZone: "+04:15", timeZoneName: "long", en0: "12/7/2021, 9:55\u202fPM +04:15", en1: "1/23/1989, 11:23\u202fAM +04:15", ar0: "٧/١٢‏/٢٠٢١، ٩:٥٥ م +04:15", ar1: "٢٣‏/١/١٩٨٩، ١١:٢٣ ص +04:15" },
{ timeZone: "+04:15", timeZoneName: "shortOffset", en0: "12/7/2021, 9:55\u202fPM +04:15", en1: "1/23/1989, 11:23\u202fAM +04:15", ar0: "٧/١٢‏/٢٠٢١، ٩:٥٥ م +04:15", ar1: "٢٣‏/١/١٩٨٩، ١١:٢٣ ص +04:15" },
{ timeZone: "+04:15", timeZoneName: "longOffset", en0: "12/7/2021, 9:55\u202fPM +04:15", en1: "1/23/1989, 11:23\u202fAM +04:15", ar0: "٧/١٢‏/٢٠٢١، ٩:٥٥ م +04:15", ar1: "٢٣‏/١/١٩٨٩، ١١:٢٣ ص +04:15" },
{ timeZone: "+04:15", timeZoneName: "shortGeneric", en0: "12/7/2021, 9:55\u202fPM +04:15", en1: "1/23/1989, 11:23\u202fAM +04:15", ar0: "٧/١٢‏/٢٠٢١، ٩:٥٥ م +04:15", ar1: "٢٣‏/١/١٩٨٩، ١١:٢٣ ص +04:15" },
{ timeZone: "+04:15", timeZoneName: "longGeneric", en0: "12/7/2021, 9:55\u202fPM +04:15", en1: "1/23/1989, 11:23\u202fAM +04:15", ar0: "٧/١٢‏/٢٠٢١، ٩:٥٥ م +04:15", ar1: "٢٣‏/١/١٩٨٩، ١١:٢٣ ص +04:15" },
{ timeZone: "+04:15", timeZoneName: "short", en0: "12/7/2021, GMT+4:15", en1: "1/23/1989, GMT+4:15", ar0: "٧/١٢‏/٢٠٢١، غرينتش+٤:١٥", ar1: "٢٣‏/١/١٩٨٩، غرينتش+٤:١٥" },
{ timeZone: "+04:15", timeZoneName: "long", en0: "12/7/2021, GMT+04:15", en1: "1/23/1989, GMT+04:15", ar0: "٧/١٢‏/٢٠٢١، غرينتش+٠٤:١٥", ar1: "٢٣‏/١/١٩٨٩، غرينتش+٠٤:١٥" },
{ timeZone: "+04:15", timeZoneName: "shortOffset", en0: "12/7/2021, GMT+4:15", en1: "1/23/1989, GMT+4:15", ar0: "٧/١٢‏/٢٠٢١، غرينتش+٤:١٥", ar1: "٢٣‏/١/١٩٨٩، غرينتش+٤:١٥" },
{ timeZone: "+04:15", timeZoneName: "longOffset", en0: "12/7/2021, GMT+04:15", en1: "1/23/1989, GMT+04:15", ar0: "٧/١٢‏/٢٠٢١، غرينتش+٠٤:١٥", ar1: "٢٣‏/١/١٩٨٩، غرينتش+٠٤:١٥" },
{ timeZone: "+04:15", timeZoneName: "shortGeneric", en0: "12/7/2021, GMT+4:15", en1: "1/23/1989, GMT+4:15", ar0: "٧/١٢‏/٢٠٢١، غرينتش+٤:١٥", ar1: "٢٣‏/١/١٩٨٩، غرينتش+٤:١٥" },
{ timeZone: "+04:15", timeZoneName: "longGeneric", en0: "12/7/2021, GMT+04:15", en1: "1/23/1989, GMT+04:15", ar0: "٧/١٢‏/٢٠٢١، غرينتش+٠٤:١٥", ar1: "٢٣‏/١/١٩٨٩، غرينتش+٠٤:١٥" },
];
test("all", () => {

View File

@ -58,8 +58,8 @@ describe("equal dates are squashed", () => {
day: "2-digit",
timeZone: "UTC",
});
expect(ja.formatRange(d0, d0)).toBe("1989/1月/23");
expect(ja.formatRange(d1, d1)).toBe("2021/12月/07");
expect(ja.formatRange(d0, d0)).toBe("1989年1月23日");
expect(ja.formatRange(d1, d1)).toBe("2021年12月07日");
});
test("with time fields", () => {
@ -104,8 +104,8 @@ describe("equal dates are squashed", () => {
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");
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", () => {
@ -130,7 +130,7 @@ describe("equal dates are squashed", () => {
describe("dateStyle", () => {
// prettier-ignore
const data = [
{ date: "full", en: "Monday, January 23, 1989\u2009\u2009Tuesday, December 7, 2021", ja: "1989年1月23日月曜日2021年12月7日火曜日" },
{ date: "full", en: "Monday, January 23, 1989\u2009\u2009Tuesday, December 7, 2021", ja: "1989/01/23(月曜日)2021/12/07(火曜日)" },
{ date: "long", en: "January 23, 1989\u2009\u2009December 7, 2021", ja: "1989/01/232021/12/07" },
{ date: "medium", en: "Jan 23, 1989\u2009\u2009Dec 7, 2021", ja: "1989/01/232021/12/07" },
{ date: "short", en: "1/23/89\u2009\u200912/7/21", ja: "1989/01/232021/12/07" },
@ -159,19 +159,17 @@ describe("dateStyle", () => {
);
const ja = new Intl.DateTimeFormat("ja", { dateStyle: "full", timeZone: "UTC" });
expect(ja.formatRange(d1, d0)).toBe("2021年12月7日火曜日1989年1月23日月曜日");
expect(ja.formatRange(d1, d0)).toBe("2021/12/07(火曜日)1989/01/23(月曜日)");
});
});
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\u202fAM Coordinated Universal Time\u2009\u20095:40:50\u202fPM Coordinated Universal Time", ja: "7時08分09秒 協定世界時17時40分50秒 協定世界時" },
{ time: "long", en: "7:08:09\u202fAM UTC\u2009\u20095:40:50\u202fPM UTC", ja: "7:08:09 UTC17:40:50 UTC" },
{ time: "medium", en: "7:08:09\u202fAM\u2009\u20095:40:50\u202fPM", ja: "7:08:0917:40:50" },
{ time: "short", en: "7:08\u202fAM\u2009\u20095:40\u202fPM", ja: "7:0817:40" },
{ time: "full", en: "1/23/1989, 7:08:09\u202fAM Coordinated Universal Time\u2009\u200912/7/2021, 5:40:50\u202fPM Coordinated Universal Time", ja: "1989/1/23 7時08分09秒 協定世界時2021/12/7 17時40分50秒 協定世界時" },
{ time: "long", en: "1/23/1989, 7:08:09\u202fAM UTC\u2009\u200912/7/2021, 5:40:50\u202fPM UTC", ja: "1989/1/23 7:08:09 UTC2021/12/7 17:40:50 UTC" },
{ time: "medium", en: "1/23/1989, 7:08:09\u202fAM\u2009\u200912/7/2021, 5:40:50\u202fPM", ja: "1989/1/23 7:08:092021/12/7 17:40:50" },
{ time: "short", en: "1/23/1989, 7:08\u202fAM\u2009\u200912/7/2021, 5:40\u202fPM", ja: "1989/1/23 7:082021/12/7 17:40" },
];
test("all", () => {
@ -188,14 +186,14 @@ describe("timeStyle", () => {
describe("dateStyle + timeStyle", () => {
// prettier-ignore
const data = [
{ date: "full", time: "full", en: "Monday, January 23, 1989 at 7:08:09\u202fAM Coordinated Universal Time\u2009\u2009Tuesday, December 7, 2021 at 5:40:50\u202fPM 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\u202fAM UTC\u2009\u2009Tuesday, December 7, 2021 at 5:40:50\u202fPM UTC", ja: "1989年1月23日月曜日 7:08:09 UTC2021年12月7日火曜日 17:40:50 UTC" },
{ date: "full", time: "medium", en: "Monday, January 23, 1989 at 7:08:09\u202fAM\u2009\u2009Tuesday, December 7, 2021 at 5:40:50\u202fPM", ja: "1989年1月23日月曜日 7:08:092021年12月7日火曜日 17:40:50" },
{ date: "full", time: "short", en: "Monday, January 23, 1989 at 7:08\u202fAM\u2009\u2009Tuesday, December 7, 2021 at 5:40\u202fPM", ja: "1989年1月23日月曜日 7:082021年12月7日火曜日 17:40" },
{ date: "long", time: "full", en: "January 23, 1989 at 7:08:09\u202fAM Coordinated Universal Time\u2009\u2009December 7, 2021 at 5:40:50\u202fPM 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\u202fAM UTC\u2009\u2009December 7, 2021 at 5:40:50\u202fPM UTC", ja: "1989年1月23日 7:08:09 UTC2021年12月7日 17:40:50 UTC" },
{ date: "long", time: "medium", en: "January 23, 1989 at 7:08:09\u202fAM\u2009\u2009December 7, 2021 at 5:40:50\u202fPM", ja: "1989年1月23日 7:08:092021年12月7日 17:40:50" },
{ date: "long", time: "short", en: "January 23, 1989 at 7:08\u202fAM\u2009\u2009December 7, 2021 at 5:40\u202fPM", ja: "1989年1月23日 7:082021年12月7日 17:40" },
{ date: "full", time: "full", en: "Monday, January 23, 1989 at 7:08:09\u202fAM Coordinated Universal Time\u2009\u2009Tuesday, December 7, 2021 at 5:40:50\u202fPM 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\u202fAM UTC\u2009\u2009Tuesday, December 7, 2021 at 5:40:50\u202fPM UTC", ja: "1989/1/23月曜日 7:08:09 UTC2021/12/7火曜日 17:40:50 UTC" },
{ date: "full", time: "medium", en: "Monday, January 23, 1989 at 7:08:09\u202fAM\u2009\u2009Tuesday, December 7, 2021 at 5:40:50\u202fPM", ja: "1989/1/23月曜日 7:08:092021/12/7火曜日 17:40:50" },
{ date: "full", time: "short", en: "Monday, January 23, 1989 at 7:08\u202fAM\u2009\u2009Tuesday, December 7, 2021 at 5:40\u202fPM", ja: "1989/1/23月曜日 7:082021/12/7火曜日 17:40" },
{ date: "long", time: "full", en: "January 23, 1989 at 7:08:09\u202fAM Coordinated Universal Time\u2009\u2009December 7, 2021 at 5:40:50\u202fPM 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\u202fAM UTC\u2009\u2009December 7, 2021 at 5:40:50\u202fPM UTC", ja: "1989/1/23 7:08:09 UTC2021/12/7 17:40:50 UTC" },
{ date: "long", time: "medium", en: "January 23, 1989 at 7:08:09\u202fAM\u2009\u2009December 7, 2021 at 5:40:50\u202fPM", ja: "1989/1/23 7:08:092021/12/7 17:40:50" },
{ date: "long", time: "short", en: "January 23, 1989 at 7:08\u202fAM\u2009\u2009December 7, 2021 at 5:40\u202fPM", ja: "1989/1/23 7:082021/12/7 17:40" },
{ date: "medium", time: "full", en: "Jan 23, 1989, 7:08:09\u202fAM Coordinated Universal Time\u2009\u2009Dec 7, 2021, 5:40:50\u202fPM 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\u202fAM UTC\u2009\u2009Dec 7, 2021, 5:40:50\u202fPM UTC", ja: "1989/01/23 7:08:09 UTC2021/12/07 17:40:50 UTC" },
{ date: "medium", time: "medium", en: "Jan 23, 1989, 7:08:09\u202fAM\u2009\u2009Dec 7, 2021, 5:40:50\u202fPM", ja: "1989/01/23 7:08:092021/12/07 17:40:50" },

View File

@ -65,10 +65,11 @@ describe("equal dates are squashed", () => {
});
expect(ja.formatRangeToParts(d0, d0)).toEqual([
{ type: "year", value: "1989", source: "shared" },
{ type: "literal", value: "/", source: "shared" },
{ type: "month", value: "1", source: "shared" },
{ type: "literal", value: "/", source: "shared" },
{ type: "literal", value: "", source: "shared" },
{ type: "month", value: "1", source: "shared" },
{ type: "literal", value: "", source: "shared" },
{ type: "day", value: "23", source: "shared" },
{ type: "literal", value: "日", source: "shared" },
]);
});
@ -141,11 +142,11 @@ describe("equal dates are squashed", () => {
});
expect(ja.formatRangeToParts(d0, d0)).toEqual([
{ type: "year", value: "1989", source: "shared" },
{ type: "literal", value: "/", source: "shared" },
{ type: "month", value: "1", source: "shared" },
{ type: "literal", value: "/", source: "shared" },
{ type: "literal", value: "", source: "shared" },
{ type: "month", value: "1", source: "shared" },
{ type: "literal", value: "", source: "shared" },
{ type: "day", value: "23", source: "shared" },
{ type: "literal", value: " ", source: "shared" },
{ type: "literal", value: " ", source: "shared" },
{ type: "hour", value: "7", source: "shared" },
{ type: "literal", value: ":", source: "shared" },
{ type: "minute", value: "08", source: "shared" },
@ -212,7 +213,7 @@ describe("dateStyle", () => {
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: ", ", source: "startRange" },
{ type: "year", value: "1989", source: "startRange" },
{ type: "literal", value: "\u2009\u2009", source: "shared" },
{ type: "literal", value: "", source: "shared" },
{ type: "weekday", value: "Tuesday", source: "endRange" },
{ type: "literal", value: ", ", source: "endRange" },
{ type: "month", value: "December", source: "endRange" },
@ -225,20 +226,21 @@ describe("dateStyle", () => {
const ja = new Intl.DateTimeFormat("ja", { dateStyle: "full", timeZone: "UTC" });
expect(ja.formatRangeToParts(d0, d1)).toEqual([
{ type: "year", value: "1989", source: "startRange" },
{ type: "literal", value: "", source: "startRange" },
{ type: "month", value: "1", source: "startRange" },
{ type: "literal", value: "", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "month", value: "01", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: "", source: "startRange" },
{ type: "literal", value: "(", source: "startRange" },
{ type: "weekday", value: "月曜日", source: "startRange" },
{ type: "literal", value: "", source: "shared" },
{ type: "literal", value: ")", source: "shared" },
{ type: "year", value: "2021", source: "endRange" },
{ type: "literal", value: "", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "month", value: "12", source: "endRange" },
{ type: "literal", value: "", source: "endRange" },
{ type: "day", value: "7", source: "endRange" },
{ type: "literal", value: "", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "07", source: "endRange" },
{ type: "literal", value: "(", source: "endRange" },
{ type: "weekday", value: "火曜日", source: "endRange" },
{ type: "literal", value: ")", source: "shared" },
]);
});
@ -361,20 +363,21 @@ describe("dateStyle", () => {
const ja = new Intl.DateTimeFormat("ja", { dateStyle: "full", timeZone: "UTC" });
expect(ja.formatRangeToParts(d1, d0)).toEqual([
{ type: "year", value: "2021", source: "startRange" },
{ type: "literal", value: "", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "month", value: "12", source: "startRange" },
{ type: "literal", value: "", source: "startRange" },
{ type: "day", value: "7", source: "startRange" },
{ type: "literal", value: "", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "day", value: "07", source: "startRange" },
{ type: "literal", value: "(", source: "startRange" },
{ type: "weekday", value: "火曜日", source: "startRange" },
{ type: "literal", value: "", source: "shared" },
{ type: "literal", value: ")", source: "shared" },
{ type: "year", value: "1989", source: "endRange" },
{ type: "literal", value: "", source: "endRange" },
{ type: "month", value: "1", source: "endRange" },
{ type: "literal", value: "", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "month", value: "01", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "23", source: "endRange" },
{ type: "literal", value: "", source: "endRange" },
{ type: "literal", value: "(", source: "endRange" },
{ type: "weekday", value: "月曜日", source: "endRange" },
{ type: "literal", value: ")", source: "shared" },
]);
});
});
@ -385,6 +388,12 @@ describe("timeStyle", () => {
test("full", () => {
const en = new Intl.DateTimeFormat("en", { timeStyle: "full", timeZone: "UTC" });
expect(en.formatRangeToParts(d0, d1)).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: "7", source: "startRange" },
{ type: "literal", value: ":", source: "startRange" },
{ type: "minute", value: "08", source: "startRange" },
@ -395,6 +404,12 @@ describe("timeStyle", () => {
{ type: "literal", value: " ", source: "startRange" },
{ type: "timeZoneName", value: "Coordinated Universal Time", source: "startRange" },
{ type: "literal", value: "\u2009\u2009", source: "shared" },
{ type: "month", value: "12", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "7", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "year", value: "2021", source: "endRange" },
{ type: "literal", value: ", ", source: "endRange" },
{ type: "hour", value: "5", source: "endRange" },
{ type: "literal", value: ":", source: "endRange" },
{ type: "minute", value: "40", source: "endRange" },
@ -408,6 +423,12 @@ describe("timeStyle", () => {
const ja = new Intl.DateTimeFormat("ja", { timeStyle: "full", timeZone: "UTC" });
expect(ja.formatRangeToParts(d0, d1)).toEqual([
{ type: "year", value: "1989", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "month", value: "1", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: " ", source: "startRange" },
{ type: "hour", value: "7", source: "startRange" },
{ type: "literal", value: "時", source: "startRange" },
{ type: "minute", value: "08", source: "startRange" },
@ -416,6 +437,12 @@ describe("timeStyle", () => {
{ type: "literal", value: "秒 ", source: "startRange" },
{ type: "timeZoneName", value: "協定世界時", source: "startRange" },
{ type: "literal", value: "", source: "shared" },
{ type: "year", value: "2021", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "month", value: "12", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "7", source: "endRange" },
{ type: "literal", value: " ", source: "endRange" },
{ type: "hour", value: "17", source: "endRange" },
{ type: "literal", value: "時", source: "endRange" },
{ type: "minute", value: "40", source: "endRange" },
@ -429,6 +456,12 @@ describe("timeStyle", () => {
test("long", () => {
const en = new Intl.DateTimeFormat("en", { timeStyle: "long", timeZone: "UTC" });
expect(en.formatRangeToParts(d0, d1)).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: "7", source: "startRange" },
{ type: "literal", value: ":", source: "startRange" },
{ type: "minute", value: "08", source: "startRange" },
@ -439,6 +472,12 @@ describe("timeStyle", () => {
{ type: "literal", value: " ", source: "startRange" },
{ type: "timeZoneName", value: "UTC", source: "startRange" },
{ type: "literal", value: "\u2009\u2009", source: "shared" },
{ type: "month", value: "12", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "7", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "year", value: "2021", source: "endRange" },
{ type: "literal", value: ", ", source: "endRange" },
{ type: "hour", value: "5", source: "endRange" },
{ type: "literal", value: ":", source: "endRange" },
{ type: "minute", value: "40", source: "endRange" },
@ -452,6 +491,12 @@ describe("timeStyle", () => {
const ja = new Intl.DateTimeFormat("ja", { timeStyle: "long", timeZone: "UTC" });
expect(ja.formatRangeToParts(d0, d1)).toEqual([
{ type: "year", value: "1989", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "month", value: "1", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: " ", source: "startRange" },
{ type: "hour", value: "7", source: "startRange" },
{ type: "literal", value: ":", source: "startRange" },
{ type: "minute", value: "08", source: "startRange" },
@ -460,6 +505,12 @@ describe("timeStyle", () => {
{ type: "literal", value: " ", source: "startRange" },
{ type: "timeZoneName", value: "UTC", source: "startRange" },
{ type: "literal", value: "", source: "shared" },
{ type: "year", value: "2021", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "month", value: "12", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "7", source: "endRange" },
{ type: "literal", value: " ", source: "endRange" },
{ type: "hour", value: "17", source: "endRange" },
{ type: "literal", value: ":", source: "endRange" },
{ type: "minute", value: "40", source: "endRange" },
@ -473,6 +524,12 @@ describe("timeStyle", () => {
test("medium", () => {
const en = new Intl.DateTimeFormat("en", { timeStyle: "medium", timeZone: "UTC" });
expect(en.formatRangeToParts(d0, d1)).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: "7", source: "startRange" },
{ type: "literal", value: ":", source: "startRange" },
{ type: "minute", value: "08", source: "startRange" },
@ -481,6 +538,12 @@ describe("timeStyle", () => {
{ type: "literal", value: "\u202f", source: "startRange" },
{ type: "dayPeriod", value: "AM", source: "startRange" },
{ type: "literal", value: "\u2009\u2009", source: "shared" },
{ type: "month", value: "12", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "7", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "year", value: "2021", source: "endRange" },
{ type: "literal", value: ", ", source: "endRange" },
{ type: "hour", value: "5", source: "endRange" },
{ type: "literal", value: ":", source: "endRange" },
{ type: "minute", value: "40", source: "endRange" },
@ -492,12 +555,24 @@ describe("timeStyle", () => {
const ja = new Intl.DateTimeFormat("ja", { timeStyle: "medium", timeZone: "UTC" });
expect(ja.formatRangeToParts(d0, d1)).toEqual([
{ type: "year", value: "1989", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "month", value: "1", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: " ", source: "startRange" },
{ type: "hour", value: "7", source: "startRange" },
{ type: "literal", value: ":", source: "startRange" },
{ type: "minute", value: "08", source: "startRange" },
{ type: "literal", value: ":", source: "startRange" },
{ type: "second", value: "09", source: "startRange" },
{ type: "literal", value: "", source: "shared" },
{ type: "year", value: "2021", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "month", value: "12", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "7", source: "endRange" },
{ type: "literal", value: " ", source: "endRange" },
{ type: "hour", value: "17", source: "endRange" },
{ type: "literal", value: ":", source: "endRange" },
{ type: "minute", value: "40", source: "endRange" },
@ -509,12 +584,24 @@ describe("timeStyle", () => {
test("short", () => {
const en = new Intl.DateTimeFormat("en", { timeStyle: "short", timeZone: "UTC" });
expect(en.formatRangeToParts(d0, d1)).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: "7", source: "startRange" },
{ type: "literal", value: ":", source: "startRange" },
{ type: "minute", value: "08", source: "startRange" },
{ type: "literal", value: "\u202f", source: "startRange" },
{ type: "dayPeriod", value: "AM", source: "startRange" },
{ type: "literal", value: "\u2009\u2009", source: "shared" },
{ type: "month", value: "12", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "7", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "year", value: "2021", source: "endRange" },
{ type: "literal", value: ", ", source: "endRange" },
{ type: "hour", value: "5", source: "endRange" },
{ type: "literal", value: ":", source: "endRange" },
{ type: "minute", value: "40", source: "endRange" },
@ -524,10 +611,22 @@ describe("timeStyle", () => {
const ja = new Intl.DateTimeFormat("ja", { timeStyle: "short", timeZone: "UTC" });
expect(ja.formatRangeToParts(d0, d1)).toEqual([
{ type: "year", value: "1989", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "month", value: "1", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: " ", source: "startRange" },
{ type: "hour", value: "7", source: "startRange" },
{ type: "literal", value: ":", source: "startRange" },
{ type: "minute", value: "08", source: "startRange" },
{ type: "literal", value: "", source: "shared" },
{ type: "year", value: "2021", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "month", value: "12", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "7", source: "endRange" },
{ type: "literal", value: " ", source: "endRange" },
{ type: "hour", value: "17", source: "endRange" },
{ type: "literal", value: ":", source: "endRange" },
{ type: "minute", value: "40", source: "endRange" },

View File

@ -211,14 +211,14 @@ describe("correct behavior", () => {
test("minute", () => {
["2-digit", "numeric"].forEach(minute => {
const en = new Intl.DateTimeFormat("en", { minute: minute });
expect(en.resolvedOptions().minute).toBe("2-digit");
expect(en.resolvedOptions().minute).toBe(minute);
});
});
test("second", () => {
["2-digit", "numeric"].forEach(second => {
const en = new Intl.DateTimeFormat("en", { second: second });
expect(en.resolvedOptions().second).toBe("2-digit");
expect(en.resolvedOptions().second).toBe(second);
});
});

View File

@ -1,18 +1,75 @@
/*
* Copyright (c) 2021-2023, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2021-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#define AK_DONT_REPLACE_STD
#include <AK/AllOf.h>
#include <AK/Array.h>
#include <AK/GenericLexer.h>
#include <AK/StringBuilder.h>
#include <AK/TypeCasts.h>
#include <LibLocale/DateTimeFormat.h>
#include <LibLocale/ICU.h>
#include <LibLocale/Locale.h>
#include <LibLocale/NumberFormat.h>
#include <stdlib.h>
#include <unicode/calendar.h>
#include <unicode/datefmt.h>
#include <unicode/dtitvfmt.h>
#include <unicode/dtptngen.h>
#include <unicode/gregocal.h>
#include <unicode/smpdtfmt.h>
#include <unicode/timezone.h>
namespace Locale {
DateTimeStyle date_time_style_from_string(StringView style)
{
if (style == "full"sv)
return DateTimeStyle::Full;
if (style == "long"sv)
return DateTimeStyle::Long;
if (style == "medium"sv)
return DateTimeStyle::Medium;
if (style == "short"sv)
return DateTimeStyle::Short;
VERIFY_NOT_REACHED();
}
StringView date_time_style_to_string(DateTimeStyle style)
{
switch (style) {
case DateTimeStyle::Full:
return "full"sv;
case DateTimeStyle::Long:
return "long"sv;
case DateTimeStyle::Medium:
return "medium"sv;
case DateTimeStyle::Short:
return "short"sv;
}
VERIFY_NOT_REACHED();
}
static constexpr icu::DateFormat::EStyle icu_date_time_style(DateTimeStyle style)
{
switch (style) {
case DateTimeStyle::Full:
return icu::DateFormat::EStyle::kFull;
case DateTimeStyle::Long:
return icu::DateFormat::EStyle::kLong;
case DateTimeStyle::Medium:
return icu::DateFormat::EStyle::kMedium;
case DateTimeStyle::Short:
return icu::DateFormat::EStyle::kShort;
}
VERIFY_NOT_REACHED();
}
HourCycle hour_cycle_from_string(StringView hour_cycle)
{
if (hour_cycle == "h11"sv)
@ -37,9 +94,28 @@ StringView hour_cycle_to_string(HourCycle hour_cycle)
return "h23"sv;
case HourCycle::H24:
return "h24"sv;
default:
VERIFY_NOT_REACHED();
}
VERIFY_NOT_REACHED();
}
static constexpr char icu_hour_cycle(Optional<HourCycle> const& hour_cycle, Optional<bool> const& hour12)
{
if (hour12.has_value())
return *hour12 ? 'h' : 'H';
if (!hour_cycle.has_value())
return 'j';
switch (*hour_cycle) {
case HourCycle::H11:
return 'K';
case HourCycle::H12:
return 'h';
case HourCycle::H23:
return 'H';
case HourCycle::H24:
return 'k';
}
VERIFY_NOT_REACHED();
}
CalendarPatternStyle calendar_pattern_style_from_string(StringView style)
@ -86,9 +162,314 @@ StringView calendar_pattern_style_to_string(CalendarPatternStyle style)
return "shortGeneric"sv;
case CalendarPatternStyle::LongGeneric:
return "longGeneric"sv;
default:
VERIFY_NOT_REACHED();
}
VERIFY_NOT_REACHED();
}
// https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
String CalendarPattern::to_pattern() const
{
// What we refer to as Narrow, Short, and Long, TR-35 refers to as Narrow, Abbreviated, and Wide.
StringBuilder builder;
if (era.has_value()) {
switch (*era) {
case CalendarPatternStyle::Narrow:
builder.append("GGGGG"sv);
break;
case CalendarPatternStyle::Short:
builder.append("G"sv);
break;
case CalendarPatternStyle::Long:
builder.append("GGGG"sv);
break;
default:
break;
}
}
if (year.has_value()) {
switch (*year) {
case CalendarPatternStyle::Numeric:
builder.append("y"sv);
break;
case CalendarPatternStyle::TwoDigit:
builder.append("yy"sv);
break;
default:
break;
}
}
if (month.has_value()) {
switch (*month) {
case CalendarPatternStyle::Numeric:
builder.append("M"sv);
break;
case CalendarPatternStyle::TwoDigit:
builder.append("MM"sv);
break;
case CalendarPatternStyle::Narrow:
builder.append("MMMMM"sv);
break;
case CalendarPatternStyle::Short:
builder.append("MMM"sv);
break;
case CalendarPatternStyle::Long:
builder.append("MMMM"sv);
break;
default:
break;
}
}
if (weekday.has_value()) {
switch (*weekday) {
case CalendarPatternStyle::Narrow:
builder.append("EEEEE"sv);
break;
case CalendarPatternStyle::Short:
builder.append("E"sv);
break;
case CalendarPatternStyle::Long:
builder.append("EEEE"sv);
break;
default:
break;
}
}
if (day.has_value()) {
switch (*day) {
case CalendarPatternStyle::Numeric:
builder.append("d"sv);
break;
case CalendarPatternStyle::TwoDigit:
builder.append("dd"sv);
break;
default:
break;
}
}
if (day_period.has_value()) {
switch (*day_period) {
case CalendarPatternStyle::Narrow:
builder.append("BBBBB"sv);
break;
case CalendarPatternStyle::Short:
builder.append("B"sv);
break;
case CalendarPatternStyle::Long:
builder.append("BBBB"sv);
break;
default:
break;
}
}
if (hour.has_value()) {
auto hour_cycle_symbol = icu_hour_cycle(hour_cycle, hour12);
switch (*hour) {
case CalendarPatternStyle::Numeric:
builder.append(hour_cycle_symbol);
break;
case CalendarPatternStyle::TwoDigit:
builder.append_repeated(hour_cycle_symbol, 2);
break;
default:
break;
}
}
if (minute.has_value()) {
switch (*minute) {
case CalendarPatternStyle::Numeric:
builder.append("m"sv);
break;
case CalendarPatternStyle::TwoDigit:
builder.append("mm"sv);
break;
default:
break;
}
}
if (second.has_value()) {
switch (*second) {
case CalendarPatternStyle::Numeric:
builder.append("s"sv);
break;
case CalendarPatternStyle::TwoDigit:
builder.append("ss"sv);
break;
default:
break;
}
}
if (fractional_second_digits.has_value()) {
for (u8 i = 0; i < *fractional_second_digits; ++i)
builder.append("S"sv);
}
if (time_zone_name.has_value()) {
switch (*time_zone_name) {
case CalendarPatternStyle::Short:
builder.append("z"sv);
break;
case CalendarPatternStyle::Long:
builder.append("zzzz"sv);
break;
case CalendarPatternStyle::ShortOffset:
builder.append("O"sv);
break;
case CalendarPatternStyle::LongOffset:
builder.append("OOOO"sv);
break;
case CalendarPatternStyle::ShortGeneric:
builder.append("v"sv);
break;
case CalendarPatternStyle::LongGeneric:
builder.append("vvvv"sv);
break;
default:
break;
}
}
return MUST(builder.to_string());
}
// https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
CalendarPattern CalendarPattern::create_from_pattern(StringView pattern)
{
GenericLexer lexer { pattern };
CalendarPattern format {};
while (!lexer.is_eof()) {
if (lexer.next_is(is_quote)) {
lexer.consume_quoted_string();
continue;
}
auto starting_char = lexer.peek();
auto segment = lexer.consume_while([&](char ch) { return ch == starting_char; });
// Era
if (all_of(segment, is_any_of("G"sv))) {
if (segment.length() <= 3)
format.era = CalendarPatternStyle::Short;
else if (segment.length() == 4)
format.era = CalendarPatternStyle::Long;
else
format.era = CalendarPatternStyle::Narrow;
}
// Year
else if (all_of(segment, is_any_of("yYuUr"sv))) {
if (segment.length() == 2)
format.year = CalendarPatternStyle::TwoDigit;
else
format.year = CalendarPatternStyle::Numeric;
}
// Month
else if (all_of(segment, is_any_of("ML"sv))) {
if (segment.length() == 1)
format.month = CalendarPatternStyle::Numeric;
else if (segment.length() == 2)
format.month = CalendarPatternStyle::TwoDigit;
else if (segment.length() == 3)
format.month = CalendarPatternStyle::Short;
else if (segment.length() == 4)
format.month = CalendarPatternStyle::Long;
else if (segment.length() == 5)
format.month = CalendarPatternStyle::Narrow;
}
// Weekday
else if (all_of(segment, is_any_of("ecE"sv))) {
if (segment.length() == 4)
format.weekday = CalendarPatternStyle::Long;
else if (segment.length() == 5)
format.weekday = CalendarPatternStyle::Narrow;
else
format.weekday = CalendarPatternStyle::Short;
}
// Day
else if (all_of(segment, is_any_of("d"sv))) {
if (segment.length() == 1)
format.day = CalendarPatternStyle::Numeric;
else
format.day = CalendarPatternStyle::TwoDigit;
} else if (all_of(segment, is_any_of("DFg"sv))) {
format.day = CalendarPatternStyle::Numeric;
}
// Day period
else if (all_of(segment, is_any_of("B"sv))) {
if (segment.length() == 4)
format.day_period = CalendarPatternStyle::Long;
else if (segment.length() == 5)
format.day_period = CalendarPatternStyle::Narrow;
else
format.day_period = CalendarPatternStyle::Short;
}
// Hour
else if (all_of(segment, is_any_of("hHKk"sv))) {
switch (starting_char) {
case 'K':
format.hour_cycle = HourCycle::H11;
break;
case 'h':
format.hour_cycle = HourCycle::H12;
break;
case 'H':
format.hour_cycle = HourCycle::H23;
break;
case 'k':
format.hour_cycle = HourCycle::H24;
break;
}
if (segment.length() == 1)
format.hour = CalendarPatternStyle::Numeric;
else
format.hour = CalendarPatternStyle::TwoDigit;
}
// Minute
else if (all_of(segment, is_any_of("m"sv))) {
if (segment.length() == 1)
format.minute = CalendarPatternStyle::Numeric;
else
format.minute = CalendarPatternStyle::TwoDigit;
}
// Second
else if (all_of(segment, is_any_of("s"sv))) {
if (segment.length() == 1)
format.second = CalendarPatternStyle::Numeric;
else
format.second = CalendarPatternStyle::TwoDigit;
} else if (all_of(segment, is_any_of("S"sv))) {
format.fractional_second_digits = static_cast<u8>(segment.length());
}
// Zone
else if (all_of(segment, is_any_of("zV"sv))) {
if (segment.length() < 4)
format.time_zone_name = CalendarPatternStyle::Short;
else
format.time_zone_name = CalendarPatternStyle::Long;
} else if (all_of(segment, is_any_of("ZOXx"sv))) {
if (segment.length() < 4)
format.time_zone_name = CalendarPatternStyle::ShortOffset;
else
format.time_zone_name = CalendarPatternStyle::LongOffset;
} else if (all_of(segment, is_any_of("v"sv))) {
if (segment.length() < 4)
format.time_zone_name = CalendarPatternStyle::ShortGeneric;
else
format.time_zone_name = CalendarPatternStyle::LongGeneric;
}
}
return format;
}
Optional<HourCycleRegion> __attribute__((weak)) hour_cycle_region_from_string(StringView) { return {}; }
@ -172,171 +553,406 @@ Optional<Weekday> get_locale_weekend_end(StringView locale)
return find_regional_values_for_locale<Optional<Weekday>>(locale, get_regional_weekend_end);
}
String combine_skeletons(StringView first, StringView second)
{
// https://unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems
constexpr auto field_order = Array {
"G"sv, // Era
"yYuUr"sv, // Year
"ML"sv, // Month
"dDFg"sv, // Day
"Eec"sv, // Weekday
"abB"sv, // Period
"hHKk"sv, // Hour
"m"sv, // Minute
"sSA"sv, // Second
"zZOvVXx"sv, // Zone
};
StringBuilder builder;
auto append_from_skeleton = [&](auto skeleton, auto ch) {
auto first_index = skeleton.find(ch);
if (!first_index.has_value())
return false;
auto last_index = skeleton.find_last(ch);
builder.append(skeleton.substring_view(*first_index, *last_index - *first_index + 1));
return true;
};
for (auto fields : field_order) {
for (auto ch : fields) {
if (append_from_skeleton(first, ch))
break;
if (append_from_skeleton(second, ch))
break;
}
}
return MUST(builder.to_string());
}
Optional<CalendarFormat> __attribute__((weak)) get_calendar_date_format(StringView, StringView) { return {}; }
Optional<CalendarFormat> __attribute__((weak)) get_calendar_time_format(StringView, StringView) { return {}; }
Optional<CalendarFormat> __attribute__((weak)) get_calendar_date_time_format(StringView, StringView) { return {}; }
Optional<CalendarFormat> get_calendar_format(StringView locale, StringView calendar, CalendarFormatType type)
{
switch (type) {
case CalendarFormatType::Date:
return get_calendar_date_format(locale, calendar);
case CalendarFormatType::Time:
return get_calendar_time_format(locale, calendar);
case CalendarFormatType::DateTime:
return get_calendar_date_time_format(locale, calendar);
default:
VERIFY_NOT_REACHED();
}
}
Vector<CalendarPattern> __attribute__((weak)) get_calendar_available_formats(StringView, StringView) { return {}; }
Optional<CalendarRangePattern> __attribute__((weak)) get_calendar_default_range_format(StringView, StringView) { return {}; }
Vector<CalendarRangePattern> __attribute__((weak)) get_calendar_range_formats(StringView, StringView, StringView) { return {}; }
Vector<CalendarRangePattern> __attribute__((weak)) get_calendar_range12_formats(StringView, StringView, StringView) { return {}; }
Optional<StringView> __attribute__((weak)) get_calendar_era_symbol(StringView, StringView, CalendarPatternStyle, Era) { return {}; }
Optional<StringView> __attribute__((weak)) get_calendar_month_symbol(StringView, StringView, CalendarPatternStyle, Month) { return {}; }
Optional<StringView> __attribute__((weak)) get_calendar_weekday_symbol(StringView, StringView, CalendarPatternStyle, Weekday) { return {}; }
Optional<StringView> __attribute__((weak)) get_calendar_day_period_symbol(StringView, StringView, CalendarPatternStyle, DayPeriod) { return {}; }
Optional<StringView> __attribute__((weak)) get_calendar_day_period_symbol_for_hour(StringView, StringView, CalendarPatternStyle, u8) { return {}; }
Optional<StringView> __attribute__((weak)) get_time_zone_name(StringView, StringView, CalendarPatternStyle, TimeZone::InDST) { return {}; }
Optional<TimeZoneFormat> __attribute__((weak)) get_time_zone_format(StringView) { return {}; }
static Optional<String> format_time_zone_offset(StringView locale, CalendarPatternStyle style, i64 offset_seconds)
// ICU does not contain a field enumeration for "literal" partitions. Define a custom field so that we may provide a
// type for those partitions.
static constexpr i32 LITERAL_FIELD = -1;
static constexpr StringView icu_date_time_format_field_to_string(i32 field)
{
auto formats = get_time_zone_format(locale);
if (!formats.has_value())
return {};
switch (field) {
case LITERAL_FIELD:
return "literal"sv;
case UDAT_ERA_FIELD:
return "era"sv;
case UDAT_YEAR_FIELD:
case UDAT_EXTENDED_YEAR_FIELD:
return "year"sv;
case UDAT_YEAR_NAME_FIELD:
return "yearName"sv;
case UDAT_RELATED_YEAR_FIELD:
return "relatedYear"sv;
case UDAT_MONTH_FIELD:
case UDAT_STANDALONE_MONTH_FIELD:
return "month"sv;
case UDAT_DAY_OF_WEEK_FIELD:
case UDAT_DOW_LOCAL_FIELD:
case UDAT_STANDALONE_DAY_FIELD:
return "weekday"sv;
case UDAT_DATE_FIELD:
return "day"sv;
case UDAT_AM_PM_FIELD:
case UDAT_AM_PM_MIDNIGHT_NOON_FIELD:
case UDAT_FLEXIBLE_DAY_PERIOD_FIELD:
return "dayPeriod"sv;
case UDAT_HOUR_OF_DAY1_FIELD:
case UDAT_HOUR_OF_DAY0_FIELD:
case UDAT_HOUR1_FIELD:
case UDAT_HOUR0_FIELD:
return "hour"sv;
case UDAT_MINUTE_FIELD:
return "minute"sv;
case UDAT_SECOND_FIELD:
return "second"sv;
case UDAT_FRACTIONAL_SECOND_FIELD:
return "fractionalSecond"sv;
case UDAT_TIMEZONE_FIELD:
case UDAT_TIMEZONE_RFC_FIELD:
case UDAT_TIMEZONE_GENERIC_FIELD:
case UDAT_TIMEZONE_SPECIAL_FIELD:
case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD:
case UDAT_TIMEZONE_ISO_FIELD:
case UDAT_TIMEZONE_ISO_LOCAL_FIELD:
return "timeZoneName"sv;
default:
return "unknown"sv;
}
}
auto number_system = get_preferred_keyword_value_for_locale(locale, "nu"sv);
if (!number_system.has_value())
return {};
static bool apply_hour_cycle_to_skeleton(icu::UnicodeString& skeleton, Optional<HourCycle> const& hour_cycle, Optional<bool> const& hour12)
{
auto hour_cycle_symbol = icu_hour_cycle(hour_cycle, hour12);
if (hour_cycle_symbol == 'j')
return false;
if (offset_seconds == 0)
return MUST(String::from_utf8(formats->gmt_zero_format));
bool changed_hour_cycle = false;
bool inside_quote = false;
auto sign = offset_seconds > 0 ? formats->symbol_ahead_sign : formats->symbol_behind_sign;
auto separator = offset_seconds > 0 ? formats->symbol_ahead_separator : formats->symbol_behind_separator;
offset_seconds = llabs(offset_seconds);
for (i32 i = 0; i < skeleton.length(); ++i) {
switch (skeleton[i]) {
case '\'':
inside_quote = !inside_quote;
break;
auto offset_hours = offset_seconds / 3'600;
offset_seconds %= 3'600;
case 'h':
case 'H':
case 'k':
case 'K':
if (!inside_quote && static_cast<char>(skeleton[i]) != hour_cycle_symbol) {
skeleton.setCharAt(i, hour_cycle_symbol);
changed_hour_cycle = true;
}
break;
auto offset_minutes = offset_seconds / 60;
offset_seconds %= 60;
StringBuilder builder;
builder.append(sign);
switch (style) {
// The long format always uses 2-digit hours field and minutes field, with optional 2-digit seconds field.
case CalendarPatternStyle::LongOffset:
builder.appendff("{:02}{}{:02}", offset_hours, separator, offset_minutes);
if (offset_seconds > 0)
builder.appendff("{}{:02}", separator, offset_seconds);
break;
// The short format is intended for the shortest representation and uses hour fields without leading zero, with optional 2-digit minutes and seconds fields.
case CalendarPatternStyle::ShortOffset:
builder.appendff("{}", offset_hours);
if (offset_minutes > 0) {
builder.appendff("{}{:02}", separator, offset_minutes);
if (offset_seconds > 0)
builder.appendff("{}{:02}", separator, offset_seconds);
default:
break;
}
break;
default:
VERIFY_NOT_REACHED();
}
// The digits used for hours, minutes and seconds fields in this format are the locale's default decimal digits.
auto result = replace_digits_for_number_system(*number_system, builder.string_view());
return MUST(MUST(String::from_utf8(formats->gmt_format)).replace("{0}"sv, result, ReplaceMode::FirstOnly));
return changed_hour_cycle;
}
// https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology
String format_time_zone(StringView locale, StringView time_zone, CalendarPatternStyle style, AK::UnixDateTime time)
static void apply_time_zone_to_formatter(icu::SimpleDateFormat& formatter, icu::Locale const& locale, StringView time_zone_identifier)
{
auto offset = TimeZone::get_time_zone_offset(time_zone, time);
if (!offset.has_value())
return MUST(String::from_utf8(time_zone));
UErrorCode status = U_ZERO_ERROR;
switch (style) {
case CalendarPatternStyle::Short:
case CalendarPatternStyle::Long:
case CalendarPatternStyle::ShortGeneric:
case CalendarPatternStyle::LongGeneric:
if (auto name = get_time_zone_name(locale, time_zone, style, offset->in_dst); name.has_value())
return MUST(String::from_utf8(*name));
break;
auto* time_zone = icu::TimeZone::createTimeZone(icu_string(time_zone_identifier));
case CalendarPatternStyle::ShortOffset:
case CalendarPatternStyle::LongOffset:
if (auto formatted_offset = format_time_zone_offset(locale, style, offset->seconds); formatted_offset.has_value())
return formatted_offset.release_value();
return MUST(String::from_utf8(time_zone));
auto* calendar = icu::Calendar::createInstance(time_zone, locale, status);
VERIFY(icu_success(status));
default:
VERIFY_NOT_REACHED();
if (calendar->getDynamicClassID() == icu::GregorianCalendar::getStaticClassID()) {
// https://tc39.es/ecma262/#sec-time-values-and-time-range
// A time value supports a slightly smaller range of -8,640,000,000,000,000 to 8,640,000,000,000,000 milliseconds.
static constexpr double ECMA_262_MINIMUM_TIME = -8.64E15;
auto* gregorian_calendar = static_cast<icu::GregorianCalendar*>(calendar);
gregorian_calendar->setGregorianChange(ECMA_262_MINIMUM_TIME, status);
VERIFY(icu_success(status));
}
// If more styles are added, consult the following table to ensure always falling back to GMT offset is still correct:
// https://unicode.org/reports/tr35/tr35-dates.html#dfst-zone
switch (style) {
case CalendarPatternStyle::Short:
case CalendarPatternStyle::ShortGeneric:
return format_time_zone(locale, time_zone, CalendarPatternStyle::ShortOffset, time);
formatter.adoptCalendar(calendar);
}
case CalendarPatternStyle::Long:
case CalendarPatternStyle::LongGeneric:
return format_time_zone(locale, time_zone, CalendarPatternStyle::LongOffset, time);
static bool is_formatted_range_actually_a_range(icu::FormattedDateInterval const& formatted)
{
UErrorCode status = U_ZERO_ERROR;
default:
VERIFY_NOT_REACHED();
auto result = formatted.toTempString(status);
if (icu_failure(status))
return false;
icu::ConstrainedFieldPosition position;
position.constrainCategory(UFIELD_CATEGORY_DATE_INTERVAL_SPAN);
auto has_range = static_cast<bool>(formatted.nextPosition(position, status));
if (icu_failure(status))
return false;
return has_range;
}
struct Range {
constexpr bool contains(i32 position) const
{
return start <= position && position < end;
}
i32 start { 0 };
i32 end { 0 };
};
class DateTimeFormatImpl : public DateTimeFormat {
public:
DateTimeFormatImpl(icu::Locale& locale, icu::UnicodeString const& pattern, StringView time_zone_identifier, NonnullOwnPtr<icu::SimpleDateFormat> formatter)
: m_locale(locale)
, m_pattern(CalendarPattern::create_from_pattern(icu_string_to_string(pattern)))
, m_formatter(move(formatter))
{
apply_time_zone_to_formatter(*m_formatter, m_locale, time_zone_identifier);
}
virtual ~DateTimeFormatImpl() override = default;
virtual CalendarPattern const& chosen_pattern() const override { return m_pattern; }
virtual String format(double time) const override
{
auto formatted_time = format_impl(time);
if (!formatted_time.has_value())
return {};
return icu_string_to_string(*formatted_time);
}
virtual Vector<Partition> format_to_parts(double time) const override
{
icu::FieldPositionIterator iterator;
auto formatted_time = format_impl(time, &iterator);
if (!formatted_time.has_value())
return {};
Vector<Partition> result;
auto create_partition = [&](i32 field, i32 begin, i32 end) {
Partition partition;
partition.type = icu_date_time_format_field_to_string(field);
partition.value = icu_string_to_string(formatted_time->tempSubStringBetween(begin, end));
partition.source = "shared"sv;
result.append(move(partition));
};
icu::FieldPosition position;
i32 previous_end_index = 0;
while (static_cast<bool>(iterator.next(position))) {
if (previous_end_index < position.getBeginIndex())
create_partition(LITERAL_FIELD, previous_end_index, position.getBeginIndex());
if (position.getField() >= 0)
create_partition(position.getField(), position.getBeginIndex(), position.getEndIndex());
previous_end_index = position.getEndIndex();
}
if (previous_end_index < formatted_time->length())
create_partition(LITERAL_FIELD, previous_end_index, formatted_time->length());
return result;
}
virtual String format_range(double start, double end) const override
{
UErrorCode status = U_ZERO_ERROR;
auto formatted = format_range_impl(start, end);
if (!formatted.has_value())
return {};
if (!is_formatted_range_actually_a_range(*formatted))
return format(start);
auto formatted_time = formatted->toTempString(status);
if (icu_failure(status))
return {};
return icu_string_to_string(formatted_time);
}
virtual Vector<Partition> format_range_to_parts(double start, double end) const override
{
UErrorCode status = U_ZERO_ERROR;
auto formatted = format_range_impl(start, end);
if (!formatted.has_value())
return {};
if (!is_formatted_range_actually_a_range(*formatted))
return format_to_parts(start);
auto formatted_time = formatted->toTempString(status);
if (icu_failure(status))
return {};
icu::ConstrainedFieldPosition position;
i32 previous_end_index = 0;
Vector<Partition> result;
Optional<Range> start_range;
Optional<Range> end_range;
auto create_partition = [&](i32 field, i32 begin, i32 end) {
Partition partition;
partition.type = icu_date_time_format_field_to_string(field);
partition.value = icu_string_to_string(formatted_time.tempSubStringBetween(begin, end));
if (start_range.has_value() && start_range->contains(begin))
partition.source = "startRange"sv;
else if (end_range.has_value() && end_range->contains(begin))
partition.source = "endRange"sv;
else
partition.source = "shared"sv;
result.append(move(partition));
};
while (static_cast<bool>(formatted->nextPosition(position, status)) && icu_success(status)) {
if (previous_end_index < position.getStart())
create_partition(LITERAL_FIELD, previous_end_index, position.getStart());
if (position.getCategory() == UFIELD_CATEGORY_DATE_INTERVAL_SPAN) {
auto& range = position.getField() == 0 ? start_range : end_range;
range = Range { position.getStart(), position.getLimit() };
} else if (position.getCategory() == UFIELD_CATEGORY_DATE) {
create_partition(position.getField(), position.getStart(), position.getLimit());
}
previous_end_index = position.getLimit();
}
if (previous_end_index < formatted_time.length())
create_partition(LITERAL_FIELD, previous_end_index, formatted_time.length());
return result;
}
private:
Optional<icu::UnicodeString> format_impl(double time, icu::FieldPositionIterator* iterator = nullptr) const
{
UErrorCode status = U_ZERO_ERROR;
icu::UnicodeString formatted_time;
m_formatter->format(time, formatted_time, iterator, status);
if (icu_failure(status))
return {};
return formatted_time;
}
Optional<icu::FormattedDateInterval> format_range_impl(double start, double end) const
{
UErrorCode status = U_ZERO_ERROR;
if (!m_range_formatter) {
icu::UnicodeString pattern;
m_formatter->toPattern(pattern);
auto skeleton = icu::DateTimePatternGenerator::staticGetSkeleton(pattern, status);
if (icu_failure(status))
return {};
auto* formatter = icu::DateIntervalFormat::createInstance(skeleton, m_locale, status);
if (icu_failure(status))
return {};
m_range_formatter = adopt_own(*formatter);
m_range_formatter->setTimeZone(m_formatter->getTimeZone());
}
auto start_calendar = adopt_own(*m_formatter->getCalendar()->clone());
start_calendar->setTime(start, status);
if (icu_failure(status))
return {};
auto end_calendar = adopt_own(*m_formatter->getCalendar()->clone());
end_calendar->setTime(end, status);
if (icu_failure(status))
return {};
auto formatted = m_range_formatter->formatToValue(*start_calendar, *end_calendar, status);
if (icu_failure(status))
return {};
return formatted;
}
icu::Locale& m_locale;
CalendarPattern m_pattern;
NonnullOwnPtr<icu::SimpleDateFormat> m_formatter;
mutable OwnPtr<icu::DateIntervalFormat> m_range_formatter;
};
NonnullOwnPtr<DateTimeFormat> DateTimeFormat::create_for_date_and_time_style(
StringView locale,
StringView time_zone_identifier,
Optional<HourCycle> const& hour_cycle,
Optional<bool> const& hour12,
Optional<DateTimeStyle> const& date_style,
Optional<DateTimeStyle> const& time_style)
{
UErrorCode status = U_ZERO_ERROR;
auto locale_data = LocaleData::for_locale(locale);
VERIFY(locale_data.has_value());
auto formatter = adopt_own(*verify_cast<icu::SimpleDateFormat>([&]() {
if (date_style.has_value() && time_style.has_value()) {
return icu::DateFormat::createDateTimeInstance(
icu_date_time_style(*date_style), icu_date_time_style(*time_style), locale_data->locale());
}
if (date_style.has_value()) {
return icu::DateFormat::createDateInstance(
icu_date_time_style(*date_style), locale_data->locale());
}
if (time_style.has_value()) {
return icu::DateFormat::createTimeInstance(
icu_date_time_style(*time_style), locale_data->locale());
}
VERIFY_NOT_REACHED();
}()));
icu::UnicodeString pattern;
formatter->toPattern(pattern);
auto skeleton = icu::DateTimePatternGenerator::staticGetSkeleton(pattern, status);
VERIFY(icu_success(status));
if (apply_hour_cycle_to_skeleton(skeleton, hour_cycle, hour12)) {
pattern = locale_data->date_time_pattern_generator().getBestPattern(skeleton, UDATPG_MATCH_ALL_FIELDS_LENGTH, status);
VERIFY(icu_success(status));
apply_hour_cycle_to_skeleton(pattern, hour_cycle, hour12);
formatter = adopt_own(*new icu::SimpleDateFormat(pattern, locale_data->locale(), status));
VERIFY(icu_success(status));
}
return adopt_own(*new DateTimeFormatImpl(locale_data->locale(), pattern, time_zone_identifier, move(formatter)));
}
NonnullOwnPtr<DateTimeFormat> DateTimeFormat::create_for_pattern_options(
StringView locale,
StringView time_zone_identifier,
CalendarPattern const& options)
{
UErrorCode status = U_ZERO_ERROR;
auto locale_data = LocaleData::for_locale(locale);
VERIFY(locale_data.has_value());
auto skeleton = icu_string(options.to_pattern());
auto pattern = locale_data->date_time_pattern_generator().getBestPattern(skeleton, UDATPG_MATCH_ALL_FIELDS_LENGTH, status);
VERIFY(icu_success(status));
apply_hour_cycle_to_skeleton(pattern, options.hour_cycle, {});
auto formatter = adopt_own(*new icu::SimpleDateFormat(pattern, locale_data->locale(), status));
VERIFY(icu_success(status));
return adopt_own(*new DateTimeFormatImpl(locale_data->locale(), pattern, time_zone_identifier, move(formatter)));
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021-2023, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2021-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -17,25 +17,14 @@
namespace Locale {
enum class Era : u8 {
BC,
AD,
};
enum class Month : u8 {
January,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December,
enum class DateTimeStyle {
Full,
Long,
Medium,
Short,
};
DateTimeStyle date_time_style_from_string(StringView);
StringView date_time_style_to_string(DateTimeStyle);
enum class Weekday : u8 {
Sunday,
@ -47,26 +36,14 @@ enum class Weekday : u8 {
Saturday,
};
enum class DayPeriod : u8 {
AM,
PM,
Noon,
Morning1,
Morning2,
Afternoon1,
Afternoon2,
Evening1,
Evening2,
Night1,
Night2,
};
enum class HourCycle : u8 {
H11,
H12,
H23,
H24,
};
HourCycle hour_cycle_from_string(StringView hour_cycle);
StringView hour_cycle_to_string(HourCycle hour_cycle);
enum class CalendarPatternStyle : u8 {
Narrow,
@ -79,95 +56,45 @@ enum class CalendarPatternStyle : u8 {
ShortGeneric,
LongGeneric,
};
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<typename Callback>
void for_each_calendar_field_zipped_with(CalendarPattern const& other, Callback&& callback)
{
callback(era, other.era, Field::Era);
callback(year, other.year, Field::Year);
callback(month, other.month, Field::Month);
callback(weekday, other.weekday, Field::Weekday);
callback(day, other.day, Field::Day);
callback(day_period, other.day_period, Field::DayPeriod);
callback(hour, other.hour, Field::Hour);
callback(minute, other.minute, Field::Minute);
callback(second, other.second, Field::Second);
callback(fractional_second_digits, other.fractional_second_digits, Field::FractionalSecondDigits);
callback(time_zone_name, other.time_zone_name, Field::TimeZoneName);
callback(hour_cycle, other.hour_cycle);
callback(era, other.era);
callback(year, other.year);
callback(month, other.month);
callback(weekday, other.weekday);
callback(day, other.day);
callback(day_period, other.day_period);
callback(hour, other.hour);
callback(minute, other.minute);
callback(second, other.second);
callback(fractional_second_digits, other.fractional_second_digits);
callback(time_zone_name, other.time_zone_name);
}
String skeleton {};
String pattern {};
Optional<String> pattern12 {};
Optional<HourCycle> hour_cycle {};
Optional<HourCycle> hour_cycle;
Optional<bool> hour12;
// https://unicode.org/reports/tr35/tr35-dates.html#Calendar_Fields
Optional<CalendarPatternStyle> era {};
Optional<CalendarPatternStyle> year {};
Optional<CalendarPatternStyle> month {};
Optional<CalendarPatternStyle> weekday {};
Optional<CalendarPatternStyle> day {};
Optional<CalendarPatternStyle> day_period {};
Optional<CalendarPatternStyle> hour {};
Optional<CalendarPatternStyle> minute {};
Optional<CalendarPatternStyle> second {};
Optional<u8> fractional_second_digits {};
Optional<CalendarPatternStyle> time_zone_name {};
};
struct CalendarRangePattern : public CalendarPattern {
enum class Field {
Era,
Year,
Month,
Day,
AmPm,
DayPeriod,
Hour,
Minute,
Second,
FractionalSecondDigits,
};
Optional<Field> field {};
String start_range {};
StringView separator {};
String end_range {};
};
enum class CalendarFormatType : u8 {
Date,
Time,
DateTime,
};
struct CalendarFormat {
CalendarPattern full_format {};
CalendarPattern long_format {};
CalendarPattern medium_format {};
CalendarPattern short_format {};
};
enum class CalendarSymbol : u8 {
DayPeriod,
Era,
Month,
Weekday,
Optional<CalendarPatternStyle> era;
Optional<CalendarPatternStyle> year;
Optional<CalendarPatternStyle> month;
Optional<CalendarPatternStyle> weekday;
Optional<CalendarPatternStyle> day;
Optional<CalendarPatternStyle> day_period;
Optional<CalendarPatternStyle> hour;
Optional<CalendarPatternStyle> minute;
Optional<CalendarPatternStyle> second;
Optional<u8> fractional_second_digits;
Optional<CalendarPatternStyle> time_zone_name;
};
struct TimeZoneFormat {
@ -181,12 +108,6 @@ struct TimeZoneFormat {
StringView gmt_zero_format {};
};
HourCycle hour_cycle_from_string(StringView hour_cycle);
StringView hour_cycle_to_string(HourCycle hour_cycle);
CalendarPatternStyle calendar_pattern_style_from_string(StringView style);
StringView calendar_pattern_style_to_string(CalendarPatternStyle style);
Optional<HourCycleRegion> hour_cycle_region_from_string(StringView hour_cycle_region);
Vector<HourCycle> get_regional_hour_cycles(StringView region);
Vector<HourCycle> get_locale_hour_cycles(StringView locale);
@ -208,25 +129,42 @@ Optional<WeekendEndRegion> weekend_end_region_from_string(StringView weekend_end
Optional<Weekday> get_regional_weekend_end(StringView region);
Optional<Weekday> get_locale_weekend_end(StringView locale);
String combine_skeletons(StringView first, StringView second);
Optional<CalendarFormat> get_calendar_date_format(StringView locale, StringView calendar);
Optional<CalendarFormat> get_calendar_time_format(StringView locale, StringView calendar);
Optional<CalendarFormat> get_calendar_date_time_format(StringView locale, StringView calendar);
Optional<CalendarFormat> get_calendar_format(StringView locale, StringView calendar, CalendarFormatType type);
Vector<CalendarPattern> get_calendar_available_formats(StringView locale, StringView calendar);
Optional<CalendarRangePattern> get_calendar_default_range_format(StringView locale, StringView calendar);
Vector<CalendarRangePattern> get_calendar_range_formats(StringView locale, StringView calendar, StringView skeleton);
Vector<CalendarRangePattern> get_calendar_range12_formats(StringView locale, StringView calendar, StringView skeleton);
Optional<StringView> get_calendar_era_symbol(StringView locale, StringView calendar, CalendarPatternStyle style, Era value);
Optional<StringView> get_calendar_month_symbol(StringView locale, StringView calendar, CalendarPatternStyle style, Month value);
Optional<StringView> get_calendar_weekday_symbol(StringView locale, StringView calendar, CalendarPatternStyle style, Weekday value);
Optional<StringView> get_calendar_day_period_symbol(StringView locale, StringView calendar, CalendarPatternStyle style, DayPeriod value);
Optional<StringView> get_calendar_day_period_symbol_for_hour(StringView locale, StringView calendar, CalendarPatternStyle style, u8 hour);
String format_time_zone(StringView locale, StringView time_zone, CalendarPatternStyle style, AK::UnixDateTime time);
Optional<StringView> get_time_zone_name(StringView locale, StringView time_zone, CalendarPatternStyle style, TimeZone::InDST in_dst);
Optional<TimeZoneFormat> get_time_zone_format(StringView locale);
class DateTimeFormat {
public:
static NonnullOwnPtr<DateTimeFormat> create_for_date_and_time_style(
StringView locale,
StringView time_zone_identifier,
Optional<HourCycle> const& hour_cycle,
Optional<bool> const& hour12,
Optional<DateTimeStyle> const& date_style,
Optional<DateTimeStyle> const& time_style);
static NonnullOwnPtr<DateTimeFormat> create_for_pattern_options(
StringView locale,
StringView time_zone_identifier,
CalendarPattern const&);
virtual ~DateTimeFormat() = default;
struct Partition {
StringView type;
String value;
StringView source;
};
virtual CalendarPattern const& chosen_pattern() const = 0;
virtual String format(double) const = 0;
virtual Vector<Partition> format_to_parts(double) const = 0;
virtual String format_range(double, double) const = 0;
virtual Vector<Partition> format_range_to_parts(double, double) const = 0;
protected:
DateTimeFormat() = default;
};
}

View File

@ -10,11 +10,7 @@
namespace Locale {
enum class CalendarFormatType : u8;
enum class CalendarPatternStyle : u8;
enum class CalendarSymbol : u8;
enum class DayPeriod : u8;
enum class Era : u8;
enum class FirstDayRegion : u8;
enum class HourCycle : u8;
enum class HourCycleRegion : u16;
@ -27,7 +23,6 @@ enum class KeywordHours : u8;
enum class KeywordNumbers : u8;
enum class Locale : u16;
enum class MinimumDaysRegion : u8;
enum class Month : u8;
enum class NumericSymbol : u8;
enum class PluralCategory : u8;
enum class Style : u8;
@ -37,9 +32,7 @@ enum class WeekendStartRegion : u8;
class NumberFormat;
struct CalendarFormat;
struct CalendarPattern;
struct CalendarRangePattern;
struct Keyword;
struct LanguageID;
struct ListFormatPart;

View File

@ -85,6 +85,11 @@ icu::DateTimePatternGenerator& LocaleData::date_time_pattern_generator()
return *m_date_time_pattern_generator;
}
icu::UnicodeString icu_string(StringView string)
{
return icu::UnicodeString::fromUTF8(icu_string_piece(string));
}
icu::StringPiece icu_string_piece(StringView string)
{
return { string.characters_without_null_termination(), static_cast<i32>(string.length()) };

View File

@ -60,6 +60,7 @@ static constexpr bool icu_failure(UErrorCode code)
return static_cast<bool>(U_FAILURE(code));
}
icu::UnicodeString icu_string(StringView string);
icu::StringPiece icu_string_piece(StringView string);
Vector<icu::UnicodeString> icu_string_list(ReadonlySpan<String> strings);