1use core::fmt::Write;
2
3use crate::{
4 civil::Weekday,
5 error::{err, ErrorContext},
6 fmt::strtime::{BrokenDownTime, Extension, Flag, Meridiem},
7 tz::Offset,
8 util::{
9 escape, parse,
10 rangeint::{ri8, RFrom},
11 t::{self, C},
12 },
13 Error, Timestamp,
14};
15
16type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>;
20type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>;
21type ParsedOffsetSeconds = ri8<0, { t::SpanZoneOffsetSeconds::MAX }>;
22
23pub(super) struct Parser<'f, 'i, 't> {
24 pub(super) fmt: &'f [u8],
25 pub(super) inp: &'i [u8],
26 pub(super) tm: &'t mut BrokenDownTime,
27}
28
29impl<'f, 'i, 't> Parser<'f, 'i, 't> {
30 pub(super) fn parse(&mut self) -> Result<(), Error> {
31 while !self.fmt.is_empty() {
32 if self.f() != b'%' {
33 self.parse_literal()?;
34 continue;
35 }
36 if !self.bump_fmt() {
37 return Err(err!(
38 "invalid format string, expected byte after '%', \
39 but found end of format string",
40 ));
41 }
42 if self.inp.is_empty() && self.f() != b'.' {
45 return Err(err!(
46 "expected non-empty input for directive %{directive}, \
47 but found end of input",
48 directive = escape::Byte(self.f()),
49 ));
50 }
51 let ext = self.parse_extension()?;
53 match self.f() {
54 b'%' => self.parse_percent().context("%% failed")?,
55 b'A' => self.parse_weekday_full().context("%A failed")?,
56 b'a' => self.parse_weekday_abbrev().context("%a failed")?,
57 b'B' => self.parse_month_name_full().context("%B failed")?,
58 b'b' => self.parse_month_name_abbrev().context("%b failed")?,
59 b'C' => self.parse_century(ext).context("%C failed")?,
60 b'D' => self.parse_american_date().context("%D failed")?,
61 b'd' => self.parse_day(ext).context("%d failed")?,
62 b'e' => self.parse_day(ext).context("%e failed")?,
63 b'F' => self.parse_iso_date().context("%F failed")?,
64 b'f' => self.parse_fractional(ext).context("%f failed")?,
65 b'G' => self.parse_iso_week_year(ext).context("%G failed")?,
66 b'g' => self.parse_iso_week_year2(ext).context("%g failed")?,
67 b'H' => self.parse_hour24(ext).context("%H failed")?,
68 b'h' => self.parse_month_name_abbrev().context("%h failed")?,
69 b'I' => self.parse_hour12(ext).context("%I failed")?,
70 b'j' => self.parse_day_of_year(ext).context("%j failed")?,
71 b'k' => self.parse_hour24(ext).context("%k failed")?,
72 b'l' => self.parse_hour12(ext).context("%l failed")?,
73 b'M' => self.parse_minute(ext).context("%M failed")?,
74 b'm' => self.parse_month(ext).context("%m failed")?,
75 b'n' => self.parse_whitespace().context("%n failed")?,
76 b'P' => self.parse_ampm().context("%P failed")?,
77 b'p' => self.parse_ampm().context("%p failed")?,
78 b'Q' => self.parse_iana_nocolon().context("%Q failed")?,
79 b'R' => self.parse_clock_nosecs().context("%R failed")?,
80 b'S' => self.parse_second(ext).context("%S failed")?,
81 b's' => self.parse_timestamp(ext).context("%s failed")?,
82 b'T' => self.parse_clock_secs().context("%T failed")?,
83 b't' => self.parse_whitespace().context("%t failed")?,
84 b'U' => self.parse_week_sun(ext).context("%U failed")?,
85 b'u' => self.parse_weekday_mon(ext).context("%u failed")?,
86 b'V' => self.parse_week_iso(ext).context("%V failed")?,
87 b'W' => self.parse_week_mon(ext).context("%W failed")?,
88 b'w' => self.parse_weekday_sun(ext).context("%w failed")?,
89 b'Y' => self.parse_year(ext).context("%Y failed")?,
90 b'y' => self.parse_year2(ext).context("%y failed")?,
91 b'z' => self.parse_offset_nocolon().context("%z failed")?,
92 b':' => {
93 if !self.bump_fmt() {
94 return Err(err!(
95 "invalid format string, expected directive \
96 after '%:'",
97 ));
98 }
99 match self.f() {
100 b'Q' => {
101 self.parse_iana_colon().context("%:Q failed")?
102 }
103 b'z' => {
104 self.parse_offset_colon().context("%:z failed")?
105 }
106 unk => {
107 return Err(err!(
108 "found unrecognized directive %{unk} \
109 following %:",
110 unk = escape::Byte(unk),
111 ));
112 }
113 }
114 }
115 b'Z' => {
116 return Err(err!("cannot parse time zone abbreviations"));
117 }
118 b'.' => {
119 if !self.bump_fmt() {
120 return Err(err!(
121 "invalid format string, expected directive \
122 after '%.'",
123 ));
124 }
125 let (width, fmt) = Extension::parse_width(self.fmt)?;
128 let ext = Extension { width, ..ext };
129 self.fmt = fmt;
130 match self.f() {
131 b'f' => self
132 .parse_dot_fractional(ext)
133 .context("%.f failed")?,
134 unk => {
135 return Err(err!(
136 "found unrecognized directive %{unk} \
137 following %.",
138 unk = escape::Byte(unk),
139 ));
140 }
141 }
142 }
143 unk => {
144 return Err(err!(
145 "found unrecognized directive %{unk}",
146 unk = escape::Byte(unk),
147 ));
148 }
149 }
150 }
151 Ok(())
152 }
153
154 fn f(&self) -> u8 {
160 self.fmt[0]
161 }
162
163 fn i(&self) -> u8 {
169 self.inp[0]
170 }
171
172 fn bump_fmt(&mut self) -> bool {
177 self.fmt = &self.fmt[1..];
178 !self.fmt.is_empty()
179 }
180
181 fn bump_input(&mut self) -> bool {
186 self.inp = &self.inp[1..];
187 !self.inp.is_empty()
188 }
189
190 fn parse_extension(&mut self) -> Result<Extension, Error> {
194 let (flag, fmt) = Extension::parse_flag(self.fmt)?;
195 let (width, fmt) = Extension::parse_width(fmt)?;
196 self.fmt = fmt;
197 Ok(Extension { flag, width })
198 }
199
200 fn parse_literal(&mut self) -> Result<(), Error> {
212 if self.f().is_ascii_whitespace() {
213 if !self.inp.is_empty() {
214 while self.i().is_ascii_whitespace() && self.bump_input() {}
215 }
216 } else if self.inp.is_empty() {
217 return Err(err!(
218 "expected to match literal byte {byte:?} from \
219 format string, but found end of input",
220 byte = escape::Byte(self.fmt[0]),
221 ));
222 } else if self.f() != self.i() {
223 return Err(err!(
224 "expected to match literal byte {expect:?} from \
225 format string, but found byte {found:?} in input",
226 expect = escape::Byte(self.f()),
227 found = escape::Byte(self.i()),
228 ));
229 } else {
230 self.bump_input();
231 }
232 self.bump_fmt();
233 Ok(())
234 }
235
236 fn parse_whitespace(&mut self) -> Result<(), Error> {
240 if !self.inp.is_empty() {
241 while self.i().is_ascii_whitespace() && self.bump_input() {}
242 }
243 self.bump_fmt();
244 Ok(())
245 }
246
247 fn parse_percent(&mut self) -> Result<(), Error> {
249 if self.i() != b'%' {
250 return Err(err!(
251 "expected '%' due to '%%' in format string, \
252 but found {byte:?} in input",
253 byte = escape::Byte(self.inp[0]),
254 ));
255 }
256 self.bump_fmt();
257 self.bump_input();
258 Ok(())
259 }
260
261 fn parse_american_date(&mut self) -> Result<(), Error> {
263 let mut p = Parser { fmt: b"%m/%d/%y", inp: self.inp, tm: self.tm };
264 p.parse()?;
265 self.inp = p.inp;
266 self.bump_fmt();
267 Ok(())
268 }
269
270 fn parse_ampm(&mut self) -> Result<(), Error> {
276 let (index, inp) = parse_ampm(self.inp)?;
277 self.inp = inp;
278
279 self.tm.meridiem = Some(match index {
280 0 => Meridiem::AM,
281 1 => Meridiem::PM,
282 index => unreachable!("unknown AM/PM index {index}"),
284 });
285 self.bump_fmt();
286 Ok(())
287 }
288
289 fn parse_clock_secs(&mut self) -> Result<(), Error> {
291 let mut p = Parser { fmt: b"%H:%M:%S", inp: self.inp, tm: self.tm };
292 p.parse()?;
293 self.inp = p.inp;
294 self.bump_fmt();
295 Ok(())
296 }
297
298 fn parse_clock_nosecs(&mut self) -> Result<(), Error> {
300 let mut p = Parser { fmt: b"%H:%M", inp: self.inp, tm: self.tm };
301 p.parse()?;
302 self.inp = p.inp;
303 self.bump_fmt();
304 Ok(())
305 }
306
307 fn parse_day(&mut self, ext: Extension) -> Result<(), Error> {
311 let (day, inp) = ext
312 .parse_number(2, Flag::PadZero, self.inp)
313 .context("failed to parse day")?;
314 self.inp = inp;
315
316 let day =
317 t::Day::try_new("day", day).context("day number is invalid")?;
318 self.tm.day = Some(day);
319 self.bump_fmt();
320 Ok(())
321 }
322
323 fn parse_day_of_year(&mut self, ext: Extension) -> Result<(), Error> {
327 let (day, inp) = ext
328 .parse_number(3, Flag::PadZero, self.inp)
329 .context("failed to parse day of year")?;
330 self.inp = inp;
331
332 let day = t::DayOfYear::try_new("day-of-year", day)
333 .context("day of year number is invalid")?;
334 self.tm.day_of_year = Some(day);
335 self.bump_fmt();
336 Ok(())
337 }
338
339 fn parse_hour24(&mut self, ext: Extension) -> Result<(), Error> {
341 let (hour, inp) = ext
342 .parse_number(2, Flag::PadZero, self.inp)
343 .context("failed to parse hour")?;
344 self.inp = inp;
345
346 let hour = t::Hour::try_new("hour", hour)
347 .context("hour number is invalid")?;
348 self.tm.hour = Some(hour);
349 self.bump_fmt();
350 Ok(())
351 }
352
353 fn parse_hour12(&mut self, ext: Extension) -> Result<(), Error> {
355 type Hour12 = ri8<1, 12>;
356
357 let (hour, inp) = ext
358 .parse_number(2, Flag::PadZero, self.inp)
359 .context("failed to parse hour")?;
360 self.inp = inp;
361
362 let hour =
363 Hour12::try_new("hour", hour).context("hour number is invalid")?;
364 self.tm.hour = Some(t::Hour::rfrom(hour));
365 self.bump_fmt();
366 Ok(())
367 }
368
369 fn parse_iso_date(&mut self) -> Result<(), Error> {
371 let mut p = Parser { fmt: b"%Y-%m-%d", inp: self.inp, tm: self.tm };
372 p.parse()?;
373 self.inp = p.inp;
374 self.bump_fmt();
375 Ok(())
376 }
377
378 fn parse_minute(&mut self, ext: Extension) -> Result<(), Error> {
380 let (minute, inp) = ext
381 .parse_number(2, Flag::PadZero, self.inp)
382 .context("failed to parse minute")?;
383 self.inp = inp;
384
385 let minute = t::Minute::try_new("minute", minute)
386 .context("minute number is invalid")?;
387 self.tm.minute = Some(minute);
388 self.bump_fmt();
389 Ok(())
390 }
391
392 fn parse_iana_nocolon(&mut self) -> Result<(), Error> {
395 #[cfg(not(feature = "alloc"))]
396 {
397 Err(err!(
398 "cannot parse `%Q` without Jiff's `alloc` feature enabled"
399 ))
400 }
401 #[cfg(feature = "alloc")]
402 {
403 use alloc::string::ToString;
404
405 if !self.inp.is_empty() && matches!(self.inp[0], b'+' | b'-') {
406 return self.parse_offset_nocolon();
407 }
408 let (iana, inp) = parse_iana(self.inp)?;
409 self.inp = inp;
410 self.tm.iana = Some(iana.to_string());
411 self.bump_fmt();
412 Ok(())
413 }
414 }
415
416 fn parse_iana_colon(&mut self) -> Result<(), Error> {
419 #[cfg(not(feature = "alloc"))]
420 {
421 Err(err!(
422 "cannot parse `%:Q` without Jiff's `alloc` feature enabled"
423 ))
424 }
425 #[cfg(feature = "alloc")]
426 {
427 use alloc::string::ToString;
428
429 if !self.inp.is_empty() && matches!(self.inp[0], b'+' | b'-') {
430 return self.parse_offset_colon();
431 }
432 let (iana, inp) = parse_iana(self.inp)?;
433 self.inp = inp;
434 self.tm.iana = Some(iana.to_string());
435 self.bump_fmt();
436 Ok(())
437 }
438 }
439
440 fn parse_offset_nocolon(&mut self) -> Result<(), Error> {
442 let (sign, inp) = parse_required_sign(self.inp)
443 .context("sign is required for time zone offset")?;
444 let (hhmm, inp) = parse::split(inp, 4).ok_or_else(|| {
445 err!(
446 "expected at least 4 digits for time zone offset \
447 after sign, but found only {len} bytes remaining",
448 len = inp.len(),
449 )
450 })?;
451
452 let hh = parse::i64(&hhmm[0..2]).with_context(|| {
453 err!(
454 "failed to parse hours from time zone offset {hhmm}",
455 hhmm = escape::Bytes(hhmm)
456 )
457 })?;
458 let hh = ParsedOffsetHours::try_new("zone-offset-hours", hh)
459 .context("time zone offset hours are not valid")?;
460 let hh = t::SpanZoneOffset::rfrom(hh);
461
462 let mm = parse::i64(&hhmm[2..4]).with_context(|| {
463 err!(
464 "failed to parse minutes from time zone offset {hhmm}",
465 hhmm = escape::Bytes(hhmm)
466 )
467 })?;
468 let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes", mm)
469 .context("time zone offset minutes are not valid")?;
470 let mm = t::SpanZoneOffset::rfrom(mm);
471
472 let (ss, inp) = if inp.len() < 2
473 || !inp[..2].iter().all(u8::is_ascii_digit)
474 {
475 (t::SpanZoneOffset::N::<0>(), inp)
476 } else {
477 let (ss, inp) = parse::split(inp, 2).unwrap();
478 let ss = parse::i64(ss).with_context(|| {
479 err!(
480 "failed to parse seconds from time zone offset {ss}",
481 ss = escape::Bytes(ss)
482 )
483 })?;
484 let ss = ParsedOffsetSeconds::try_new("zone-offset-seconds", ss)
485 .context("time zone offset seconds are not valid")?;
486 if inp.starts_with(b".") {
487 return Err(err!(
492 "parsing fractional seconds in time zone offset \
493 is not supported",
494 ));
495 }
496 (t::SpanZoneOffset::rfrom(ss), inp)
497 };
498
499 let seconds = hh * C(3_600) + mm * C(60) + ss;
500 let offset = Offset::from_seconds_ranged(seconds * sign);
501 self.tm.offset = Some(offset);
502 self.inp = inp;
503 self.bump_fmt();
504
505 Ok(())
506 }
507
508 fn parse_offset_colon(&mut self) -> Result<(), Error> {
510 let (sign, inp) = parse_required_sign(self.inp)
511 .context("sign is required for time zone offset")?;
512 let (hhmm, inp) = parse::split(inp, 5).ok_or_else(|| {
513 err!(
514 "expected at least HH:MM digits for time zone offset \
515 after sign, but found only {len} bytes remaining",
516 len = inp.len(),
517 )
518 })?;
519 if hhmm[2] != b':' {
520 return Err(err!(
521 "expected colon after between HH and MM in time zone \
522 offset, but found {found:?} instead",
523 found = escape::Byte(hhmm[2]),
524 ));
525 }
526
527 let hh = parse::i64(&hhmm[0..2]).with_context(|| {
528 err!(
529 "failed to parse hours from time zone offset {hhmm}",
530 hhmm = escape::Bytes(hhmm)
531 )
532 })?;
533 let hh = ParsedOffsetHours::try_new("zone-offset-hours", hh)
534 .context("time zone offset hours are not valid")?;
535 let hh = t::SpanZoneOffset::rfrom(hh);
536
537 let mm = parse::i64(&hhmm[3..5]).with_context(|| {
538 err!(
539 "failed to parse minutes from time zone offset {hhmm}",
540 hhmm = escape::Bytes(hhmm)
541 )
542 })?;
543 let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes", mm)
544 .context("time zone offset minutes are not valid")?;
545 let mm = t::SpanZoneOffset::rfrom(mm);
546
547 let (ss, inp) = if inp.len() < 3
548 || inp[0] != b':'
549 || !inp[1..3].iter().all(u8::is_ascii_digit)
550 {
551 (t::SpanZoneOffset::N::<0>(), inp)
552 } else {
553 let (ss, inp) = parse::split(&inp[1..], 2).unwrap();
554 let ss = parse::i64(ss).with_context(|| {
555 err!(
556 "failed to parse seconds from time zone offset {ss}",
557 ss = escape::Bytes(ss)
558 )
559 })?;
560 let ss = ParsedOffsetSeconds::try_new("zone-offset-seconds", ss)
561 .context("time zone offset seconds are not valid")?;
562 if inp.starts_with(b".") {
563 return Err(err!(
568 "parsing fractional seconds in time zone offset \
569 is not supported",
570 ));
571 }
572 (t::SpanZoneOffset::rfrom(ss), inp)
573 };
574
575 let seconds = hh * C(3_600) + mm * C(60) + ss;
576 let offset = Offset::from_seconds_ranged(seconds * sign);
577 self.tm.offset = Some(offset);
578 self.inp = inp;
579 self.bump_fmt();
580
581 Ok(())
582 }
583
584 fn parse_second(&mut self, ext: Extension) -> Result<(), Error> {
586 let (mut second, inp) = ext
587 .parse_number(2, Flag::PadZero, self.inp)
588 .context("failed to parse second")?;
589 self.inp = inp;
590
591 if second == 60 {
595 second = 59;
596 }
597 let second = t::Second::try_new("second", second)
598 .context("second number is invalid")?;
599 self.tm.second = Some(second);
600 self.bump_fmt();
601 Ok(())
602 }
603
604 fn parse_timestamp(&mut self, ext: Extension) -> Result<(), Error> {
606 let (sign, inp) = parse_optional_sign(self.inp);
607 let (timestamp, inp) = ext
608 .parse_number(19, Flag::PadSpace, inp)
610 .context("failed to parse Unix timestamp (in seconds)")?;
611 let timestamp = timestamp.checked_mul(sign).ok_or_else(|| {
615 err!(
616 "parsed Unix timestamp `{timestamp}` with a \
617 leading `-` sign, which causes overflow",
618 )
619 })?;
620 let timestamp =
621 Timestamp::from_second(timestamp).with_context(|| {
622 err!(
623 "parsed Unix timestamp `{timestamp}`, \
624 but out of range of valid Jiff `Timestamp`",
625 )
626 })?;
627 self.inp = inp;
628
629 let dt = Offset::UTC.to_datetime(timestamp);
633 let (d, t) = (dt.date(), dt.time());
634 self.tm.offset = Some(Offset::UTC);
635 self.tm.year = Some(d.year_ranged());
636 self.tm.month = Some(d.month_ranged());
637 self.tm.day = Some(d.day_ranged());
638 self.tm.hour = Some(t.hour_ranged());
639 self.tm.minute = Some(t.minute_ranged());
640 self.tm.second = Some(t.second_ranged());
641 self.tm.subsec = Some(t.subsec_nanosecond_ranged());
642 self.tm.meridiem = Some(Meridiem::from(t));
643
644 self.bump_fmt();
645 Ok(())
646 }
647
648 fn parse_fractional(&mut self, _ext: Extension) -> Result<(), Error> {
656 let mkdigits = parse::slicer(self.inp);
657 while mkdigits(self.inp).len() < 9
658 && self.inp.first().map_or(false, u8::is_ascii_digit)
659 {
660 self.inp = &self.inp[1..];
661 }
662 let digits = mkdigits(self.inp);
663 if digits.is_empty() {
664 return Err(err!(
665 "expected at least one fractional decimal digit, \
666 but did not find any",
667 ));
668 }
669 let nanoseconds = parse::fraction(digits, 9).map_err(|err| {
673 err!(
674 "failed to parse {digits:?} as fractional second component \
675 (up to 9 digits, nanosecond precision): {err}",
676 digits = escape::Bytes(digits),
677 )
678 })?;
679 let nanoseconds =
684 t::SubsecNanosecond::try_new("nanoseconds", nanoseconds).map_err(
685 |err| err!("fractional nanoseconds are not valid: {err}"),
686 )?;
687 self.tm.subsec = Some(nanoseconds);
688 self.bump_fmt();
689 Ok(())
690 }
691
692 fn parse_dot_fractional(&mut self, ext: Extension) -> Result<(), Error> {
696 if !self.inp.starts_with(b".") {
697 self.bump_fmt();
698 return Ok(());
699 }
700 self.inp = &self.inp[1..];
701 self.parse_fractional(ext)
702 }
703
704 fn parse_month(&mut self, ext: Extension) -> Result<(), Error> {
706 let (month, inp) = ext
707 .parse_number(2, Flag::PadZero, self.inp)
708 .context("failed to parse month")?;
709 self.inp = inp;
710
711 let month = t::Month::try_new("month", month)
712 .context("month number is invalid")?;
713 self.tm.month = Some(month);
714 self.bump_fmt();
715 Ok(())
716 }
717
718 fn parse_month_name_abbrev(&mut self) -> Result<(), Error> {
720 let (index, inp) = parse_month_name_abbrev(self.inp)?;
721 self.inp = inp;
722
723 let index = i8::try_from(index).unwrap();
725 self.tm.month = Some(t::Month::new(index + 1).unwrap());
726 self.bump_fmt();
727 Ok(())
728 }
729
730 fn parse_month_name_full(&mut self) -> Result<(), Error> {
732 static CHOICES: &'static [&'static [u8]] = &[
733 b"January",
734 b"February",
735 b"March",
736 b"April",
737 b"May",
738 b"June",
739 b"July",
740 b"August",
741 b"September",
742 b"October",
743 b"November",
744 b"December",
745 ];
746
747 let (index, inp) = parse_choice(self.inp, CHOICES)
748 .context("unrecognized month name")?;
749 self.inp = inp;
750
751 let index = i8::try_from(index).unwrap();
753 self.tm.month = Some(t::Month::new(index + 1).unwrap());
754 self.bump_fmt();
755 Ok(())
756 }
757
758 fn parse_weekday_abbrev(&mut self) -> Result<(), Error> {
760 let (index, inp) = parse_weekday_abbrev(self.inp)?;
761 self.inp = inp;
762
763 let index = i8::try_from(index).unwrap();
765 self.tm.weekday =
766 Some(Weekday::from_sunday_zero_offset(index).unwrap());
767 self.bump_fmt();
768 Ok(())
769 }
770
771 fn parse_weekday_full(&mut self) -> Result<(), Error> {
773 static CHOICES: &'static [&'static [u8]] = &[
774 b"Sunday",
775 b"Monday",
776 b"Tueday",
777 b"Wednesday",
778 b"Thursday",
779 b"Friday",
780 b"Saturday",
781 ];
782
783 let (index, inp) = parse_choice(self.inp, CHOICES)
784 .context("unrecognized weekday abbreviation")?;
785 self.inp = inp;
786
787 let index = i8::try_from(index).unwrap();
789 self.tm.weekday =
790 Some(Weekday::from_sunday_zero_offset(index).unwrap());
791 self.bump_fmt();
792 Ok(())
793 }
794
795 fn parse_weekday_mon(&mut self, ext: Extension) -> Result<(), Error> {
798 let (weekday, inp) = ext
799 .parse_number(1, Flag::NoPad, self.inp)
800 .context("failed to parse weekday number")?;
801 self.inp = inp;
802
803 let weekday = i8::try_from(weekday).map_err(|_| {
804 err!("parsed weekday number `{weekday}` is invalid")
805 })?;
806 let weekday = Weekday::from_monday_one_offset(weekday)
807 .context("weekday number is invalid")?;
808 self.tm.weekday = Some(weekday);
809 self.bump_fmt();
810 Ok(())
811 }
812
813 fn parse_weekday_sun(&mut self, ext: Extension) -> Result<(), Error> {
815 let (weekday, inp) = ext
816 .parse_number(1, Flag::NoPad, self.inp)
817 .context("failed to parse weekday number")?;
818 self.inp = inp;
819
820 let weekday = i8::try_from(weekday).map_err(|_| {
821 err!("parsed weekday number `{weekday}` is invalid")
822 })?;
823 let weekday = Weekday::from_sunday_zero_offset(weekday)
824 .context("weekday number is invalid")?;
825 self.tm.weekday = Some(weekday);
826 self.bump_fmt();
827 Ok(())
828 }
829
830 fn parse_week_sun(&mut self, ext: Extension) -> Result<(), Error> {
833 let (week, inp) = ext
834 .parse_number(2, Flag::PadZero, self.inp)
835 .context("failed to parse Sunday-based week number")?;
836 self.inp = inp;
837
838 let week = t::WeekNum::try_new("week", week)
839 .context("Sunday-based week number is invalid")?;
840 self.tm.week_sun = Some(week);
841 self.bump_fmt();
842 Ok(())
843 }
844
845 fn parse_week_iso(&mut self, ext: Extension) -> Result<(), Error> {
847 let (week, inp) = ext
848 .parse_number(2, Flag::PadZero, self.inp)
849 .context("failed to parse ISO 8601 week number")?;
850 self.inp = inp;
851
852 let week = t::ISOWeek::try_new("week", week)
853 .context("ISO 8601 week number is invalid")?;
854 self.tm.iso_week = Some(week);
855 self.bump_fmt();
856 Ok(())
857 }
858
859 fn parse_week_mon(&mut self, ext: Extension) -> Result<(), Error> {
862 let (week, inp) = ext
863 .parse_number(2, Flag::PadZero, self.inp)
864 .context("failed to parse Monday-based week number")?;
865 self.inp = inp;
866
867 let week = t::WeekNum::try_new("week", week)
868 .context("Monday-based week number is invalid")?;
869 self.tm.week_mon = Some(week);
870 self.bump_fmt();
871 Ok(())
872 }
873
874 fn parse_year(&mut self, ext: Extension) -> Result<(), Error> {
876 let (sign, inp) = parse_optional_sign(self.inp);
877 let (year, inp) = ext
878 .parse_number(4, Flag::PadZero, inp)
879 .context("failed to parse year")?;
880 self.inp = inp;
881
882 let year = sign.checked_mul(year).unwrap();
885 let year = t::Year::try_new("year", year)
886 .context("year number is invalid")?;
887 self.tm.year = Some(year);
888 self.bump_fmt();
889 Ok(())
890 }
891
892 fn parse_year2(&mut self, ext: Extension) -> Result<(), Error> {
896 type Year2Digit = ri8<0, 99>;
897
898 let (year, inp) = ext
899 .parse_number(2, Flag::PadZero, self.inp)
900 .context("failed to parse 2-digit year")?;
901 self.inp = inp;
902
903 let year = Year2Digit::try_new("year (2 digits)", year)
904 .context("year number is invalid")?;
905 let mut year = t::Year::rfrom(year);
906 if year <= 68 {
907 year += C(2000);
908 } else {
909 year += C(1900);
910 }
911 self.tm.year = Some(year);
912 self.bump_fmt();
913 Ok(())
914 }
915
916 fn parse_century(&mut self, ext: Extension) -> Result<(), Error> {
919 let (sign, inp) = parse_optional_sign(self.inp);
920 let (century, inp) = ext
921 .parse_number(2, Flag::NoPad, inp)
922 .context("failed to parse century")?;
923 self.inp = inp;
924
925 let century = sign.checked_mul(century).unwrap();
928 let year = century.checked_mul(100).unwrap();
931 let year = t::Year::try_new("year", year)
933 .context("year number (from century) is invalid")?;
934 self.tm.year = Some(year);
935 self.bump_fmt();
936 Ok(())
937 }
938
939 fn parse_iso_week_year(&mut self, ext: Extension) -> Result<(), Error> {
941 let (sign, inp) = parse_optional_sign(self.inp);
942 let (year, inp) = ext
943 .parse_number(4, Flag::PadZero, inp)
944 .context("failed to parse ISO 8601 week-based year")?;
945 self.inp = inp;
946
947 let year = sign.checked_mul(year).unwrap();
950 let year = t::ISOYear::try_new("year", year)
951 .context("ISO 8601 week-based year number is invalid")?;
952 self.tm.iso_week_year = Some(year);
953 self.bump_fmt();
954 Ok(())
955 }
956
957 fn parse_iso_week_year2(&mut self, ext: Extension) -> Result<(), Error> {
961 type Year2Digit = ri8<0, 99>;
962
963 let (year, inp) = ext
964 .parse_number(2, Flag::PadZero, self.inp)
965 .context("failed to parse 2-digit ISO 8601 week-based year")?;
966 self.inp = inp;
967
968 let year = Year2Digit::try_new("year (2 digits)", year)
969 .context("ISO 8601 week-based year number is invalid")?;
970 let mut year = t::ISOYear::rfrom(year);
971 if year <= 68 {
972 year += C(2000);
973 } else {
974 year += C(1900);
975 }
976 self.tm.iso_week_year = Some(year);
977 self.bump_fmt();
978 Ok(())
979 }
980}
981
982impl Extension {
983 #[inline(always)]
999 fn parse_number<'i>(
1000 self,
1001 default_pad_width: usize,
1002 default_flag: Flag,
1003 mut inp: &'i [u8],
1004 ) -> Result<(i64, &'i [u8]), Error> {
1005 let flag = self.flag.unwrap_or(default_flag);
1006 let zero_pad_width = match flag {
1007 Flag::PadSpace | Flag::NoPad => 0,
1008 _ => self.width.map(usize::from).unwrap_or(default_pad_width),
1009 };
1010 let max_digits = default_pad_width.max(zero_pad_width);
1011
1012 while inp.get(0).map_or(false, |b| b.is_ascii_whitespace()) {
1014 inp = &inp[1..];
1015 }
1016 let mut digits = 0;
1017 while digits < inp.len()
1018 && digits < zero_pad_width
1019 && inp[digits] == b'0'
1020 {
1021 digits += 1;
1022 }
1023 let mut n: i64 = 0;
1024 while digits < inp.len()
1025 && digits < max_digits
1026 && inp[digits].is_ascii_digit()
1027 {
1028 let byte = inp[digits];
1029 digits += 1;
1030 let digit = i64::from(byte - b'0');
1034 n = n
1035 .checked_mul(10)
1036 .and_then(|n| n.checked_add(digit))
1037 .ok_or_else(|| {
1038 err!(
1039 "number '{}' too big to parse into 64-bit integer",
1040 escape::Bytes(&inp[..digits]),
1041 )
1042 })?;
1043 }
1044 if digits == 0 {
1045 return Err(err!("invalid number, no digits found"));
1046 }
1047 Ok((n, &inp[digits..]))
1048 }
1049}
1050
1051#[inline(always)]
1056fn parse_optional_sign<'i>(input: &'i [u8]) -> (i64, &'i [u8]) {
1057 if input.is_empty() {
1058 (1, input)
1059 } else if input[0] == b'-' {
1060 (-1, &input[1..])
1061 } else if input[0] == b'+' {
1062 (1, &input[1..])
1063 } else {
1064 (1, input)
1065 }
1066}
1067
1068#[inline(always)]
1073fn parse_required_sign<'i>(
1074 input: &'i [u8],
1075) -> Result<(t::Sign, &'i [u8]), Error> {
1076 if input.is_empty() {
1077 Err(err!("expected +/- sign, but found end of input"))
1078 } else if input[0] == b'-' {
1079 Ok((t::Sign::N::<-1>(), &input[1..]))
1080 } else if input[0] == b'+' {
1081 Ok((t::Sign::N::<1>(), &input[1..]))
1082 } else {
1083 Err(err!(
1084 "expected +/- sign, but found {found:?} instead",
1085 found = escape::Byte(input[0])
1086 ))
1087 }
1088}
1089
1090fn parse_choice<'i>(
1097 input: &'i [u8],
1098 choices: &[&'static [u8]],
1099) -> Result<(usize, &'i [u8]), Error> {
1100 for (i, choice) in choices.into_iter().enumerate() {
1101 if input.len() < choice.len() {
1102 continue;
1103 }
1104 let (candidate, input) = input.split_at(choice.len());
1105 if candidate.eq_ignore_ascii_case(choice) {
1106 return Ok((i, input));
1107 }
1108 }
1109 #[cfg(feature = "alloc")]
1110 {
1111 let mut err = alloc::format!(
1112 "failed to find expected choice at beginning of {input:?}, \
1113 available choices are: ",
1114 input = escape::Bytes(input),
1115 );
1116 for (i, choice) in choices.iter().enumerate() {
1117 if i > 0 {
1118 write!(err, ", ").unwrap();
1119 }
1120 write!(err, "{}", escape::Bytes(choice)).unwrap();
1121 }
1122 Err(Error::adhoc(err))
1123 }
1124 #[cfg(not(feature = "alloc"))]
1125 {
1126 Err(err!(
1127 "failed to find expected value from a set of allowed choices"
1128 ))
1129 }
1130}
1131
1132#[inline(always)]
1137fn parse_ampm<'i>(input: &'i [u8]) -> Result<(usize, &'i [u8]), Error> {
1138 if input.len() < 2 {
1139 return Err(err!(
1140 "expected to find AM or PM, \
1141 but the remaining input, {input:?}, is too short \
1142 to contain one",
1143 input = escape::Bytes(input),
1144 ));
1145 }
1146 let (x, input) = input.split_at(2);
1147 let candidate = &[x[0].to_ascii_lowercase(), x[1].to_ascii_lowercase()];
1148 let index = match candidate {
1149 b"am" => 0,
1150 b"pm" => 1,
1151 _ => {
1152 return Err(err!(
1153 "expected to find AM or PM, but found \
1154 {candidate:?} instead",
1155 candidate = escape::Bytes(x),
1156 ))
1157 }
1158 };
1159 Ok((index, input))
1160}
1161
1162#[inline(always)]
1167fn parse_weekday_abbrev<'i>(
1168 input: &'i [u8],
1169) -> Result<(usize, &'i [u8]), Error> {
1170 if input.len() < 3 {
1171 return Err(err!(
1172 "expected to find a weekday abbreviation, \
1173 but the remaining input, {input:?}, is too short \
1174 to contain one",
1175 input = escape::Bytes(input),
1176 ));
1177 }
1178 let (x, input) = input.split_at(3);
1179 let candidate = &[
1180 x[0].to_ascii_lowercase(),
1181 x[1].to_ascii_lowercase(),
1182 x[2].to_ascii_lowercase(),
1183 ];
1184 let index = match candidate {
1185 b"sun" => 0,
1186 b"mon" => 1,
1187 b"tue" => 2,
1188 b"wed" => 3,
1189 b"thu" => 4,
1190 b"fri" => 5,
1191 b"sat" => 6,
1192 _ => {
1193 return Err(err!(
1194 "expected to find weekday abbreviation, but found \
1195 {candidate:?} instead",
1196 candidate = escape::Bytes(x),
1197 ))
1198 }
1199 };
1200 Ok((index, input))
1201}
1202
1203#[inline(always)]
1208fn parse_month_name_abbrev<'i>(
1209 input: &'i [u8],
1210) -> Result<(usize, &'i [u8]), Error> {
1211 if input.len() < 3 {
1212 return Err(err!(
1213 "expected to find a month name abbreviation, \
1214 but the remaining input, {input:?}, is too short \
1215 to contain one",
1216 input = escape::Bytes(input),
1217 ));
1218 }
1219 let (x, input) = input.split_at(3);
1220 let candidate = &[
1221 x[0].to_ascii_lowercase(),
1222 x[1].to_ascii_lowercase(),
1223 x[2].to_ascii_lowercase(),
1224 ];
1225 let index = match candidate {
1226 b"jan" => 0,
1227 b"feb" => 1,
1228 b"mar" => 2,
1229 b"apr" => 3,
1230 b"may" => 4,
1231 b"jun" => 5,
1232 b"jul" => 6,
1233 b"aug" => 7,
1234 b"sep" => 8,
1235 b"oct" => 9,
1236 b"nov" => 10,
1237 b"dec" => 11,
1238 _ => {
1239 return Err(err!(
1240 "expected to find month name abbreviation, but found \
1241 {candidate:?} instead",
1242 candidate = escape::Bytes(x),
1243 ))
1244 }
1245 };
1246 Ok((index, input))
1247}
1248
1249#[inline(always)]
1250fn parse_iana<'i>(input: &'i [u8]) -> Result<(&'i str, &'i [u8]), Error> {
1251 let mkiana = parse::slicer(input);
1252 let (_, mut input) = parse_iana_component(input)?;
1253 while input.starts_with(b"/") {
1254 input = &input[1..];
1255 let (_, unconsumed) = parse_iana_component(input)?;
1256 input = unconsumed;
1257 }
1258 let iana = core::str::from_utf8(mkiana(input)).expect("ASCII");
1263 Ok((iana, input))
1264}
1265
1266#[inline(always)]
1270fn parse_iana_component<'i>(
1271 mut input: &'i [u8],
1272) -> Result<(&'i [u8], &'i [u8]), Error> {
1273 let mkname = parse::slicer(input);
1274 if input.is_empty() {
1275 return Err(err!(
1276 "expected the start of an IANA time zone identifier \
1277 name or component, but found end of input instead",
1278 ));
1279 }
1280 if !matches!(input[0], b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') {
1281 return Err(err!(
1282 "expected the start of an IANA time zone identifier \
1283 name or component, but found {:?} instead",
1284 escape::Byte(input[0]),
1285 ));
1286 }
1287 input = &input[1..];
1288
1289 let is_iana_char = |byte| {
1290 matches!(
1291 byte,
1292 b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
1293 )
1294 };
1295 while !input.is_empty() && is_iana_char(input[0]) {
1296 input = &input[1..];
1297 }
1298 Ok((mkname(input), input))
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303 use alloc::string::ToString;
1304
1305 use super::*;
1306
1307 #[test]
1308 fn ok_parse_zoned() {
1309 if crate::tz::db().is_definitively_empty() {
1310 return;
1311 }
1312
1313 let p = |fmt: &str, input: &str| {
1314 BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1315 .unwrap()
1316 .to_zoned()
1317 .unwrap()
1318 };
1319
1320 insta::assert_debug_snapshot!(
1321 p("%h %d, %Y %H:%M:%S %z", "Apr 1, 2022 20:46:15 -0400"),
1322 @"2022-04-01T20:46:15-04:00[-04:00]",
1323 );
1324 insta::assert_debug_snapshot!(
1325 p("%h %d, %Y %H:%M:%S %Q", "Apr 1, 2022 20:46:15 -0400"),
1326 @"2022-04-01T20:46:15-04:00[-04:00]",
1327 );
1328 insta::assert_debug_snapshot!(
1329 p("%h %d, %Y %H:%M:%S [%Q]", "Apr 1, 2022 20:46:15 [America/New_York]"),
1330 @"2022-04-01T20:46:15-04:00[America/New_York]",
1331 );
1332 insta::assert_debug_snapshot!(
1333 p("%h %d, %Y %H:%M:%S %Q", "Apr 1, 2022 20:46:15 America/New_York"),
1334 @"2022-04-01T20:46:15-04:00[America/New_York]",
1335 );
1336 insta::assert_debug_snapshot!(
1337 p("%h %d, %Y %H:%M:%S %:z %:Q", "Apr 1, 2022 20:46:15 -08:00 -04:00"),
1338 @"2022-04-01T20:46:15-04:00[-04:00]",
1339 );
1340 }
1341
1342 #[test]
1343 fn ok_parse_timestamp() {
1344 let p = |fmt: &str, input: &str| {
1345 BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1346 .unwrap()
1347 .to_timestamp()
1348 .unwrap()
1349 };
1350
1351 insta::assert_debug_snapshot!(
1352 p("%h %d, %Y %H:%M:%S %z", "Apr 1, 2022 20:46:15 -0400"),
1353 @"2022-04-02T00:46:15Z",
1354 );
1355 insta::assert_debug_snapshot!(
1356 p("%h %d, %Y %H:%M:%S %z", "Apr 1, 2022 20:46:15 +0400"),
1357 @"2022-04-01T16:46:15Z",
1358 );
1359 insta::assert_debug_snapshot!(
1360 p("%h %d, %Y %H:%M:%S %z", "Apr 1, 2022 20:46:15 -040059"),
1361 @"2022-04-02T00:47:14Z",
1362 );
1363
1364 insta::assert_debug_snapshot!(
1365 p("%h %d, %Y %H:%M:%S %:z", "Apr 1, 2022 20:46:15 -04:00"),
1366 @"2022-04-02T00:46:15Z",
1367 );
1368 insta::assert_debug_snapshot!(
1369 p("%h %d, %Y %H:%M:%S %:z", "Apr 1, 2022 20:46:15 +04:00"),
1370 @"2022-04-01T16:46:15Z",
1371 );
1372 insta::assert_debug_snapshot!(
1373 p("%h %d, %Y %H:%M:%S %:z", "Apr 1, 2022 20:46:15 -04:00:59"),
1374 @"2022-04-02T00:47:14Z",
1375 );
1376
1377 insta::assert_debug_snapshot!(
1378 p("%s", "0"),
1379 @"1970-01-01T00:00:00Z",
1380 );
1381 insta::assert_debug_snapshot!(
1382 p("%s", "-0"),
1383 @"1970-01-01T00:00:00Z",
1384 );
1385 insta::assert_debug_snapshot!(
1386 p("%s", "-1"),
1387 @"1969-12-31T23:59:59Z",
1388 );
1389 insta::assert_debug_snapshot!(
1390 p("%s", "1"),
1391 @"1970-01-01T00:00:01Z",
1392 );
1393 insta::assert_debug_snapshot!(
1394 p("%s", "+1"),
1395 @"1970-01-01T00:00:01Z",
1396 );
1397 insta::assert_debug_snapshot!(
1398 p("%s", "1737396540"),
1399 @"2025-01-20T18:09:00Z",
1400 );
1401 insta::assert_debug_snapshot!(
1402 p("%s", "-377705023201"),
1403 @"-009999-01-02T01:59:59Z",
1404 );
1405 insta::assert_debug_snapshot!(
1406 p("%s", "253402207200"),
1407 @"9999-12-30T22:00:00Z",
1408 );
1409 }
1410
1411 #[test]
1412 fn ok_parse_datetime() {
1413 let p = |fmt: &str, input: &str| {
1414 BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1415 .unwrap()
1416 .to_datetime()
1417 .unwrap()
1418 };
1419
1420 insta::assert_debug_snapshot!(
1421 p("%h %d, %Y %H:%M:%S", "Apr 1, 2022 20:46:15"),
1422 @"2022-04-01T20:46:15",
1423 );
1424 insta::assert_debug_snapshot!(
1425 p("%h %05d, %Y %H:%M:%S", "Apr 1, 2022 20:46:15"),
1426 @"2022-04-01T20:46:15",
1427 );
1428 }
1429
1430 #[test]
1431 fn ok_parse_date() {
1432 let p = |fmt: &str, input: &str| {
1433 BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1434 .unwrap()
1435 .to_date()
1436 .unwrap()
1437 };
1438
1439 insta::assert_debug_snapshot!(
1440 p("%m/%d/%y", "1/1/99"),
1441 @"1999-01-01",
1442 );
1443 insta::assert_debug_snapshot!(
1444 p("%m/%d/%04y", "1/1/0099"),
1445 @"1999-01-01",
1446 );
1447 insta::assert_debug_snapshot!(
1448 p("%D", "1/1/99"),
1449 @"1999-01-01",
1450 );
1451 insta::assert_debug_snapshot!(
1452 p("%m/%d/%Y", "1/1/0099"),
1453 @"0099-01-01",
1454 );
1455 insta::assert_debug_snapshot!(
1456 p("%m/%d/%Y", "1/1/1999"),
1457 @"1999-01-01",
1458 );
1459 insta::assert_debug_snapshot!(
1460 p("%m/%d/%Y", "12/31/9999"),
1461 @"9999-12-31",
1462 );
1463 insta::assert_debug_snapshot!(
1464 p("%m/%d/%Y", "01/01/-9999"),
1465 @"-009999-01-01",
1466 );
1467 insta::assert_snapshot!(
1468 p("%a %m/%d/%Y", "sun 7/14/2024"),
1469 @"2024-07-14",
1470 );
1471 insta::assert_snapshot!(
1472 p("%A %m/%d/%Y", "sUnDaY 7/14/2024"),
1473 @"2024-07-14",
1474 );
1475 insta::assert_snapshot!(
1476 p("%b %d %Y", "Jul 14 2024"),
1477 @"2024-07-14",
1478 );
1479 insta::assert_snapshot!(
1480 p("%B %d, %Y", "July 14, 2024"),
1481 @"2024-07-14",
1482 );
1483 insta::assert_snapshot!(
1484 p("%A, %B %d, %Y", "Wednesday, dEcEmBeR 25, 2024"),
1485 @"2024-12-25",
1486 );
1487
1488 insta::assert_debug_snapshot!(
1489 p("%Y%m%d", "20240730"),
1490 @"2024-07-30",
1491 );
1492 insta::assert_debug_snapshot!(
1493 p("%Y%m%d", "09990730"),
1494 @"0999-07-30",
1495 );
1496 insta::assert_debug_snapshot!(
1497 p("%Y%m%d", "9990111"),
1498 @"9990-11-01",
1499 );
1500 insta::assert_debug_snapshot!(
1501 p("%3Y%m%d", "09990111"),
1502 @"0999-01-11",
1503 );
1504 insta::assert_debug_snapshot!(
1505 p("%5Y%m%d", "09990111"),
1506 @"9990-11-01",
1507 );
1508 insta::assert_debug_snapshot!(
1509 p("%5Y%m%d", "009990111"),
1510 @"0999-01-11",
1511 );
1512
1513 insta::assert_debug_snapshot!(
1514 p("%C-%m-%d", "20-07-01"),
1515 @"2000-07-01",
1516 );
1517 insta::assert_debug_snapshot!(
1518 p("%C-%m-%d", "-20-07-01"),
1519 @"-002000-07-01",
1520 );
1521 insta::assert_debug_snapshot!(
1522 p("%C-%m-%d", "9-07-01"),
1523 @"0900-07-01",
1524 );
1525 insta::assert_debug_snapshot!(
1526 p("%C-%m-%d", "-9-07-01"),
1527 @"-000900-07-01",
1528 );
1529 insta::assert_debug_snapshot!(
1530 p("%C-%m-%d", "09-07-01"),
1531 @"0900-07-01",
1532 );
1533 insta::assert_debug_snapshot!(
1534 p("%C-%m-%d", "-09-07-01"),
1535 @"-000900-07-01",
1536 );
1537 insta::assert_debug_snapshot!(
1538 p("%C-%m-%d", "0-07-01"),
1539 @"0000-07-01",
1540 );
1541 insta::assert_debug_snapshot!(
1542 p("%C-%m-%d", "-0-07-01"),
1543 @"0000-07-01",
1544 );
1545
1546 insta::assert_snapshot!(
1547 p("%u %m/%d/%Y", "7 7/14/2024"),
1548 @"2024-07-14",
1549 );
1550 insta::assert_snapshot!(
1551 p("%w %m/%d/%Y", "0 7/14/2024"),
1552 @"2024-07-14",
1553 );
1554
1555 insta::assert_snapshot!(
1556 p("%Y-%U-%u", "2025-00-6"),
1557 @"2025-01-04",
1558 );
1559 insta::assert_snapshot!(
1560 p("%Y-%U-%u", "2025-01-7"),
1561 @"2025-01-05",
1562 );
1563 insta::assert_snapshot!(
1564 p("%Y-%U-%u", "2025-01-1"),
1565 @"2025-01-06",
1566 );
1567
1568 insta::assert_snapshot!(
1569 p("%Y-%W-%u", "2025-00-6"),
1570 @"2025-01-04",
1571 );
1572 insta::assert_snapshot!(
1573 p("%Y-%W-%u", "2025-00-7"),
1574 @"2025-01-05",
1575 );
1576 insta::assert_snapshot!(
1577 p("%Y-%W-%u", "2025-01-1"),
1578 @"2025-01-06",
1579 );
1580 insta::assert_snapshot!(
1581 p("%Y-%W-%u", "2025-01-2"),
1582 @"2025-01-07",
1583 );
1584 }
1585
1586 #[test]
1587 fn ok_parse_time() {
1588 let p = |fmt: &str, input: &str| {
1589 BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1590 .unwrap()
1591 .to_time()
1592 .unwrap()
1593 };
1594
1595 insta::assert_debug_snapshot!(
1596 p("%H:%M", "15:48"),
1597 @"15:48:00",
1598 );
1599 insta::assert_debug_snapshot!(
1600 p("%H:%M:%S", "15:48:59"),
1601 @"15:48:59",
1602 );
1603 insta::assert_debug_snapshot!(
1604 p("%H:%M:%S", "15:48:60"),
1605 @"15:48:59",
1606 );
1607 insta::assert_debug_snapshot!(
1608 p("%T", "15:48:59"),
1609 @"15:48:59",
1610 );
1611 insta::assert_debug_snapshot!(
1612 p("%R", "15:48"),
1613 @"15:48:00",
1614 );
1615
1616 insta::assert_debug_snapshot!(
1617 p("%H %p", "5 am"),
1618 @"05:00:00",
1619 );
1620 insta::assert_debug_snapshot!(
1621 p("%H%p", "5am"),
1622 @"05:00:00",
1623 );
1624 insta::assert_debug_snapshot!(
1625 p("%H%p", "11pm"),
1626 @"23:00:00",
1627 );
1628 insta::assert_debug_snapshot!(
1629 p("%I%p", "11pm"),
1630 @"23:00:00",
1631 );
1632 insta::assert_debug_snapshot!(
1633 p("%I%p", "12am"),
1634 @"00:00:00",
1635 );
1636 insta::assert_debug_snapshot!(
1637 p("%H%p", "23pm"),
1638 @"23:00:00",
1639 );
1640 insta::assert_debug_snapshot!(
1641 p("%H%p", "23am"),
1642 @"11:00:00",
1643 );
1644
1645 insta::assert_debug_snapshot!(
1646 p("%H:%M:%S%.f", "15:48:01.1"),
1647 @"15:48:01.1",
1648 );
1649 insta::assert_debug_snapshot!(
1650 p("%H:%M:%S%.255f", "15:48:01.1"),
1651 @"15:48:01.1",
1652 );
1653 insta::assert_debug_snapshot!(
1654 p("%H:%M:%S%255.255f", "15:48:01.1"),
1655 @"15:48:01.1",
1656 );
1657 insta::assert_debug_snapshot!(
1658 p("%H:%M:%S%.f", "15:48:01"),
1659 @"15:48:01",
1660 );
1661 insta::assert_debug_snapshot!(
1662 p("%H:%M:%S%.fa", "15:48:01a"),
1663 @"15:48:01",
1664 );
1665 insta::assert_debug_snapshot!(
1666 p("%H:%M:%S%.f", "15:48:01.123456789"),
1667 @"15:48:01.123456789",
1668 );
1669 insta::assert_debug_snapshot!(
1670 p("%H:%M:%S%.f", "15:48:01.000000001"),
1671 @"15:48:01.000000001",
1672 );
1673
1674 insta::assert_debug_snapshot!(
1675 p("%H:%M:%S.%f", "15:48:01.1"),
1676 @"15:48:01.1",
1677 );
1678 insta::assert_debug_snapshot!(
1679 p("%H:%M:%S.%3f", "15:48:01.123"),
1680 @"15:48:01.123",
1681 );
1682 insta::assert_debug_snapshot!(
1683 p("%H:%M:%S.%3f", "15:48:01.123456"),
1684 @"15:48:01.123456",
1685 );
1686
1687 insta::assert_debug_snapshot!(
1688 p("%H", "09"),
1689 @"09:00:00",
1690 );
1691 insta::assert_debug_snapshot!(
1692 p("%H", " 9"),
1693 @"09:00:00",
1694 );
1695 insta::assert_debug_snapshot!(
1696 p("%H", "15"),
1697 @"15:00:00",
1698 );
1699 insta::assert_debug_snapshot!(
1700 p("%k", "09"),
1701 @"09:00:00",
1702 );
1703 insta::assert_debug_snapshot!(
1704 p("%k", " 9"),
1705 @"09:00:00",
1706 );
1707 insta::assert_debug_snapshot!(
1708 p("%k", "15"),
1709 @"15:00:00",
1710 );
1711
1712 insta::assert_debug_snapshot!(
1713 p("%I", "09"),
1714 @"09:00:00",
1715 );
1716 insta::assert_debug_snapshot!(
1717 p("%I", " 9"),
1718 @"09:00:00",
1719 );
1720 insta::assert_debug_snapshot!(
1721 p("%l", "09"),
1722 @"09:00:00",
1723 );
1724 insta::assert_debug_snapshot!(
1725 p("%l", " 9"),
1726 @"09:00:00",
1727 );
1728 }
1729
1730 #[test]
1731 fn ok_parse_whitespace() {
1732 let p = |fmt: &str, input: &str| {
1733 BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1734 .unwrap()
1735 .to_time()
1736 .unwrap()
1737 };
1738
1739 insta::assert_debug_snapshot!(
1740 p("%H%M", "1548"),
1741 @"15:48:00",
1742 );
1743 insta::assert_debug_snapshot!(
1744 p("%H%M", "15\n48"),
1745 @"15:48:00",
1746 );
1747 insta::assert_debug_snapshot!(
1748 p("%H%M", "15\t48"),
1749 @"15:48:00",
1750 );
1751 insta::assert_debug_snapshot!(
1752 p("%H%n%M", "1548"),
1753 @"15:48:00",
1754 );
1755 insta::assert_debug_snapshot!(
1756 p("%H%n%M", "15\n48"),
1757 @"15:48:00",
1758 );
1759 insta::assert_debug_snapshot!(
1760 p("%H%n%M", "15\t48"),
1761 @"15:48:00",
1762 );
1763 insta::assert_debug_snapshot!(
1764 p("%H%t%M", "1548"),
1765 @"15:48:00",
1766 );
1767 insta::assert_debug_snapshot!(
1768 p("%H%t%M", "15\n48"),
1769 @"15:48:00",
1770 );
1771 insta::assert_debug_snapshot!(
1772 p("%H%t%M", "15\t48"),
1773 @"15:48:00",
1774 );
1775 }
1776
1777 #[test]
1778 fn err_parse() {
1779 let p = |fmt: &str, input: &str| {
1780 BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1781 .unwrap_err()
1782 .to_string()
1783 };
1784
1785 insta::assert_snapshot!(
1786 p("%M", ""),
1787 @"strptime parsing failed: expected non-empty input for directive %M, but found end of input",
1788 );
1789 insta::assert_snapshot!(
1790 p("%M", "a"),
1791 @"strptime parsing failed: %M failed: failed to parse minute: invalid number, no digits found",
1792 );
1793 insta::assert_snapshot!(
1794 p("%M%S", "15"),
1795 @"strptime parsing failed: expected non-empty input for directive %S, but found end of input",
1796 );
1797 insta::assert_snapshot!(
1798 p("%M%a", "Sun"),
1799 @"strptime parsing failed: %M failed: failed to parse minute: invalid number, no digits found",
1800 );
1801
1802 insta::assert_snapshot!(
1803 p("%y", "999"),
1804 @r###"strptime expects to consume the entire input, but "9" remains unparsed"###,
1805 );
1806 insta::assert_snapshot!(
1807 p("%Y", "-10000"),
1808 @r###"strptime expects to consume the entire input, but "0" remains unparsed"###,
1809 );
1810 insta::assert_snapshot!(
1811 p("%Y", "10000"),
1812 @r###"strptime expects to consume the entire input, but "0" remains unparsed"###,
1813 );
1814 insta::assert_snapshot!(
1815 p("%A %m/%d/%y", "Mon 7/14/24"),
1816 @r###"strptime parsing failed: %A failed: unrecognized weekday abbreviation: failed to find expected choice at beginning of "Mon 7/14/24", available choices are: Sunday, Monday, Tueday, Wednesday, Thursday, Friday, Saturday"###,
1817 );
1818 insta::assert_snapshot!(
1819 p("%b", "Bad"),
1820 @r###"strptime parsing failed: %b failed: expected to find month name abbreviation, but found "Bad" instead"###,
1821 );
1822 insta::assert_snapshot!(
1823 p("%h", "July"),
1824 @r###"strptime expects to consume the entire input, but "y" remains unparsed"###,
1825 );
1826 insta::assert_snapshot!(
1827 p("%B", "Jul"),
1828 @r###"strptime parsing failed: %B failed: unrecognized month name: failed to find expected choice at beginning of "Jul", available choices are: January, February, March, April, May, June, July, August, September, October, November, December"###,
1829 );
1830 insta::assert_snapshot!(
1831 p("%H", "24"),
1832 @"strptime parsing failed: %H failed: hour number is invalid: parameter 'hour' with value 24 is not in the required range of 0..=23",
1833 );
1834 insta::assert_snapshot!(
1835 p("%M", "60"),
1836 @"strptime parsing failed: %M failed: minute number is invalid: parameter 'minute' with value 60 is not in the required range of 0..=59",
1837 );
1838 insta::assert_snapshot!(
1839 p("%S", "61"),
1840 @"strptime parsing failed: %S failed: second number is invalid: parameter 'second' with value 61 is not in the required range of 0..=59",
1841 );
1842 insta::assert_snapshot!(
1843 p("%I", "0"),
1844 @"strptime parsing failed: %I failed: hour number is invalid: parameter 'hour' with value 0 is not in the required range of 1..=12",
1845 );
1846 insta::assert_snapshot!(
1847 p("%I", "13"),
1848 @"strptime parsing failed: %I failed: hour number is invalid: parameter 'hour' with value 13 is not in the required range of 1..=12",
1849 );
1850 insta::assert_snapshot!(
1851 p("%p", "aa"),
1852 @r###"strptime parsing failed: %p failed: expected to find AM or PM, but found "aa" instead"###,
1853 );
1854
1855 insta::assert_snapshot!(
1856 p("%_", " "),
1857 @r###"strptime parsing failed: expected to find specifier directive after flag "_", but found end of format string"###,
1858 );
1859 insta::assert_snapshot!(
1860 p("%-", " "),
1861 @r###"strptime parsing failed: expected to find specifier directive after flag "-", but found end of format string"###,
1862 );
1863 insta::assert_snapshot!(
1864 p("%0", " "),
1865 @r###"strptime parsing failed: expected to find specifier directive after flag "0", but found end of format string"###,
1866 );
1867 insta::assert_snapshot!(
1868 p("%^", " "),
1869 @r###"strptime parsing failed: expected to find specifier directive after flag "^", but found end of format string"###,
1870 );
1871 insta::assert_snapshot!(
1872 p("%#", " "),
1873 @r###"strptime parsing failed: expected to find specifier directive after flag "#", but found end of format string"###,
1874 );
1875 insta::assert_snapshot!(
1876 p("%_1", " "),
1877 @"strptime parsing failed: expected to find specifier directive after width 1, but found end of format string",
1878 );
1879 insta::assert_snapshot!(
1880 p("%_23", " "),
1881 @"strptime parsing failed: expected to find specifier directive after width 23, but found end of format string",
1882 );
1883
1884 insta::assert_snapshot!(
1885 p("%H:%M:%S%.f", "15:59:01."),
1886 @"strptime parsing failed: %.f failed: expected at least one fractional decimal digit, but did not find any",
1887 );
1888 insta::assert_snapshot!(
1889 p("%H:%M:%S%.f", "15:59:01.a"),
1890 @"strptime parsing failed: %.f failed: expected at least one fractional decimal digit, but did not find any",
1891 );
1892 insta::assert_snapshot!(
1893 p("%H:%M:%S%.f", "15:59:01.1234567891"),
1894 @r###"strptime expects to consume the entire input, but "1" remains unparsed"###,
1895 );
1896 insta::assert_snapshot!(
1897 p("%H:%M:%S.%f", "15:59:01."),
1898 @"strptime parsing failed: expected non-empty input for directive %f, but found end of input",
1899 );
1900 insta::assert_snapshot!(
1901 p("%H:%M:%S.%f", "15:59:01"),
1902 @r###"strptime parsing failed: expected to match literal byte "." from format string, but found end of input"###,
1903 );
1904 insta::assert_snapshot!(
1905 p("%H:%M:%S.%f", "15:59:01.a"),
1906 @"strptime parsing failed: %f failed: expected at least one fractional decimal digit, but did not find any",
1907 );
1908
1909 insta::assert_snapshot!(
1910 p("%Q", "+America/New_York"),
1911 @"strptime parsing failed: %Q failed: failed to parse hours from time zone offset Amer: invalid digit, expected 0-9 but got A",
1912 );
1913 insta::assert_snapshot!(
1914 p("%Q", "-America/New_York"),
1915 @"strptime parsing failed: %Q failed: failed to parse hours from time zone offset Amer: invalid digit, expected 0-9 but got A",
1916 );
1917 insta::assert_snapshot!(
1918 p("%:Q", "+0400"),
1919 @"strptime parsing failed: %:Q failed: expected at least HH:MM digits for time zone offset after sign, but found only 4 bytes remaining",
1920 );
1921 insta::assert_snapshot!(
1922 p("%Q", "+04:00"),
1923 @"strptime parsing failed: %Q failed: failed to parse minutes from time zone offset 04:0: invalid digit, expected 0-9 but got :",
1924 );
1925 insta::assert_snapshot!(
1926 p("%Q", "America/"),
1927 @"strptime parsing failed: %Q failed: expected the start of an IANA time zone identifier name or component, but found end of input instead",
1928 );
1929 insta::assert_snapshot!(
1930 p("%Q", "America/+"),
1931 @r###"strptime parsing failed: %Q failed: expected the start of an IANA time zone identifier name or component, but found "+" instead"###,
1932 );
1933
1934 insta::assert_snapshot!(
1935 p("%s", "-377705023202"),
1936 @"strptime parsing failed: %s failed: parsed Unix timestamp `-377705023202`, but out of range of valid Jiff `Timestamp`: parameter 'second' with value -377705023202 is not in the required range of -377705023201..=253402207200",
1937 );
1938 insta::assert_snapshot!(
1939 p("%s", "253402207201"),
1940 @"strptime parsing failed: %s failed: parsed Unix timestamp `253402207201`, but out of range of valid Jiff `Timestamp`: parameter 'second' with value 253402207201 is not in the required range of -377705023201..=253402207200",
1941 );
1942 insta::assert_snapshot!(
1943 p("%s", "-9999999999999999999"),
1944 @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number '9999999999999999999' too big to parse into 64-bit integer",
1945 );
1946 insta::assert_snapshot!(
1947 p("%s", "9999999999999999999"),
1948 @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number '9999999999999999999' too big to parse into 64-bit integer",
1949 );
1950
1951 insta::assert_snapshot!(
1952 p("%u", "0"),
1953 @"strptime parsing failed: %u failed: weekday number is invalid: parameter 'weekday' with value 0 is not in the required range of 1..=7",
1954 );
1955 insta::assert_snapshot!(
1956 p("%w", "7"),
1957 @"strptime parsing failed: %w failed: weekday number is invalid: parameter 'weekday' with value 7 is not in the required range of 0..=6",
1958 );
1959 insta::assert_snapshot!(
1960 p("%u", "128"),
1961 @r###"strptime expects to consume the entire input, but "28" remains unparsed"###,
1962 );
1963 insta::assert_snapshot!(
1964 p("%w", "128"),
1965 @r###"strptime expects to consume the entire input, but "28" remains unparsed"###,
1966 );
1967 }
1968
1969 #[test]
1970 fn err_parse_date() {
1971 let p = |fmt: &str, input: &str| {
1972 BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1973 .unwrap()
1974 .to_date()
1975 .unwrap_err()
1976 .to_string()
1977 };
1978
1979 insta::assert_snapshot!(
1980 p("%Y", "2024"),
1981 @"a month/day, day-of-year or week date must be present to create a date, but none were found",
1982 );
1983 insta::assert_snapshot!(
1984 p("%m", "7"),
1985 @"missing year, date cannot be created",
1986 );
1987 insta::assert_snapshot!(
1988 p("%d", "25"),
1989 @"missing year, date cannot be created",
1990 );
1991 insta::assert_snapshot!(
1992 p("%Y-%m", "2024-7"),
1993 @"a month/day, day-of-year or week date must be present to create a date, but none were found",
1994 );
1995 insta::assert_snapshot!(
1996 p("%Y-%d", "2024-25"),
1997 @"a month/day, day-of-year or week date must be present to create a date, but none were found",
1998 );
1999 insta::assert_snapshot!(
2000 p("%m-%d", "7-25"),
2001 @"missing year, date cannot be created",
2002 );
2003
2004 insta::assert_snapshot!(
2005 p("%m/%d/%y", "6/31/24"),
2006 @"invalid date: parameter 'day' with value 31 is not in the required range of 1..=30",
2007 );
2008 insta::assert_snapshot!(
2009 p("%m/%d/%y", "2/29/23"),
2010 @"invalid date: parameter 'day' with value 29 is not in the required range of 1..=28",
2011 );
2012 insta::assert_snapshot!(
2013 p("%a %m/%d/%y", "Mon 7/14/24"),
2014 @"parsed weekday Monday does not match weekday Sunday from parsed date 2024-07-14",
2015 );
2016 insta::assert_snapshot!(
2017 p("%A %m/%d/%y", "Monday 7/14/24"),
2018 @"parsed weekday Monday does not match weekday Sunday from parsed date 2024-07-14",
2019 );
2020
2021 insta::assert_snapshot!(
2022 p("%Y-%U-%u", "2025-00-2"),
2023 @"weekday `Tuesday` is not valid for Sunday based week number `0` in year `2025`",
2024 );
2025 insta::assert_snapshot!(
2026 p("%Y-%W-%u", "2025-00-2"),
2027 @"weekday `Tuesday` is not valid for Monday based week number `0` in year `2025`",
2028 );
2029 }
2030
2031 #[test]
2032 fn err_parse_time() {
2033 let p = |fmt: &str, input: &str| {
2034 BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
2035 .unwrap()
2036 .to_time()
2037 .unwrap_err()
2038 .to_string()
2039 };
2040
2041 insta::assert_snapshot!(
2042 p("%M", "59"),
2043 @"parsing format did not include hour directive, but did include minute directive (cannot have smaller time units with bigger time units missing)",
2044 );
2045 insta::assert_snapshot!(
2046 p("%S", "59"),
2047 @"parsing format did not include hour directive, but did include second directive (cannot have smaller time units with bigger time units missing)",
2048 );
2049 insta::assert_snapshot!(
2050 p("%M:%S", "59:59"),
2051 @"parsing format did not include hour directive, but did include minute directive (cannot have smaller time units with bigger time units missing)",
2052 );
2053 insta::assert_snapshot!(
2054 p("%H:%S", "15:59"),
2055 @"parsing format did not include minute directive, but did include second directive (cannot have smaller time units with bigger time units missing)",
2056 );
2057 }
2058}