Skip to main content

deep_time/strptime/
mod.rs

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