Skip to main content

canwu_time/
lib.rs

1//! Historical simulation time, independent from rendering frames and wall time.
2
3use serde::{Deserialize, Serialize};
4use std::fmt::{Display, Formatter};
5use std::ops::{Add, AddAssign, Sub};
6
7#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
8#[serde(transparent)]
9pub struct SimDuration(i64);
10
11impl SimDuration {
12    pub const ZERO: Self = Self(0);
13
14    #[must_use]
15    pub const fn minutes(minutes: i64) -> Self {
16        Self(minutes)
17    }
18
19    /// Creates a duration from hours.
20    ///
21    /// # Panics
22    ///
23    /// Panics when the value cannot be represented in simulation minutes. Use
24    /// [`Self::checked_hours`] for data-dependent input.
25    #[must_use]
26    pub const fn hours(hours: i64) -> Self {
27        match Self::checked_hours(hours) {
28            Some(duration) => duration,
29            None => panic!("simulation duration hours exceed the supported range"),
30        }
31    }
32
33    #[must_use]
34    pub const fn checked_hours(hours: i64) -> Option<Self> {
35        match hours.checked_mul(60) {
36            Some(minutes) => Some(Self(minutes)),
37            None => None,
38        }
39    }
40
41    /// Creates a duration from days.
42    ///
43    /// # Panics
44    ///
45    /// Panics when the value cannot be represented in simulation minutes. Use
46    /// [`Self::checked_days`] for data-dependent input.
47    #[must_use]
48    pub const fn days(days: i64) -> Self {
49        match Self::checked_days(days) {
50            Some(duration) => duration,
51            None => panic!("simulation duration days exceed the supported range"),
52        }
53    }
54
55    #[must_use]
56    pub const fn checked_days(days: i64) -> Option<Self> {
57        match days.checked_mul(24 * 60) {
58            Some(minutes) => Some(Self(minutes)),
59            None => None,
60        }
61    }
62
63    #[must_use]
64    pub const fn as_minutes(self) -> i64 {
65        self.0
66    }
67
68    #[must_use]
69    pub const fn is_negative(self) -> bool {
70        self.0 < 0
71    }
72
73    #[must_use]
74    pub const fn checked_add(self, other: Self) -> Option<Self> {
75        match self.0.checked_add(other.0) {
76            Some(minutes) => Some(Self(minutes)),
77            None => None,
78        }
79    }
80}
81
82#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
83#[serde(transparent)]
84pub struct SimTime(i64);
85
86impl SimTime {
87    pub const EPOCH: Self = Self(0);
88
89    #[must_use]
90    pub const fn from_minutes(minutes_from_epoch: i64) -> Self {
91        Self(minutes_from_epoch)
92    }
93
94    #[must_use]
95    pub const fn as_minutes(self) -> i64 {
96        self.0
97    }
98
99    #[must_use]
100    pub const fn checked_add(self, duration: SimDuration) -> Option<Self> {
101        match self.0.checked_add(duration.0) {
102            Some(minutes) => Some(Self(minutes)),
103            None => None,
104        }
105    }
106
107    #[must_use]
108    pub const fn checked_sub(self, other: Self) -> Option<SimDuration> {
109        match self.0.checked_sub(other.0) {
110            Some(minutes) => Some(SimDuration(minutes)),
111            None => None,
112        }
113    }
114}
115
116impl Add<SimDuration> for SimTime {
117    type Output = Self;
118
119    fn add(self, duration: SimDuration) -> Self::Output {
120        self.checked_add(duration)
121            .expect("simulation time addition exceeds the supported range")
122    }
123}
124
125impl Add for SimDuration {
126    type Output = Self;
127
128    fn add(self, other: Self) -> Self::Output {
129        self.checked_add(other)
130            .expect("simulation duration addition exceeds the supported range")
131    }
132}
133
134impl AddAssign<SimDuration> for SimTime {
135    fn add_assign(&mut self, duration: SimDuration) {
136        *self = *self + duration;
137    }
138}
139
140impl Sub for SimTime {
141    type Output = SimDuration;
142
143    fn sub(self, other: Self) -> Self::Output {
144        self.checked_sub(other)
145            .expect("simulation time subtraction exceeds the supported range")
146    }
147}
148
149impl Display for SimTime {
150    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
151        let day = self.0.div_euclid(24 * 60);
152        let within_day = self.0.rem_euclid(24 * 60);
153        let hour = within_day / 60;
154        let minute = within_day % 60;
155        write!(formatter, "Day {day}, {hour:02}:{minute:02}")
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::{SimDuration, SimTime};
162
163    #[test]
164    fn checked_time_apis_reject_overflow_without_silent_saturation() {
165        assert_eq!(SimDuration::checked_hours(i64::MAX), None);
166        assert_eq!(SimDuration::checked_days(i64::MAX), None);
167        assert_eq!(
168            SimDuration::minutes(i64::MAX).checked_add(SimDuration::minutes(1)),
169            None
170        );
171        assert_eq!(
172            SimTime::from_minutes(i64::MAX).checked_add(SimDuration::minutes(1)),
173            None
174        );
175        assert_eq!(
176            SimTime::from_minutes(i64::MAX).checked_sub(SimTime::from_minutes(i64::MIN)),
177            None
178        );
179
180        assert!(
181            std::panic::catch_unwind(|| SimDuration::hours(i64::MAX)).is_err(),
182            "the convenience constructor must not clamp"
183        );
184        assert!(
185            std::panic::catch_unwind(|| {
186                SimTime::from_minutes(i64::MAX) + SimDuration::minutes(1)
187            })
188            .is_err(),
189            "the addition operator must not clamp"
190        );
191    }
192}