forge_agent/env/
timezone.rs1use time::format_description::well_known::Rfc3339;
7use time_tz::{OffsetDateTimeExt, Tz, timezones};
8
9pub fn system_timezone() -> &'static Tz {
15 match iana_time_zone::get_timezone() {
16 Ok(name) => timezones::get_by_name(&name).unwrap_or_else(|| {
17 tracing::warn!(
18 target: "forge_agent::env::timezone",
19 %name,
20 "unknown system timezone; falling back to UTC",
21 );
22 timezones::db::UTC
23 }),
24 Err(error) => {
25 tracing::warn!(
26 target: "forge_agent::env::timezone",
27 %error,
28 "system timezone unavailable; falling back to UTC",
29 );
30 timezones::db::UTC
31 }
32 }
33}
34
35pub fn local_clock(instant: &str) -> Option<String> {
41 let parsed = time::OffsetDateTime::parse(instant, &Rfc3339).ok()?;
42 let local = parsed.to_timezone(system_timezone());
43 let today = time::OffsetDateTime::now_utc().to_timezone(system_timezone()).date();
44 let clock = format!("{:02}:{:02}:{:02}", local.hour(), local.minute(), local.second());
45 Some(if local.date() == today { clock } else { format!("{} {clock}", local.date()) })
46}
47
48pub fn millis_between(from: &str, to: &str) -> Option<u64> {
51 let from = time::OffsetDateTime::parse(from, &Rfc3339).ok()?;
52 let to = time::OffsetDateTime::parse(to, &Rfc3339).ok()?;
53 u64::try_from((to - from).whole_milliseconds()).ok()
54}
55
56#[cfg(test)]
57mod tests {
58 use super::{Rfc3339, local_clock, millis_between, system_timezone};
59 use time_tz::OffsetDateTimeExt;
60
61 #[test]
67 fn a_row_instant_reads_as_the_reader_own_wall_clock() {
68 let instant = "2020-04-22T04:15:27.000Z";
69 let local =
70 time::OffsetDateTime::parse(instant, &time::format_description::well_known::Rfc3339)
71 .expect("the fixture parses")
72 .to_timezone(system_timezone());
73 let clock = format!("{:02}:{:02}:{:02}", local.hour(), local.minute(), local.second());
74
75 assert_eq!(
76 local_clock(instant).as_deref(),
77 Some(format!("{} {clock}", local.date()).as_str()),
78 "an instant from another day renders as that date and the host's own clock",
79 );
80 }
81
82 #[test]
86 fn the_clock_is_the_hosts_and_not_the_instants_own() {
87 let instant = "2020-04-22T04:15:27.000Z";
88 let rendered = local_clock(instant).expect("the fixture parses");
89 let offset = time::OffsetDateTime::now_utc().to_timezone(system_timezone()).offset();
90
91 if offset.is_utc() {
92 assert!(
93 rendered.ends_with("04:15:27"),
94 "a UTC host renders the instant's own clock: {rendered}"
95 );
96 } else {
97 assert!(
98 !rendered.ends_with("04:15:27"),
99 "the host's own clock is not the instant's own: {rendered}",
100 );
101 }
102 }
103
104 #[test]
107 fn todays_instant_carries_the_time_alone() {
108 let now = time::OffsetDateTime::now_utc().to_timezone(system_timezone());
109 let rendered = local_clock(&now.format(&Rfc3339).expect("formats")).expect("parses");
110
111 assert_eq!(
112 rendered,
113 format!("{:02}:{:02}:{:02}", now.hour(), now.minute(), now.second()),
114 "the reader's own clock, and no date on the day it belongs to",
115 );
116 }
117
118 #[test]
120 fn an_unparseable_instant_has_no_wall_clock() {
121 assert_eq!(local_clock("not a timestamp"), None, "no clock is invented for it");
122 }
123
124 #[test]
127 fn a_span_is_the_time_between_two_rows_and_nothing_when_it_runs_backwards() {
128 assert_eq!(
129 millis_between("2026-04-22T04:15:27.000Z", "2026-04-22T04:18:08.000Z"),
130 Some(161_000),
131 "two minutes and forty-one seconds",
132 );
133 assert_eq!(
134 millis_between("2026-04-22T04:18:08.000Z", "2026-04-22T04:15:27.000Z"),
135 None,
136 "a span that runs backwards is not a duration",
137 );
138 assert_eq!(
139 millis_between("2026-04-22T04:15:27.000Z", "nonsense"),
140 None,
141 "and neither is a pair with an unreadable end"
142 );
143 }
144}