Skip to main content

gitway_lib/
time.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//! ISO 8601 timestamp helpers with no external crate dependency.
3//!
4//! Exposed at the library level so every Gitway binary (the main CLI plus
5//! the `gitway-keygen` and `gitway-add` shims) can emit the same timestamp
6//! format in structured JSON output and single-line diagnostic records.
7
8use std::time::{SystemTime, UNIX_EPOCH};
9
10/// Returns the current UTC time as an ISO 8601 string (e.g. `2026-04-12T14:30:00Z`).
11#[must_use]
12pub fn now_iso8601() -> String {
13    let secs = SystemTime::now()
14        .duration_since(UNIX_EPOCH)
15        .unwrap_or_default()
16        .as_secs();
17    epoch_secs_to_iso8601(secs)
18}
19
20/// Converts a Unix timestamp (seconds since 1970-01-01T00:00:00Z) to ISO 8601.
21///
22/// Uses the civil calendar algorithm from
23/// <https://howardhinnant.github.io/date_algorithms.html> — no external crate
24/// required.  Valid for any date representable as a positive `u64` epoch.
25#[must_use]
26#[expect(
27    clippy::similar_names,
28    reason = "doe/doy are the standard abbreviations in the Hinnant date algorithm"
29)]
30pub fn epoch_secs_to_iso8601(secs: u64) -> String {
31    let sec = secs % 60;
32    let mins = secs / 60;
33    let min = mins % 60;
34    let hours = mins / 60;
35    let hour = hours % 24;
36    let days = hours / 24;
37
38    // Civil date from days-since-epoch.
39    let z = days + 719_468;
40    let era = z / 146_097;
41    let doe = z - era * 146_097;
42    let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
43    let y = yoe + era * 400;
44    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
45    let mp = (5 * doy + 2) / 153;
46    let d = doy - (153 * mp + 2) / 5 + 1;
47    let m = if mp < 10 { mp + 3 } else { mp - 9 };
48    let y = if m <= 2 { y + 1 } else { y };
49
50    format!("{y:04}-{m:02}-{d:02}T{hour:02}:{min:02}:{sec:02}Z")
51}
52
53#[cfg(test)]
54mod tests {
55    use super::epoch_secs_to_iso8601;
56
57    #[test]
58    fn epoch_secs_to_iso8601_unix_epoch() {
59        assert_eq!(epoch_secs_to_iso8601(0), "1970-01-01T00:00:00Z");
60    }
61
62    #[test]
63    fn epoch_secs_to_iso8601_known_date() {
64        // 2026-04-12T00:00:00Z — verified manually.
65        // Days from epoch: 56 years × 365 + 14 leap days + 101 days into 2026
66        // = 20440 + 14 + 101 = 20555 days × 86400 s/day = 1_775_952_000
67        assert_eq!(epoch_secs_to_iso8601(1_775_952_000), "2026-04-12T00:00:00Z");
68    }
69}