1use 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 #[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 #[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}