Skip to main content

deep_time/alloc_parse/
parse_duration.rs

1use crate::math::round;
2use crate::{Dt, DtErr, DtErrKind, Lang, Real, Scale, an_err, natural_duration_to_iso};
3use alloc::string::String;
4
5impl Dt {
6    /// Parses duration strings, tries formats in the following order:
7    ///
8    /// 1. Strict ISO 8601 e.g. **`P1DT2H30M`**
9    /// 2. Common natural-language formats e.g. **`2 wks, 3 days, and 2 mins`**
10    /// 3. Media duration format e.g. **`1:07:54:30`**
11    /// 4. Numerical milliseconds, decimals counted as fractional milliseconds
12    ///
13    /// Returns a [`Dt`].
14    pub fn from_str_duration(s: &str, lang: Lang) -> Result<Dt, DtErr> {
15        if s.is_empty() {
16            return Err(an_err!(DtErrKind::Empty));
17        }
18
19        if Dt::looks_like_iso(s) {
20            return Dt::from_iso_duration(s).map_err(|e| {
21                an_err!(
22                    " {}",
23                    s => e
24                )
25            });
26        }
27
28        let lower = s.to_lowercase();
29        if let Ok(dur) = Dt::from_natural_duration(&lower, lang, true) {
30            return Ok(dur);
31        }
32
33        if let Ok(dur) = Dt::from_str_media_duration(s) {
34            return Ok(dur);
35        }
36
37        if let Ok(ms) = s.parse::<Real>() {
38            if !ms.is_finite() {
39                return Err(an_err!(DtErrKind::OutOfRange, "{}", s));
40            }
41            let nanos = round(ms * 1_000_000.0) as i128;
42
43            let span = Dt::from_ns(nanos, 0, Scale::TAI, Scale::TAI);
44            return Ok(span);
45        }
46
47        Err(an_err!(DtErrKind::InvalidInput, "{}", s))
48    }
49
50    /// Converts a natural language duration into an ISO duration.
51    pub fn natural_to_iso(s: &str, lang: Lang) -> Result<String, DtErr> {
52        let lower = s.to_lowercase();
53        match natural_duration_to_iso(&lower, lang, true) {
54            Ok(iso) => Ok(iso),
55            Err(e) => Err(an_err!(
56                " {}",
57                s => e
58            )),
59        }
60    }
61
62    /// Accepts: `P1Y`, `-P2W`, `PT1.5H`, `P1DT2H30M`, `+P3D`, `p1y`, `P1,5S`, `PT0S`, etc.
63    /// Rejects: anything with whitespace, lone "P"/"-P"/"PT", "P123", "Please wait 5m",
64    ///          "1.5h", "P1Yabc", "P1Y!", or **any string longer than 128 bytes**.
65    fn looks_like_iso(s: &str) -> bool {
66        let len = s.len();
67        if matches!(len, 0 | 1) {
68            return false;
69        }
70        let b = s.as_bytes();
71        let mut i = 0usize;
72        // Optional leading sign
73        if matches!(b[0], b'+' | b'-') {
74            i += 1;
75        }
76        // Must start with P/p after optional sign
77        if !matches!(b[i], b'P' | b'p') {
78            return false;
79        }
80        i += 1;
81        let mut has_digit = false;
82        let mut has_designator = false;
83        while i < len {
84            match b[i] {
85                b'0'..=b'9' => has_digit = true,
86                b'.' | b',' => {} // decimal separators allowed by ISO 8601
87                b'Y' | b'y' | b'M' | b'm' | b'W' | b'w' | b'D' | b'd' | b'T' | b't' | b'H'
88                | b'h' | b'S' | b's' => {
89                    has_designator = true;
90                }
91                _ => return false, // any other character = not ISO
92            }
93
94            i += 1;
95        }
96        // Must contain at least one digit *and* one designator after the initial P
97        has_digit && has_designator
98    }
99}