Skip to main content

git_policy_cli/
iso_time.rs

1//! What:
2//!  UTC timestamps in the exact form of JavaScript's `Date.prototype.toISOString`:
3//!       `YYYY-MM-DDTHH:MM:SS.mmmZ`.
4//! Why:
5//!  Transaction owner records carry `createdAt` and capture records `invokedAt` in this
6//!      form;
7//!  recovery orders dead transactions and the reservation grants turns by comparing
8//!      these strings,
9//!  so both wrappers must write the same shape.
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! new Date().toISOString()
14//! ```
15
16/// Milliseconds in one day.
17const MILLISECONDS_PER_DAY: i64 = 86_400_000;
18
19/// What:
20///  The civil date of a day count since 1970-01-01,
21///  as (year,
22///  month,
23///  day).
24///       `(i64, u32, u32)` is a tuple of three numbers.
25/// Why:
26///   Howard Hinnant's `civil_from_days`,
27///  exact for the proleptic Gregorian calendar
28///       JavaScript dates use.
29///
30/// In TS you'd write (pseudocode):
31/// ```ts
32/// const d = new Date(days * 86400000); [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()]
33/// ```
34pub fn civil_from_days(days: i64) -> (i64, u32, u32) {
35    let shifted: i64 = days + 719_468;
36    let era: i64 = shifted.div_euclid(146_097);
37    let day_of_era: i64 = shifted.rem_euclid(146_097);
38    let year_of_era: i64 =
39        (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
40    let day_of_year: i64 = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
41    let month_index: i64 = (5 * day_of_year + 2) / 153;
42    let day: i64 = day_of_year - (153 * month_index + 2) / 5 + 1;
43    let month: i64 = if month_index < 10 {
44        month_index + 3
45    } else {
46        month_index - 9
47    };
48    let year: i64 = year_of_era + era * 400 + i64::from(month <= 2);
49    // Both fit: month is 1 to 12 and day 1 to 31.
50    return (year, month as u32, day as u32);
51}
52
53/// What:
54///  Format milliseconds since the Unix epoch as `toISOString` does for years 0 to 9999.
55/// Why:
56///   The wrapper only formats the current time;
57///  the four-digit year form covers it.
58///
59/// In TS you'd write (pseudocode):
60/// ```ts
61/// new Date(milliseconds).toISOString()
62/// ```
63pub fn format_iso_milliseconds(milliseconds: i64) -> String {
64    let days: i64 = milliseconds.div_euclid(MILLISECONDS_PER_DAY);
65    let within: i64 = milliseconds.rem_euclid(MILLISECONDS_PER_DAY);
66    let (year, month, day) = civil_from_days(days);
67    let hours: i64 = within / 3_600_000;
68    let minutes: i64 = within / 60_000 % 60;
69    let seconds: i64 = within / 1000 % 60;
70    let millis: i64 = within % 1000;
71    return format!(
72        "{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}.{millis:03}Z"
73    );
74}
75
76/// What:
77///  The current time as `toISOString` text.
78/// Why:
79///   Owner and capture records record when the invocation started.
80///
81/// In TS you'd write (pseudocode):
82/// ```ts
83/// new Date().toISOString()
84/// ```
85pub fn now_iso() -> String {
86    // A clock before 1970 reads as the epoch; nothing orders by such a time.
87    let elapsed: std::time::Duration = std::time::SystemTime::now()
88        .duration_since(std::time::UNIX_EPOCH)
89        .unwrap_or_default();
90    let milliseconds: i64 = i64::try_from(elapsed.as_millis()).unwrap_or(i64::MAX);
91    return format_iso_milliseconds(milliseconds);
92}
93
94/// Calendar controls stay out of the release executable.
95#[cfg(test)]
96#[path = "iso_time_tests.rs"]
97mod tests;