Skip to main content

deep_time/time_parts/
from_str.rs

1use crate::{DtErr, DtErrKind, Offset, TimeParts, an_err, parser::Parser};
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 rich set of `%` directives (similar to C `strptime`, Python
8    /// `strftime`/`strptime`, and common extensions used by `chrono`/`jiff`).
9    ///
10    /// The parser populates a [`TimeParts`] struct with all fields that can be
11    /// extracted from the input. After parsing, [`Self::finish`] is called
12    /// automatically to apply defaults and validation.
13    ///
14    /// # Parameters
15    ///
16    /// - `fmt`: The format string containing `%` directives.
17    /// - `input`: The string to parse.
18    /// - `inp_can_end_before_fmt`: If `true`, the input may end before the format
19    ///   string is fully consumed (extra format specifiers are ignored).
20    /// - `fmt_can_end_before_inp`: If `true`, the format may end before the input
21    ///   is fully consumed (trailing characters in the input are allowed).
22    /// - `allow_partial_date`: If `true`, a missing month/day will be defaulted
23    ///   to `1` instead of returning an [`Incomplete`] error.
24    ///
25    /// # Errors
26    ///
27    /// Returns [`DtErr`] for:
28    /// - Parse failures (`InvalidFormat`, `OutOfRange`, etc.)
29    /// - Incomplete data when `allow_partial_date` is `false`
30    /// - Trailing characters (when `fmt_can_end_before_inp` is `false`)
31    pub fn from_str(
32        fmt: &str,
33        input: &str,
34        inp_can_end_before_fmt: bool,
35        fmt_can_end_before_inp: bool,
36        allow_partial_date: bool,
37    ) -> Result<TimeParts, DtErr> {
38        let mut parts = TimeParts::new_utc();
39        let mut parser = Parser::new(
40            fmt.as_bytes(),
41            input.as_bytes(),
42            &mut parts,
43            inp_can_end_before_fmt,
44        );
45        parser.parse()?;
46        if parser.inp.is_empty() || fmt_can_end_before_inp {
47            // All input consumed → finalize
48            parts.finish(allow_partial_date)?;
49            Ok(parts)
50        } else {
51            // Trailing characters remain
52            Err(an_err!(DtErrKind::TrailingCharacters))
53        }
54    }
55
56    /// Finalizes a [`TimeParts`] after parsing by applying sensible defaults and
57    /// performing validation.
58    ///
59    /// This is called automatically by the various parsing paths (`from_str`,
60    /// CCSDS parsers, etc.). It ensures the struct is in a consistent state
61    /// before being turned into a full [`Dt`] or passed to other converters.
62    ///
63    /// # Behavior
64    ///
65    /// - If a Unix timestamp is present, it takes precedence and the time
66    ///   components are defaulted to `00:00:00.000000000` with a UTC offset.
67    /// - Otherwise:
68    ///   - Hour/minute/second/attoseconds/offset are defaulted to `0` / `Utc`.
69    ///   - Leap seconds (`second == 60`) are detected and flagged.
70    /// - Date completeness is checked in this priority order:
71    ///   1. Calendar date (`year`, `month`, `day`)
72    ///   2. Ordinal date (`year`, `day_of_year`)
73    ///   3. ISO week date (`iso_week_year`, `iso_week`)
74    /// - If `allow_partial_date` is `true`, missing month/day are defaulted to `1`.
75    ///
76    /// # Errors
77    ///
78    /// - [`DtErrKind::Incomplete`] if no valid date representation is present.
79    /// - [`DtErrKind::OutOfRange`] for seconds outside `0..=60`.
80    pub fn finish(&mut self, allow_partial_date: bool) -> core::result::Result<&mut Self, DtErr> {
81        if self.unix_timestamp_seconds.is_some() {
82            if self.hour.is_none() {
83                self.hour = Some(0);
84            }
85            if self.minute.is_none() {
86                self.minute = Some(0);
87            }
88            if self.second.is_none() {
89                self.second = Some(0);
90            }
91            if self.attos.is_none() {
92                self.attos = Some(0);
93            }
94            if self.offset.is_none() {
95                self.offset = Some(Offset::Utc);
96            }
97            return Ok(self);
98        }
99
100        // Sensible defaults for time components (most tests expect a full datetime)
101        if self.hour.is_none() {
102            self.hour = Some(0);
103        }
104        if self.minute.is_none() {
105            self.minute = Some(0);
106        }
107        if let Some(sec) = self.second {
108            if sec == 60 {
109                self.is_leap_second = true;
110            } else if sec > 60 {
111                return Err(an_err!(DtErrKind::OutOfRange, "seconds (0..=60): {}", sec));
112            }
113        } else {
114            self.second = Some(0);
115        }
116        if self.attos.is_none() {
117            self.attos = Some(0);
118        }
119        if self.offset.is_none() {
120            self.offset = Some(Offset::Utc);
121        }
122
123        let has_calendar_date = if allow_partial_date {
124            if self.day.is_none() {
125                self.day = Some(1);
126            }
127            if self.month.is_none() {
128                self.month = Some(1);
129            }
130            self.year.is_some()
131        } else {
132            self.year.is_some() && self.month.is_some() && self.day.is_some()
133        };
134        let has_ordinal_date = self.year.is_some() && self.day_of_year.is_some();
135        let has_iso_week_date = self.iso_week_year.is_some() && self.iso_week.is_some();
136
137        if !has_calendar_date && !has_ordinal_date && !has_iso_week_date {
138            return Err(an_err!(DtErrKind::Incomplete));
139        }
140
141        Ok(self)
142    }
143}