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