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). Partial dates (`%Y`, `%Y-%m`)
131 /// // default missing month/day to 1.
132 /// let cfg = ParseCfg {
133 /// parse: Some(vec!["%d/%m/%Y".into(), "%Y-%m-%d".into(), "%Y".into()]),
134 /// mode: Mode::Explicit,
135 /// ..Default::default()
136 /// };
137 /// let dt = Dt::from_str_parse("15/03/2024", &cfg).unwrap();
138 /// let yr = Dt::from_str_parse("2024", &cfg).unwrap();
139 /// assert_eq!(yr, Dt::from_ymd(2024, 1, 1, Scale::UTC, 0, 0, 0, 0));
140 ///
141 /// // Relative dates — build config once, borrow repeatedly
142 /// let ref_time = Dt::from_ymd(2026, 6, 16, Scale::UTC, 12, 0, 0, 0);
143 /// let cfg = ParseCfg {
144 /// ref_time: Some(ref_time),
145 /// ..Default::default()
146 /// };
147 /// let dt = Dt::from_str_parse("next Monday at 14:00", &cfg).unwrap();
148 ///
149 /// assert_eq!(dt, Dt::from_ymd(2026, 6, 22, Scale::UTC, 14, 0, 0, 0));
150 /// ```
151 ///
152 /// ## Notes
153 ///
154 /// - The `Smart` + `Auto` combination gives the best real-world success rate for mixed data.
155 /// - Relative expressions and syslog-style no-year dates need a reference time. If `ref_time` is `None`
156 /// and the `std` feature is enabled, system time is used; without `std`, set `ref_time` explicitly or
157 /// parsing will fail.
158 /// - All successfully parsed [`Dt`] values are stored with attosecond precision on the internal
159 /// TAI timescale.
160 /// - Timezone handling (IANA names and fixed offsets) is fully supported when the `jiff-tz` feature
161 /// is enabled.
162 ///
163 /// ## Supported Languages:
164 ///
165 /// Language support here basically means supporting abbreviated and full day and month names.
166 /// Non-Ascii types of numeric characters are also supported such as full width digits.
167 ///
168 /// Some day/month names in non-English languages are not supported due to clashes, any such missing
169 /// support is noted below.
170 ///
171 /// - En
172 /// - De
173 /// - Won't parse "t" as short form for day.
174 /// - Es
175 /// - English word "ago" won't be detected as relative date word.
176 /// - Won't parse "mar" as tuesday, will instead parse as march.
177 /// - Fr
178 /// - Won't parse "mar" as tuesday, will instead parse as march.
179 ///
180 /// ## See also
181 ///
182 /// - [`ParseCfg`]
183 /// - [`Order`]
184 /// - [`Mode`]
185 /// - [`Lang`]
186 /// - [`Dt`](../struct.Dt.html)
187 /// - [`Dt::from_str`](../struct.Dt.html#method.from_str)
188 pub fn from_str_parse(s: &str, opts: &ParseCfg) -> Result<Dt, DtErr> {
189 if s.is_empty() {
190 return Err(an_err!(DtErrKind::Empty));
191 } else if s.len() > STRTIME_SIZE {
192 return Err(an_err!(DtErrKind::InvalidLen));
193 }
194
195 let lang: Lang = opts.lang;
196 let ref_time = &opts.ref_time;
197
198 let lowered: Cow<str> = if opts.to_lower {
199 Cow::Owned(s.to_lowercase())
200 } else {
201 Cow::Borrowed(s)
202 };
203
204 let classification = match classify_date(&lowered, lang, ref_time, opts.relative) {
205 Ok(ClassifiedDate::Parsed(time_point)) => return Ok(time_point),
206 Ok(ClassifiedDate::Cls(c)) => c,
207 Err(e) => {
208 // std::eprintln!("{}", e);
209 return Err(an_err!(" {}", s => e));
210 }
211 };
212
213 // eprintln!("{:?}", classification);
214
215 // let xx = &classification.date;
216 // if xx != trimmed {
217 // eprintln!("NOT EQUAL: {:?}, {:?}", trimmed, xx);
218 // }
219 // eprintln!("BEFORE & AFTER: {:?}, {:?}", lowered, &classification.date);
220
221 let normalized = &classification.date;
222
223 let (mode, date_order) = if let Some(formats) = &opts.parse {
224 if !formats.is_empty() {
225 for fmt in formats {
226 // `allow_partial_date = true`: formats that omit month/day
227 // (e.g. `%Y`, `%Y-%m`, `%y`) default the missing fields to 1
228 // via [`Parts::finish`]. Matches `try_compatible_formats`.
229 // Without this, Explicit `%Y` on `"2024"` fails with Incomplete.
230 if let Ok(value) = Self::from_strptime(normalized, fmt, true, true, true) {
231 return Ok(value);
232 }
233 }
234 // None of the provided formats worked and mode is Explicit
235 if opts.mode == Mode::Explicit {
236 return Err(an_err!(DtErrKind::InvalidInput, "{}", s));
237 }
238 }
239 (opts.mode, opts.order)
240 } else {
241 (opts.mode, opts.order)
242 };
243
244 // if s == "on the 5th of april 2024 at 00:00am" {
245 // std::eprintln!("{:?}", classification);
246 // }
247 // std::eprintln!("{:?}", classification);
248
249 if classification.is_pure_numeric {
250 match mode {
251 Mode::UnixTimestamp => {
252 if let Some(dt) = parse_pure_numeric_unix_timestamp(
253 normalized,
254 classification.num_non_decimal_digits as usize,
255 ) {
256 return Ok(dt);
257 }
258 }
259 _ => {
260 if let Some(dt) = try_pure_numeric(
261 normalized,
262 classification.num_digits,
263 classification.num_non_decimal_digits,
264 classification.is_decimal,
265 mode,
266 ) {
267 // std::eprintln!("NUMERIC INPUT SUCCESS: {:?}", s);
268 return Ok(dt);
269 }
270 }
271 }
272 }
273 if !classification.has_year
274 && let Some(dt) = parse_syslog_no_year(normalized, lang, ref_time)
275 {
276 return Ok(dt);
277 }
278 if let Some(dt) = match date_order {
279 Order::Smart => {
280 let order = smart_detect_date_order(normalized, &classification);
281 let mut result: Option<Dt>;
282
283 match order {
284 OrderFirst::Day => {
285 result = try_compatible_formats(
286 normalized,
287 generate_ambiguous_day_first_candidates(&classification),
288 );
289 // std::eprintln!("done trying day first: {:?}", result);
290
291 if result.is_none() {
292 result = try_compatible_formats(
293 normalized,
294 generate_ambiguous_month_first_candidates(&classification),
295 );
296 // std::eprintln!("done trying month first: {:?}", result);
297 }
298
299 if result.is_none() {
300 result = try_compatible_formats(
301 normalized,
302 generate_ambiguous_year_first_candidates(&classification),
303 );
304 // std::eprintln!("done trying year first: {:?}", result);
305 }
306 }
307 OrderFirst::Month => {
308 result = try_compatible_formats(
309 normalized,
310 generate_ambiguous_month_first_candidates(&classification),
311 );
312 // std::eprintln!("done trying month first: {:?}", result);
313
314 if result.is_none() {
315 result = try_compatible_formats(
316 normalized,
317 generate_ambiguous_day_first_candidates(&classification),
318 );
319 // std::eprintln!("done trying day first: {:?}", result);
320 }
321
322 if result.is_none() {
323 result = try_compatible_formats(
324 normalized,
325 generate_ambiguous_year_first_candidates(&classification),
326 );
327 // std::eprintln!("done trying year first: {:?}", result);
328 }
329 }
330 OrderFirst::Year => {
331 result = try_compatible_formats(
332 normalized,
333 generate_ambiguous_year_first_candidates(&classification),
334 );
335 // std::eprintln!("done trying year first: {:?}", result);
336
337 if result.is_none() {
338 result = try_compatible_formats(
339 normalized,
340 generate_ambiguous_day_first_candidates(&classification),
341 );
342 // std::eprintln!("done trying day first: {:?}", result);
343 }
344
345 if result.is_none() {
346 result = try_compatible_formats(
347 normalized,
348 generate_ambiguous_month_first_candidates(&classification),
349 );
350 // std::eprintln!("done trying month first: {:?}", result);
351 }
352 }
353 }
354
355 result
356 }
357 // Preferred order first, then the other two (same chains as Smart).
358 // try_unambiguous stays last, after this whole match.
359 Order::Year => {
360 let mut result = try_compatible_formats(
361 normalized,
362 generate_ambiguous_year_first_candidates(&classification),
363 );
364 if result.is_none() {
365 result = try_compatible_formats(
366 normalized,
367 generate_ambiguous_day_first_candidates(&classification),
368 );
369 }
370 if result.is_none() {
371 result = try_compatible_formats(
372 normalized,
373 generate_ambiguous_month_first_candidates(&classification),
374 );
375 }
376 result
377 }
378 Order::Day => {
379 let mut result = try_compatible_formats(
380 normalized,
381 generate_ambiguous_day_first_candidates(&classification),
382 );
383 if result.is_none() {
384 result = try_compatible_formats(
385 normalized,
386 generate_ambiguous_month_first_candidates(&classification),
387 );
388 }
389 if result.is_none() {
390 result = try_compatible_formats(
391 normalized,
392 generate_ambiguous_year_first_candidates(&classification),
393 );
394 }
395 result
396 }
397 Order::Month => {
398 let mut result = try_compatible_formats(
399 normalized,
400 generate_ambiguous_month_first_candidates(&classification),
401 );
402 if result.is_none() {
403 result = try_compatible_formats(
404 normalized,
405 generate_ambiguous_day_first_candidates(&classification),
406 );
407 }
408 if result.is_none() {
409 result = try_compatible_formats(
410 normalized,
411 generate_ambiguous_year_first_candidates(&classification),
412 );
413 }
414 result
415 }
416 } {
417 return Ok(dt);
418 }
419 if let Some(dt) = try_unambiguous(normalized, &classification) {
420 return Ok(dt);
421 }
422 // std::eprintln!("NOW trying numeric timestamp");
423 if classification.is_pure_numeric
424 && mode != Mode::UnixTimestamp
425 && let Some(dt) = parse_pure_numeric_unix_timestamp(
426 normalized,
427 classification.num_non_decimal_digits as usize,
428 )
429 {
430 return Ok(dt);
431 }
432 Err(an_err!(DtErrKind::InvalidInput, "{}", s))
433 }
434
435 /// Same parsing logic as [`Dt::from_str_parse`](../struct.Dt.html#method.from_str_parse),
436 /// but returns attoseconds since the library epoch: 2000-01-01 12:00:00 UTC
437 /// (on the UTC scale).
438 ///
439 /// Returns `Some(attos)` on success (negative for pre-2000 dates) or `None`
440 /// on any parse error.
441 #[inline]
442 pub fn str_to_attos(s: &str, opts: &ParseCfg) -> Option<i128> {
443 Dt::from_str_parse(s, opts).ok().map(|dt| dt.to_attos())
444 }
445
446 /// Same parsing logic as [`Dt::from_str_parse`](../struct.Dt.html#method.from_str_parse),
447 /// but returns milliseconds since the library epoch: 2000-01-01 12:00:00 UTC
448 /// (on the UTC scale).
449 ///
450 /// Returns `Some(millis)` on success (negative for pre-2000 dates) or `None`
451 /// on any parse error.
452 #[inline]
453 pub fn str_to_ms(s: &str, opts: &ParseCfg) -> Option<i128> {
454 Dt::from_str_parse(s, opts).ok().map(|dt| dt.to_ms().0)
455 }
456
457 /// Same parsing logic as [`Dt::from_str_parse`](../struct.Dt.html#method.from_str_parse),
458 /// but returns nanoseconds since the library epoch: 2000-01-01 12:00:00 UTC
459 /// (on the UTC scale).
460 ///
461 /// Returns `Some(nanos)` on success (negative for pre-2000 dates) or `None`
462 /// on any parse error.
463 #[inline]
464 pub fn str_to_ns(s: &str, opts: &ParseCfg) -> Option<i128> {
465 Dt::from_str_parse(s, opts).ok().map(|dt| dt.to_ns().0)
466 }
467
468 /// Same parsing logic as [`Dt::from_str_parse`](../struct.Dt.html#method.from_str_parse),
469 /// but returns milliseconds since the UNIX epoch: (1970-01-01 00:00:00 UTC).
470 ///
471 /// Returns `Some(millis)` on success (negative for pre-2000 dates) or `None`
472 /// on any parse error.
473 #[inline]
474 pub fn str_to_unix_ms(s: &str, opts: &ParseCfg) -> Option<i128> {
475 Dt::from_str_parse(s, opts)
476 .ok()
477 .map(|dt| dt.to_scale_and_diff(Dt::UNIX_EPOCH, false).to_ms().0)
478 }
479
480 /// Same parsing logic as [`Dt::from_str_parse`](../struct.Dt.html#method.from_str_parse),
481 /// but returns nanoseconds since the UNIX epoch: (1970-01-01 00:00:00 UTC).
482 ///
483 /// Returns `Some(nanos)` on success (negative for pre-2000 dates) or `None`
484 /// on any parse error.
485 #[inline]
486 pub fn str_to_unix_ns(s: &str, opts: &ParseCfg) -> Option<i128> {
487 Dt::from_str_parse(s, opts)
488 .ok()
489 .map(|dt| dt.to_scale_and_diff(Dt::UNIX_EPOCH, false).to_ns().0)
490 }
491}
492
493/// Core zero-allocation helper (updated to match the new `&str` signature).
494///
495/// The `fmt` we get from the iterator is still `'static`, but it coerces automatically
496/// to `&str`, so everything continues to work.
497#[inline]
498pub(crate) fn try_compatible_formats<I>(s: &str, formats: I) -> Option<Dt>
499where
500 I: IntoIterator<Item = String>,
501{
502 // let mut dt = None;
503
504 // for fmt in formats.into_iter() {
505 // eprintln!("TRYING FMT: {}", fmt);
506 // dt = match Dt::from_strptime(s, &fmt, true, true, true) {
507 // Ok(parsed) => Some(parsed),
508 // Err(e) => {
509 // eprintln!(" FAILED with: {:?}", e);
510 // continue;
511 // }
512 // };
513 // if dt.is_some() {
514 // break;
515 // }
516 // // === DEBUG ===
517 // // eprintln!("Tried format: {:?}", fmt);
518 // }
519
520 // dt
521 formats
522 .into_iter()
523 .find_map(|fmt| Dt::from_strptime(s, &fmt, true, true, true).ok())
524}
525
526#[inline]
527pub(crate) fn try_unambiguous(s: &str, classification: &DateClassification) -> Option<Dt> {
528 if matches!(classification.bytes_len, 6..=8)
529 && let Some(dt) = parse_yyyy_mm(s.as_bytes())
530 {
531 return Some(dt);
532 }
533 try_compatible_formats(s, generate_unambiguous_candidates(classification))
534}