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    pub fn finish(&mut self, allow_partial_date: bool) -> core::result::Result<&mut Self, DtErr> {
80        if self.unix_timestamp_seconds.is_some() {
81            if self.hr.is_none() {
82                self.hr = Some(0);
83            }
84            if self.min.is_none() {
85                self.min = Some(0);
86            }
87            if self.sec.is_none() {
88                self.sec = Some(0);
89            }
90            if self.attos.is_none() {
91                self.attos = Some(0);
92            }
93            if self.offset.is_none() {
94                self.offset = Some(Offset::Utc);
95            }
96            return Ok(self);
97        }
98
99        // Sensible defaults for time components (most tests expect a full datetime)
100        if self.hr.is_none() {
101            self.hr = Some(0);
102        }
103        if self.min.is_none() {
104            self.min = Some(0);
105        }
106        if let Some(sec) = self.sec {
107            if sec == 60 {
108                self.is_leap_sec = true;
109            } else if sec > 60 {
110                return Err(an_err!(DtErrKind::OutOfRange, "seconds (0..=60): {}", sec));
111            }
112        } else {
113            self.sec = Some(0);
114        }
115        if self.attos.is_none() {
116            self.attos = Some(0);
117        }
118        if self.offset.is_none() {
119            self.offset = Some(Offset::Utc);
120        }
121
122        let has_calendar_date = if allow_partial_date {
123            if self.day.is_none() {
124                self.day = Some(1);
125            }
126            if self.mo.is_none() {
127                self.mo = Some(1);
128            }
129            self.yr.is_some()
130        } else {
131            self.yr.is_some() && self.mo.is_some() && self.day.is_some()
132        };
133        let has_ordinal_date = self.yr.is_some() && self.day_of_yr.is_some();
134        let has_iso_week_date = self.iso_wk_yr.is_some() && self.iso_wk.is_some();
135
136        if !has_calendar_date && !has_ordinal_date && !has_iso_week_date {
137            return Err(an_err!(DtErrKind::Incomplete));
138        }
139
140        Ok(self)
141    }
142}