Skip to main content

bel/
duration.rs

1//! A signed duration type for the expression language.
2//!
3//! The standard [`core::time::Duration`] is unsigned, but BEL's duration
4//! arithmetic (subtraction, negation) needs signed values. This type stores a
5//! signed count of nanoseconds in an `i128`, which is far larger than any
6//! duration the language needs.
7
8use core::ops::{Add, Mul, Neg, Sub};
9
10use nom::{
11    IResult, Parser, branch::alt, bytes::complete::tag, character::complete::char, combinator::opt, multi::many1,
12    number::complete::double,
13};
14
15const NANOS_PER_SECOND: i128 = 1_000_000_000;
16
17/// A signed duration with nanosecond precision.
18#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
19pub struct Duration {
20    nanos: i128,
21}
22
23impl Duration {
24    /// The zero duration.
25    pub const ZERO: Duration = Duration {
26        nanos: 0,
27    };
28
29    /// Creates a duration from a signed number of nanoseconds.
30    #[must_use]
31    pub const fn from_nanos(nanos: i128) -> Duration {
32        Duration {
33            nanos,
34        }
35    }
36
37    /// Creates a duration from a signed number of nanoseconds.
38    #[must_use]
39    pub const fn nanoseconds(nanos: i64) -> Duration {
40        Duration {
41            nanos: nanos as i128,
42        }
43    }
44
45    /// Creates a duration from a signed number of microseconds.
46    #[must_use]
47    pub const fn microseconds(micros: i64) -> Duration {
48        Duration {
49            nanos: micros as i128 * 1_000,
50        }
51    }
52
53    /// Creates a duration from a signed number of milliseconds.
54    #[must_use]
55    pub const fn milliseconds(millis: i64) -> Duration {
56        Duration {
57            nanos: millis as i128 * 1_000_000,
58        }
59    }
60
61    /// Creates a duration from a signed number of seconds.
62    #[must_use]
63    pub const fn seconds(seconds: i64) -> Duration {
64        Duration {
65            nanos: seconds as i128 * NANOS_PER_SECOND,
66        }
67    }
68
69    /// Creates a duration from a signed number of minutes.
70    #[must_use]
71    pub const fn minutes(minutes: i64) -> Duration {
72        Duration {
73            nanos: minutes as i128 * 60 * NANOS_PER_SECOND,
74        }
75    }
76
77    /// Creates a duration from a signed number of hours.
78    #[must_use]
79    pub const fn hours(hours: i64) -> Duration {
80        Duration {
81            nanos: hours as i128 * 3600 * NANOS_PER_SECOND,
82        }
83    }
84
85    /// Returns the total number of nanoseconds.
86    #[must_use]
87    pub const fn as_nanos(self) -> i128 {
88        self.nanos
89    }
90
91    /// Returns the total number of nanoseconds, or `None` if it does not fit in
92    /// an `i64`.
93    #[must_use]
94    pub const fn num_nanoseconds(self) -> Option<i64> {
95        if self.nanos >= i64::MIN as i128 && self.nanos <= i64::MAX as i128 {
96            Some(self.nanos as i64)
97        } else {
98            None
99        }
100    }
101
102    /// Returns the number of whole seconds, truncated toward zero.
103    #[must_use]
104    pub const fn num_seconds(self) -> i64 {
105        (self.nanos / NANOS_PER_SECOND) as i64
106    }
107
108    /// Returns `true` if this duration is zero.
109    #[must_use]
110    pub const fn is_zero(self) -> bool {
111        self.nanos == 0
112    }
113
114    /// Returns `true` if this duration is negative.
115    #[must_use]
116    pub const fn is_negative(self) -> bool {
117        self.nanos < 0
118    }
119
120    /// Adds two durations, returning `None` on overflow.
121    #[must_use]
122    pub const fn checked_add(self, other: Duration) -> Option<Duration> {
123        match self.nanos.checked_add(other.nanos) {
124            Some(nanos) => Some(Duration {
125                nanos,
126            }),
127            None => None,
128        }
129    }
130
131    /// Subtracts two durations, returning `None` on overflow.
132    #[must_use]
133    pub const fn checked_sub(self, other: Duration) -> Option<Duration> {
134        match self.nanos.checked_sub(other.nanos) {
135            Some(nanos) => Some(Duration {
136                nanos,
137            }),
138            None => None,
139        }
140    }
141
142    /// Returns the whole seconds component used for serialization.
143    pub(crate) const fn serialized_secs(self) -> i64 {
144        (self.nanos / NANOS_PER_SECOND) as i64
145    }
146
147    /// Returns the sub-second nanosecond component used for serialization.
148    pub(crate) const fn serialized_nanos(self) -> i32 {
149        (self.nanos % NANOS_PER_SECOND) as i32
150    }
151
152    /// Rebuilds a duration from the serialized `(secs, nanos)` pair.
153    pub(crate) const fn from_secs_nanos(secs: i64, nanos: i32) -> Duration {
154        Duration {
155            nanos: secs as i128 * NANOS_PER_SECOND + nanos as i128,
156        }
157    }
158}
159
160impl core::fmt::Display for Duration {
161    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
162        f.write_str(&format_duration(self))
163    }
164}
165
166impl Add for Duration {
167    type Output = Duration;
168
169    fn add(self, rhs: Duration) -> Duration {
170        Duration {
171            nanos: self.nanos + rhs.nanos,
172        }
173    }
174}
175
176impl Sub for Duration {
177    type Output = Duration;
178
179    fn sub(self, rhs: Duration) -> Duration {
180        Duration {
181            nanos: self.nanos - rhs.nanos,
182        }
183    }
184}
185
186impl Mul<i32> for Duration {
187    type Output = Duration;
188
189    fn mul(self, rhs: i32) -> Duration {
190        Duration {
191            nanos: self.nanos * rhs as i128,
192        }
193    }
194}
195
196impl Neg for Duration {
197    type Output = Duration;
198
199    fn neg(self) -> Duration {
200        Duration {
201            nanos: -self.nanos,
202        }
203    }
204}
205
206/// Parses a duration string into a [`Duration`]. Duration strings support the
207/// following grammar:
208///
209/// DurationString -> Sign? Number Unit String?
210/// Sign           -> '-'
211/// Number         -> Digit+ ('.' Digit+)?
212/// Digit          -> '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
213/// Unit           -> 'h' | 'm' | 's' | 'ms' | 'us' | 'ns'
214/// String         -> DurationString
215///
216/// # Examples
217/// - `1h` parses as 1 hour
218/// - `1.5h` parses as 1 hour and 30 minutes
219/// - `1h30m` parses as 1 hour and 30 minutes
220/// - `1h30m1s` parses as 1 hour, 30 minutes, and 1 second
221/// - `1ms` parses as 1 millisecond
222/// - `1.5ms` parses as 1 millisecond and 500 microseconds
223/// - `1ns` parses as 1 nanosecond
224/// - `1.5ns` parses as 1 nanosecond (sub-nanosecond durations not supported)
225pub fn parse_duration(i: &str) -> IResult<&str, Duration> {
226    let (i, neg) = opt(parse_negative).parse(i)?;
227    if i == "0" {
228        return Ok((i, Duration::ZERO));
229    }
230    let (i, duration) = many1(parse_number_unit)
231        .parse(i)
232        .map(|(i, d)| (i, d.iter().fold(Duration::ZERO, |acc, next| acc + *next)))?;
233    Ok((i, if neg.is_some() { -duration } else { duration }))
234}
235
236enum Unit {
237    Nanosecond,
238    Microsecond,
239    Millisecond,
240    Second,
241    Minute,
242    Hour,
243}
244
245impl Unit {
246    fn nanos(&self) -> i64 {
247        match self {
248            Unit::Nanosecond => 1,
249            Unit::Microsecond => 1_000,
250            Unit::Millisecond => 1_000_000,
251            Unit::Second => 1_000_000_000,
252            Unit::Minute => 60 * 1_000_000_000,
253            Unit::Hour => 60 * 60 * 1_000_000_000,
254        }
255    }
256}
257
258fn parse_number_unit(i: &str) -> IResult<&str, Duration> {
259    let (i, num) = double(i)?;
260    let (i, unit) = parse_unit(i)?;
261    let duration = to_duration(num, unit);
262    Ok((i, duration))
263}
264
265fn parse_negative(i: &str) -> IResult<&str, ()> {
266    let (i, _): (&str, char) = char('-').parse(i)?;
267    Ok((i, ()))
268}
269
270fn parse_unit(i: &str) -> IResult<&str, Unit> {
271    alt((
272        tag("ms").map(|_| Unit::Millisecond),
273        tag("us").map(|_| Unit::Microsecond),
274        tag("ns").map(|_| Unit::Nanosecond),
275        char('h').map(|_| Unit::Hour),
276        char('m').map(|_| Unit::Minute),
277        char('s').map(|_| Unit::Second),
278    ))
279    .parse(i)
280}
281
282fn to_duration(num: f64, unit: Unit) -> Duration {
283    Duration::from_nanos((num * unit.nanos() as f64).trunc() as i128)
284}
285
286/// Formats a [`Duration`] into a string. The returned string represents the
287/// duration in the form "72h3m0.5s". Leading zero units are omitted. As a
288/// special case, durations less than one second format use a smaller unit
289/// (milli-, micro-, or nanoseconds) to ensure that the leading digit is
290/// non-zero. The zero duration formats as `0s`.
291///
292/// This is a direct port of the Go version of the `time.Duration.String()`
293/// function.
294pub fn format_duration(d: &Duration) -> String {
295    let buf = &mut [0u8; 32];
296    let mut w = buf.len();
297
298    let mut neg = false;
299    let mut u = d
300        .num_nanoseconds()
301        .map(|n| {
302            if n < 0 {
303                neg = true;
304            }
305            n as u64
306        })
307        .unwrap_or_else(|| {
308            let s = d.num_seconds();
309            if s < 0 {
310                neg = true;
311            }
312            s as u64 * SECOND
313        });
314
315    if u < SECOND {
316        // Special case: if duration is smaller than a second,
317        // use smaller units, like 1.2ms
318        let mut _prec = 0;
319        w -= 1;
320        buf[w] = b's';
321        w -= 1;
322
323        if u == 0 {
324            return "0s".to_string();
325        } else if u < MICROSECOND {
326            _prec = 0;
327            buf[w] = b'n';
328        } else if u < MILLISECOND {
329            _prec = 3;
330            // U+00B5 'µ' micro sign == 0xC2 0xB5
331            buf[w] = 0xB5;
332            w -= 1;
333            buf[w] = 0xC2;
334        } else {
335            _prec = 6;
336            buf[w] = b'm';
337        }
338        (w, u) = format_float(&mut buf[..w], u, _prec);
339        w = format_int(&mut buf[..w], u);
340    } else {
341        w -= 1;
342        buf[w] = b's';
343        (w, u) = format_float(&mut buf[..w], u, 9);
344
345        // u is now integer number of seconds
346        w = format_int(&mut buf[..w], u % 60);
347        u /= 60;
348
349        // u is now integer number of minutes
350        if u > 0 {
351            w -= 1;
352            buf[w] = b'm';
353            w = format_int(&mut buf[..w], u % 60);
354            u /= 60;
355
356            // u is now integer number of hours
357            if u > 0 {
358                w -= 1;
359                buf[w] = b'h';
360                w = format_int(&mut buf[..w], u);
361            }
362        }
363    }
364
365    if neg {
366        w -= 1;
367        buf[w] = b'-';
368    }
369    String::from_utf8_lossy(&buf[w..]).into_owned()
370}
371
372const SECOND: u64 = 1_000_000_000;
373const MILLISECOND: u64 = 1_000_000;
374const MICROSECOND: u64 = 1_000;
375
376fn format_float(buf: &mut [u8], mut v: u64, prec: usize) -> (usize, u64) {
377    let mut w = buf.len();
378    let mut print = false;
379    for _ in 0..prec {
380        let digit = v % 10;
381        print = print || digit != 0;
382        if print {
383            w -= 1;
384            buf[w] = digit as u8 + b'0';
385        }
386        v /= 10;
387    }
388    if print {
389        w -= 1;
390        buf[w] = b'.';
391    }
392    (w, v)
393}
394
395fn format_int(buf: &mut [u8], mut v: u64) -> usize {
396    let mut w = buf.len();
397    if v == 0 {
398        w -= 1;
399        buf[w] = b'0';
400    } else {
401        while v > 0 {
402            w -= 1;
403            buf[w] = (v % 10) as u8 + b'0';
404            v /= 10;
405        }
406    }
407    w
408}
409
410#[cfg(test)]
411mod tests {
412    use crate::duration::{Duration, format_duration, parse_duration};
413
414    fn assert_duration(input: &str, expected: Duration) {
415        let (_, duration) = parse_duration(input).unwrap();
416        assert_eq!(duration, expected, "{input}");
417    }
418
419    fn assert_print_duration(input: Duration, expected: &str) {
420        let actual = format_duration(&input);
421        assert_eq!(actual, expected, "{input}");
422    }
423
424    macro_rules! assert_durations {
425        ($($str:expr => $duration:expr),*$(,)?) => {
426            #[test]
427            fn test_durations() {
428                $(
429                    assert_duration($str, $duration);
430                )*
431            }
432        };
433    }
434
435    macro_rules! assert_duration_format {
436        ($($duration:expr => $str:expr),*$(,)?) => {
437            #[test]
438            fn test_format_durations() {
439                $(
440                    assert_print_duration($duration, $str);
441                )*
442            }
443        };
444    }
445
446    assert_durations! {
447        "1s" => Duration::seconds(1),
448        "-1s" => Duration::seconds(-1),
449        "1.1s" => Duration::seconds(1) + Duration::milliseconds(100),
450        "1.5m" => Duration::minutes(1) + Duration::seconds(30),
451        "1m1s" => Duration::minutes(1) + Duration::seconds(1),
452        "1h1m1s" => Duration::hours(1) + Duration::minutes(1) + Duration::seconds(1),
453        "1ms" => Duration::milliseconds(1),
454        "1us" => Duration::microseconds(1),
455        "1ns" => Duration::nanoseconds(1),
456        "1.1ns" => Duration::nanoseconds(1),
457        "1.123us" => Duration::microseconds(1) + Duration::nanoseconds(123),
458        "0s" => Duration::ZERO,
459        "0h0m0s" => Duration::ZERO,
460        "0h0m1s" => Duration::seconds(1),
461        "0" => Duration::ZERO,
462        "-0" => Duration::ZERO,
463    }
464
465    assert_duration_format! {
466        Duration::ZERO => "0s",
467        Duration::nanoseconds(1) => "1ns",
468        Duration::nanoseconds(1100) => "1.1µs",
469        Duration::microseconds(2200) => "2.2ms",
470        Duration::milliseconds(3300) => "3.3s",
471        Duration::minutes(4) + Duration::seconds(5) => "4m5s",
472        Duration::minutes(4) + Duration::milliseconds(5001) => "4m5.001s",
473        Duration::hours(5) + Duration::minutes(6) + Duration::milliseconds(7001) => "5h6m7.001s",
474        Duration::minutes(8) + Duration::nanoseconds(1) => "8m0.000000001s",
475        Duration::nanoseconds(i64::MAX) => "2562047h47m16.854775807s",
476        Duration::nanoseconds(i64::MIN) => "-2562047h47m16.854775808s",
477    }
478}