Skip to main content

tatara_lisp_script/stdlib/
time.rs

1//! Time + duration helpers.
2//!
3//!   (now)                    → integer — unix seconds
4//!   (now-ms)                 → integer — unix milliseconds
5//!   (now-ns)                 → integer — unix nanoseconds (monotonic)
6//!   (now-rfc3339)            → string — current time formatted RFC 3339 UTC
7//!   (sleep SECONDS)          → nil — blocks the script
8//!   (sleep-ms MILLIS)        → nil
9//!   (elapsed-since START-NS) → integer — ns since START-NS (from (now-ns))
10
11use std::thread;
12use std::time::{Duration, SystemTime, UNIX_EPOCH};
13
14use std::sync::Arc;
15use tatara_lisp_eval::{Arity, Interpreter, Value};
16
17use crate::script_ctx::ScriptCtx;
18use crate::stdlib::env::int_arg;
19
20pub fn install(interp: &mut Interpreter<ScriptCtx>) {
21    interp.register_fn(
22        "now",
23        Arity::Exact(0),
24        |_args: &[Value], _ctx: &mut ScriptCtx, _sp| {
25            Ok(Value::Int(
26                SystemTime::now()
27                    .duration_since(UNIX_EPOCH)
28                    .map(|d| d.as_secs() as i64)
29                    .unwrap_or(0),
30            ))
31        },
32    );
33
34    interp.register_fn(
35        "now-ms",
36        Arity::Exact(0),
37        |_args: &[Value], _ctx: &mut ScriptCtx, _sp| {
38            Ok(Value::Int(
39                SystemTime::now()
40                    .duration_since(UNIX_EPOCH)
41                    .map(|d| d.as_millis() as i64)
42                    .unwrap_or(0),
43            ))
44        },
45    );
46
47    interp.register_fn(
48        "now-ns",
49        Arity::Exact(0),
50        |_args: &[Value], _ctx: &mut ScriptCtx, _sp| {
51            Ok(Value::Int(
52                SystemTime::now()
53                    .duration_since(UNIX_EPOCH)
54                    .map(|d| d.as_nanos() as i64)
55                    .unwrap_or(0),
56            ))
57        },
58    );
59
60    interp.register_fn(
61        "now-rfc3339",
62        Arity::Exact(0),
63        |_args: &[Value], _ctx: &mut ScriptCtx, _sp| {
64            let now = SystemTime::now()
65                .duration_since(UNIX_EPOCH)
66                .map(|d| d.as_secs() as i64)
67                .unwrap_or(0);
68            Ok(Value::Str(Arc::from(format_rfc3339_utc(now))))
69        },
70    );
71
72    interp.register_fn(
73        "sleep",
74        Arity::Exact(1),
75        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
76            let secs = int_arg(&args[0], "sleep", sp)?;
77            if secs > 0 {
78                thread::sleep(Duration::from_secs(secs as u64));
79            }
80            Ok(Value::Nil)
81        },
82    );
83
84    interp.register_fn(
85        "sleep-ms",
86        Arity::Exact(1),
87        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
88            let ms = int_arg(&args[0], "sleep-ms", sp)?;
89            if ms > 0 {
90                thread::sleep(Duration::from_millis(ms as u64));
91            }
92            Ok(Value::Nil)
93        },
94    );
95
96    interp.register_fn(
97        "elapsed-since",
98        Arity::Exact(1),
99        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
100            let start_ns = int_arg(&args[0], "elapsed-since", sp)?;
101            let now_ns = SystemTime::now()
102                .duration_since(UNIX_EPOCH)
103                .map(|d| d.as_nanos() as i64)
104                .unwrap_or(0);
105            Ok(Value::Int(now_ns - start_ns))
106        },
107    );
108}
109
110/// Format a unix-seconds timestamp as RFC-3339 UTC (no external crate).
111/// Good enough for log lines + filenames; NOT timezone-aware.
112fn format_rfc3339_utc(unix_secs: i64) -> String {
113    // Civil calendar math — days since 1970-01-01.
114    let (mut y, mut m, mut d, h, mi, s) = seconds_to_datetime(unix_secs);
115    if d == 0 {
116        d = 1;
117    }
118    if m == 0 {
119        m = 1;
120    }
121    if y == 0 {
122        y = 1970;
123    }
124    format!("{y:04}-{m:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z")
125}
126
127fn seconds_to_datetime(unix_secs: i64) -> (i64, u32, u32, u32, u32, u32) {
128    let days = unix_secs / 86_400;
129    let rem = unix_secs.rem_euclid(86_400);
130    let h = (rem / 3600) as u32;
131    let mi = ((rem % 3600) / 60) as u32;
132    let s = (rem % 60) as u32;
133    let (y, m, d) = days_to_ymd(days);
134    (y, m, d, h, mi, s)
135}
136
137/// Days since 1970-01-01 → (year, month, day).
138fn days_to_ymd(days: i64) -> (i64, u32, u32) {
139    // Howard Hinnant "date algorithm" civil-from-days.
140    let z = days + 719_468;
141    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
142    let doe = (z - era * 146_097) as u32; // 0..=146_096
143    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
144    let y = yoe as i64 + era * 400;
145    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
146    let mp = (5 * doy + 2) / 153;
147    let d = doy - (153 * mp + 2) / 5 + 1;
148    let m = if mp < 10 { mp + 3 } else { mp - 9 };
149    let y = if m <= 2 { y + 1 } else { y };
150    (y, m, d)
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn rfc3339_epoch() {
159        assert_eq!(format_rfc3339_utc(0), "1970-01-01T00:00:00Z");
160    }
161
162    #[test]
163    fn rfc3339_y2k() {
164        // 946684800 = 2000-01-01T00:00:00Z
165        assert_eq!(format_rfc3339_utc(946_684_800), "2000-01-01T00:00:00Z");
166    }
167}