Skip to main content

archiver/
time.rs

1//! Timestamps as the archiver wants them.
2
3use chrono::{DateTime, Utc};
4
5/// `t` (UTC epoch seconds) as `YYYY-MM-DDTHH:MM:SS.mmmZ`, rounded to the
6/// millisecond. Non-finite or out-of-range times are clamped to the epoch
7/// and to year 9999.
8pub fn format_time(t: f64) -> String {
9    const MAX_MS: f64 = 253_402_300_799_999.0; // 9999-12-31T23:59:59.999Z
10    let ms = if t.is_finite() {
11        (t * 1000.0).round().clamp(0.0, MAX_MS)
12    } else if t > 0.0 {
13        MAX_MS
14    } else {
15        0.0
16    };
17    DateTime::<Utc>::from_timestamp_millis(ms as i64)
18        .unwrap_or_default()
19        .format("%Y-%m-%dT%H:%M:%S%.3fZ")
20        .to_string()
21}
22
23/// An archiver timestamp (`Instant.parse` / ISO offset forms, e.g.
24/// `2024-01-02T03:04:05.123Z` or `2024-01-02T03:04:05-07:00`) as epoch seconds.
25pub fn parse_time(s: &str) -> Option<f64> {
26    let t = DateTime::parse_from_rfc3339(s.trim()).ok()?;
27    Some(t.timestamp() as f64 + f64::from(t.timestamp_subsec_nanos()) * 1e-9)
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33
34    #[test]
35    fn formats_utc_millis() {
36        assert_eq!(format_time(0.0), "1970-01-01T00:00:00.000Z");
37        assert_eq!(format_time(1_704_067_200.0), "2024-01-01T00:00:00.000Z");
38        assert_eq!(format_time(1_704_067_200.123), "2024-01-01T00:00:00.123Z");
39        assert_eq!(format_time(1_704_067_200.999_6), "2024-01-01T00:00:01.000Z");
40        assert_eq!(format_time(f64::NAN), "1970-01-01T00:00:00.000Z");
41        assert_eq!(format_time(f64::INFINITY), "9999-12-31T23:59:59.999Z");
42        assert_eq!(format_time(-5.0), "1970-01-01T00:00:00.000Z");
43    }
44
45    #[test]
46    fn parses_instant_and_offset_forms() {
47        assert_eq!(parse_time("2024-01-01T00:00:00Z"), Some(1_704_067_200.0));
48        let t = parse_time("2024-01-01T00:00:00.123456789Z").unwrap();
49        assert!((t - 1_704_067_200.123_457).abs() < 1e-6);
50        assert_eq!(
51            parse_time("2023-12-31T17:00:00-07:00"),
52            Some(1_704_067_200.0)
53        );
54        assert_eq!(parse_time("yesterday"), None);
55        assert_eq!(parse_time("2024-01-01T00:00:00"), None);
56    }
57
58    #[test]
59    fn round_trips() {
60        for t in [1_700_000_000.5, 1_234_567_890.001, 86_400.25] {
61            assert!((parse_time(&format_time(t)).unwrap() - t).abs() < 1e-3);
62        }
63    }
64}