deep_time/time_parts/from_str.rs
1use crate::{DtErr, DtErrKind, Parser, TimeParts, an_err};
2
3impl TimeParts {
4 /// Parser equivalent to `strptime` with a provided format string.
5 ///
6 /// The parser populates a [`TimeParts`] struct. After successful parsing,
7 /// [`Self::finish`] is called automatically to apply defaults and validation.
8 ///
9 /// ## Parameters
10 ///
11 /// - `fmt`: The format string containing `%` directives.
12 /// - `input`: The string to parse.
13 /// - `inp_can_end_before_fmt`: If `true`, the input may end before the format
14 /// string is fully consumed (extra format specifiers are ignored).
15 /// - `fmt_can_end_before_inp`: If `true`, the format may end before the input
16 /// is fully consumed (trailing characters in the input are allowed).
17 /// - `allow_partial_date`: If `true`, a missing month/day will be defaulted
18 /// to `1` instead of returning an [`Incomplete`] error.
19 ///
20 /// ## Supported Directives
21 ///
22 /// The format string supports literal characters and the following `%` directives.
23 /// Literal non-whitespace characters must match the input exactly.
24 /// Whitespace in the format matches (and consumes) any leading ASCII whitespace in the input.
25 ///
26 /// Many directives accept **format extensions** right after `%`:
27 /// - **Flags**: `-` (no pad), `_` (space pad), `0` (zero pad), `^`/`#` (treated as default)
28 /// - **Width**: 1–3 digits (affects numeric field width / padding expectations)
29 /// - **Colons** (only for `%z`): `:`, `::`, `:::` to control offset format
30 ///
31 /// ### Year / Century / Unbounded
32 /// - `%Y` — Four-digit year (e.g. `2024`). Supports sign, flags, and width.
33 /// - `%y` — Two-digit year (`00`–`99`; `00`–`68` → 2000+, `69`–`99` → 1900s).
34 /// - `%C` — Century (`00`–`99`).
35 /// - `%G` — Four-digit ISO week-based year.
36 /// - `%g` — Two-digit ISO week-based year (same century rule as `%y`).
37 /// - `%*` — **Unbounded year** (arbitrary length, supports negative years). *Library extension.*
38 ///
39 /// ### Month
40 /// - `%m` — Month number `01`–`12`.
41 /// - `%B` — Full English month name (e.g. `January`).
42 /// - `%b`, `%h` — Abbreviated English month name (3 letters, e.g. `Jan`).
43 ///
44 /// ### Day
45 /// - `%d`, `%e` — Day of month `01`–`31` (`%e` allows space padding).
46 /// - `%j` — Day of year `001`–`366`.
47 ///
48 /// ### Time of day
49 /// - `%H`, `%k` — Hour `00`–`23` (24-hour clock; `%k` allows space padding).
50 /// - `%I`, `%l` — Hour `01`–`12` (12-hour clock).
51 /// - `%M` — Minute `00`–`59`.
52 /// - `%S` — Second `00`–`60` (leap second allowed).
53 /// - `%f`, `%N` — Fractional seconds (up to 18 digits = attoseconds).
54 /// Width controls precision (`%3f` = ms, `%6N` = µs, `%9f` = ns, etc.).
55 /// Both accept an optional leading `.` in the input.
56 /// - `%.f`, `%.N`, `%.3f`, `%.6N`, ... — Same fractional parsing, but the
57 /// dot before the fraction is **optional** in the input (consumes literal `.` if present).
58 /// - `%P`, `%p` — `AM`/`PM` indicator (case-insensitive).
59 ///
60 /// ### Weekday / Week number
61 /// - `%A` — Full English weekday name (e.g. `Monday`).
62 /// - `%a` — Abbreviated English weekday name (3 letters, e.g. `Mon`).
63 /// - `%u` — Weekday number Monday=`1` … Sunday=`7`.
64 /// - `%w` — Weekday number Sunday=`0` … Saturday=`6`.
65 /// - `%U` — Week number (Sunday-first week), `00`–`53`.
66 /// - `%W` — Week number (Monday-first week), `00`–`53`.
67 /// - `%V` — ISO 8601 week number `01`–`53`.
68 ///
69 /// ### Timezone, Offset & Scale
70 /// - `%z` — Timezone offset. Colon count selects format:
71 /// - `%z` → `±HH[MM[SS]]` (minutes/seconds optional)
72 /// - `%:z` → `±HH:MM` (minutes required)
73 /// - `%::z` → `±HH:MM:SS` (seconds optional)
74 /// - `%:::z` → `±HH:MM:SS` (more flexible)
75 /// - `%Q` — IANA timezone name (e.g. `America/New_York`) **or** numeric offset
76 /// (if input starts with `+`/`-`). *Library extension.*
77 /// - `%L` — Time scale abbreviation (e.g. `TAI`, `UTC`, `GPS`). See [`Scale`].
78 /// *Library extension.*
79 ///
80 /// ### Shortcuts (compound directives)
81 /// - `%F` — Equivalent to `%Y-%m-%d` (ISO date).
82 /// - `%D` — Equivalent to `%m/%d/%y` (US date).
83 /// - `%T` — Equivalent to `%H:%M:%S`.
84 /// - `%R` — Equivalent to `%H:%M`.
85 ///
86 /// ### Other
87 /// - `%%` — Literal `%` character.
88 /// - `%s` — Unix timestamp (seconds since epoch; up to 19 digits, can be negative).
89 /// - `%n`, `%t` — Any whitespace (consumes it from input).
90 ///
91 /// ### Unsupported / Unknown
92 /// - `%c`, `%r`, `%x`, `%X`, `%Z` → [`DtErrKind::UnsupportedItem`]
93 /// - Any other unknown directive character → [`DtErrKind::UnknownItem`]
94 ///
95 /// ## Errors
96 ///
97 /// Returns [`DtErr`] containing one of the following [`DtErrKind`] variants:
98 ///
99 /// ### Format string errors
100 ///
101 /// - [`DtErrKind::TruncatedDirective`] — The format string ended immediately
102 /// after a `%` or after a `.` in a fractional directive (e.g. `%.`).
103 /// - [`DtErrKind::UnknownItem`] — Unknown `%` directive character.
104 /// - [`DtErrKind::UnsupportedItem`] — Known but unsupported directive
105 /// (e.g. `%c`, `%r`, `%x`, `%X`, `%Z`).
106 /// - [`DtErrKind::BadFractional`] — Malformed fractional directive
107 /// (e.g. `%.x` where `x` is not `f` or `N`).
108 ///
109 /// ### Input parsing errors
110 ///
111 /// - [`DtErrKind::UnexpectedInputEnd`] — Input ended before a required value
112 /// could be parsed.
113 /// - `Expected*` variants:
114 /// - [`DtErrKind::ExpectedYear`]
115 /// - [`DtErrKind::ExpectedMonth`]
116 /// - [`DtErrKind::ExpectedDay`]
117 /// - [`DtErrKind::ExpectedDayOfYear`]
118 /// - [`DtErrKind::ExpectedHour`]
119 /// - [`DtErrKind::ExpectedMinute`]
120 /// - [`DtErrKind::ExpectedSecond`]
121 /// - [`DtErrKind::ExpectedFractionalSeconds`]
122 /// - [`DtErrKind::ExpectedTimestamp`]
123 /// - [`DtErrKind::ExpectedWeekNumber`]
124 /// - [`DtErrKind::ExpectedWeekdayNumber`]
125 /// - [`DtErrKind::MismatchedLiteral`] — A literal character from the format
126 /// string did not match the input.
127 /// - [`DtErrKind::OutOfRange`] — A numeric value was parsed but is outside
128 /// the valid range for that component (e.g. month 13, hour 25, day 32).
129 /// - [`DtErrKind::InvalidName`] — Unrecognized month name, weekday name,
130 /// or `am`/`pm` value.
131 /// - [`DtErrKind::InvalidTimezoneOffset`] — Invalid or malformed timezone
132 /// offset / IANA name.
133 /// - [`DtErrKind::MustStartWith`] — Timezone offset did not start with
134 /// `+` or `-`.
135 ///
136 /// ### Post-processing / validation errors
137 ///
138 /// - [`DtErrKind::Incomplete`] — Required date components (month/day) were
139 /// missing and `allow_partial_date` was `false`.
140 /// - [`DtErrKind::TrailingCharacters`] — The input contained trailing
141 /// characters after parsing and `fmt_can_end_before_inp` was `false`.
142 ///
143 /// Because [`DtErrKind`] is `#[non_exhaustive]`, additional variants may
144 /// appear in the future. You can match on the variants you care about and
145 /// use a wildcard arm for the rest.
146 ///
147 /// The concrete error kind is available via [`DtErr::kind()`] (or by
148 /// iterating [`DtErr::trace()`] if the error was chained with context
149 /// higher up the call stack).
150 pub fn from_str(
151 fmt: &str,
152 input: &str,
153 inp_can_end_before_fmt: bool,
154 fmt_can_end_before_inp: bool,
155 allow_partial_date: bool,
156 ) -> Result<TimeParts, DtErr> {
157 let mut parts = TimeParts::new_utc();
158 let mut parser = Parser::new(
159 fmt.as_bytes(),
160 input.as_bytes(),
161 &mut parts,
162 inp_can_end_before_fmt,
163 );
164 parser.parse()?;
165 if parser.inp.is_empty() || fmt_can_end_before_inp {
166 // All input consumed → finalize
167 parts.finish(allow_partial_date)?;
168 Ok(parts)
169 } else {
170 // Trailing characters remain
171 Err(an_err!(DtErrKind::TrailingCharacters))
172 }
173 }
174
175 /// Finalizes a [`TimeParts`] after parsing by applying sensible defaults and
176 /// performing validation.
177 ///
178 /// This is called automatically by the various parsing paths (`from_str`,
179 /// CCSDS parsers, etc.). It ensures the struct is in a consistent state
180 /// before being turned into a full [`Dt`] or passed to other converters.
181 ///
182 /// ## Behavior
183 ///
184 /// - If a Unix timestamp is present then no action is taken.
185 /// - Date completeness is checked in this priority order:
186 /// 1. Calendar date (`year`, `month`, `day`)
187 /// 2. Ordinal date (`year`, `day_of_year`)
188 /// 3. ISO week date (`iso_week_year`, `iso_week`)
189 /// - If `allow_partial_date` is `true`, missing month/day are defaulted to `1`.
190 ///
191 /// ## Errors
192 ///
193 /// - [`DtErrKind::Incomplete`] if no valid date representation is present.
194 #[inline(always)]
195 pub fn finish(&mut self, allow_partial_date: bool) -> Result<(), DtErr> {
196 if self.unix_timestamp_seconds.is_none() {
197 let has_calendar_date = if allow_partial_date {
198 if self.day.is_none() {
199 self.day = Some(1);
200 }
201 if self.mo.is_none() {
202 self.mo = Some(1);
203 }
204 self.yr.is_some()
205 } else {
206 self.yr.is_some() && self.mo.is_some() && self.day.is_some()
207 };
208 let has_ordinal_date = self.yr.is_some() && self.day_of_yr.is_some();
209 let has_iso_week_date = self.iso_wk_yr.is_some() && self.iso_wk.is_some();
210
211 if !has_calendar_date && !has_ordinal_date && !has_iso_week_date {
212 return Err(an_err!(DtErrKind::Incomplete));
213 }
214 }
215
216 Ok(())
217 }
218}