pyth_lazer_protocol/
time.rs

1#[cfg(test)]
2mod tests;
3
4use {
5    anyhow::Context,
6    protobuf::{
7        well_known_types::{
8            duration::Duration as ProtobufDuration, timestamp::Timestamp as ProtobufTimestamp,
9        },
10        MessageField,
11    },
12    serde::{Deserialize, Serialize},
13    std::time::{Duration, SystemTime},
14    utoipa::ToSchema,
15};
16
17/// Unix timestamp with microsecond resolution.
18#[derive(
19    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
20)]
21#[repr(transparent)]
22#[schema(value_type = u64)]
23pub struct TimestampUs(u64);
24
25#[cfg_attr(feature = "mry", mry::mry)]
26impl TimestampUs {
27    pub fn now() -> Self {
28        SystemTime::now().try_into().expect("invalid system time")
29    }
30}
31
32impl TimestampUs {
33    pub const UNIX_EPOCH: Self = Self(0);
34    pub const MAX: Self = Self(u64::MAX);
35
36    #[inline]
37    pub const fn from_micros(micros: u64) -> Self {
38        Self(micros)
39    }
40
41    #[inline]
42    pub const fn as_micros(self) -> u64 {
43        self.0
44    }
45
46    #[inline]
47    pub fn as_nanos(self) -> u128 {
48        // never overflows
49        u128::from(self.0) * 1000
50    }
51
52    #[inline]
53    pub fn as_nanos_i128(self) -> i128 {
54        // never overflows
55        i128::from(self.0) * 1000
56    }
57
58    #[inline]
59    pub fn from_nanos(nanos: u128) -> anyhow::Result<Self> {
60        let micros = nanos
61            .checked_div(1000)
62            .context("nanos.checked_div(1000) failed")?;
63        Ok(Self::from_micros(micros.try_into()?))
64    }
65
66    #[inline]
67    pub fn from_nanos_i128(nanos: i128) -> anyhow::Result<Self> {
68        let micros = nanos
69            .checked_div(1000)
70            .context("nanos.checked_div(1000) failed")?;
71        Ok(Self::from_micros(micros.try_into()?))
72    }
73
74    #[inline]
75    pub fn as_millis(self) -> u64 {
76        self.0 / 1000
77    }
78
79    #[inline]
80    pub fn from_millis(millis: u64) -> anyhow::Result<Self> {
81        let micros = millis
82            .checked_mul(1000)
83            .context("millis.checked_mul(1000) failed")?;
84        Ok(Self::from_micros(micros))
85    }
86
87    #[inline]
88    pub fn as_secs(self) -> u64 {
89        self.0 / 1_000_000
90    }
91
92    #[inline]
93    pub fn from_secs(secs: u64) -> anyhow::Result<Self> {
94        let micros = secs
95            .checked_mul(1_000_000)
96            .context("secs.checked_mul(1_000_000) failed")?;
97        Ok(Self::from_micros(micros))
98    }
99
100    #[inline]
101    pub fn duration_since(self, other: Self) -> anyhow::Result<DurationUs> {
102        Ok(DurationUs(
103            self.0
104                .checked_sub(other.0)
105                .context("timestamp.checked_sub(duration) failed")?,
106        ))
107    }
108
109    #[inline]
110    pub fn saturating_duration_since(self, other: Self) -> DurationUs {
111        DurationUs(self.0.saturating_sub(other.0))
112    }
113
114    #[inline]
115    pub fn elapsed(self) -> anyhow::Result<DurationUs> {
116        Self::now().duration_since(self)
117    }
118
119    #[inline]
120    pub fn saturating_elapsed(self) -> DurationUs {
121        Self::now().saturating_duration_since(self)
122    }
123
124    #[inline]
125    pub fn saturating_add(self, duration: DurationUs) -> TimestampUs {
126        TimestampUs(self.0.saturating_add(duration.0))
127    }
128
129    #[inline]
130    pub fn saturating_sub(self, duration: DurationUs) -> TimestampUs {
131        TimestampUs(self.0.saturating_sub(duration.0))
132    }
133
134    #[inline]
135    pub fn is_multiple_of(self, duration: DurationUs) -> bool {
136        match self.0.checked_rem(duration.0) {
137            Some(rem) => rem == 0,
138            None => true,
139        }
140    }
141
142    /// Calculates the smallest value greater than or equal to self that is a multiple of `duration`.
143    #[inline]
144    pub fn next_multiple_of(self, duration: DurationUs) -> anyhow::Result<TimestampUs> {
145        Ok(TimestampUs(
146            self.0
147                .checked_next_multiple_of(duration.0)
148                .context("checked_next_multiple_of failed")?,
149        ))
150    }
151
152    /// Calculates the smallest value less than or equal to self that is a multiple of `duration`.
153    #[inline]
154    pub fn previous_multiple_of(self, duration: DurationUs) -> anyhow::Result<TimestampUs> {
155        Ok(TimestampUs(
156            self.0
157                .checked_div(duration.0)
158                .context("checked_div failed")?
159                .checked_mul(duration.0)
160                .context("checked_mul failed")?,
161        ))
162    }
163
164    #[inline]
165    pub fn checked_add(self, duration: DurationUs) -> anyhow::Result<Self> {
166        Ok(TimestampUs(
167            self.0
168                .checked_add(duration.0)
169                .context("checked_add failed")?,
170        ))
171    }
172
173    #[inline]
174    pub fn checked_sub(self, duration: DurationUs) -> anyhow::Result<Self> {
175        Ok(TimestampUs(
176            self.0
177                .checked_sub(duration.0)
178                .context("checked_sub failed")?,
179        ))
180    }
181}
182
183impl TryFrom<ProtobufTimestamp> for TimestampUs {
184    type Error = anyhow::Error;
185
186    #[inline]
187    fn try_from(timestamp: ProtobufTimestamp) -> anyhow::Result<Self> {
188        TryFrom::<&ProtobufTimestamp>::try_from(&timestamp)
189    }
190}
191
192impl TryFrom<&ProtobufTimestamp> for TimestampUs {
193    type Error = anyhow::Error;
194
195    fn try_from(timestamp: &ProtobufTimestamp) -> anyhow::Result<Self> {
196        let seconds_in_micros: u64 = timestamp
197            .seconds
198            .checked_mul(1_000_000)
199            .context("checked_mul failed")?
200            .try_into()?;
201        let nanos_in_micros: u64 = timestamp
202            .nanos
203            .checked_div(1_000)
204            .context("checked_div failed")?
205            .try_into()?;
206        Ok(TimestampUs(
207            seconds_in_micros
208                .checked_add(nanos_in_micros)
209                .context("checked_add failed")?,
210        ))
211    }
212}
213
214impl From<TimestampUs> for ProtobufTimestamp {
215    fn from(timestamp: TimestampUs) -> Self {
216        // u64 to i64 after this division can never overflow because the value cannot be too big
217        ProtobufTimestamp {
218            #[allow(clippy::cast_possible_wrap)]
219            seconds: (timestamp.0 / 1_000_000) as i64,
220            // never fails, never overflows
221            nanos: (timestamp.0 % 1_000_000) as i32 * 1000,
222            special_fields: Default::default(),
223        }
224    }
225}
226
227impl From<TimestampUs> for MessageField<ProtobufTimestamp> {
228    #[inline]
229    fn from(value: TimestampUs) -> Self {
230        MessageField::some(value.into())
231    }
232}
233
234impl TryFrom<SystemTime> for TimestampUs {
235    type Error = anyhow::Error;
236
237    fn try_from(value: SystemTime) -> Result<Self, Self::Error> {
238        let value = value
239            .duration_since(SystemTime::UNIX_EPOCH)
240            .context("invalid system time")?
241            .as_micros()
242            .try_into()?;
243        Ok(Self(value))
244    }
245}
246
247impl TryFrom<TimestampUs> for SystemTime {
248    type Error = anyhow::Error;
249
250    fn try_from(value: TimestampUs) -> Result<Self, Self::Error> {
251        SystemTime::UNIX_EPOCH
252            .checked_add(Duration::from_micros(value.as_micros()))
253            .context("checked_add failed")
254    }
255}
256
257impl TryFrom<&chrono::DateTime<chrono::Utc>> for TimestampUs {
258    type Error = anyhow::Error;
259
260    #[inline]
261    fn try_from(value: &chrono::DateTime<chrono::Utc>) -> Result<Self, Self::Error> {
262        Ok(Self(value.timestamp_micros().try_into()?))
263    }
264}
265
266impl TryFrom<chrono::DateTime<chrono::Utc>> for TimestampUs {
267    type Error = anyhow::Error;
268
269    #[inline]
270    fn try_from(value: chrono::DateTime<chrono::Utc>) -> Result<Self, Self::Error> {
271        TryFrom::<&chrono::DateTime<chrono::Utc>>::try_from(&value)
272    }
273}
274
275impl TryFrom<TimestampUs> for chrono::DateTime<chrono::Utc> {
276    type Error = anyhow::Error;
277
278    #[inline]
279    fn try_from(value: TimestampUs) -> Result<Self, Self::Error> {
280        chrono::DateTime::<chrono::Utc>::from_timestamp_micros(value.as_micros().try_into()?)
281            .with_context(|| format!("cannot convert timestamp to datetime: {value:?}"))
282    }
283}
284
285/// Non-negative duration with microsecond resolution.
286#[derive(
287    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
288)]
289#[schema(value_type = u64)]
290pub struct DurationUs(u64);
291
292impl DurationUs {
293    pub const ZERO: Self = Self(0);
294
295    #[inline]
296    pub const fn from_micros(micros: u64) -> Self {
297        Self(micros)
298    }
299
300    #[inline]
301    pub const fn as_micros(self) -> u64 {
302        self.0
303    }
304
305    #[inline]
306    pub fn as_nanos(self) -> u128 {
307        // never overflows
308        u128::from(self.0) * 1000
309    }
310
311    #[inline]
312    pub fn as_nanos_i128(self) -> i128 {
313        // never overflows
314        i128::from(self.0) * 1000
315    }
316
317    #[inline]
318    pub fn from_nanos(nanos: u128) -> anyhow::Result<Self> {
319        let micros = nanos.checked_div(1000).context("checked_div failed")?;
320        Ok(Self::from_micros(micros.try_into()?))
321    }
322
323    #[inline]
324    pub fn as_millis(self) -> u64 {
325        self.0 / 1000
326    }
327
328    #[inline]
329    pub const fn from_millis_u32(millis: u32) -> Self {
330        // never overflows
331        Self((millis as u64) * 1_000)
332    }
333
334    #[inline]
335    pub fn from_millis(millis: u64) -> anyhow::Result<Self> {
336        let micros = millis
337            .checked_mul(1000)
338            .context("millis.checked_mul(1000) failed")?;
339        Ok(Self::from_micros(micros))
340    }
341
342    #[inline]
343    pub fn as_secs(self) -> u64 {
344        self.0 / 1_000_000
345    }
346
347    #[inline]
348    pub const fn from_secs_u32(secs: u32) -> Self {
349        // never overflows
350        Self((secs as u64) * 1_000_000)
351    }
352
353    #[inline]
354    pub fn from_secs(secs: u64) -> anyhow::Result<Self> {
355        let micros = secs
356            .checked_mul(1_000_000)
357            .context("secs.checked_mul(1_000_000) failed")?;
358        Ok(Self::from_micros(micros))
359    }
360
361    #[inline]
362    pub fn from_days_u16(days: u16) -> Self {
363        // never overflows
364        Self((days as u64) * 24 * 3600 * 1_000_000)
365    }
366
367    #[inline]
368    pub fn is_multiple_of(self, other: DurationUs) -> bool {
369        match self.0.checked_rem(other.0) {
370            Some(rem) => rem == 0,
371            None => true,
372        }
373    }
374
375    #[inline]
376    pub const fn is_zero(self) -> bool {
377        self.0 == 0
378    }
379
380    #[inline]
381    pub const fn is_positive(self) -> bool {
382        self.0 > 0
383    }
384
385    #[inline]
386    pub fn checked_add(self, other: DurationUs) -> anyhow::Result<Self> {
387        Ok(DurationUs(
388            self.0.checked_add(other.0).context("checked_add failed")?,
389        ))
390    }
391
392    #[inline]
393    pub fn checked_sub(self, other: DurationUs) -> anyhow::Result<Self> {
394        Ok(DurationUs(
395            self.0.checked_sub(other.0).context("checked_sub failed")?,
396        ))
397    }
398
399    #[inline]
400    pub fn checked_mul(self, n: u64) -> anyhow::Result<DurationUs> {
401        Ok(DurationUs(
402            self.0.checked_mul(n).context("checked_mul failed")?,
403        ))
404    }
405
406    #[inline]
407    pub fn checked_div(self, n: u64) -> anyhow::Result<DurationUs> {
408        Ok(DurationUs(
409            self.0.checked_div(n).context("checked_div failed")?,
410        ))
411    }
412}
413
414impl From<DurationUs> for Duration {
415    #[inline]
416    fn from(value: DurationUs) -> Self {
417        Duration::from_micros(value.as_micros())
418    }
419}
420
421impl TryFrom<Duration> for DurationUs {
422    type Error = anyhow::Error;
423
424    #[inline]
425    fn try_from(value: Duration) -> Result<Self, Self::Error> {
426        Ok(Self(value.as_micros().try_into()?))
427    }
428}
429
430impl TryFrom<ProtobufDuration> for DurationUs {
431    type Error = anyhow::Error;
432
433    #[inline]
434    fn try_from(duration: ProtobufDuration) -> anyhow::Result<Self> {
435        TryFrom::<&ProtobufDuration>::try_from(&duration)
436    }
437}
438
439impl TryFrom<&ProtobufDuration> for DurationUs {
440    type Error = anyhow::Error;
441
442    fn try_from(duration: &ProtobufDuration) -> anyhow::Result<Self> {
443        let seconds_in_micros: u64 = duration
444            .seconds
445            .checked_mul(1_000_000)
446            .context("checked_mul failed")?
447            .try_into()?;
448        let nanos_in_micros: u64 = duration
449            .nanos
450            .checked_div(1_000)
451            .context("nanos.checked_div(1_000) failed")?
452            .try_into()?;
453        Ok(DurationUs(
454            seconds_in_micros
455                .checked_add(nanos_in_micros)
456                .context("checked_add failed")?,
457        ))
458    }
459}
460
461impl From<DurationUs> for ProtobufDuration {
462    fn from(duration: DurationUs) -> Self {
463        ProtobufDuration {
464            // u64 to i64 after this division can never overflow because the value cannot be too big
465            #[allow(clippy::cast_possible_wrap)]
466            seconds: (duration.0 / 1_000_000) as i64,
467            // never fails, never overflows
468            nanos: (duration.0 % 1_000_000) as i32 * 1000,
469            special_fields: Default::default(),
470        }
471    }
472}
473
474pub mod duration_us_serde_humantime {
475    use std::time::Duration;
476
477    use serde::{de::Error, Deserialize, Serialize};
478
479    use crate::time::DurationUs;
480
481    pub fn serialize<S>(value: &DurationUs, serializer: S) -> Result<S::Ok, S::Error>
482    where
483        S: serde::Serializer,
484    {
485        humantime_serde::Serde::from(Duration::from(*value)).serialize(serializer)
486    }
487
488    pub fn deserialize<'de, D>(deserializer: D) -> Result<DurationUs, D::Error>
489    where
490        D: serde::Deserializer<'de>,
491    {
492        let value = humantime_serde::Serde::<Duration>::deserialize(deserializer)?;
493        value.into_inner().try_into().map_err(D::Error::custom)
494    }
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, ToSchema)]
498#[schema(as = String, example = "fixed_rate@200ms")]
499pub struct FixedRate {
500    rate: DurationUs,
501}
502
503impl FixedRate {
504    pub const RATE_50_MS: Self = Self {
505        rate: DurationUs::from_millis_u32(50),
506    };
507    pub const RATE_200_MS: Self = Self {
508        rate: DurationUs::from_millis_u32(200),
509    };
510    pub const RATE_1000_MS: Self = Self {
511        rate: DurationUs::from_millis_u32(1000),
512    };
513
514    // Assumptions (tested below):
515    // - Values are sorted.
516    // - 1 second contains a whole number of each interval.
517    // - all intervals are divisable by the smallest interval.
518    pub const ALL: [Self; 3] = [Self::RATE_50_MS, Self::RATE_200_MS, Self::RATE_1000_MS];
519    pub const MIN: Self = Self::ALL[0];
520
521    pub fn from_millis(millis: u32) -> Option<Self> {
522        Self::ALL
523            .into_iter()
524            .find(|v| v.rate.as_millis() == u64::from(millis))
525    }
526
527    pub fn duration(self) -> DurationUs {
528        self.rate
529    }
530}
531
532impl TryFrom<DurationUs> for FixedRate {
533    type Error = anyhow::Error;
534
535    fn try_from(value: DurationUs) -> Result<Self, Self::Error> {
536        Self::ALL
537            .into_iter()
538            .find(|v| v.rate == value)
539            .with_context(|| format!("unsupported rate: {value:?}"))
540    }
541}
542
543impl TryFrom<&ProtobufDuration> for FixedRate {
544    type Error = anyhow::Error;
545
546    fn try_from(value: &ProtobufDuration) -> Result<Self, Self::Error> {
547        let duration = DurationUs::try_from(value)?;
548        Self::try_from(duration)
549    }
550}
551
552impl TryFrom<ProtobufDuration> for FixedRate {
553    type Error = anyhow::Error;
554
555    fn try_from(duration: ProtobufDuration) -> anyhow::Result<Self> {
556        TryFrom::<&ProtobufDuration>::try_from(&duration)
557    }
558}
559
560impl From<FixedRate> for DurationUs {
561    fn from(value: FixedRate) -> Self {
562        value.rate
563    }
564}
565
566impl From<FixedRate> for ProtobufDuration {
567    fn from(value: FixedRate) -> Self {
568        value.rate.into()
569    }
570}
571
572#[test]
573fn fixed_rate_values() {
574    assert!(
575        FixedRate::ALL.windows(2).all(|w| w[0] < w[1]),
576        "values must be unique and sorted"
577    );
578    for value in FixedRate::ALL {
579        assert_eq!(
580            1_000_000 % value.duration().as_micros(),
581            0,
582            "1 s must contain whole number of intervals"
583        );
584        assert_eq!(
585            value.duration().as_micros() % FixedRate::MIN.duration().as_micros(),
586            0,
587            "the interval's borders must be a subset of the minimal interval's borders"
588        );
589    }
590}