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}