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}