mz_ore/time.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Timing related extensions.
17
18use std::time::Duration;
19
20use num::Zero;
21
22/// Generic Error type returned from methods on [`DurationExt`].
23#[derive(Copy, Clone, Debug)]
24pub struct DurationError(#[allow(dead_code)] &'static str);
25
26/// Extensions for [`std::time::Duration`].
27pub trait DurationExt {
28 /// Creates a [`Duration`] from the specified number of seconds represented as an `i64`.
29 ///
30 /// Returns an error if the provided number of seconds is negative.
31 ///
32 /// ```
33 /// use std::time::Duration;
34 /// use mz_ore::time::DurationExt;
35 ///
36 /// // Negative numbers will fail.
37 /// assert!(Duration::try_from_secs_i64(-100).is_err());
38 ///
39 /// // i64 max works.
40 /// assert!(Duration::try_from_secs_i64(i64::MAX).is_ok());
41 /// ```
42 fn try_from_secs_i64(secs: i64) -> Result<Duration, DurationError>;
43
44 /// Saturating `Duration` multiplication. Computes `self * rhs`, saturating at the numeric
45 /// bounds instead of overflowing.
46 ///
47 /// ```
48 /// use std::time::Duration;
49 /// use mz_ore::time::DurationExt;
50 ///
51 /// let one = Duration::from_secs(1);
52 /// assert_eq!(one.saturating_mul_f64(f64::INFINITY), Duration::from_secs(u64::MAX));
53 ///
54 /// assert_eq!(one.saturating_mul_f64(f64::NEG_INFINITY), Duration::from_secs(0));
55 /// assert_eq!(one.saturating_mul_f64(f64::NAN), Duration::from_secs(0));
56 /// assert_eq!(one.saturating_mul_f64(-0.0), Duration::from_secs(0));
57 /// assert_eq!(one.saturating_mul_f64(0.0), Duration::from_secs(0));
58 ///
59 /// assert_eq!(Duration::from_secs(20).saturating_mul_f64(0.1), Duration::from_secs(2));
60 /// ```
61 fn saturating_mul_f64(&self, rhs: f64) -> Duration;
62}
63
64impl DurationExt for Duration {
65 fn try_from_secs_i64(secs: i64) -> Result<Duration, DurationError> {
66 let secs: u64 = secs
67 .try_into()
68 .map_err(|_| DurationError("negative number of seconds"))?;
69 Ok(Duration::from_secs(secs))
70 }
71
72 fn saturating_mul_f64(&self, rhs: f64) -> Duration {
73 let x = self.as_secs_f64() * rhs;
74 let bound = if x.is_sign_negative() || x.is_nan() || x.is_zero() {
75 u64::MIN
76 } else {
77 u64::MAX
78 };
79 Duration::try_from_secs_f64(x).unwrap_or(Duration::from_secs(bound))
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::DurationExt;
86 use proptest::prelude::*;
87 use std::time::Duration;
88
89 const ONE: Duration = Duration::from_secs(1);
90
91 proptest! {
92 #[crate::test]
93 fn proptest_saturating_mul_f64(rhs: f64) {
94 // Saturating multiplication should never panic.
95 let _ = ONE.saturating_mul_f64(rhs);
96 }
97 }
98}