From 741b179ca50116f57eb2219fea94d7630b6afe66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 2 Oct 2026 14:08:04 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=9A=E6=8A=BD=E5=87=BA?= =?UTF-8?q?=E5=85=AC=E5=8E=86=E6=97=A5=E6=9C=9F=E5=9F=BA=E5=85=83=E4=B8=BA?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E6=A8=A1=E5=9D=97=20civil=5Fcalendar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 server-rs/crates/module-runtime/src/civil_calendar.rs:days_in_month / is_leap_year / days_from_civil / civil_from_days / weekday_from_date_key - application.rs 删除上述重复实现,改为引用新模块,行为不变 - 新模块自带单元测试:闰年规则、日序号往返、已知日期星期 - 后续会员自然月账期、分析维表等纯日期逻辑统一复用该模块 Co-authored-by: Junie --- .../crates/module-runtime/src/application.rs | 52 +--------- .../module-runtime/src/civil_calendar.rs | 94 +++++++++++++++++++ server-rs/crates/module-runtime/src/lib.rs | 2 + 3 files changed, 99 insertions(+), 49 deletions(-) create mode 100644 server-rs/crates/module-runtime/src/civil_calendar.rs diff --git a/server-rs/crates/module-runtime/src/application.rs b/server-rs/crates/module-runtime/src/application.rs index 8e7bd07c8..19de95527 100644 --- a/server-rs/crates/module-runtime/src/application.rs +++ b/server-rs/crates/module-runtime/src/application.rs @@ -8,6 +8,9 @@ use serde_json::Value; use shared_kernel::{offset_datetime_to_unix_micros, parse_rfc3339}; use std::collections::{BTreeMap, BTreeSet}; +use crate::civil_calendar::{ + civil_from_days, days_from_civil, days_in_month, is_leap_year, weekday_from_date_key, +}; use crate::domain::*; use crate::errors::RuntimeProfileFieldError; use crate::format_utc_micros; @@ -1375,25 +1378,6 @@ fn validate_calendar_date(year: i32, month: u8, day: u8) -> Result<(), RuntimePr Ok(()) } -fn days_in_month(year: i32, month: u8) -> u8 { - match month { - 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, - 4 | 6 | 9 | 11 => 30, - 2 if is_leap_year(year) => 29, - 2 => 28, - _ => 0, - } -} - -fn is_leap_year(year: i32) -> bool { - (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 -} - -fn weekday_from_date_key(date_key: i64) -> u8 { - // 中文注释:1970-01-01 是周四;这里返回 ISO weekday,周一=1,周日=7。 - (date_key + 3).rem_euclid(7) as u8 + 1 -} - fn iso_week_key(year: i32, month: u8, day: u8, weekday: u8) -> i32 { let ordinal = ordinal_day(year, month, day); let week = (i32::from(ordinal) - i32::from(weekday) + 10).div_euclid(7); @@ -1430,36 +1414,6 @@ fn iso_weeks_in_year(year: i32) -> i32 { } } -fn days_from_civil(year: i32, month: u8, day: u8) -> i64 { - // 中文注释:Howard Hinnant civil calendar 算法,返回 1970-01-01 起的日序号。 - let adjusted_year = year - if month <= 2 { 1 } else { 0 }; - let era = adjusted_year.div_euclid(400); - let year_of_era = adjusted_year - era * 400; - let month = i32::from(month); - let day = i32::from(day); - let month_prime = month + if month > 2 { -3 } else { 9 }; - let day_of_year = (153 * month_prime + 2) / 5 + day - 1; - let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year; - i64::from(era * 146_097 + day_of_era - 719_468) -} - -fn civil_from_days(date_key: i64) -> (i32, u8, u8) { - // 中文注释:days_from_civil 的反向算法,避免依赖运行环境时区。 - let z = date_key + 719_468; - let era = z.div_euclid(146_097); - let day_of_era = z - era * 146_097; - let year_of_era = (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - - day_of_era / 146_096) - .div_euclid(365); - let mut year = year_of_era + era * 400; - let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100); - let month_prime = (5 * day_of_year + 2).div_euclid(153); - let day = day_of_year - (153 * month_prime + 2).div_euclid(5) + 1; - let month = month_prime + if month_prime < 10 { 3 } else { -9 }; - year += if month <= 2 { 1 } else { 0 }; - (year as i32, month as u8, day as u8) -} - pub fn build_default_runtime_profile_task_config( updated_at_micros: i64, updated_by: String, diff --git a/server-rs/crates/module-runtime/src/civil_calendar.rs b/server-rs/crates/module-runtime/src/civil_calendar.rs new file mode 100644 index 000000000..60c0a4ec8 --- /dev/null +++ b/server-rs/crates/module-runtime/src/civil_calendar.rs @@ -0,0 +1,94 @@ +//! 公历日期基元(不依赖运行环境时区)。 +//! +//! 只做「公历年月日 ↔ 1970-01-01 起日序号」的纯换算,采用 Howard Hinnant 的 civil calendar +//! 算法。北京时间(UTC+8)与业务日边界由调用方叠加固定偏移,因此本模块永远不读系统时区。 + +/// 公历给定年月的天数,含闰年规则。 +pub fn days_in_month(year: i32, month: u8) -> u8 { + match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 if is_leap_year(year) => 29, + 2 => 28, + _ => 0, + } +} + +/// 公历闰年判断。 +pub fn is_leap_year(year: i32) -> bool { + (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 +} + +/// 公历年月日 → 1970-01-01 起的日序号。 +pub fn days_from_civil(year: i32, month: u8, day: u8) -> i64 { + // 中文注释:Howard Hinnant civil calendar 算法,返回 1970-01-01 起的日序号。 + let adjusted_year = year - if month <= 2 { 1 } else { 0 }; + let era = adjusted_year.div_euclid(400); + let year_of_era = adjusted_year - era * 400; + let month = i32::from(month); + let day = i32::from(day); + let month_prime = month + if month > 2 { -3 } else { 9 }; + let day_of_year = (153 * month_prime + 2) / 5 + day - 1; + let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year; + i64::from(era * 146_097 + day_of_era - 719_468) +} + +/// 1970-01-01 起的日序号 → 公历年月日。 +pub fn civil_from_days(date_key: i64) -> (i32, u8, u8) { + // 中文注释:days_from_civil 的反向算法,避免依赖运行环境时区。 + let z = date_key + 719_468; + let era = z.div_euclid(146_097); + let day_of_era = z - era * 146_097; + let year_of_era = (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 + - day_of_era / 146_096) + .div_euclid(365); + let mut year = year_of_era + era * 400; + let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100); + let month_prime = (5 * day_of_year + 2).div_euclid(153); + let day = day_of_year - (153 * month_prime + 2).div_euclid(5) + 1; + let month = month_prime + if month_prime < 10 { 3 } else { -9 }; + year += if month <= 2 { 1 } else { 0 }; + (year as i32, month as u8, day as u8) +} + +/// ISO 星期:周一 = 1,周日 = 7。 +pub fn weekday_from_date_key(date_key: i64) -> u8 { + // 中文注释:1970-01-01 是周四;这里返回 ISO weekday,周一=1,周日=7。 + (date_key + 3).rem_euclid(7) as u8 + 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn civil_round_trip_holds_across_month_and_year_boundaries() { + for (year, month, day) in [ + (2024, 2, 29), + (2026, 1, 1), + (2026, 12, 31), + (1970, 1, 1), + (2100, 12, 31), + ] { + let date_key = days_from_civil(year, month, day); + assert_eq!(civil_from_days(date_key), (year, month, day)); + } + } + + #[test] + fn days_in_month_applies_leap_year_rules() { + assert_eq!(days_in_month(2024, 2), 29); + assert_eq!(days_in_month(2023, 2), 28); + assert_eq!(days_in_month(2100, 2), 28); + assert_eq!(days_in_month(2000, 2), 29); + assert_eq!(days_in_month(2026, 4), 30); + assert_eq!(days_in_month(2026, 12), 31); + } + + #[test] + fn weekday_from_date_key_matches_known_dates() { + // 中文注释:1970-01-01 是周四;2026-01-01 也是周四。 + assert_eq!(weekday_from_date_key(days_from_civil(1970, 1, 1)), 4); + assert_eq!(weekday_from_date_key(days_from_civil(2026, 1, 1)), 4); + } +} diff --git a/server-rs/crates/module-runtime/src/lib.rs b/server-rs/crates/module-runtime/src/lib.rs index c6698f7a6..ac2ad0863 100644 --- a/server-rs/crates/module-runtime/src/lib.rs +++ b/server-rs/crates/module-runtime/src/lib.rs @@ -1,5 +1,7 @@ pub mod agc_analytics; mod agc_models; +pub mod civil_calendar; +pub use civil_calendar::*; mod llm_billing; pub use llm_billing::*; mod application;