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