Skip to main content

mingli_contract/
validate.rs

1//! `Query` 与出生时刻的取值域。
2//!
3//! 校验属于契约而不属于用例:它说的是「什么样的入参算数」,与谁来用这份入参无关。
4//! 从前它住在用例层,于是任何一扇只想排一张盘的门要么把用例层整个链进来,
5//! 要么不校验——后者会让同一个 2 月 31 日在服务端被拒、在浏览器里被历法换算悄悄
6//! 挪成 3 月 3 日,两扇门给出的不是同一个答案。搬到这里之后,拿得到 `Query` 的人
7//! 就一定拿得到校验。
8//!
9//! 用例层按原路径 re-export 这几个函数,对外签名一字未动。
10
11/// 一个时刻的取值域。两条交付路共用——HTTP 走 `Birth::validate`(承接层),wasm 走
12/// [`validate_query`],两边落到的是同一段判断,不各写一份。
13///
14/// # Errors
15///
16/// 年、月、日、时、分、时区中任一越界时返回面向调用方的中文说明。
17pub fn validate_instant(year: i32, month: u32, day: u32, hour: u32, minute: u32, tz: f64) -> Result<(), String> {
18    if !(1900..=2100).contains(&year) {
19        return Err("year 仅支持 1900–2100".into());
20    }
21    if !(1..=12).contains(&month) {
22        return Err("month 须 1–12".into());
23    }
24    // 日要按当月实际长度收,不能一律放到 31:2 月 31 日会被历法换算悄悄挪成 3 月 3 日,
25    // 于是打错一个数字的人拿到的是**另一天**的盘,而界面上没有任何迹象
26    let last = days_in_month(year, month);
27    if !(1..=last).contains(&day) {
28        return Err(format!("{year} 年 {month} 月只有 {last} 天"));
29    }
30    if hour > 23 || minute > 59 {
31        return Err("hour/minute 越界".into());
32    }
33    // 现实中的 UTC 偏移落在 −12 到 +14 之间(+14 是 Kiritimati)
34    if !(-12.0..=14.0).contains(&tz) {
35        return Err("tz 须在 −12 到 +14 之间".into());
36    }
37    Ok(())
38}
39
40/// 排盘入参的取值域。wasm 那扇门吃的是 [`crate::Query`] 而非 `Birth`,
41/// 两者字段不同、该收的东西一样,故各有一个入口、共用同一段判断。
42///
43/// # Errors
44///
45/// 时刻或坐标越界时返回面向调用方的中文说明。
46pub fn validate_query(q: &crate::Query) -> Result<(), String> {
47    validate_instant(q.year, q.month, q.day, q.hour, q.minute, q.tz)?;
48    validate_coords(q.latitude, q.longitude)
49}
50
51/// 公历某年某月有几天。`month` 须已在 1–12 内。
52#[must_use]
53pub fn days_in_month(year: i32, month: u32) -> u32 {
54    match month {
55        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
56        4 | 6 | 9 | 11 => 30,
57        2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
58        2 => 28,
59        _ => 0,
60    }
61}
62
63/// 坐标的取值域。纬度不在 `Birth` 上(只有占星那一路要它,走 `Query`),故单独一条。
64///
65/// # Errors
66///
67/// 纬度不在 −90–90、或经度不在 −180–180 时返回说明。
68pub fn validate_coords(latitude: Option<f64>, longitude: Option<f64>) -> Result<(), String> {
69    if let Some(lat) = latitude
70        && !(-90.0..=90.0).contains(&lat)
71    {
72        return Err("latitude 须在 −90 到 90 之间".into());
73    }
74    if let Some(lon) = longitude
75        && !(-180.0..=180.0).contains(&lon)
76    {
77        return Err("longitude 须在 −180 到 180 之间".into());
78    }
79    Ok(())
80}