Skip to main content

polydat_nodes/
datetime.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Datetime and epoch function nodes.
5
6/// Scale a u64 to epoch milliseconds by multiplying by a factor.
7///
8/// Signature: `(input: u64) -> (u64)`
9/// Param: `factor: u64` — milliseconds per input unit.
10///
11/// Example: `EpochScale(1000)` treats input as seconds → millis.
12#[polydat::polydat_node(category = Datetime)]
13fn epoch_scale(
14    input: u64,
15    #[poly_default(1u64)] factor: polydat::derive_support::Const<u64>,
16) -> u64 {
17    input.wrapping_mul(*factor)
18}
19
20impl EpochScale {
21    /// Scale by 1: milliseconds stay milliseconds.
22    pub fn millis() -> Self {
23        Self::new(1)
24    }
25    /// Scale by 1,000: seconds to milliseconds.
26    pub fn seconds() -> Self {
27        Self::new(1_000)
28    }
29    /// Scale by 60,000: minutes to milliseconds.
30    pub fn minutes() -> Self {
31        Self::new(60_000)
32    }
33    /// Scale by 3,600,000: hours to milliseconds.
34    pub fn hours() -> Self {
35        Self::new(3_600_000)
36    }
37}
38
39/// Add a base epoch offset to a u64 value.
40#[polydat::polydat_node(category = Datetime)]
41fn epoch_offset(
42    input: u64,
43    #[poly_default(0u64)] base_epoch_ms: polydat::derive_support::Const<u64>,
44) -> u64 {
45    input.wrapping_add(*base_epoch_ms)
46}
47
48impl EpochOffset {
49    /// 2024-01-01T00:00:00Z in epoch millis.
50    pub fn from_2024() -> Self {
51        Self::new(1_704_067_200_000)
52    }
53    /// 2025-01-01T00:00:00Z in epoch millis.
54    pub fn from_2025() -> Self {
55        Self::new(1_735_689_600_000)
56    }
57}
58
59/// Format an epoch-millis u64 as an ISO-8601-like timestamp string.
60///
61/// Signature: `(input: u64) -> (String)`
62///
63/// Produces: `"YYYY-MM-DDThh:mm:ss.mmmZ"`
64/// Uses a simple arithmetic calendar (no timezone, no leap second handling).
65#[polydat::polydat_node(category = Datetime)]
66fn to_timestamp(input: u64) -> String {
67    epoch_ms_to_iso(input)
68}
69
70/// Decompose epoch millis into date/time components.
71#[polydat::polydat_node(
72    category = Datetime,
73    output_names(year, month, day, hour, minute, second, millis),
74)]
75fn date_components(input: u64) -> (u64, u64, u64, u64, u64, u64, u64) {
76    decompose_epoch_ms(input)
77}
78
79// --- Calendar arithmetic (simplified, no leap seconds) ---
80
81const MILLIS_PER_SEC: u64 = 1_000;
82#[allow(dead_code)]
83const MILLIS_PER_MIN: u64 = 60_000;
84#[allow(dead_code)]
85const MILLIS_PER_HOUR: u64 = 3_600_000;
86#[allow(dead_code)]
87const MILLIS_PER_DAY: u64 = 86_400_000;
88
89fn is_leap_year(y: u64) -> bool {
90    (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
91}
92
93fn days_in_month(y: u64, m: u64) -> u64 {
94    match m {
95        1 => 31,
96        2 => {
97            if is_leap_year(y) {
98                29
99            } else {
100                28
101            }
102        }
103        3 => 31,
104        4 => 30,
105        5 => 31,
106        6 => 30,
107        7 => 31,
108        8 => 31,
109        9 => 30,
110        10 => 31,
111        11 => 30,
112        12 => 31,
113        _ => 30,
114    }
115}
116
117fn decompose_epoch_ms(epoch_ms: u64) -> (u64, u64, u64, u64, u64, u64, u64) {
118    let mut remaining = epoch_ms;
119    let ms = remaining % MILLIS_PER_SEC;
120    remaining /= MILLIS_PER_SEC;
121    let sec = remaining % 60;
122    remaining /= 60;
123    let min = remaining % 60;
124    remaining /= 60;
125    let hour = remaining % 24;
126    let mut days = remaining / 24;
127
128    // Convert days since epoch (1970-01-01) to y/m/d
129    let mut year = 1970u64;
130    loop {
131        let days_in_year = if is_leap_year(year) { 366 } else { 365 };
132        if days < days_in_year {
133            break;
134        }
135        days -= days_in_year;
136        year += 1;
137    }
138    let mut month = 1u64;
139    loop {
140        let dim = days_in_month(year, month);
141        if days < dim {
142            break;
143        }
144        days -= dim;
145        month += 1;
146    }
147    let day = days + 1;
148
149    (year, month, day, hour, min, sec, ms)
150}
151
152fn epoch_ms_to_iso(epoch_ms: u64) -> String {
153    let (y, mo, d, h, mi, s, ms) = decompose_epoch_ms(epoch_ms);
154    format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}.{ms:03}Z")
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use polydat::ast::{PolydatNode, Value};
161
162    #[test]
163    fn epoch_scale_seconds() {
164        let node = EpochScale::seconds();
165        let mut out = [Value::None];
166        node.eval(&[Value::U64(5)], &mut out);
167        assert_eq!(out[0].as_u64(), 5000);
168    }
169
170    #[test]
171    fn epoch_offset_basic() {
172        let node = EpochOffset::new(1_000_000);
173        let mut out = [Value::None];
174        node.eval(&[Value::U64(500)], &mut out);
175        assert_eq!(out[0].as_u64(), 1_000_500);
176    }
177
178    #[test]
179    fn to_timestamp_epoch_zero() {
180        let node = ToTimestamp::new();
181        let mut out = [Value::None];
182        node.eval(&[Value::U64(0)], &mut out);
183        assert_eq!(out[0].as_str(), "1970-01-01T00:00:00.000Z");
184    }
185
186    #[test]
187    fn to_timestamp_known_date() {
188        let node = ToTimestamp::new();
189        let mut out = [Value::None];
190        // 2024-01-01T00:00:00.000Z = 1704067200000
191        node.eval(&[Value::U64(1_704_067_200_000)], &mut out);
192        assert_eq!(out[0].as_str(), "2024-01-01T00:00:00.000Z");
193    }
194
195    #[test]
196    fn date_components_epoch_zero() {
197        let node = DateComponents::new();
198        let mut out = vec![Value::None; 7];
199        node.eval(&[Value::U64(0)], &mut out);
200        assert_eq!(out[0].as_u64(), 1970);
201        assert_eq!(out[1].as_u64(), 1);
202        assert_eq!(out[2].as_u64(), 1);
203        assert_eq!(out[3].as_u64(), 0);
204        assert_eq!(out[4].as_u64(), 0);
205        assert_eq!(out[5].as_u64(), 0);
206        assert_eq!(out[6].as_u64(), 0);
207    }
208
209    #[test]
210    fn date_components_known() {
211        let node = DateComponents::new();
212        let mut out = vec![Value::None; 7];
213        // 2024-03-15T14:30:45.123Z
214        // Manually: days from epoch to 2024-03-15 = 19797
215        // 19797 * 86400000 + 14*3600000 + 30*60000 + 45*1000 + 123
216        let epoch = 19797u64 * MILLIS_PER_DAY
217            + 14 * MILLIS_PER_HOUR
218            + 30 * MILLIS_PER_MIN
219            + 45 * MILLIS_PER_SEC
220            + 123;
221        node.eval(&[Value::U64(epoch)], &mut out);
222        assert_eq!(out[0].as_u64(), 2024);
223        assert_eq!(out[1].as_u64(), 3);
224        assert_eq!(out[2].as_u64(), 15);
225        assert_eq!(out[3].as_u64(), 14);
226        assert_eq!(out[4].as_u64(), 30);
227        assert_eq!(out[5].as_u64(), 45);
228        assert_eq!(out[6].as_u64(), 123);
229    }
230
231    #[test]
232    fn epoch_scale_compiled() {
233        let node = EpochScale::seconds();
234        let op = node.compiled_u64().unwrap();
235        let mut out = [0u64];
236        op(&[5], &mut out);
237        assert_eq!(out[0], 5000);
238    }
239}