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