Skip to main content

deep_time/strtime/
mod.rs

1pub mod parser;
2pub mod printer;
3
4use crate::error::{DtErr, DtErrKind};
5use crate::{BufStr, Dt, Lang, Parts, STRTIME_SIZE, an_err};
6use core::result::Result;
7use core::str;
8
9pub(crate) use parser::*;
10pub(crate) use printer::*;
11
12/// Optional `%` directive extensions: flag, width, and colon count.
13#[derive(Clone, Copy, Debug, Default)]
14pub(crate) struct FmtExtensions {
15    pub(crate) flag: FmtFlag,
16    pub(crate) width: Option<u8>,
17    pub(crate) colons: u8,
18}
19
20/// Flags that may appear immediately after `%` and before the directive.
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub(crate) enum FmtFlag {
23    #[default]
24    None,
25    PadSpace,
26    PadZero,
27    NoPad,
28    Uppercase,
29    Swapcase,
30}
31
32impl FmtFlag {
33    #[inline(always)]
34    pub(crate) fn from_byte(byte: u8) -> Self {
35        match byte {
36            b'_' => Self::PadSpace,
37            b'0' => Self::PadZero,
38            b'-' => Self::NoPad,
39            b'^' => Self::Uppercase,
40            b'#' => Self::Swapcase,
41            _ => Self::None,
42        }
43    }
44
45    /// Resolve the padding flag for numeric parsing.
46    ///
47    /// `None`, `Uppercase`, and `Swapcase` defer to the directive default;
48    /// the three pad flags override it.
49    #[inline(always)]
50    pub(crate) fn resolve(self, default: FmtFlag) -> FmtFlag {
51        match self {
52            Self::None | Self::Uppercase | Self::Swapcase => default,
53            pad => pad,
54        }
55    }
56}
57
58/// A pre-validated, reusable **parse** format for `strptime`-style directives.
59///
60/// Construct once with [`StrPTimeFmt::new`](../struct.StrPTimeFmt.html#method.new)
61/// or [`Dt::parse_fmt`](../struct.Dt.html#method.parse_fmt). On success the format
62/// is known to be syntactically valid for this crate’s parser, so later use will
63/// not fail because the format itself is unusable.
64///
65/// ## What this type is for
66///
67/// - **Fail-fast format checking** — catch unknown, unsupported, or truncated
68///   `%` directives at construction time (e.g. config load), not on every parse.
69/// - **Reuse** — store the validated format and call
70///   [`to_dt`](../struct.StrPTimeFmt.html#method.to_dt) (or reformat helpers)
71///   many times without re-checking directive syntax.
72///
73/// ## Guarantee
74///
75/// After `new` succeeds, parsing with this value
76/// ([`to_dt`](../struct.StrPTimeFmt.html#method.to_dt), and the parse step of
77/// [`to_str`](../struct.StrPTimeFmt.html#method.to_str) /
78/// [`to_str_b`](../struct.StrPTimeFmt.html#method.to_str_b)) will **not** fail
79/// with format-structure errors such as [`DtErrKind::UnknownItem`],
80/// [`DtErrKind::UnsupportedItem`], [`DtErrKind::TruncatedDirective`], or
81/// [`DtErrKind::InvalidFractional`].
82///
83/// Parsing can still fail for **input** reasons: mismatched literals, missing
84/// fields, out-of-range values, incomplete dates, trailing characters, bad
85/// offsets, and so on. See
86/// [`Dt::from_strptime`](../struct.Dt.html#method.from_strptime).
87///
88/// ## What this type is *not*
89///
90/// - **Not a speed optimization.** Validation does not make strptime/strftime
91///   faster; it only checks the format once and stores a copy.
92/// - **Not a print-format type.** The stored string is the **input** (parse)
93///   pattern. Methods that take an `output_fmt` argument do **not** pre-validate
94///   that second format — only the format inside this struct is guaranteed.
95/// - **Not the free-form auto-parser.** For guessing formats from text, use
96///   [`Dt::from_str`](../struct.Dt.html#method.from_str) / the `parse` feature,
97///   not this type.
98///
99/// ## Constraints
100///
101/// - Format must be **ASCII** only.
102/// - Format length must be ≤
103///   [`MAX_FMT_LEN`](../struct.StrPTimeFmt.html#associatedconstant.MAX_FMT_LEN)
104///   (256 bytes).
105/// - Bytes are copied into an owned fixed-size buffer (no heap allocation).
106/// - Directives match the parse grammar documented on
107///   [`Dt::from_strptime`](../struct.Dt.html#method.from_strptime) (including
108///   library extensions such as `%L`, `%*`, `%J`, `%Q`). Printer-only spellings
109///   (e.g. trim flag `~` in `%.~f`) are **not** accepted here.
110///
111/// ## Examples
112///
113/// ```rust
114/// use deep_time::{Dt, StrPTimeFmt};
115///
116/// // Prefer validating once (e.g. at startup) then reusing.
117/// let fmt = StrPTimeFmt::new("%Y-%m-%d %H:%M:%S").unwrap();
118/// // equivalent: Dt::parse_fmt("%Y-%m-%d %H:%M:%S").unwrap();
119///
120/// let dt = fmt.to_dt("2025-05-23 14:30:00", false, false, false).unwrap();
121/// assert_eq!(dt.to_ymd().yr(), 2025);
122///
123/// // Construction fails on a broken format — before any input is parsed.
124/// assert!(StrPTimeFmt::new("%Y-%m-%").is_err());
125/// assert!(StrPTimeFmt::new("%c").is_err()); // unsupported locale directive
126/// ```
127///
128/// ## See also
129///
130/// - [`StrPTimeFmt::new`](../struct.StrPTimeFmt.html#method.new)
131/// - [`StrPTimeFmt::to_dt`](../struct.StrPTimeFmt.html#method.to_dt)
132/// - [`StrPTimeFmt::to_str_b`](../struct.StrPTimeFmt.html#method.to_str_b)
133/// - [`StrPTimeFmt::to_str`](../struct.StrPTimeFmt.html#method.to_str)
134/// - [`Dt::parse_fmt`](../struct.Dt.html#method.parse_fmt)
135#[derive(Debug, Clone)]
136pub struct StrPTimeFmt {
137    fmt: [u8; Self::MAX_FMT_LEN],
138    len: usize,
139}
140
141impl StrPTimeFmt {
142    /// Maximum length of a format string in bytes (not characters).
143    ///
144    /// Longer strings are rejected with [`DtErrKind::InvalidLen`] at construction.
145    pub const MAX_FMT_LEN: usize = 256;
146
147    /// Validates a `strptime`-style format and stores it for reuse.
148    ///
149    /// On success, the returned value carries the guarantee described on
150    /// [`StrPTimeFmt`](../struct.StrPTimeFmt.html): the format will not fail later
151    /// for structural reasons when used to **parse**.
152    ///
153    /// Accepts the same `%` directives and extensions as
154    /// [`Dt::from_strptime`](../struct.Dt.html#method.from_strptime) (one optional
155    /// flag `-`/`_`/`0`/`^`/`#`, optional width digits, up to three colons for
156    /// offsets, and `%.f` / `%.N` fractional forms).
157    ///
158    /// ## Errors
159    ///
160    /// - [`DtErrKind::InvalidLen`] — longer than
161    ///   [`MAX_FMT_LEN`](../struct.StrPTimeFmt.html#associatedconstant.MAX_FMT_LEN).
162    /// - [`DtErrKind::InvalidInput`] — not valid ASCII.
163    /// - [`DtErrKind::TruncatedDirective`] — a bare `%` at end of format (no
164    ///   directive letter).
165    /// - [`DtErrKind::UnexpectedEnd`] — `%` followed only by flags, width, and/or
166    ///   colons, with no directive letter.
167    /// - [`DtErrKind::ExpectedFractional`] — a `%.` sequence with nothing after
168    ///   optional digits (incomplete fractional directive).
169    /// - [`DtErrKind::InvalidFractional`] — a `%.` sequence followed by a character
170    ///   other than `f` or `N` (also rejects printer-only forms such as `%.~f`).
171    /// - [`DtErrKind::UnsupportedItem`] — `%c`, `%r`, `%x`, `%X`, or `%Z`.
172    /// - [`DtErrKind::UnknownItem`] — any other unrecognized `%` directive, or
173    ///   more than three colons before a directive (e.g. `%::::z`).
174    ///
175    /// ## Examples
176    ///
177    /// ```rust
178    /// use deep_time::StrPTimeFmt;
179    ///
180    /// let fmt = StrPTimeFmt::new("%F %T").unwrap();
181    /// let dt = fmt.to_dt("2025-05-23 14:30:00", false, false, false).unwrap();
182    /// assert_eq!(dt.to_ymd().day(), 23);
183    /// ```
184    pub fn new(fmt: &str) -> Result<Self, DtErr> {
185        if fmt.len() > Self::MAX_FMT_LEN {
186            return Err(an_err!(DtErrKind::InvalidLen));
187        }
188        let fmt = fmt.as_bytes();
189        if !fmt.is_ascii() {
190            return Err(an_err!(DtErrKind::InvalidInput, "must be ascii"));
191        }
192
193        Self::validate_format(fmt)?;
194
195        let mut buffer = [0u8; Self::MAX_FMT_LEN];
196        buffer[..fmt.len()].copy_from_slice(fmt);
197
198        Ok(Self {
199            fmt: buffer,
200            len: fmt.len(),
201        })
202    }
203
204    /// Parses `s` with this format and converts the result to a [`Dt`].
205    ///
206    /// Equivalent to
207    /// [`Dt::from_strptime`](../struct.Dt.html#method.from_strptime)`(s, fmt, …)`
208    /// where `fmt` is the string validated at construction — except the format
209    /// has already been checked, so failures should only reflect the **input**
210    /// (or post-parse conversion), not broken directive syntax.
211    ///
212    /// ## Parameters
213    ///
214    /// - `s`: The input string to parse.
215    /// - `inp_can_end_before_fmt`: Allow input to end before the format is fully
216    ///   consumed (parse what is present, ignore remaining directives).
217    /// - `fmt_can_end_before_inp`: Allow the format to end before the input is
218    ///   fully consumed (trailing characters in `s` are OK).
219    /// - `allow_partial_date`: Default missing month/day to `1` instead of
220    ///   returning [`DtErrKind::Incomplete`].
221    ///
222    /// Full directive list and flag semantics:
223    /// [`Dt::from_strptime`](../struct.Dt.html#method.from_strptime).
224    ///
225    /// ## Errors
226    ///
227    /// Same **input** and conversion errors as
228    /// [`Dt::from_strptime`](../struct.Dt.html#method.from_strptime) (mismatch,
229    /// expected fields, out of range, trailing characters, incomplete date,
230    /// timezone issues, etc.). Format-structure errors for the stored pattern
231    /// are not expected after a successful
232    /// [`new`](../struct.StrPTimeFmt.html#method.new).
233    ///
234    /// ## Examples
235    ///
236    /// ```rust
237    /// use deep_time::StrPTimeFmt;
238    ///
239    /// let fmt = StrPTimeFmt::new("%F %T").unwrap();
240    /// let dt = fmt.to_dt("2025-05-23 14:30:00", false, false, false).unwrap();
241    /// assert_eq!(dt.to_ymd().hr(), 14);
242    /// ```
243    pub fn to_dt(
244        &self,
245        s: &str,
246        inp_can_end_before_fmt: bool,
247        fmt_can_end_before_inp: bool,
248        allow_partial_date: bool,
249    ) -> Result<Dt, DtErr> {
250        Parts::from_strptime(
251            self.as_str()?,
252            s,
253            inp_can_end_before_fmt,
254            fmt_can_end_before_inp,
255            allow_partial_date,
256        )
257        .and_then(|p| p.to_dt())
258    }
259
260    /// Parse `s` with this format, then format the resulting [`Dt`] with
261    /// `output_fmt` into an owned [`String`](`alloc::string::String`).
262    ///
263    /// Requires the `alloc` feature. Prefer
264    /// [`to_str_b`](../struct.StrPTimeFmt.html#method.to_str_b) when you want a
265    /// fixed stack buffer and no allocator.
266    ///
267    /// **Important:** only the format stored in `self` is pre-validated.
268    /// `output_fmt` is passed straight to formatting and may fail with
269    /// format-structure errors (truncated `%`, unknown directives, etc.) if it
270    /// is invalid. Validate it separately with
271    /// [`StrPTimeFmt::new`](../struct.StrPTimeFmt.html#method.new) if you need
272    /// the same guarantee for a parse pattern (some print-only spellings are
273    /// still rejected by `new`).
274    ///
275    /// ## Parameters
276    ///
277    /// - `s`: Input datetime string, parsed with `self`.
278    /// - `output_fmt`: Format string for the **output** (not pre-validated).
279    /// - `inp_can_end_before_fmt`, `fmt_can_end_before_inp`, `allow_partial_date`:
280    ///   passed through to [`to_dt`](../struct.StrPTimeFmt.html#method.to_dt).
281    /// - `lang`: Language for weekday/month names in the output.
282    ///
283    /// ## Examples
284    ///
285    /// ```rust
286    /// # #[cfg(feature = "alloc")]
287    /// # {
288    /// use deep_time::{Lang, StrPTimeFmt};
289    ///
290    /// let fmt = StrPTimeFmt::new("%Y-%m-%dT%H:%M:%S").unwrap();
291    /// let s = fmt
292    ///     .to_str("2000-01-01T12:00:00", "%d %m %Y %H:%M:%S", false, false, false, Lang::En)
293    ///     .unwrap();
294    /// assert_eq!(s, "01 01 2000 12:00:00");
295    /// # }
296    /// ```
297    #[cfg(feature = "alloc")]
298    pub fn to_str(
299        &self,
300        s: &str,
301        output_fmt: &str,
302        inp_can_end_before_fmt: bool,
303        fmt_can_end_before_inp: bool,
304        allow_partial_date: bool,
305        lang: Lang,
306    ) -> Result<alloc::string::String, DtErr> {
307        Parts::from_strptime(
308            self.as_str()?,
309            s,
310            inp_can_end_before_fmt,
311            fmt_can_end_before_inp,
312            allow_partial_date,
313        )?
314        .to_dt()?
315        .to_str(output_fmt, lang)
316    }
317
318    /// Parse `s` with this format, then format the resulting [`Dt`] with
319    /// `output_fmt` into a stack [`BufStr`].
320    ///
321    /// Same pipeline as [`to_str`](../struct.StrPTimeFmt.html#method.to_str),
322    /// without requiring `alloc`.
323    ///
324    /// **Important:** only the format stored in `self` is pre-validated.
325    /// `output_fmt` is not checked at construction of this value and may fail
326    /// if it contains invalid or unsupported directives. See
327    /// [`to_str`](../struct.StrPTimeFmt.html#method.to_str) for details.
328    ///
329    /// ## Parameters
330    ///
331    /// - `s`: Input datetime string, parsed with `self`.
332    /// - `output_fmt`: Format string for the **output** (not pre-validated).
333    /// - `inp_can_end_before_fmt`, `fmt_can_end_before_inp`, `allow_partial_date`:
334    ///   passed through to [`to_dt`](../struct.StrPTimeFmt.html#method.to_dt).
335    /// - `lang`: Language for weekday/month names in the output.
336    ///
337    /// ## Examples
338    ///
339    /// ```rust
340    /// use deep_time::{Lang, StrPTimeFmt};
341    ///
342    /// let fmt = StrPTimeFmt::new("%Y-%m-%dT%H:%M:%S").unwrap();
343    /// let s = fmt
344    ///     .to_str_b("2000-01-01T12:00:00", "%d %m %Y %H:%M:%S", false, false, false, Lang::En)
345    ///     .unwrap();
346    /// assert_eq!(s.as_str(), "01 01 2000 12:00:00");
347    /// ```
348    pub fn to_str_b(
349        &self,
350        s: &str,
351        output_fmt: &str,
352        inp_can_end_before_fmt: bool,
353        fmt_can_end_before_inp: bool,
354        allow_partial_date: bool,
355        lang: Lang,
356    ) -> Result<BufStr<STRTIME_SIZE>, DtErr> {
357        Parts::from_strptime(
358            self.as_str()?,
359            s,
360            inp_can_end_before_fmt,
361            fmt_can_end_before_inp,
362            allow_partial_date,
363        )?
364        .to_dt()?
365        .to_str_b(output_fmt, lang)
366    }
367
368    fn validate_format(mut fmt: &[u8]) -> Result<(), DtErr> {
369        while !fmt.is_empty() {
370            if fmt[0] != b'%' {
371                // literal character (including whitespace) — always valid
372                fmt = &fmt[1..];
373                continue;
374            }
375
376            // lone % at end of format
377            if fmt.len() == 1 {
378                return Err(an_err!(DtErrKind::TruncatedDirective));
379            }
380            fmt = &fmt[1..]; // eat %
381
382            // Skip format extensions (flag / width / colons)
383            // Flag (at most one)
384            if !fmt.is_empty() {
385                match fmt[0] {
386                    b'-' | b'_' | b'0' | b'^' | b'#' => {
387                        fmt = &fmt[1..];
388                    }
389                    _ => {}
390                }
391            }
392
393            // Width: consume all consecutive digits (parser consumes any number of digits)
394            while !fmt.is_empty() && fmt[0].is_ascii_digit() {
395                fmt = &fmt[1..];
396            }
397
398            // Colons: match the parser — hard cap at 3. Extra `:` become the
399            // directive byte and must be rejected as unknown (e.g. `%::::z`).
400            let mut colons = 0u8;
401            while colons < 3 && !fmt.is_empty() && fmt[0] == b':' {
402                fmt = &fmt[1..];
403                colons += 1;
404            }
405
406            if fmt.is_empty() {
407                return Err(an_err!(DtErrKind::UnexpectedEnd));
408            }
409
410            let directive = fmt[0];
411
412            match directive {
413            // all currently supported directives
414            b'%' | b'A' | b'a' | b'B' | b'b' | b'h' | b'C' | b'd' | b'e' |
415            b'f' | b'N' | b'G' | b'g' | b'H' | b'k' | b'I' | b'l' | b'j' |
416            b'J' | b'M' | b'm' | b'n' | b't' | b'P' | b'p' | b'Q' | b'S' | b's' |
417            b'U' | b'u' | b'V' | b'W' | b'w' | b'Y' | b'y' | b'z' |
418            // shortcuts
419            b'F' | b'D' | b'T' | b'R' |
420            // library directives
421            b'L' | b'*' => {
422                fmt = &fmt[1..];
423            }
424
425            b'.' => {
426                // special case for %.f / %.3N / %-.3f etc.
427                fmt = &fmt[1..]; // eat the .
428
429                // optional width/precision digits (e.g. 3 in %.3N)
430                while !fmt.is_empty() && fmt[0].is_ascii_digit() {
431                    fmt = &fmt[1..];
432                }
433
434                if fmt.is_empty() {
435                    return Err(an_err!(DtErrKind::ExpectedFractional));
436                }
437                let next = fmt[0];
438                if !matches!(next, b'f' | b'N') {
439                    return Err(an_err!(DtErrKind::InvalidFractional, "{}", char::from(next)));
440                }
441                fmt = &fmt[1..];
442            }
443
444            // explicitly unsupported
445            b'c' | b'r' | b'X' | b'x' | b'Z' => {
446                return Err(an_err!(
447                    DtErrKind::UnsupportedItem,
448                    "{}",
449                    char::from(directive)
450                ));
451            }
452
453            _ => {
454                return Err(an_err!(DtErrKind::UnknownItem));
455            }
456        }
457        }
458
459        Ok(())
460    }
461
462    #[inline]
463    fn as_bytes(&self) -> &[u8] {
464        &self.fmt[..self.len]
465    }
466
467    #[inline]
468    fn as_str(&self) -> Result<&str, DtErr> {
469        match core::str::from_utf8(self.as_bytes()) {
470            Ok(f) => Ok(f),
471            Err(e) => Err(an_err!(DtErrKind::InvalidBytes, "{}", e)),
472        }
473    }
474}
475
476impl Dt {
477    /// Validates a `strptime`-style format into a reusable [`StrPTimeFmt`].
478    ///
479    /// Convenience alias for
480    /// [`StrPTimeFmt::new`](../struct.StrPTimeFmt.html#method.new). The format is
481    /// checked once for syntax and supported directives, then stored in a
482    /// fixed-size buffer.
483    ///
484    /// After this succeeds, parsing with the returned value will not fail because
485    /// the format itself is unusable — only because the **input** does not match
486    /// (or conversion fails). See the guarantee on
487    /// [`StrPTimeFmt`](../struct.StrPTimeFmt.html).
488    ///
489    /// This is **not** a performance optimization; it only validates and owns a
490    /// copy of the format. Only ASCII formats up to
491    /// [`StrPTimeFmt::MAX_FMT_LEN`](../struct.StrPTimeFmt.html#associatedconstant.MAX_FMT_LEN)
492    /// bytes are accepted.
493    ///
494    /// ## Parameters
495    ///
496    /// - `strptime_fmt`: Parse format using `%` directives (e.g. `"%Y-%m-%d %H:%M:%S"`,
497    ///   `"%F %T"`, `"%Y-%m-%dT%H:%M:%S%.3fZ"`). Same grammar as
498    ///   [`from_strptime`](../struct.Dt.html#method.from_strptime).
499    ///
500    /// ## Errors
501    ///
502    /// Same as [`StrPTimeFmt::new`](../struct.StrPTimeFmt.html#method.new): overlong,
503    /// non-ASCII, truncated/malformed directives, unknown or unsupported items
504    /// (`%c`, `%r`, `%x`, `%X`, `%Z`, etc.).
505    ///
506    /// ## Examples
507    ///
508    /// ```rust
509    /// use deep_time::Dt;
510    ///
511    /// let fmt = Dt::parse_fmt("%F %T").unwrap();
512    /// let dt = fmt.to_dt("2025-05-23 14:30:00", false, false, false).unwrap();
513    /// assert_eq!(dt.to_ymd().min(), 30);
514    /// ```
515    #[inline(always)]
516    pub fn parse_fmt(strptime_fmt: &str) -> Result<StrPTimeFmt, DtErr> {
517        StrPTimeFmt::new(strptime_fmt)
518    }
519}
520
521#[cfg(feature = "defmt")]
522impl defmt::Format for StrPTimeFmt {
523    fn format(&self, f: defmt::Formatter) {
524        match self.as_str() {
525            Ok(fmt) => defmt::write!(f, "{}", fmt),
526            Err(_) => defmt::write!(f, "StrPTimeFmt<invalid utf8>"),
527        }
528    }
529}