Skip to main content

pepl_stdlib/modules/
time.rs

1//! `time` stdlib module — timestamp operations.
2//!
3//! All timestamps are milliseconds since Unix epoch as f64.
4//! Functions: now, format, diff, day_of_week, start_of_day.
5
6use crate::error::StdlibError;
7use crate::module::StdlibModule;
8use crate::value::Value;
9
10/// Milliseconds per day.
11const MS_PER_DAY: f64 = 86_400_000.0;
12/// Milliseconds per second.
13const MS_PER_SECOND: f64 = 1_000.0;
14
15/// The `time` stdlib module.
16pub struct TimeModule;
17
18impl TimeModule {
19    pub fn new() -> Self {
20        Self
21    }
22}
23
24impl Default for TimeModule {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl StdlibModule for TimeModule {
31    fn name(&self) -> &'static str {
32        "time"
33    }
34
35    fn has_function(&self, function: &str) -> bool {
36        matches!(
37            function,
38            "now" | "format" | "diff" | "day_of_week" | "start_of_day"
39        )
40    }
41
42    fn call(&self, function: &str, args: Vec<Value>) -> Result<Value, StdlibError> {
43        match function {
44            "now" => self.now(args),
45            "format" => self.format(args),
46            "diff" => self.diff(args),
47            "day_of_week" => self.day_of_week(args),
48            "start_of_day" => self.start_of_day(args),
49            _ => Err(StdlibError::unknown_function("time", function)),
50        }
51    }
52}
53
54impl TimeModule {
55    /// time.now() → number
56    /// Returns a deterministic stub value (0). In production, the host injects
57    /// the current timestamp via `env.host_call`.
58    fn now(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
59        if !args.is_empty() {
60            return Err(StdlibError::wrong_args("time.now", 0, args.len()));
61        }
62        // Deterministic stub — host provides real value at runtime
63        Ok(Value::Number(0.0))
64    }
65
66    /// time.format(timestamp, pattern) → string
67    /// Supports patterns: "YYYY-MM-DD", "HH:mm:ss", "HH:mm",
68    /// "YYYY-MM-DD HH:mm:ss", and others via placeholder replacement.
69    fn format(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
70        if args.len() != 2 {
71            return Err(StdlibError::wrong_args("time.format", 2, args.len()));
72        }
73        let ts = extract_number("time.format", &args[0], 1)?;
74        let pattern = extract_string("time.format", &args[1], 2)?;
75
76        let (year, month, day, hour, min, sec) = timestamp_to_parts(ts);
77
78        let result = pattern
79            .replace("YYYY", &format!("{:04}", year))
80            .replace("MM", &format!("{:02}", month))
81            .replace("DD", &format!("{:02}", day))
82            .replace("HH", &format!("{:02}", hour))
83            .replace("mm", &format!("{:02}", min))
84            .replace("ss", &format!("{:02}", sec));
85
86        Ok(Value::String(result))
87    }
88
89    /// time.diff(a, b) → number
90    /// Returns `a - b` in milliseconds.
91    fn diff(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
92        if args.len() != 2 {
93            return Err(StdlibError::wrong_args("time.diff", 2, args.len()));
94        }
95        let a = extract_number("time.diff", &args[0], 1)?;
96        let b = extract_number("time.diff", &args[1], 2)?;
97        Ok(Value::Number(a - b))
98    }
99
100    /// time.day_of_week(timestamp) → number
101    /// Returns 0 (Sunday) through 6 (Saturday).
102    /// Uses the fact that Unix epoch (Jan 1, 1970) was a Thursday (4).
103    fn day_of_week(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
104        if args.len() != 1 {
105            return Err(StdlibError::wrong_args("time.day_of_week", 1, args.len()));
106        }
107        let ts = extract_number("time.day_of_week", &args[0], 1)?;
108        // Days since epoch, Thursday = 4
109        let days = (ts / MS_PER_DAY).floor() as i64;
110        // (days + 4) % 7 — epoch was Thursday
111        let dow = ((days % 7 + 4) % 7 + 7) % 7;
112        Ok(Value::Number(dow as f64))
113    }
114
115    /// time.start_of_day(timestamp) → number
116    /// Truncates to midnight (UTC).
117    fn start_of_day(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
118        if args.len() != 1 {
119            return Err(StdlibError::wrong_args("time.start_of_day", 1, args.len()));
120        }
121        let ts = extract_number("time.start_of_day", &args[0], 1)?;
122        let day_start = (ts / MS_PER_DAY).floor() * MS_PER_DAY;
123        Ok(Value::Number(day_start))
124    }
125}
126
127// ── Date arithmetic helpers ─────────────────────────────────────────────────
128
129/// Convert a UTC millisecond timestamp to (year, month, day, hour, min, sec).
130/// Uses a civil calendar algorithm (no external dependencies).
131fn timestamp_to_parts(ts: f64) -> (i64, u32, u32, u32, u32, u32) {
132    let total_ms = ts as i64;
133    let total_sec = total_ms.div_euclid(MS_PER_SECOND as i64);
134    let sec = total_sec.rem_euclid(60) as u32;
135    let total_min = total_sec.div_euclid(60);
136    let min = total_min.rem_euclid(60) as u32;
137    let total_hour = total_min.div_euclid(60);
138    let hour = total_hour.rem_euclid(24) as u32;
139
140    let days = total_hour.div_euclid(24);
141    let (year, month, day) = days_to_civil(days);
142    (year, month, day, hour, min, sec)
143}
144
145/// Convert days since Unix epoch to (year, month, day).
146/// Algorithm from Howard Hinnant's `chrono`-compatible civil calendar.
147fn days_to_civil(days: i64) -> (i64, u32, u32) {
148    let z = days + 719468;
149    let era = if z >= 0 { z } else { z - 146096 } / 146097;
150    let doe = (z - era * 146097) as u32; // day of era [0, 146096]
151    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // year of era
152    let y = yoe as i64 + era * 400;
153    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // day of year [0, 365]
154    let mp = (5 * doy + 2) / 153; // month indicator [0, 11]
155    let d = doy - (153 * mp + 2) / 5 + 1;
156    let m = if mp < 10 { mp + 3 } else { mp - 9 };
157    let y = if m <= 2 { y + 1 } else { y };
158    (y, m, d)
159}
160
161// ── Helpers ──────────────────────────────────────────────────────────────────
162
163fn extract_number(func: &str, val: &Value, pos: usize) -> Result<f64, StdlibError> {
164    match val {
165        Value::Number(n) => Ok(*n),
166        _ => Err(StdlibError::type_mismatch(
167            func,
168            pos,
169            "number",
170            val.type_name(),
171        )),
172    }
173}
174
175fn extract_string<'a>(func: &str, val: &'a Value, pos: usize) -> Result<&'a str, StdlibError> {
176    match val {
177        Value::String(s) => Ok(s),
178        _ => Err(StdlibError::type_mismatch(
179            func,
180            pos,
181            "string",
182            val.type_name(),
183        )),
184    }
185}