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