ladybird/Libraries/LibUnicode/Calendar.cpp
Timothy Flynn ef899027c5 LibUnicode: Use a calendar with icu4x glue to format lunisolar calendars
Commit 86c8a57794 caused one regression in
test/intl402/DateTimeFormat. It is expected that Intl.DateTimeFormat and
Temporal produce consistent results.

Due to the píngqì approximation implemented in icu4x, it actually does
not totally align with icu4c for lunisolar calendars at extreme dates.
Ideally, icu4x will one day support all Intl.DateTimeFormat operations
as well. But for now, the fix is to create a custom icu::Calendar class
for lunisolar calendars that pipes calculations to icu4x.
2026-03-12 17:29:59 -05:00

196 lines
7.2 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2026, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibUnicode/Calendar.h>
struct FfiISODate {
i32 year;
u8 month;
u8 day;
};
struct FfiOptionalISODate {
FfiISODate iso_date;
bool has_value;
};
struct FfiCalendarDate {
i32 year;
u8 month;
u8 month_code[5];
u8 month_code_length;
u8 day;
u8 day_of_week;
u16 day_of_year;
u8 days_in_week;
u8 days_in_month;
u16 days_in_year;
u8 months_in_year;
bool in_leap_year;
};
extern "C" {
FfiCalendarDate icu_iso_date_to_calendar_date(u8 const* calendar, size_t calendar_length, i32 iso_year, u8 iso_month, u8 iso_day);
FfiOptionalISODate icu_calendar_date_to_iso_date(u8 const* calendar, size_t calendar_length, i32 arithmetic_year, u8 ordinal_month, u8 day);
FfiOptionalISODate icu_iso_year_and_month_code_to_iso_date(u8 const* calendar, size_t calendar_length, i32 iso_year, u8 const* month_code, size_t month_code_length, u8 day);
FfiOptionalISODate icu_calendar_year_and_month_code_to_iso_date(u8 const* calendar, size_t calendar_length, i32 arithmetic_year, u8 const* month_code, size_t month_code_length, u8 day);
u8 icu_calendar_months_in_year(u8 const* calendar, size_t calendar_length, i32 arithmetic_year);
u8 icu_calendar_days_in_month(u8 const* calendar, size_t calendar_length, i32 arithmetic_year, u8 ordinal_month);
u8 icu_calendar_max_days_in_month_code(u8 const* calendar, size_t calendar_length, u8 const* month_code, size_t month_code_length);
bool icu_year_contains_month_code(u8 const* calendar, size_t calendar_length, i32 arithmetic_year, u8 const* month_code, size_t month_code_length);
} // extern "C"
namespace Unicode {
// https://tc39.es/proposal-temporal/#prod-MonthCode
static constexpr bool is_valid_month_code_string(StringView month_code)
{
// MonthCode :::
// M00L
// M0 NonZeroDigit L[opt]
// M NonZeroDigit DecimalDigit L[opt]
auto length = month_code.length();
if (length != 3 && length != 4)
return false;
if (month_code[0] != 'M')
return false;
if (!is_ascii_digit(month_code[1]) || !is_ascii_digit(month_code[2]))
return false;
if (length == 3 && month_code[1] == '0' && month_code[2] == '0')
return false;
if (length == 4 && month_code[3] != 'L')
return false;
return true;
}
// 12.2.1 ParseMonthCode ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-parsemonthcode
Optional<MonthCode> parse_month_code(StringView month_code)
{
// 3. If ParseText(StringToCodePoints(monthCode), MonthCode) is a List of errors, throw a RangeError exception.
if (!is_valid_month_code_string(month_code))
return {};
// 4. Let isLeapMonth be false.
auto is_leap_month = false;
// 5. If the length of monthCode = 4, then
if (month_code.length() == 4) {
// a. Assert: The fourth code unit of monthCode is 0x004C (LATIN CAPITAL LETTER L).
VERIFY(month_code[3] == 'L');
// b. Set isLeapMonth to true.
is_leap_month = true;
}
// 6. Let monthCodeDigits be the substring of monthCode from 1 to 3.
auto month_code_digits = month_code.substring_view(1, 2);
// 7. Let monthNumber be (StringToNumber(monthCodeDigits)).
auto month_number = month_code_digits.to_number<u8>().value();
// 8. Return the Record { [[MonthNumber]]: monthNumber, [[IsLeapMonth]]: isLeapMonth }.
return MonthCode { month_number, is_leap_month };
}
// 12.2.2 CreateMonthCode ( monthNumber, isLeapMonth ), https://tc39.es/proposal-temporal/#sec-temporal-createmonthcode
String create_month_code(u8 month_number, bool is_leap_month)
{
// 1. Assert: If isLeapMonth is false, monthNumber > 0.
if (!is_leap_month)
VERIFY(month_number > 0);
// 2. Let numberPart be ToZeroPaddedDecimalString(monthNumber, 2).
// 3. If isLeapMonth is true, then
if (is_leap_month) {
// a. Return the string-concatenation of the code unit 0x004D (LATIN CAPITAL LETTER M), numberPart, and the
// code unit 0x004C (LATIN CAPITAL LETTER L).
return MUST(String::formatted("M{:02}L", month_number));
}
// 4. Return the string-concatenation of the code unit 0x004D (LATIN CAPITAL LETTER M) and numberPart.
return MUST(String::formatted("M{:02}", month_number));
}
CalendarDate iso_date_to_calendar_date(String const& calendar, ISODate iso_date)
{
auto result = icu_iso_date_to_calendar_date(calendar.bytes().data(), calendar.bytes().size(), iso_date.year, iso_date.month, iso_date.day);
return CalendarDate {
.era = {},
.era_year = {},
.year = result.year,
.month = result.month,
.month_code = String::from_utf8_without_validation({ result.month_code, result.month_code_length }),
.day = result.day,
.day_of_week = result.day_of_week,
.day_of_year = result.day_of_year,
.week_of_year = {},
.days_in_week = result.days_in_week,
.days_in_month = result.days_in_month,
.days_in_year = result.days_in_year,
.months_in_year = result.months_in_year,
.in_leap_year = result.in_leap_year,
};
}
Optional<ISODate> calendar_date_to_iso_date(String const& calendar, i32 year, u8 month, u8 day)
{
auto result = icu_calendar_date_to_iso_date(calendar.bytes().data(), calendar.bytes().size(), year, month, day);
if (!result.has_value)
return {};
return ISODate { result.iso_date.year, result.iso_date.month, result.iso_date.day };
}
Optional<ISODate> iso_year_and_month_code_to_iso_date(String const& calendar, i32 year, StringView month_code, u8 day)
{
auto result = icu_iso_year_and_month_code_to_iso_date(calendar.bytes().data(), calendar.bytes().size(), year, month_code.bytes().data(), month_code.length(), day);
if (!result.has_value)
return {};
return ISODate { result.iso_date.year, result.iso_date.month, result.iso_date.day };
}
Optional<ISODate> calendar_year_and_month_code_to_iso_date(String const& calendar, i32 arithmetic_year, StringView month_code, u8 day)
{
auto result = icu_calendar_year_and_month_code_to_iso_date(calendar.bytes().data(), calendar.bytes().size(), arithmetic_year, month_code.bytes().data(), month_code.length(), day);
if (!result.has_value)
return {};
return ISODate { result.iso_date.year, result.iso_date.month, result.iso_date.day };
}
u8 calendar_months_in_year(String const& calendar, i32 arithmetic_year)
{
return icu_calendar_months_in_year(calendar.bytes().data(), calendar.bytes().size(), arithmetic_year);
}
u8 calendar_days_in_month(String const& calendar, i32 arithmetic_year, u8 ordinal_month)
{
return icu_calendar_days_in_month(calendar.bytes().data(), calendar.bytes().size(), arithmetic_year, ordinal_month);
}
u8 calendar_max_days_in_month_code(String const& calendar, StringView month_code)
{
return icu_calendar_max_days_in_month_code(calendar.bytes().data(), calendar.bytes().size(), month_code.bytes().data(), month_code.length());
}
bool calendar_year_contains_month_code(String const& calendar, i32 arithmetic_year, StringView month_code)
{
return icu_year_contains_month_code(calendar.bytes().data(), calendar.bytes().size(), arithmetic_year, month_code.bytes().data(), month_code.length());
}
}