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}