1use crate::{
105 error::{err, Error, ErrorContext},
106 fmt::{
107 temporal::{PiecesNumericOffset, PiecesOffset},
108 util::{parse_temporal_fraction, FractionalFormatter},
109 Parsed,
110 },
111 tz::Offset,
112 util::{
113 escape, parse,
114 rangeint::{ri8, RFrom},
115 t::{self, C},
116 },
117};
118
119type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>;
123type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>;
124type ParsedOffsetSeconds = ri8<0, { t::SpanZoneOffsetSeconds::MAX }>;
125
126#[derive(Debug)]
132pub(crate) struct ParsedOffset {
133 kind: ParsedOffsetKind,
135}
136
137impl ParsedOffset {
138 pub(crate) fn to_offset(&self) -> Result<Offset, Error> {
152 match self.kind {
153 ParsedOffsetKind::Zulu => Ok(Offset::UTC),
154 ParsedOffsetKind::Numeric(ref numeric) => numeric.to_offset(),
155 }
156 }
157
158 pub(crate) fn to_pieces_offset(&self) -> Result<PiecesOffset, Error> {
164 match self.kind {
165 ParsedOffsetKind::Zulu => Ok(PiecesOffset::Zulu),
166 ParsedOffsetKind::Numeric(ref numeric) => {
167 let mut off = PiecesNumericOffset::from(numeric.to_offset()?);
168 if numeric.sign < C(0) {
169 off = off.with_negative_zero();
170 }
171 Ok(PiecesOffset::from(off))
172 }
173 }
174 }
175
176 pub(crate) fn is_zulu(&self) -> bool {
182 matches!(self.kind, ParsedOffsetKind::Zulu)
183 }
184
185 pub(crate) fn has_subminute(&self) -> bool {
187 let ParsedOffsetKind::Numeric(ref numeric) = self.kind else {
188 return false;
189 };
190 numeric.seconds.is_some()
191 }
192}
193
194#[derive(Debug)]
196enum ParsedOffsetKind {
197 Zulu,
200 Numeric(Numeric),
202}
203
204struct Numeric {
206 sign: t::Sign,
209 hours: ParsedOffsetHours,
212 minutes: Option<ParsedOffsetMinutes>,
214 seconds: Option<ParsedOffsetSeconds>,
217 nanoseconds: Option<t::SubsecNanosecond>,
220}
221
222impl Numeric {
223 fn to_offset(&self) -> Result<Offset, Error> {
229 let mut seconds = t::SpanZoneOffset::rfrom(C(3_600) * self.hours);
230 if let Some(part_minutes) = self.minutes {
231 seconds += C(60) * part_minutes;
232 }
233 if let Some(part_seconds) = self.seconds {
234 seconds += part_seconds;
235 }
236 if let Some(part_nanoseconds) = self.nanoseconds {
237 if part_nanoseconds >= C(500_000_000) {
238 seconds = seconds
239 .try_checked_add("offset-seconds", C(1))
240 .with_context(|| {
241 err!(
242 "due to precision loss, UTC offset '{}' is \
243 rounded to a value that is out of bounds",
244 self,
245 )
246 })?;
247 }
248 }
249 Ok(Offset::from_seconds_ranged(seconds * self.sign))
250 }
251}
252
253impl core::fmt::Display for Numeric {
256 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
257 if self.sign == C(-1) {
258 write!(f, "-")?;
259 } else {
260 write!(f, "+")?;
261 }
262 write!(f, "{:02}", self.hours)?;
263 if let Some(minutes) = self.minutes {
264 write!(f, ":{:02}", minutes)?;
265 }
266 if let Some(seconds) = self.seconds {
267 write!(f, ":{:02}", seconds)?;
268 }
269 if let Some(nanos) = self.nanoseconds {
270 static FMT: FractionalFormatter = FractionalFormatter::new();
271 write!(f, ".{}", FMT.format(i64::from(nanos)).as_str())?;
272 }
273 Ok(())
274 }
275}
276
277impl core::fmt::Debug for Numeric {
280 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
281 core::fmt::Display::fmt(self, f)
282 }
283}
284
285#[derive(Debug)]
298pub(crate) struct Parser {
299 zulu: bool,
300 require_minute: bool,
301 require_second: bool,
302 subminute: bool,
303 subsecond: bool,
304 colon: Colon,
305}
306
307impl Parser {
308 pub(crate) const fn new() -> Parser {
310 Parser {
311 zulu: true,
312 require_minute: false,
313 require_second: false,
314 subminute: true,
315 subsecond: true,
316 colon: Colon::Optional,
317 }
318 }
319
320 pub(crate) const fn zulu(self, yes: bool) -> Parser {
328 Parser { zulu: yes, ..self }
329 }
330
331 pub(crate) const fn require_minute(self, yes: bool) -> Parser {
336 Parser { require_minute: yes, ..self }
337 }
338
339 pub(crate) const fn require_second(self, yes: bool) -> Parser {
346 Parser { require_second: yes, ..self }
347 }
348
349 pub(crate) const fn subminute(self, yes: bool) -> Parser {
356 Parser { subminute: yes, ..self }
357 }
358
359 pub(crate) const fn subsecond(self, yes: bool) -> Parser {
370 Parser { subsecond: yes, ..self }
371 }
372
373 pub(crate) const fn colon(self, colon: Colon) -> Parser {
377 Parser { colon, ..self }
378 }
379
380 pub(crate) fn parse<'i>(
408 &self,
409 mut input: &'i [u8],
410 ) -> Result<Parsed<'i, ParsedOffset>, Error> {
411 if input.is_empty() {
412 return Err(err!("expected UTC offset, but found end of input"));
413 }
414
415 if input[0] == b'Z' || input[0] == b'z' {
416 if !self.zulu {
417 return Err(err!(
418 "found {z:?} in {original:?} where a numeric UTC offset \
419 was expected (this context does not permit \
420 the Zulu offset)",
421 z = escape::Byte(input[0]),
422 original = escape::Bytes(input),
423 ));
424 }
425 input = &input[1..];
426 let value = ParsedOffset { kind: ParsedOffsetKind::Zulu };
427 return Ok(Parsed { value, input });
428 }
429 let Parsed { value: numeric, input } = self.parse_numeric(input)?;
430 let value = ParsedOffset { kind: ParsedOffsetKind::Numeric(numeric) };
431 Ok(Parsed { value, input })
432 }
433
434 #[cfg_attr(feature = "perf-inline", inline(always))]
440 pub(crate) fn parse_optional<'i>(
441 &self,
442 input: &'i [u8],
443 ) -> Result<Parsed<'i, Option<ParsedOffset>>, Error> {
444 let Some(first) = input.first().copied() else {
445 return Ok(Parsed { value: None, input });
446 };
447 if !matches!(first, b'z' | b'Z' | b'+' | b'-') {
448 return Ok(Parsed { value: None, input });
449 }
450 let Parsed { value, input } = self.parse(input)?;
451 Ok(Parsed { value: Some(value), input })
452 }
453
454 #[cfg_attr(feature = "perf-inline", inline(always))]
459 fn parse_numeric<'i>(
460 &self,
461 input: &'i [u8],
462 ) -> Result<Parsed<'i, Numeric>, Error> {
463 let original = escape::Bytes(input);
464
465 let Parsed { value: sign, input } =
467 self.parse_sign(input).with_context(|| {
468 err!("failed to parse sign in UTC numeric offset {original:?}")
469 })?;
470
471 let Parsed { value: hours, input } =
473 self.parse_hours(input).with_context(|| {
474 err!(
475 "failed to parse hours in UTC numeric offset {original:?}"
476 )
477 })?;
478 let extended = match self.colon {
479 Colon::Optional => input.starts_with(b":"),
480 Colon::Required => {
481 if !input.is_empty() && !input.starts_with(b":") {
482 return Err(err!(
483 "parsed hour component of time zone offset from \
484 {original:?}, but could not find required colon \
485 separator",
486 ));
487 }
488 true
489 }
490 Colon::Absent => {
491 if !input.is_empty() && input.starts_with(b":") {
492 return Err(err!(
493 "parsed hour component of time zone offset from \
494 {original:?}, but found colon after hours which \
495 is not allowed",
496 ));
497 }
498 false
499 }
500 };
501
502 let mut numeric = Numeric {
504 sign,
505 hours,
506 minutes: None,
507 seconds: None,
508 nanoseconds: None,
509 };
510
511 let Parsed { value: has_minutes, input } =
513 self.parse_separator(input, extended).with_context(|| {
514 err!(
515 "failed to parse separator after hours in \
516 UTC numeric offset {original:?}"
517 )
518 })?;
519 if !has_minutes {
520 if self.require_minute || (self.subminute && self.require_second) {
521 return Err(err!(
522 "parsed hour component of time zone offset from \
523 {original:?}, but could not find required minute \
524 component",
525 ));
526 }
527 return Ok(Parsed { value: numeric, input });
528 }
529
530 let Parsed { value: minutes, input } =
532 self.parse_minutes(input).with_context(|| {
533 err!(
534 "failed to parse minutes in UTC numeric offset \
535 {original:?}"
536 )
537 })?;
538 numeric.minutes = Some(minutes);
539
540 if !self.subminute {
542 if input.get(0).map_or(false, |&b| b == b':') {
549 return Err(err!(
550 "subminute precision for UTC numeric offset {original:?} \
551 is not enabled in this context (must provide only \
552 integral minutes)",
553 ));
554 }
555 return Ok(Parsed { value: numeric, input });
556 }
557
558 let Parsed { value: has_seconds, input } =
560 self.parse_separator(input, extended).with_context(|| {
561 err!(
562 "failed to parse separator after minutes in \
563 UTC numeric offset {original:?}"
564 )
565 })?;
566 if !has_seconds {
567 if self.require_second {
568 return Err(err!(
569 "parsed hour and minute components of time zone offset \
570 from {original:?}, but could not find required second \
571 component",
572 ));
573 }
574 return Ok(Parsed { value: numeric, input });
575 }
576
577 let Parsed { value: seconds, input } =
579 self.parse_seconds(input).with_context(|| {
580 err!(
581 "failed to parse seconds in UTC numeric offset \
582 {original:?}"
583 )
584 })?;
585 numeric.seconds = Some(seconds);
586
587 if !self.subsecond {
589 if input.get(0).map_or(false, |&b| b == b'.' || b == b',') {
590 return Err(err!(
591 "subsecond precision for UTC numeric offset {original:?} \
592 is not enabled in this context (must provide only \
593 integral minutes or seconds)",
594 ));
595 }
596 return Ok(Parsed { value: numeric, input });
597 }
598
599 let Parsed { value: nanoseconds, input } =
601 parse_temporal_fraction(input).with_context(|| {
602 err!(
603 "failed to parse fractional nanoseconds in \
604 UTC numeric offset {original:?}",
605 )
606 })?;
607 numeric.nanoseconds = nanoseconds;
608 Ok(Parsed { value: numeric, input })
609 }
610
611 #[cfg_attr(feature = "perf-inline", inline(always))]
612 fn parse_sign<'i>(
613 &self,
614 input: &'i [u8],
615 ) -> Result<Parsed<'i, t::Sign>, Error> {
616 let sign = input.get(0).copied().ok_or_else(|| {
617 err!("expected UTC numeric offset, but found end of input")
618 })?;
619 let sign = if sign == b'+' {
620 t::Sign::N::<1>()
621 } else if sign == b'-' {
622 t::Sign::N::<-1>()
623 } else {
624 return Err(err!(
625 "expected '+' or '-' sign at start of UTC numeric offset, \
626 but found {found:?} instead",
627 found = escape::Byte(sign),
628 ));
629 };
630 Ok(Parsed { value: sign, input: &input[1..] })
631 }
632
633 #[cfg_attr(feature = "perf-inline", inline(always))]
634 fn parse_hours<'i>(
635 &self,
636 input: &'i [u8],
637 ) -> Result<Parsed<'i, ParsedOffsetHours>, Error> {
638 let (hours, input) = parse::split(input, 2).ok_or_else(|| {
639 err!("expected two digit hour after sign, but found end of input",)
640 })?;
641 let hours = parse::i64(hours).with_context(|| {
642 err!(
643 "failed to parse {hours:?} as hours (a two digit integer)",
644 hours = escape::Bytes(hours),
645 )
646 })?;
647 let hours = ParsedOffsetHours::try_new("hours", hours)
653 .context("offset hours are not valid")?;
654 Ok(Parsed { value: hours, input })
655 }
656
657 #[cfg_attr(feature = "perf-inline", inline(always))]
658 fn parse_minutes<'i>(
659 &self,
660 input: &'i [u8],
661 ) -> Result<Parsed<'i, ParsedOffsetMinutes>, Error> {
662 let (minutes, input) = parse::split(input, 2).ok_or_else(|| {
663 err!(
664 "expected two digit minute after hours, \
665 but found end of input",
666 )
667 })?;
668 let minutes = parse::i64(minutes).with_context(|| {
669 err!(
670 "failed to parse {minutes:?} as minutes (a two digit integer)",
671 minutes = escape::Bytes(minutes),
672 )
673 })?;
674 let minutes = ParsedOffsetMinutes::try_new("minutes", minutes)
675 .context("minutes are not valid")?;
676 Ok(Parsed { value: minutes, input })
677 }
678
679 #[cfg_attr(feature = "perf-inline", inline(always))]
680 fn parse_seconds<'i>(
681 &self,
682 input: &'i [u8],
683 ) -> Result<Parsed<'i, ParsedOffsetSeconds>, Error> {
684 let (seconds, input) = parse::split(input, 2).ok_or_else(|| {
685 err!(
686 "expected two digit second after hours, \
687 but found end of input",
688 )
689 })?;
690 let seconds = parse::i64(seconds).with_context(|| {
691 err!(
692 "failed to parse {seconds:?} as seconds (a two digit integer)",
693 seconds = escape::Bytes(seconds),
694 )
695 })?;
696 let seconds = ParsedOffsetSeconds::try_new("seconds", seconds)
697 .context("time zone offset seconds are not valid")?;
698 Ok(Parsed { value: seconds, input })
699 }
700
701 #[cfg_attr(feature = "perf-inline", inline(always))]
712 fn parse_separator<'i>(
713 &self,
714 mut input: &'i [u8],
715 extended: bool,
716 ) -> Result<Parsed<'i, bool>, Error> {
717 if !extended {
718 let expected =
719 input.len() >= 2 && input[..2].iter().all(u8::is_ascii_digit);
720 return Ok(Parsed { value: expected, input });
721 }
722 let is_separator = input.get(0).map_or(false, |&b| b == b':');
723 if is_separator {
724 input = &input[1..];
725 }
726 Ok(Parsed { value: is_separator, input })
727 }
728}
729
730#[derive(Debug)]
732pub(crate) enum Colon {
733 Optional,
736 Required,
738 Absent,
740}
741
742#[cfg(test)]
743mod tests {
744 use crate::util::rangeint::RInto;
745
746 use super::*;
747
748 #[test]
749 fn ok_zulu() {
750 let p = |input| Parser::new().parse(input).unwrap();
751
752 insta::assert_debug_snapshot!(p(b"Z"), @r###"
753 Parsed {
754 value: ParsedOffset {
755 kind: Zulu,
756 },
757 input: "",
758 }
759 "###);
760 insta::assert_debug_snapshot!(p(b"z"), @r###"
761 Parsed {
762 value: ParsedOffset {
763 kind: Zulu,
764 },
765 input: "",
766 }
767 "###);
768 }
769
770 #[test]
771 fn ok_numeric() {
772 let p = |input| Parser::new().parse(input).unwrap();
773
774 insta::assert_debug_snapshot!(p(b"-05"), @r###"
775 Parsed {
776 value: ParsedOffset {
777 kind: Numeric(
778 -05,
779 ),
780 },
781 input: "",
782 }
783 "###);
784 }
785
786 #[test]
788 fn ok_numeric_complete() {
789 let p = |input| Parser::new().parse_numeric(input).unwrap();
790
791 insta::assert_debug_snapshot!(p(b"-05"), @r###"
792 Parsed {
793 value: -05,
794 input: "",
795 }
796 "###);
797 insta::assert_debug_snapshot!(p(b"+05"), @r###"
798 Parsed {
799 value: +05,
800 input: "",
801 }
802 "###);
803
804 insta::assert_debug_snapshot!(p(b"+25:59"), @r###"
805 Parsed {
806 value: +25:59,
807 input: "",
808 }
809 "###);
810 insta::assert_debug_snapshot!(p(b"+2559"), @r###"
811 Parsed {
812 value: +25:59,
813 input: "",
814 }
815 "###);
816
817 insta::assert_debug_snapshot!(p(b"+25:59:59"), @r###"
818 Parsed {
819 value: +25:59:59,
820 input: "",
821 }
822 "###);
823 insta::assert_debug_snapshot!(p(b"+255959"), @r###"
824 Parsed {
825 value: +25:59:59,
826 input: "",
827 }
828 "###);
829
830 insta::assert_debug_snapshot!(p(b"+25:59:59.999"), @r###"
831 Parsed {
832 value: +25:59:59.999,
833 input: "",
834 }
835 "###);
836 insta::assert_debug_snapshot!(p(b"+25:59:59,999"), @r###"
837 Parsed {
838 value: +25:59:59.999,
839 input: "",
840 }
841 "###);
842 insta::assert_debug_snapshot!(p(b"+255959.999"), @r###"
843 Parsed {
844 value: +25:59:59.999,
845 input: "",
846 }
847 "###);
848 insta::assert_debug_snapshot!(p(b"+255959,999"), @r###"
849 Parsed {
850 value: +25:59:59.999,
851 input: "",
852 }
853 "###);
854
855 insta::assert_debug_snapshot!(p(b"+25:59:59.999999999"), @r###"
856 Parsed {
857 value: +25:59:59.999999999,
858 input: "",
859 }
860 "###);
861 }
862
863 #[test]
866 fn ok_numeric_incomplete() {
867 let p = |input| Parser::new().parse_numeric(input).unwrap();
868
869 insta::assert_debug_snapshot!(p(b"-05a"), @r###"
870 Parsed {
871 value: -05,
872 input: "a",
873 }
874 "###);
875 insta::assert_debug_snapshot!(p(b"-05:12a"), @r###"
876 Parsed {
877 value: -05:12,
878 input: "a",
879 }
880 "###);
881 insta::assert_debug_snapshot!(p(b"-05:12."), @r###"
882 Parsed {
883 value: -05:12,
884 input: ".",
885 }
886 "###);
887 insta::assert_debug_snapshot!(p(b"-05:12,"), @r###"
888 Parsed {
889 value: -05:12,
890 input: ",",
891 }
892 "###);
893 insta::assert_debug_snapshot!(p(b"-0512a"), @r###"
894 Parsed {
895 value: -05:12,
896 input: "a",
897 }
898 "###);
899 insta::assert_debug_snapshot!(p(b"-0512:"), @r###"
900 Parsed {
901 value: -05:12,
902 input: ":",
903 }
904 "###);
905 insta::assert_debug_snapshot!(p(b"-05:12:34a"), @r###"
906 Parsed {
907 value: -05:12:34,
908 input: "a",
909 }
910 "###);
911 insta::assert_debug_snapshot!(p(b"-05:12:34.9a"), @r###"
912 Parsed {
913 value: -05:12:34.9,
914 input: "a",
915 }
916 "###);
917 insta::assert_debug_snapshot!(p(b"-05:12:34.9."), @r###"
918 Parsed {
919 value: -05:12:34.9,
920 input: ".",
921 }
922 "###);
923 insta::assert_debug_snapshot!(p(b"-05:12:34.9,"), @r###"
924 Parsed {
925 value: -05:12:34.9,
926 input: ",",
927 }
928 "###);
929 }
930
931 #[test]
935 fn err_numeric_empty() {
936 insta::assert_snapshot!(
937 Parser::new().parse_numeric(b"").unwrap_err(),
938 @r###"failed to parse sign in UTC numeric offset "": expected UTC numeric offset, but found end of input"###,
939 );
940 }
941
942 #[test]
944 fn err_numeric_notsign() {
945 insta::assert_snapshot!(
946 Parser::new().parse_numeric(b"*").unwrap_err(),
947 @r###"failed to parse sign in UTC numeric offset "*": expected '+' or '-' sign at start of UTC numeric offset, but found "*" instead"###,
948 );
949 }
950
951 #[test]
953 fn err_numeric_hours_too_short() {
954 insta::assert_snapshot!(
955 Parser::new().parse_numeric(b"+a").unwrap_err(),
956 @r###"failed to parse hours in UTC numeric offset "+a": expected two digit hour after sign, but found end of input"###,
957 );
958 }
959
960 #[test]
962 fn err_numeric_hours_invalid_digits() {
963 insta::assert_snapshot!(
964 Parser::new().parse_numeric(b"+ab").unwrap_err(),
965 @r###"failed to parse hours in UTC numeric offset "+ab": failed to parse "ab" as hours (a two digit integer): invalid digit, expected 0-9 but got a"###,
966 );
967 }
968
969 #[test]
971 fn err_numeric_hours_out_of_range() {
972 insta::assert_snapshot!(
973 Parser::new().parse_numeric(b"-26").unwrap_err(),
974 @r###"failed to parse hours in UTC numeric offset "-26": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###,
975 );
976 }
977
978 #[test]
980 fn err_numeric_minutes_too_short() {
981 insta::assert_snapshot!(
982 Parser::new().parse_numeric(b"+05:a").unwrap_err(),
983 @r###"failed to parse minutes in UTC numeric offset "+05:a": expected two digit minute after hours, but found end of input"###,
984 );
985 }
986
987 #[test]
989 fn err_numeric_minutes_invalid_digits() {
990 insta::assert_snapshot!(
991 Parser::new().parse_numeric(b"+05:ab").unwrap_err(),
992 @r###"failed to parse minutes in UTC numeric offset "+05:ab": failed to parse "ab" as minutes (a two digit integer): invalid digit, expected 0-9 but got a"###,
993 );
994 }
995
996 #[test]
998 fn err_numeric_minutes_out_of_range() {
999 insta::assert_snapshot!(
1000 Parser::new().parse_numeric(b"-05:60").unwrap_err(),
1001 @r###"failed to parse minutes in UTC numeric offset "-05:60": minutes are not valid: parameter 'minutes' with value 60 is not in the required range of 0..=59"###,
1002 );
1003 }
1004
1005 #[test]
1007 fn err_numeric_seconds_too_short() {
1008 insta::assert_snapshot!(
1009 Parser::new().parse_numeric(b"+05:30:a").unwrap_err(),
1010 @r###"failed to parse seconds in UTC numeric offset "+05:30:a": expected two digit second after hours, but found end of input"###,
1011 );
1012 }
1013
1014 #[test]
1016 fn err_numeric_seconds_invalid_digits() {
1017 insta::assert_snapshot!(
1018 Parser::new().parse_numeric(b"+05:30:ab").unwrap_err(),
1019 @r###"failed to parse seconds in UTC numeric offset "+05:30:ab": failed to parse "ab" as seconds (a two digit integer): invalid digit, expected 0-9 but got a"###,
1020 );
1021 }
1022
1023 #[test]
1025 fn err_numeric_seconds_out_of_range() {
1026 insta::assert_snapshot!(
1027 Parser::new().parse_numeric(b"-05:30:60").unwrap_err(),
1028 @r###"failed to parse seconds in UTC numeric offset "-05:30:60": time zone offset seconds are not valid: parameter 'seconds' with value 60 is not in the required range of 0..=59"###,
1029 );
1030 }
1031
1032 #[test]
1035 fn err_numeric_fraction_non_empty() {
1036 insta::assert_snapshot!(
1037 Parser::new().parse_numeric(b"-05:30:44.").unwrap_err(),
1038 @r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44.": found decimal after seconds component, but did not find any decimal digits after decimal"###,
1039 );
1040 insta::assert_snapshot!(
1041 Parser::new().parse_numeric(b"-05:30:44,").unwrap_err(),
1042 @r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44,": found decimal after seconds component, but did not find any decimal digits after decimal"###,
1043 );
1044
1045 insta::assert_snapshot!(
1047 Parser::new().parse_numeric(b"-05:30:44.a").unwrap_err(),
1048 @r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44.a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
1049 );
1050 insta::assert_snapshot!(
1051 Parser::new().parse_numeric(b"-05:30:44,a").unwrap_err(),
1052 @r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44,a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
1053 );
1054
1055 insta::assert_snapshot!(
1057 Parser::new().parse_numeric(b"-053044.a").unwrap_err(),
1058 @r###"failed to parse fractional nanoseconds in UTC numeric offset "-053044.a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
1059 );
1060 insta::assert_snapshot!(
1061 Parser::new().parse_numeric(b"-053044,a").unwrap_err(),
1062 @r###"failed to parse fractional nanoseconds in UTC numeric offset "-053044,a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
1063 );
1064 }
1065
1066 #[test]
1070 fn err_numeric_subminute_disabled_but_desired() {
1071 insta::assert_snapshot!(
1072 Parser::new().subminute(false).parse_numeric(b"-05:59:32").unwrap_err(),
1073 @r###"subminute precision for UTC numeric offset "-05:59:32" is not enabled in this context (must provide only integral minutes)"###,
1074 );
1075 }
1076
1077 #[test]
1080 fn err_zulu_disabled_but_desired() {
1081 insta::assert_snapshot!(
1082 Parser::new().zulu(false).parse(b"Z").unwrap_err(),
1083 @r###"found "Z" in "Z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###,
1084 );
1085 insta::assert_snapshot!(
1086 Parser::new().zulu(false).parse(b"z").unwrap_err(),
1087 @r###"found "z" in "z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###,
1088 );
1089 }
1090
1091 #[test]
1096 fn err_numeric_too_big_for_offset() {
1097 let numeric = Numeric {
1098 sign: t::Sign::MAX_SELF,
1099 hours: ParsedOffsetHours::MAX_SELF,
1100 minutes: Some(ParsedOffsetMinutes::MAX_SELF),
1101 seconds: Some(ParsedOffsetSeconds::MAX_SELF),
1102 nanoseconds: Some(C(499_999_999).rinto()),
1103 };
1104 assert_eq!(numeric.to_offset().unwrap(), Offset::MAX);
1105
1106 let numeric = Numeric {
1107 sign: t::Sign::MAX_SELF,
1108 hours: ParsedOffsetHours::MAX_SELF,
1109 minutes: Some(ParsedOffsetMinutes::MAX_SELF),
1110 seconds: Some(ParsedOffsetSeconds::MAX_SELF),
1111 nanoseconds: Some(C(500_000_000).rinto()),
1112 };
1113 insta::assert_snapshot!(
1114 numeric.to_offset().unwrap_err(),
1115 @"due to precision loss, UTC offset '+25:59:59.5' is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599",
1116 );
1117 }
1118
1119 #[test]
1121 fn err_numeric_too_small_for_offset() {
1122 let numeric = Numeric {
1123 sign: t::Sign::MIN_SELF,
1124 hours: ParsedOffsetHours::MAX_SELF,
1125 minutes: Some(ParsedOffsetMinutes::MAX_SELF),
1126 seconds: Some(ParsedOffsetSeconds::MAX_SELF),
1127 nanoseconds: Some(C(499_999_999).rinto()),
1128 };
1129 assert_eq!(numeric.to_offset().unwrap(), Offset::MIN);
1130
1131 let numeric = Numeric {
1132 sign: t::Sign::MIN_SELF,
1133 hours: ParsedOffsetHours::MAX_SELF,
1134 minutes: Some(ParsedOffsetMinutes::MAX_SELF),
1135 seconds: Some(ParsedOffsetSeconds::MAX_SELF),
1136 nanoseconds: Some(C(500_000_000).rinto()),
1137 };
1138 insta::assert_snapshot!(
1139 numeric.to_offset().unwrap_err(),
1140 @"due to precision loss, UTC offset '-25:59:59.5' is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599",
1141 );
1142 }
1143}