#[polydat::polydat_node(category = Datetime)]
fn epoch_scale(
input: u64,
#[poly_default(1u64)] factor: polydat::derive_support::Const<u64>,
) -> u64 {
input.wrapping_mul(*factor)
}
impl EpochScale {
pub fn millis() -> Self {
Self::new(1)
}
pub fn seconds() -> Self {
Self::new(1_000)
}
pub fn minutes() -> Self {
Self::new(60_000)
}
pub fn hours() -> Self {
Self::new(3_600_000)
}
}
#[polydat::polydat_node(category = Datetime)]
fn epoch_offset(
input: u64,
#[poly_default(0u64)] base_epoch_ms: polydat::derive_support::Const<u64>,
) -> u64 {
input.wrapping_add(*base_epoch_ms)
}
impl EpochOffset {
pub fn from_2024() -> Self {
Self::new(1_704_067_200_000)
}
pub fn from_2025() -> Self {
Self::new(1_735_689_600_000)
}
}
#[polydat::polydat_node(category = Datetime)]
fn to_timestamp(input: u64) -> String {
epoch_ms_to_iso(input)
}
#[polydat::polydat_node(
category = Datetime,
output_names(year, month, day, hour, minute, second, millis),
)]
fn date_components(input: u64) -> (u64, u64, u64, u64, u64, u64, u64) {
decompose_epoch_ms(input)
}
const MILLIS_PER_SEC: u64 = 1_000;
#[allow(dead_code)]
const MILLIS_PER_MIN: u64 = 60_000;
#[allow(dead_code)]
const MILLIS_PER_HOUR: u64 = 3_600_000;
#[allow(dead_code)]
const MILLIS_PER_DAY: u64 = 86_400_000;
fn decompose_epoch_ms(epoch_ms: u64) -> (u64, u64, u64, u64, u64, u64, u64) {
let mut remaining = epoch_ms;
let ms = remaining % MILLIS_PER_SEC;
remaining /= MILLIS_PER_SEC;
let sec = remaining % 60;
remaining /= 60;
let min = remaining % 60;
remaining /= 60;
let hour = remaining % 24;
let (year, month, day) = civil_from_days(remaining / 24);
(year, month, day, hour, min, sec, ms)
}
fn civil_from_days(days: u64) -> (u64, u64, u64) {
let z = days + 719_468; let era = z / 146_097;
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;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = yoe + era * 400 + u64::from(month <= 2);
(year, month, day)
}
fn epoch_ms_to_iso(epoch_ms: u64) -> String {
let (y, mo, d, h, mi, s, ms) = decompose_epoch_ms(epoch_ms);
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}.{ms:03}Z")
}
#[cfg(test)]
mod tests {
use super::*;
use polydat::ast::{PolydatNode, Value};
#[test]
fn the_closed_form_calendar_agrees_with_the_walk() {
let mut days = 0u64;
let (mut y, mut m, mut d) = (1970u64, 1u64, 1u64);
let leap =
|y: u64| (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400);
while y <= 10_000 {
assert_eq!(civil_from_days(days), (y, m, d), "day {days}");
let len = [
31,
if leap(y) { 29 } else { 28 },
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
][m as usize - 1];
(y, m, d) = if d < len {
(y, m, d + 1)
} else if m < 12 {
(y, m + 1, 1)
} else {
(y + 1, 1, 1)
};
days += 1;
}
}
#[test]
fn the_largest_epoch_decomposes_in_constant_time() {
let t = std::time::Instant::now();
let (y, mo, d, h, mi, s, ms) = decompose_epoch_ms(u64::MAX);
assert!(t.elapsed() < std::time::Duration::from_secs(1));
assert!(y > 584_000_000 && (1..=12).contains(&mo) && (1..=31).contains(&d));
assert!(h < 24 && mi < 60 && s < 60 && ms < 1000);
}
#[test]
fn epoch_scale_seconds() {
let node = EpochScale::seconds();
let mut out = [Value::None];
node.eval(&[Value::U64(5)], &mut out);
assert_eq!(out[0].as_u64(), 5000);
}
#[test]
fn epoch_offset_basic() {
let node = EpochOffset::new(1_000_000);
let mut out = [Value::None];
node.eval(&[Value::U64(500)], &mut out);
assert_eq!(out[0].as_u64(), 1_000_500);
}
#[test]
fn to_timestamp_epoch_zero() {
let node = ToTimestamp::new();
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
assert_eq!(out[0].as_str(), "1970-01-01T00:00:00.000Z");
}
#[test]
fn to_timestamp_known_date() {
let node = ToTimestamp::new();
let mut out = [Value::None];
node.eval(&[Value::U64(1_704_067_200_000)], &mut out);
assert_eq!(out[0].as_str(), "2024-01-01T00:00:00.000Z");
}
#[test]
fn date_components_epoch_zero() {
let node = DateComponents::new();
let mut out = vec![Value::None; 7];
node.eval(&[Value::U64(0)], &mut out);
assert_eq!(out[0].as_u64(), 1970);
assert_eq!(out[1].as_u64(), 1);
assert_eq!(out[2].as_u64(), 1);
assert_eq!(out[3].as_u64(), 0);
assert_eq!(out[4].as_u64(), 0);
assert_eq!(out[5].as_u64(), 0);
assert_eq!(out[6].as_u64(), 0);
}
#[test]
fn date_components_known() {
let node = DateComponents::new();
let mut out = vec![Value::None; 7];
let epoch = 19797u64 * MILLIS_PER_DAY
+ 14 * MILLIS_PER_HOUR
+ 30 * MILLIS_PER_MIN
+ 45 * MILLIS_PER_SEC
+ 123;
node.eval(&[Value::U64(epoch)], &mut out);
assert_eq!(out[0].as_u64(), 2024);
assert_eq!(out[1].as_u64(), 3);
assert_eq!(out[2].as_u64(), 15);
assert_eq!(out[3].as_u64(), 14);
assert_eq!(out[4].as_u64(), 30);
assert_eq!(out[5].as_u64(), 45);
assert_eq!(out[6].as_u64(), 123);
}
#[test]
fn epoch_scale_compiled() {
let node = EpochScale::seconds();
let op = node.compiled_u64().unwrap();
let mut out = [0u64];
op(&[5], &mut out);
assert_eq!(out[0], 5000);
}
}