Skip to main content

deep_time/strptime/
mod.rs

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