fix(会员账期): 极端锚点推进月份饱和而非 panic
- advance_beijing_months_clamped 去掉 beijing_local_micros 后的 expect,改为安全年份区间 + unwrap_or 按方向饱和(评审第 16 条) - 目标年份超出 i64 微秒可表示范围(约 ±29 万年)时返回 i64::MAX / i64::MIN,避免历法乘法与 checked_* 溢出 panic - 新增 advance_saturates_instead_of_panicking_on_extreme_anchors 覆盖 i64::MAX / i64::MIN / u32::MAX
This commit is contained in:
@@ -43,14 +43,37 @@ pub fn beijing_local_micros(year: i32, month: u8, day: u8, time_of_day_micros: i
|
||||
/// 把北京时间锚点推进 `months` 个自然月,日期夹取到目标月最后一天,时刻保持不变。
|
||||
///
|
||||
/// `months = 0` 返回锚点本身,因此调用方可以安全地用「第 n 期 = 推进 n 个自然月」表达账期。
|
||||
///
|
||||
/// 目标年份超出 i64 微秒可表示范围(约 ±29 万年)时按方向饱和到 `i64::MAX` / `i64::MIN`:
|
||||
/// 该输入在物理上不可表示,饱和既保留「更晚 / 更早」的方向,又避免历法乘法与
|
||||
/// [`beijing_local_micros`] 的 `checked_*` 溢出让这个纯函数 panic。
|
||||
pub fn advance_beijing_months_clamped(anchor_micros: i64, months: u32) -> i64 {
|
||||
// 中文注释:`beijing_local_date_parts` 对极端 micros 会饱和后再反解,年份可能已被 i32 截断;
|
||||
// 这里先按 i64 算目标年份,只在安全区间内才落回 i32 历法换算。
|
||||
const MIN_SAFE_YEAR: i64 = -300_000;
|
||||
const MAX_SAFE_YEAR: i64 = 300_000;
|
||||
|
||||
let (year, month, day, time_of_day_micros) = beijing_local_date_parts(anchor_micros);
|
||||
let total_months = i64::from(year) * 12 + i64::from(month) - 1 + i64::from(months);
|
||||
let target_year = total_months.div_euclid(12) as i32;
|
||||
let target_year = total_months.div_euclid(12);
|
||||
let target_month = (total_months.rem_euclid(12) + 1) as u8;
|
||||
if target_year < MIN_SAFE_YEAR {
|
||||
return i64::MIN;
|
||||
}
|
||||
if target_year > MAX_SAFE_YEAR {
|
||||
return i64::MAX;
|
||||
}
|
||||
let target_year = target_year as i32;
|
||||
let target_day = day.min(days_in_month(target_year, target_month));
|
||||
beijing_local_micros(target_year, target_month, target_day, time_of_day_micros)
|
||||
.expect("夹取后的北京本地年月日与当日时刻必定合法")
|
||||
beijing_local_micros(target_year, target_month, target_day, time_of_day_micros).unwrap_or_else(
|
||||
|| {
|
||||
if anchor_micros < 0 {
|
||||
i64::MIN
|
||||
} else {
|
||||
i64::MAX
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// 第 `index` 期(1-based)的账期窗口 `(cycle_started_at, cycle_resets_at)`。
|
||||
@@ -98,6 +121,14 @@ mod tests {
|
||||
.expect("测试用的北京本地时间应当合法")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advance_saturates_instead_of_panicking_on_extreme_anchors() {
|
||||
// 中文注释:极端锚点已无法精确表示目标月份,本用例只要求「不 panic 且方向不反转」。
|
||||
assert_eq!(advance_beijing_months_clamped(i64::MAX, 1), i64::MAX);
|
||||
assert!(advance_beijing_months_clamped(i64::MIN, 1) < 0);
|
||||
assert_eq!(advance_beijing_months_clamped(i64::MAX, u32::MAX), i64::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beijing_local_parts_round_trip() {
|
||||
let micros = beijing(2026, 10, 2, 9, 20);
|
||||
|
||||
Reference in New Issue
Block a user