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