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