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