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