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