1use std::{
23 fmt,
24 ops::{Add, Sub},
25 str::FromStr,
26 time::Duration,
27};
28
29use borsh::{BorshDeserialize, BorshSerialize};
30use serde::{Deserialize, Serialize};
31
32#[derive(
34 BorshSerialize,
35 BorshDeserialize,
36 Clone,
37 Copy,
38 Debug,
39 Default,
40 Eq,
41 Hash,
42 Ord,
43 PartialEq,
44 PartialOrd,
45 Serialize,
46 Deserialize,
47)]
48#[serde(transparent)]
49#[repr(transparent)]
50pub struct TimestampMs(u64);
51
52impl TimestampMs {
53 pub const ZERO: Self = Self(0);
55 pub const MAX: Self = Self(u64::MAX);
57
58 pub const fn new(millis: u64) -> Self {
60 Self(millis)
61 }
62
63 pub const fn as_millis(self) -> u64 {
65 self.0
66 }
67
68 pub const fn to_le_bytes(self) -> [u8; 8] {
70 self.0.to_le_bytes()
71 }
72
73 pub const fn from_le_bytes(bytes: [u8; 8]) -> Self {
75 Self(u64::from_le_bytes(bytes))
76 }
77
78 pub const fn saturating_add(self, rhs: DurationMs) -> Self {
80 Self(self.0.saturating_add(rhs.0))
81 }
82
83 pub const fn saturating_sub(self, rhs: DurationMs) -> Self {
85 Self(self.0.saturating_sub(rhs.0))
86 }
87
88 pub const fn checked_sub(self, rhs: DurationMs) -> Option<Self> {
90 match self.0.checked_sub(rhs.0) {
91 Some(v) => Some(Self(v)),
92 None => None,
93 }
94 }
95
96 pub const fn checked_add(self, rhs: DurationMs) -> Option<Self> {
98 match self.0.checked_add(rhs.0) {
99 Some(v) => Some(Self(v)),
100 None => None,
101 }
102 }
103
104 pub const fn saturating_duration_since(self, earlier: Self) -> DurationMs {
107 DurationMs(self.0.saturating_sub(earlier.0))
108 }
109}
110
111impl From<u64> for TimestampMs {
112 fn from(millis: u64) -> Self {
113 Self(millis)
114 }
115}
116
117impl From<TimestampMs> for u64 {
118 fn from(ts: TimestampMs) -> Self {
119 ts.0
120 }
121}
122
123impl fmt::Display for TimestampMs {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 write!(f, "{}", self.0)
126 }
127}
128
129impl FromStr for TimestampMs {
130 type Err = std::num::ParseIntError;
131 fn from_str(s: &str) -> Result<Self, Self::Err> {
132 s.parse::<u64>().map(Self)
133 }
134}
135
136impl Add<DurationMs> for TimestampMs {
137 type Output = TimestampMs;
138 fn add(self, rhs: DurationMs) -> Self::Output {
139 TimestampMs(self.0.saturating_add(rhs.0))
140 }
141}
142
143impl Sub<DurationMs> for TimestampMs {
144 type Output = TimestampMs;
145 fn sub(self, rhs: DurationMs) -> Self::Output {
146 TimestampMs(self.0.saturating_sub(rhs.0))
147 }
148}
149
150impl Sub<TimestampMs> for TimestampMs {
151 type Output = DurationMs;
152 fn sub(self, rhs: TimestampMs) -> Self::Output {
153 DurationMs(self.0.saturating_sub(rhs.0))
154 }
155}
156
157#[derive(
159 BorshSerialize,
160 BorshDeserialize,
161 Clone,
162 Copy,
163 Debug,
164 Default,
165 Eq,
166 Hash,
167 Ord,
168 PartialEq,
169 PartialOrd,
170 Serialize,
171 Deserialize,
172)]
173#[serde(transparent)]
174#[repr(transparent)]
175pub struct DurationMs(u64);
176
177impl DurationMs {
178 pub const ZERO: Self = Self(0);
180 pub const MAX: Self = Self(u64::MAX);
182
183 pub const fn new(millis: u64) -> Self {
185 Self(millis)
186 }
187
188 pub const fn from_secs(secs: u64) -> Self {
190 Self(secs.saturating_mul(1000))
191 }
192
193 pub const fn as_millis(self) -> u64 {
195 self.0
196 }
197
198 pub const fn saturating_add(self, rhs: Self) -> Self {
200 Self(self.0.saturating_add(rhs.0))
201 }
202
203 pub const fn saturating_sub(self, rhs: Self) -> Self {
205 Self(self.0.saturating_sub(rhs.0))
206 }
207}
208
209impl From<u64> for DurationMs {
210 fn from(millis: u64) -> Self {
211 Self(millis)
212 }
213}
214
215impl From<DurationMs> for u64 {
216 fn from(span: DurationMs) -> Self {
217 span.0
218 }
219}
220
221impl From<DurationMs> for Duration {
222 fn from(span: DurationMs) -> Self {
223 Duration::from_millis(span.0)
224 }
225}
226
227impl fmt::Display for DurationMs {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 write!(f, "{}", self.0)
233 }
234}
235
236impl FromStr for DurationMs {
237 type Err = std::num::ParseIntError;
238 fn from_str(s: &str) -> Result<Self, Self::Err> {
239 s.parse::<u64>().map(Self)
240 }
241}
242
243impl Add for DurationMs {
244 type Output = DurationMs;
245 fn add(self, rhs: Self) -> Self::Output {
246 DurationMs(self.0.saturating_add(rhs.0))
247 }
248}
249
250impl Sub for DurationMs {
251 type Output = DurationMs;
252 fn sub(self, rhs: Self) -> Self::Output {
253 DurationMs(self.0.saturating_sub(rhs.0))
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn instant_plus_duration_is_instant() {
263 let t = TimestampMs::new(1_000);
264 assert_eq!(
265 t.saturating_add(DurationMs::new(500)),
266 TimestampMs::new(1_500)
267 );
268 assert_eq!(t + DurationMs::new(500), TimestampMs::new(1_500));
269 }
270
271 #[test]
272 fn instant_minus_instant_is_duration() {
273 let a = TimestampMs::new(1_500);
274 let b = TimestampMs::new(1_000);
275 assert_eq!(a.saturating_duration_since(b), DurationMs::new(500));
276 assert_eq!(a - b, DurationMs::new(500));
277 }
278
279 #[test]
280 fn saturation_clamps() {
281 assert_eq!(
282 TimestampMs::ZERO.saturating_sub(DurationMs::new(1)),
283 TimestampMs::ZERO
284 );
285 assert_eq!(
286 TimestampMs::MAX.saturating_add(DurationMs::new(1)),
287 TimestampMs::MAX
288 );
289 assert_eq!(
290 TimestampMs::new(1).saturating_duration_since(TimestampMs::new(5)),
291 DurationMs::ZERO
292 );
293 }
294
295 #[test]
296 fn serde_is_transparent() {
297 let t = TimestampMs::new(42);
298 assert_eq!(serde_json::to_string(&t).unwrap(), "42");
299 let d: DurationMs = serde_json::from_str("7").unwrap();
300 assert_eq!(d, DurationMs::new(7));
301 }
302
303 #[test]
304 fn borsh_roundtrips() {
305 let t = TimestampMs::new(123);
306 let bytes = borsh::to_vec(&t).unwrap();
307 assert_eq!(bytes, 123u64.to_le_bytes());
308 assert_eq!(borsh::from_slice::<TimestampMs>(&bytes).unwrap(), t);
309 }
310
311 #[test]
316 fn bcs_matches_u64() {
317 for raw in [0u64, 1, 42, 0xdead_beef, u64::MAX] {
318 assert_eq!(
319 bcs::to_bytes(&TimestampMs::new(raw)).unwrap(),
320 bcs::to_bytes(&raw).unwrap()
321 );
322 assert_eq!(
323 bcs::to_bytes(&DurationMs::new(raw)).unwrap(),
324 bcs::to_bytes(&raw).unwrap()
325 );
326 assert_eq!(
327 bcs::from_bytes::<TimestampMs>(&bcs::to_bytes(&raw).unwrap()).unwrap(),
328 TimestampMs::new(raw)
329 );
330 }
331 }
332
333 #[test]
336 fn bincode_matches_u64() {
337 use bincode::Options;
338 for raw in [0u64, 1, 42, u64::MAX] {
339 assert_eq!(
340 bincode::serialize(&TimestampMs::new(raw)).unwrap(),
341 bincode::serialize(&raw).unwrap()
342 );
343 }
344 let cfg = bincode::DefaultOptions::new()
345 .with_big_endian()
346 .with_fixint_encoding();
347 assert_eq!(
348 cfg.serialize(&TimestampMs::new(42)).unwrap(),
349 cfg.serialize(&42u64).unwrap()
350 );
351 }
352}