Skip to main content

deep_time/dt/
from_str.rs

1use crate::{
2    Dt, DtErr, DtErrKind, SEC_PER_DAY, SEC_PER_MONTH, SEC_PER_WEEK, SEC_PER_YEAR, Scale,
3    StrPTimeFmt, TimeParts, an_err,
4};
5
6struct ParsedComponent {
7    unit: u8,
8    signed_int: i64,
9    frac_digits: usize,
10    frac_num: i64,
11}
12
13impl Dt {
14    #[inline]
15    pub fn from_str(
16        s: &str,
17        fmt: &str,
18        inp_can_end_before_fmt: bool,
19        fmt_can_end_before_inp: bool,
20        allow_partial_date: bool,
21    ) -> Result<Dt, DtErr> {
22        TimeParts::from_str(
23            fmt,
24            s,
25            inp_can_end_before_fmt,
26            fmt_can_end_before_inp,
27            allow_partial_date,
28        )?
29        .to_dt()
30    }
31
32    #[inline]
33    pub fn parse_fmt(strptime_fmt: &str) -> Result<StrPTimeFmt, DtErr> {
34        StrPTimeFmt::new(strptime_fmt)
35    }
36
37    pub fn from_iso_duration(s: &str) -> Result<Dt, DtErr> {
38        let len = s.len();
39        if len == 0 {
40            return Err(an_err!(DtErrKind::Incomplete, "empty"));
41        }
42
43        let b = s.as_bytes();
44        let mut i = 0usize;
45
46        // Optional leading sign (+ or -)
47        let mut sign: i64 = 1;
48        if i < len && matches!(b[i], b'+' | b'-') {
49            if b[i] == b'-' {
50                sign = -1;
51            }
52            i += 1;
53        }
54
55        // Must start with P/p
56        if i >= len || !matches!(b[i], b'P' | b'p') {
57            return Err(an_err!(DtErrKind::MustStartWith, "P"));
58        }
59        i += 1;
60
61        // Find the (single) T/t separator
62        let t_pos = b[i..]
63            .iter()
64            .position(|&c| matches!(c, b'T' | b't'))
65            .map(|p| i + p);
66
67        let (date_part, time_part) = match t_pos {
68            Some(pos) => {
69                if pos == len - 1 {
70                    return Err(an_err!(DtErrKind::InvalidSyntax, "T with no time"));
71                }
72                if b[pos + 1..].iter().any(|&c| matches!(c, b'T' | b't')) {
73                    return Err(an_err!(DtErrKind::InvalidSyntax, "multiple T"));
74                }
75                (&b[i..pos], &b[pos + 1..])
76            }
77            None => (&b[i..], &[] as &[u8]),
78        };
79
80        let mut has_fraction = false;
81        let mut total_nanos: i128 = 0;
82
83        // Both date and time parts now use the same fixed-length logic
84        Self::parse_duration_part(date_part, &mut total_nanos, true, sign, &mut has_fraction)?;
85        Self::parse_duration_part(time_part, &mut total_nanos, false, sign, &mut has_fraction)?;
86
87        // Convert accumulated nanoseconds to attoseconds and build Dt
88        let total_attos = total_nanos * 1_000_000_000i128;
89        Ok(Dt::from_attos(total_attos, Scale::TAI))
90    }
91
92    /// Parses a single component (number + optional fraction + unit) from the slice,
93    /// advancing the index `i`. Returns `None` when the slice is exhausted.
94    fn parse_next_component(
95        chars: &[u8],
96        i: &mut usize,
97        sign: i64,
98        has_fraction: &mut bool,
99    ) -> Result<Option<ParsedComponent>, DtErr> {
100        if *i >= chars.len() {
101            return Ok(None);
102        }
103
104        if *has_fraction {
105            return Err(an_err!(DtErrKind::InvalidSyntax, "components after frac"));
106        }
107
108        // Parse integer part
109        let start = *i;
110        while *i < chars.len() && chars[*i].is_ascii_digit() {
111            *i += 1;
112        }
113        if start == *i {
114            return Err(an_err!(DtErrKind::ExpectedValue, "number"));
115        }
116
117        let int_str = core::str::from_utf8(&chars[start..*i])
118            .map_err(|_| an_err!(DtErrKind::InvalidNumber, "invalid utf8 in int"))?;
119        let int: i64 = int_str.parse().map_err(|e: core::num::ParseIntError| {
120            an_err!(DtErrKind::InvalidNumber, "{}: {}", int_str, e)
121        })?;
122
123        // Parse optional fraction
124        let mut frac_num: i64 = 0;
125        let mut frac_digits: usize = 0;
126        if *i < chars.len() && matches!(chars[*i], b'.' | b',') {
127            *i += 1;
128            let frac_start = *i;
129            while *i < chars.len() && chars[*i].is_ascii_digit() {
130                *i += 1;
131            }
132            frac_digits = *i - frac_start;
133            if frac_digits == 0 {
134                return Err(an_err!(DtErrKind::ExpectedValue, "empty frac after ."));
135            }
136            if frac_digits > 9 {
137                return Err(an_err!(DtErrKind::OutOfRange, "frac >9"));
138            }
139
140            let frac_str = core::str::from_utf8(&chars[frac_start..*i])
141                .map_err(|_| an_err!(DtErrKind::InvalidNumber, "invalid utf8 in frac"))?;
142            frac_num = frac_str.parse().map_err(|e: core::num::ParseIntError| {
143                an_err!(DtErrKind::InvalidNumber, "{}: {}", frac_str, e)
144            })?;
145        }
146
147        // Unit must follow
148        if *i >= chars.len() {
149            return Err(an_err!(
150                DtErrKind::InvalidSyntax,
151                "missing unit after number"
152            ));
153        }
154        let unit = chars[*i];
155        *i += 1;
156
157        // Only seconds support a fractional part
158        if frac_digits > 0 {
159            if !matches!(unit, b'S' | b's') {
160                return Err(an_err!(
161                    DtErrKind::InvalidSyntax,
162                    "frac only supported for seconds"
163                ));
164            }
165            *has_fraction = true;
166        }
167
168        let signed_int = (int as i128 * sign as i128) as i64;
169
170        Ok(Some(ParsedComponent {
171            unit,
172            signed_int,
173            frac_digits,
174            frac_num,
175        }))
176    }
177
178    /// Helper that parses **one section** of an ISO duration (date or time part)
179    /// and accumulates nanoseconds into `total_nanos`.
180    ///
181    /// Years, months, weeks, and days are converted using the fixed-length
182    /// constants (the only sensible semantics for a pure `Dt`).
183    fn parse_duration_part(
184        chars: &[u8],
185        total_nanos: &mut i128,
186        is_date: bool,
187        sign: i64,
188        has_fraction: &mut bool,
189    ) -> Result<(), DtErr> {
190        let mut i = 0;
191        while let Some(comp) = Self::parse_next_component(chars, &mut i, sign, has_fraction)? {
192            let contrib_nanos = match (is_date, comp.unit) {
193                (true, b'Y' | b'y') => {
194                    let total_secs = (comp.signed_int as i128)
195                        .checked_mul(SEC_PER_YEAR)
196                        .ok_or_else(|| an_err!(DtErrKind::OutOfRange, "year"))?;
197                    total_secs * 1_000_000_000i128
198                }
199                (true, b'M' | b'm') => {
200                    let total_secs = (comp.signed_int as i128)
201                        .checked_mul(SEC_PER_MONTH)
202                        .ok_or_else(|| an_err!(DtErrKind::OutOfRange, "month"))?;
203                    total_secs * 1_000_000_000i128
204                }
205                (true, b'W' | b'w') => {
206                    let total_secs = (comp.signed_int as i128)
207                        .checked_mul(SEC_PER_WEEK as i128)
208                        .ok_or_else(|| an_err!(DtErrKind::OutOfRange, "week"))?;
209                    total_secs * 1_000_000_000i128
210                }
211                (true, b'D' | b'd') => {
212                    let total_secs = (comp.signed_int as i128)
213                        .checked_mul(SEC_PER_DAY)
214                        .ok_or_else(|| an_err!(DtErrKind::OutOfRange, "day"))?;
215                    total_secs * 1_000_000_000i128
216                }
217                (false, b'H' | b'h') => (comp.signed_int as i128) * 3_600_000_000_000i128,
218                (false, b'M' | b'm') => (comp.signed_int as i128) * 60_000_000_000i128,
219                (false, b'S' | b's') => {
220                    let mut sec_nanos = (comp.signed_int as i128) * 1_000_000_000i128;
221                    if comp.frac_digits > 0 {
222                        let frac_ns = (comp.frac_num as i128 * sign as i128 * 1_000_000_000i128)
223                            / 10i128.pow(comp.frac_digits as u32);
224                        sec_nanos += frac_ns;
225                    }
226                    sec_nanos
227                }
228                _ => {
229                    return Err(an_err!(DtErrKind::InvalidItem, "{}", comp.unit as char));
230                }
231            };
232
233            *total_nanos = total_nanos.saturating_add(contrib_nanos);
234        }
235        Ok(())
236    }
237
238    /// Accepts: `P1Y`, `-P2W`, `PT1.5H`, `P1DT2H30M`, `+P3D`, `p1y`, `P1,5S`, `PT0S`, etc.
239    /// Rejects: anything with whitespace, lone "P"/"-P"/"PT", "P123", "Please wait 5m",
240    ///          "1.5h", "P1Yabc", "P1Y!", or **any string longer than 128 bytes**.
241    pub fn looks_like_iso(s: &str) -> bool {
242        let len = s.len();
243        if matches!(len, 0 | 1) {
244            return false;
245        }
246        let b = s.as_bytes();
247        let mut i = 0usize;
248        // Optional leading sign
249        if matches!(b[0], b'+' | b'-') {
250            i += 1;
251        }
252        // Must start with P/p after optional sign
253        if !matches!(b[i], b'P' | b'p') {
254            return false;
255        }
256        i += 1;
257        let mut has_digit = false;
258        let mut has_designator = false;
259        while i < len {
260            match b[i] {
261                b'0'..=b'9' => has_digit = true,
262                b'.' | b',' => {} // decimal separators allowed by ISO 8601
263                b'Y' | b'y' | b'M' | b'm' | b'W' | b'w' | b'D' | b'd' | b'T' | b't' | b'H'
264                | b'h' | b'S' | b's' => {
265                    has_designator = true;
266                }
267                _ => return false, // any other character = not ISO
268            }
269
270            i += 1;
271        }
272        // Must contain at least one digit *and* one designator after the initial P
273        has_digit && has_designator
274    }
275}