Skip to main content

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}