1use crate::{
2 Dt, DtErr, DtErrKind, SEC_PER_DAY, SEC_PER_MONTH, SEC_PER_WEEK, SEC_PER_YEAR, Scale, TimeParts,
3 an_err, parser::Parser,
4};
5
6struct ParsedComponent {
7 unit: u8,
8 signed_int: i64,
9 frac_digits: usize,
10 frac_num: i64,
11}
12
13const MAX_FORMAT_LEN: usize = 256;
14
15#[derive(Debug, Clone, Copy)]
21pub struct StrPTimeFmt {
22 fmt: [u8; MAX_FORMAT_LEN],
23 len: usize,
24}
25
26impl StrPTimeFmt {
27 pub fn new(fmt: &str) -> Result<Self, DtErr> {
33 if fmt.len() > MAX_FORMAT_LEN {
34 return Err(an_err!(
35 DtErrKind::UnexpectedEnd,
36 "format string too long (max {} bytes)",
37 MAX_FORMAT_LEN
38 ));
39 }
40 let fmt = fmt.as_bytes();
41 if !fmt.is_ascii() {
42 return Err(an_err!(
43 DtErrKind::UnexpectedEnd,
44 "format string must be ASCII"
45 ));
46 }
47
48 Self::validate_format(fmt)?;
49
50 let mut buffer = [0u8; MAX_FORMAT_LEN];
51 buffer[..fmt.len()].copy_from_slice(fmt);
52
53 Ok(Self {
54 fmt: buffer,
55 len: fmt.len(),
56 })
57 }
58
59 fn validate_format(mut fmt: &[u8]) -> Result<(), DtErr> {
60 while !fmt.is_empty() {
61 if fmt[0] != b'%' {
62 fmt = &fmt[1..];
64 continue;
65 }
66
67 if fmt.len() == 1 {
69 return Err(an_err!(DtErrKind::UnexpectedEnd, "after %"));
70 }
71 fmt = &fmt[1..]; let (_, _, _, new_fmt) = Parser::parse_format_extensions(fmt, 0);
75 fmt = new_fmt;
76
77 if fmt.is_empty() {
78 return Err(an_err!(DtErrKind::UnexpectedEnd, "expected directive"));
79 }
80
81 let directive = fmt[0];
82
83 match directive {
84 b'%' | b'A' | b'a' | b'B' | b'b' | b'h' | b'C' | b'd' | b'e' |
86 b'f' | b'N' | b'G' | b'g' | b'H' | b'k' | b'I' | b'l' | b'j' |
87 b'M' | b'm' | b'n' | b't' | b'P' | b'p' | b'Q' | b'S' | b's' |
88 b'U' | b'u' | b'V' | b'W' | b'w' | b'Y' | b'y' | b'z' |
89 b'F' | b'D' | b'T' | b'R' |
91 b'*' => {
93 fmt = &fmt[1..];
94 }
95
96 b'.' => {
97 fmt = &fmt[1..]; while !fmt.is_empty() && fmt[0].is_ascii_digit() {
102 fmt = &fmt[1..];
103 }
104
105 let next = fmt.first().copied().unwrap_or(0);
106 if !matches!(next, b'f' | b'N') {
107 return Err(an_err!(DtErrKind::BadFractional, "{}", char::from(next)));
108 }
109 fmt = &fmt[1..];
110 }
111
112 b'c' | b'r' | b'X' | b'x' | b'Z' => {
114 return Err(an_err!(
115 DtErrKind::UnsupportedDirective,
116 "{}",
117 char::from(directive)
118 ));
119 }
120
121 _ => {
122 return Err(an_err!(DtErrKind::UnknownDirective));
123 }
124 }
125 }
126
127 Ok(())
128 }
129
130 #[inline]
131 fn as_bytes(&self) -> &[u8] {
132 &self.fmt[..self.len]
133 }
134
135 #[inline]
136 fn as_str(&self) -> Result<&str, DtErr> {
137 match core::str::from_utf8(self.as_bytes()) {
138 Ok(f) => Ok(f),
139 Err(e) => Err(an_err!(DtErrKind::InvalidBytes, "{}", e)),
140 }
141 }
142
143 pub fn to_dt(
145 &self,
146 s: &str,
147 inp_can_end_before_fmt: bool,
148 fmt_can_end_before_inp: bool,
149 allow_partial_date: bool,
150 ) -> Result<Dt, DtErr> {
151 TimeParts::from_str(
152 self.as_str()?,
153 s,
154 inp_can_end_before_fmt,
155 fmt_can_end_before_inp,
156 allow_partial_date,
157 )
158 .and_then(|p| p.to_time_point())
159 }
160
161 #[cfg(feature = "alloc")]
162 pub fn to_str(
163 &self,
164 current: Scale,
165 s: &str,
166 inp_can_end_before_fmt: bool,
167 fmt_can_end_before_inp: bool,
168 allow_partial_date: bool,
169 ) -> Result<alloc::string::String, DtErr> {
170 self.to_dt(
171 s,
172 inp_can_end_before_fmt,
173 fmt_can_end_before_inp,
174 allow_partial_date,
175 )?
176 .to_str(current, self.as_str()?)
177 }
178}
179
180impl Dt {
181 #[inline]
182 pub fn from_str(
183 s: &str,
184 fmt: &str,
185 inp_can_end_before_fmt: bool,
186 fmt_can_end_before_inp: bool,
187 allow_partial_date: bool,
188 ) -> Result<Dt, DtErr> {
189 TimeParts::from_str(
190 fmt,
191 s,
192 inp_can_end_before_fmt,
193 fmt_can_end_before_inp,
194 allow_partial_date,
195 )?
196 .to_time_point()
197 }
198
199 #[inline]
200 pub fn parse_fmt(strptime_fmt: &str) -> Result<StrPTimeFmt, DtErr> {
201 StrPTimeFmt::new(strptime_fmt)
202 }
203
204 pub fn from_iso(s: &str) -> Result<Dt, DtErr> {
205 let len = s.len();
206 if len == 0 {
207 return Err(an_err!(DtErrKind::Incomplete, "empty"));
208 }
209
210 let b = s.as_bytes();
211 let mut i = 0usize;
212
213 let mut sign: i64 = 1;
215 if i < len && matches!(b[i], b'+' | b'-') {
216 if b[i] == b'-' {
217 sign = -1;
218 }
219 i += 1;
220 }
221
222 if i >= len || !matches!(b[i], b'P' | b'p') {
224 return Err(an_err!(DtErrKind::MustStartWith, "P"));
225 }
226 i += 1;
227
228 let t_pos = b[i..]
230 .iter()
231 .position(|&c| matches!(c, b'T' | b't'))
232 .map(|p| i + p);
233
234 let (date_part, time_part) = match t_pos {
235 Some(pos) => {
236 if pos == len - 1 {
237 return Err(an_err!(DtErrKind::InvalidSyntax, "T with no time"));
238 }
239 if b[pos + 1..].iter().any(|&c| matches!(c, b'T' | b't')) {
240 return Err(an_err!(DtErrKind::InvalidSyntax, "multiple T"));
241 }
242 (&b[i..pos], &b[pos + 1..])
243 }
244 None => (&b[i..], &[] as &[u8]),
245 };
246
247 let mut has_fraction = false;
248 let mut total_nanos: i128 = 0;
249
250 Self::parse_duration_part(date_part, &mut total_nanos, true, sign, &mut has_fraction)?;
252 Self::parse_duration_part(time_part, &mut total_nanos, false, sign, &mut has_fraction)?;
253
254 let total_attos = total_nanos * 1_000_000_000i128;
256 Ok(Dt::from_attos(total_attos, Scale::TAI))
257 }
258
259 fn parse_next_component(
262 chars: &[u8],
263 i: &mut usize,
264 sign: i64,
265 has_fraction: &mut bool,
266 ) -> Result<Option<ParsedComponent>, DtErr> {
267 if *i >= chars.len() {
268 return Ok(None);
269 }
270
271 if *has_fraction {
272 return Err(an_err!(DtErrKind::InvalidSyntax, "components after frac"));
273 }
274
275 let start = *i;
277 while *i < chars.len() && chars[*i].is_ascii_digit() {
278 *i += 1;
279 }
280 if start == *i {
281 return Err(an_err!(DtErrKind::ExpectedValue, "number"));
282 }
283
284 let int_str = core::str::from_utf8(&chars[start..*i])
285 .map_err(|_| an_err!(DtErrKind::InvalidNumber, "invalid utf8 in int"))?;
286 let int: i64 = int_str.parse().map_err(|e: core::num::ParseIntError| {
287 an_err!(DtErrKind::InvalidNumber, "{}: {}", int_str, e)
288 })?;
289
290 let mut frac_num: i64 = 0;
292 let mut frac_digits: usize = 0;
293 if *i < chars.len() && matches!(chars[*i], b'.' | b',') {
294 *i += 1;
295 let frac_start = *i;
296 while *i < chars.len() && chars[*i].is_ascii_digit() {
297 *i += 1;
298 }
299 frac_digits = *i - frac_start;
300 if frac_digits == 0 {
301 return Err(an_err!(DtErrKind::ExpectedValue, "empty frac after ."));
302 }
303 if frac_digits > 9 {
304 return Err(an_err!(DtErrKind::OutOfRange, "frac >9"));
305 }
306
307 let frac_str = core::str::from_utf8(&chars[frac_start..*i])
308 .map_err(|_| an_err!(DtErrKind::InvalidNumber, "invalid utf8 in frac"))?;
309 frac_num = frac_str.parse().map_err(|e: core::num::ParseIntError| {
310 an_err!(DtErrKind::InvalidNumber, "{}: {}", frac_str, e)
311 })?;
312 }
313
314 if *i >= chars.len() {
316 return Err(an_err!(
317 DtErrKind::InvalidSyntax,
318 "missing unit after number"
319 ));
320 }
321 let unit = chars[*i];
322 *i += 1;
323
324 if frac_digits > 0 {
326 if !matches!(unit, b'S' | b's') {
327 return Err(an_err!(
328 DtErrKind::InvalidSyntax,
329 "frac only supported for seconds"
330 ));
331 }
332 *has_fraction = true;
333 }
334
335 let signed_int = (int as i128 * sign as i128) as i64;
336
337 Ok(Some(ParsedComponent {
338 unit,
339 signed_int,
340 frac_digits,
341 frac_num,
342 }))
343 }
344
345 fn parse_duration_part(
351 chars: &[u8],
352 total_nanos: &mut i128,
353 is_date: bool,
354 sign: i64,
355 has_fraction: &mut bool,
356 ) -> Result<(), DtErr> {
357 let mut i = 0;
358 while let Some(comp) = Self::parse_next_component(chars, &mut i, sign, has_fraction)? {
359 let contrib_nanos = match (is_date, comp.unit) {
360 (true, b'Y' | b'y') => {
361 let total_secs = (comp.signed_int as i128)
362 .checked_mul(SEC_PER_YEAR)
363 .ok_or_else(|| an_err!(DtErrKind::OutOfRange, "year"))?;
364 total_secs * 1_000_000_000i128
365 }
366 (true, b'M' | b'm') => {
367 let total_secs = (comp.signed_int as i128)
368 .checked_mul(SEC_PER_MONTH)
369 .ok_or_else(|| an_err!(DtErrKind::OutOfRange, "month"))?;
370 total_secs * 1_000_000_000i128
371 }
372 (true, b'W' | b'w') => {
373 let total_secs = (comp.signed_int as i128)
374 .checked_mul(SEC_PER_WEEK as i128)
375 .ok_or_else(|| an_err!(DtErrKind::OutOfRange, "week"))?;
376 total_secs * 1_000_000_000i128
377 }
378 (true, b'D' | b'd') => {
379 let total_secs = (comp.signed_int as i128)
380 .checked_mul(SEC_PER_DAY)
381 .ok_or_else(|| an_err!(DtErrKind::OutOfRange, "day"))?;
382 total_secs * 1_000_000_000i128
383 }
384 (false, b'H' | b'h') => (comp.signed_int as i128) * 3_600_000_000_000i128,
385 (false, b'M' | b'm') => (comp.signed_int as i128) * 60_000_000_000i128,
386 (false, b'S' | b's') => {
387 let mut sec_nanos = (comp.signed_int as i128) * 1_000_000_000i128;
388 if comp.frac_digits > 0 {
389 let frac_ns = (comp.frac_num as i128 * sign as i128 * 1_000_000_000i128)
390 / 10i128.pow(comp.frac_digits as u32);
391 sec_nanos += frac_ns;
392 }
393 sec_nanos
394 }
395 _ => {
396 return Err(an_err!(DtErrKind::InvalidItem, "{}", comp.unit as char));
397 }
398 };
399
400 *total_nanos = total_nanos.saturating_add(contrib_nanos);
401 }
402 Ok(())
403 }
404
405 pub fn looks_like_iso(s: &str) -> bool {
409 let len = s.len();
410 if matches!(len, 0 | 1) {
411 return false;
412 }
413 let b = s.as_bytes();
414 let mut i = 0usize;
415 if matches!(b[0], b'+' | b'-') {
417 i += 1;
418 }
419 if !matches!(b[i], b'P' | b'p') {
421 return false;
422 }
423 i += 1;
424 let mut has_digit = false;
425 let mut has_designator = false;
426 while i < len {
427 match b[i] {
428 b'0'..=b'9' => has_digit = true,
429 b'.' | b',' => {} b'Y' | b'y' | b'M' | b'm' | b'W' | b'w' | b'D' | b'd' | b'T' | b't' | b'H'
431 | b'h' | b'S' | b's' => {
432 has_designator = true;
433 }
434 _ => return false, }
436
437 i += 1;
438 }
439 has_digit && has_designator
441 }
442}