deep_time/alloc_parse/parse_date.rs
1use crate::{
2 ClassifiedDate, DateClassification, Dt, DtErr, DtErrKind, Lang, Mode, Order, OrderFirst,
3 ParseCfg, STRTIME_SIZE, an_err, classify_date, generate_ambiguous_day_first_candidates,
4 generate_ambiguous_month_first_candidates, generate_ambiguous_year_first_candidates,
5 generate_unambiguous_candidates, parse_pure_numeric_unix_timestamp, parse_syslog_no_year,
6 parse_yyyy_mm, smart_detect_date_order, try_pure_numeric,
7};
8use alloc::borrow::Cow;
9use alloc::string::String;
10
11impl Dt {
12 /// Automatically parses datetime [`str`] into a [`Dt`] by guessing and generating the
13 /// format. Supports the vast majority of date formats.
14 ///
15 /// - Requires the `"parse"` feature (which enables `alloc`).
16 /// - For ordinary calendar and numeric inputs, the returned [`Dt`] is converted to `TAI`
17 /// from a given time scale if the input includes one, or if not then `UTC`. The
18 /// exception to this is the format `[86400s TAI>UTC]`, with this input format no time
19 /// scale conversion happens, and the returned [`Dt`]'s `scale` and `target` are taken
20 /// from the string as written.
21 /// - The `attos` field is an [`i128`] attosecond count measured from the
22 /// library epoch of 2000-01-01 noon. See [`Scale`](../enum.Scale.html).
23 /// - The returned [`Dt`] is **not** in local time. If a timezone is parsed,
24 /// it is used only to find the offset so the result is a non-local instant.
25 ///
26 /// ## Parameters
27 ///
28 /// - `s`: The string to parse. Must be non-empty and no longer than [`STRTIME_SIZE`]
29 /// bytes. Empty strings or overly long inputs return an error.
30 /// - `opts`: The [`ParseCfg`] to use. Pass `&ParseCfg::DEFAULT` (or `&ParseCfg::default()`)
31 /// to use the standard smart defaults. You can create a `ParseCfg` once and pass `&cfg`
32 /// on every call for consistent behavior and to avoid repeated construction.
33 ///
34 /// ## Configuration Options
35 ///
36 /// These are the fields of the configuration options struct [`ParseCfg`], their types and defaults.
37 ///
38 /// See [`ParseCfg`] for more information.
39 ///
40 /// | Field | Type and Default | Effect |
41 /// |----------------|----------------------------------|--------|
42 /// | `lang` | [`Lang::En`] | Language, scroll down to see currently supported languages |
43 /// | `order` | [`Order::Smart`] | How to resolve ambiguous numeric dates like `01/02/03` |
44 /// | `mode` | [`Mode::Auto`] | Special handling for purely numeric inputs |
45 /// | `parse` | [`Option<Vec<String>>`] - `None` | An explicit list of formats to try, if the [`Mode`] is Explicit then only these formats are tried |
46 /// | `relative` | [`bool`] - `true` | Enable phrases like "tomorrow", "in 3 days" |
47 /// | `ref_time` | [`Option<Dt>`] - `None` | Reference time for relative dates and syslog-style "no-year" dates |
48 /// | `to_lower` | [`bool`] - `true` | Automatically lowercase the input, **only** set to false if it's already lowercase |
49 ///
50 /// ## Purely Numeric Inputs
51 ///
52 /// When the input consists **only** of digits (and optionally a decimal point),
53 /// the parser uses a fast, mode-aware path before trying any other strategies.
54 /// The exact interpretation depends on the number of digits and the selected `mode`.
55 ///
56 /// | Digits | Example(s) | `Mode` | Interpreted as | Notes |
57 /// |--------|--------------------------|-----------------|-----------------------------------------|-------|
58 /// | 1–4 | `2024`, `24`, `5` | `Auto`/`Legacy` | Year (2-digit uses 2000/1900 pivot) | 1- and 3-digit years only work in `Scientific` |
59 /// | 5 | `24123`, `60400` | `Legacy` | Ordinal date (YYDDD) | — |
60 /// | 5 | `60400`, `60400.75` | `Scientific` | Modified Julian Date (MJD) | Fractional days supported |
61 /// | 5 | `24123`, `60400.75` | `Auto` | Ordinal (non-decimal) or MJD (decimal) | Smart default |
62 /// | 6 | `240315`, `202403` | `Auto` | YYYYMM if plausible year, else YYMMDD | Most common compact form |
63 /// | 6 | `240315` | `Legacy` | YYMMDD preferred | — |
64 /// | 6 | `202403` | `Scientific` | YYYYMM preferred | — |
65 /// | 7 | `2024123` | `Legacy` | Ordinal date (YYYYDDD) | — |
66 /// | 7 | `2460123`, `2460123.5` | `Scientific` | Julian Day (JD) | Fractional days supported |
67 /// | 7 | `2024123` | `Auto` | Ordinal (integer) or JD (decimal) | Smart default |
68 /// | 10–11 | `1735689600` | any | Unix seconds | — |
69 /// | 12–15 | `1735689600123` | any | Unix milliseconds | Most common high-precision case |
70 /// | 16–18 | `1735689600123456` | any | Unix microseconds | — |
71 /// | 19+ | `1735689600123456789` | any | Unix nanoseconds | Full precision |
72 ///
73 /// Use `Mode::UnixTimestamp` when you know the input is always a Unix timestamp.
74 ///
75 /// ## Ambiguous Numeric Dates
76 ///
77 /// Dates where the components could map to different orders (e.g. `01/02/03`,
78 /// `3-4-5`, `15.03.24`, `2024.03.15`) are resolved via the `order` field:
79 ///
80 /// - **`Order::Smart`** (default) — Applies the fast heuristic described in [`Order::Smart`].
81 /// It strongly prefers modern/tech conventions (Year-first for compact/ISO-like data)
82 /// while handling the majority of international and US-style dates.
83 ///
84 /// - **`Order::Year`**, **`Order::Day`**, or **`Order::Month`** prefer that
85 /// order first, then try the other two (same fallback chains as Smart).
86 ///
87 /// ## Supported Formats
88 ///
89 /// The parser tokenizes known words (month/day names, relative phrases, timezones, etc.), generates candidate
90 /// formats from the token pattern, and tries them until one matches. Thousands of layouts are supported.
91 ///
92 /// Separators generally don't matter, they could be spaces, slashes, or hyphens, but **not colons** - colons are
93 /// reserved for the time connector, times, and offsets.
94 ///
95 /// Generally speaking the date part must come first, and stuff like time components, offsets and iana timezone names
96 /// must come afterwards.
97 ///
98 /// - **ISO 8601** and variants: `2024-03-15`, `2024-03-15T14:30:00Z`, `2024-03-15T14:30:00+01:00[Europe/Paris]`
99 /// - **Display form** — same text as [`Dt`] Display / `.to_string()`, e.g.
100 /// `[86400s TAI>UTC]`. Parsed like [`Dt::from_str`](../struct.Dt.html#method.from_str)
101 /// (no scale conversion).
102 /// - **Named dates** (in supported languages): `15 March 2024`, `15 mars 2024`, `15. März 2024`, `15 de marzo de 2024`
103 /// - **Week dates**: `2024-W15`, `2024-W15-3`, `2024W15` (missing weekday defaults to Monday)
104 /// - **Syslog-style** (no year): `Mar 5 10:23:45` (year inferred from `ref_time`)
105 /// - **Relative expressions**: `tomorrow`, `in 3 days`, `2 weeks ago`
106 /// - **12-hour time**: `2:30 PM`, `14:30:45.123`
107 /// - **Offsets and timezones**: `+0100`, `-05:30`, `Z`, IANA timezone names (with the `jiff-tz` feature enabled)
108 /// - **Library time scales**: `TAI`, `TT`, etc. are detected and parsed, must come after the date part of the input
109 ///
110 /// Relative dates are also automatically supported, except for bare numbers with no colons like `0900`, as these
111 /// are differently interpreted.
112 ///
113 /// ## Examples
114 ///
115 /// ```rust
116 /// use deep_time::{Dt, ParseCfg, Order, Mode, Scale};
117 ///
118 /// // Default smart parsing
119 /// let dt = Dt::from_str_parse("2024-03-15 14:30:00", &ParseCfg::DEFAULT).unwrap();
120 ///
121 /// // German named date (requires the `de` feature)
122 /// # #[cfg(feature = "de")]
123 /// # {
124 /// # use deep_time::Lang;
125 /// let cfg = ParseCfg { lang: Lang::De, ..Default::default() };
126 /// let dt = Dt::from_str_parse("15. März 2024 um 14:30", &cfg).unwrap();
127 /// # }
128 ///
129 /// // Pure numeric compact form
130 /// let dt = Dt::from_str_parse("20240315", &ParseCfg::DEFAULT).unwrap(); // March 15, 2024
131 ///
132 /// // Unix timestamp (milliseconds)
133 /// let cfg = ParseCfg { mode: Mode::UnixTimestamp, ..Default::default() };
134 /// let dt = Dt::from_str_parse("1735689600123", &cfg).unwrap();
135 ///
136 /// // Explicit formats only (no fallback). Partial dates (`%Y`, `%Y-%m`)
137 /// // default missing month/day to 1.
138 /// let cfg = ParseCfg {
139 /// parse: Some(vec!["%d/%m/%Y".into(), "%Y-%m-%d".into(), "%Y".into()]),
140 /// mode: Mode::Explicit,
141 /// ..Default::default()
142 /// };
143 /// let dt = Dt::from_str_parse("15/03/2024", &cfg).unwrap();
144 /// let yr = Dt::from_str_parse("2024", &cfg).unwrap();
145 /// assert_eq!(yr, Dt::from_ymd(2024, 1, 1, Scale::UTC, 0, 0, 0, 0));
146 ///
147 /// // Relative dates — build config once, borrow repeatedly
148 /// let ref_time = Dt::from_ymd(2026, 6, 16, Scale::UTC, 12, 0, 0, 0);
149 /// let cfg = ParseCfg {
150 /// ref_time: Some(ref_time),
151 /// ..Default::default()
152 /// };
153 /// let dt = Dt::from_str_parse("next Monday at 14:00", &cfg).unwrap();
154 ///
155 /// assert_eq!(dt, Dt::from_ymd(2026, 6, 22, Scale::UTC, 14, 0, 0, 0));
156 /// ```
157 ///
158 /// ## Notes
159 ///
160 /// - The `Smart` + `Auto` combination gives the best real-world success rate for mixed data.
161 /// - Relative expressions and syslog-style no-year dates need a reference time. If `ref_time` is `None`
162 /// and the `std` feature is enabled, system time is used; without `std`, set `ref_time` explicitly or
163 /// parsing will fail.
164 /// - All successfully parsed [`Dt`] values are stored with attosecond precision on the internal
165 /// TAI timescale.
166 /// - Timezone handling (IANA names and fixed offsets) is fully supported when the `jiff-tz` feature
167 /// is enabled.
168 ///
169 /// ## Supported Languages:
170 ///
171 /// Language support here basically means supporting abbreviated and full day and month names.
172 /// Non-Ascii types of numeric characters are also supported such as full width digits.
173 ///
174 /// Some day/month names in non-English languages are not supported due to clashes, any such missing
175 /// support is noted below.
176 ///
177 /// - En
178 /// - De
179 /// - Won't parse "t" as short form for day.
180 /// - Es
181 /// - English word "ago" won't be detected as relative date word.
182 /// - Won't parse "mar" as tuesday, will instead parse as march.
183 /// - Fr
184 /// - Won't parse "mar" as tuesday, will instead parse as march.
185 ///
186 /// ## See also
187 ///
188 /// - [`ParseCfg`]
189 /// - [`Order`]
190 /// - [`Mode`]
191 /// - [`Lang`]
192 /// - [`Dt`](../struct.Dt.html)
193 /// - [`Dt::from_str`](../struct.Dt.html#method.from_str)
194 pub fn from_str_parse(s: &str, opts: &ParseCfg) -> Result<Dt, DtErr> {
195 if s.is_empty() {
196 return Err(an_err!(DtErrKind::Empty));
197 } else if s.len() > STRTIME_SIZE {
198 return Err(an_err!(DtErrKind::InvalidLen));
199 }
200
201 let lang: Lang = opts.lang;
202 let ref_time = &opts.ref_time;
203
204 let lowered: Cow<str> = if opts.to_lower {
205 Cow::Owned(s.to_lowercase())
206 } else {
207 Cow::Borrowed(s)
208 };
209
210 let classification = match classify_date(&lowered, lang, ref_time, opts.relative) {
211 Ok(ClassifiedDate::Parsed(time_point)) => return Ok(time_point),
212 Ok(ClassifiedDate::Cls(c)) => c,
213 Err(e) => {
214 // std::eprintln!("{}", e);
215 return Err(an_err!(" {}", s => e));
216 }
217 };
218
219 // eprintln!("{:?}", classification);
220
221 // let xx = &classification.date;
222 // if xx != trimmed {
223 // eprintln!("NOT EQUAL: {:?}, {:?}", trimmed, xx);
224 // }
225 // eprintln!("BEFORE & AFTER: {:?}, {:?}", lowered, &classification.date);
226
227 let normalized = &classification.date;
228
229 let (mode, date_order) = if let Some(formats) = &opts.parse {
230 if !formats.is_empty() {
231 for fmt in formats {
232 // `allow_partial_date = true`: formats that omit month/day
233 // (e.g. `%Y`, `%Y-%m`, `%y`) default the missing fields to 1
234 // via [`Parts::finish`]. Matches `try_compatible_formats`.
235 // Without this, Explicit `%Y` on `"2024"` fails with Incomplete.
236 if let Ok(value) = Self::from_strptime(normalized, fmt, true, true, true) {
237 return Ok(value);
238 }
239 }
240 // None of the provided formats worked and mode is Explicit
241 if opts.mode == Mode::Explicit {
242 return Err(an_err!(DtErrKind::InvalidInput, "{}", s));
243 }
244 }
245 (opts.mode, opts.order)
246 } else {
247 (opts.mode, opts.order)
248 };
249
250 // if s == "on the 5th of april 2024 at 00:00am" {
251 // std::eprintln!("{:?}", classification);
252 // }
253 // std::eprintln!("{:?}", classification);
254
255 if classification.is_pure_numeric {
256 match mode {
257 Mode::UnixTimestamp => {
258 if let Some(dt) = parse_pure_numeric_unix_timestamp(
259 normalized,
260 classification.num_non_decimal_digits as usize,
261 ) {
262 return Ok(dt);
263 }
264 }
265 _ => {
266 if let Some(dt) = try_pure_numeric(
267 normalized,
268 classification.num_digits,
269 classification.num_non_decimal_digits,
270 classification.is_decimal,
271 mode,
272 ) {
273 // std::eprintln!("NUMERIC INPUT SUCCESS: {:?}", s);
274 return Ok(dt);
275 }
276 }
277 }
278 }
279 if !classification.has_year
280 && let Some(dt) = parse_syslog_no_year(normalized, lang, ref_time)
281 {
282 return Ok(dt);
283 }
284 if let Some(dt) = match date_order {
285 Order::Smart => {
286 let order = smart_detect_date_order(normalized, &classification);
287 let mut result: Option<Dt>;
288
289 match order {
290 OrderFirst::Day => {
291 result = try_compatible_formats(
292 normalized,
293 generate_ambiguous_day_first_candidates(&classification),
294 );
295 // std::eprintln!("done trying day first: {:?}", result);
296
297 if result.is_none() {
298 result = try_compatible_formats(
299 normalized,
300 generate_ambiguous_month_first_candidates(&classification),
301 );
302 // std::eprintln!("done trying month first: {:?}", result);
303 }
304
305 if result.is_none() {
306 result = try_compatible_formats(
307 normalized,
308 generate_ambiguous_year_first_candidates(&classification),
309 );
310 // std::eprintln!("done trying year first: {:?}", result);
311 }
312 }
313 OrderFirst::Month => {
314 result = try_compatible_formats(
315 normalized,
316 generate_ambiguous_month_first_candidates(&classification),
317 );
318 // std::eprintln!("done trying month first: {:?}", result);
319
320 if result.is_none() {
321 result = try_compatible_formats(
322 normalized,
323 generate_ambiguous_day_first_candidates(&classification),
324 );
325 // std::eprintln!("done trying day first: {:?}", result);
326 }
327
328 if result.is_none() {
329 result = try_compatible_formats(
330 normalized,
331 generate_ambiguous_year_first_candidates(&classification),
332 );
333 // std::eprintln!("done trying year first: {:?}", result);
334 }
335 }
336 OrderFirst::Year => {
337 result = try_compatible_formats(
338 normalized,
339 generate_ambiguous_year_first_candidates(&classification),
340 );
341 // std::eprintln!("done trying year first: {:?}", result);
342
343 if result.is_none() {
344 result = try_compatible_formats(
345 normalized,
346 generate_ambiguous_day_first_candidates(&classification),
347 );
348 // std::eprintln!("done trying day first: {:?}", result);
349 }
350
351 if result.is_none() {
352 result = try_compatible_formats(
353 normalized,
354 generate_ambiguous_month_first_candidates(&classification),
355 );
356 // std::eprintln!("done trying month first: {:?}", result);
357 }
358 }
359 }
360
361 result
362 }
363 // Preferred order first, then the other two (same chains as Smart).
364 // try_unambiguous stays last, after this whole match.
365 Order::Year => {
366 let mut result = try_compatible_formats(
367 normalized,
368 generate_ambiguous_year_first_candidates(&classification),
369 );
370 if result.is_none() {
371 result = try_compatible_formats(
372 normalized,
373 generate_ambiguous_day_first_candidates(&classification),
374 );
375 }
376 if result.is_none() {
377 result = try_compatible_formats(
378 normalized,
379 generate_ambiguous_month_first_candidates(&classification),
380 );
381 }
382 result
383 }
384 Order::Day => {
385 let mut result = try_compatible_formats(
386 normalized,
387 generate_ambiguous_day_first_candidates(&classification),
388 );
389 if result.is_none() {
390 result = try_compatible_formats(
391 normalized,
392 generate_ambiguous_month_first_candidates(&classification),
393 );
394 }
395 if result.is_none() {
396 result = try_compatible_formats(
397 normalized,
398 generate_ambiguous_year_first_candidates(&classification),
399 );
400 }
401 result
402 }
403 Order::Month => {
404 let mut result = try_compatible_formats(
405 normalized,
406 generate_ambiguous_month_first_candidates(&classification),
407 );
408 if result.is_none() {
409 result = try_compatible_formats(
410 normalized,
411 generate_ambiguous_day_first_candidates(&classification),
412 );
413 }
414 if result.is_none() {
415 result = try_compatible_formats(
416 normalized,
417 generate_ambiguous_year_first_candidates(&classification),
418 );
419 }
420 result
421 }
422 } {
423 return Ok(dt);
424 }
425 if let Some(dt) = try_unambiguous(normalized, &classification) {
426 return Ok(dt);
427 }
428 // std::eprintln!("NOW trying numeric timestamp");
429 if classification.is_pure_numeric
430 && mode != Mode::UnixTimestamp
431 && let Some(dt) = parse_pure_numeric_unix_timestamp(
432 normalized,
433 classification.num_non_decimal_digits as usize,
434 )
435 {
436 return Ok(dt);
437 }
438 Err(an_err!(DtErrKind::InvalidInput, "{}", s))
439 }
440
441 /// Same parsing logic as [`Dt::from_str_parse`](../struct.Dt.html#method.from_str_parse),
442 /// but returns attoseconds since the library epoch: 2000-01-01 12:00:00 UTC
443 /// (on the UTC scale).
444 ///
445 /// Returns `Some(attos)` on success (negative for pre-2000 dates) or `None`
446 /// on any parse error.
447 #[inline]
448 pub fn str_to_attos(s: &str, opts: &ParseCfg) -> Option<i128> {
449 Dt::from_str_parse(s, opts).ok().map(|dt| dt.to_attos())
450 }
451
452 /// Same parsing logic as [`Dt::from_str_parse`](../struct.Dt.html#method.from_str_parse),
453 /// but returns milliseconds since the library epoch: 2000-01-01 12:00:00 UTC
454 /// (on the UTC scale).
455 ///
456 /// Returns `Some(millis)` on success (negative for pre-2000 dates) or `None`
457 /// on any parse error.
458 #[inline]
459 pub fn str_to_ms(s: &str, opts: &ParseCfg) -> Option<i128> {
460 Dt::from_str_parse(s, opts).ok().map(|dt| dt.to_ms().0)
461 }
462
463 /// Same parsing logic as [`Dt::from_str_parse`](../struct.Dt.html#method.from_str_parse),
464 /// but returns nanoseconds since the library epoch: 2000-01-01 12:00:00 UTC
465 /// (on the UTC scale).
466 ///
467 /// Returns `Some(nanos)` on success (negative for pre-2000 dates) or `None`
468 /// on any parse error.
469 #[inline]
470 pub fn str_to_ns(s: &str, opts: &ParseCfg) -> Option<i128> {
471 Dt::from_str_parse(s, opts).ok().map(|dt| dt.to_ns().0)
472 }
473
474 /// Same parsing logic as [`Dt::from_str_parse`](../struct.Dt.html#method.from_str_parse),
475 /// but returns milliseconds since the UNIX epoch: (1970-01-01 00:00:00 UTC).
476 ///
477 /// Returns `Some(millis)` on success (negative for pre-2000 dates) or `None`
478 /// on any parse error.
479 #[inline]
480 pub fn str_to_unix_ms(s: &str, opts: &ParseCfg) -> Option<i128> {
481 Dt::from_str_parse(s, opts)
482 .ok()
483 .map(|dt| dt.to_scale_and_diff(Dt::UNIX_EPOCH, false).to_ms().0)
484 }
485
486 /// Same parsing logic as [`Dt::from_str_parse`](../struct.Dt.html#method.from_str_parse),
487 /// but returns nanoseconds since the UNIX epoch: (1970-01-01 00:00:00 UTC).
488 ///
489 /// Returns `Some(nanos)` on success (negative for pre-2000 dates) or `None`
490 /// on any parse error.
491 #[inline]
492 pub fn str_to_unix_ns(s: &str, opts: &ParseCfg) -> Option<i128> {
493 Dt::from_str_parse(s, opts)
494 .ok()
495 .map(|dt| dt.to_scale_and_diff(Dt::UNIX_EPOCH, false).to_ns().0)
496 }
497}
498
499/// Core zero-allocation helper (updated to match the new `&str` signature).
500///
501/// The `fmt` we get from the iterator is still `'static`, but it coerces automatically
502/// to `&str`, so everything continues to work.
503#[inline]
504pub(crate) fn try_compatible_formats<I>(s: &str, formats: I) -> Option<Dt>
505where
506 I: IntoIterator<Item = String>,
507{
508 // let mut dt = None;
509
510 // for fmt in formats.into_iter() {
511 // eprintln!("TRYING FMT: {}", fmt);
512 // dt = match Dt::from_strptime(s, &fmt, true, true, true) {
513 // Ok(parsed) => Some(parsed),
514 // Err(e) => {
515 // eprintln!(" FAILED with: {:?}", e);
516 // continue;
517 // }
518 // };
519 // if dt.is_some() {
520 // break;
521 // }
522 // // === DEBUG ===
523 // // eprintln!("Tried format: {:?}", fmt);
524 // }
525
526 // dt
527 formats
528 .into_iter()
529 .find_map(|fmt| Dt::from_strptime(s, &fmt, true, true, true).ok())
530}
531
532#[inline]
533pub(crate) fn try_unambiguous(s: &str, classification: &DateClassification) -> Option<Dt> {
534 if matches!(classification.bytes_len, 6..=8)
535 && let Some(dt) = parse_yyyy_mm(s.as_bytes())
536 {
537 return Some(dt);
538 }
539 try_compatible_formats(s, generate_unambiguous_candidates(classification))
540}