Skip to main content

ek_data/
time.rs

1//! 零依赖时间面:纪元纳秒 ↔ civil(年月日时分秒)双向换算。
2//!
3//! Python 版 `TickData.datetime` 等字段用本地 naive `datetime`;
4//! Rust 版统一为 **UTC 纪元纳秒**([`DtNs`]),本地化/展示层由使用方处理
5//! (DESIGN.md 决定 8-A)。换算采用 Howard Hinnant 的 civil_from_days /
6//! days_from_civil 算法(公历 1582-10-15 起全域正确),均以单测钉死。
7//!
8//! ## 可表达范围(重要)
9//!
10//! `DtNs = i64` 纳秒 → 覆盖 **1677-09-21T00:12:43.145224192Z ..
11//! 2262-04-11T23:47:16.854775807Z**([`DTNS_MIN`]/[`DTNS_MAX`])。
12//! 超出范围时[`try_civil_to_nanos`] 返回 `None`,[`civil_to_nanos`] panic
13//! (对齐 Python `datetime` 超范围抛 `ValueError` 的失败面);
14//! 远日期(如股票 IPO 日 1990 前 / 交易所属历史日期)请用 [`Day`] 序数表达。
15
16use std::time::{SystemTime, UNIX_EPOCH};
17
18/// 一秒的纳秒数。
19pub const NANOS_PER_SEC: i64 = 1_000_000_000;
20/// 一天的纳秒数(86400 s,不涉闰秒)。
21pub const NANOS_PER_DAY: i64 = 86_400 * NANOS_PER_SEC;
22
23/// `DtNs` 可表达的 civil 下界近似:1677-09-21T00:12:43.145224192Z
24/// (即 `i64::MIN` 纳秒;换算取整到纳秒需 +1 才落在合法 civil 上)。
25pub const DTNS_MIN: DtNs = i64::MIN;
26/// `DtNs` 可表达的 civil 上界:2262-04-11T23:47:16.854775807Z。
27pub const DTNS_MAX: DtNs = i64::MAX;
28
29/// 时间戳:Unix 纪元(The Epoch, 1970-01-01T00:00:00Z)起的纳秒。
30pub type DtNs = i64;
31
32/// 交易日序数:Unix 纪元起的天数(向下取整,含负数)。
33pub type Day = i32;
34
35/// civil 纪元日天数 → 纪元日序数(Hinnant days_from_civil)。
36///
37/// 公历,"March through February" 换年修正。
38pub fn days_from_civil(y: i32, m: u32, d: u32) -> Day {
39    let y = if m <= 2 { y - 1 } else { y } as i64;
40    let era = if y >= 0 { y } else { y - 399 } / 400;
41    let yoe = y - era * 400; // [0, 399]
42    let mp: i64 = ((m as i64) + 9) % 12; // [0, 11]: Mar = 0
43    let doy = (153 * mp + 2) / 5 + (d as i64) - 1; // [0, 365]
44    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
45    (era * 146097 + doe - 719468) as Day
46}
47
48/// 纪元日序数 → civil (年, 月, 日)(Hinnant civil_from_days)。
49pub fn civil_from_days(z: Day) -> (i32, u32, u32) {
50    let z = z as i64 + 719468;
51    let era = if z >= 0 { z } else { z - 146096 } / 146097;
52    let doe = z - era * 146097; // [0, 146096]
53    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
54    let y = yoe + era * 400;
55    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
56    let mp = (5 * doy + 2) / 153; // [0, 11]
57    let d = (doy - (153 * mp + 2) / 5 + 1) as u32; // [1, 31]
58    let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32; // [1, 12]
59    ((y + if m <= 2 { 1 } else { 0 }) as i32, m, d)
60}
61
62/// civil 六元组 + 毫微段(一日内)。
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct Civil {
65    /// 年(可为负)
66    pub year: i32,
67    /// 月 [1, 12]
68    pub month: u32,
69    /// 日 [1, 31]
70    pub day: u32,
71    /// 时 [0, 23]
72    pub hour: u32,
73    /// 分 [0, 59]
74    pub minute: u32,
75    /// 秒 [0, 59]
76    pub second: u32,
77    /// 纳秒段 [0, 999999999]
78    pub nanos: u32,
79}
80
81/// civil → 纪元纳秒(以 civil 视作 UTC,无时区移位)。
82///
83/// **输入越界即 panic**(月>12、2 月 30 日、纳秒段超 1e9、超出 [`DTNS_MAX`]
84/// 范围等),对齐 Python `datetime` 抛 `ValueError` 的失败面;
85/// 需要可失败路径请用 [`try_civil_to_nanos`]。
86pub fn civil_to_nanos(c: Civil) -> DtNs {
87    match try_civil_to_nanos(c) {
88        Some(t) => t,
89        None => panic!("civil 越界或超出 DtNs 可表达范围: {c:?}"),
90    }
91}
92
93/// civil → 纪元纳秒的可失败版本;严格校验日历合法性(含闰年 2 月)与 i64 范围。
94pub fn try_civil_to_nanos(c: Civil) -> Option<DtNs> {
95    if !is_valid_civil(&c) {
96        return None;
97    }
98    let days = days_from_civil(c.year, c.month, c.day) as i64;
99    let secs_of_day = (c.hour as i64) * 3600 + (c.minute as i64) * 60 + c.second as i64;
100    days.checked_mul(NANOS_PER_DAY)?
101        .checked_add(secs_of_day.checked_mul(NANOS_PER_SEC)?)?
102        .checked_add(c.nanos as i64)
103}
104
105/// 闰年判定(公历规则)。
106pub fn is_leap_year(year: i32) -> bool {
107    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
108}
109
110/// 某年某月的天数(月越界返回 0)。
111pub fn days_in_month(year: i32, month: u32) -> u32 {
112    match month {
113        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
114        4 | 6 | 9 | 11 => 30,
115        2 => {
116            if is_leap_year(year) {
117                29
118            } else {
119                28
120            }
121        }
122        _ => 0,
123    }
124}
125
126/// civil 合法性(月 1-12、日不超当月天数、时分秒纳秒在域内)。
127pub fn is_valid_civil(c: &Civil) -> bool {
128    (1..=12).contains(&c.month)
129        && c.day >= 1
130        && c.day <= days_in_month(c.year, c.month)
131        && c.hour < 24
132        && c.minute < 60
133        && c.second < 60
134        && c.nanos < 1_000_000_000
135}
136
137/// 解析 `"YYYY-MM-DD[ HH:MM[:SS[.fffffffff]]]"`(亦接受 `T` 分隔与纯日期)。
138///
139/// 例:`"2024-12-25 09:30:01"`、`"2024-12-25T09:30"`、`"2024-12-25"`。
140/// 小数秒不足 9 位右补零(`".5"` = 500ms)。非法日期/时间一律 `None`。
141pub fn parse_civil(s: &str) -> Option<DtNs> {
142    let s = s.trim();
143    let (date_str, time_str) = match s.split_once(['T', ' ']) {
144        Some((d, t)) => (d, Some(t)),
145        None => (s, None),
146    };
147    let mut date_parts = date_str.split('-');
148    let year: i32 = date_parts.next()?.parse().ok()?;
149    let month: u32 = date_parts.next()?.parse().ok()?;
150    let day: u32 = date_parts.next()?.parse().ok()?;
151    if date_parts.next().is_some() {
152        return None;
153    }
154
155    let (hour, minute, second, nanos) = match time_str {
156        None => (0, 0, 0, 0),
157        Some(t) => {
158            let (hms, frac) = match t.split_once('.') {
159                Some((a, b)) => (a, Some(b)),
160                None => (t, None),
161            };
162            let mut it = hms.split(':');
163            let hour: u32 = it.next()?.parse().ok()?;
164            let minute: u32 = it.next().unwrap_or("0").parse().ok()?;
165            let second: u32 = it.next().unwrap_or("0").parse().ok()?;
166            if it.next().is_some() {
167                return None;
168            }
169            let nanos = match frac {
170                None => 0,
171                Some(f) => {
172                    if f.is_empty() || f.len() > 9 || !f.bytes().all(|b| b.is_ascii_digit()) {
173                        return None;
174                    }
175                    let v: u32 = f.parse().ok()?;
176                    v * 10u32.pow(9 - f.len() as u32)
177                }
178            };
179            (hour, minute, second, nanos)
180        }
181    };
182
183    try_civil_to_nanos(Civil {
184        year,
185        month,
186        day,
187        hour,
188        minute,
189        second,
190        nanos,
191    })
192}
193
194/// 纪元纳秒 → civil(UTC 视角;除法对负纳秒采用 floor 保证越日正确)。
195pub fn civil_from_nanos(t: DtNs) -> Civil {
196    let days = t.div_euclid(NANOS_PER_DAY);
197    let secs_of_day = t.rem_euclid(NANOS_PER_DAY);
198    let (year, month, day) = civil_from_days(days as Day);
199    let total_sec = secs_of_day / NANOS_PER_SEC;
200    let nanos = (secs_of_day % NANOS_PER_SEC) as u32;
201    Civil {
202        year,
203        month,
204        day,
205        hour: (total_sec / 3600) as u32,
206        minute: (total_sec % 3600 / 60) as u32,
207        second: (total_sec % 60) as u32,
208        nanos,
209    }
210}
211
212/// 当前时刻的纪元纳秒(SystemTime 视角)。
213pub fn now_nanos() -> DtNs {
214    from_system_time(SystemTime::now())
215}
216
217/// 纪元纳秒所在交易日序数(向下取整,负纳秒同样正确)。
218pub fn trading_day(t: DtNs) -> Day {
219    t.div_euclid(NANOS_PER_DAY) as Day
220}
221
222/// `SystemTime` → 纪元纳秒(epoch 之前取负;独立函数,避免为基本类型
223/// `i64` 实现外部 trait——orphan 规则)。
224pub fn from_system_time(t: SystemTime) -> DtNs {
225    match t.duration_since(UNIX_EPOCH) {
226        Ok(d) => d.as_nanos() as DtNs,
227        Err(e) => -(e.duration().as_nanos() as DtNs),
228    }
229}
230
231/// `yyyy-mm-dd hh:mm:ss(.fffffffff 有 nanos 才显示)` 文本(UTC 视角)。
232///
233/// 仅供日志/调试;持久层自行格式化。
234pub fn format_civil(t: DtNs) -> String {
235    let c = civil_from_nanos(t);
236    let base = format!(
237        "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
238        c.year, c.month, c.day, c.hour, c.minute, c.second
239    );
240    if c.nanos == 0 {
241        base
242    } else {
243        format!("{base}.{:09}", c.nanos)
244    }
245}