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