systemd_duration/
duration.rs

1// SPDX-License-Identifier: CC0-1.0
2//
3// This file is part of systemd-duration.
4//
5// To the extent possible under law, the author(s) have dedicated all copyright
6// and related and neighboring rights to this software to the public domain
7// worldwide. This software is distributed without any warranty.
8//
9// You should have received a copy of the CC0 Public Domain Dedication along
10// with this software. If not, see <https://creativecommons.org/publicdomain/zero/1.0/>.
11
12use std::convert::TryFrom;
13
14use crate::error;
15
16/// A measurement of a given span of time.
17#[derive(Copy, Clone, Debug)]
18pub enum Duration {
19    Year(f64),
20    Month(f64),
21    Week(f64),
22    Day(f64),
23    Hour(f64),
24    Minute(f64),
25    Second(f64),
26    Millisecond(f64),
27    Microsecond(f64),
28    Nanosecond(i64),
29}
30
31/// A container of durations, which when summed give the total duration.
32#[derive(Clone, Debug)]
33pub struct Container(Vec<Duration>);
34
35impl Container {
36    /// Create a new container object from the given durations.
37    #[must_use]
38    pub const fn new(durations: Vec<Duration>) -> Self {
39        Self(durations)
40    }
41}
42
43/// Just a place to shove conversion factors.
44#[allow(clippy::module_name_repetitions)]
45struct Convert;
46
47// Systemd uses 365.25 (Julian average) which has an error of 0.0075 days per year relative to the
48// Gregorian calendar, or about one in every 133⅓ years.
49//
50// For the durations systemd deals with, this is not a practical issue in reality. However,
51// because the deviation is small, there's no harm in being more accurate vs. being "incompatible."
52impl Convert {
53    const SECS_PER_MIN: f64 = 60.0;
54    const SECS_PER_HOUR: f64 = 60.0 * Self::SECS_PER_MIN;
55    const SECS_PER_DAY: f64 = 24.0 * Self::SECS_PER_HOUR;
56    const SECS_PER_WEEK: f64 = 7.0 * Self::SECS_PER_DAY;
57    const SECS_PER_MONTH: f64 = 30.436_875_f64 * Self::SECS_PER_DAY;
58    const SECS_PER_YEAR: f64 = 365.242_5_f64 * Self::SECS_PER_DAY;
59    const NANOS_PER_SEC: f64 = 1_000_000_000.0;
60    const NANOS_PER_MILLI: f64 = Self::NANOS_PER_SEC / 1_000.0;
61    const NANOS_PER_MICRO: f64 = Self::NANOS_PER_MILLI / 1_000.0;
62}
63
64/// Conversions from [`Duration`] to [`std::time::Duration`]
65pub mod stdtime {
66    use super::{error, Container, Convert, Duration, TryFrom};
67
68    macro_rules! duration_ge_second {
69        ($secs_per_interval:expr, $count:expr) => {{
70            let sign = ($count).signum();
71            if sign <= -1.0 || sign.is_nan() {
72                return Err(error::Error::DurationOverflow);
73            }
74
75            ::std::time::Duration::from_secs_f64(($secs_per_interval) * ($count))
76        }};
77    }
78
79    macro_rules! duration_lt_second {
80        ($nanos_per_interval:expr, $count:expr) => {{
81            let nanos: f64 = ($nanos_per_interval) * ($count);
82            if !nanos.is_finite() {
83                return Err(error::Error::DurationOverflow);
84            }
85
86            let rounded = nanos.round();
87            #[allow(clippy::cast_possible_truncation)]
88            let int_nanos = rounded as i64;
89
90            // Ensure the conversion didn't silently overflow or truncate
91            #[allow(clippy::cast_precision_loss)]
92            if (int_nanos as f64 - rounded).abs() > f64::EPSILON {
93                return Err(error::Error::DurationOverflow);
94            }
95
96            match u64::try_from(int_nanos) {
97                Ok(valid) => ::std::time::Duration::from_nanos(valid),
98                Err(_) => return Err(error::Error::DurationOverflow),
99            }
100        }};
101    }
102
103    impl TryFrom<Container> for std::time::Duration {
104        type Error = error::Error;
105
106        /// Convert a [`Duration`] into an [`std::time::Duration`]
107        fn try_from(durations: Container) -> Result<Self, Self::Error> {
108            let mut duration_sum = Self::new(0, 0);
109
110            for duration in &durations.0 {
111                duration_sum += match duration {
112                    Duration::Year(count) => {
113                        duration_ge_second!(Convert::SECS_PER_YEAR, count)
114                    }
115                    Duration::Month(count) => {
116                        duration_ge_second!(Convert::SECS_PER_MONTH, count)
117                    }
118                    Duration::Week(count) => {
119                        duration_ge_second!(Convert::SECS_PER_WEEK, count)
120                    }
121                    Duration::Day(count) => {
122                        duration_ge_second!(Convert::SECS_PER_DAY, count)
123                    }
124                    Duration::Hour(count) => {
125                        duration_ge_second!(Convert::SECS_PER_HOUR, count)
126                    }
127                    Duration::Minute(count) => {
128                        duration_ge_second!(Convert::SECS_PER_MIN, count)
129                    }
130                    Duration::Second(count) => duration_ge_second!(1.0, count),
131                    Duration::Millisecond(count) => {
132                        duration_lt_second!(Convert::NANOS_PER_MILLI, count)
133                    }
134                    Duration::Microsecond(count) => {
135                        duration_lt_second!(Convert::NANOS_PER_MICRO, count)
136                    }
137                    Duration::Nanosecond(count) => {
138                        if *count < 0 {
139                            return Err(error::Error::DurationOverflow);
140                        }
141
142                        // Checked above
143                        #[allow(clippy::cast_sign_loss)]
144                        Self::from_nanos(*count as u64)
145                    }
146                }
147            }
148
149            Ok(duration_sum)
150        }
151    }
152}
153
154/// Conversions from [`Duration`] into [`chrono::TimeDelta`][::chrono::TimeDelta]
155#[cfg(feature = "with-chrono")]
156pub mod chrono {
157    use super::{error, Container, Convert, Duration, TryFrom};
158
159    macro_rules! duration_ge_second {
160        ($secs_per_interval:expr, $count:expr) => {{
161            let seconds = ($secs_per_interval) * ($count);
162            if seconds.is_infinite() || seconds > i64::MAX as f64 || seconds < i64::MIN as f64 {
163                return Err(error::Error::DurationOverflow);
164            }
165            let (seconds, nanos) = (
166                seconds.trunc(),
167                (seconds - seconds.trunc()) * Convert::NANOS_PER_SEC,
168            );
169            ::chrono::TimeDelta::new(seconds as i64, nanos as u32).unwrap()
170        }};
171    }
172
173    macro_rules! duration_lt_second {
174        ($nanos_per_interval:expr, $count:expr) => {{
175            let nanos = ($nanos_per_interval) * ($count);
176            if nanos.is_infinite() || nanos > i64::MAX as f64 || nanos < i64::MIN as f64 {
177                return Err(error::Error::DurationOverflow);
178            }
179            ::chrono::TimeDelta::nanoseconds(nanos.round() as i64)
180        }};
181    }
182
183    impl TryFrom<Container> for ::chrono::TimeDelta {
184        type Error = error::Error;
185
186        /// Convert a [`Duration`] into a [`::chrono::TimeDelta`]
187        fn try_from(durations: Container) -> Result<Self, Self::Error> {
188            let mut duration_sum = Self::new(0, 0).unwrap();
189            for duration in &durations.0 {
190                duration_sum += match duration {
191                    Duration::Year(count) => {
192                        duration_ge_second!(Convert::SECS_PER_YEAR, count)
193                    }
194                    Duration::Month(count) => {
195                        duration_ge_second!(Convert::SECS_PER_MONTH, count)
196                    }
197                    Duration::Week(count) => {
198                        duration_ge_second!(Convert::SECS_PER_WEEK, count)
199                    }
200                    Duration::Day(count) => {
201                        duration_ge_second!(Convert::SECS_PER_DAY, count)
202                    }
203                    Duration::Hour(count) => {
204                        duration_ge_second!(Convert::SECS_PER_HOUR, count)
205                    }
206                    Duration::Minute(count) => {
207                        duration_ge_second!(Convert::SECS_PER_MIN, count)
208                    }
209                    Duration::Second(count) => duration_ge_second!(1.0f64, count),
210                    Duration::Millisecond(count) => {
211                        duration_lt_second!(Convert::NANOS_PER_MILLI, count)
212                    }
213                    Duration::Microsecond(count) => {
214                        duration_lt_second!(Convert::NANOS_PER_MICRO, count)
215                    }
216                    Duration::Nanosecond(count) => Self::nanoseconds(*count),
217                };
218            }
219
220            Ok(duration_sum)
221        }
222    }
223}
224
225/// Conversions from [`Duration`] into [`::time::Duration`]
226#[cfg(feature = "with-time")]
227pub mod time {
228    use super::{error, Container, Convert, Duration, TryFrom};
229
230    macro_rules! duration_ge_second {
231        ($secs_per_interval:expr, $count:expr) => {{
232            ::time::Duration::checked_seconds_f64(($secs_per_interval) * ($count))
233                .ok_or(error::Error::DurationOverflow)?
234        }};
235    }
236
237    macro_rules! duration_lt_second {
238        ($nanos_per_interval:expr, $count:expr) => {{
239            let nanos = ($nanos_per_interval) * ($count);
240            if nanos.is_infinite() || nanos > i64::MAX as f64 || nanos < i64::MIN as f64 {
241                return Err(error::Error::DurationOverflow);
242            }
243            ::time::Duration::nanoseconds(nanos.round() as i64)
244        }};
245    }
246
247    /// Convert a [`Duration`] into a [`::time::Duration`]
248    impl TryFrom<Container> for ::time::Duration {
249        type Error = error::Error;
250
251        fn try_from(durations: Container) -> Result<Self, Self::Error> {
252            let mut duration_sum = Self::new(0, 0);
253
254            for duration in &durations.0 {
255                duration_sum += match duration {
256                    Duration::Year(count) => {
257                        duration_ge_second!(Convert::SECS_PER_YEAR, count)
258                    }
259                    Duration::Month(count) => {
260                        duration_ge_second!(Convert::SECS_PER_MONTH, count)
261                    }
262                    Duration::Week(count) => {
263                        duration_ge_second!(Convert::SECS_PER_WEEK, count)
264                    }
265                    Duration::Day(count) => {
266                        duration_ge_second!(Convert::SECS_PER_DAY, count)
267                    }
268                    Duration::Hour(count) => {
269                        duration_ge_second!(Convert::SECS_PER_HOUR, count)
270                    }
271                    Duration::Minute(count) => {
272                        duration_ge_second!(Convert::SECS_PER_MIN, count)
273                    }
274                    Duration::Second(count) => duration_ge_second!(1.0, count),
275                    Duration::Millisecond(count) => {
276                        duration_lt_second!(Convert::NANOS_PER_MILLI, count)
277                    }
278                    Duration::Microsecond(count) => {
279                        duration_lt_second!(Convert::NANOS_PER_MICRO, count)
280                    }
281                    Duration::Nanosecond(count) => Self::nanoseconds(*count),
282                }
283            }
284
285            Ok(duration_sum)
286        }
287    }
288}