const ACCESS_EPOCH_JDN: i64 = 2_415_019;
const SECS_PER_DAY: f64 = 86_400.0;
pub fn timestamp_to_parts(ts: f64) -> (i32, u32, u32, u32, u32, u32) {
if !ts.is_finite() {
return (1899, 12, 30, 0, 0, 0); }
let days = ts.floor() as i64;
let jdn = ACCESS_EPOCH_JDN + days;
let (year, month, day) = jdn_to_gregorian(jdn);
let frac = (ts - ts.floor()).abs();
let total_secs = (frac * SECS_PER_DAY + 0.5) as u32; let hour = total_secs / 3600;
let minute = (total_secs % 3600) / 60;
let second = total_secs % 60;
(year, month, day, hour, minute, second)
}
pub fn format_timestamp(ts: f64, fmt: &str) -> String {
let (year, month, day, hour, minute, second) = timestamp_to_parts(ts);
let mut result = String::with_capacity(fmt.len() + 8);
let mut chars = fmt.chars();
while let Some(c) = chars.next() {
if c == '%' {
match chars.next() {
Some('Y') => result.push_str(&format!("{year:04}")),
Some('m') => result.push_str(&format!("{month:02}")),
Some('d') => result.push_str(&format!("{day:02}")),
Some('H') => result.push_str(&format!("{hour:02}")),
Some('M') => result.push_str(&format!("{minute:02}")),
Some('S') => result.push_str(&format!("{second:02}")),
Some('%') => result.push('%'),
Some(other) => {
result.push('%');
result.push(other);
}
None => result.push('%'),
}
} else {
result.push(c);
}
}
result
}
pub fn is_date_only(ts: f64) -> bool {
if !ts.is_finite() {
return true;
}
let frac = (ts - ts.floor()).abs();
frac < 1e-9
}
const EXT_DATETIME_EPOCH_JDN: i64 = 1_721_426;
pub fn format_ext_datetime(days: i64, seconds: i64, nanos100: i64) -> String {
let (y, m, d) = jdn_to_gregorian(EXT_DATETIME_EPOCH_JDN + days);
let h = seconds / 3600;
let min = (seconds % 3600) / 60;
let sec = seconds % 60;
if seconds == 0 && nanos100 == 0 {
format!("{y:04}-{m:02}-{d:02}")
} else if nanos100 == 0 {
format!("{y:04}-{m:02}-{d:02} {h:02}:{min:02}:{sec:02}")
} else {
format!("{y:04}-{m:02}-{d:02} {h:02}:{min:02}:{sec:02}.{nanos100:07}")
}
}
fn jdn_to_gregorian(jdn: i64) -> (i32, u32, u32) {
let a = jdn + 32044;
let b = (4 * a + 3) / 146_097;
let c = a - (146_097 * b) / 4;
let d = (4 * c + 3) / 1461;
let e = c - (1461 * d) / 4;
let m = (5 * e + 2) / 153;
let day = e - (153 * m + 2) / 5 + 1;
let month = m + 3 - 12 * (m / 10);
let year = 100 * b + d - 4800 + m / 10;
(year as i32, month as u32, day as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn epoch_zero() {
let (y, m, d, h, min, s) = timestamp_to_parts(0.0);
assert_eq!((y, m, d, h, min, s), (1899, 12, 30, 0, 0, 0));
}
#[test]
fn known_date_2003_01_02() {
let ts = 37623.0;
let (y, m, d, h, min, s) = timestamp_to_parts(ts);
assert_eq!((y, m, d), (2003, 1, 2));
assert_eq!((h, min, s), (0, 0, 0));
}
#[test]
fn date_with_time() {
let ts = 37623.5;
let (y, m, d, h, min, s) = timestamp_to_parts(ts);
assert_eq!((y, m, d), (2003, 1, 2));
assert_eq!((h, min, s), (12, 0, 0));
}
#[test]
fn date_with_time_detailed() {
let ts = 37623.75;
let (_, _, _, h, min, s) = timestamp_to_parts(ts);
assert_eq!((h, min, s), (18, 0, 0));
}
#[test]
fn leap_year_feb29() {
let ts = 36585.0;
let (y, m, d, _, _, _) = timestamp_to_parts(ts);
assert_eq!((y, m, d), (2000, 2, 29));
}
#[test]
fn negative_value() {
let ts = -1.0;
let (y, m, d, _, _, _) = timestamp_to_parts(ts);
assert_eq!((y, m, d), (1899, 12, 29));
}
#[test]
fn is_date_only_true() {
assert!(is_date_only(37623.0));
}
#[test]
fn is_date_only_false() {
assert!(!is_date_only(37623.5));
}
#[test]
fn is_date_only_epsilon() {
assert!(is_date_only(37623.0 + 1e-12));
}
#[test]
fn format_year() {
let s = format_timestamp(37623.0, "%Y");
assert_eq!(s, "2003");
}
#[test]
fn format_month() {
let s = format_timestamp(37623.0, "%m");
assert_eq!(s, "01");
}
#[test]
fn format_day() {
let s = format_timestamp(37623.0, "%d");
assert_eq!(s, "02");
}
#[test]
fn format_hour() {
let s = format_timestamp(37623.5, "%H");
assert_eq!(s, "12");
}
#[test]
fn format_minute() {
let ts = 37623.0 + 30.0 / 1440.0;
let s = format_timestamp(ts, "%M");
assert_eq!(s, "30");
}
#[test]
fn format_second() {
let ts = 37623.0 + 45.0 / 86400.0;
let s = format_timestamp(ts, "%S");
assert_eq!(s, "45");
}
#[test]
fn format_percent_literal() {
let s = format_timestamp(37623.0, "%%");
assert_eq!(s, "%");
}
#[test]
fn format_custom_dmy() {
let s = format_timestamp(37623.0, "%d/%m/%Y");
assert_eq!(s, "02/01/2003");
}
#[test]
fn format_full_datetime() {
let ts = 37623.5;
let s = format_timestamp(ts, "%Y-%m-%d %H:%M:%S");
assert_eq!(s, "2003-01-02 12:00:00");
}
#[test]
fn day_one() {
let (y, m, d, _, _, _) = timestamp_to_parts(1.0);
assert_eq!((y, m, d), (1899, 12, 31));
}
#[test]
fn day_two() {
let (y, m, d, _, _, _) = timestamp_to_parts(2.0);
assert_eq!((y, m, d), (1900, 1, 1));
}
#[test]
fn nan_returns_epoch() {
assert_eq!(timestamp_to_parts(f64::NAN), (1899, 12, 30, 0, 0, 0));
}
#[test]
fn infinity_returns_epoch() {
assert_eq!(timestamp_to_parts(f64::INFINITY), (1899, 12, 30, 0, 0, 0));
}
#[test]
fn neg_infinity_returns_epoch() {
assert_eq!(
timestamp_to_parts(f64::NEG_INFINITY),
(1899, 12, 30, 0, 0, 0)
);
}
#[test]
fn is_date_only_nan() {
assert!(is_date_only(f64::NAN));
}
#[test]
fn is_date_only_infinity() {
assert!(is_date_only(f64::INFINITY));
}
#[test]
fn format_unknown_specifier() {
let s = format_timestamp(37623.0, "%Z");
assert_eq!(s, "%Z");
}
#[test]
fn format_trailing_percent() {
let s = format_timestamp(37623.0, "end%");
assert!(s.ends_with('%'));
}
#[test]
fn format_no_specifiers() {
let s = format_timestamp(37623.0, "plain text");
assert_eq!(s, "plain text");
}
#[test]
fn format_empty_string() {
let s = format_timestamp(37623.0, "");
assert_eq!(s, "");
}
#[test]
fn ext_datetime_date_only() {
assert_eq!(format_ext_datetime(737592, 0, 0), "2020-06-17");
}
#[test]
fn ext_datetime_with_time_and_nanos() {
assert_eq!(
format_ext_datetime(737954, 81912, 3456789),
"2021-06-14 22:45:12.3456789"
);
}
#[test]
fn ext_datetime_epoch() {
assert_eq!(format_ext_datetime(0, 0, 0), "0001-01-01");
}
#[test]
fn ext_datetime_time_no_nanos() {
assert_eq!(format_ext_datetime(737954, 45900, 0), "2021-06-14 12:45:00");
}
}