重构:抽出公历日期基元为独立模块 civil_calendar

- 新增 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 <junie@jetbrains.com>
This commit is contained in:
2026-10-02 14:08:04 +08:00
parent b827c93d39
commit 741b179ca5
3 changed files with 99 additions and 49 deletions
@@ -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,
@@ -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);
}
}
@@ -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;