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