Skip to main content

deep_time/strptime/
mod.rs

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