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#[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 u128::from(self.0) * 1000
47 }
48
49 #[inline]
50 pub fn as_nanos_i128(self) -> i128 {
51 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 #[inline]
141 pub fn next_multiple_of(self, duration: DurationUs) -> anyhow::Result<TimestampUs> {
142 #[inline]
144 fn checked_next_multiple_of(lhs: u64, rhs: u64) -> Option<u64> {
145 match lhs.checked_rem(rhs)? {
146 0 => Some(lhs),
147 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 #[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(×tamp)
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 ProtobufTimestamp {
224 #[allow(clippy::cast_possible_wrap)]
225 seconds: (timestamp.0 / 1_000_000) as i64,
226 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#[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 u128::from(self.0) * 1000
313 }
314
315 #[inline]
316 pub fn as_nanos_i128(self) -> i128 {
317 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 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 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 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 #[allow(clippy::cast_possible_wrap)]
470 seconds: (duration.0 / 1_000_000) as i64,
471 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 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}