Skip to main content

deser_core/ext/
duration.rs

1use alloc::string::ToString;
2use core::fmt;
3use core::str::FromStr;
4
5use crate::error::Error;
6use crate::event::Atom;
7use crate::ext::Extension;
8use crate::ext::datetime::write_fraction;
9use crate::ext::known::{WellKnown, impl_well_known, invalid, out_of_range, round, trunc};
10
11/// A signed, exact length of time.
12///
13/// This is a well-known extension type (see [`ext`](crate::ext)).  The
14/// seconds and nanoseconds always have the same sign: `-1.5` seconds are
15/// `seconds: -1` and `nanosecond: -500_000_000`.
16///
17/// The fallback is the ISO 8601 representation as string (`PT1H30M`,
18/// `-PT0.5S`).  Only hours, minutes and seconds are used and accepted as
19/// days, months and years are not of a fixed length.  Fractions are only
20/// supported for seconds.
21///
22/// ```
23/// use deser::ext::Duration;
24///
25/// let duration: Duration = "PT1H0.5S".parse().unwrap();
26/// assert_eq!(duration, Duration { seconds: 3600, nanosecond: 500_000_000 });
27/// assert_eq!(duration.to_string(), "PT1H0.5S");
28/// ```
29///
30/// [`std::time::Duration`] serializes as [`Duration`], as do the duration
31/// types of `jiff`, `chrono` and `time` with the respective features.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
33pub struct Duration {
34    /// The whole seconds.
35    pub seconds: i64,
36    /// The fraction of the second in nanoseconds (`-999_999_999` to
37    /// `999_999_999`).
38    pub nanosecond: i32,
39}
40
41impl Duration {
42    /// Returns `true` if seconds and nanoseconds are in range and have the
43    /// same sign.
44    pub fn is_valid(&self) -> bool {
45        self.nanosecond.unsigned_abs() < 1_000_000_000
46            && !(self.seconds > 0 && self.nanosecond < 0)
47            && !(self.seconds < 0 && self.nanosecond > 0)
48    }
49
50    /// Returns `true` if the duration is negative.
51    pub fn is_negative(&self) -> bool {
52        self.seconds < 0 || self.nanosecond < 0
53    }
54
55    /// Returns the duration in seconds as float.
56    pub fn as_secs_f64(&self) -> f64 {
57        self.seconds as f64 + f64::from(self.nanosecond) / 1e9
58    }
59}
60
61impl fmt::Display for Duration {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        if self.is_negative() {
64            f.write_str("-")?;
65        }
66        f.write_str("PT")?;
67        let seconds = self.seconds.unsigned_abs();
68        let nanosecond = self.nanosecond.unsigned_abs();
69        let (hours, minutes, seconds) = (seconds / 3600, seconds / 60 % 60, seconds % 60);
70        if hours != 0 {
71            write!(f, "{}H", hours)?;
72        }
73        if minutes != 0 {
74            write!(f, "{}M", minutes)?;
75        }
76        if seconds != 0 || nanosecond != 0 || (hours == 0 && minutes == 0) {
77            write!(f, "{}", seconds)?;
78            write_fraction(f, nanosecond)?;
79            f.write_str("S")?;
80        }
81        Ok(())
82    }
83}
84
85impl FromStr for Duration {
86    type Err = Error;
87
88    fn from_str(s: &str) -> Result<Duration, Error> {
89        let (negative, rest) = match s.as_bytes().first() {
90            Some(b'-') => (true, &s[1..]),
91            Some(b'+') => (false, &s[1..]),
92            _ => (false, s),
93        };
94        let rest = rest
95            .strip_prefix("PT")
96            .or_else(|| rest.strip_prefix("pt"))
97            .ok_or_else(|| invalid("invalid duration, expected ISO 8601 duration (PT...)"))?;
98        if rest.is_empty() {
99            return Err(invalid("invalid duration"));
100        }
101
102        let overflow = || out_of_range("duration out of range");
103        let mut seconds: i64 = 0;
104        let mut nanosecond: i64 = 0;
105        // the units have to be in order: hours, minutes, seconds
106        let mut last_unit = 0;
107        let mut rest = rest.as_bytes();
108        while !rest.is_empty() {
109            let int_len = rest.iter().take_while(|x| x.is_ascii_digit()).count();
110            if int_len == 0 {
111                return Err(invalid("invalid duration"));
112            }
113            let value: i64 = core::str::from_utf8(&rest[..int_len])
114                .unwrap()
115                .parse()
116                .map_err(|_| overflow())?;
117            rest = &rest[int_len..];
118            let mut fraction = None;
119            if let Some(b'.' | b',') = rest.first() {
120                let frac_len = rest[1..].iter().take_while(|x| x.is_ascii_digit()).count();
121                if frac_len == 0 {
122                    return Err(invalid("invalid duration"));
123                }
124                let mut nanos = 0i64;
125                for (idx, &c) in rest[1..1 + frac_len].iter().enumerate() {
126                    // digits beyond nanoseconds are truncated
127                    if idx < 9 {
128                        nanos = nanos * 10 + i64::from(c - b'0');
129                    }
130                }
131                for _ in frac_len..9 {
132                    nanos *= 10;
133                }
134                fraction = Some(nanos);
135                rest = &rest[1 + frac_len..];
136            }
137            let (unit, factor) = match rest.first() {
138                Some(b'H' | b'h') => (1, 3600),
139                Some(b'M' | b'm') => (2, 60),
140                Some(b'S' | b's') => (3, 1),
141                _ => return Err(invalid("invalid duration unit")),
142            };
143            if unit <= last_unit || (fraction.is_some() && unit != 3) {
144                return Err(invalid("invalid duration"));
145            }
146            last_unit = unit;
147            rest = &rest[1..];
148            seconds = value
149                .checked_mul(factor)
150                .and_then(|x| x.checked_add(seconds))
151                .ok_or_else(overflow)?;
152            nanosecond = fraction.unwrap_or(0);
153        }
154
155        if negative {
156            seconds = -seconds;
157            nanosecond = -nanosecond;
158        }
159        Ok(Duration {
160            seconds,
161            nanosecond: nanosecond as i32,
162        })
163    }
164}
165
166impl Extension for Duration {
167    fn name(&self) -> &str {
168        "duration"
169    }
170
171    fn fallback(&self) -> Atom<'_> {
172        Atom::Str(self.to_string().into())
173    }
174}
175
176impl WellKnown for Duration {
177    const EXPECTING: &'static str = "duration";
178
179    /// Accepts durations, strings and numbers (seconds).
180    fn from_atom(atom: &Atom) -> Result<Option<Duration>, Error> {
181        Ok(Some(match *atom {
182            Atom::Ext(ref ext) => match ext.downcast_ref::<Duration>() {
183                Some(value) => *value,
184                None => return Ok(None),
185            },
186            Atom::Str(ref value) => value.parse()?,
187            Atom::U64(value) => Duration {
188                seconds: i64::try_from(value).map_err(|_| out_of_range("duration out of range"))?,
189                nanosecond: 0,
190            },
191            Atom::I64(value) => Duration {
192                seconds: value,
193                nanosecond: 0,
194            },
195            Atom::F64(value) => {
196                if !value.is_finite() || value.abs() >= 9.2e18 {
197                    return Err(out_of_range("duration out of range"));
198                }
199                let seconds = trunc(value);
200                Duration {
201                    seconds: seconds as i64,
202                    nanosecond: round((value - seconds) * 1e9) as i32,
203                }
204            }
205            _ => return Ok(None),
206        }))
207    }
208}
209
210impl_well_known!(Duration);
211
212#[test]
213fn test_duration_format() {
214    let cases = [
215        (0, 0, "PT0S"),
216        (1, 500_000_000, "PT1.5S"),
217        (-1, -500_000_000, "-PT1.5S"),
218        (0, -1, "-PT0.000000001S"),
219        (3600, 0, "PT1H"),
220        (3661, 0, "PT1H1M1S"),
221        (90, 0, "PT1M30S"),
222        (i64::MAX, 999_999_999, "PT2562047788015215H30M7.999999999S"),
223        (
224            -i64::MAX,
225            -999_999_999,
226            "-PT2562047788015215H30M7.999999999S",
227        ),
228    ];
229    for (seconds, nanosecond, expected) in cases {
230        let duration = Duration {
231            seconds,
232            nanosecond,
233        };
234        assert_eq!(duration.to_string(), expected);
235        assert_eq!(expected.parse::<Duration>().unwrap(), duration);
236    }
237    assert_eq!(
238        "pt1m".parse::<Duration>().unwrap(),
239        Duration {
240            seconds: 60,
241            nanosecond: 0
242        }
243    );
244    for invalid in [
245        "", "P", "PT", "P1D", "PT1S1M", "PT1.5M", "PT.5S", "PT1", "PTS", "1S",
246    ] {
247        assert!(invalid.parse::<Duration>().is_err(), "{}", invalid);
248    }
249}