2021-11-27 19:54:48 +00:00
|
|
|
/*
|
2024-06-12 14:47:20 +00:00
|
|
|
* Copyright (c) 2021-2024, Tim Flynn <trflynn89@serenityos.org>
|
2021-11-27 19:54:48 +00:00
|
|
|
*
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
*/
|
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
#include <AK/AllOf.h>
|
2021-12-09 00:52:48 +00:00
|
|
|
#include <AK/Array.h>
|
2024-06-12 14:47:20 +00:00
|
|
|
#include <AK/GenericLexer.h>
|
2021-12-09 00:52:48 +00:00
|
|
|
#include <AK/StringBuilder.h>
|
2024-06-12 14:47:20 +00:00
|
|
|
#include <AK/TypeCasts.h>
|
2024-06-23 13:14:27 +00:00
|
|
|
#include <LibUnicode/DateTimeFormat.h>
|
|
|
|
#include <LibUnicode/ICU.h>
|
|
|
|
#include <LibUnicode/Locale.h>
|
|
|
|
#include <LibUnicode/NumberFormat.h>
|
|
|
|
#include <LibUnicode/PartitionRange.h>
|
2022-01-11 16:35:50 +00:00
|
|
|
#include <stdlib.h>
|
2021-11-27 19:54:48 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
#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>
|
2024-06-12 21:00:45 +00:00
|
|
|
#include <unicode/ucal.h>
|
2024-06-12 14:47:20 +00:00
|
|
|
|
2024-06-23 13:14:27 +00:00
|
|
|
namespace Unicode {
|
2021-11-27 19:54:48 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
LibUnicode: Parse and generate regional hour cycles
Unlike most data in the CLDR, hour cycles are not stored on a per-locale
basis. Instead, they are keyed by a string that is usually a region, but
sometimes is a locale. Therefore, given a locale, to determine the hour
cycles for that locale, we:
1. Check if the locale itself is assigned hour cycles.
2. If the locale has a region, check if that region is assigned hour
cycles.
3. Otherwise, maximize that locale, and if the maximized locale has
a region, check if that region is assigned hour cycles.
4. If the above all fail, fallback to the "001" region.
Further, each locale's default hour cycle is the first assigned hour
cycle.
2021-11-28 01:57:21 +00:00
|
|
|
HourCycle hour_cycle_from_string(StringView hour_cycle)
|
|
|
|
{
|
|
|
|
if (hour_cycle == "h11"sv)
|
2022-09-02 16:01:10 +00:00
|
|
|
return HourCycle::H11;
|
2023-01-27 15:27:33 +00:00
|
|
|
if (hour_cycle == "h12"sv)
|
2022-09-02 16:01:10 +00:00
|
|
|
return HourCycle::H12;
|
2023-01-27 15:27:33 +00:00
|
|
|
if (hour_cycle == "h23"sv)
|
2022-09-02 16:01:10 +00:00
|
|
|
return HourCycle::H23;
|
2023-01-27 15:27:33 +00:00
|
|
|
if (hour_cycle == "h24"sv)
|
2022-09-02 16:01:10 +00:00
|
|
|
return HourCycle::H24;
|
LibUnicode: Parse and generate regional hour cycles
Unlike most data in the CLDR, hour cycles are not stored on a per-locale
basis. Instead, they are keyed by a string that is usually a region, but
sometimes is a locale. Therefore, given a locale, to determine the hour
cycles for that locale, we:
1. Check if the locale itself is assigned hour cycles.
2. If the locale has a region, check if that region is assigned hour
cycles.
3. Otherwise, maximize that locale, and if the maximized locale has
a region, check if that region is assigned hour cycles.
4. If the above all fail, fallback to the "001" region.
Further, each locale's default hour cycle is the first assigned hour
cycle.
2021-11-28 01:57:21 +00:00
|
|
|
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;
|
|
|
|
}
|
2024-06-12 14:47:20 +00:00
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
}
|
|
|
|
|
2024-06-12 20:16:49 +00:00
|
|
|
Optional<HourCycle> default_hour_cycle(StringView locale)
|
|
|
|
{
|
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
|
|
|
|
|
|
auto locale_data = LocaleData::for_locale(locale);
|
|
|
|
if (!locale_data.has_value())
|
|
|
|
return {};
|
|
|
|
|
|
|
|
auto hour_cycle = locale_data->date_time_pattern_generator().getDefaultHourCycle(status);
|
|
|
|
if (icu_failure(status))
|
|
|
|
return {};
|
|
|
|
|
|
|
|
switch (hour_cycle) {
|
|
|
|
case UDAT_HOUR_CYCLE_11:
|
|
|
|
return HourCycle::H11;
|
|
|
|
case UDAT_HOUR_CYCLE_12:
|
|
|
|
return HourCycle::H12;
|
|
|
|
case UDAT_HOUR_CYCLE_23:
|
|
|
|
return HourCycle::H23;
|
|
|
|
case UDAT_HOUR_CYCLE_24:
|
|
|
|
return HourCycle::H24;
|
|
|
|
}
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
}
|
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
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();
|
LibUnicode: Parse and generate regional hour cycles
Unlike most data in the CLDR, hour cycles are not stored on a per-locale
basis. Instead, they are keyed by a string that is usually a region, but
sometimes is a locale. Therefore, given a locale, to determine the hour
cycles for that locale, we:
1. Check if the locale itself is assigned hour cycles.
2. If the locale has a region, check if that region is assigned hour
cycles.
3. Otherwise, maximize that locale, and if the maximized locale has
a region, check if that region is assigned hour cycles.
4. If the above all fail, fallback to the "001" region.
Further, each locale's default hour cycle is the first assigned hour
cycle.
2021-11-28 01:57:21 +00:00
|
|
|
}
|
|
|
|
|
2021-11-27 19:54:48 +00:00
|
|
|
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;
|
2022-01-02 19:23:24 +00:00
|
|
|
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;
|
2021-11-27 19:54:48 +00:00
|
|
|
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:
|
2021-11-30 15:39:56 +00:00
|
|
|
return "numeric"sv;
|
2021-11-27 19:54:48 +00:00
|
|
|
case CalendarPatternStyle::TwoDigit:
|
|
|
|
return "2-digit"sv;
|
2022-01-02 19:23:24 +00:00
|
|
|
case CalendarPatternStyle::ShortOffset:
|
|
|
|
return "shortOffset"sv;
|
|
|
|
case CalendarPatternStyle::LongOffset:
|
|
|
|
return "longOffset"sv;
|
|
|
|
case CalendarPatternStyle::ShortGeneric:
|
|
|
|
return "shortGeneric"sv;
|
|
|
|
case CalendarPatternStyle::LongGeneric:
|
|
|
|
return "longGeneric"sv;
|
2021-11-27 19:54:48 +00:00
|
|
|
}
|
2024-06-12 14:47:20 +00:00
|
|
|
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;
|
2021-11-27 19:54:48 +00:00
|
|
|
}
|
|
|
|
|
2023-08-22 20:41:36 +00:00
|
|
|
template<typename T, typename GetRegionalValues>
|
|
|
|
static T find_regional_values_for_locale(StringView locale, GetRegionalValues&& get_regional_values)
|
LibUnicode: Parse and generate regional hour cycles
Unlike most data in the CLDR, hour cycles are not stored on a per-locale
basis. Instead, they are keyed by a string that is usually a region, but
sometimes is a locale. Therefore, given a locale, to determine the hour
cycles for that locale, we:
1. Check if the locale itself is assigned hour cycles.
2. If the locale has a region, check if that region is assigned hour
cycles.
3. Otherwise, maximize that locale, and if the maximized locale has
a region, check if that region is assigned hour cycles.
4. If the above all fail, fallback to the "001" region.
Further, each locale's default hour cycle is the first assigned hour
cycle.
2021-11-28 01:57:21 +00:00
|
|
|
{
|
2022-07-06 12:21:32 +00:00
|
|
|
auto has_value = [](auto const& container) {
|
|
|
|
if constexpr (requires { container.has_value(); })
|
|
|
|
return container.has_value();
|
|
|
|
else
|
|
|
|
return !container.is_empty();
|
|
|
|
};
|
|
|
|
|
2023-08-22 20:41:36 +00:00
|
|
|
if (auto regional_values = get_regional_values(locale); has_value(regional_values))
|
2022-07-06 12:21:32 +00:00
|
|
|
return regional_values;
|
2021-12-16 04:19:17 +00:00
|
|
|
|
2022-07-06 12:21:32 +00:00
|
|
|
auto return_default_values = [&]() { return get_regional_values("001"sv); };
|
LibUnicode: Parse and generate regional hour cycles
Unlike most data in the CLDR, hour cycles are not stored on a per-locale
basis. Instead, they are keyed by a string that is usually a region, but
sometimes is a locale. Therefore, given a locale, to determine the hour
cycles for that locale, we:
1. Check if the locale itself is assigned hour cycles.
2. If the locale has a region, check if that region is assigned hour
cycles.
3. Otherwise, maximize that locale, and if the maximized locale has
a region, check if that region is assigned hour cycles.
4. If the above all fail, fallback to the "001" region.
Further, each locale's default hour cycle is the first assigned hour
cycle.
2021-11-28 01:57:21 +00:00
|
|
|
|
2023-08-22 19:39:18 +00:00
|
|
|
auto language = parse_unicode_language_id(locale);
|
LibUnicode: Parse and generate regional hour cycles
Unlike most data in the CLDR, hour cycles are not stored on a per-locale
basis. Instead, they are keyed by a string that is usually a region, but
sometimes is a locale. Therefore, given a locale, to determine the hour
cycles for that locale, we:
1. Check if the locale itself is assigned hour cycles.
2. If the locale has a region, check if that region is assigned hour
cycles.
3. Otherwise, maximize that locale, and if the maximized locale has
a region, check if that region is assigned hour cycles.
4. If the above all fail, fallback to the "001" region.
Further, each locale's default hour cycle is the first assigned hour
cycle.
2021-11-28 01:57:21 +00:00
|
|
|
if (!language.has_value())
|
2022-07-06 12:21:32 +00:00
|
|
|
return return_default_values();
|
LibUnicode: Parse and generate regional hour cycles
Unlike most data in the CLDR, hour cycles are not stored on a per-locale
basis. Instead, they are keyed by a string that is usually a region, but
sometimes is a locale. Therefore, given a locale, to determine the hour
cycles for that locale, we:
1. Check if the locale itself is assigned hour cycles.
2. If the locale has a region, check if that region is assigned hour
cycles.
3. Otherwise, maximize that locale, and if the maximized locale has
a region, check if that region is assigned hour cycles.
4. If the above all fail, fallback to the "001" region.
Further, each locale's default hour cycle is the first assigned hour
cycle.
2021-11-28 01:57:21 +00:00
|
|
|
|
2024-06-08 17:23:26 +00:00
|
|
|
if (!language->region.has_value()) {
|
|
|
|
if (auto maximized = add_likely_subtags(language->to_string()); maximized.has_value())
|
|
|
|
language = parse_unicode_language_id(*maximized);
|
|
|
|
}
|
|
|
|
|
LibUnicode: Parse and generate regional hour cycles
Unlike most data in the CLDR, hour cycles are not stored on a per-locale
basis. Instead, they are keyed by a string that is usually a region, but
sometimes is a locale. Therefore, given a locale, to determine the hour
cycles for that locale, we:
1. Check if the locale itself is assigned hour cycles.
2. If the locale has a region, check if that region is assigned hour
cycles.
3. Otherwise, maximize that locale, and if the maximized locale has
a region, check if that region is assigned hour cycles.
4. If the above all fail, fallback to the "001" region.
Further, each locale's default hour cycle is the first assigned hour
cycle.
2021-11-28 01:57:21 +00:00
|
|
|
if (!language.has_value() || !language->region.has_value())
|
2022-07-06 12:21:32 +00:00
|
|
|
return return_default_values();
|
LibUnicode: Parse and generate regional hour cycles
Unlike most data in the CLDR, hour cycles are not stored on a per-locale
basis. Instead, they are keyed by a string that is usually a region, but
sometimes is a locale. Therefore, given a locale, to determine the hour
cycles for that locale, we:
1. Check if the locale itself is assigned hour cycles.
2. If the locale has a region, check if that region is assigned hour
cycles.
3. Otherwise, maximize that locale, and if the maximized locale has
a region, check if that region is assigned hour cycles.
4. If the above all fail, fallback to the "001" region.
Further, each locale's default hour cycle is the first assigned hour
cycle.
2021-11-28 01:57:21 +00:00
|
|
|
|
2023-08-22 20:41:36 +00:00
|
|
|
if (auto regional_values = get_regional_values(*language->region); has_value(regional_values))
|
2022-07-06 12:21:32 +00:00
|
|
|
return regional_values;
|
LibUnicode: Parse and generate regional hour cycles
Unlike most data in the CLDR, hour cycles are not stored on a per-locale
basis. Instead, they are keyed by a string that is usually a region, but
sometimes is a locale. Therefore, given a locale, to determine the hour
cycles for that locale, we:
1. Check if the locale itself is assigned hour cycles.
2. If the locale has a region, check if that region is assigned hour
cycles.
3. Otherwise, maximize that locale, and if the maximized locale has
a region, check if that region is assigned hour cycles.
4. If the above all fail, fallback to the "001" region.
Further, each locale's default hour cycle is the first assigned hour
cycle.
2021-11-28 01:57:21 +00:00
|
|
|
|
2022-07-06 12:21:32 +00:00
|
|
|
return return_default_values();
|
|
|
|
}
|
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
// 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)
|
2021-12-09 00:52:48 +00:00
|
|
|
{
|
2024-06-12 14:47:20 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2021-12-09 00:52:48 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
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;
|
|
|
|
|
|
|
|
bool changed_hour_cycle = false;
|
|
|
|
bool inside_quote = false;
|
|
|
|
|
|
|
|
for (i32 i = 0; i < skeleton.length(); ++i) {
|
|
|
|
switch (skeleton[i]) {
|
|
|
|
case '\'':
|
|
|
|
inside_quote = !inside_quote;
|
|
|
|
break;
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return changed_hour_cycle;
|
|
|
|
}
|
2021-12-09 00:52:48 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
static void apply_time_zone_to_formatter(icu::SimpleDateFormat& formatter, icu::Locale const& locale, StringView time_zone_identifier)
|
|
|
|
{
|
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
2021-12-09 00:52:48 +00:00
|
|
|
|
2024-09-03 15:20:20 +00:00
|
|
|
auto time_zone_data = TimeZoneData::for_time_zone(time_zone_identifier);
|
2021-12-09 00:52:48 +00:00
|
|
|
|
2024-09-03 15:20:20 +00:00
|
|
|
auto* calendar = icu::Calendar::createInstance(time_zone_data->time_zone(), locale, status);
|
2024-06-12 14:47:20 +00:00
|
|
|
VERIFY(icu_success(status));
|
2021-12-09 00:52:48 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
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));
|
2021-12-09 00:52:48 +00:00
|
|
|
}
|
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
formatter.adoptCalendar(calendar);
|
2021-12-09 00:52:48 +00:00
|
|
|
}
|
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
static bool is_formatted_range_actually_a_range(icu::FormattedDateInterval const& formatted)
|
2021-11-27 19:54:48 +00:00
|
|
|
{
|
2024-06-12 14:47:20 +00:00
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
|
|
|
|
|
|
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;
|
2021-11-27 19:54:48 +00:00
|
|
|
}
|
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
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);
|
|
|
|
}
|
2022-01-11 16:35:50 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
virtual ~DateTimeFormatImpl() override = default;
|
2022-01-11 16:35:50 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
virtual CalendarPattern const& chosen_pattern() const override { return m_pattern; }
|
2022-01-11 16:35:50 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
virtual String format(double time) const override
|
|
|
|
{
|
|
|
|
auto formatted_time = format_impl(time);
|
|
|
|
if (!formatted_time.has_value())
|
|
|
|
return {};
|
2022-01-11 16:35:50 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
return icu_string_to_string(*formatted_time);
|
|
|
|
}
|
2022-01-11 16:35:50 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
virtual Vector<Partition> format_to_parts(double time) const override
|
|
|
|
{
|
|
|
|
icu::FieldPositionIterator iterator;
|
2022-01-11 16:35:50 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
auto formatted_time = format_impl(time, &iterator);
|
|
|
|
if (!formatted_time.has_value())
|
|
|
|
return {};
|
2022-01-11 16:35:50 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
Vector<Partition> result;
|
2022-01-11 16:35:50 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
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));
|
|
|
|
};
|
2022-01-11 16:35:50 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
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();
|
2022-01-11 16:35:50 +00:00
|
|
|
}
|
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
if (previous_end_index < formatted_time->length())
|
|
|
|
create_partition(LITERAL_FIELD, previous_end_index, formatted_time->length());
|
|
|
|
|
|
|
|
return result;
|
2022-01-11 16:35:50 +00:00
|
|
|
}
|
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
virtual String format_range(double start, double end) const override
|
|
|
|
{
|
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
2022-01-11 16:35:50 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
auto formatted = format_range_impl(start, end);
|
|
|
|
if (!formatted.has_value())
|
|
|
|
return {};
|
2022-01-19 19:54:19 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
if (!is_formatted_range_actually_a_range(*formatted))
|
|
|
|
return format(start);
|
2022-01-11 16:35:50 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
auto formatted_time = formatted->toTempString(status);
|
|
|
|
if (icu_failure(status))
|
|
|
|
return {};
|
2022-01-11 16:35:50 +00:00
|
|
|
|
2024-08-01 15:30:17 +00:00
|
|
|
normalize_spaces(formatted_time);
|
2024-06-12 14:47:20 +00:00
|
|
|
return icu_string_to_string(formatted_time);
|
2022-01-11 16:35:50 +00:00
|
|
|
}
|
2022-01-11 17:06:26 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
virtual Vector<Partition> format_range_to_parts(double start, double end) const override
|
|
|
|
{
|
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
2022-01-11 17:06:26 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
auto formatted = format_range_impl(start, end);
|
|
|
|
if (!formatted.has_value())
|
|
|
|
return {};
|
2022-01-11 17:06:26 +00:00
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
if (!is_formatted_range_actually_a_range(*formatted))
|
|
|
|
return format_to_parts(start);
|
|
|
|
|
|
|
|
auto formatted_time = formatted->toTempString(status);
|
|
|
|
if (icu_failure(status))
|
|
|
|
return {};
|
|
|
|
|
2024-08-01 15:30:17 +00:00
|
|
|
normalize_spaces(formatted_time);
|
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
icu::ConstrainedFieldPosition position;
|
|
|
|
i32 previous_end_index = 0;
|
|
|
|
|
|
|
|
Vector<Partition> result;
|
2024-06-19 16:46:45 +00:00
|
|
|
Optional<PartitionRange> start_range;
|
|
|
|
Optional<PartitionRange> end_range;
|
2024-06-12 14:47:20 +00:00
|
|
|
|
|
|
|
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;
|
2024-06-19 16:46:45 +00:00
|
|
|
range = PartitionRange { position.getField(), position.getStart(), position.getLimit() };
|
2024-06-12 14:47:20 +00:00
|
|
|
} 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 {};
|
|
|
|
|
2024-08-01 15:30:17 +00:00
|
|
|
normalize_spaces(formatted_time);
|
2024-06-12 14:47:20 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-08-01 15:30:17 +00:00
|
|
|
// ICU 72 introduced the use of NBSP to separate time fields and day periods. All major browsers have found that
|
|
|
|
// this significantly breaks web compatibilty, and they all replace these spaces with normal ASCII spaces. See:
|
|
|
|
//
|
|
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1806042
|
|
|
|
// https://bugs.webkit.org/show_bug.cgi?id=252147
|
|
|
|
// https://issues.chromium.org/issues/40256057
|
|
|
|
static void normalize_spaces(icu::UnicodeString& string)
|
|
|
|
{
|
|
|
|
static char16_t NARROW_NO_BREAK_SPACE = 0x202f;
|
|
|
|
static char16_t THIN_SPACE = 0x2009;
|
|
|
|
|
|
|
|
for (i32 i = 0; i < string.length(); ++i) {
|
|
|
|
if (string[i] == NARROW_NO_BREAK_SPACE || string[i] == THIN_SPACE)
|
|
|
|
string.setCharAt(i, ' ');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-12 14:47:20 +00:00
|
|
|
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());
|
|
|
|
}
|
2022-01-11 17:06:26 +00:00
|
|
|
VERIFY_NOT_REACHED();
|
2024-06-12 14:47:20 +00:00
|
|
|
}()));
|
|
|
|
|
|
|
|
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));
|
2022-01-11 17:06:26 +00:00
|
|
|
}
|
2024-06-12 14:47:20 +00:00
|
|
|
|
|
|
|
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)));
|
2022-01-11 16:35:50 +00:00
|
|
|
}
|
2021-12-06 20:46:49 +00:00
|
|
|
|
2024-06-12 21:00:45 +00:00
|
|
|
static constexpr Weekday icu_calendar_day_to_weekday(UCalendarDaysOfWeek day)
|
|
|
|
{
|
|
|
|
switch (day) {
|
|
|
|
case UCAL_SUNDAY:
|
|
|
|
return Weekday::Sunday;
|
|
|
|
case UCAL_MONDAY:
|
|
|
|
return Weekday::Monday;
|
|
|
|
case UCAL_TUESDAY:
|
|
|
|
return Weekday::Tuesday;
|
|
|
|
case UCAL_WEDNESDAY:
|
|
|
|
return Weekday::Wednesday;
|
|
|
|
case UCAL_THURSDAY:
|
|
|
|
return Weekday::Thursday;
|
|
|
|
case UCAL_FRIDAY:
|
|
|
|
return Weekday::Friday;
|
|
|
|
case UCAL_SATURDAY:
|
|
|
|
return Weekday::Saturday;
|
|
|
|
}
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
}
|
|
|
|
|
|
|
|
WeekInfo week_info_of_locale(StringView locale)
|
|
|
|
{
|
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
|
|
|
|
|
|
auto locale_data = LocaleData::for_locale(locale);
|
|
|
|
if (!locale_data.has_value())
|
|
|
|
return {};
|
|
|
|
|
|
|
|
auto calendar = adopt_own_if_nonnull(icu::Calendar::createInstance(locale_data->locale(), status));
|
|
|
|
if (icu_failure(status))
|
|
|
|
return {};
|
|
|
|
|
|
|
|
WeekInfo week_info;
|
|
|
|
week_info.minimal_days_in_first_week = calendar->getMinimalDaysInFirstWeek();
|
|
|
|
|
|
|
|
if (auto day = calendar->getFirstDayOfWeek(status); icu_success(status))
|
|
|
|
week_info.first_day_of_week = icu_calendar_day_to_weekday(day);
|
|
|
|
|
|
|
|
auto append_if_weekend = [&](auto day) {
|
|
|
|
auto type = calendar->getDayOfWeekType(day, status);
|
|
|
|
if (icu_failure(status))
|
|
|
|
return;
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case UCAL_WEEKEND_ONSET:
|
|
|
|
case UCAL_WEEKEND_CEASE:
|
|
|
|
case UCAL_WEEKEND:
|
|
|
|
week_info.weekend_days.append(icu_calendar_day_to_weekday(day));
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
append_if_weekend(UCAL_SUNDAY);
|
|
|
|
append_if_weekend(UCAL_MONDAY);
|
|
|
|
append_if_weekend(UCAL_TUESDAY);
|
|
|
|
append_if_weekend(UCAL_WEDNESDAY);
|
|
|
|
append_if_weekend(UCAL_THURSDAY);
|
|
|
|
append_if_weekend(UCAL_FRIDAY);
|
|
|
|
append_if_weekend(UCAL_SATURDAY);
|
|
|
|
|
|
|
|
return week_info;
|
|
|
|
}
|
|
|
|
|
2021-11-27 19:54:48 +00:00
|
|
|
}
|