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