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}