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().unwrap(), "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 // reuse existing helper for flags/width/colons
225 let (_, _, _, new_fmt) = Parser::parse_format_extensions(fmt, 0);
226 fmt = new_fmt;
227
228 if fmt.is_empty() {
229 return Err(an_err!(DtErrKind::UnexpectedEnd, "expected directive"));
230 }
231
232 let directive = fmt[0];
233
234 match directive {
235 // all currently supported directives
236 b'%' | b'A' | b'a' | b'B' | b'b' | b'h' | b'C' | b'd' | b'e' |
237 b'f' | b'N' | b'G' | b'g' | b'H' | b'k' | b'I' | b'l' | b'j' |
238 b'M' | b'm' | b'n' | b't' | b'P' | b'p' | b'Q' | b'S' | b's' |
239 b'U' | b'u' | b'V' | b'W' | b'w' | b'Y' | b'y' | b'z' |
240 // shortcuts
241 b'F' | b'D' | b'T' | b'R' |
242 // library directives
243 b'L' | b'*' => {
244 fmt = &fmt[1..];
245 }
246
247 b'.' => {
248 // special case for %.f / %.3N etc.
249 fmt = &fmt[1..]; // eat the .
250
251 // optional width digits
252 while !fmt.is_empty() && fmt[0].is_ascii_digit() {
253 fmt = &fmt[1..];
254 }
255
256 let next = fmt.first().copied().unwrap_or(0);
257 if !matches!(next, b'f' | b'N') {
258 return Err(an_err!(DtErrKind::BadFractional, "{}", char::from(next)));
259 }
260 fmt = &fmt[1..];
261 }
262
263 // explicitly unsupported (same as Parser)
264 b'c' | b'r' | b'X' | b'x' | b'Z' => {
265 return Err(an_err!(
266 DtErrKind::UnsupportedDirective,
267 "{}",
268 char::from(directive)
269 ));
270 }
271
272 _ => {
273 return Err(an_err!(DtErrKind::UnknownDirective));
274 }
275 }
276 }
277
278 Ok(())
279 }
280
281 #[inline]
282 fn as_bytes(&self) -> &[u8] {
283 &self.fmt[..self.len]
284 }
285
286 #[inline]
287 fn as_str(&self) -> Result<&str, DtErr> {
288 match core::str::from_utf8(self.as_bytes()) {
289 Ok(f) => Ok(f),
290 Err(e) => Err(an_err!(DtErrKind::InvalidBytes, "{}", e)),
291 }
292 }
293}