go_duration/
lib.rs

1use std::{
2    fmt::{self, Write},
3    str::FromStr,
4};
5
6use ::nom::{Finish, Parser};
7
8pub mod nom;
9#[cfg(feature = "serde")]
10pub mod serde;
11
12#[derive(Debug, Clone, PartialEq, thiserror::Error)]
13pub enum GoDurationParseError {
14    #[error("time: invalid duration")]
15    InvalidDuration,
16    #[error("time: missing unit in duration")]
17    MissingUnit,
18    #[error("time: unknown unit \"{0}\" in duration")]
19    UnknownUnit(String),
20}
21
22#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
23pub struct GoDuration(
24    /// nanoseconds
25    pub i64,
26);
27
28impl GoDuration {
29    pub const ZERO: Self = GoDuration(0);
30    pub const MIN: Self = GoDuration(i64::MIN);
31    pub const MAX: Self = GoDuration(i64::MAX);
32
33    #[inline]
34    #[must_use]
35    pub fn nanoseconds(&self) -> i64 {
36        self.0
37    }
38
39    #[must_use]
40    pub fn abs(&self) -> Self {
41        Self(0i64.saturating_add_unsigned(self.0.unsigned_abs()))
42    }
43}
44
45impl TryFrom<&str> for GoDuration {
46    type Error = GoDurationParseError;
47
48    fn try_from(value: &str) -> Result<Self, Self::Error> {
49        value.parse()
50    }
51}
52
53impl From<i64> for GoDuration {
54    fn from(nanoseconds: i64) -> Self {
55        Self(nanoseconds)
56    }
57}
58
59impl fmt::Display for GoDuration {
60    fn fmt(&self, f: &mut std::fmt::Formatter) -> fmt::Result {
61        let nanos = self.0;
62        if nanos.is_negative() {
63            f.write_char('-')?;
64        }
65        let mut nanos = nanos.unsigned_abs();
66        if nanos >= NANOS_PER_HOUR {
67            f.write_fmt(format_args!("{}h", nanos / NANOS_PER_HOUR))?;
68            nanos %= NANOS_PER_HOUR;
69            f.write_fmt(format_args!("{}m", nanos / NANOS_PER_MINUTE))?;
70            nanos %= NANOS_PER_MINUTE;
71            return f.write_fmt(format_args!("{}s", nanos as f64 / NANOS_PER_SECOND as f64));
72        }
73        if nanos >= NANOS_PER_MINUTE {
74            f.write_fmt(format_args!("{}m", nanos / NANOS_PER_MINUTE))?;
75            nanos %= NANOS_PER_MINUTE;
76            return f.write_fmt(format_args!("{}s", nanos as f64 / NANOS_PER_SECOND as f64));
77        }
78        if nanos >= NANOS_PER_SECOND {
79            return f.write_fmt(format_args!("{}s", nanos as f64 / NANOS_PER_SECOND as f64));
80        }
81        if nanos >= NANOS_PER_MILLISECOND {
82            return f.write_fmt(format_args!(
83                "{}ms",
84                nanos as f64 / NANOS_PER_MILLISECOND as f64
85            ));
86        }
87        if nanos >= NANOS_PER_MICROSECOND {
88            return f.write_fmt(format_args!(
89                "{}\u{00B5}s",
90                nanos as f64 / NANOS_PER_MICROSECOND as f64
91            ));
92        }
93        if nanos > 0 {
94            return f.write_fmt(format_args!("{nanos}ns"));
95        }
96        f.write_str("0s")
97    }
98}
99
100impl FromStr for GoDuration {
101    type Err = GoDurationParseError;
102
103    fn from_str(s: &str) -> Result<Self, Self::Err> {
104        parse_go_duration(s)
105    }
106}
107
108pub(crate) const NANOS_PER_MICROSECOND: u64 = 1_000;
109pub(crate) const NANOS_PER_MILLISECOND: u64 = 1_000_000;
110pub(crate) const NANOS_PER_SECOND: u64 = 1_000_000_000;
111pub(crate) const NANOS_PER_MINUTE: u64 = NANOS_PER_SECOND * 60;
112pub(crate) const NANOS_PER_HOUR: u64 = NANOS_PER_MINUTE * 60;
113
114pub fn parse_go_duration(input: &str) -> Result<GoDuration, GoDurationParseError> {
115    nom::go_duration.parse(input).finish().map(|(_, dur)| dur)
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_parse_valid() {
124        let cases = [
125            ("0s", 0),
126            ("+42ns", 42),
127            (".1us", 100),
128            (".1ns0.9ns", 0),
129            ("1ns9ns", 10),
130            ("1.ns", 1),
131            ("2ns", 2),
132            ("2us", 2000),
133            ("-2us", -2000),
134            ("0.2us", 200),
135            ("0.0000000000003h", 1),
136            ("1ns", 1),
137            ("1us", 1_000),
138            ("1\u{00B5}s", 1_000),
139            ("1\u{03BC}s", 1_000),
140            ("1ms", 1_000_000),
141            ("1s", 1_000_000_000),
142            ("1m", 60_000_000_000),
143            ("1h", 3_600_000_000_000),
144            ("9223372036854775807ns", 9_223_372_036_854_775_807),
145            ("-9223372036854775808ns", -9_223_372_036_854_775_808),
146        ];
147
148        for (input, expected) in cases {
149            let output = parse_go_duration(input);
150            let output = output.expect(input);
151            assert_eq!(expected, output.0, "{input}");
152        }
153    }
154
155    #[test]
156    fn test_parse_invalid() {
157        let cases = [
158            ("", GoDurationParseError::InvalidDuration),
159            ("0", GoDurationParseError::MissingUnit),
160            (
161                "-1m-30s",
162                GoDurationParseError::UnknownUnit("m-".to_string()),
163            ),
164            ("-2", GoDurationParseError::MissingUnit),
165            ("0z", GoDurationParseError::UnknownUnit("z".to_string())),
166            (
167                "1m-30s",
168                GoDurationParseError::UnknownUnit("m-".to_string()),
169            ),
170            (
171                "1m+30s",
172                GoDurationParseError::UnknownUnit("m+".to_string()),
173            ),
174            (
175                "-1m+30s",
176                GoDurationParseError::UnknownUnit("m+".to_string()),
177            ),
178            (
179                "9223372036854775808ns",
180                GoDurationParseError::InvalidDuration,
181            ),
182            (
183                "-9223372036854775809ns",
184                GoDurationParseError::InvalidDuration,
185            ),
186            ("-", GoDurationParseError::InvalidDuration),
187            ("+", GoDurationParseError::InvalidDuration),
188            (" ", GoDurationParseError::InvalidDuration),
189            ("-1 m", GoDurationParseError::UnknownUnit(" m".to_string())),
190            ("17h ", GoDurationParseError::UnknownUnit("h ".to_string())),
191            (" 42s", GoDurationParseError::InvalidDuration),
192        ];
193
194        for (input, expected) in cases {
195            let output = parse_go_duration(input);
196            assert!(output.is_err(), "{input} {output:?}");
197
198            let output = output.unwrap_err();
199            assert_eq!(output, expected, "{input}");
200        }
201    }
202
203    #[test]
204    fn test_format() {
205        let cases = [
206            (4000 * NANOS_PER_SECOND as i64, "1h6m40s"),
207            (90 * NANOS_PER_MINUTE as i64, "1h30m0s"),
208            (-1, "-1ns"),
209            (0, "0s"),
210            (1, "1ns"),
211            (NANOS_PER_MICROSECOND as i64 - 1, "999ns"),
212            (NANOS_PER_MICROSECOND as i64, "1\u{00B5}s"),
213            (NANOS_PER_MICROSECOND as i64 + 1, "1.001\u{00B5}s"),
214            (NANOS_PER_MILLISECOND as i64 - 1, "999.999\u{00B5}s"),
215            (NANOS_PER_MILLISECOND as i64, "1ms"),
216            (NANOS_PER_MILLISECOND as i64 + 1, "1.000001ms"),
217            (NANOS_PER_SECOND as i64 - 1, "999.999999ms"),
218            (NANOS_PER_SECOND as i64, "1s"),
219            (NANOS_PER_SECOND as i64 + 1, "1.000000001s"),
220            (NANOS_PER_MINUTE as i64 - 1, "59.999999999s"),
221            (NANOS_PER_MINUTE as i64, "1m0s"),
222            (NANOS_PER_MINUTE as i64 + 1, "1m0.000000001s"),
223            (NANOS_PER_HOUR as i64 - 1, "59m59.999999999s"),
224            (NANOS_PER_HOUR as i64, "1h0m0s"),
225            (NANOS_PER_HOUR as i64 + 1, "1h0m0.000000001s"),
226            (i64::MIN, "-2562047h47m16.854775808s"),
227            (i64::MAX, "2562047h47m16.854775807s"),
228        ];
229        for (input, expected) in cases {
230            let output = GoDuration(input).to_string();
231            assert_eq!(expected, output, "{input}");
232        }
233    }
234
235    #[test]
236    fn test_try_from_trait() {
237        let output = GoDuration::try_from("42ns");
238        assert!(output.is_ok());
239        assert_eq!(GoDuration(42), output.unwrap());
240    }
241
242    #[test]
243    fn test_from_trait() {
244        let output = GoDuration::from(-23);
245        assert_eq!(GoDuration(-23), output);
246    }
247
248    #[test]
249    fn test_duration_abs() {
250        let cases = [
251            (i64::MIN, i64::MAX),
252            (i64::MAX, i64::MAX),
253            (0, 0),
254            (-42, 42),
255        ];
256
257        for (input, expected) in cases {
258            let output = GoDuration(input).abs();
259            assert_eq!(expected, output.nanoseconds(), "{input}");
260        }
261    }
262}