Skip to main content

deep_time/time_parts/
from_str.rs

1use crate::{DtErr, DtErrKind, Offset, Parser, TimeParts, an_err};
2
3impl TimeParts {
4    /// Low-level parser equivalent to `strptime` with a provided format string.
5    ///
6    /// This is the core entry point for format-string based parsing in the library.
7    /// It supports a large range of `%` directives (the same as jiff pretty much).
8    ///
9    /// The parser populates a [`TimeParts`] struct with all fields that can be
10    /// extracted from the input. After parsing, [`Self::finish`] is called
11    /// automatically to apply defaults and validation.
12    ///
13    /// ## Parameters
14    ///
15    /// - `fmt`: The format string containing `%` directives.
16    /// - `input`: The string to parse.
17    /// - `inp_can_end_before_fmt`: If `true`, the input may end before the format
18    ///   string is fully consumed (extra format specifiers are ignored).
19    /// - `fmt_can_end_before_inp`: If `true`, the format may end before the input
20    ///   is fully consumed (trailing characters in the input are allowed).
21    /// - `allow_partial_date`: If `true`, a missing month/day will be defaulted
22    ///   to `1` instead of returning an [`Incomplete`] error.
23    ///
24    /// ## Errors
25    ///
26    /// Returns [`DtErr`] for:
27    /// - Parse failures (`InvalidFormat`, `OutOfRange`, etc.)
28    /// - Incomplete data when `allow_partial_date` is `false`
29    /// - Trailing characters (when `fmt_can_end_before_inp` is `false`)
30    pub fn from_str(
31        fmt: &str,
32        input: &str,
33        inp_can_end_before_fmt: bool,
34        fmt_can_end_before_inp: bool,
35        allow_partial_date: bool,
36    ) -> Result<TimeParts, DtErr> {
37        let mut parts = TimeParts::new_utc();
38        let mut parser = Parser::new(
39            fmt.as_bytes(),
40            input.as_bytes(),
41            &mut parts,
42            inp_can_end_before_fmt,
43        );
44        parser.parse()?;
45        if parser.inp.is_empty() || fmt_can_end_before_inp {
46            // All input consumed → finalize
47            parts.finish(allow_partial_date)?;
48            Ok(parts)
49        } else {
50            // Trailing characters remain
51            Err(an_err!(DtErrKind::TrailingCharacters))
52        }
53    }
54
55    /// Finalizes a [`TimeParts`] after parsing by applying sensible defaults and
56    /// performing validation.
57    ///
58    /// This is called automatically by the various parsing paths (`from_str`,
59    /// CCSDS parsers, etc.). It ensures the struct is in a consistent state
60    /// before being turned into a full [`Dt`] or passed to other converters.
61    ///
62    /// ## Behavior
63    ///
64    /// - If a Unix timestamp is present, it takes precedence and the time
65    ///   components are defaulted to `00:00:00.000000000` with a UTC offset.
66    /// - Otherwise:
67    ///   - Hour/minute/second/attoseconds/offset are defaulted to `0` / `Utc`.
68    ///   - Leap seconds (`second == 60`) are detected and flagged.
69    /// - Date completeness is checked in this priority order:
70    ///   1. Calendar date (`year`, `month`, `day`)
71    ///   2. Ordinal date (`year`, `day_of_year`)
72    ///   3. ISO week date (`iso_week_year`, `iso_week`)
73    /// - If `allow_partial_date` is `true`, missing month/day are defaulted to `1`.
74    ///
75    /// ## Errors
76    ///
77    /// - [`DtErrKind::Incomplete`] if no valid date representation is present.
78    /// - [`DtErrKind::OutOfRange`] for seconds outside `0..=60`.
79    #[inline(always)]
80    pub(crate) fn finish(&mut self, allow_partial_date: bool) -> Result<(), DtErr> {
81        if self.offset.is_none() {
82            self.offset = Some(Offset::Utc);
83        }
84        if self.unix_timestamp_seconds.is_none() {
85            let has_calendar_date = if allow_partial_date {
86                if self.day.is_none() {
87                    self.day = Some(1);
88                }
89                if self.mo.is_none() {
90                    self.mo = Some(1);
91                }
92                self.yr.is_some()
93            } else {
94                self.yr.is_some() && self.mo.is_some() && self.day.is_some()
95            };
96            let has_ordinal_date = self.yr.is_some() && self.day_of_yr.is_some();
97            let has_iso_week_date = self.iso_wk_yr.is_some() && self.iso_wk.is_some();
98
99            if !has_calendar_date && !has_ordinal_date && !has_iso_week_date {
100                return Err(an_err!(DtErrKind::Incomplete));
101            }
102        }
103
104        Ok(())
105    }
106}