mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-22 23:50:19 +00:00
82c681e44b
This incurs a whole host of changes in, among others, JavaScript Intl and Date.
345 lines
14 KiB
C++
345 lines
14 KiB
C++
/*
|
|
* Copyright (c) 2021-2023, Tim Flynn <trflynn89@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/Array.h>
|
|
#include <AK/StringBuilder.h>
|
|
#include <LibLocale/DateTimeFormat.h>
|
|
#include <LibLocale/Locale.h>
|
|
#include <LibLocale/NumberFormat.h>
|
|
#include <stdlib.h>
|
|
|
|
namespace Locale {
|
|
|
|
HourCycle hour_cycle_from_string(StringView hour_cycle)
|
|
{
|
|
if (hour_cycle == "h11"sv)
|
|
return HourCycle::H11;
|
|
if (hour_cycle == "h12"sv)
|
|
return HourCycle::H12;
|
|
if (hour_cycle == "h23"sv)
|
|
return HourCycle::H23;
|
|
if (hour_cycle == "h24"sv)
|
|
return HourCycle::H24;
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
StringView hour_cycle_to_string(HourCycle hour_cycle)
|
|
{
|
|
switch (hour_cycle) {
|
|
case HourCycle::H11:
|
|
return "h11"sv;
|
|
case HourCycle::H12:
|
|
return "h12"sv;
|
|
case HourCycle::H23:
|
|
return "h23"sv;
|
|
case HourCycle::H24:
|
|
return "h24"sv;
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
CalendarPatternStyle calendar_pattern_style_from_string(StringView style)
|
|
{
|
|
if (style == "narrow"sv)
|
|
return CalendarPatternStyle::Narrow;
|
|
if (style == "short"sv)
|
|
return CalendarPatternStyle::Short;
|
|
if (style == "long"sv)
|
|
return CalendarPatternStyle::Long;
|
|
if (style == "numeric"sv)
|
|
return CalendarPatternStyle::Numeric;
|
|
if (style == "2-digit"sv)
|
|
return CalendarPatternStyle::TwoDigit;
|
|
if (style == "shortOffset"sv)
|
|
return CalendarPatternStyle::ShortOffset;
|
|
if (style == "longOffset"sv)
|
|
return CalendarPatternStyle::LongOffset;
|
|
if (style == "shortGeneric"sv)
|
|
return CalendarPatternStyle::ShortGeneric;
|
|
if (style == "longGeneric"sv)
|
|
return CalendarPatternStyle::LongGeneric;
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
StringView calendar_pattern_style_to_string(CalendarPatternStyle style)
|
|
{
|
|
switch (style) {
|
|
case CalendarPatternStyle::Narrow:
|
|
return "narrow"sv;
|
|
case CalendarPatternStyle::Short:
|
|
return "short"sv;
|
|
case CalendarPatternStyle::Long:
|
|
return "long"sv;
|
|
case CalendarPatternStyle::Numeric:
|
|
return "numeric"sv;
|
|
case CalendarPatternStyle::TwoDigit:
|
|
return "2-digit"sv;
|
|
case CalendarPatternStyle::ShortOffset:
|
|
return "shortOffset"sv;
|
|
case CalendarPatternStyle::LongOffset:
|
|
return "longOffset"sv;
|
|
case CalendarPatternStyle::ShortGeneric:
|
|
return "shortGeneric"sv;
|
|
case CalendarPatternStyle::LongGeneric:
|
|
return "longGeneric"sv;
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
Optional<HourCycleRegion> __attribute__((weak)) hour_cycle_region_from_string(StringView) { return {}; }
|
|
ErrorOr<Vector<HourCycle>> __attribute__((weak)) get_regional_hour_cycles(StringView) { return Vector<HourCycle> {}; }
|
|
|
|
template<typename T, FallibleFunction<StringView> GetRegionalValues>
|
|
static ErrorOr<T> find_regional_values_for_locale(StringView locale, GetRegionalValues&& get_regional_values)
|
|
{
|
|
auto has_value = [](auto const& container) {
|
|
if constexpr (requires { container.has_value(); })
|
|
return container.has_value();
|
|
else
|
|
return !container.is_empty();
|
|
};
|
|
|
|
if (auto regional_values = TRY(get_regional_values(locale)); has_value(regional_values))
|
|
return regional_values;
|
|
|
|
auto return_default_values = [&]() { return get_regional_values("001"sv); };
|
|
|
|
auto language = TRY(parse_unicode_language_id(locale));
|
|
if (!language.has_value())
|
|
return return_default_values();
|
|
|
|
if (!language->region.has_value())
|
|
language = TRY(add_likely_subtags(*language));
|
|
if (!language.has_value() || !language->region.has_value())
|
|
return return_default_values();
|
|
|
|
if (auto regional_values = TRY(get_regional_values(*language->region)); has_value(regional_values))
|
|
return regional_values;
|
|
|
|
return return_default_values();
|
|
}
|
|
|
|
template<typename T, typename GetRegionalValues>
|
|
static ErrorOr<T> find_regional_values_for_locale(StringView locale, GetRegionalValues&& get_regional_values)
|
|
{
|
|
return find_regional_values_for_locale<T>(locale, [&](auto region) -> ErrorOr<T> { return get_regional_values(region); });
|
|
}
|
|
|
|
// https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
|
|
ErrorOr<Vector<HourCycle>> get_locale_hour_cycles(StringView locale)
|
|
{
|
|
return find_regional_values_for_locale<Vector<HourCycle>>(locale, get_regional_hour_cycles);
|
|
}
|
|
|
|
ErrorOr<Optional<HourCycle>> get_default_regional_hour_cycle(StringView locale)
|
|
{
|
|
if (auto hour_cycles = TRY(get_locale_hour_cycles(locale)); !hour_cycles.is_empty())
|
|
return hour_cycles.first();
|
|
return OptionalNone {};
|
|
}
|
|
|
|
Optional<MinimumDaysRegion> __attribute__((weak)) minimum_days_region_from_string(StringView) { return {}; }
|
|
Optional<u8> __attribute__((weak)) get_regional_minimum_days(StringView) { return {}; }
|
|
|
|
ErrorOr<Optional<u8>> get_locale_minimum_days(StringView locale)
|
|
{
|
|
return find_regional_values_for_locale<Optional<u8>>(locale, get_regional_minimum_days);
|
|
}
|
|
|
|
Optional<FirstDayRegion> __attribute__((weak)) first_day_region_from_string(StringView) { return {}; }
|
|
Optional<Weekday> __attribute__((weak)) get_regional_first_day(StringView) { return {}; }
|
|
|
|
ErrorOr<Optional<Weekday>> get_locale_first_day(StringView locale)
|
|
{
|
|
return find_regional_values_for_locale<Optional<Weekday>>(locale, get_regional_first_day);
|
|
}
|
|
|
|
Optional<WeekendStartRegion> __attribute__((weak)) weekend_start_region_from_string(StringView) { return {}; }
|
|
Optional<Weekday> __attribute__((weak)) get_regional_weekend_start(StringView) { return {}; }
|
|
|
|
ErrorOr<Optional<Weekday>> get_locale_weekend_start(StringView locale)
|
|
{
|
|
return find_regional_values_for_locale<Optional<Weekday>>(locale, get_regional_weekend_start);
|
|
}
|
|
|
|
Optional<WeekendEndRegion> __attribute__((weak)) weekend_end_region_from_string(StringView) { return {}; }
|
|
Optional<Weekday> __attribute__((weak)) get_regional_weekend_end(StringView) { return {}; }
|
|
|
|
ErrorOr<Optional<Weekday>> get_locale_weekend_end(StringView locale)
|
|
{
|
|
return find_regional_values_for_locale<Optional<Weekday>>(locale, get_regional_weekend_end);
|
|
}
|
|
|
|
ErrorOr<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) -> ErrorOr<bool> {
|
|
auto first_index = skeleton.find(ch);
|
|
if (!first_index.has_value())
|
|
return false;
|
|
|
|
auto last_index = skeleton.find_last(ch);
|
|
|
|
TRY(builder.try_append(skeleton.substring_view(*first_index, *last_index - *first_index + 1)));
|
|
return true;
|
|
};
|
|
|
|
for (auto fields : field_order) {
|
|
for (auto ch : fields) {
|
|
if (TRY(append_from_skeleton(first, ch)))
|
|
break;
|
|
if (TRY(append_from_skeleton(second, ch)))
|
|
break;
|
|
}
|
|
}
|
|
|
|
return builder.to_string();
|
|
}
|
|
|
|
ErrorOr<Optional<CalendarFormat>> __attribute__((weak)) get_calendar_date_format(StringView, StringView) { return OptionalNone {}; }
|
|
ErrorOr<Optional<CalendarFormat>> __attribute__((weak)) get_calendar_time_format(StringView, StringView) { return OptionalNone {}; }
|
|
ErrorOr<Optional<CalendarFormat>> __attribute__((weak)) get_calendar_date_time_format(StringView, StringView) { return OptionalNone {}; }
|
|
|
|
ErrorOr<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();
|
|
}
|
|
}
|
|
|
|
ErrorOr<Vector<CalendarPattern>> __attribute__((weak)) get_calendar_available_formats(StringView, StringView) { return Vector<CalendarPattern> {}; }
|
|
ErrorOr<Optional<CalendarRangePattern>> __attribute__((weak)) get_calendar_default_range_format(StringView, StringView) { return OptionalNone {}; }
|
|
ErrorOr<Vector<CalendarRangePattern>> __attribute__((weak)) get_calendar_range_formats(StringView, StringView, StringView) { return Vector<CalendarRangePattern> {}; }
|
|
ErrorOr<Vector<CalendarRangePattern>> __attribute__((weak)) get_calendar_range12_formats(StringView, StringView, StringView) { return Vector<CalendarRangePattern> {}; }
|
|
ErrorOr<Optional<StringView>> __attribute__((weak)) get_calendar_era_symbol(StringView, StringView, CalendarPatternStyle, Era) { return OptionalNone {}; }
|
|
ErrorOr<Optional<StringView>> __attribute__((weak)) get_calendar_month_symbol(StringView, StringView, CalendarPatternStyle, Month) { return OptionalNone {}; }
|
|
ErrorOr<Optional<StringView>> __attribute__((weak)) get_calendar_weekday_symbol(StringView, StringView, CalendarPatternStyle, Weekday) { return OptionalNone {}; }
|
|
ErrorOr<Optional<StringView>> __attribute__((weak)) get_calendar_day_period_symbol(StringView, StringView, CalendarPatternStyle, DayPeriod) { return OptionalNone {}; }
|
|
ErrorOr<Optional<StringView>> __attribute__((weak)) get_calendar_day_period_symbol_for_hour(StringView, StringView, CalendarPatternStyle, u8) { return OptionalNone {}; }
|
|
|
|
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 ErrorOr<Optional<String>> format_time_zone_offset(StringView locale, CalendarPatternStyle style, i64 offset_seconds)
|
|
{
|
|
auto formats = get_time_zone_format(locale);
|
|
if (!formats.has_value())
|
|
return OptionalNone {};
|
|
|
|
auto number_system = TRY(get_preferred_keyword_value_for_locale(locale, "nu"sv));
|
|
if (!number_system.has_value())
|
|
return OptionalNone {};
|
|
|
|
if (offset_seconds == 0)
|
|
return String::from_utf8(formats->gmt_zero_format);
|
|
|
|
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);
|
|
|
|
auto offset_hours = offset_seconds / 3'600;
|
|
offset_seconds %= 3'600;
|
|
|
|
auto offset_minutes = offset_seconds / 60;
|
|
offset_seconds %= 60;
|
|
|
|
StringBuilder builder;
|
|
TRY(builder.try_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:
|
|
TRY(builder.try_appendff("{:02}{}{:02}", offset_hours, separator, offset_minutes));
|
|
if (offset_seconds > 0)
|
|
TRY(builder.try_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:
|
|
TRY(builder.try_appendff("{}", offset_hours));
|
|
if (offset_minutes > 0) {
|
|
TRY(builder.try_appendff("{}{:02}", separator, offset_minutes));
|
|
if (offset_seconds > 0)
|
|
TRY(builder.try_appendff("{}{:02}", separator, offset_seconds));
|
|
}
|
|
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 = TRY(replace_digits_for_number_system(*number_system, TRY(builder.to_string())));
|
|
return TRY(String::from_utf8(formats->gmt_format)).replace("{0}"sv, result, ReplaceMode::FirstOnly);
|
|
}
|
|
|
|
// https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology
|
|
ErrorOr<String> format_time_zone(StringView locale, StringView time_zone, CalendarPatternStyle style, AK::UnixDateTime time)
|
|
{
|
|
auto offset = TimeZone::get_time_zone_offset(time_zone, time);
|
|
if (!offset.has_value())
|
|
return String::from_utf8(time_zone);
|
|
|
|
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 String::from_utf8(*name);
|
|
break;
|
|
|
|
case CalendarPatternStyle::ShortOffset:
|
|
case CalendarPatternStyle::LongOffset:
|
|
if (auto formatted_offset = TRY(format_time_zone_offset(locale, style, offset->seconds)); formatted_offset.has_value())
|
|
return formatted_offset.release_value();
|
|
return String::from_utf8(time_zone);
|
|
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
// 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);
|
|
|
|
case CalendarPatternStyle::Long:
|
|
case CalendarPatternStyle::LongGeneric:
|
|
return format_time_zone(locale, time_zone, CalendarPatternStyle::LongOffset, time);
|
|
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
}
|