Skip to main content

geam_time/
source.rs

1use super::TimeSource;
2use crate::HostFailure;
3use jiff::{Timestamp, tz::TimeZone};
4use num_bigint::BigInt;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7/// A wall-clock source backed by the operating system clock and time zone.
8#[derive(Debug, Clone, Copy)]
9pub struct SystemTimeSource;
10
11impl TimeSource for SystemTimeSource {
12    fn system_time(&mut self) -> Result<SystemTime, HostFailure> {
13        Ok(SystemTime::now())
14    }
15
16    fn local_offset_seconds(&mut self) -> Result<i32, HostFailure> {
17        current_offset_seconds(
18            map_system_time_zone(TimeZone::try_system()),
19            Timestamp::now(),
20        )
21    }
22}
23
24pub(super) fn split_system_time(time: SystemTime) -> (BigInt, BigInt) {
25    match time.duration_since(UNIX_EPOCH) {
26        Ok(duration) => (
27            BigInt::from(duration.as_secs()),
28            BigInt::from(duration.subsec_nanos()),
29        ),
30        Err(error) => {
31            let duration = error.duration();
32            let seconds = BigInt::from(duration.as_secs());
33            let nanoseconds = duration.subsec_nanos();
34            if nanoseconds == 0 {
35                (-seconds, BigInt::from(0))
36            } else {
37                (-seconds - 1, BigInt::from(1_000_000_000u32 - nanoseconds))
38            }
39        }
40    }
41}
42
43fn map_system_time_zone<Error>(result: Result<TimeZone, Error>) -> Result<TimeZone, HostFailure> {
44    result.map_err(|_| HostFailure::new("could not determine the current local UTC offset"))
45}
46
47fn current_offset_seconds(
48    time_zone: Result<TimeZone, HostFailure>,
49    timestamp: Timestamp,
50) -> Result<i32, HostFailure> {
51    Ok(time_zone?.to_offset(timestamp).seconds())
52}
53
54#[cfg(test)]
55mod tests {
56    use super::{
57        SystemTimeSource, current_offset_seconds, map_system_time_zone, split_system_time,
58    };
59    use crate::TimeSource;
60    use jiff::{
61        Timestamp,
62        tz::{TimeZone, offset},
63    };
64    use num_bigint::BigInt;
65    use std::time::{Duration, UNIX_EPOCH};
66
67    #[test]
68    fn splits_system_time_into_canonical_seconds_and_nanoseconds() {
69        for (time, expected) in [
70            (UNIX_EPOCH, (BigInt::from(0), BigInt::from(0))),
71            (
72                UNIX_EPOCH + Duration::from_nanos(1),
73                (BigInt::from(0), BigInt::from(1)),
74            ),
75            (
76                UNIX_EPOCH + Duration::new(100_000_000_000, 999_999_999),
77                (
78                    BigInt::from(100_000_000_000u64),
79                    BigInt::from(999_999_999u32),
80                ),
81            ),
82            (
83                UNIX_EPOCH - Duration::from_nanos(1),
84                (BigInt::from(-1), BigInt::from(999_999_999u32)),
85            ),
86            (
87                UNIX_EPOCH - Duration::from_secs(1),
88                (BigInt::from(-1), BigInt::from(0)),
89            ),
90            (
91                UNIX_EPOCH - Duration::new(2, 3),
92                (BigInt::from(-3), BigInt::from(999_999_997u32)),
93            ),
94        ] {
95            assert_eq!(split_system_time(time), expected);
96        }
97    }
98
99    #[test]
100    fn maps_system_time_zone_discovery_without_a_silent_fallback() {
101        let tokyo = TimeZone::fixed(offset(9));
102        let failure = map_system_time_zone::<()>(Err(()))
103            .expect_err("failed discovery should remain a host failure");
104
105        assert_eq!(
106            map_system_time_zone::<()>(Ok(tokyo.clone()))
107                .expect("known time zone should remain available"),
108            tokyo,
109        );
110        assert_eq!(
111            current_offset_seconds(Ok(tokyo), Timestamp::UNIX_EPOCH)
112                .expect("known time zone should have an offset"),
113            32_400,
114        );
115        assert_eq!(
116            current_offset_seconds(Err(failure), Timestamp::UNIX_EPOCH)
117                .expect_err("failed discovery should remain a host failure")
118                .message(),
119            "could not determine the current local UTC offset",
120        );
121    }
122
123    #[test]
124    fn system_source_reads_the_current_wall_clock_and_offset_fallibly() {
125        let mut source = SystemTimeSource;
126        source
127            .system_time()
128            .expect("the operating system wall clock should be available");
129
130        assert!(
131            source
132                .local_offset_seconds()
133                .map_or(true, |offset| (-86_400..=86_400).contains(&offset)),
134        );
135    }
136}