From d878af4b9c3e8031d8452300cc90cd0cb5efe279 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 16 Oct 2026 06:42:50 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=B8=8C=E4=BC=AF?= =?UTF-8?q?=E6=9D=A5=E5=8E=86=E6=B3=95=E6=A0=B8=E5=BF=83=E4=B8=8E=20PlainD?= =?UTF-8?q?ate=20=E5=8E=86=E6=B3=95=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 2 +- .../oxide_builtins/src/temporal/calendar.rs | 547 ++++++++++++++++++ crates/oxide_builtins/src/temporal/mod.rs | 1 + .../oxide_builtins/src/temporal/plain_date.rs | 260 +++++++-- .../src/temporal/plain_year_month.rs | 42 +- 5 files changed, 799 insertions(+), 53 deletions(-) create mode 100644 crates/oxide_builtins/src/temporal/calendar.rs diff --git a/Cargo.toml b/Cargo.toml index fa8831cc..1c9bd51c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ ] [workspace.package] -version = "0.19.23" +version = "0.19.24" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/temporal/calendar.rs b/crates/oxide_builtins/src/temporal/calendar.rs new file mode 100644 index 00000000..60120655 --- /dev/null +++ b/crates/oxide_builtins/src/temporal/calendar.rs @@ -0,0 +1,547 @@ +//! 日历核心层:非 ISO 历法与 ISO 日期(proleptic Gregorian 纪元日)的双向换算、 +//! 年长/闰年/月长/纪元/月码等字段查询。 +//! +//! 本层只服务纯算术历法(无数据表)。hebrew 采用 keviyah(四门表)算法, +//! 与 ICU4X `calendrical_calculations::hebrew_keviyah` 同源;后续历法(偏移族、 +//! 数据表族)按历法族逐步补入。 +//! +//! 对象存储约定:PlainDate 等对象槽位恒存 ISO 年/月/日,日历 ID 决定 getter +//! 如何解释该 ISO 日期;from 路径把历法字段换算成 ISO 后入槽。 + +use super::common::{civil_from_days, days_from_civil}; + +/// hebrew 历法纪元(Tishrei 1 年 1)对应的 ISO 纪元日(1970-01-01 为 0)。 +/// 由 RataDie -1373427(0001-01-01 为 1)换算:-1373427 - 719163。 +const HEBREW_EPOCH_DAYS: i128 = -2_092_590; + +/// Molad Beharad(纪元首朔)相对周首(周六 18:00)的 ḥalakim 偏移。 +const MOLAD_BEHERAD_OFFSET: i64 = 31_524; +/// 一个朔望月的 ḥalakim 时长(29 日 12 时 793 分)。 +const HEBREW_LUNATION_TIME: i64 = 765_433; +/// 一周的 ḥalakim 数(1080 × 24 × 7)。 +const HALAKIM_IN_WEEK: i64 = 181_440; + +/// d-h-p 记法转 ḥalakim(d 为 1 起周几,h 为 0-23 时,p 为 0-1079 分)。 +const fn hal(d: i32, h: i32, p: i32) -> i64 { + ((d - 1) * 24 + h) as i64 * 1080 + p as i64 +} + +/// 四门表:按 Metonic 周期年型分四组,每组 7 个 ḥalakim 分界, +/// 决定该年 molad 落在哪个 keviyah。 +const FOUR_GATES_LMINUSONE: [i64; 7] = [ + hal(7, 18, 0), + hal(1, 9, 204), + hal(2, 18, 0), + hal(3, 9, 204), + hal(5, 9, 204), + hal(5, 18, 0), + hal(6, 9, 204), +]; +const FOUR_GATES_LPLUSONE: [i64; 7] = [ + hal(7, 18, 0), + hal(1, 9, 204), + hal(2, 15, 589), + hal(3, 9, 204), + hal(5, 9, 204), + hal(5, 18, 0), + hal(6, 0, 408), +]; +const FOUR_GATES_LPLUSMINUSONE: [i64; 7] = [ + hal(7, 18, 0), + hal(1, 9, 204), + hal(2, 15, 589), + hal(3, 9, 204), + hal(5, 9, 204), + hal(5, 18, 0), + hal(6, 9, 204), +]; +const FOUR_GATES_LEAP: [i64; 7] = [ + hal(7, 18, 0), + hal(1, 20, 491), + hal(2, 18, 0), + hal(3, 18, 0), + hal(4, 11, 695), + hal(5, 18, 0), + hal(6, 20, 491), +]; + +/// 年型:定 Heshvan 与 Kislev 月长(缺 29/29、常 29/30、全 30/30)。 +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +enum YearType { + Deficient = -1, + Regular = 0, + Complete = 1, +} + +/// 新年首日允许的周几(Rosh Hashanah 推迟规则后只剩四种)。 +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +enum StartOfYear { + Monday = 2, + Tuesday = 3, + Thursday = 5, + Saturday = 7, +} + +/// 一年的 keviyah(14 种):新年周几 + 年型 + 逾越节周几。 +/// 判别值按四门表位置排列,闰年 keviyah 排在平年之后(+7)。 +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +enum Keviyah { + // 平年 + BetHetGimel = 0, // 2D3 + BetShinHe = 1, // 2C5 + GimelKafHe = 2, // 3R5 + HeKafZayin = 3, // 5R7 + HeShinAlef = 4, // 5C1 + ZayinHetAlef = 5, // 7D1 + ZayinShinGimel = 6, // 7C3 + // 闰年 + BetHetHe = 7, // 2D5 + BetShinZayin = 8, // 2C7 + GimelKafZayin = 9, // 3R7 + HeHetAlef = 10, // 5D1 + HeShinGimel = 11, // 5C3 + ZayinHetGimel = 12, // 7D3 + ZayinShinHe = 13, // 7C5 +} + +impl Keviyah { + /// 年型(keviyah 第二字母:ח 缺 / כ 常 / ש 全)。 + fn year_type(self) -> YearType { + match self { + Keviyah::BetHetGimel => YearType::Deficient, + Keviyah::BetShinHe => YearType::Complete, + Keviyah::GimelKafHe => YearType::Regular, + Keviyah::HeKafZayin => YearType::Regular, + Keviyah::HeShinAlef => YearType::Complete, + Keviyah::ZayinHetAlef => YearType::Deficient, + Keviyah::ZayinShinGimel => YearType::Complete, + Keviyah::BetHetHe => YearType::Deficient, + Keviyah::BetShinZayin => YearType::Complete, + Keviyah::GimelKafZayin => YearType::Regular, + Keviyah::HeHetAlef => YearType::Deficient, + Keviyah::HeShinGimel => YearType::Complete, + Keviyah::ZayinHetGimel => YearType::Deficient, + Keviyah::ZayinShinHe => YearType::Complete, + } + } + + /// 新年首日周几(keviyah 首字母:ב 一 / ג 二 / ה 五 / ז 七)。 + fn start_of_year(self) -> StartOfYear { + match self { + Keviyah::BetHetGimel => StartOfYear::Monday, + Keviyah::BetShinHe => StartOfYear::Monday, + Keviyah::GimelKafHe => StartOfYear::Tuesday, + Keviyah::HeKafZayin => StartOfYear::Thursday, + Keviyah::HeShinAlef => StartOfYear::Thursday, + Keviyah::ZayinHetAlef => StartOfYear::Saturday, + Keviyah::ZayinShinGimel => StartOfYear::Saturday, + Keviyah::BetHetHe => StartOfYear::Monday, + Keviyah::BetShinZayin => StartOfYear::Monday, + Keviyah::GimelKafZayin => StartOfYear::Tuesday, + Keviyah::HeHetAlef => StartOfYear::Thursday, + Keviyah::HeShinGimel => StartOfYear::Thursday, + Keviyah::ZayinHetGimel => StartOfYear::Saturday, + Keviyah::ZayinShinHe => StartOfYear::Saturday, + } + } + + /// 闰年 keviyah(判别值 ≥ 7)。 + fn is_leap(self) -> bool { + self >= Keviyah::BetHetHe + } + + /// 第 ordinal_month 个序月(1 起,Tishrei 为 1)的月长。 + fn month_len(self, ordinal_month: u8) -> u8 { + let year_type = self.year_type(); + let is_leap = self.is_leap(); + // 闰年中 Adar 之后的序月对应上一个月号。 + let month_number = ordinal_month - (is_leap && ordinal_month >= 6) as u8; + 29 + + ( + month_number % 2 + // Heshvan 在全年型为长月 + + (ordinal_month == 2 && year_type == YearType::Complete) as u8 + // Kislev 在缺年型为短月 + - (ordinal_month == 3 && year_type == YearType::Deficient) as u8 + ) + } + + /// 第 ordinal_month 个序月之前的累计天数。 + fn days_preceding(self, ordinal_month: u8) -> u16 { + let year_type = self.year_type(); + let is_leap = self.is_leap(); + let month_number = ordinal_month - (is_leap && ordinal_month > 6) as u8; + 29 * (ordinal_month as u16 - 1) + + ( + month_number / 2 + // Adar I 为长月 + + (is_leap && ordinal_month > 6) as u8 + // Heshvan 在全年型为长月 + + (ordinal_month > 2 && year_type == YearType::Complete) as u8 + // Kislev 在缺年型为短月 + - (ordinal_month > 3 && year_type == YearType::Deficient) as u8 + ) as u16 + } + + /// 年内第 day_of_year 天(1 起)折算为(序月, 月内日)。 + fn month_day_for(self, day_of_year: u16) -> (u8, u8) { + // 以 30 除(而非 29)以覆盖年初所有前月皆 30 天的情形。 + let mut month = ((day_of_year - 1) / 30) as u8 + 1; + let mut days_before_month = self.days_preceding(month); + let mut last_day_of_month = self.days_preceding(month + 1); + while day_of_year > last_day_of_month { + month += 1; + days_before_month = last_day_of_month; + last_day_of_month = self.days_preceding(month + 1); + } + (month, (day_of_year - days_before_month) as u8) + } + + /// 年长(平年 353/354/355,闰年 383/384/385)。 + fn year_length(self) -> u16 { + let base = if self.is_leap() { 384 } else { 354 }; + (base + self.year_type() as i16) as u16 + } +} + +/// Metonic 周期年型(19 年 7 闰:3、6、8、11、14、17、19 闰)。 +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +enum MetonicCycleType { + LMinusOne, + LPlusOne, + LPlusMinusOne, + Leap, +} + +impl MetonicCycleType { + fn for_h_year(h_year: i128) -> Self { + let remainder = h_year.rem_euclid(19); + match remainder { + 2 | 5 | 10 | 13 | 16 => Self::LMinusOne, + 1 | 4 | 9 | 12 | 15 => Self::LPlusOne, + 7 | 18 => Self::LPlusMinusOne, + _ => Self::Leap, + } + } +} + +/// 年 h_year 的 Tishrei 朔前累计月数(Metonic 周期:235 月 / 19 年)。 +fn months_preceding_molad(h_year: i128) -> i128 { + (235 * (h_year - 1) + 1).div_euclid(19) +} + +/// 年 h_year 的 Tishrei 朔:(自 Beharad 周起的周数, 周内 ḥalakim)。 +fn molad_details(h_year: i128) -> (i128, i64) { + let months_preceding = months_preceding_molad(h_year); + let molad = i128::from(MOLAD_BEHERAD_OFFSET) + months_preceding * i128::from(HEBREW_LUNATION_TIME); + ( + molad.div_euclid(i128::from(HALAKIM_IN_WEEK)), + molad.rem_euclid(i128::from(HALAKIM_IN_WEEK)) as i64, + ) +} + +/// 四门表查询:按年型与周内 ḥalakim 定 keviyah。 +fn keviyah_for(cycle_type: MetonicCycleType, in_week: i64) -> Keviyah { + let gate = match cycle_type { + MetonicCycleType::LMinusOne => &FOUR_GATES_LMINUSONE, + MetonicCycleType::LPlusOne => &FOUR_GATES_LPLUSONE, + MetonicCycleType::LPlusMinusOne => &FOUR_GATES_LPLUSMINUSONE, + MetonicCycleType::Leap => &FOUR_GATES_LEAP, + }; + // 首项跨周(周尾与周首同属一个 keviyah),其余按上界切分。 + let keviyot = if in_week >= gate[0] || in_week < gate[1] { + (Keviyah::BetHetGimel, Keviyah::BetHetHe) + } else if in_week < gate[2] { + (Keviyah::BetShinHe, Keviyah::BetShinZayin) + } else if in_week < gate[3] { + (Keviyah::GimelKafHe, Keviyah::GimelKafZayin) + } else if in_week < gate[4] { + (Keviyah::HeKafZayin, Keviyah::HeHetAlef) + } else if in_week < gate[5] { + (Keviyah::HeShinAlef, Keviyah::HeShinGimel) + } else if in_week < gate[6] { + (Keviyah::ZayinHetAlef, Keviyah::ZayinHetGimel) + } else { + (Keviyah::ZayinShinGimel, Keviyah::ZayinShinHe) + }; + if cycle_type == MetonicCycleType::Leap { + keviyot.1 + } else { + keviyot.0 + } +} + +/// 一年的全部信息:keviyah + 自 Beharad 周起的周数。 +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +struct YearInfo { + keviyah: Keviyah, + weeks_since_beharad: i128, +} + +impl YearInfo { + /// 计算年 h_year 的 YearInfo。 + fn compute_for(h_year: i128) -> Self { + let (mut weeks_since_beharad, in_week) = molad_details(h_year); + let cycle_type = MetonicCycleType::for_h_year(h_year); + let keviyah = keviyah_for(cycle_type, in_week); + // 周六(Hebrew Saturday)后六小时无条件推迟到周一,跨入新周。 + if in_week >= hal(7, 18, 0) { + weeks_since_beharad += 1; + } + Self { + keviyah, + weeks_since_beharad, + } + } + + /// 新年首日(Tishrei 1)的 ISO 纪元日。 + fn new_year(self) -> i128 { + // Beharad 周从周一开始。 + const BEHARAD_START_OF_YEAR: i128 = StartOfYear::Monday as i128; + let days_since_beharad = self.weeks_since_beharad * 7 + + self.keviyah.start_of_year() as i128 + - BEHARAD_START_OF_YEAR; + HEBREW_EPOCH_DAYS + days_since_beharad + } + + /// 含日 rd 的年份:先以平均年长(35975351/98496)近似,再校正。 + fn year_containing_rd(rd: i128) -> (Self, i128) { + let mut h_year = (rd - HEBREW_EPOCH_DAYS) * 98496 / 35975351 + (rd >= HEBREW_EPOCH_DAYS) as i128; + let mut year = Self::compute_for(h_year); + if rd < year.new_year() { + h_year -= 1; + year = Self::compute_for(h_year); + } else if rd >= year.new_year() + year.keviyah.year_length() as i128 { + h_year += 1; + year = Self::compute_for(h_year); + } + (year, h_year) + } +} + +// ───────────────────────── hebrew 对外接口 ───────────────────────── + +/// hebrew 是否闰年(19 年周期 3、6、8、11、14、17、19 闰)。 +pub(crate) fn hebrew_is_leap(h_year: i128) -> bool { + YearInfo::compute_for(h_year).keviyah.is_leap() +} + +/// hebrew 年长(353/354/355/383/384/385)。 +pub(crate) fn hebrew_year_length(h_year: i128) -> i128 { + YearInfo::compute_for(h_year).keviyah.year_length() as i128 +} + +/// hebrew 某序月的月长。 +pub(crate) fn hebrew_days_in_month(h_year: i128, ordinal_month: u32) -> i128 { + YearInfo::compute_for(h_year).keviyah.month_len(ordinal_month as u8) as i128 +} + +/// hebrew 年内的月数(平年 12、闰年 13)。 +pub(crate) fn hebrew_months_in_year(h_year: i128) -> i128 { + 12 + YearInfo::compute_for(h_year).keviyah.is_leap() as i128 +} + +/// hebrew 纪元恒为 "am"(Anno Mundi),eraYear 即年号。 +pub(crate) const HEBREW_ERA: &str = "am"; + +/// hebrew 日期(年, 序月, 日)→ ISO 纪元日。 +pub(crate) fn hebrew_to_iso_days(h_year: i128, ordinal_month: u32, day: u32) -> i128 { + let year_info = YearInfo::compute_for(h_year); + year_info.new_year() + + year_info.keviyah.days_preceding(ordinal_month as u8) as i128 + + (day - 1) as i128 +} + +/// ISO 纪元日 → hebrew 日期(年, 序月, 日)。 +pub(crate) fn iso_days_to_hebrew(iso_days: i128) -> (i128, u32, u32) { + let (year_info, h_year) = YearInfo::year_containing_rd(iso_days); + let day_of_year = (iso_days - year_info.new_year()) as u16 + 1; + let (month, day) = year_info.keviyah.month_day_for(day_of_year); + (h_year, month as u32, day as u32) +} + +/// hebrew 日期(年, 序月, 日)→ ISO 日期分量。 +pub(crate) fn hebrew_to_iso(h_year: i128, ordinal_month: u32, day: u32) -> (i32, u32, u32) { + let (y, m, d) = civil_from_days(hebrew_to_iso_days(h_year, ordinal_month, day)); + (y as i32, m as u32, d as u32) +} + +/// ISO 日期分量 → hebrew 日期(年, 序月, 日)。 +pub(crate) fn iso_to_hebrew(iso_year: i32, iso_month: u32, iso_day: u32) -> (i128, u32, u32) { + iso_days_to_hebrew(days_from_civil( + i128::from(iso_year), + i128::from(iso_month), + i128::from(iso_day), + )) +} + +/// 数值月(平年 1-12、闰年 1-13)→ 序月:hebrew 数值月即序月(闰年 Adar I 为 6、 +/// Adar II 为 7、Elul 为 13),恒等映射。越界返回 None。 +pub(crate) fn hebrew_ordinal_from_number(h_year: i128, number: u32) -> Option { + let max = if hebrew_is_leap(h_year) { 13 } else { 12 }; + (1..=max).contains(&number).then_some(number) +} + +/// 序月 → 数值月:hebrew 数值月即序月(平年 1-12、闰年 1-13),恒等映射。 +pub(crate) fn hebrew_number_from_ordinal(ordinal_month: u32) -> Option { + (1..=13).contains(&ordinal_month).then_some(ordinal_month) +} + +/// 月码 → 序月:M05L 仅闰年合法(序月 6);M06-M12 闰年 +1。 +/// 非法(含 M13、非闰年的 M05L、M02L 等)返回 None。 +pub(crate) fn hebrew_ordinal_from_month_code(h_year: i128, code: &str) -> Option { + let is_leap = hebrew_is_leap(h_year); + if code.ends_with('L') { + return (code == "M05L" && is_leap).then_some(6); + } + let number: u32 = code[1..3].parse().ok()?; + if !(1..=12).contains(&number) { + return None; + } + let ordinal = number + (is_leap && number >= 6) as u32; + Some(ordinal) +} + +/// 序月 → 月码:闰年序月 6 为 M05L,序月 7 及以上 -1。 +pub(crate) fn hebrew_month_code(h_year: i128, ordinal_month: u32) -> String { + let is_leap = hebrew_is_leap(h_year); + if is_leap && ordinal_month == 6 { + return "M05L".to_string(); + } + let number = ordinal_month - (is_leap && ordinal_month >= 6) as u32; + format!("M{number:02}") +} + +#[cfg(test)] +mod tests { + use super::*; + + // 锚点:Tishrei 1 年 3760 应为周六(ICU4X 同款测试向量)。 + #[test] + fn test_epoch_3760_saturday() { + let (y, m, d) = hebrew_to_iso(3760, 1, 1); + // 1 年 8 月 28 日(proleptic Gregorian) + assert_eq!((y, m, d), (-1, 8, 28)); + } + + // 锚点:Rosh Hashanah 5785 = 2024-10-03(周四),5784 = 2023-09-16(周六), + // 5786 = 2025-09-23(周二)。新年首日不可能是周日/周三/周五,可自查锚点。 + #[test] + fn test_rosh_hashanah_anchors() { + let (y, m, d) = hebrew_to_iso(5784, 1, 1); + assert_eq!((y, m, d), (2023, 9, 16)); + let (y, m, d) = hebrew_to_iso(5785, 1, 1); + assert_eq!((y, m, d), (2024, 10, 3)); + let (y, m, d) = hebrew_to_iso(5786, 1, 1); + assert_eq!((y, m, d), (2025, 9, 23)); + } + + // 双向换算自洽(语料年份区间)。 + #[test] + fn test_roundtrip() { + for h_year in [0, 1, 5, 10, 3700, 5781, 5782, 5783, 5784, 5785, 5800] { + let months = if hebrew_is_leap(h_year) { 13 } else { 12 }; + for month in 1..=months { + let dim = hebrew_days_in_month(h_year, month) as u32; + let (y, m, d) = hebrew_to_iso(h_year, month, dim); + let (hy, hm, hd) = iso_to_hebrew(y, m, d); + assert_eq!((hy, hm, hd), (h_year, month, dim), "year {h_year} month {month}"); + } + } + } + + // keviyah 年长取值合法(语料 validKeviahSymbols 的 6 种年长)。 + #[test] + fn test_year_lengths_valid() { + for year in 3700..=5800 { + let len = hebrew_year_length(year); + assert!( + matches!(len, 353 | 354 | 355 | 383 | 384 | 385), + "year {year} -> {len}" + ); + } + } + + // 语料 basic-hebrew 5783(平年)逐月长度。 + #[test] + fn test_month_lengths_5783() { + let expected = [30, 30, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29]; + for (index, &len) in expected.iter().enumerate() { + let month = (index + 1) as u32; + assert_eq!(hebrew_days_in_month(5783, month), len as i128, "month {month}"); + } + assert_eq!(hebrew_months_in_year(5783), 12); + assert!(!hebrew_is_leap(5783)); + } + + // 语料 leap-year-hebrew 5784(闰年)逐月长度。 + #[test] + fn test_month_lengths_5784() { + let expected = [30, 29, 29, 29, 30, 30, 29, 30, 29, 30, 29, 30, 29]; + for (index, &len) in expected.iter().enumerate() { + let month = (index + 1) as u32; + assert_eq!(hebrew_days_in_month(5784, month), len as i128, "month {month}"); + } + assert_eq!(hebrew_months_in_year(5784), 13); + assert!(hebrew_is_leap(5784)); + } + + // 语料 calc-epoch-year-hebrew:年 0..10 的 Cheshvan / Kislev 月长 + // (含 ICU4C ICU-23007 的纪元年推迟修正)。 + #[test] + fn test_epoch_years_cheshvan_kislev() { + let cheshvan = [29, 30, 30, 29, 29, 30, 30, 29, 29, 30, 29]; + let kislev = [30, 30, 30, 29, 30, 30, 30, 30, 29, 30, 30]; + for (year, (c, k)) in cheshvan.iter().zip(kislev.iter()).enumerate() { + assert_eq!(hebrew_days_in_month(year as i128, 2), *c as i128, "year {year}"); + assert_eq!(hebrew_days_in_month(year as i128, 3), *k as i128, "year {year}"); + } + } + + // 月码映射(闰年 5784 / 平年 5783)。 + #[test] + fn test_month_code_mapping() { + // 闰年 + assert_eq!(hebrew_month_code(5784, 6), "M05L"); + assert_eq!(hebrew_month_code(5784, 7), "M06"); + assert_eq!(hebrew_month_code(5784, 13), "M12"); + assert_eq!(hebrew_ordinal_from_month_code(5784, "M05L"), Some(6)); + assert_eq!(hebrew_ordinal_from_month_code(5784, "M06"), Some(7)); + assert_eq!(hebrew_ordinal_from_month_code(5784, "M12"), Some(13)); + assert_eq!(hebrew_ordinal_from_month_code(5784, "M13"), None); + // 平年 + assert_eq!(hebrew_month_code(5783, 6), "M06"); + assert_eq!(hebrew_month_code(5783, 12), "M12"); + assert_eq!(hebrew_ordinal_from_month_code(5783, "M06"), Some(6)); + assert_eq!(hebrew_ordinal_from_month_code(5783, "M05L"), None); + assert_eq!(hebrew_ordinal_from_month_code(5783, "M12"), Some(12)); + } + + // ICU4X 测试向量:ISO ↔ hebrew 双向换算。 + #[derive(Clone, Copy)] + struct Vector4x { + iso: (i32, u32, u32), + heb: (i32, u32, u32), + } + + #[test] + fn test_icu4x_vectors() { + let cases: [Vector4x; 6] = [ + // iso 为公历年月日,heb 为希伯来年与序数月与日 + Vector4x { iso: (2021, 1, 10), heb: (5781, 4, 26) }, + Vector4x { iso: (2021, 9, 10), heb: (5782, 1, 4) }, + Vector4x { iso: (2022, 2, 10), heb: (5782, 6, 9) }, // Adar I(闰年) + Vector4x { iso: (2022, 3, 10), heb: (5782, 7, 7) }, // Adar II(闰年) + Vector4x { iso: (2022, 10, 10), heb: (5783, 1, 15) }, + Vector4x { iso: (2022, 12, 25), heb: (5783, 4, 1) }, + ]; + for v in cases { + let (iy, im, id) = v.iso; + let (hy, hm, hd) = v.heb; + let (y, m, d) = hebrew_to_iso(hy as i128, hm, hd); + assert_eq!((y, m, d), (iy, im, id), "hebrew {hy}-{hm}-{hd}"); + let (ry, rm, rd) = iso_to_hebrew(iy, im, id); + assert_eq!((ry, rm, rd), (hy as i128, hm, hd), "iso {iy}-{im}-{id}"); + } + } +} diff --git a/crates/oxide_builtins/src/temporal/mod.rs b/crates/oxide_builtins/src/temporal/mod.rs index 318c408e..00c900a0 100644 --- a/crates/oxide_builtins/src/temporal/mod.rs +++ b/crates/oxide_builtins/src/temporal/mod.rs @@ -1,3 +1,4 @@ +mod calendar; mod common; mod difference; mod duration; diff --git a/crates/oxide_builtins/src/temporal/plain_date.rs b/crates/oxide_builtins/src/temporal/plain_date.rs index bc64c951..944e2787 100644 --- a/crates/oxide_builtins/src/temporal/plain_date.rs +++ b/crates/oxide_builtins/src/temporal/plain_date.rs @@ -5,14 +5,20 @@ use oxide_runtime_api::{to_string, NativeResult, ProtoKind, VmHost}; use oxide_types::object::JsObject; use oxide_types::value::JsValue; +use super::calendar::{ + hebrew_days_in_month, hebrew_is_leap, hebrew_month_code, hebrew_months_in_year, + hebrew_number_from_ordinal, hebrew_ordinal_from_month_code, hebrew_ordinal_from_number, hebrew_to_iso, + hebrew_year_length, iso_to_hebrew, HEBREW_ERA, +}; use super::common::native_try; use super::common::{ calendar_annotation, civil_from_days, days_from_civil, days_in_month, ensure_plain_date, format_iso_year, - get_calendar_id, get_double_prop, initialize_temporal_receiver, is_ctor_call, parse_plain_date_string, - parse_round_options, plain_date_time_object_parts, receiver_obj, reject_partial_object_with_calendar_or_time_zone, - resolve_date_time_parts, temporal_calendar_id, temporal_calendar_id_strict, temporal_number_component, - temporal_option_string, temporal_option_value, temporal_overflow, temporal_to_show_calendar, - to_temporal_time_zone_identifier, zoned_date_time_plain_parts, DateTimeParts, ShowCalendar, + get_calendar_id, get_double_prop, initialize_temporal_receiver, is_ctor_call, month_code_well_formed, + parse_plain_date_string, parse_round_options, plain_date_time_object_parts, receiver_obj, + reject_partial_object_with_calendar_or_time_zone, resolve_date_time_parts, temporal_calendar_id, + temporal_calendar_id_strict, temporal_number_component, temporal_option_string, temporal_option_value, + temporal_overflow, temporal_to_show_calendar, to_temporal_time_zone_identifier, zoned_date_time_plain_parts, + DateTimeParts, ShowCalendar, }; use super::difference::{difference_core, parse_difference_settings, IsoDateTime}; use super::duration::duration_like_values; @@ -249,22 +255,54 @@ fn plain_date_ymd(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64), Ok((get_double_prop(obj, 0), get_double_prop(obj, 1), get_double_prop(obj, 2))) } -/// `Temporal.PlainDate.prototype.year` getter。 +/// 读 PlainDate 的 ISO 分量(槽 0-2)与日历 ID(槽 3),含 receiver 校验。 +/// getter 按日历 ID 分发:hebrew 换算为历法字段,其余历法直返 ISO 分量。 +fn plain_date_iso_ymd_calendar( + vm: &mut H, args: &[u8], +) -> Result<(i32, u32, u32, String), JsValue> { + let ptr = receiver_obj(vm, args)?; + let obj = unsafe { &*ptr }; + ensure_plain_date(vm, obj)?; + let y = get_double_prop(obj, 0); + let m = get_double_prop(obj, 1); + let d = get_double_prop(obj, 2); + if y.is_nan() || m.is_nan() || d.is_nan() { + return Err(crate::error::create_type_error(vm, "called on incompatible receiver")); + } + let calendar = get_calendar_id(obj, 3); + Ok((y.trunc() as i32, m.trunc() as u32, d.trunc() as u32, calendar)) +} + +/// `Temporal.PlainDate.prototype.year` getter:hebrew 历返历法年,其余历法返 ISO 年。 pub fn plain_date_year(vm: &mut H, args: &[u8]) -> NativeResult { - let (y, _, _) = native_try!(plain_date_ymd(vm, args)); - NativeResult::Ok(JsValue::float(y)) + let (y, m, d, calendar) = native_try!(plain_date_iso_ymd_calendar(vm, args)); + if calendar == "hebrew" { + let (hy, _, _) = iso_to_hebrew(y, m, d); + return NativeResult::Ok(JsValue::float(hy as f64)); + } + NativeResult::Ok(JsValue::float(y as f64)) } -/// `Temporal.PlainDate.prototype.month` getter。 +/// `Temporal.PlainDate.prototype.month` getter:hebrew 历返数值月(序月),其余历法返 ISO 月。 pub fn plain_date_month(vm: &mut H, args: &[u8]) -> NativeResult { - let (_, m, _) = native_try!(plain_date_ymd(vm, args)); - NativeResult::Ok(JsValue::float(m)) + let (y, m, d, calendar) = native_try!(plain_date_iso_ymd_calendar(vm, args)); + if calendar == "hebrew" { + let (hy, ordinal, _) = iso_to_hebrew(y, m, d); + let _ = hy; + let number = hebrew_number_from_ordinal(ordinal).unwrap_or(0); + return NativeResult::Ok(JsValue::float(number as f64)); + } + NativeResult::Ok(JsValue::float(m as f64)) } -/// `Temporal.PlainDate.prototype.day` getter。 +/// `Temporal.PlainDate.prototype.day` getter:hebrew 历返历法日,其余历法返 ISO 日。 pub fn plain_date_day(vm: &mut H, args: &[u8]) -> NativeResult { - let (_, _, d) = native_try!(plain_date_ymd(vm, args)); - NativeResult::Ok(JsValue::float(d)) + let (y, m, d, calendar) = native_try!(plain_date_iso_ymd_calendar(vm, args)); + if calendar == "hebrew" { + let (_hy, _ordinal, hebrew_day) = iso_to_hebrew(y, m, d); + return NativeResult::Ok(JsValue::float(hebrew_day as f64)); + } + NativeResult::Ok(JsValue::float(d as f64)) } /// PlainDate ISO 串:auto/never 加 iso8601 日历保持裸 `±YYYY-MM-DD`; @@ -350,10 +388,130 @@ fn object_date_ymd( let (year, month, day, _) = zoned_date_time_plain_parts(vm, obj)?; return Ok((year, month, day, Some(get_calendar_id(obj, 2)))); } + // 先读日历:hebrew 走专用解析(era/eraYear 校验 + hebrew→ISO 换算), + // 其余历法走 ISO 路径。 + let calendar_raw = temporal_option_value(vm, obj, val, "calendar")?; + let calendar = temporal_calendar_id(vm, calendar_raw)?; + if calendar.as_deref() == Some("hebrew") { + let (year, month, day) = hebrew_object_date_ymd(vm, obj, val, constrain)?; + return Ok((year, month, day, calendar)); + } let (year, month, day, _, calendar) = plain_date_time_object_parts(vm, val, obj, constrain, true)?; Ok((year, month, day, calendar)) } +/// hebrew property bag 解析:读 era/eraYear/year/month/monthCode/day,定 hebrew +/// 年/序月/日,换算为 ISO 年/月/日后入槽。 +/// +/// # 步骤 +/// 1. 读序(字典序):day → era → eraYear → month → monthCode → year。 +/// 2. day 必填(TypeError)。 +/// 3. era 族:恰一个现 → TypeError;双现 → era 须为 "am"(RangeError),eraYear 定年; +/// 双缺 → year 必填(TypeError)。 +/// 4. month/monthCode 定序月(冲突 → RangeError)。 +/// 5. day 校验(1..=月长;constrain 钳制、reject 抛 RangeError)。 +/// 6. hebrew → ISO 换算。 +fn hebrew_object_date_ymd( + vm: &mut H, obj: &JsObject, value: JsValue, constrain: bool, +) -> Result<(i32, u32, u32), JsValue> { + let day_raw = temporal_option_value(vm, obj, value, "day")?; + let era_raw = temporal_option_value(vm, obj, value, "era")?; + let era_year_raw = temporal_option_value(vm, obj, value, "eraYear")?; + let month_raw = temporal_option_value(vm, obj, value, "month")?; + let month_code_raw = temporal_option_value(vm, obj, value, "monthCode")?; + let year_raw = temporal_option_value(vm, obj, value, "year")?; + + if day_raw.is_undefined() { + return Err(crate::error::create_type_error(vm, "day is required")); + } + + // era 族:恰一个现 → TypeError;双现 → era 须为 "am"、eraYear 定年;双缺 → year 必填。 + let era_present = !era_raw.is_undefined(); + let era_year_present = !era_year_raw.is_undefined(); + if era_present != era_year_present { + return Err(crate::error::create_type_error( + vm, "era and eraYear must both be present or both absent", + )); + } + let hebrew_year = if era_year_present { + let era = temporal_option_string(vm, era_raw)?; + if era != HEBREW_ERA { + return Err(crate::error::create_range_error(vm, "invalid era")); + } + let era_year = temporal_number_component(vm, era_year_raw)?; + if era_year < i32::MIN as f64 || era_year > i32::MAX as f64 { + return Err(crate::error::create_range_error(vm, "invalid eraYear")); + } + era_year as i32 + } else { + if year_raw.is_undefined() { + return Err(crate::error::create_type_error(vm, "year is required")); + } + let year = temporal_number_component(vm, year_raw)?; + if year < i32::MIN as f64 || year > i32::MAX as f64 { + return Err(crate::error::create_range_error(vm, "invalid year")); + } + year as i32 + }; + + // month / monthCode 定序月。 + let month_code = if month_code_raw.is_undefined() { + None + } else { + if !month_code_raw.is_string() { + return Err(crate::error::create_type_error(vm, "monthCode must be a string")); + } + let code = to_string(month_code_raw); + if !month_code_well_formed(&code) { + return Err(crate::error::create_range_error(vm, "invalid monthCode")); + } + Some(code) + }; + let ordinal_month = match (month_raw.is_undefined(), month_code) { + (true, None) => { + return Err(crate::error::create_type_error(vm, "month is required")); + } + (true, Some(code)) => hebrew_ordinal_from_month_code(hebrew_year as i128, &code) + .ok_or_else(|| crate::error::create_range_error(vm, "invalid monthCode"))?, + (false, Some(code)) => { + let ordinal_from_code = hebrew_ordinal_from_month_code(hebrew_year as i128, &code) + .ok_or_else(|| crate::error::create_range_error(vm, "invalid monthCode"))?; + let month_num = temporal_number_component(vm, month_raw)?; + let ordinal_from_num = hebrew_ordinal_from_number(hebrew_year as i128, month_num as u32) + .ok_or_else(|| crate::error::create_range_error(vm, "invalid month"))?; + if ordinal_from_code != ordinal_from_num { + return Err(crate::error::create_range_error(vm, "month and monthCode disagree")); + } + ordinal_from_num + } + (false, None) => { + let month_num = temporal_number_component(vm, month_raw)?; + hebrew_ordinal_from_number(hebrew_year as i128, month_num as u32) + .ok_or_else(|| crate::error::create_range_error(vm, "invalid month"))? + } + }; + + // day 校验(1..=月长;constrain 钳制、reject 抛 RangeError)。 + let day = temporal_number_component(vm, day_raw)?; + if day < 1.0 { + return Err(crate::error::create_range_error(vm, "invalid day")); + } + let month_len = hebrew_days_in_month(hebrew_year as i128, ordinal_month) as u32; + let day = if day as u32 > month_len { + if constrain { + month_len + } else { + return Err(crate::error::create_range_error(vm, "invalid day")); + } + } else { + day as u32 + }; + + // hebrew → ISO 换算。 + let (iso_year, iso_month, iso_day) = hebrew_to_iso(hebrew_year as i128, ordinal_month, day); + Ok((iso_year, iso_month, iso_day)) +} + fn date_like_ymd(vm: &mut H, val: JsValue) -> Result<(i32, u32, u32, Option), JsValue> { let ymd = if val.is_string() { let (y, m, d) = parse_plain_date_string(&to_string(val)) @@ -414,13 +572,14 @@ pub(crate) fn days_in_month_iso(year: i32, month: u32) -> u32 { } } -/// `Temporal.PlainDate.prototype.daysInMonth` getter。 +/// `Temporal.PlainDate.prototype.daysInMonth` getter:hebrew 历返历法月长,其余历法返 ISO 月长。 pub fn plain_date_days_in_month(vm: &mut H, args: &[u8]) -> NativeResult { - let date = match plain_date_naive(vm, args) { - Ok(d) => d, - Err(e) => return NativeResult::Err(e), - }; - NativeResult::Ok(JsValue::float(days_in_month_iso(date.year(), date.month()) as f64)) + let (y, m, d, calendar) = native_try!(plain_date_iso_ymd_calendar(vm, args)); + if calendar == "hebrew" { + let (hy, ordinal, _) = iso_to_hebrew(y, m, d); + return NativeResult::Ok(JsValue::float(hebrew_days_in_month(hy, ordinal) as f64)); + } + NativeResult::Ok(JsValue::float(days_in_month_iso(y, m) as f64)) } /// `Temporal.PlainDate.prototype.daysInWeek` getter:恒 7。 @@ -433,28 +592,34 @@ pub(crate) fn is_leap_year_iso(year: i32) -> bool { NaiveDate::from_ymd_opt(year, 2, 29).is_some() } -/// `Temporal.PlainDate.prototype.daysInYear` getter:366(闰年)或 365。 +/// `Temporal.PlainDate.prototype.daysInYear` getter:hebrew 历返历法年长,其余历法 366/365。 pub fn plain_date_days_in_year(vm: &mut H, args: &[u8]) -> NativeResult { - let date = match plain_date_naive(vm, args) { - Ok(d) => d, - Err(e) => return NativeResult::Err(e), - }; - NativeResult::Ok(JsValue::float(if is_leap_year_iso(date.year()) { 366.0 } else { 365.0 })) + let (y, m, d, calendar) = native_try!(plain_date_iso_ymd_calendar(vm, args)); + if calendar == "hebrew" { + let (hy, _, _) = iso_to_hebrew(y, m, d); + return NativeResult::Ok(JsValue::float(hebrew_year_length(hy) as f64)); + } + NativeResult::Ok(JsValue::float(if is_leap_year_iso(y) { 366.0 } else { 365.0 })) } -/// `Temporal.PlainDate.prototype.monthsInYear` getter:恒 12(ISO 日历)。 +/// `Temporal.PlainDate.prototype.monthsInYear` getter:hebrew 历 12/13,其余历法恒 12。 pub fn plain_date_months_in_year(vm: &mut H, args: &[u8]) -> NativeResult { - let _ = native_try!(plain_date_ymd(vm, args)); + let (y, m, d, calendar) = native_try!(plain_date_iso_ymd_calendar(vm, args)); + if calendar == "hebrew" { + let (hy, _, _) = iso_to_hebrew(y, m, d); + return NativeResult::Ok(JsValue::float(hebrew_months_in_year(hy) as f64)); + } NativeResult::Ok(JsValue::float(12.0)) } -/// `Temporal.PlainDate.prototype.inLeapYear` getter。 +/// `Temporal.PlainDate.prototype.inLeapYear` getter:hebrew 历按历法闰年判定。 pub fn plain_date_in_leap_year(vm: &mut H, args: &[u8]) -> NativeResult { - let date = match plain_date_naive(vm, args) { - Ok(d) => d, - Err(e) => return NativeResult::Err(e), - }; - NativeResult::Ok(JsValue::bool(is_leap_year_iso(date.year()))) + let (y, m, d, calendar) = native_try!(plain_date_iso_ymd_calendar(vm, args)); + if calendar == "hebrew" { + let (hy, _, _) = iso_to_hebrew(y, m, d); + return NativeResult::Ok(JsValue::bool(hebrew_is_leap(hy))); + } + NativeResult::Ok(JsValue::bool(is_leap_year_iso(y))) } /// `Temporal.PlainDate.prototype.weekOfYear` getter:ISO 周数。 @@ -475,24 +640,33 @@ pub fn plain_date_year_of_week(vm: &mut H, args: &[u8]) -> NativeResu NativeResult::Ok(JsValue::float(date.iso_week().year() as f64)) } -/// `Temporal.PlainDate.prototype.monthCode` getter:`M01`..`M12`。 +/// `Temporal.PlainDate.prototype.monthCode` getter:ISO 历 `M01`..`M12`; +/// hebrew 历含闰月 `M05L`。 pub fn plain_date_month_code(vm: &mut H, args: &[u8]) -> NativeResult { - let (_, m, _) = match plain_date_ymd(vm, args) { - Ok(x) => x, - Err(e) => return NativeResult::Err(e), - }; + let (y, m, d, calendar) = native_try!(plain_date_iso_ymd_calendar(vm, args)); + if calendar == "hebrew" { + let (hy, ordinal, _) = iso_to_hebrew(y, m, d); + return NativeResult::Ok(vm.new_string_owned(hebrew_month_code(hy, ordinal))); + } NativeResult::Ok(vm.new_string(&format!("M{m:02}"))) } -/// `Temporal.PlainDate.prototype.era` getter:ISO 日历无纪元,恒 undefined。 +/// `Temporal.PlainDate.prototype.era` getter:hebrew 历恒 "am",其余历法无纪元恒 undefined。 pub fn plain_date_era(vm: &mut H, args: &[u8]) -> NativeResult { - let _ = native_try!(plain_date_ymd(vm, args)); + let (_y, _m, _d, calendar) = native_try!(plain_date_iso_ymd_calendar(vm, args)); + if calendar == "hebrew" { + return NativeResult::Ok(vm.new_string(HEBREW_ERA)); + } NativeResult::Ok(JsValue::undefined()) } -/// `Temporal.PlainDate.prototype.eraYear` getter:ISO 日历无纪元,恒 undefined。 +/// `Temporal.PlainDate.prototype.eraYear` getter:hebrew 历返历法年,其余历法恒 undefined。 pub fn plain_date_era_year(vm: &mut H, args: &[u8]) -> NativeResult { - let _ = native_try!(plain_date_ymd(vm, args)); + let (y, m, d, calendar) = native_try!(plain_date_iso_ymd_calendar(vm, args)); + if calendar == "hebrew" { + let (hy, _, _) = iso_to_hebrew(y, m, d); + return NativeResult::Ok(JsValue::float(hy as f64)); + } NativeResult::Ok(JsValue::undefined()) } diff --git a/crates/oxide_builtins/src/temporal/plain_year_month.rs b/crates/oxide_builtins/src/temporal/plain_year_month.rs index c48817e3..920ac2d2 100644 --- a/crates/oxide_builtins/src/temporal/plain_year_month.rs +++ b/crates/oxide_builtins/src/temporal/plain_year_month.rs @@ -4,6 +4,7 @@ use oxide_runtime_api::{to_string, NativeResult, ProtoKind, VmHost}; use oxide_types::object::JsObject; use oxide_types::value::JsValue; +use super::calendar::HEBREW_ERA; use super::common::native_try; use super::{ calendar_annotation, compare_iso_date, days_from_civil, days_in_month_iso, difference_core, duration_like_values, @@ -351,12 +352,27 @@ pub fn plain_year_month_from(vm: &mut H, args: &[u8]) -> NativeResult let era_raw = native_try!(temporal_option_value(vm, obj, value, "era")); let era_year_raw = native_try!(temporal_option_value(vm, obj, value, "eraYear")); let constrain = native_try!(temporal_overflow(vm, args)); - // era 族:双现 → RangeError;恰一个 → 忽略(ISO 无纪元体系)。 - if !era_raw.is_undefined() && !era_year_raw.is_undefined() { + // era 族:hebrew(有纪元)恰一个现 → TypeError、双现校验 era 为 "am"; + // 其余历法沿用既有处理(双现 RangeError、恰一个忽略)。 + let era_present = !era_raw.is_undefined(); + let era_year_present = !era_year_raw.is_undefined(); + if calendar == "hebrew" { + if era_present != era_year_present { + return NativeResult::Err(crate::error::create_type_error( + vm, "era and eraYear must both be present or both absent", + )); + } + if era_present { + let era = native_try!(temporal_option_string(vm, era_raw)); + if era != HEBREW_ERA { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid era")); + } + } + } else if era_present && era_year_present { return NativeResult::Err(crate::error::create_range_error(vm, "era and eraYear cannot both be present")); } - // 存在性:year 必填(先于 monthCode 语法);month|monthCode 至少其一。 - if year_raw.is_undefined() { + // 存在性:year 必填(hebrew 由 eraYear 定年时豁免);month|monthCode 至少其一。 + if year_raw.is_undefined() && !(calendar == "hebrew" && era_year_present) { return NativeResult::Err(crate::error::create_type_error(vm, "year is required")); } if month_raw.is_undefined() && month_code_raw.is_undefined() { @@ -397,12 +413,20 @@ pub fn plain_year_month_from(vm: &mut H, args: &[u8]) -> NativeResult } None => (None, false), }; - // year 转换(TypeError/RangeError)在 monthCode 语法之后。 - let year_raw = native_try!(temporal_number_component(vm, year_raw)); - let year = if year_raw >= i32::MIN as f64 && year_raw <= i32::MAX as f64 { - year_raw as i32 + // year 转换(TypeError/RangeError)在 monthCode 语法之后;hebrew 由 eraYear 定年。 + let year = if calendar == "hebrew" && era_year_present { + let era_year = native_try!(temporal_number_component(vm, era_year_raw)); + if era_year < i32::MIN as f64 || era_year > i32::MAX as f64 { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid eraYear")); + } + era_year as i32 } else { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid year")); + let year_raw = native_try!(temporal_number_component(vm, year_raw)); + if year_raw >= i32::MIN as f64 && year_raw <= i32::MAX as f64 { + year_raw as i32 + } else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid year")); + } }; let month_f = match month_raw.is_undefined() { true => None,