polydat_nodes/
datetime.rs1#[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 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 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 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 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}