1#[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 pub fn millis() -> Self {
23 Self::new(1)
24 }
25 pub fn seconds() -> Self {
27 Self::new(1_000)
28 }
29 pub fn minutes() -> Self {
31 Self::new(60_000)
32 }
33 pub fn hours() -> Self {
35 Self::new(3_600_000)
36 }
37}
38
39#[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 pub fn from_2024() -> Self {
51 Self::new(1_704_067_200_000)
52 }
53 pub fn from_2025() -> Self {
55 Self::new(1_735_689_600_000)
56 }
57}
58
59#[polydat::polydat_node(category = Datetime)]
66fn to_timestamp(input: u64) -> String {
67 epoch_ms_to_iso(input)
68}
69
70#[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
79const 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 decompose_epoch_ms(epoch_ms: u64) -> (u64, u64, u64, u64, u64, u64, u64) {
90 let mut remaining = epoch_ms;
91 let ms = remaining % MILLIS_PER_SEC;
92 remaining /= MILLIS_PER_SEC;
93 let sec = remaining % 60;
94 remaining /= 60;
95 let min = remaining % 60;
96 remaining /= 60;
97 let hour = remaining % 24;
98 let (year, month, day) = civil_from_days(remaining / 24);
99 (year, month, day, hour, min, sec, ms)
100}
101
102fn civil_from_days(days: u64) -> (u64, u64, u64) {
115 let z = days + 719_468; let era = z / 146_097;
117 let doe = z - era * 146_097; let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let day = doy - (153 * mp + 2) / 5 + 1;
122 let month = if mp < 10 { mp + 3 } else { mp - 9 };
123 let year = yoe + era * 400 + u64::from(month <= 2);
124 (year, month, day)
125}
126
127fn epoch_ms_to_iso(epoch_ms: u64) -> String {
128 let (y, mo, d, h, mi, s, ms) = decompose_epoch_ms(epoch_ms);
129 format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}.{ms:03}Z")
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use polydat::ast::{PolydatNode, Value};
136
137 #[test]
144 fn the_closed_form_calendar_agrees_with_the_walk() {
145 let mut days = 0u64;
146 let (mut y, mut m, mut d) = (1970u64, 1u64, 1u64);
147 let leap =
148 |y: u64| (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400);
149 while y <= 10_000 {
150 assert_eq!(civil_from_days(days), (y, m, d), "day {days}");
151 let len = [
152 31,
153 if leap(y) { 29 } else { 28 },
154 31,
155 30,
156 31,
157 30,
158 31,
159 31,
160 30,
161 31,
162 30,
163 31,
164 ][m as usize - 1];
165 (y, m, d) = if d < len {
166 (y, m, d + 1)
167 } else if m < 12 {
168 (y, m + 1, 1)
169 } else {
170 (y + 1, 1, 1)
171 };
172 days += 1;
173 }
174 }
175
176 #[test]
181 fn the_largest_epoch_decomposes_in_constant_time() {
182 let t = std::time::Instant::now();
183 let (y, mo, d, h, mi, s, ms) = decompose_epoch_ms(u64::MAX);
184 assert!(t.elapsed() < std::time::Duration::from_secs(1));
185 assert!(y > 584_000_000 && (1..=12).contains(&mo) && (1..=31).contains(&d));
186 assert!(h < 24 && mi < 60 && s < 60 && ms < 1000);
187 }
188
189 #[test]
190 fn epoch_scale_seconds() {
191 let node = EpochScale::seconds();
192 let mut out = [Value::None];
193 node.eval(&[Value::U64(5)], &mut out);
194 assert_eq!(out[0].as_u64(), 5000);
195 }
196
197 #[test]
198 fn epoch_offset_basic() {
199 let node = EpochOffset::new(1_000_000);
200 let mut out = [Value::None];
201 node.eval(&[Value::U64(500)], &mut out);
202 assert_eq!(out[0].as_u64(), 1_000_500);
203 }
204
205 #[test]
206 fn to_timestamp_epoch_zero() {
207 let node = ToTimestamp::new();
208 let mut out = [Value::None];
209 node.eval(&[Value::U64(0)], &mut out);
210 assert_eq!(out[0].as_str(), "1970-01-01T00:00:00.000Z");
211 }
212
213 #[test]
214 fn to_timestamp_known_date() {
215 let node = ToTimestamp::new();
216 let mut out = [Value::None];
217 node.eval(&[Value::U64(1_704_067_200_000)], &mut out);
219 assert_eq!(out[0].as_str(), "2024-01-01T00:00:00.000Z");
220 }
221
222 #[test]
223 fn date_components_epoch_zero() {
224 let node = DateComponents::new();
225 let mut out = vec![Value::None; 7];
226 node.eval(&[Value::U64(0)], &mut out);
227 assert_eq!(out[0].as_u64(), 1970);
228 assert_eq!(out[1].as_u64(), 1);
229 assert_eq!(out[2].as_u64(), 1);
230 assert_eq!(out[3].as_u64(), 0);
231 assert_eq!(out[4].as_u64(), 0);
232 assert_eq!(out[5].as_u64(), 0);
233 assert_eq!(out[6].as_u64(), 0);
234 }
235
236 #[test]
237 fn date_components_known() {
238 let node = DateComponents::new();
239 let mut out = vec![Value::None; 7];
240 let epoch = 19797u64 * MILLIS_PER_DAY
244 + 14 * MILLIS_PER_HOUR
245 + 30 * MILLIS_PER_MIN
246 + 45 * MILLIS_PER_SEC
247 + 123;
248 node.eval(&[Value::U64(epoch)], &mut out);
249 assert_eq!(out[0].as_u64(), 2024);
250 assert_eq!(out[1].as_u64(), 3);
251 assert_eq!(out[2].as_u64(), 15);
252 assert_eq!(out[3].as_u64(), 14);
253 assert_eq!(out[4].as_u64(), 30);
254 assert_eq!(out[5].as_u64(), 45);
255 assert_eq!(out[6].as_u64(), 123);
256 }
257
258 #[test]
259 fn epoch_scale_compiled() {
260 let node = EpochScale::seconds();
261 let op = node.compiled_u64().unwrap();
262 let mut out = [0u64];
263 op(&[5], &mut out);
264 assert_eq!(out[0], 5000);
265 }
266}