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