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::*;
10
11/// Optional `%` directive extensions: flag, width, and colon count.
12#[derive(Clone, Copy, Debug, Default)]
13pub(crate) struct FmtExtensions {
14    pub(crate) flag: FmtFlag,
15    pub(crate) width: Option<u8>,
16    pub(crate) colons: u8,
17}
18
19/// Flags that may appear immediately after `%` and before the directive.
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21pub(crate) enum FmtFlag {
22    #[default]
23    None,
24    PadSpace,
25    PadZero,
26    NoPad,
27    Uppercase,
28    Swapcase,
29}
30
31impl FmtFlag {
32    #[inline(always)]
33    pub(crate) fn from_byte(byte: u8) -> Self {
34        match byte {
35            b'_' => Self::PadSpace,
36            b'0' => Self::PadZero,
37            b'-' => Self::NoPad,
38            b'^' => Self::Uppercase,
39            b'#' => Self::Swapcase,
40            _ => Self::None,
41        }
42    }
43
44    /// Resolve the padding flag for numeric parsing.
45    ///
46    /// `None`, `Uppercase`, and `Swapcase` defer to the directive default;
47    /// the three pad flags override it.
48    #[inline(always)]
49    pub(crate) fn resolve(self, default: FmtFlag) -> FmtFlag {
50        match self {
51            Self::None | Self::Uppercase | Self::Swapcase => default,
52            pad => pad,
53        }
54    }
55}
56
57/// A pre-validated, reusable date/time format string.
58///
59/// - Format is validated **once** at construction (`new` returns `Result`).
60/// - Format bytes are copied into an owned fixed-size buffer.
61/// - Only ASCII formats are accepted.
62///
63/// ## See also
64///
65/// - [`StrPTimeFmt::new`](../struct.StrPTimeFmt.html#method.new)
66/// - [`StrPTimeFmt::to_dt`](../struct.StrPTimeFmt.html#method.to_dt)
67/// - [`StrPTimeFmt::to_str`](../struct.StrPTimeFmt.html#method.to_str)
68#[derive(Debug, Clone)]
69pub struct StrPTimeFmt {
70    fmt: [u8; Self::MAX_FMT_LEN],
71    len: usize,
72}
73
74impl StrPTimeFmt {
75    /// Maximum allowed length of a format string in bytes.
76    pub const MAX_FMT_LEN: usize = 256;
77
78    /// Creates a new validated format.
79    ///
80    /// - Validates syntax and supported directives.
81    /// - Requires the format to be valid ASCII and ≤ 256 bytes.
82    /// - Returns a [`DtErr`] on any failure.
83    ///
84    /// ## Errors
85    ///
86    /// - [`DtErrKind::InvalidLen`] if the format string is longer than 256 bytes.
87    /// - [`DtErrKind::InvalidInput`] if the format string is not valid ASCII.
88    /// - [`DtErrKind::TruncatedDirective`] if a `%` appears at the end of the format
89    ///   with no directive character following it.
90    /// - [`DtErrKind::UnexpectedEnd`] if a `%` is followed only by flags, width digits,
91    ///   or colons, with no directive character after them.
92    /// - [`DtErrKind::ExpectedFractional`] if a `%.` sequence is not followed by a
93    ///   directive character.
94    /// - [`DtErrKind::InvalidFractional`] if a `%.` sequence is followed by a character
95    ///   other than `f` or `N`.
96    /// - [`DtErrKind::UnsupportedItem`] if the format contains `%c`, `%r`, `%x`, `%X`,
97    ///   or `%Z`.
98    /// - [`DtErrKind::UnknownItem`] if the format contains any other unrecognized `%`
99    ///   directive.
100    ///
101    /// ## Examples
102    ///
103    /// ```rust
104    /// # #[cfg(feature = "parse")]
105    /// # {
106    /// use deep_time::{Dt, Lang, StrPTimeFmt};
107    ///
108    /// let fmt = Dt::parse_fmt("%F %T").unwrap();
109    ///
110    /// // parse a datetime
111    /// let dt = fmt.to_dt("2025-05-23 14:30:00", false, false, false).unwrap();
112    ///
113    /// // change a datetimes format
114    /// let s = fmt.to_str("2000-01-01 12:00:00", "%d %m %Y %H:%M:%S", false, false, false, Lang::En).unwrap();
115    ///
116    /// assert_eq!(s, "01 01 2000 12:00:00");
117    /// # }
118    /// ```
119    pub fn new(fmt: &str) -> Result<Self, DtErr> {
120        if fmt.len() > Self::MAX_FMT_LEN {
121            return Err(an_err!(DtErrKind::InvalidLen));
122        }
123        let fmt = fmt.as_bytes();
124        if !fmt.is_ascii() {
125            return Err(an_err!(DtErrKind::InvalidInput, "must be ascii"));
126        }
127
128        Self::validate_format(fmt)?;
129
130        let mut buffer = [0u8; Self::MAX_FMT_LEN];
131        buffer[..fmt.len()].copy_from_slice(fmt);
132
133        Ok(Self {
134            fmt: buffer,
135            len: fmt.len(),
136        })
137    }
138
139    /// Parses a date/time string using this pre-validated format.
140    ///
141    /// The four boolean flags control lenient parsing behavior — see
142    /// [`Dt::from_str`](../struct.Dt.html#method.from_str) for full documentation.
143    ///
144    /// ## Parameters
145    ///
146    /// - `s`: The input string to parse.
147    /// - `inp_can_end_before_fmt`: Allow input to end before format is fully consumed.
148    /// - `fmt_can_end_before_inp`: Allow format to end before input is fully consumed.
149    /// - `allow_partial_date`: Default missing month/day to `1` instead of erroring.
150    ///
151    /// ## Errors
152    ///
153    /// - [`DtErrKind::InvalidBytes`] if `as_str()` fails to convert the stored format
154    ///   back to `&str`.
155    /// - Any error returned by `Parts::from_str` followed by `Parts::to_dt` (see the
156    ///   error documentation on [`Dt::from_str`] for the complete list).
157    ///
158    /// ## Examples
159    ///
160    /// ```rust
161    /// use deep_time::{Dt, StrPTimeFmt};
162    ///
163    /// let fmt = Dt::parse_fmt("%F %T").unwrap();
164    /// let dt = fmt.to_dt("2025-05-23 14:30:00", false, false, false).unwrap();
165    /// ```
166    pub fn to_dt(
167        &self,
168        s: &str,
169        inp_can_end_before_fmt: bool,
170        fmt_can_end_before_inp: bool,
171        allow_partial_date: bool,
172    ) -> Result<Dt, DtErr> {
173        Parts::from_str(
174            self.as_str()?,
175            s,
176            inp_can_end_before_fmt,
177            fmt_can_end_before_inp,
178            allow_partial_date,
179        )
180        .and_then(|p| p.to_dt())
181    }
182
183    /// Formats a [`Dt`] into a string using this pre-validated format and a given
184    /// output format.
185    ///
186    /// Effectively parses a [`prim@str`] with the contained format, then outputs a
187    /// [`String`](`alloc::string::String`) with a new given format.
188    ///
189    /// Requires the `alloc` feature.
190    ///
191    /// ## Parameters
192    ///
193    /// - `s`: datetime input [`prim@str`].
194    /// - `output_fmt`: The new format to output the datetime as.
195    /// - The remaining three flags are passed through to the internal `to_dt` call.
196    ///
197    /// ## Examples
198    ///
199    /// ```rust
200    /// # #[cfg(feature = "alloc")]
201    /// # {
202    /// use deep_time::{Dt, Lang, StrPTimeFmt};
203    ///
204    /// let fmt = Dt::parse_fmt("%Y-%m-%dT%H:%M:%S").unwrap();
205    /// let s = fmt.to_str("2000-01-01T12:00:00", "%d %m %Y %H:%M:%S", false, false, false, Lang::En).unwrap();
206    ///
207    /// assert_eq!(s, "01 01 2000 12:00:00");
208    /// # }
209    /// ```
210    #[cfg(feature = "alloc")]
211    pub fn to_str(
212        &self,
213        s: &str,
214        output_fmt: &str,
215        inp_can_end_before_fmt: bool,
216        fmt_can_end_before_inp: bool,
217        allow_partial_date: bool,
218        lang: Lang,
219    ) -> Result<alloc::string::String, DtErr> {
220        Parts::from_str(
221            self.as_str()?,
222            s,
223            inp_can_end_before_fmt,
224            fmt_can_end_before_inp,
225            allow_partial_date,
226        )?
227        .to_dt()?
228        .to_str(output_fmt, lang)
229    }
230
231    /// Formats a [`Dt`] into a [`BufStr`] using this pre-validated format and a given
232    /// output format.
233    ///
234    /// Effectively parses a [`prim@str`] with the contained format, then outputs a
235    /// [`BufStr`] with a new given format.
236    ///
237    /// ## Parameters
238    ///
239    /// - `s`: datetime input [`prim@str`].
240    /// - `output_fmt`: The new format to output the datetime as.
241    /// - The remaining three flags are passed through to the internal `to_dt` call.
242    ///
243    /// ## Examples
244    ///
245    /// ```rust
246    /// use deep_time::{Dt, Lang, StrPTimeFmt};
247    ///
248    /// let fmt = Dt::parse_fmt("%Y-%m-%dT%H:%M:%S").unwrap();
249    /// let s = fmt.to_str_b("2000-01-01T12:00:00", "%d %m %Y %H:%M:%S", false, false, false, Lang::En).unwrap();
250    ///
251    /// assert_eq!(s.as_str(), "01 01 2000 12:00:00");
252    /// ```
253    pub fn to_str_b(
254        &self,
255        s: &str,
256        output_fmt: &str,
257        inp_can_end_before_fmt: bool,
258        fmt_can_end_before_inp: bool,
259        allow_partial_date: bool,
260        lang: Lang,
261    ) -> Result<BufStr<STRTIME_SIZE>, DtErr> {
262        Parts::from_str(
263            self.as_str()?,
264            s,
265            inp_can_end_before_fmt,
266            fmt_can_end_before_inp,
267            allow_partial_date,
268        )?
269        .to_dt()?
270        .to_str_b(output_fmt, lang)
271    }
272
273    fn validate_format(mut fmt: &[u8]) -> Result<(), DtErr> {
274        while !fmt.is_empty() {
275            if fmt[0] != b'%' {
276                // literal character (including whitespace) — always valid
277                fmt = &fmt[1..];
278                continue;
279            }
280
281            // lone % at end of format
282            if fmt.len() == 1 {
283                return Err(an_err!(DtErrKind::TruncatedDirective));
284            }
285            fmt = &fmt[1..]; // eat %
286
287            // Skip format extensions (flag / width / colons)
288            // Flag (at most one)
289            if !fmt.is_empty() {
290                match fmt[0] {
291                    b'-' | b'_' | b'0' | b'^' | b'#' => {
292                        fmt = &fmt[1..];
293                    }
294                    _ => {}
295                }
296            }
297
298            // Width: consume all consecutive digits (parser consumes any number of digits)
299            while !fmt.is_empty() && fmt[0].is_ascii_digit() {
300                fmt = &fmt[1..];
301            }
302
303            // Colons: consume all consecutive colons
304            while !fmt.is_empty() && fmt[0] == b':' {
305                fmt = &fmt[1..];
306            }
307
308            if fmt.is_empty() {
309                return Err(an_err!(DtErrKind::UnexpectedEnd));
310            }
311
312            let directive = fmt[0];
313
314            match directive {
315            // all currently supported directives
316            b'%' | b'A' | b'a' | b'B' | b'b' | b'h' | b'C' | b'd' | b'e' |
317            b'f' | b'N' | b'G' | b'g' | b'H' | b'k' | b'I' | b'l' | b'j' |
318            b'J' | b'M' | b'm' | b'n' | b't' | b'P' | b'p' | b'Q' | b'S' | b's' |
319            b'U' | b'u' | b'V' | b'W' | b'w' | b'Y' | b'y' | b'z' |
320            // shortcuts
321            b'F' | b'D' | b'T' | b'R' |
322            // library directives
323            b'L' | b'*' => {
324                fmt = &fmt[1..];
325            }
326
327            b'.' => {
328                // special case for %.f / %.3N / %-.3f etc.
329                fmt = &fmt[1..]; // eat the .
330
331                // optional width/precision digits (e.g. 3 in %.3N)
332                while !fmt.is_empty() && fmt[0].is_ascii_digit() {
333                    fmt = &fmt[1..];
334                }
335
336                if fmt.is_empty() {
337                    return Err(an_err!(DtErrKind::ExpectedFractional));
338                }
339                let next = fmt[0];
340                if !matches!(next, b'f' | b'N') {
341                    return Err(an_err!(DtErrKind::InvalidFractional, "{}", char::from(next)));
342                }
343                fmt = &fmt[1..];
344            }
345
346            // explicitly unsupported
347            b'c' | b'r' | b'X' | b'x' | b'Z' => {
348                return Err(an_err!(
349                    DtErrKind::UnsupportedItem,
350                    "{}",
351                    char::from(directive)
352                ));
353            }
354
355            _ => {
356                return Err(an_err!(DtErrKind::UnknownItem));
357            }
358        }
359        }
360
361        Ok(())
362    }
363
364    #[inline]
365    fn as_bytes(&self) -> &[u8] {
366        &self.fmt[..self.len]
367    }
368
369    #[inline]
370    fn as_str(&self) -> Result<&str, DtErr> {
371        match core::str::from_utf8(self.as_bytes()) {
372            Ok(f) => Ok(f),
373            Err(e) => Err(an_err!(DtErrKind::InvalidBytes, "{}", e)),
374        }
375    }
376}
377
378#[cfg(feature = "defmt")]
379impl defmt::Format for StrPTimeFmt {
380    fn format(&self, f: defmt::Formatter) {
381        match self.as_str() {
382            Ok(fmt) => defmt::write!(f, "{}", fmt),
383            Err(_) => defmt::write!(f, "StrPTimeFmt<invalid utf8>"),
384        }
385    }
386}