deep_time/strptime/mod.rs
1pub mod parser;
2
3#[cfg(feature = "alloc")]
4use crate::Lang;
5use crate::error::{DtErr, DtErrKind};
6use crate::{Dt, TimeParts, an_err};
7use core::result::Result;
8use core::str;
9
10pub(crate) use parser::*;
11
12/// A pre-validated, reusable date/time format string.
13///
14/// - Format is validated **once** at construction (`new` returns `Result`).
15/// - Format bytes are copied into an owned fixed-size buffer.
16/// - Only ASCII formats are accepted.
17///
18/// ## See also
19///
20/// - [`StrPTimeFmt::new`]
21/// - [`StrPTimeFmt::to_dt`]
22/// - [`StrPTimeFmt::to_str`]
23#[derive(Debug, Clone, Copy)]
24pub struct StrPTimeFmt {
25 fmt: [u8; Self::MAX_FORMAT_LEN],
26 len: usize,
27}
28
29impl StrPTimeFmt {
30 pub const MAX_FORMAT_LEN: usize = 256;
31
32 /// Creates a new validated format.
33 ///
34 /// - Validates syntax and supported directives.
35 /// - Requires the format to be valid ASCII and ≤ 256 bytes.
36 /// - Returns a `DtErr` on any failure.
37 ///
38 /// ## Examples
39 ///
40 /// ```rust
41 /// # #[cfg(feature = "parse")]
42 /// # {
43 /// use deep_time::{Dt, Lang, StrPTimeFmt};
44 ///
45 /// let fmt = Dt::parse_fmt("%F %T").unwrap();
46 ///
47 /// // parse a datetime
48 /// let dt = fmt.to_dt("2025-05-23 14:30:00", false, false, false).unwrap();
49 ///
50 /// // change a datetimes format
51 /// let s = fmt.to_str("2000-01-01 12:00:00", "%d %m %Y %H:%M:%S", false, false, false, Lang::En).unwrap();
52 ///
53 /// assert_eq!(s, "01 01 2000 12:00:00");
54 /// # }
55 /// ```
56 pub fn new(fmt: &str) -> Result<Self, DtErr> {
57 if fmt.len() > Self::MAX_FORMAT_LEN {
58 return Err(an_err!(
59 DtErrKind::UnexpectedEnd,
60 "format string too long (max {} bytes)",
61 Self::MAX_FORMAT_LEN
62 ));
63 }
64 let fmt = fmt.as_bytes();
65 if !fmt.is_ascii() {
66 return Err(an_err!(
67 DtErrKind::UnexpectedEnd,
68 "format string must be ASCII"
69 ));
70 }
71
72 Self::validate_format(fmt)?;
73
74 let mut buffer = [0u8; Self::MAX_FORMAT_LEN];
75 buffer[..fmt.len()].copy_from_slice(fmt);
76
77 Ok(Self {
78 fmt: buffer,
79 len: fmt.len(),
80 })
81 }
82
83 /// Parses a date/time string using this pre-validated format.
84 ///
85 /// The four boolean flags control lenient parsing behavior — see
86 /// [`Dt::from_str`](../struct.Dt.html#method.from_str) for full documentation.
87 ///
88 /// ## Parameters
89 ///
90 /// - `s`: The input string to parse.
91 /// - `inp_can_end_before_fmt`: Allow input to end before format is fully consumed.
92 /// - `fmt_can_end_before_inp`: Allow format to end before input is fully consumed.
93 /// - `allow_partial_date`: Default missing month/day to `1` instead of erroring.
94 ///
95 /// ## Errors
96 ///
97 /// Returns [`DtErr`] for parse failures, incomplete data, trailing characters, etc.
98 ///
99 /// ## Examples
100 ///
101 /// ```rust
102 /// use deep_time::{Dt, StrPTimeFmt};
103 ///
104 /// let fmt = Dt::parse_fmt("%F %T").unwrap();
105 /// let dt = fmt.to_dt("2025-05-23 14:30:00", false, false, false).unwrap();
106 /// ```
107 pub fn to_dt(
108 &self,
109 s: &str,
110 inp_can_end_before_fmt: bool,
111 fmt_can_end_before_inp: bool,
112 allow_partial_date: bool,
113 ) -> Result<Dt, DtErr> {
114 TimeParts::from_str(
115 self.as_str()?,
116 s,
117 inp_can_end_before_fmt,
118 fmt_can_end_before_inp,
119 allow_partial_date,
120 )
121 .and_then(|p| p.to_dt())
122 }
123
124 /// Formats a [`Dt`] into a string using this pre-validated format and a given
125 /// output format.
126 ///
127 /// Effectively parses a [`str`] with the contained format, then outputs a
128 /// [`String`](`alloc::string::String`) to a new given format.
129 ///
130 /// Requires the `alloc` feature.
131 ///
132 /// ## Parameters
133 ///
134 /// - `s`: datetime input [`str`].
135 /// - `output_fmt`: The new format to output the datetime as.
136 /// - The remaining three flags are passed through to the internal `to_dt` call.
137 ///
138 /// ## Examples
139 ///
140 /// ```rust
141 /// # #[cfg(feature = "alloc")]
142 /// # {
143 /// use deep_time::{Dt, Lang, StrPTimeFmt};
144 ///
145 /// let fmt = Dt::parse_fmt("%Y-%m-%dT%H:%M:%S").unwrap();
146 /// let s = fmt.to_str("2000-01-01T12:00:00", "%d %m %Y %H:%M:%S", false, false, false, Lang::En).unwrap();
147 ///
148 /// assert_eq!(s, "01 01 2000 12:00:00");
149 /// # }
150 /// ```
151 #[cfg(feature = "alloc")]
152 pub fn to_str(
153 &self,
154 s: &str,
155 output_fmt: &str,
156 inp_can_end_before_fmt: bool,
157 fmt_can_end_before_inp: bool,
158 allow_partial_date: bool,
159 lang: Lang,
160 ) -> Result<alloc::string::String, DtErr> {
161 let parts = TimeParts::from_str(
162 self.as_str()?,
163 s,
164 inp_can_end_before_fmt,
165 fmt_can_end_before_inp,
166 allow_partial_date,
167 )?;
168 parts.to_dt()?.to_str(output_fmt, lang)
169 }
170
171 fn validate_format(mut fmt: &[u8]) -> Result<(), DtErr> {
172 while !fmt.is_empty() {
173 if fmt[0] != b'%' {
174 // literal character (including whitespace) — always valid
175 fmt = &fmt[1..];
176 continue;
177 }
178
179 // lone % at end of format
180 if fmt.len() == 1 {
181 return Err(an_err!(DtErrKind::UnexpectedEnd, "after %"));
182 }
183 fmt = &fmt[1..]; // eat %
184
185 // reuse existing helper for flags/width/colons
186 let (_, _, _, new_fmt) = Parser::parse_format_extensions(fmt, 0);
187 fmt = new_fmt;
188
189 if fmt.is_empty() {
190 return Err(an_err!(DtErrKind::UnexpectedEnd, "expected directive"));
191 }
192
193 let directive = fmt[0];
194
195 match directive {
196 // all currently supported directives
197 b'%' | b'A' | b'a' | b'B' | b'b' | b'h' | b'C' | b'd' | b'e' |
198 b'f' | b'N' | b'G' | b'g' | b'H' | b'k' | b'I' | b'l' | b'j' |
199 b'M' | b'm' | b'n' | b't' | b'P' | b'p' | b'Q' | b'S' | b's' |
200 b'U' | b'u' | b'V' | b'W' | b'w' | b'Y' | b'y' | b'z' |
201 // shortcuts
202 b'F' | b'D' | b'T' | b'R' |
203 // library directives
204 b'L' | b'*' => {
205 fmt = &fmt[1..];
206 }
207
208 b'.' => {
209 // special case for %.f / %.3N etc.
210 fmt = &fmt[1..]; // eat the .
211
212 // optional width digits
213 while !fmt.is_empty() && fmt[0].is_ascii_digit() {
214 fmt = &fmt[1..];
215 }
216
217 let next = fmt.first().copied().unwrap_or(0);
218 if !matches!(next, b'f' | b'N') {
219 return Err(an_err!(DtErrKind::BadFractional, "{}", char::from(next)));
220 }
221 fmt = &fmt[1..];
222 }
223
224 // explicitly unsupported (same as Parser)
225 b'c' | b'r' | b'X' | b'x' | b'Z' => {
226 return Err(an_err!(
227 DtErrKind::UnsupportedDirective,
228 "{}",
229 char::from(directive)
230 ));
231 }
232
233 _ => {
234 return Err(an_err!(DtErrKind::UnknownDirective));
235 }
236 }
237 }
238
239 Ok(())
240 }
241
242 #[inline]
243 fn as_bytes(&self) -> &[u8] {
244 &self.fmt[..self.len]
245 }
246
247 #[inline]
248 fn as_str(&self) -> Result<&str, DtErr> {
249 match core::str::from_utf8(self.as_bytes()) {
250 Ok(f) => Ok(f),
251 Err(e) => Err(an_err!(DtErrKind::InvalidBytes, "{}", e)),
252 }
253 }
254}