spacetimedb_sats/
time_duration.rs1use crate::timestamp::MICROSECONDS_PER_SECOND;
2use crate::{de::Deserialize, impl_st, ser::Serialize, AlgebraicType, AlgebraicValue};
3use std::fmt;
4use std::ops::{Add, AddAssign, Sub, SubAssign};
5use std::time::Duration;
6
7#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, Debug)]
8#[sats(crate = crate)]
9pub struct TimeDuration {
18 __time_duration_micros__: i64,
19}
20
21impl_st!([] TimeDuration, AlgebraicType::time_duration());
22
23impl TimeDuration {
24 pub const ZERO: TimeDuration = TimeDuration {
25 __time_duration_micros__: 0,
26 };
27
28 pub fn to_micros(self) -> i64 {
30 self.__time_duration_micros__
31 }
32
33 pub fn from_micros(micros: i64) -> Self {
35 Self {
36 __time_duration_micros__: micros,
37 }
38 }
39
40 pub fn to_duration(self) -> Result<Duration, Duration> {
42 let micros = self.to_micros();
43 let duration = Duration::from_micros(micros.unsigned_abs());
44 if micros >= 0 {
45 Ok(duration)
46 } else {
47 Err(duration)
48 }
49 }
50
51 pub fn to_duration_abs(self) -> Duration {
55 match self.to_duration() {
56 Ok(dur) | Err(dur) => dur,
57 }
58 }
59
60 pub fn to_duration_saturating(self) -> Duration {
62 self.to_duration().unwrap_or(Duration::ZERO)
63 }
64
65 pub fn abs(self) -> Self {
67 Self::from_micros(self.to_micros().saturating_abs())
68 }
69
70 pub fn from_duration(duration: Duration) -> Self {
74 Self::from_micros(
75 duration
76 .as_micros()
77 .try_into()
78 .expect("Duration overflows i64 microseconds"),
79 )
80 }
81
82 pub fn checked_add(self, other: Self) -> Option<Self> {
84 self.to_micros().checked_add(other.to_micros()).map(Self::from_micros)
85 }
86
87 pub fn checked_sub(self, other: Self) -> Option<Self> {
89 self.to_micros().checked_sub(other.to_micros()).map(Self::from_micros)
90 }
91
92 pub fn to_iso8601(self) -> String {
105 chrono::Duration::microseconds(self.to_micros()).to_string()
106 }
107}
108
109impl From<Duration> for TimeDuration {
110 fn from(d: Duration) -> TimeDuration {
111 TimeDuration::from_duration(d)
112 }
113}
114
115impl TryFrom<TimeDuration> for Duration {
116 type Error = Duration;
117 fn try_from(d: TimeDuration) -> Result<Duration, Duration> {
119 d.to_duration()
120 }
121}
122
123impl fmt::Display for TimeDuration {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 let micros = self.to_micros();
126 let sign = if micros < 0 { "-" } else { "+" };
127 let pos = micros.abs();
128 let secs = pos / MICROSECONDS_PER_SECOND;
129 let micros_remaining = pos % MICROSECONDS_PER_SECOND;
130 write!(f, "{sign}{secs}.{micros_remaining:06}")
131 }
132}
133
134impl Add for TimeDuration {
135 type Output = Self;
136
137 fn add(self, rhs: Self) -> Self::Output {
138 self.checked_add(rhs).unwrap()
139 }
140}
141
142impl Sub for TimeDuration {
143 type Output = Self;
144
145 fn sub(self, rhs: Self) -> Self::Output {
146 self.checked_sub(rhs).unwrap()
147 }
148}
149
150impl AddAssign for TimeDuration {
151 fn add_assign(&mut self, rhs: Self) {
152 *self = *self + rhs;
153 }
154}
155
156impl SubAssign for TimeDuration {
157 fn sub_assign(&mut self, rhs: Self) {
158 *self = *self - rhs;
159 }
160}
161
162impl From<TimeDuration> for AlgebraicValue {
169 fn from(value: TimeDuration) -> Self {
170 AlgebraicValue::product([value.to_micros().into()])
171 }
172}
173
174#[cfg(test)]
175mod test {
176 use super::*;
177 use crate::GroundSpacetimeType;
178 use proptest::prelude::*;
179 use std::time::SystemTime;
180
181 #[test]
182 fn timestamp_type_matches() {
183 assert_eq!(AlgebraicType::time_duration(), TimeDuration::get_type());
184 assert!(TimeDuration::get_type().is_time_duration());
185 assert!(TimeDuration::get_type().is_special());
186 }
187
188 #[test]
189 fn round_trip_duration_through_time_duration() {
190 let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
191 let rounded = Duration::from_micros(now.as_micros() as _);
192 let time_duration = TimeDuration::from_duration(rounded);
193 let now_prime = time_duration.to_duration().unwrap();
194 assert_eq!(rounded, now_prime);
195 }
196
197 proptest! {
198 #[test]
199 fn round_trip_time_duration_through_systemtime(micros in any::<i64>().prop_map(|n| n.abs())) {
200 let time_duration = TimeDuration::from_micros(micros);
201 let duration = time_duration.to_duration().unwrap();
202 let time_duration_prime = TimeDuration::from_duration(duration);
203 prop_assert_eq!(time_duration_prime, time_duration);
204 prop_assert_eq!(time_duration_prime.to_micros(), micros);
205 }
206
207 #[test]
208 fn arithmetic_as_expected(lhs in any::<i64>(), rhs in any::<i64>()) {
209 let lhs_time_duration = TimeDuration::from_micros(lhs);
210 let rhs_time_duration = TimeDuration::from_micros(rhs);
211
212 if let Some(sum) = lhs.checked_add(rhs) {
213 let sum_time_duration = lhs_time_duration.checked_add(rhs_time_duration);
214 prop_assert!(sum_time_duration.is_some());
215 prop_assert_eq!(sum_time_duration.unwrap().to_micros(), sum);
216
217 prop_assert_eq!((lhs_time_duration + rhs_time_duration).to_micros(), sum);
218
219 let mut sum_assign = lhs_time_duration;
220 sum_assign += rhs_time_duration;
221 prop_assert_eq!(sum_assign.to_micros(), sum);
222 } else {
223 prop_assert!(lhs_time_duration.checked_add(rhs_time_duration).is_none());
224 }
225
226 if let Some(diff) = lhs.checked_sub(rhs) {
227 let diff_time_duration = lhs_time_duration.checked_sub(rhs_time_duration);
228 prop_assert!(diff_time_duration.is_some());
229 prop_assert_eq!(diff_time_duration.unwrap().to_micros(), diff);
230
231 prop_assert_eq!((lhs_time_duration - rhs_time_duration).to_micros(), diff);
232
233 let mut diff_assign = lhs_time_duration;
234 diff_assign -= rhs_time_duration;
235 prop_assert_eq!(diff_assign.to_micros(), diff);
236 } else {
237 prop_assert!(lhs_time_duration.checked_sub(rhs_time_duration).is_none());
238 }
239 }
240 }
241}