Skip to main content

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}