重构:抽出公历日期基元为独立模块 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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user