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