deep_time/dt/from_str.rs
1use crate::{
2 ATTOS_PER_SEC_I128, Dt, DtErr, DtErrKind, Parts, SEC_PER_DAY, SEC_PER_MONTH, SEC_PER_WEEK,
3 SEC_PER_YEAR, Scale, StrPTimeFmt, an_err, dt,
4};
5use core::str::FromStr;
6
7#[cfg(feature = "parse")]
8use crate::ParseCfg;
9
10#[cfg(feature = "parse")]
11impl FromStr for Dt {
12 type Err = DtErr;
13
14 #[inline]
15 fn from_str(s: &str) -> Result<Self, DtErr> {
16 Dt::from_str_parse(s, &ParseCfg::DEFAULT)
17 }
18}
19
20#[cfg(not(feature = "parse"))]
21impl FromStr for Dt {
22 type Err = DtErr;
23
24 #[inline]
25 fn from_str(s: &str) -> Result<Self, DtErr> {
26 Self::from_str(s)
27 }
28}
29
30struct ParsedComponent {
31 unit: u8,
32 signed_int: i64,
33 frac_digits: usize,
34 frac_num: i64,
35}
36
37impl Dt {
38 /// Parses a date/time string.
39 ///
40 /// - When the `parse` feature is enabled: uses the smart auto-parser.
41 /// - When the `parse` feature is disabled: falls back to the fast ISO 8601 parser
42 /// ([`Dt::from_str`](../struct.Dt.html#method.from_str)).
43 ///
44 /// ## Examples
45 ///
46 /// ```rust
47 /// use deep_time::{Dt, Scale};
48 ///
49 /// // uses impl FromStr but Dt::parse provides the same functionality
50 /// let x: Dt = "2000-01-01 12:00:00".parse().unwrap();
51 ///
52 /// let ymd = x.to_ymd();
53 /// assert_eq!(ymd.yr(), 2000);
54 /// assert_eq!(ymd.mo(), 1);
55 /// assert_eq!(ymd.day(), 1);
56 /// assert_eq!(ymd.hr(), 12);
57 /// assert_eq!(ymd.min(), 0);
58 /// assert_eq!(ymd.sec(), 0);
59 /// assert_eq!(ymd.attos(), 0);
60 /// ```
61 ///
62 /// ## See also
63 ///
64 /// - [`Dt::from_str_parse`](../struct.Dt.html#method.from_str_parse)
65 /// - [`Dt::from_str`](../struct.Dt.html#method.from_str)
66 #[inline(always)]
67 pub fn parse(s: &str) -> Result<Self, DtErr> {
68 #[cfg(feature = "parse")]
69 {
70 Self::from_str_parse(s, &ParseCfg::DEFAULT)
71 }
72 #[cfg(not(feature = "parse"))]
73 {
74 Self::from_str(s)
75 }
76 }
77
78 /// Parser equivalent to `strptime` with a provided format string.
79 ///
80 /// The returned [`Dt`] will be on the `TAI` time scale, converted from whatever
81 /// optional time scale (`%L`) was provided in the input. If no time scale was
82 /// provided then it's converted from `UTC` -> `TAI`.
83 ///
84 /// The result is that the [`Dt`]'s `scale` field will be `TAI` and its `target`
85 /// field will be whatever time scale it was converted from (`UTC` if no time
86 /// scale was in the input).
87 ///
88 /// ## Parameters
89 ///
90 /// - `fmt`: The format string containing `%` directives.
91 /// - `input`: The string to parse.
92 /// - `inp_can_end_before_fmt`: If `true`, the input may end before the format
93 /// string is fully consumed (extra format specifiers are ignored).
94 /// - `fmt_can_end_before_inp`: If `true`, the format may end before the input
95 /// is fully consumed (trailing characters in the input are allowed).
96 /// - `allow_partial_date`: If `true`, a missing month/day will be defaulted
97 /// to `1` instead of returning a [`DtErrKind::Incomplete`](../error/enum.DtErrKind.html#variant.Incomplete) error.
98 ///
99 /// ## Supported Directives
100 ///
101 /// The format string supports literal characters and the following `%` directives.
102 /// Literal non-whitespace characters must match the input exactly.
103 /// Whitespace in the format matches (and consumes) any leading ASCII whitespace in the input.
104 ///
105 /// Many directives accept **format extensions** right after `%`:
106 /// - **Flags**: `-` (no pad), `_` (space pad), `0` (zero pad), `^`/`#` (treated as default)
107 /// - **Width**: 1–3 digits (affects numeric field width / padding expectations)
108 /// - **Colons** (only for `%z`): `:`, `::`, `:::` to control offset format
109 ///
110 /// ### Year / Century / Unbounded
111 /// - `%Y` — Four-digit year (e.g. `2024`). Supports sign, flags, and width.
112 /// - `%y` — Two-digit year (`00`–`99`; `00`–`68` → 2000+, `69`–`99` → 1900s).
113 /// - `%C` — Century (`00`–`99`).
114 /// - `%G` — Four-digit ISO week-based year.
115 /// - `%g` — Two-digit ISO week-based year (same century rule as `%y`).
116 /// - `%*` — **Unbounded year** (arbitrary length, supports negative years). *Library extension.*
117 ///
118 /// ### Month
119 /// - `%m` — Month number `01`–`12`.
120 /// - `%B` — Full English month name (e.g. `January`).
121 /// - `%b`, `%h` — Abbreviated English month name (3 letters, e.g. `Jan`).
122 ///
123 /// ### Day
124 /// - `%d`, `%e` — Day of month `01`–`31` (`%e` allows space padding).
125 /// - `%j` — Day of year `001`–`366`.
126 ///
127 /// ### Time of day
128 /// - `%H`, `%k` — Hour `00`–`23` (24-hour clock; `%k` allows space padding).
129 /// - `%I`, `%l` — Hour `01`–`12` (12-hour clock).
130 /// - `%M` — Minute `00`–`59`.
131 /// - `%S` — Second `00`–`60` (leap second allowed).
132 /// - `%f`, `%N` — Fractional seconds (up to 18 digits = attoseconds).
133 /// Width controls precision (`%3f` = ms, `%6N` = µs, `%9f` = ns, etc.).
134 /// Both accept an optional leading `.` in the input.
135 /// - `%.f`, `%.N`, `%.3f`, `%.6N`, ... — Same fractional parsing, but the
136 /// dot before the fraction is **optional** in the input (consumes literal `.` if present).
137 /// - `%P`, `%p` — `AM`/`PM` indicator (case-insensitive).
138 ///
139 /// ### Weekday / Week number
140 /// - `%A` — Full English weekday name (e.g. `Monday`).
141 /// - `%a` — Abbreviated English weekday name (3 letters, e.g. `Mon`).
142 /// - `%u` — Weekday number Monday=`1` … Sunday=`7`.
143 /// - `%w` — Weekday number Sunday=`0` … Saturday=`6`.
144 /// - `%U` — Week number (Sunday-first week), `00`–`53`.
145 /// - `%W` — Week number (Monday-first week), `00`–`53`.
146 /// - `%V` — ISO 8601 week number `01`–`53`.
147 ///
148 /// ### Timezone, Offset & Scale
149 /// - `%z` — Timezone offset. Colon count selects format:
150 /// - `%z` → `±HH[MM[SS]]` (minutes/seconds optional)
151 /// - `%:z` → `±HH:MM` (minutes required)
152 /// - `%::z` → `±HH:MM:SS` (seconds optional)
153 /// - `%:::z` → `±HH:MM:SS` (more flexible)
154 /// - `%Q` — IANA timezone name (e.g. `America/New_York`) **or** numeric offset
155 /// (if input starts with `+`/`-`). *Library extension.*
156 /// - `%L` — Time scale abbreviation (e.g. `TAI`, `UTC`, `GPS`). See [`Scale`].
157 /// *Library extension.*
158 ///
159 /// ### Shortcuts (compound directives)
160 /// - `%F` — Equivalent to `%Y-%m-%d` (ISO date).
161 /// - `%D` — Equivalent to `%m/%d/%y` (US date).
162 /// - `%T` — Equivalent to `%H:%M:%S`.
163 /// - `%R` — Equivalent to `%H:%M`.
164 ///
165 /// ### Other
166 /// - `%%` — Literal `%` character.
167 /// - `%s` — Unix timestamp (seconds since 1970-01-01 00:00 UTC, can be negative).
168 /// This directive greedily consumes any fractional seconds.
169 /// - `%J` — Seconds since 2000-01-01 12:00 TAI (2000-01-01 noon epoch), can be
170 /// negative.
171 /// This directive greedily consumes any fractional seconds.
172 /// - `%n`, `%t` — Any whitespace (consumes it from input).
173 ///
174 /// ### Unsupported / Unknown
175 /// - `%c`, `%r`, `%x`, `%X`, `%Z` → [`DtErrKind::UnsupportedItem`]
176 /// - Any other unknown directive character → [`DtErrKind::UnknownItem`]
177 ///
178 /// ## Errors
179 ///
180 /// Returns a [`DtErr`] if either the strptime-style parser or the subsequent
181 /// conversion from [`Parts`] to [`Dt`] fails.
182 ///
183 /// ### Format string errors
184 ///
185 /// - [`DtErrKind::TruncatedDirective`] — A `%` appeared at the end of the format
186 /// string, or after flags/width/colons with no directive character following it.
187 /// - [`DtErrKind::UnexpectedEnd`] — A `%` was followed only by extensions with no
188 /// directive character.
189 /// - [`DtErrKind::InvalidFractional`] — A `%.` fractional directive was followed by
190 /// an invalid character (not `f` or `N`).
191 /// - [`DtErrKind::ExpectedFractional`] — A `%.` fractional directive was started
192 /// but no directive character followed the dot.
193 /// - [`DtErrKind::UnsupportedItem`] — The format contains `%c`, `%r`, `%x`, `%X`,
194 /// or `%Z`.
195 /// - [`DtErrKind::UnknownItem`] — The format contains an unrecognized `%` directive.
196 ///
197 /// ### Input parsing errors
198 ///
199 /// - [`DtErrKind::UnexpectedEnd`] — The input ended before a required value could
200 /// be parsed.
201 /// - `Expected*` variants:
202 /// - [`DtErrKind::ExpectedYear`], [`DtErrKind::ExpectedCentury`],
203 /// [`DtErrKind::ExpectedMonth`], [`DtErrKind::ExpectedDay`],
204 /// [`DtErrKind::ExpectedDayOfYear`], [`DtErrKind::ExpectedHour`],
205 /// [`DtErrKind::ExpectedMinute`], [`DtErrKind::ExpectedSecond`],
206 /// [`DtErrKind::ExpectedFractional`], [`DtErrKind::ExpectedTimestamp`],
207 /// [`DtErrKind::ExpectedWeekNumber`], [`DtErrKind::ExpectedMonWeekday`],
208 /// [`DtErrKind::ExpectedSunWeekday`], [`DtErrKind::ExpectedMonWeek`],
209 /// [`DtErrKind::ExpectedSunWeek`]
210 /// - Out-of-range errors:
211 /// - [`DtErrKind::MonthOutOfRange`], [`DtErrKind::DayOutOfRange`],
212 /// [`DtErrKind::DayOfYearOutOfRange`], [`DtErrKind::HourOutOfRange`],
213 /// [`DtErrKind::MinuteOutOfRange`], [`DtErrKind::SecondOutOfRange`],
214 /// [`DtErrKind::IsoWeekOutOfRange`], [`DtErrKind::MonWeekdayOutOfRange`],
215 /// [`DtErrKind::SunWeekdayOutOfRange`]
216 /// - [`DtErrKind::MismatchedLiteral`] — A literal character in the format string
217 /// did not match the input.
218 /// - Name errors: [`DtErrKind::InvalidMonthName`], [`DtErrKind::InvalidWeekdayName`],
219 /// [`DtErrKind::InvalidMeridiem`].
220 ///
221 /// ### Timezone and Offset errors
222 ///
223 /// - [`DtErrKind::OffsetMissingSign`] — A timezone offset (`%z` / `%Q`) did not
224 /// start with `+` or `-`.
225 /// - [`DtErrKind::InvalidOffsetHour`] — Invalid hour value in a timezone offset.
226 /// - [`DtErrKind::InvalidOffsetMinute`] — Invalid minute value in a timezone offset.
227 /// - [`DtErrKind::InvalidOffsetSecond`] — Invalid second value in a timezone offset.
228 /// - [`DtErrKind::InvalidOffsetColons`] — Incorrect number of colons or missing
229 /// required colon in a timezone offset.
230 /// - [`DtErrKind::InvalidOffset`] — General failure while parsing a numeric
231 /// timezone offset.
232 /// - [`DtErrKind::InvalidTimeZone`] — Invalid or unparseable IANA timezone name
233 /// (used by the `%Q` directive).
234 ///
235 /// ### Post-processing / validation errors
236 ///
237 /// - [`DtErrKind::TrailingCharacters`] — The input contained trailing characters
238 /// after parsing and `fmt_can_end_before_inp` was `false`.
239 /// - [`DtErrKind::Incomplete`] — Required date components (month or day) were
240 /// missing and `allow_partial_date` was `false`.
241 ///
242 /// ### Conversion to [`Dt`] errors
243 ///
244 /// These errors can occur *after* successful parsing, inside [`Parts::to_dt`]:
245 ///
246 /// - [`DtErrKind::InvalidDate`] or [`DtErrKind::InvalidInput`] — Unable to
247 /// construct a valid date from the parsed components.
248 /// - Out-of-range or conflicting field errors (e.g. [`DtErrKind::DayOfYearOutOfRange`],
249 /// [`DtErrKind::IsoWeekOutOfRange`], [`DtErrKind::WeekOutOfRange`], etc.).
250 /// - [`DtErrKind::InvalidItem`] — ISO week 53 requested for a year that does not
251 /// contain 53 ISO weeks.
252 /// - Feature-dependent errors (when `jiff-tz` is involved):
253 /// - [`DtErrKind::InvalidTimeZone`], [`DtErrKind::InvalidNumber`],
254 /// [`DtErrKind::InvalidBytes`].
255 ///
256 /// The error kind is available via [`DtErr::kind()`].
257 #[inline(always)]
258 pub fn from_strptime(
259 s: &str,
260 fmt: &str,
261 inp_can_end_before_fmt: bool,
262 fmt_can_end_before_inp: bool,
263 allow_partial_date: bool,
264 ) -> Result<Dt, DtErr> {
265 Parts::from_strptime(
266 fmt,
267 s,
268 inp_can_end_before_fmt,
269 fmt_can_end_before_inp,
270 allow_partial_date,
271 )?
272 .to_dt()
273 }
274
275 /// Parses and validates a `strptime`-style format string into a reusable [`StrPTimeFmt`].
276 ///
277 /// The format is checked once for syntax errors and unsupported directives,
278 /// then stored in a compact fixed-size buffer. The resulting `StrPTimeFmt` is
279 /// can be used repeatedly with
280 /// [`StrPTimeFmt::to_dt`](../struct.StrPTimeFmt.html#method.to_dt)
281 /// and
282 /// [`StrPTimeFmt::to_str`](../struct.StrPTimeFmt.html#method.to_str)
283 /// without re-validating.
284 ///
285 /// - This unfortunately doesn't improve parsing performance.
286 /// - Only ASCII formats up to
287 /// [`StrPTimeFmt::MAX_FMT_LEN`](../struct.StrPTimeFmt.html#associatedconstant.MAX_FMT_LEN)
288 /// bytes are accepted.
289 ///
290 /// ## Parameters
291 ///
292 /// - `strptime_fmt`: The format string using `%` directives (e.g. `"%Y-%m-%d %H:%M:%S"`,
293 /// `"%F %T"`, `"%Y-%m-%dT%H:%M:%S%.3fZ"`).
294 ///
295 /// ## Errors
296 ///
297 /// Returns [`DtErr`] if the format is:
298 /// - Longer than
299 /// [`StrPTimeFmt::MAX_FMT_LEN`](../struct.StrPTimeFmt.html#associatedconstant.MAX_FMT_LEN)
300 /// bytes.
301 /// - Not valid ASCII.
302 /// - Contains unknown, unsupported, or malformed directives.
303 #[inline(always)]
304 pub fn parse_fmt(strptime_fmt: &str) -> Result<StrPTimeFmt, DtErr> {
305 StrPTimeFmt::new(strptime_fmt)
306 }
307
308 /// Fast, no-alloc parser for common ISO-like and epoch-style date-time strings.
309 ///
310 /// Equivalent to [`Parts::from_str`](../civil_parts/struct.Parts.html#method.from_str)
311 /// followed by [`Parts::to_dt`](../civil_parts/struct.Parts.html#method.to_dt). The
312 /// formats and lenience rules below are those of the `Parts` parser; this method
313 /// then resolves components to a single instant.
314 ///
315 /// - Only **ASCII** input is supported.
316 /// - Inputs longer than [`STRTIME_SIZE`](../consts/constant.STRTIME_SIZE.html) are
317 /// rejected with [`DtErrKind::InvalidLen`](../error/enum.DtErrKind.html#variant.InvalidLen).
318 /// - Leading non-date junk is skipped until a year-like start (`digit` or `±`digit)
319 /// or a recognized alphabetic prefix (`JD` / `MJD` / `SEC`, case-insensitive).
320 /// - Trailing characters after a successful parse are generally ignored (lenient).
321 /// - Considerably faster than format-string / smart parsers when the input is one
322 /// of the shapes below.
323 /// - Timezones beyond UTC aliases require the `jiff-tz` or `jiff-tz-bundle` feature
324 /// (both require `alloc`).
325 ///
326 /// ## Returns
327 ///
328 /// Always a [`Dt`] on the **`TAI`** time scale (after any conversion).
329 ///
330 /// - No trailing scale + civil ISO-like input (calendar / DOY / ISO week) → interpret
331 /// as **UTC**, then convert **UTC → TAI** (leap seconds applied).
332 /// - No trailing scale + `SEC` / `JD` / `MJD` → interpret as **TAI** (no conversion).
333 /// - Trailing scale present (e.g. `TDB`, `GPS`) → convert **that scale → TAI**
334 /// (no-op if the scale is already `TAI`).
335 ///
336 /// The returned value’s `target` reflects the scale assumed before conversion to TAI.
337 ///
338 /// ## Supported formats
339 ///
340 /// An **optional** library time scale at the end of the input (e.g. `TAI`) is
341 /// supported for all of the formats below.
342 ///
343 /// ### ISO-like civil date-times
344 ///
345 /// #### Format examples
346 ///
347 /// - **`+2000-01-01T17:00:00 -0500 [America/New_York] TAI`**
348 /// - **`2024 Apr 18, 14:30:25 [America/New_York]`** — month abbrev or full English name
349 /// - **`2024-109 14:30:25`** — day of year (`%Y-%j`)
350 /// - **`2024-W11`**, **`2024W11`**, **`2024-W11-4`** — ISO week date (`%G-W%V`, optional
351 /// weekday `%u` with Monday=`1` … Sunday=`7`)
352 /// - **`2024`**, **`2024-03`**, **`2024 Mar`** — partial dates (missing month/day → `1`)
353 /// - **`2024-04-18T9:3:5.5`**, **`2024-04-18T143025`** — flexible / compact time
354 ///
355 /// #### Date forms (after an optional sign and year digits)
356 ///
357 /// Year digits are taken **literally** (no century window): `99-01-01` is year 99 AD.
358 /// Year overflow during accumulation yields
359 /// [`DtErrKind::YearOutOfRange`](../error/enum.DtErrKind.html#variant.YearOutOfRange).
360 ///
361 /// Exactly one of the following is used:
362 ///
363 /// 1. **ISO week** — `W`/`w` immediately after the year (or after a non-letter
364 /// separator such as `-`): e.g. `2024-W11`, `2024W114`.
365 /// - Week number required right after `W` (1–2 digits); `0` becomes week `1`.
366 /// - Optional weekday: `-4` or basic trailing digit `1..=7` (Monday=`1` … Sunday=`7`);
367 /// if omitted, Monday is used when resolving the instant.
368 /// - This is **not** strftime `%W` (Monday week-of-year on a calendar year).
369 /// 2. **Day of year** — three slots `[digit|space][digit|space][digit]` not followed
370 /// by another digit (so `2024-0401` is calendar `04-01`, not DOY). Space padding
371 /// is allowed (`2024- 9`).
372 /// 3. **Calendar month/day** — numeric month (1–2 digits) or English month name
373 /// (abbrev or full; matched from the first three letters), then day (1–2 digits).
374 ///
375 /// **Partial calendar dates** default missing fields to `1` (January / day 1):
376 ///
377 /// - Year only: `2024`, `2024-` → 1 January.
378 /// - Year-month: `2024-03`, `2024 Mar` → day 1.
379 /// - Explicit zero month/day digits are treated like omitted (`2024-00`, `2024-03-0` → 1).
380 /// - Year or year-month may be followed by time: `2024T12:00`, `2024-03T12:00`.
381 ///
382 /// #### Time (optional)
383 ///
384 /// - Usually introduced by `T`/`t`, space, or another non-digit separator; compact
385 /// glued times after a full day are also accepted (e.g. `…18143025`).
386 /// - Hour, minute, and second are **1 or 2** digits when the field ends at `:` /
387 /// space (or, for seconds, `.` before a fraction).
388 /// - Compact digit runs without separators are supported (e.g. `T143025`).
389 /// - Fractional seconds: `.` then digits (up to 18 kept as attoseconds; extra digits
390 /// ignored).
391 /// - Optional trailing `Z`/`z` is consumed.
392 /// - Minutes must be `≤ 59`; seconds must be `≤ 60` (leap second `60` allowed; resolved
393 /// by [`Parts::to_dt`](../civil_parts/struct.Parts.html#method.to_dt)).
394 ///
395 /// #### Optional trailing components
396 ///
397 /// - **Offset** — `+`/`-` then hours (and optional minutes), with or without `:`:
398 /// `+02:00`, `-0530`, also allowed directly after the date.
399 /// - **IANA name** — must be in square brackets, e.g. `[America/New_York]`.
400 /// Resolving non-UTC aliases requires the `jiff-tz` or `jiff-tz-bundle` feature
401 /// (both require `alloc`).
402 /// - **Scale** — library abbreviation, e.g. `TAI`, `UTC`, `TDB`, `GPS`.
403 ///
404 /// ### Seconds since 2000-01-01 noon (library epoch)
405 ///
406 /// #### Format examples
407 ///
408 /// - **`SEC 1234.567 TDB`**
409 /// - **`sec1234.5 TAI`**
410 ///
411 /// #### Notes
412 ///
413 /// - `sec` prefix is required (case-insensitive).
414 /// - Fractional seconds optional.
415 ///
416 /// ### Julian Date
417 ///
418 /// #### Format examples
419 ///
420 /// - **`JD 2451545.0 TAI`**
421 /// - **`JD2451545.25 TT`**
422 ///
423 /// #### Notes
424 ///
425 /// - `jd` prefix is required (case-insensitive).
426 /// - Fractional days optional.
427 ///
428 /// ### Modified Julian Date
429 ///
430 /// #### Format examples
431 ///
432 /// - **`MJD 51544.5 TT`**
433 /// - **`mjd 51544.25`**
434 ///
435 /// #### Notes
436 ///
437 /// - `mjd` prefix is required (case-insensitive).
438 /// - Fractional days optional.
439 ///
440 /// ## See also
441 ///
442 /// - [`Parts::from_str`](../civil_parts/struct.Parts.html#method.from_str) —
443 /// same string parse without resolving to a [`Dt`].
444 #[allow(clippy::should_implement_trait)]
445 #[inline(always)]
446 pub fn from_str(s: &str) -> Result<Self, DtErr> {
447 Parts::from_str(s)?.to_dt()
448 }
449
450 /// Parses a decimal seconds string (with optional fractional part) as seconds
451 /// since
452 /// [`Dt::ZERO`](../struct.Dt.html#associatedconstant.ZERO)
453 /// on the chosen time scale.
454 ///
455 /// The returned [`Dt`] is on the `TAI` time [`Scale`], having been converted
456 /// to `TAI` from whatever the **trailing** scale is, or if no scale is provided
457 /// then no conversion takes place.
458 ///
459 /// Leading non-numeric characters are skipped until a number start is found
460 /// (`+`, `-`, `.`, or digit).
461 ///
462 /// - Fractional seconds are limited to the first 18 digits (attosecond
463 /// precision); extra digits are truncated.
464 /// - Oversized integer parts saturate instead of failing.
465 /// - Inputs longer than [`STRTIME_SIZE`](../consts/constant.STRTIME_SIZE.html) are rejected.
466 /// - Returns `None` only for completely unparseable input (empty, sign/dot
467 /// only, no digits after skipping, etc.).
468 ///
469 /// ## Examples
470 ///
471 /// ```rust
472 /// use deep_time::{Dt, Scale};
473 ///
474 /// let d = Dt::from_str_sec_f("1700000000.123456789012345678", Some(Scale::TAI)).unwrap();
475 /// assert_eq!(d.to_sec64_floor(), 1700000000);
476 ///
477 /// // Leading junk is skipped
478 /// let d = Dt::from_str_sec_f("ts= -0.00123 suffix", Some(Scale::TAI)).unwrap();
479 /// assert!(d.to_attos() < 0);
480 ///
481 /// // Pure negative fraction
482 /// let d = Dt::from_str_sec_f("-.5", Some(Scale::TT)).unwrap();
483 /// assert!(d.to_attos() < 0);
484 ///
485 /// // Scale parsed from trailing abbreviation when passing None
486 /// let d = Dt::from_str_sec_f("42.75 GPS", None).unwrap();
487 /// assert_eq!(d.target, Scale::GPS);
488 ///
489 /// // 1 attosecond
490 /// let d = Dt::from_str_sec_f("0.000000000000000001", Some(Scale::TAI)).unwrap();
491 /// assert_eq!(d.to_attos() % 1_000_000_000_000_000_000, 1);
492 /// ```
493 pub fn from_str_sec_f(s: &str, scale: Option<Scale>) -> Option<Dt> {
494 let parsed = Parts::parse_str_f(s.as_bytes(), scale)?;
495
496 let int_attos = (parsed.int_u as i128) * ATTOS_PER_SEC_I128;
497 let signed_attos = if parsed.negative {
498 -int_attos - (parsed.frac_attos as i128)
499 } else {
500 int_attos + (parsed.frac_attos as i128)
501 };
502
503 Some(Dt::new(signed_attos, parsed.scale, parsed.scale).to_tai())
504 }
505
506 /// Parses a decimal Julian Date string (with optional fractional part).
507 ///
508 /// The returned [`Dt`] is on the `TAI` time [`Scale`], having been converted
509 /// to `TAI` from whatever the **trailing** scale is, or if no scale is provided
510 /// then no conversion takes place.
511 ///
512 /// Leading junk is skipped the same way as [`Dt::from_str_sec_f`].
513 /// Fractional day precision up to 18 digits.
514 ///
515 /// Returns `None` for unparseable input.
516 ///
517 /// JD 2451545.0 is the library epoch (2000-01-01 noon).
518 ///
519 /// ## Examples
520 ///
521 /// ```rust
522 /// use deep_time::{Dt, Scale};
523 ///
524 /// let d = Dt::from_str_jd_f("2451545.0", Some(Scale::TAI)).unwrap();
525 /// assert_eq!(d.to_jd(), (2_451_545, 0));
526 ///
527 /// let d = Dt::from_str_jd_f("2451545.25 TT", None).unwrap();
528 /// assert_eq!(d.target, Scale::TT);
529 ///
530 /// let d = Dt::from_str_jd_f("2451544.5", Some(Scale::TAI)).unwrap();
531 /// assert!(d.to_attos() < 0);
532 /// ```
533 pub fn from_str_jd_f(s: &str, scale: Option<Scale>) -> Option<Dt> {
534 Parts::from_str_jd_f(s, scale).and_then(|p| p.to_dt().ok())
535 }
536
537 /// Parses a decimal Modified Julian Date string (with optional fractional part).
538 ///
539 /// The returned [`Dt`] is on the `TAI` time [`Scale`], having been converted
540 /// to `TAI` from whatever the **trailing** scale is, or if no scale is provided
541 /// then no conversion takes place.
542 ///
543 /// Leading junk is skipped the same way as [`Dt::from_str_sec_f`].
544 /// Fractional day precision up to 18 digits.
545 ///
546 /// Returns `None` for unparseable input.
547 ///
548 /// MJD 51544.5 is the library epoch (2000-01-01 noon).
549 ///
550 /// ## Examples
551 ///
552 /// ```rust
553 /// use deep_time::{Dt, Scale};
554 ///
555 /// let d = Dt::from_str_mjd_f("51544.5", Some(Scale::TAI)).unwrap();
556 /// assert_eq!(d.to_jd(), (2_451_545, 0));
557 ///
558 /// let d = Dt::from_str_mjd_f("51544.25 TT", None).unwrap();
559 /// assert_eq!(d.target, Scale::TT);
560 ///
561 /// let d = Dt::from_str_mjd_f("51543.5", Some(Scale::TAI)).unwrap();
562 /// assert!(d.to_attos() < 0);
563 /// ```
564 pub fn from_str_mjd_f(s: &str, scale: Option<Scale>) -> Option<Dt> {
565 Parts::from_str_mjd_f(s, scale).and_then(|p| p.to_dt().ok())
566 }
567
568 /// Parses an ISO 8601 duration string into a [`Dt`] representing a pure time interval.
569 ///
570 /// Supports the full `PnYnMnDTnHnMnS` format (case-insensitive), including:
571 /// - Optional leading `+` or `-` sign
572 /// - `P` / `p` prefix (required)
573 /// - Optional `T` / `t` separator between date and time parts
574 /// - Weeks (`W` / `w`)
575 /// - Fractional seconds with up to 9 digits of precision (nanosecond resolution;
576 /// the parsed value is scaled to attosecond resolution in the resulting [`Dt`]).
577 ///
578 /// The returned [`Dt`] is a **duration** (signed interval) on the TAI scale.
579 /// It can be added to/subtracted from other `Dt` values, multiplied/divided,
580 /// rounded, etc.
581 ///
582 /// ## Not Reference-Time Aware
583 ///
584 /// This parser is **not reference-time aware**. Calendar units (`Y`, `M`) are
585 /// converted to a fixed number of seconds using standard average lengths
586 /// rather than being resolved against a specific date. This makes parsing
587 /// fast and allocation-free, but `P1M` always represents exactly the same
588 /// duration regardless of context.
589 ///
590 /// ## Parameters
591 ///
592 /// - `s`: The ISO 8601 duration string (e.g. `"P1Y2M3DT4H5M6.123456789012345678S"`,
593 /// `"-PT30M"`, `"P7W"`, `"+P1DT12H"`).
594 ///
595 /// ## Errors
596 ///
597 /// Returns a [`DtErr`] if parsing fails. The error kind is available via
598 /// [`DtErr::kind()`].
599 ///
600 /// ### Input / structure errors
601 ///
602 /// - [`DtErrKind::Empty`] — The input string is empty.
603 /// - [`DtErrKind::MustStartWith`] — Missing `P` / `p` prefix (after optional leading sign).
604 /// - [`DtErrKind::InvalidSyntax`] — Invalid syntax, e.g. `T` with no following time part,
605 /// or more than one `T`/`t` separator.
606 /// - [`DtErrKind::TrailingCharacters`] — Additional components appear after a fractional
607 /// seconds value (only the final `S` component may carry a fraction).
608 ///
609 /// ### Component parsing errors
610 ///
611 /// - [`DtErrKind::ExpectedValue`] — Expected a numeric value for a component but found none.
612 /// - [`DtErrKind::ExpectedFractional`] — A `.` or `,` was present for a fractional part
613 /// but no digits followed.
614 /// - [`DtErrKind::ExpectedUnit`] — A number was parsed but no unit designator
615 /// (`Y`/`M`/`W`/`D`/`H`/`S` etc.) followed it.
616 /// - [`DtErrKind::InvalidNumber`] — A numeric component could not be parsed as an `i64`
617 /// (typically too large).
618 /// - [`DtErrKind::InvalidBytes`] — Internal UTF-8 conversion failure while reading a number
619 /// (should not occur for valid ASCII input).
620 /// - [`DtErrKind::InvalidFractional`] — The fractional part digits could not be parsed as an integer.
621 /// - [`DtErrKind::FracOutOfRange`] — More than 9 digits were supplied for fractional seconds.
622 /// - [`DtErrKind::InvalidItem`] — A fractional part was supplied on a unit other than seconds.
623 ///
624 /// ### Unit and range errors
625 ///
626 /// - [`DtErrKind::UnknownItem`] — An unknown unit designator character was used.
627 /// - [`DtErrKind::YearOutOfRange`], [`DtErrKind::MonthOutOfRange`],
628 /// [`DtErrKind::WeekOutOfRange`], [`DtErrKind::DayOutOfRange`] — The component value
629 /// (after sign) overflows when multiplied by the corresponding fixed-length constant
630 /// (checked arithmetic).
631 pub fn from_iso_duration(s: &str) -> Result<Dt, DtErr> {
632 let len = s.len();
633 if len == 0 {
634 return Err(an_err!(DtErrKind::Empty));
635 }
636
637 let b = s.as_bytes();
638 let mut i = 0usize;
639
640 // Optional leading sign (+ or -)
641 let mut sign: i64 = 1;
642 if i < len && matches!(b[i], b'+' | b'-') {
643 if b[i] == b'-' {
644 sign = -1;
645 }
646 i += 1;
647 }
648
649 // Must start with P/p
650 if i >= len || !matches!(b[i], b'P' | b'p') {
651 return Err(an_err!(DtErrKind::MustStartWith));
652 }
653 i += 1;
654
655 // Find the (single) T/t separator
656 let t_pos = b[i..]
657 .iter()
658 .position(|&c| matches!(c, b'T' | b't'))
659 .map(|p| i + p);
660
661 let (date_part, time_part) = match t_pos {
662 Some(pos) => {
663 if pos == len - 1 {
664 return Err(an_err!(DtErrKind::InvalidSyntax));
665 }
666 if b[pos + 1..].iter().any(|&c| matches!(c, b'T' | b't')) {
667 return Err(an_err!(DtErrKind::InvalidSyntax));
668 }
669 (&b[i..pos], &b[pos + 1..])
670 }
671 None => (&b[i..], &[] as &[u8]),
672 };
673
674 let mut has_fraction = false;
675 let mut total_nanos: i128 = 0;
676
677 // Both date and time parts now use the same fixed-length logic
678 Self::parse_duration_part(date_part, &mut total_nanos, true, sign, &mut has_fraction)?;
679 Self::parse_duration_part(time_part, &mut total_nanos, false, sign, &mut has_fraction)?;
680
681 // Convert accumulated nanoseconds to attoseconds and build Dt
682 let total_attos = total_nanos * 1_000_000_000i128;
683 Ok(dt!(total_attos))
684 }
685
686 /// Parses a single component (number + optional fraction + unit) from the slice,
687 /// advancing the index `i`. Returns `None` when the slice is exhausted.
688 fn parse_next_component(
689 chars: &[u8],
690 i: &mut usize,
691 sign: i64,
692 has_fraction: &mut bool,
693 ) -> Result<Option<ParsedComponent>, DtErr> {
694 if *i >= chars.len() {
695 return Ok(None);
696 }
697
698 if *has_fraction {
699 return Err(an_err!(DtErrKind::TrailingCharacters));
700 }
701
702 // Parse integer part
703 let start = *i;
704 while *i < chars.len() && chars[*i].is_ascii_digit() {
705 *i += 1;
706 }
707 if start == *i {
708 return Err(an_err!(DtErrKind::ExpectedValue));
709 }
710
711 let int_str = core::str::from_utf8(&chars[start..*i])
712 .map_err(|e| an_err!(DtErrKind::InvalidBytes, "{}", e))?;
713 let int: i64 = int_str.parse().map_err(|e: core::num::ParseIntError| {
714 an_err!(DtErrKind::InvalidNumber, "{}: {}", int_str, e)
715 })?;
716
717 // Parse optional fraction
718 let mut frac_num: i64 = 0;
719 let mut frac_digits: usize = 0;
720 if *i < chars.len() && matches!(chars[*i], b'.' | b',') {
721 *i += 1;
722 let frac_start = *i;
723 while *i < chars.len() && chars[*i].is_ascii_digit() {
724 *i += 1;
725 }
726 frac_digits = *i - frac_start;
727 if frac_digits == 0 {
728 return Err(an_err!(DtErrKind::ExpectedFractional));
729 }
730 if frac_digits > 9 {
731 return Err(an_err!(DtErrKind::FracOutOfRange));
732 }
733
734 let frac_str = core::str::from_utf8(&chars[frac_start..*i])
735 .map_err(|e| an_err!(DtErrKind::InvalidBytes, "{}", e))?;
736 frac_num = frac_str.parse().map_err(|e: core::num::ParseIntError| {
737 an_err!(DtErrKind::InvalidFractional, "{}: {}", frac_str, e)
738 })?;
739 }
740
741 // Unit must follow
742 if *i >= chars.len() {
743 return Err(an_err!(DtErrKind::ExpectedUnit));
744 }
745 let unit = chars[*i];
746 *i += 1;
747
748 // Only seconds support a fractional part
749 if frac_digits > 0 {
750 if !matches!(unit, b'S' | b's') {
751 return Err(an_err!(DtErrKind::InvalidItem));
752 }
753 *has_fraction = true;
754 }
755
756 let signed_int = (int as i128 * sign as i128) as i64;
757
758 Ok(Some(ParsedComponent {
759 unit,
760 signed_int,
761 frac_digits,
762 frac_num,
763 }))
764 }
765
766 /// Helper that parses **one section** of an ISO duration (date or time part)
767 /// and accumulates nanoseconds into `total_nanos`.
768 ///
769 /// Years, months, weeks, and days are converted using the fixed-length
770 /// constants (the only sensible semantics for a pure `Dt`).
771 fn parse_duration_part(
772 chars: &[u8],
773 total_nanos: &mut i128,
774 is_date: bool,
775 sign: i64,
776 has_fraction: &mut bool,
777 ) -> Result<(), DtErr> {
778 let mut i = 0;
779 while let Some(comp) = Self::parse_next_component(chars, &mut i, sign, has_fraction)? {
780 let contrib_nanos = match (is_date, comp.unit) {
781 (true, b'Y' | b'y') => {
782 let total_sec = (comp.signed_int as i128)
783 .checked_mul(SEC_PER_YEAR)
784 .ok_or_else(|| an_err!(DtErrKind::YearOutOfRange))?;
785 total_sec * 1_000_000_000i128
786 }
787 (true, b'M' | b'm') => {
788 let total_sec = (comp.signed_int as i128)
789 .checked_mul(SEC_PER_MONTH)
790 .ok_or_else(|| an_err!(DtErrKind::MonthOutOfRange))?;
791 total_sec * 1_000_000_000i128
792 }
793 (true, b'W' | b'w') => {
794 let total_sec = (comp.signed_int as i128)
795 .checked_mul(SEC_PER_WEEK as i128)
796 .ok_or_else(|| an_err!(DtErrKind::WeekOutOfRange))?;
797 total_sec * 1_000_000_000i128
798 }
799 (true, b'D' | b'd') => {
800 let total_sec = (comp.signed_int as i128)
801 .checked_mul(SEC_PER_DAY)
802 .ok_or_else(|| an_err!(DtErrKind::DayOutOfRange))?;
803 total_sec * 1_000_000_000i128
804 }
805 (false, b'H' | b'h') => (comp.signed_int as i128) * 3_600_000_000_000i128,
806 (false, b'M' | b'm') => (comp.signed_int as i128) * 60_000_000_000i128,
807 (false, b'S' | b's') => {
808 let mut sec_nanos = (comp.signed_int as i128) * 1_000_000_000i128;
809 if comp.frac_digits > 0 {
810 let frac_ns = (comp.frac_num as i128 * sign as i128 * 1_000_000_000i128)
811 / 10i128.pow(comp.frac_digits as u32);
812 sec_nanos += frac_ns;
813 }
814 sec_nanos
815 }
816 _ => {
817 return Err(an_err!(DtErrKind::UnknownItem, "{}", comp.unit as char));
818 }
819 };
820
821 *total_nanos = total_nanos.saturating_add(contrib_nanos);
822 }
823 Ok(())
824 }
825
826 /// Parses a media-style duration string.
827 ///
828 /// Accepts formats like:
829 /// - `"0:45"`, `"9:41"`
830 /// - `"1:23:45"`
831 /// - `"1:07:54:30"`
832 /// - `"-1:23:45"`
833 ///
834 /// ## Errors
835 ///
836 /// Returns a [`DtErr`] if the input cannot be parsed as a valid media-style
837 /// duration. The error kind is available via [`DtErr::kind`].
838 ///
839 /// This function uses saturating arithmetic, so it never returns range or
840 /// overflow errors.
841 ///
842 /// ### Input / structure errors
843 ///
844 /// - [`DtErrKind::Empty`] — The string is empty or contains only ASCII whitespace.
845 /// - [`DtErrKind::InvalidInput`] — A single minus sign with nothing after it.
846 /// - [`DtErrKind::InvalidSyntax`] — The input does not contain exactly 2, 3, or 4
847 /// colon-separated numeric components.
848 /// - [`DtErrKind::TrailingCharacters`] — Non-whitespace characters remain after
849 /// the final numeric component.
850 ///
851 /// ### Parsing errors
852 ///
853 /// - [`DtErrKind::ExpectedValue`] — A component was expected to begin with a digit
854 /// (either at the start of the string or immediately after a `:`) but did not.
855 ///
856 /// ## See also
857 ///
858 /// - [`Dt::to_str_media_duration`](../struct.Dt.html#method.to_str_media_duration)
859 /// - [`Dt::to_str_b_media_duration`](../struct.Dt.html#method.to_str_b_media_duration)
860 pub fn from_str_media_duration(input: &str) -> Result<Dt, DtErr> {
861 let bytes = input.as_bytes();
862 let len = bytes.len();
863 let mut pos: usize = 0;
864
865 // Skip leading whitespace
866 while pos < len && bytes[pos].is_ascii_whitespace() {
867 pos += 1;
868 }
869
870 if pos == len {
871 return Err(an_err!(DtErrKind::Empty));
872 }
873
874 // Optional single leading minus
875 let negative = if bytes[pos] == b'-' {
876 pos += 1;
877 if pos == len {
878 return Err(an_err!(DtErrKind::InvalidInput));
879 }
880 true
881 } else {
882 false
883 };
884
885 // Parse up to 4 numeric components separated by ':'
886 let mut components: [i128; 4] = [0; 4];
887 let mut count: usize = 0;
888
889 loop {
890 if count >= 4 {
891 break;
892 }
893
894 // Parse one number
895 if pos >= len || !bytes[pos].is_ascii_digit() {
896 return Err(an_err!(DtErrKind::ExpectedValue));
897 }
898
899 let mut value: i128 = 0;
900 while pos < len && bytes[pos].is_ascii_digit() {
901 value = value
902 .saturating_mul(10)
903 .saturating_add((bytes[pos] - b'0') as i128);
904 pos += 1;
905 }
906
907 components[count] = value;
908 count += 1;
909
910 // Check for more components
911 if pos >= len || bytes[pos] != b':' {
912 break;
913 }
914
915 pos += 1; // consume ':'
916
917 // Reject trailing ':' with no number after it
918 if pos >= len || !bytes[pos].is_ascii_digit() {
919 return Err(an_err!(DtErrKind::ExpectedValue));
920 }
921 }
922
923 if !(2..=4).contains(&count) {
924 return Err(an_err!(DtErrKind::InvalidSyntax));
925 }
926
927 // Skip trailing whitespace
928 while pos < len && bytes[pos].is_ascii_whitespace() {
929 pos += 1;
930 }
931
932 if pos != len {
933 return Err(an_err!(DtErrKind::TrailingCharacters));
934 }
935
936 // Convert to total seconds
937 let total_sec: i128 = match count {
938 2 => components[0] * 60 + components[1], // M:SS
939 3 => components[0] * 3600 + components[1] * 60 + components[2], // H:MM:SS
940 4 => components[0] * 86400 + components[1] * 3600 + components[2] * 60 + components[3], // D:H:MM:SS
941 _ => unreachable!(),
942 };
943
944 let total_sec = if negative { -total_sec } else { total_sec };
945 let attos = total_sec.saturating_mul(ATTOS_PER_SEC_I128);
946
947 Ok(dt!(attos))
948 }
949
950 /// Hours:Minutes elapsed time: `H:MM` or `H:MM:SS[.frac]` (hours unbounded).
951 ///
952 /// Minutes and seconds must be `0..=59`. Fractional seconds (optional) may
953 /// follow only the seconds field, up to 18 digits. Requires at least one
954 /// `:`. Optional leading `-` and ASCII whitespace. Returns a pure duration
955 /// on `Scale::TAI`.
956 ///
957 /// Not media duration: two fields are **hours:minutes**, not minutes:seconds.
958 #[inline]
959 pub fn from_str_h_mm_duration(input: &str) -> Result<Dt, DtErr> {
960 Ok(dt!(Self::h_mm_duration_attos(input)?))
961 }
962
963 /// Shared by [`from_str_h_mm_duration`] and relative natural parse.
964 pub(crate) fn h_mm_duration_attos(input: &str) -> Result<i128, DtErr> {
965 let bytes = input.as_bytes();
966 let len = bytes.len();
967 let mut pos = 0usize;
968
969 while pos < len && bytes[pos].is_ascii_whitespace() {
970 pos += 1;
971 }
972 if pos == len {
973 return Err(an_err!(DtErrKind::Empty));
974 }
975
976 let neg = if bytes[pos] == b'-' {
977 pos += 1;
978 if pos == len {
979 return Err(an_err!(DtErrKind::InvalidInput));
980 }
981 true
982 } else {
983 false
984 };
985
986 let hours = parse_mil_uint(bytes, &mut pos, len)?;
987 // At least one colon is required (H:MM or H:MM:SS).
988 if pos >= len || bytes[pos] != b':' {
989 return Err(an_err!(DtErrKind::InvalidSyntax));
990 }
991 pos += 1;
992
993 let minutes = parse_mil_uint(bytes, &mut pos, len)?;
994 if minutes > 59 {
995 return Err(an_err!(DtErrKind::InvalidInput));
996 }
997
998 let mut seconds: i128 = 0;
999 let mut frac_attos: i128 = 0;
1000
1001 if pos < len && bytes[pos] == b':' {
1002 pos += 1;
1003 seconds = parse_mil_uint(bytes, &mut pos, len)?;
1004 if seconds > 59 {
1005 return Err(an_err!(DtErrKind::InvalidInput));
1006 }
1007 if pos < len && bytes[pos] == b'.' {
1008 pos += 1;
1009 if pos >= len || !bytes[pos].is_ascii_digit() {
1010 return Err(an_err!(DtErrKind::ExpectedValue));
1011 }
1012 let mut digits = 0u32;
1013 let mut frac = 0i128;
1014 while pos < len && bytes[pos].is_ascii_digit() {
1015 if digits < 18 {
1016 frac = frac
1017 .saturating_mul(10)
1018 .saturating_add((bytes[pos] - b'0') as i128);
1019 digits += 1;
1020 }
1021 pos += 1;
1022 }
1023 frac_attos = frac.saturating_mul(10i128.pow(18 - digits));
1024 }
1025 } else if pos < len && bytes[pos] == b'.' {
1026 // No fractional minutes.
1027 return Err(an_err!(DtErrKind::InvalidSyntax));
1028 }
1029
1030 while pos < len && bytes[pos].is_ascii_whitespace() {
1031 pos += 1;
1032 }
1033 if pos != len {
1034 return Err(an_err!(DtErrKind::TrailingCharacters));
1035 }
1036
1037 let mut attos = hours
1038 .saturating_mul(3600)
1039 .saturating_add(minutes.saturating_mul(60))
1040 .saturating_add(seconds)
1041 .saturating_mul(ATTOS_PER_SEC_I128)
1042 .saturating_add(frac_attos);
1043 if neg {
1044 attos = -attos;
1045 }
1046 Ok(attos)
1047 }
1048}
1049
1050#[inline]
1051fn parse_mil_uint(bytes: &[u8], pos: &mut usize, len: usize) -> Result<i128, DtErr> {
1052 if *pos >= len || !bytes[*pos].is_ascii_digit() {
1053 return Err(an_err!(DtErrKind::ExpectedValue));
1054 }
1055 let mut v: i128 = 0;
1056 while *pos < len && bytes[*pos].is_ascii_digit() {
1057 v = v
1058 .saturating_mul(10)
1059 .saturating_add((bytes[*pos] - b'0') as i128);
1060 *pos += 1;
1061 }
1062 Ok(v)
1063}