Skip to main content

deep_time/strtime/
mod.rs

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