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