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