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
use crate::{DtErr, DtErrKind, Offset, Parser, TimeParts, an_err};
impl TimeParts {
/// Low-level parser equivalent to `strptime` with a provided format string.
///
/// This is the core entry point for format-string based parsing in the library.
/// It supports a large range of `%` directives (the same as jiff pretty much).
///
/// The parser populates a [`TimeParts`] struct with all fields that can be
/// extracted from the input. After 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.
///
/// ## Errors
///
/// Returns [`DtErr`] for:
/// - Parse failures (`InvalidFormat`, `OutOfRange`, etc.)
/// - Incomplete data when `allow_partial_date` is `false`
/// - Trailing characters (when `fmt_can_end_before_inp` is `false`)
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, it takes precedence and the time
/// components are defaulted to `00:00:00.000000000` with a UTC offset.
/// - Otherwise:
/// - Hour/minute/second/attoseconds/offset are defaulted to `0` / `Utc`.
/// - Leap seconds (`second == 60`) are detected and flagged.
/// - 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.
/// - [`DtErrKind::OutOfRange`] for seconds outside `0..=60`.
pub fn finish(&mut self, allow_partial_date: bool) -> core::result::Result<&mut Self, DtErr> {
if self.unix_timestamp_seconds.is_some() {
if self.hr.is_none() {
self.hr = Some(0);
}
if self.min.is_none() {
self.min = Some(0);
}
if self.sec.is_none() {
self.sec = Some(0);
}
if self.attos.is_none() {
self.attos = Some(0);
}
if self.offset.is_none() {
self.offset = Some(Offset::Utc);
}
return Ok(self);
}
// Sensible defaults for time components (most tests expect a full datetime)
if self.hr.is_none() {
self.hr = Some(0);
}
if self.min.is_none() {
self.min = Some(0);
}
if let Some(sec) = self.sec {
if sec == 60 {
self.is_leap_sec = true;
} else if sec > 60 {
return Err(an_err!(DtErrKind::OutOfRange, "seconds (0..=60): {}", sec));
}
} else {
self.sec = Some(0);
}
if self.attos.is_none() {
self.attos = Some(0);
}
if self.offset.is_none() {
self.offset = Some(Offset::Utc);
}
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(self)
}
}