Skip to main content

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