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
8use std::time::{SystemTime, UNIX_EPOCH};
9
10/// 一秒的纳秒数。
11pub const NANOS_PER_SEC: i64 = 1_000_000_000;
12/// 一天的纳秒数(86400 s,不涉闰秒)。
13pub const NANOS_PER_DAY: i64 = 86_400 * NANOS_PER_SEC;
14
15/// 时间戳:Unix 纪元(The Epoch, 1970-01-01T00:00:00Z)起的纳秒。
16pub type DtNs = i64;
17
18/// 交易日序数:Unix 纪元起的天数(向下取整,含负数)。
19pub type Day = i32;
20
21/// civil 纪元日天数 → 纪元日序数(Hinnant days_from_civil)。
22///
23/// 公历,"March through February" 换年修正。
24pub fn days_from_civil(y: i32, m: u32, d: u32) -> Day {
25    let y = if m <= 2 { y - 1 } else { y } as i64;
26    let era = if y >= 0 { y } else { y - 399 } / 400;
27    let yoe = y - era * 400; // [0, 399]
28    let mp: i64 = ((m as i64) + 9) % 12; // [0, 11]: Mar = 0
29    let doy = (153 * mp + 2) / 5 + (d as i64) - 1; // [0, 365]
30    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
31    (era * 146097 + doe - 719468) as Day
32}
33
34/// 纪元日序数 → civil (年, 月, 日)(Hinnant civil_from_days)。
35pub fn civil_from_days(z: Day) -> (i32, u32, u32) {
36    let z = z as i64 + 719468;
37    let era = if z >= 0 { z } else { z - 146096 } / 146097;
38    let doe = z - era * 146097; // [0, 146096]
39    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
40    let y = yoe + era * 400;
41    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
42    let mp = (5 * doy + 2) / 153; // [0, 11]
43    let d = (doy - (153 * mp + 2) / 5 + 1) as u32; // [1, 31]
44    let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32; // [1, 12]
45    ((y + if m <= 2 { 1 } else { 0 }) as i32, m, d)
46}
47
48/// civil 六元组 + 毫微段(一日内)。
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct Civil {
51    /// 年(可为负)
52    pub year: i32,
53    /// 月 [1, 12]
54    pub month: u32,
55    /// 日 [1, 31]
56    pub day: u32,
57    /// 时 [0, 23]
58    pub hour: u32,
59    /// 分 [0, 59]
60    pub minute: u32,
61    /// 秒 [0, 59]
62    pub second: u32,
63    /// 纳秒段 [0, 999999999]
64    pub nanos: u32,
65}
66
67/// civil → 纪元纳秒(以 civil 视作 UTC,无时区移位)。
68pub fn civil_to_nanos(c: Civil) -> DtNs {
69    let days = days_from_civil(c.year, c.month, c.day) as i64;
70    days * NANOS_PER_DAY
71        + (c.hour as i64) * 3600 * NANOS_PER_SEC
72        + (c.minute as i64) * 60 * NANOS_PER_SEC
73        + (c.second as i64) * NANOS_PER_SEC
74        + c.nanos as i64
75}
76
77/// 纪元纳秒 → civil(UTC 视角;除法对负纳秒采用 floor 保证越日正确)。
78pub fn civil_from_nanos(t: DtNs) -> Civil {
79    let days = t.div_euclid(NANOS_PER_DAY);
80    let secs_of_day = t.rem_euclid(NANOS_PER_DAY);
81    let (year, month, day) = civil_from_days(days as Day);
82    let total_sec = secs_of_day / NANOS_PER_SEC;
83    let nanos = (secs_of_day % NANOS_PER_SEC) as u32;
84    Civil {
85        year,
86        month,
87        day,
88        hour: (total_sec / 3600) as u32,
89        minute: (total_sec % 3600 / 60) as u32,
90        second: (total_sec % 60) as u32,
91        nanos,
92    }
93}
94
95/// 当前时刻的纪元纳秒(SystemTime 视角)。
96pub fn now_nanos() -> DtNs {
97    from_system_time(SystemTime::now())
98}
99
100/// 纪元纳秒所在交易日序数(向下取整,负纳秒同样正确)。
101pub fn trading_day(t: DtNs) -> Day {
102    t.div_euclid(NANOS_PER_DAY) as Day
103}
104
105/// `SystemTime` → 纪元纳秒(epoch 之前取负;独立函数,避免为基本类型
106/// `i64` 实现外部 trait——orphan 规则)。
107pub fn from_system_time(t: SystemTime) -> DtNs {
108    match t.duration_since(UNIX_EPOCH) {
109        Ok(d) => d.as_nanos() as DtNs,
110        Err(e) => -(e.duration().as_nanos() as DtNs),
111    }
112}
113
114/// `yyyy-mm-dd hh:mm:ss(.fffffffff 有 nanos 才显示)` 文本(UTC 视角)。
115///
116/// 仅供日志/调试;持久层自行格式化。
117pub fn format_civil(t: DtNs) -> String {
118    let c = civil_from_nanos(t);
119    let base = format!(
120        "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
121        c.year, c.month, c.day, c.hour, c.minute, c.second
122    );
123    if c.nanos == 0 {
124        base
125    } else {
126        format!("{base}.{:09}", c.nanos)
127    }
128}