From 86c8a5779454471eea4820fade9899757e9b2d73 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 10 Mar 2026 09:19:29 -0400 Subject: [PATCH] LibJS+LibUnicode: Use icu4x for Temporal calendar operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the icu4c-based calendar implementation with one built on the icu4x Rust crate (icu_calendar). The icu4c API does not expose the píngqì month-assignment algorithm used by the Chinese and Dangi lunisolar calendars. Our old code had to approximate this by walking months via epoch millisecond arithmetic and manually tracking leap month positions, which produced incorrect month codes and ordinal month numbers for certain years. The icu4x calendar crate handles píngqì natively. With this patch, which is almost a 1-to-1 mapping of ICU invocations, we pass 100% of all Temporal test262 tests. The end goal might be to use icu4x for all of our ICU needs. But it does not yet provide the APIs needed for all ECMA-402 prototypes. --- Cargo.lock | 324 ++++++++++ Cargo.toml | 1 + Libraries/LibJS/CMakeLists.txt | 2 +- Libraries/LibJS/Runtime/Temporal/Calendar.cpp | 28 +- Libraries/LibJS/Runtime/Temporal/Calendar.h | 5 +- Libraries/LibUnicode/CMakeLists.txt | 6 + Libraries/LibUnicode/Calendar.cpp | 551 ++-------------- Libraries/LibUnicode/Calendar.h | 15 +- Libraries/LibUnicode/DateTimeFormat.cpp | 10 +- Libraries/LibUnicode/ICU.cpp | 51 +- Libraries/LibUnicode/ICU.h | 31 +- Libraries/LibUnicode/Rust/Cargo.toml | 13 + Libraries/LibUnicode/Rust/src/calendar.rs | 604 ++++++++++++++++++ Libraries/LibUnicode/Rust/src/lib.rs | 7 + Meta/CMake/flatpak/cargo-sources.json | 248 +++++++ .../Temporal/PlainDate/PlainDate.from.js | 39 ++ .../PlainDate.prototype.daysInYear.js | 19 + .../PlainDate.prototype.monthCode.js | 37 ++ 18 files changed, 1379 insertions(+), 612 deletions(-) create mode 100644 Libraries/LibUnicode/Rust/Cargo.toml create mode 100644 Libraries/LibUnicode/Rust/src/calendar.rs create mode 100644 Libraries/LibUnicode/Rust/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 173e3b7617..b6504dbd3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,130 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" name = "bytecode_def" version = "0.1.0" +[[package]] +name = "calendrical_calculations" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" +dependencies = [ + "core_maths", + "displaydoc", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "icu_calendar" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar_data", + "icu_locale", + "icu_locale_core", + "icu_provider", + "ixdtf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_calendar_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c5f1d16b4c3a2642d3a719f18f6b06070ab0aef246a6418130c955ae08aa831" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ixdtf" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992" + [[package]] name = "libjs_rust" version = "0.1.0" @@ -23,6 +147,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libunicode_rust" +version = "0.1.0" +dependencies = [ + "calendrical_calculations", + "icu_calendar", +] + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "num-bigint" version = "0.4.6" @@ -51,8 +195,188 @@ dependencies = [ "autocfg", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index a31dd6f073..1ba28ffd2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "Libraries/LibJS/Rust", + "Libraries/LibUnicode/Rust", ] exclude = [ "Libraries/LibJS/AsmIntGen", diff --git a/Libraries/LibJS/CMakeLists.txt b/Libraries/LibJS/CMakeLists.txt index ea5d184159..bf205b829a 100644 --- a/Libraries/LibJS/CMakeLists.txt +++ b/Libraries/LibJS/CMakeLists.txt @@ -307,7 +307,7 @@ endif() target_link_libraries(LibJS PUBLIC JSClangPlugin) if (ENABLE_RUST) - corrosion_import_crate(MANIFEST_PATH Rust/Cargo.toml) + corrosion_import_crate(MANIFEST_PATH Rust/Cargo.toml CRATES libjs_rust) target_link_libraries(LibJS PRIVATE libjs_rust) target_compile_definitions(LibJS PRIVATE ENABLE_RUST) install(TARGETS libjs_rust EXPORT LagomTargets diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp index 0739b71b29..34c52833fc 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -1790,36 +1790,12 @@ bool year_contains_month_code(String const& calendar, i32 arithmetic_year, Strin VERIFY(is_valid_month_code_for_calendar(calendar, month_code)); // 2. If ! ParseMonthCode(monthCode).[[IsLeap]] is false, return true. - auto [month_number, is_leap_month] = parse_month_code(month_code); - if (!is_leap_month) + if (!parse_month_code(month_code).is_leap_month) return true; // 3. Return whether the leap month indicated by monthCode exists in the year arithmeticYear in calendar, using // calendar-dependent behaviour. - if (calendar.is_one_of("chinese"sv, "dangi"sv)) { - auto months_in_year = calendar_months_in_year(calendar, arithmetic_year); - if (months_in_year <= 12) - return false; - - // Check each ordinal month to see if it matches the leap month code. - for (u8 month = 1; month <= months_in_year; ++month) { - auto info = Unicode::chinese_ordinal_month_code(calendar, arithmetic_year, month); - if (info.has_value() && info->is_leap_month && info->month_number == month_number) - return true; - } - - return false; - } - - if (calendar == "hebrew"sv) { - if (month_number != Unicode::HEBREW_ADAR_I_MONTH_NUMBER) - return false; - - auto months_in_year = calendar_months_in_year(calendar, arithmetic_year); - return months_in_year == 13; - } - - return false; + return Unicode::calendar_year_contains_month_code(calendar, arithmetic_year, month_code); } // 4.1.6 ConstrainMonthCode ( calendar, arithmeticYear, monthCode, overflow ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-constrainmonthcode diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Libraries/LibJS/Runtime/Temporal/Calendar.h index a4d3f9a90f..911ba864d9 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.h +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -22,7 +22,10 @@ namespace JS::Temporal { extern String ISO8601_CALENDAR; // 12.2 Month Codes, https://tc39.es/proposal-temporal/#sec-temporal-month-codes -using MonthCode = Unicode::MonthCode; +struct MonthCode { + u8 month_number { 0 }; + bool is_leap_month { false }; +}; // 12.3.1 Calendar Date Records, https://tc39.es/proposal-temporal/#sec-temporal-calendar-date-records using CalendarDate = Unicode::CalendarDate; diff --git a/Libraries/LibUnicode/CMakeLists.txt b/Libraries/LibUnicode/CMakeLists.txt index 62b2b9b98b..f05e55f606 100644 --- a/Libraries/LibUnicode/CMakeLists.txt +++ b/Libraries/LibUnicode/CMakeLists.txt @@ -28,6 +28,12 @@ ladybird_lib(LibUnicode unicode) find_package(ICU 78.2 EXACT REQUIRED COMPONENTS data i18n uc) target_link_libraries(LibUnicode PRIVATE ICU::i18n ICU::uc ICU::data) +corrosion_import_crate(MANIFEST_PATH Rust/Cargo.toml CRATES libunicode_rust) +target_link_libraries(LibUnicode PRIVATE libunicode_rust) +install(TARGETS libunicode_rust EXPORT LagomTargets + ARCHIVE COMPONENT Lagom_Development +) + # FIXME: Add support for building LibGfx in sanitize # lld-link: error: /failifmismatch: mismatch detected for 'annotate_string': # >>> lagom-unicode.lib(TimeZone.cpp.obj) has value 1 diff --git a/Libraries/LibUnicode/Calendar.cpp b/Libraries/LibUnicode/Calendar.cpp index 18cf8b7b52..3c0b684bd5 100644 --- a/Libraries/LibUnicode/Calendar.cpp +++ b/Libraries/LibUnicode/Calendar.cpp @@ -4,534 +4,107 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include #include -#include -#include +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_calendar_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); + +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 { -static icu::Calendar& proleptic_gregorian_calendar() -{ - static auto calendar_data = CalendarData::for_calendar("gregory"_string); - VERIFY(calendar_data.has_value()); - - return calendar_data->calendar(); -} - -static void set_icu_calendar_to_iso_date(icu::Calendar& calendar, ISODate iso_date) -{ - UErrorCode status = U_ZERO_ERROR; - - auto& gregorian = proleptic_gregorian_calendar(); - gregorian.clear(); - gregorian.set(UCAL_EXTENDED_YEAR, iso_date.year); - gregorian.set(UCAL_MONTH, iso_date.month - 1); - gregorian.set(UCAL_DATE, iso_date.day); - - auto epoch_ms = ICU_MUST(gregorian.getTime(status)); - ICU_MUST_VOID(calendar.setTime(epoch_ms, status)); -} - -struct ChineseMonthEntry { - u8 month { 0 }; - u8 days_in_month { 0 }; - i32 is_leap_month { 0 }; -}; - -struct ChineseYearLayout { - u8 month_count { 0 }; - Array months; // This is a 1-based array to map to ICU ordinal months. -}; - -struct ChineseYearKey { - String calendar; - i32 extended_year { 0 }; - - bool operator==(ChineseYearKey const& other) const = default; -}; - -struct ChineseExtendedYearKey { - String calendar; - i32 arithmetic_year { 0 }; - - bool operator==(ChineseExtendedYearKey const&) const = default; -}; - -static HashMap s_chinese_year_layout_cache; -static HashMap s_chinese_extended_year_cache; - -static i32 chinese_arithmetic_year_to_extended_year(String const& calendar_name, icu::Calendar& calendar, i32 arithmetic_year) -{ - return s_chinese_extended_year_cache.ensure({ calendar_name, arithmetic_year }, [&]() { - UErrorCode status = U_ZERO_ERROR; - - ISODate approximate_iso_date { arithmetic_year, 6, 15 }; - set_icu_calendar_to_iso_date(calendar, approximate_iso_date); - - return ICU_MUST(calendar.get(UCAL_EXTENDED_YEAR, status)); - }); -} - -static ChineseYearLayout const& chinese_year_layout(String const& calendar_name, icu::Calendar& calendar, i32 extended_year) -{ - return s_chinese_year_layout_cache.ensure({ calendar_name, extended_year }, [&]() { - UErrorCode status = U_ZERO_ERROR; - ChineseYearLayout layout; - - auto cloned = adopt_own(*calendar.clone()); - cloned->set(UCAL_MONTH, 0); - cloned->set(UCAL_IS_LEAP_MONTH, 0); - cloned->set(UCAL_DATE, 1); - - auto save_month_layout = [&]() { - layout.months[++layout.month_count] = ChineseMonthEntry { - .month = static_cast(ICU_MUST(cloned->get(UCAL_MONTH, status))), - .days_in_month = static_cast(ICU_MUST(cloned->getActualMaximum(UCAL_DATE, status))), - .is_leap_month = ICU_MUST(cloned->get(UCAL_IS_LEAP_MONTH, status)), - }; - }; - - auto start_year = ICU_MUST(cloned->get(UCAL_EXTENDED_YEAR, status)); - save_month_layout(); - - for (int i = 0; i < 13; ++i) { - cloned->add(UCAL_MONTH, 1, status); - if (icu_failure(status)) - break; - - auto year = cloned->get(UCAL_EXTENDED_YEAR, status); - if (icu_failure(status) || year != start_year) - break; - - save_month_layout(); - } - - return layout; - }); -} - -static ChineseYearLayout const& chinese_year_layout(String const& calendar_name, icu::Calendar& calendar) -{ - UErrorCode status = U_ZERO_ERROR; - - auto extended_year = ICU_MUST(calendar.get(UCAL_EXTENDED_YEAR, status)); - return chinese_year_layout(calendar_name, calendar, extended_year); -} - -static bool is_hebrew_leap_year(icu::Calendar& calendar) -{ - UErrorCode status = U_ZERO_ERROR; - - auto cloned = adopt_own(*calendar.clone()); - cloned->set(UCAL_MONTH, HEBREW_ADAR_I_MONTH_NUMBER); - cloned->set(UCAL_DATE, 1); - - auto month = ICU_MUST(cloned->get(UCAL_MONTH, status)); - return month == HEBREW_ADAR_I_MONTH_NUMBER; -} - -static i32 hebrew_ordinal_month_to_icu_month(icu::Calendar& calendar, u8 ordinal_month) -{ - if (is_hebrew_leap_year(calendar)) - return ordinal_month - 1; - return ordinal_month <= HEBREW_ADAR_I_MONTH_NUMBER ? ordinal_month - 1 : ordinal_month; -} - -static u8 compute_ordinal_month(icu::Calendar& calendar, String const& calendar_name) -{ - UErrorCode status = U_ZERO_ERROR; - - auto month = static_cast(ICU_MUST(calendar.get(UCAL_MONTH, status))); - - if (calendar_name.is_one_of("chinese"sv, "dangi"sv)) { - auto const& layout = chinese_year_layout(calendar_name, calendar); - auto is_leap_month = ICU_MUST(calendar.get(UCAL_IS_LEAP_MONTH, status)); - - for (u8 ordinal = 1; ordinal <= layout.month_count; ++ordinal) { - if (layout.months[ordinal].month == month && layout.months[ordinal].is_leap_month == is_leap_month) - return ordinal; - } - - VERIFY_NOT_REACHED(); - } - - if (calendar_name == "hebrew"sv) { - if (is_hebrew_leap_year(calendar)) - return month + 1; - return month <= HEBREW_ADAR_I_MONTH_NUMBER ? month + 1 : month; - } - - return month + 1; -} - -struct ICUMonthCode { - i32 month { 0 }; - i32 is_leap_month { 0 }; -}; -static ICUMonthCode month_code_to_icu_fields(StringView month_code, StringView calendar) -{ - auto month_number = month_code.substring_view(1, 2).to_number().value(); - bool is_leap_month = month_code.length() == 4 && month_code[3] == 'L'; - - if (calendar == "hebrew"sv) { - if (is_leap_month || month_number > HEBREW_ADAR_I_MONTH_NUMBER) - return { month_number, is_leap_month }; - return { month_number - 1, 0 }; - } - - return { month_number - 1, is_leap_month }; -} - -static String compute_month_code(icu::Calendar& calendar, String const& calendar_name) -{ - UErrorCode status = U_ZERO_ERROR; - - auto month = static_cast(ICU_MUST(calendar.get(UCAL_MONTH, status))); - auto is_leap_month = ICU_MUST(calendar.get(UCAL_IS_LEAP_MONTH, status)); - - if (is_leap_month != 0) { - if (calendar_name == "hebrew"sv) - return String::from_utf8_without_validation(HEBREW_ADAR_I_MONTH_CODE.bytes()); - return MUST(String::formatted("M{:02}L", month + 1)); - } - - if (calendar_name == "hebrew"sv) { - if (month < HEBREW_ADAR_I_MONTH_NUMBER) - return MUST(String::formatted("M{:02}", month + 1)); - if (month == HEBREW_ADAR_I_MONTH_NUMBER && is_hebrew_leap_year(calendar)) - return String::from_utf8_without_validation(HEBREW_ADAR_I_MONTH_CODE.bytes()); - return MUST(String::formatted("M{:02}", month)); - } - - return MUST(String::formatted("M{:02}", month + 1)); -} - -static i32 arithmetic_year_to_extended_year(String const& calendar, i32 arithmetic_year) -{ - if (calendar == "buddhist"sv) - return arithmetic_year + BUDDHIST_EPOCH_ISO_YEAR; - if (calendar == "roc"sv) - return arithmetic_year + ROC_EPOCH_ISO_YEAR; - return arithmetic_year; -} - -static i32 extended_year_to_arithmetic_year(String const& calendar, i32 extended_year) -{ - if (calendar == "buddhist"sv) - return extended_year - BUDDHIST_EPOCH_ISO_YEAR; - if (calendar == "roc"sv) - return extended_year - ROC_EPOCH_ISO_YEAR; - return extended_year; -} - -static void set_calendar_to_arithmetic_year(icu::Calendar& calendar, String const& calendar_name, i32 arithmetic_year) -{ - auto extended_year = calendar_name.is_one_of("chinese"sv, "dangi"sv) - ? chinese_arithmetic_year_to_extended_year(calendar_name, calendar, arithmetic_year) - : arithmetic_year_to_extended_year(calendar_name, arithmetic_year); - - calendar.set(UCAL_EXTENDED_YEAR, extended_year); -} - CalendarDate iso_date_to_calendar_date(String const& calendar, ISODate iso_date) { - UErrorCode status = U_ZERO_ERROR; - - auto calendar_data = CalendarData::for_calendar(calendar); - if (!calendar_data.has_value()) - return {}; - - auto& icu_calendar = calendar_data->calendar(); - set_icu_calendar_to_iso_date(icu_calendar, iso_date); - - i32 arithmetic_year = 0; - if (calendar.is_one_of("chinese"sv, "dangi"sv)) { - // For Chinese/Dangi calendars, the arithmetic year is the ISO year of the start of the current calendar year. - auto year_start = adopt_own(*icu_calendar.clone()); - year_start->set(UCAL_MONTH, 0); - year_start->set(UCAL_DATE, 1); - - auto epoch_ms = ICU_MUST(year_start->getTime(status)); - - auto& gregorian = proleptic_gregorian_calendar(); - ICU_MUST_VOID(gregorian.setTime(epoch_ms, status)); - - arithmetic_year = ICU_MUST(gregorian.get(UCAL_EXTENDED_YEAR, status)); - } else { - auto extended_year = ICU_MUST(icu_calendar.get(UCAL_EXTENDED_YEAR, status)); - arithmetic_year = extended_year_to_arithmetic_year(calendar, extended_year); - } - - auto ordinal_month = compute_ordinal_month(icu_calendar, calendar); - auto month_code = compute_month_code(icu_calendar, calendar); - auto day = static_cast(ICU_MUST(icu_calendar.get(UCAL_DATE, status))); - - // In ICU, the days of the week begin with Sunday=1. Temporal expects Monday=1 and Sunday=7 instead. - auto day_of_week = static_cast(ICU_MUST(icu_calendar.get(UCAL_DAY_OF_WEEK, status))); - day_of_week = day_of_week == UCAL_SUNDAY ? 7 : day_of_week - 1; - - auto day_of_year = static_cast(ICU_MUST(icu_calendar.get(UCAL_DAY_OF_YEAR, status))); - auto days_in_month = static_cast(ICU_MUST(icu_calendar.getActualMaximum(UCAL_DATE, status))); - auto days_in_year = static_cast(ICU_MUST(icu_calendar.getActualMaximum(UCAL_DAY_OF_YEAR, status))); - auto months_in_year = calendar_months_in_year(calendar, arithmetic_year); - auto in_leap_year = ICU_MUST(icu_calendar.inTemporalLeapYear(status)); + 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 = arithmetic_year, - .month = ordinal_month, - .month_code = move(month_code), - .day = day, - .day_of_week = day_of_week, - .day_of_year = day_of_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 = 7, - .days_in_month = days_in_month, - .days_in_year = days_in_year, - .months_in_year = months_in_year, - .in_leap_year = in_leap_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 calendar_date_to_iso_date(String const& calendar, i32 arithmetic_year, u8 ordinal_month, u8 day) +Optional calendar_date_to_iso_date(String const& calendar, i32 year, u8 month, u8 day) { - UErrorCode status = U_ZERO_ERROR; - - auto calendar_data = CalendarData::for_calendar(calendar); - if (!calendar_data.has_value()) + auto result = icu_calendar_date_to_iso_date(calendar.bytes().data(), calendar.bytes().size(), year, month, day); + if (!result.has_value) return {}; - auto& icu_calendar = calendar_data->calendar(); - icu_calendar.clear(); - - set_calendar_to_arithmetic_year(icu_calendar, calendar, arithmetic_year); - - if (calendar.is_one_of("chinese"sv, "dangi"sv)) { - auto const& layout = chinese_year_layout(calendar, icu_calendar); - - if (ordinal_month >= 1 && ordinal_month <= layout.month_count) { - icu_calendar.set(UCAL_MONTH, layout.months[ordinal_month].month); - icu_calendar.set(UCAL_IS_LEAP_MONTH, layout.months[ordinal_month].is_leap_month); - } - - icu_calendar.set(UCAL_DATE, day); - } else if (calendar == "hebrew"sv) { - auto icu_month = hebrew_ordinal_month_to_icu_month(icu_calendar, ordinal_month); - icu_calendar.set(UCAL_MONTH, icu_month); - icu_calendar.set(UCAL_DATE, day); - } else { - icu_calendar.set(UCAL_MONTH, ordinal_month - 1); - icu_calendar.set(UCAL_DATE, day); - } - - icu_calendar.set(UCAL_HOUR_OF_DAY, 12); - - auto epoch_ms = ICU_MUST(icu_calendar.getTime(status)); - - auto& gregorian = proleptic_gregorian_calendar(); - ICU_MUST_VOID(gregorian.setTime(epoch_ms, status)); - - auto iso_year = ICU_MUST(gregorian.get(UCAL_EXTENDED_YEAR, status)); - auto iso_month = ICU_MUST(gregorian.get(UCAL_MONTH, status)) + 1; - auto iso_day = ICU_MUST(gregorian.get(UCAL_DATE, status)); - - return ISODate { iso_year, static_cast(iso_month), static_cast(iso_day) }; + return ISODate { result.iso_date.year, result.iso_date.month, result.iso_date.day }; } Optional calendar_month_code_to_iso_date(String const& calendar, i32 year, StringView month_code, u8 day) { - UErrorCode status = U_ZERO_ERROR; - - auto calendar_data = CalendarData::for_calendar(calendar); - if (!calendar_data.has_value()) + auto result = icu_calendar_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 {}; - auto& icu_calendar = calendar_data->calendar(); - auto& gregorian = proleptic_gregorian_calendar(); - - set_icu_calendar_to_iso_date(icu_calendar, { year, 1, 1 }); - auto start_extended_year = ICU_MUST(icu_calendar.get(UCAL_EXTENDED_YEAR, status)); - - set_icu_calendar_to_iso_date(icu_calendar, { year, 12, 31 }); - auto end_extended_year = ICU_MUST(icu_calendar.get(UCAL_EXTENDED_YEAR, status)); - - auto [icu_month, icu_is_leap_month] = month_code_to_icu_fields(month_code, calendar); - auto is_chinese_or_dangi = calendar.is_one_of("chinese"sv, "dangi"sv); - - Optional best_iso_date; - - for (auto extended_year = start_extended_year; extended_year <= end_extended_year; ++extended_year) { - icu_calendar.clear(); - icu_calendar.set(UCAL_EXTENDED_YEAR, extended_year); - icu_calendar.set(UCAL_MONTH, icu_month); - if (is_chinese_or_dangi) - icu_calendar.set(UCAL_IS_LEAP_MONTH, icu_is_leap_month); - icu_calendar.set(UCAL_DATE, day); - icu_calendar.set(UCAL_HOUR_OF_DAY, 12); - - auto epoch_ms = ICU_MUST(icu_calendar.getTime(status)); - ICU_MUST_VOID(gregorian.setTime(epoch_ms, status)); - - auto resolved_month = ICU_MUST(icu_calendar.get(UCAL_MONTH, status)); - if (resolved_month != icu_month) - continue; - - if (is_chinese_or_dangi) { - auto resolved_is_leap_month = ICU_MUST(icu_calendar.get(UCAL_IS_LEAP_MONTH, status)); - if (resolved_is_leap_month != icu_is_leap_month) - continue; - } - - auto resolved_day = ICU_MUST(icu_calendar.get(UCAL_DATE, status)); - if (resolved_day != day) - continue; - - auto iso_year = static_cast(ICU_MUST(gregorian.get(UCAL_EXTENDED_YEAR, status))); - if (iso_year != year) - continue; - - auto iso_month = static_cast(ICU_MUST(gregorian.get(UCAL_MONTH, status)) + 1); - auto iso_day = static_cast(ICU_MUST(gregorian.get(UCAL_DATE, status))); - - if (!best_iso_date.has_value() || iso_month > best_iso_date->month || (iso_month == best_iso_date->month && iso_day > best_iso_date->day)) - best_iso_date = ISODate { iso_year, iso_month, iso_day }; - } - - return best_iso_date; + 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) { - UErrorCode status = U_ZERO_ERROR; - - auto calendar_data = CalendarData::for_calendar(calendar); - if (!calendar_data.has_value()) - return 12; - - auto& icu_calendar = calendar_data->calendar(); - - if (calendar.is_one_of("chinese"sv, "dangi"sv)) { - auto extended_year = chinese_arithmetic_year_to_extended_year(calendar, icu_calendar, arithmetic_year); - return chinese_year_layout(calendar, icu_calendar, extended_year).month_count; - } - - icu_calendar.clear(); - set_calendar_to_arithmetic_year(icu_calendar, calendar, arithmetic_year); - icu_calendar.set(UCAL_MONTH, 0); - icu_calendar.set(UCAL_IS_LEAP_MONTH, 0); - icu_calendar.set(UCAL_DATE, 1); - - if (calendar == "hebrew"sv) - return is_hebrew_leap_year(icu_calendar) ? 13 : 12; - - return static_cast(ICU_MUST(icu_calendar.getActualMaximum(UCAL_MONTH, status)) + 1); + 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) { - UErrorCode status = U_ZERO_ERROR; - - auto calendar_data = CalendarData::for_calendar(calendar); - if (!calendar_data.has_value()) - return 30; - - auto& icu_calendar = calendar_data->calendar(); - - if (calendar.is_one_of("chinese"sv, "dangi"sv)) { - auto extended_year = chinese_arithmetic_year_to_extended_year(calendar, icu_calendar, arithmetic_year); - auto const& layout = chinese_year_layout(calendar, icu_calendar, extended_year); - - if (ordinal_month >= 1 && ordinal_month <= layout.month_count) - return layout.months[ordinal_month].days_in_month; - - return 30; - } - - icu_calendar.clear(); - set_calendar_to_arithmetic_year(icu_calendar, calendar, arithmetic_year); - - if (calendar == "hebrew"sv) { - auto icu_month = hebrew_ordinal_month_to_icu_month(icu_calendar, ordinal_month); - icu_calendar.set(UCAL_MONTH, icu_month); - } else { - icu_calendar.set(UCAL_MONTH, ordinal_month - 1); - } - - icu_calendar.set(UCAL_DATE, 1); - - return static_cast(ICU_MUST(icu_calendar.getActualMaximum(UCAL_DATE, status))); + 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) { - UErrorCode status = U_ZERO_ERROR; - - auto calendar_data = CalendarData::for_calendar(calendar); - if (!calendar_data.has_value()) - return 30; - - auto& icu_calendar = calendar_data->calendar(); - - set_icu_calendar_to_iso_date(icu_calendar, { 1970, 7, 1 }); - auto base_extended_year = ICU_MUST(icu_calendar.get(UCAL_EXTENDED_YEAR, status)); - - auto [icu_month, icu_is_leap] = month_code_to_icu_fields(month_code, calendar); - u8 max_days_in_month = 0; - - for (auto offset = -2; offset <= 2; ++offset) { - icu_calendar.clear(); - icu_calendar.set(UCAL_EXTENDED_YEAR, base_extended_year + offset); - icu_calendar.set(UCAL_MONTH, icu_month); - icu_calendar.set(UCAL_DATE, 1); - - if (icu_is_leap != 0 || (calendar == "hebrew"sv && month_code == HEBREW_ADAR_I_MONTH_CODE)) { - auto resolved = ICU_MUST(icu_calendar.get(UCAL_MONTH, status)); - if (resolved != icu_month) - continue; - } - - auto days_in_month = static_cast(ICU_MUST(icu_calendar.getActualMaximum(UCAL_DATE, status))); - max_days_in_month = max(max_days_in_month, days_in_month); - } - - return max_days_in_month > 0 ? max_days_in_month : 30; + return icu_calendar_max_days_in_month_code(calendar.bytes().data(), calendar.bytes().size(), month_code.bytes().data(), month_code.length()); } -Optional chinese_ordinal_month_code(String const& calendar, i32 arithmetic_year, u8 ordinal_month) +bool calendar_year_contains_month_code(String const& calendar, i32 arithmetic_year, StringView month_code) { - ASSERT(calendar.is_one_of("chinese"sv, "dangi"sv)); - - auto calendar_data = CalendarData::for_calendar(calendar); - if (!calendar_data.has_value()) - return {}; - - auto& icu_calendar = calendar_data->calendar(); - - auto extended_year = chinese_arithmetic_year_to_extended_year(calendar, icu_calendar, arithmetic_year); - auto const& layout = chinese_year_layout(calendar, icu_calendar, extended_year); - - if (ordinal_month < 1 || ordinal_month > layout.month_count) - return {}; - - auto const& entry = layout.months[ordinal_month]; - return MonthCode { static_cast(entry.month + 1), entry.is_leap_month != 0 }; + return icu_year_contains_month_code(calendar.bytes().data(), calendar.bytes().size(), arithmetic_year, month_code.bytes().data(), month_code.length()); } } - -namespace AK { - -template<> -struct Traits : public DefaultTraits { - static unsigned hash(Unicode::ChineseYearKey const& key) { return pair_int_hash(key.calendar.hash(), key.extended_year); } -}; - -template<> -struct Traits : public DefaultTraits { - static unsigned hash(Unicode::ChineseExtendedYearKey const& key) { return pair_int_hash(key.calendar.hash(), key.arithmetic_year); } -}; - -} diff --git a/Libraries/LibUnicode/Calendar.h b/Libraries/LibUnicode/Calendar.h index 3d29090d39..f0d8271721 100644 --- a/Libraries/LibUnicode/Calendar.h +++ b/Libraries/LibUnicode/Calendar.h @@ -19,12 +19,6 @@ struct ISODate { u8 day { 0 }; }; -// 12.2 Month Codes, https://tc39.es/proposal-temporal/#sec-temporal-month-codes -struct MonthCode { - u8 month_number { 0 }; - bool is_leap_month { false }; -}; - // 14.3 The Year-Week Record Specification Type, https://tc39.es/proposal-temporal/#sec-year-week-record-specification-type struct YearWeek { Optional week; @@ -49,12 +43,6 @@ struct CalendarDate { bool in_leap_year { false }; }; -constexpr inline auto HEBREW_ADAR_I_MONTH_CODE = "M05L"sv; -constexpr inline auto HEBREW_ADAR_I_MONTH_NUMBER = 5u; - -constexpr inline auto BUDDHIST_EPOCH_ISO_YEAR = -543; -constexpr inline auto ROC_EPOCH_ISO_YEAR = 1911; - CalendarDate iso_date_to_calendar_date(String const& calendar, ISODate); Optional calendar_date_to_iso_date(String const& calendar, i32 year, u8 month, u8 day); Optional calendar_month_code_to_iso_date(String const& calendar, i32 year, StringView month_code, u8 day); @@ -62,7 +50,6 @@ Optional calendar_month_code_to_iso_date(String const& calendar, i32 ye u8 calendar_months_in_year(String const& calendar, i32 arithmetic_year); u8 calendar_days_in_month(String const& calendar, i32 arithmetic_year, u8 ordinal_month); u8 calendar_max_days_in_month_code(String const& calendar, StringView month_code); - -Optional chinese_ordinal_month_code(String const& calendar, i32 arithmetic_year, u8 ordinal_month); +bool calendar_year_contains_month_code(String const& calendar, i32 arithmetic_year, StringView month_code); } diff --git a/Libraries/LibUnicode/DateTimeFormat.cpp b/Libraries/LibUnicode/DateTimeFormat.cpp index 7caf70a3b2..0d258ebdd9 100644 --- a/Libraries/LibUnicode/DateTimeFormat.cpp +++ b/Libraries/LibUnicode/DateTimeFormat.cpp @@ -628,7 +628,15 @@ static void apply_time_zone_to_formatter(icu::SimpleDateFormat& formatter, icu:: auto* calendar = icu::Calendar::createInstance(time_zone_data->time_zone(), locale, status); verify_icu_success(status); - CalendarData::adjust_time_range_for_proleptic_calendar(*calendar); + if (auto* gregorian_calendar = as_if(*calendar)) { + // 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; + + gregorian_calendar->setGregorianChange(ECMA_262_MINIMUM_TIME, status); + verify_icu_success(status); + } + formatter.adoptCalendar(calendar); } diff --git a/Libraries/LibUnicode/ICU.cpp b/Libraries/LibUnicode/ICU.cpp index a2a6057539..1ab0c1b2ec 100644 --- a/Libraries/LibUnicode/ICU.cpp +++ b/Libraries/LibUnicode/ICU.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024-2025, Tim Flynn + * Copyright (c) 2024-2026, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -10,7 +10,6 @@ #include #include -#include #include #include #include @@ -18,7 +17,6 @@ namespace Unicode { static HashMap> s_locale_cache; -static HashMap> s_calendar_cache; static HashMap> s_time_zone_cache; Optional LocaleData::for_locale(StringView locale) @@ -116,53 +114,6 @@ icu::TimeZoneNames& LocaleData::time_zone_names() return *m_time_zone_names; } -Optional CalendarData::for_calendar(String const& calendar) -{ - auto& calendar_data = s_calendar_cache.ensure(calendar, [&]() -> OwnPtr { - UErrorCode status = U_ZERO_ERROR; - - auto const* legacy_calendar = uloc_toLegacyType("calendar", ByteString(calendar).characters()); - if (!legacy_calendar) - return nullptr; - - auto locale_data = LocaleData::for_locale("und"sv); - VERIFY(locale_data.has_value()); - - locale_data->locale().setKeywordValue("calendar", legacy_calendar, status); - if (icu_failure(status)) - return nullptr; - - auto icu_calendar = adopt_own_if_nonnull(icu::Calendar::createInstance(locale_data->locale(), status)); - if (icu_failure(status)) - return nullptr; - - return adopt_own(*new CalendarData { icu_calendar.release_nonnull() }); - }); - - if (calendar_data) - return *calendar_data; - return {}; -} - -void CalendarData::adjust_time_range_for_proleptic_calendar(icu::Calendar& icu_calendar) -{ - // 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 UDate ECMA_262_MINIMUM_TIME = -8.64E15; - UErrorCode status = U_ZERO_ERROR; - - if (auto* gregorian_calendar = as_if(icu_calendar)) { - gregorian_calendar->setGregorianChange(ECMA_262_MINIMUM_TIME, status); - verify_icu_success(status); - } -} - -CalendarData::CalendarData(NonnullOwnPtr calendar) - : m_calendar(move(calendar)) -{ - adjust_time_range_for_proleptic_calendar(*m_calendar); -} - Optional TimeZoneData::for_time_zone(StringView time_zone) { auto time_zone_data = s_time_zone_cache.get(time_zone); diff --git a/Libraries/LibUnicode/ICU.h b/Libraries/LibUnicode/ICU.h index 439bb79b32..9e7e95df27 100644 --- a/Libraries/LibUnicode/ICU.h +++ b/Libraries/LibUnicode/ICU.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024-2025, Tim Flynn + * Copyright (c) 2024-2026, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -25,7 +25,6 @@ #include U_NAMESPACE_BEGIN -class Calendar; class DateTimePatternGenerator; class LocaleDisplayNames; class NumberingSystem; @@ -69,20 +68,6 @@ private: Optional m_digital_format; }; -class CalendarData { -public: - static Optional for_calendar(String const& calendar); - - static void adjust_time_range_for_proleptic_calendar(icu::Calendar&); - - ALWAYS_INLINE icu::Calendar& calendar() { return *m_calendar; } - -private: - explicit CalendarData(NonnullOwnPtr); - - NonnullOwnPtr m_calendar; -}; - class TimeZoneData { public: static Optional for_time_zone(StringView time_zone); @@ -113,20 +98,6 @@ inline void verify_icu_success(UErrorCode code, SourceLocation location = Source } } -#define ICU_MUST(expression) \ - ({ \ - /* Ignore -Wshadow to allow nesting the macro. */ \ - AK_IGNORE_DIAGNOSTIC("-Wshadow", auto _temporary_result = (expression)); \ - verify_icu_success(status); \ - _temporary_result; \ - }) - -#define ICU_MUST_VOID(expression) \ - ({ \ - expression; \ - verify_icu_success(status); \ - }) - ALWAYS_INLINE icu::StringPiece icu_string_piece(StringView string) { return { string.characters_without_null_termination(), static_cast(string.length()) }; diff --git a/Libraries/LibUnicode/Rust/Cargo.toml b/Libraries/LibUnicode/Rust/Cargo.toml new file mode 100644 index 0000000000..4724b260af --- /dev/null +++ b/Libraries/LibUnicode/Rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "libunicode_rust" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["staticlib"] + +# After changing dependencies, regenerate the Flatpak sources: +# python3 Meta/CMake/flatpak/generate-cargo-sources.py +[dependencies] +calendrical_calculations = "0.2" +icu_calendar = { version = "2.1.1", features = ["compiled_data"] } diff --git a/Libraries/LibUnicode/Rust/src/calendar.rs b/Libraries/LibUnicode/Rust/src/calendar.rs new file mode 100644 index 0000000000..fa6eb8dfde --- /dev/null +++ b/Libraries/LibUnicode/Rust/src/calendar.rs @@ -0,0 +1,604 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +use calendrical_calculations::rata_die::RataDie; +use icu_calendar::{AnyCalendar, AnyCalendarKind, Date, types::MonthCode}; +use std::panic::{AssertUnwindSafe, catch_unwind}; + +#[repr(C)] +pub struct FfiISODate { + pub year: i32, + pub month: u8, + pub day: u8, +} + +#[repr(C)] +pub struct FfiOptionalISODate { + pub iso_date: FfiISODate, + pub has_value: bool, +} + +#[repr(C)] +pub struct FfiCalendarDate { + pub year: i32, + pub month: u8, + pub month_code: [u8; 5], // e.g. "M01\0\0" or "M05L\0" + pub month_code_length: u8, + pub day: u8, + pub day_of_week: u8, + pub day_of_year: u16, + pub days_in_week: u8, + pub days_in_month: u8, + pub days_in_year: u16, + pub months_in_year: u8, + pub in_leap_year: bool, +} + +const EMPTY_ISO_DATE: FfiOptionalISODate = FfiOptionalISODate { + iso_date: FfiISODate { + year: 0, + month: 0, + day: 0, + }, + has_value: false, +}; + +const EMPTY_CALENDAR_DATE: FfiCalendarDate = FfiCalendarDate { + year: 0, + month: 0, + month_code: [0; 5], + month_code_length: 0, + day: 0, + day_of_week: 0, + day_of_year: 0, + days_in_week: 7, + days_in_month: 0, + days_in_year: 0, + months_in_year: 0, + in_leap_year: false, +}; + +fn abort_on_panic R, R>(f: F) -> R { + match catch_unwind(AssertUnwindSafe(f)) { + Ok(result) => result, + Err(_) => std::process::abort(), + } +} + +/// SAFETY: All FFI string inputs are ASCII calendar names (e.g. "chinese") or month codes (e.g. "M01"). +fn ascii_string_from_ffi<'a>(string: *const u8, length: usize) -> &'a str { + let bytes = unsafe { std::slice::from_raw_parts(string, length) }; + debug_assert!(bytes.is_ascii(), "Expected ASCII input from FFI"); + unsafe { std::str::from_utf8_unchecked(bytes) } +} + +fn iso_date_to_ffi(iso_date: Option) -> FfiOptionalISODate { + match iso_date { + Some(iso_date) => FfiOptionalISODate { + iso_date, + has_value: true, + }, + None => EMPTY_ISO_DATE, + } +} + +fn make_calendar(calendar_name: &str) -> Option { + let kind = match calendar_name { + "iso8601" => AnyCalendarKind::Iso, + "gregory" => AnyCalendarKind::Gregorian, + "buddhist" => AnyCalendarKind::Buddhist, + "chinese" => AnyCalendarKind::Chinese, + "coptic" => AnyCalendarKind::Coptic, + "dangi" => AnyCalendarKind::Dangi, + "ethiopic" => AnyCalendarKind::Ethiopian, + "ethioaa" => AnyCalendarKind::EthiopianAmeteAlem, + "hebrew" => AnyCalendarKind::Hebrew, + "indian" => AnyCalendarKind::Indian, + "islamic-civil" | "islamicc" => AnyCalendarKind::HijriTabularTypeIIFriday, + "islamic-tbla" => AnyCalendarKind::HijriTabularTypeIIThursday, + "islamic-umalqura" => AnyCalendarKind::HijriUmmAlQura, + "islamic" => AnyCalendarKind::HijriSimulatedMecca, + "japanese" => AnyCalendarKind::Japanese, + "persian" => AnyCalendarKind::Persian, + "roc" => AnyCalendarKind::Roc, + _ => return None, + }; + Some(AnyCalendar::new(kind)) +} + +fn is_chinese_or_dangi(calendar_name: &str) -> bool { + calendar_name == "chinese" || calendar_name == "dangi" +} + +fn parse_month_code(month_code: &str) -> Option { + if month_code.len() < 3 || !month_code.starts_with('M') { + return None; + } + + let month_number: u8 = month_code[1..3].parse().ok()?; + let is_leap_month = month_code.len() == 4 && month_code.as_bytes()[3] == b'L'; + + if is_leap_month { + MonthCode::new_leap(month_number) + } else { + MonthCode::new_normal(month_number) + } +} + +fn encode_month_code(month_code: &MonthCode) -> ([u8; 5], u8) { + let serialized = month_code.0.as_str(); + let bytes = serialized.as_bytes(); + + let mut buffer = [0u8; 5]; + let length = bytes.len().min(5); + buffer[..length].copy_from_slice(&bytes[..length]); + + (buffer, length as u8) +} + +struct ChineseMonthInfo { + month_code: MonthCode, + days_in_month: u8, +} + +/// Collect months starting from a known year-start date, iterating via RataDie arithmetic. +fn collect_year_months( + calendar: &AnyCalendar, + year_start: &Date, + extended_year: i32, +) -> Vec { + let months_in_year = year_start.months_in_year(); + + let mut result = Vec::with_capacity(months_in_year as usize); + let mut current_date = year_start.clone(); + + for _ in 0..months_in_year { + let month_code = current_date.month().standard_code; + let days_in_month = current_date.days_in_month(); + + result.push(ChineseMonthInfo { + month_code, + days_in_month, + }); + + // Advance to day 1 of next month via RataDie arithmetic. + let rata_die = + RataDie::new(current_date.to_rata_die().to_i64_date() + days_in_month as i64); + + let next_date = Date::from_rata_die(rata_die, calendar.clone()); + if next_date.year().extended_year() != extended_year { + break; + } + + current_date = next_date; + } + + result +} + +/// Look up the ICU4X extended year for a Chinese/Dangi arithmetic year. +fn chinese_or_dangi_extended_year(calendar: &AnyCalendar, arithmetic_year: i32) -> Option { + // Chinese new year falls in Jan/Feb. Use June 15 to land in the right calendar year. + let approximate_iso = Date::try_new_iso(arithmetic_year, 6, 15).ok()?; + let calendar_date = Date::new_from_iso(approximate_iso, calendar.clone()); + + Some(calendar_date.year().extended_year()) +} + +/// Build a list of month info for each ordinal month in a Chinese/Dangi year. +fn chinese_year_months( + calendar: &AnyCalendar, + arithmetic_year: i32, +) -> Option> { + let extended_year = chinese_or_dangi_extended_year(calendar, arithmetic_year)?; + let month_one_code = MonthCode::new_normal(1)?; + + let year_start = + Date::try_new_from_codes(None, extended_year, month_one_code, 1, calendar.clone()).ok()?; + + Some(collect_year_months(calendar, &year_start, extended_year)) +} + +/// Construct a calendar date from an arithmetic year and ordinal month. +fn make_calendar_date_from_ordinal( + calendar_name: &str, + calendar: &AnyCalendar, + arithmetic_year: i32, + ordinal_month: u8, + day: u8, +) -> Option> { + if is_chinese_or_dangi(calendar_name) { + let months = chinese_year_months(calendar, arithmetic_year)?; + + let month_index = (ordinal_month as usize).checked_sub(1)?; + if month_index >= months.len() { + return None; + } + + let month_code = months[month_index].month_code; + let extended_year = chinese_or_dangi_extended_year(calendar, arithmetic_year)?; + + Date::try_new_from_codes(None, extended_year, month_code, day, calendar.clone()).ok() + } else if calendar_name == "hebrew" { + // Determine if this is a leap year by trying to construct a date with M05L. + let adar_i_code = MonthCode::new_leap(5)?; + let is_leap_year = + Date::try_new_from_codes(None, arithmetic_year, adar_i_code, 1, calendar.clone()) + .is_ok(); + + let month_code = if is_leap_year { + if ordinal_month == 6 { + MonthCode::new_leap(5)? + } else if ordinal_month > 6 { + MonthCode::new_normal(ordinal_month - 1)? + } else { + MonthCode::new_normal(ordinal_month)? + } + } else { + MonthCode::new_normal(ordinal_month)? + }; + + Date::try_new_from_codes(None, arithmetic_year, month_code, day, calendar.clone()).ok() + } else { + let month_code = MonthCode::new_normal(ordinal_month)?; + Date::try_new_from_codes(None, arithmetic_year, month_code, day, calendar.clone()).ok() + } +} + +fn iso_date_to_calendar_date_impl( + calendar_name: &str, + iso_year: i32, + iso_month: u8, + iso_day: u8, +) -> Option { + let calendar = make_calendar(calendar_name)?; + + let iso_date = Date::try_new_iso(iso_year, iso_month, iso_day).ok()?; + let calendar_date = Date::new_from_iso(iso_date, calendar.clone()); + + let (arithmetic_year, ordinal_month) = if is_chinese_or_dangi(calendar_name) { + let extended_year = calendar_date.year().extended_year(); + + let month_one_code = MonthCode::new_normal(1)?; + let current_month_code = calendar_date.month().standard_code; + + let year_start = + Date::try_new_from_codes(None, extended_year, month_one_code, 1, calendar.clone()) + .ok()?; + + let ordinal = collect_year_months(&calendar, &year_start, extended_year) + .iter() + .position(|m| m.month_code == current_month_code) + .map(|i| (i + 1) as u8) + .unwrap_or(1); + + (year_start.to_iso().extended_year(), ordinal) + } else { + ( + calendar_date.extended_year(), + calendar_date.month().ordinal as u8, + ) + }; + + let month_code = calendar_date.month().standard_code; + let (month_code_buffer, month_code_length) = encode_month_code(&month_code); + + Some(FfiCalendarDate { + year: arithmetic_year, + month: ordinal_month, + month_code: month_code_buffer, + month_code_length, + day: calendar_date.day_of_month().0, + day_of_week: calendar_date.day_of_week() as u8, + day_of_year: calendar_date.day_of_year().0, + days_in_week: 7, + days_in_month: calendar_date.days_in_month(), + days_in_year: calendar_date.days_in_year(), + months_in_year: calendar_date.months_in_year(), + in_leap_year: calendar_date.is_in_leap_year(), + }) +} + +/// # Safety +/// `calendar` must point to a valid UTF-8 string of `calendar_length` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn icu_iso_date_to_calendar_date( + calendar: *const u8, + calendar_length: usize, + iso_year: i32, + iso_month: u8, + iso_day: u8, +) -> FfiCalendarDate { + abort_on_panic(|| { + let calendar_name = ascii_string_from_ffi(calendar, calendar_length); + + iso_date_to_calendar_date_impl(calendar_name, iso_year, iso_month, iso_day) + .unwrap_or(EMPTY_CALENDAR_DATE) + }) +} + +fn calendar_date_to_iso_date_impl( + calendar_name: &str, + arithmetic_year: i32, + ordinal_month: u8, + day: u8, +) -> Option { + let calendar = make_calendar(calendar_name)?; + + let calendar_date = make_calendar_date_from_ordinal( + calendar_name, + &calendar, + arithmetic_year, + ordinal_month, + day, + )?; + + let iso_date = calendar_date.to_iso(); + + Some(FfiISODate { + year: iso_date.extended_year(), + month: iso_date.month().ordinal as u8, + day: iso_date.day_of_month().0, + }) +} + +/// # Safety +/// `calendar` must point to a valid UTF-8 string of `calendar_length` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn icu_calendar_date_to_iso_date( + calendar: *const u8, + calendar_length: usize, + arithmetic_year: i32, + ordinal_month: u8, + day: u8, +) -> FfiOptionalISODate { + abort_on_panic(|| { + let calendar_name = ascii_string_from_ffi(calendar, calendar_length); + + iso_date_to_ffi(calendar_date_to_iso_date_impl( + calendar_name, + arithmetic_year, + ordinal_month, + day, + )) + }) +} + +fn calendar_month_code_to_iso_date_impl( + calendar_name: &str, + month_code_string: &str, + iso_year: i32, + day: u8, +) -> Option { + let calendar = make_calendar(calendar_name)?; + let month_code = parse_month_code(month_code_string)?; + + let iso_jan1 = Date::try_new_iso(iso_year, 1, 1).ok()?; + let iso_dec31 = Date::try_new_iso(iso_year, 12, 31).ok()?; + + let calendar_jan1 = Date::new_from_iso(iso_jan1, calendar.clone()); + let calendar_dec31 = Date::new_from_iso(iso_dec31, calendar.clone()); + + let start_extended_year = calendar_jan1.extended_year(); + let end_extended_year = calendar_dec31.extended_year(); + + let mut best_iso_date: Option = None; + + for extended_year in start_extended_year..=end_extended_year { + let Ok(candidate) = + Date::try_new_from_codes(None, extended_year, month_code, day, calendar.clone()) + else { + continue; + }; + + if candidate.month().standard_code != month_code || candidate.day_of_month().0 != day { + continue; + } + + let iso_date = candidate.to_iso(); + if iso_date.extended_year() != iso_year { + continue; + } + + let candidate_date = FfiISODate { + year: iso_date.extended_year(), + month: iso_date.month().ordinal, + day: iso_date.day_of_month().0, + }; + + best_iso_date = Some(match best_iso_date { + Some(b) if (b.month, b.day) > (candidate_date.month, candidate_date.day) => b, + _ => candidate_date, + }); + } + + best_iso_date +} + +/// # Safety +/// `calendar` must point to a valid UTF-8 string of `calendar_length` bytes. +/// `month_code` must point to a valid UTF-8 string of `month_code_length` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn icu_calendar_month_code_to_iso_date( + calendar: *const u8, + calendar_length: usize, + iso_year: i32, + month_code: *const u8, + month_code_length: usize, + day: u8, +) -> FfiOptionalISODate { + abort_on_panic(|| { + let calendar_name = ascii_string_from_ffi(calendar, calendar_length); + let month_code_string = ascii_string_from_ffi(month_code, month_code_length); + + iso_date_to_ffi(calendar_month_code_to_iso_date_impl( + calendar_name, + month_code_string, + iso_year, + day, + )) + }) +} + +fn calendar_months_in_year_impl(calendar_name: &str, arithmetic_year: i32) -> Option { + let calendar = make_calendar(calendar_name)?; + + if is_chinese_or_dangi(calendar_name) { + let months = chinese_year_months(&calendar, arithmetic_year)?; + return Some(months.len() as u8); + } + + let month_one_code = MonthCode::new_normal(1)?; + + let date = Date::try_new_from_codes(None, arithmetic_year, month_one_code, 1, calendar).ok()?; + Some(date.months_in_year()) +} + +/// # Safety +/// `calendar` must point to a valid UTF-8 string of `calendar_length` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn icu_calendar_months_in_year( + calendar: *const u8, + calendar_length: usize, + arithmetic_year: i32, +) -> u8 { + abort_on_panic(|| { + let calendar_name = ascii_string_from_ffi(calendar, calendar_length); + + calendar_months_in_year_impl(calendar_name, arithmetic_year).unwrap_or(12) + }) +} + +fn calendar_days_in_month_impl( + calendar_name: &str, + arithmetic_year: i32, + ordinal_month: u8, +) -> Option { + let calendar = make_calendar(calendar_name)?; + + if is_chinese_or_dangi(calendar_name) { + let months = chinese_year_months(&calendar, arithmetic_year)?; + let month_index = (ordinal_month as usize).checked_sub(1)?; + + return months.get(month_index).map(|m| m.days_in_month); + } + + let date = make_calendar_date_from_ordinal( + calendar_name, + &calendar, + arithmetic_year, + ordinal_month, + 1, + )?; + + Some(date.days_in_month()) +} + +/// # Safety +/// `calendar` must point to a valid UTF-8 string of `calendar_length` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn icu_calendar_days_in_month( + calendar: *const u8, + calendar_length: usize, + arithmetic_year: i32, + ordinal_month: u8, +) -> u8 { + abort_on_panic(|| { + let calendar_name = ascii_string_from_ffi(calendar, calendar_length); + + calendar_days_in_month_impl(calendar_name, arithmetic_year, ordinal_month).unwrap_or(30) + }) +} + +fn calendar_max_days_in_month_code_impl( + calendar_name: &str, + month_code_string: &str, +) -> Option { + let calendar = make_calendar(calendar_name)?; + let month_code = parse_month_code(month_code_string)?; + + let base_iso_date = Date::try_new_iso(1970, 7, 1).ok()?; + let base_calendar_date = Date::new_from_iso(base_iso_date, calendar.clone()); + let base_extended_year = base_calendar_date.extended_year(); + + let mut max_days_in_month: u8 = 0; + + for offset in -2i32..=2 { + let extended_year = base_extended_year + offset; + + if let Ok(date) = + Date::try_new_from_codes(None, extended_year, month_code, 1, calendar.clone()) + && date.month().standard_code == month_code + { + max_days_in_month = max_days_in_month.max(date.days_in_month()); + } + } + + if max_days_in_month > 0 { + Some(max_days_in_month) + } else { + None + } +} + +/// # Safety +/// `calendar` must point to a valid UTF-8 string of `calendar_length` bytes. +/// `month_code` must point to a valid UTF-8 string of `month_code_length` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn icu_calendar_max_days_in_month_code( + calendar: *const u8, + calendar_length: usize, + month_code: *const u8, + month_code_length: usize, +) -> u8 { + abort_on_panic(|| { + let calendar_name = ascii_string_from_ffi(calendar, calendar_length); + let month_code = ascii_string_from_ffi(month_code, month_code_length); + + calendar_max_days_in_month_code_impl(calendar_name, month_code).unwrap_or(30) + }) +} + +fn year_contains_month_code_impl( + calendar_name: &str, + arithmetic_year: i32, + month_code_string: &str, +) -> Option { + let calendar = make_calendar(calendar_name)?; + let month_code = parse_month_code(month_code_string)?; + + let contains_month_code = if is_chinese_or_dangi(calendar_name) { + let months = chinese_year_months(&calendar, arithmetic_year)?; + months.iter().any(|m| m.month_code == month_code) + } else { + matches!( + Date::try_new_from_codes(None, arithmetic_year, month_code, 1, calendar.clone()), + Ok(date) if date.month().standard_code == month_code + ) + }; + + Some(contains_month_code) +} + +/// # Safety +/// `calendar` must point to a valid UTF-8 string of `calendar_length` bytes. +/// `month_code` must point to a valid UTF-8 string of `month_code_length` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn icu_year_contains_month_code( + calendar: *const u8, + calendar_length: usize, + arithmetic_year: i32, + month_code: *const u8, + month_code_length: usize, +) -> bool { + abort_on_panic(|| { + let calendar_name = ascii_string_from_ffi(calendar, calendar_length); + let month_code_string = ascii_string_from_ffi(month_code, month_code_length); + + year_contains_month_code_impl(calendar_name, arithmetic_year, month_code_string) + .unwrap_or(false) + }) +} diff --git a/Libraries/LibUnicode/Rust/src/lib.rs b/Libraries/LibUnicode/Rust/src/lib.rs new file mode 100644 index 0000000000..b05052b957 --- /dev/null +++ b/Libraries/LibUnicode/Rust/src/lib.rs @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +mod calendar; diff --git a/Meta/CMake/flatpak/cargo-sources.json b/Meta/CMake/flatpak/cargo-sources.json index 8ff0faab52..a46dd9d0a9 100644 --- a/Meta/CMake/flatpak/cargo-sources.json +++ b/Meta/CMake/flatpak/cargo-sources.json @@ -12,6 +12,97 @@ "sha256": "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8", "dest": "cargo/vendor/autocfg-1.5.0" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/calendrical_calculations/calendrical_calculations-0.2.3.crate", + "sha256": "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7", + "dest": "cargo/vendor/calendrical_calculations-0.2.3" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/core_maths/core_maths-0.1.1.crate", + "sha256": "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30", + "dest": "cargo/vendor/core_maths-0.1.1" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/displaydoc/displaydoc-0.2.5.crate", + "sha256": "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0", + "dest": "cargo/vendor/displaydoc-0.2.5" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_calendar/icu_calendar-2.1.1.crate", + "sha256": "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e", + "dest": "cargo/vendor/icu_calendar-2.1.1" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_calendar_data/icu_calendar_data-2.1.1.crate", + "sha256": "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d", + "dest": "cargo/vendor/icu_calendar_data-2.1.1" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_collections/icu_collections-2.1.1.crate", + "sha256": "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43", + "dest": "cargo/vendor/icu_collections-2.1.1" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_locale/icu_locale-2.1.1.crate", + "sha256": "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60", + "dest": "cargo/vendor/icu_locale-2.1.1" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_locale_core/icu_locale_core-2.1.1.crate", + "sha256": "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6", + "dest": "cargo/vendor/icu_locale_core-2.1.1" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_locale_data/icu_locale_data-2.1.2.crate", + "sha256": "1c5f1d16b4c3a2642d3a719f18f6b06070ab0aef246a6418130c955ae08aa831", + "dest": "cargo/vendor/icu_locale_data-2.1.2" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_provider/icu_provider-2.1.1.crate", + "sha256": "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614", + "dest": "cargo/vendor/icu_provider-2.1.1" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ixdtf/ixdtf-0.6.4.crate", + "sha256": "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992", + "dest": "cargo/vendor/ixdtf-0.6.4" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libm/libm-0.2.16.crate", + "sha256": "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981", + "dest": "cargo/vendor/libm-0.2.16" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/litemap/litemap-0.8.1.crate", + "sha256": "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77", + "dest": "cargo/vendor/litemap-0.8.1" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -33,6 +124,76 @@ "sha256": "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841", "dest": "cargo/vendor/num-traits-0.2.19" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/potential_utf/potential_utf-0.1.4.crate", + "sha256": "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77", + "dest": "cargo/vendor/potential_utf-0.1.4" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/proc-macro2/proc-macro2-1.0.106.crate", + "sha256": "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934", + "dest": "cargo/vendor/proc-macro2-1.0.106" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/quote/quote-1.0.45.crate", + "sha256": "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924", + "dest": "cargo/vendor/quote-1.0.45" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde/serde-1.0.228.crate", + "sha256": "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e", + "dest": "cargo/vendor/serde-1.0.228" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde_core/serde_core-1.0.228.crate", + "sha256": "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad", + "dest": "cargo/vendor/serde_core-1.0.228" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde_derive/serde_derive-1.0.228.crate", + "sha256": "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79", + "dest": "cargo/vendor/serde_derive-1.0.228" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/stable_deref_trait/stable_deref_trait-1.2.1.crate", + "sha256": "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596", + "dest": "cargo/vendor/stable_deref_trait-1.2.1" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/syn/syn-2.0.117.crate", + "sha256": "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99", + "dest": "cargo/vendor/syn-2.0.117" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/synstructure/synstructure-0.13.2.crate", + "sha256": "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2", + "dest": "cargo/vendor/synstructure-0.13.2" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tinystr/tinystr-0.8.2.crate", + "sha256": "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869", + "dest": "cargo/vendor/tinystr-0.8.2" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -40,14 +201,101 @@ "sha256": "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5", "dest": "cargo/vendor/unicode-ident-1.0.22" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/writeable/writeable-0.6.2.crate", + "sha256": "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9", + "dest": "cargo/vendor/writeable-0.6.2" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/yoke/yoke-0.8.1.crate", + "sha256": "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954", + "dest": "cargo/vendor/yoke-0.8.1" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/yoke-derive/yoke-derive-0.8.1.crate", + "sha256": "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d", + "dest": "cargo/vendor/yoke-derive-0.8.1" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerofrom/zerofrom-0.1.6.crate", + "sha256": "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5", + "dest": "cargo/vendor/zerofrom-0.1.6" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerofrom-derive/zerofrom-derive-0.1.6.crate", + "sha256": "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502", + "dest": "cargo/vendor/zerofrom-derive-0.1.6" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerotrie/zerotrie-0.2.3.crate", + "sha256": "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851", + "dest": "cargo/vendor/zerotrie-0.2.3" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerovec/zerovec-0.11.5.crate", + "sha256": "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002", + "dest": "cargo/vendor/zerovec-0.11.5" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerovec-derive/zerovec-derive-0.11.2.crate", + "sha256": "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3", + "dest": "cargo/vendor/zerovec-derive-0.11.2" + }, { "type": "shell", "commands": [ "echo '{\"files\": {}, \"package\": \"c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8\"}' > cargo/vendor/autocfg-1.5.0/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7\"}' > cargo/vendor/calendrical_calculations-0.2.3/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30\"}' > cargo/vendor/core_maths-0.1.1/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0\"}' > cargo/vendor/displaydoc-0.2.5/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e\"}' > cargo/vendor/icu_calendar-2.1.1/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d\"}' > cargo/vendor/icu_calendar_data-2.1.1/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43\"}' > cargo/vendor/icu_collections-2.1.1/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60\"}' > cargo/vendor/icu_locale-2.1.1/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6\"}' > cargo/vendor/icu_locale_core-2.1.1/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"1c5f1d16b4c3a2642d3a719f18f6b06070ab0aef246a6418130c955ae08aa831\"}' > cargo/vendor/icu_locale_data-2.1.2/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614\"}' > cargo/vendor/icu_provider-2.1.1/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992\"}' > cargo/vendor/ixdtf-0.6.4/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981\"}' > cargo/vendor/libm-0.2.16/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77\"}' > cargo/vendor/litemap-0.8.1/.cargo-checksum.json", "echo '{\"files\": {}, \"package\": \"a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9\"}' > cargo/vendor/num-bigint-0.4.6/.cargo-checksum.json", "echo '{\"files\": {}, \"package\": \"7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f\"}' > cargo/vendor/num-integer-0.1.46/.cargo-checksum.json", "echo '{\"files\": {}, \"package\": \"071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841\"}' > cargo/vendor/num-traits-0.2.19/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77\"}' > cargo/vendor/potential_utf-0.1.4/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934\"}' > cargo/vendor/proc-macro2-1.0.106/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924\"}' > cargo/vendor/quote-1.0.45/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e\"}' > cargo/vendor/serde-1.0.228/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad\"}' > cargo/vendor/serde_core-1.0.228/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79\"}' > cargo/vendor/serde_derive-1.0.228/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596\"}' > cargo/vendor/stable_deref_trait-1.2.1/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99\"}' > cargo/vendor/syn-2.0.117/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2\"}' > cargo/vendor/synstructure-0.13.2/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869\"}' > cargo/vendor/tinystr-0.8.2/.cargo-checksum.json", "echo '{\"files\": {}, \"package\": \"9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5\"}' > cargo/vendor/unicode-ident-1.0.22/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9\"}' > cargo/vendor/writeable-0.6.2/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954\"}' > cargo/vendor/yoke-0.8.1/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d\"}' > cargo/vendor/yoke-derive-0.8.1/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5\"}' > cargo/vendor/zerofrom-0.1.6/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502\"}' > cargo/vendor/zerofrom-derive-0.1.6/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851\"}' > cargo/vendor/zerotrie-0.2.3/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002\"}' > cargo/vendor/zerovec-0.11.5/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3\"}' > cargo/vendor/zerovec-derive-0.11.2/.cargo-checksum.json", "mkdir -p .cargo", "printf '[source.crates-io]\\nreplace-with = \"vendored-sources\"\\n\\n[source.vendored-sources]\\ndirectory = \"cargo/vendor\"\\n' > .cargo/config.toml" ] diff --git a/Tests/LibJS/Runtime/builtins/Temporal/PlainDate/PlainDate.from.js b/Tests/LibJS/Runtime/builtins/Temporal/PlainDate/PlainDate.from.js index 00158c7817..3045c6cd46 100644 --- a/Tests/LibJS/Runtime/builtins/Temporal/PlainDate/PlainDate.from.js +++ b/Tests/LibJS/Runtime/builtins/Temporal/PlainDate/PlainDate.from.js @@ -116,6 +116,45 @@ describe("correct behavior", () => { }).toThrowWithMessage(RangeError, "day"); }); + test("chinese calendar extreme dates", () => { + const farPast = Temporal.PlainDate.from({ calendar: "chinese", year: -250000, month: 1, day: 1 }); + expect(farPast.year).toBe(-250000); + + const farFuture = Temporal.PlainDate.from({ calendar: "chinese", year: 250000, month: 1, day: 1 }); + expect(farFuture.year).toBe(250000); + }); + + test("dangi calendar extreme dates", () => { + const farPast = Temporal.PlainDate.from({ calendar: "dangi", year: -250000, month: 1, day: 1 }); + expect(farPast.year).toBe(-250000); + + const farFuture = Temporal.PlainDate.from({ calendar: "dangi", year: 250000, month: 1, day: 1 }); + expect(farFuture.year).toBe(250000); + }); + + test("hebrew calendar epoch year (Rosh Hashanah postponement)", () => { + const expectedCheshvan = [29, 30, 30, 29, 29, 30, 30, 29, 29, 30, 29]; + const expectedKislev = [30, 30, 30, 29, 30, 30, 30, 30, 29, 30, 30]; + + for (let year = 0; year < expectedCheshvan.length; ++year) { + const cheshvan = Temporal.PlainDate.from({ + calendar: "hebrew", + year, + monthCode: "M02", + day: 30, + }); + expect(cheshvan.day).toBe(expectedCheshvan[year]); + + const kislev = Temporal.PlainDate.from({ + calendar: "hebrew", + year, + monthCode: "M03", + day: 30, + }); + expect(kislev.day).toBe(expectedKislev[year]); + } + }); + test("japanese era boundary: Heisei to Reiwa", () => { // April 30, 2019 is the last day of Heisei. const lastHeisei = Temporal.PlainDate.from("2019-04-30[u-ca=japanese]"); diff --git a/Tests/LibJS/Runtime/builtins/Temporal/PlainDate/PlainDate.prototype.daysInYear.js b/Tests/LibJS/Runtime/builtins/Temporal/PlainDate/PlainDate.prototype.daysInYear.js index 90eb232fd0..1aa037c99b 100644 --- a/Tests/LibJS/Runtime/builtins/Temporal/PlainDate/PlainDate.prototype.daysInYear.js +++ b/Tests/LibJS/Runtime/builtins/Temporal/PlainDate/PlainDate.prototype.daysInYear.js @@ -3,6 +3,25 @@ describe("correct behavior", () => { const date = new Temporal.PlainDate(2021, 7, 23); expect(date.daysInYear).toBe(365); }); + + test("chinese calendar days in year", () => { + const expectedDays = { + 2020: 384, + 2021: 354, + 2022: 355, + 2023: 384, + 2024: 354, + 2025: 384, + 2026: 354, + 2027: 354, + 2028: 384, + }; + + for (const [year, days] of Object.entries(expectedDays)) { + const date = Temporal.PlainDate.from({ year: Number(year), month: 1, day: 1, calendar: "chinese" }); + expect(date.daysInYear).toBe(days); + } + }); }); describe("errors", () => { diff --git a/Tests/LibJS/Runtime/builtins/Temporal/PlainDate/PlainDate.prototype.monthCode.js b/Tests/LibJS/Runtime/builtins/Temporal/PlainDate/PlainDate.prototype.monthCode.js index 7024e90627..92ab62620d 100644 --- a/Tests/LibJS/Runtime/builtins/Temporal/PlainDate/PlainDate.prototype.monthCode.js +++ b/Tests/LibJS/Runtime/builtins/Temporal/PlainDate/PlainDate.prototype.monthCode.js @@ -31,6 +31,43 @@ describe("correct behavior", () => { expect(plainDate.monthCode).toBe("M02L"); }); + test("chinese calendar leap month codes across decades", () => { + const leapMonthCases = [ + { year: 2001, month: 5, monthCode: "M04L" }, + { year: 2004, month: 3, monthCode: "M02L" }, + { year: 2006, month: 8, monthCode: "M07L" }, + { year: 2009, month: 6, monthCode: "M05L" }, + { year: 2012, month: 5, monthCode: "M04L" }, + { year: 2017, month: 7, monthCode: "M06L" }, + { year: 2020, month: 5, monthCode: "M04L" }, + { year: 2023, month: 3, monthCode: "M02L" }, + { year: 2025, month: 7, monthCode: "M06L" }, + { year: 2028, month: 6, monthCode: "M05L" }, + ]; + + for (const { year, month, monthCode } of leapMonthCases) { + const fromMonth = Temporal.PlainDate.from({ + year, + month, + day: 1, + calendar: "chinese", + }); + expect(fromMonth.monthCode).toBe(monthCode); + expect(fromMonth.month).toBe(month); + + const fromCode = Temporal.PlainDate.from({ + year, + monthCode, + day: 1, + calendar: "chinese", + }); + expect(fromCode.monthCode).toBe(monthCode); + expect(fromCode.month).toBe(month); + + expect(fromMonth.equals(fromCode)).toBeTrue(); + } + }); + test("coptic calendar 13th month", () => { // The Coptic calendar has a 13th month (Nasie). // 2024-09-06 falls in month 13 of Coptic year 1740.