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!(
272 f,
273 ".{}",
274 FMT.format(i32::from(nanos).unsigned_abs()).as_str()
275 )?;
276 }
277 Ok(())
278 }
279}
280
281impl core::fmt::Debug for Numeric {
284 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
285 core::fmt::Display::fmt(self, f)
286 }
287}
288
289#[derive(Debug)]
302pub(crate) struct Parser {
303 zulu: bool,
304 require_minute: bool,
305 require_second: bool,
306 subminute: bool,
307 subsecond: bool,
308 colon: Colon,
309}
310
311impl Parser {
312 pub(crate) const fn new() -> Parser {
314 Parser {
315 zulu: true,
316 require_minute: false,
317 require_second: false,
318 subminute: true,
319 subsecond: true,
320 colon: Colon::Optional,
321 }
322 }
323
324 pub(crate) const fn zulu(self, yes: bool) -> Parser {
332 Parser { zulu: yes, ..self }
333 }
334
335 pub(crate) const fn require_minute(self, yes: bool) -> Parser {
340 Parser { require_minute: yes, ..self }
341 }
342
343 pub(crate) const fn require_second(self, yes: bool) -> Parser {
350 Parser { require_second: yes, ..self }
351 }
352
353 pub(crate) const fn subminute(self, yes: bool) -> Parser {
360 Parser { subminute: yes, ..self }
361 }
362
363 pub(crate) const fn subsecond(self, yes: bool) -> Parser {
374 Parser { subsecond: yes, ..self }
375 }
376
377 pub(crate) const fn colon(self, colon: Colon) -> Parser {
381 Parser { colon, ..self }
382 }
383
384 pub(crate) fn parse<'i>(
412 &self,
413 mut input: &'i [u8],
414 ) -> Result<Parsed<'i, ParsedOffset>, Error> {
415 if input.is_empty() {
416 return Err(err!("expected UTC offset, but found end of input"));
417 }
418
419 if input[0] == b'Z' || input[0] == b'z' {
420 if !self.zulu {
421 return Err(err!(
422 "found {z:?} in {original:?} where a numeric UTC offset \
423 was expected (this context does not permit \
424 the Zulu offset)",
425 z = escape::Byte(input[0]),
426 original = escape::Bytes(input),
427 ));
428 }
429 input = &input[1..];
430 let value = ParsedOffset { kind: ParsedOffsetKind::Zulu };
431 return Ok(Parsed { value, input });
432 }
433 let Parsed { value: numeric, input } = self.parse_numeric(input)?;
434 let value = ParsedOffset { kind: ParsedOffsetKind::Numeric(numeric) };
435 Ok(Parsed { value, input })
436 }
437
438 #[cfg_attr(feature = "perf-inline", inline(always))]
444 pub(crate) fn parse_optional<'i>(
445 &self,
446 input: &'i [u8],
447 ) -> Result<Parsed<'i, Option<ParsedOffset>>, Error> {
448 let Some(first) = input.first().copied() else {
449 return Ok(Parsed { value: None, input });
450 };
451 if !matches!(first, b'z' | b'Z' | b'+' | b'-') {
452 return Ok(Parsed { value: None, input });
453 }
454 let Parsed { value, input } = self.parse(input)?;
455 Ok(Parsed { value: Some(value), input })
456 }
457
458 #[cfg_attr(feature = "perf-inline", inline(always))]
463 fn parse_numeric<'i>(
464 &self,
465 input: &'i [u8],
466 ) -> Result<Parsed<'i, Numeric>, Error> {
467 let original = escape::Bytes(input);
468
469 let Parsed { value: sign, input } =
471 self.parse_sign(input).with_context(|| {
472 err!("failed to parse sign in UTC numeric offset {original:?}")
473 })?;
474
475 let Parsed { value: hours, input } =
477 self.parse_hours(input).with_context(|| {
478 err!(
479 "failed to parse hours in UTC numeric offset {original:?}"
480 )
481 })?;
482 let extended = match self.colon {
483 Colon::Optional => input.starts_with(b":"),
484 Colon::Required => {
485 if !input.is_empty() && !input.starts_with(b":") {
486 return Err(err!(
487 "parsed hour component of time zone offset from \
488 {original:?}, but could not find required colon \
489 separator",
490 ));
491 }
492 true
493 }
494 Colon::Absent => {
495 if !input.is_empty() && input.starts_with(b":") {
496 return Err(err!(
497 "parsed hour component of time zone offset from \
498 {original:?}, but found colon after hours which \
499 is not allowed",
500 ));
501 }
502 false
503 }
504 };
505
506 let mut numeric = Numeric {
508 sign,
509 hours,
510 minutes: None,
511 seconds: None,
512 nanoseconds: None,
513 };
514
515 let Parsed { value: has_minutes, input } =
517 self.parse_separator(input, extended).with_context(|| {
518 err!(
519 "failed to parse separator after hours in \
520 UTC numeric offset {original:?}"
521 )
522 })?;
523 if !has_minutes {
524 if self.require_minute || (self.subminute && self.require_second) {
525 return Err(err!(
526 "parsed hour component of time zone offset from \
527 {original:?}, but could not find required minute \
528 component",
529 ));
530 }
531 return Ok(Parsed { value: numeric, input });
532 }
533
534 let Parsed { value: minutes, input } =
536 self.parse_minutes(input).with_context(|| {
537 err!(
538 "failed to parse minutes in UTC numeric offset \
539 {original:?}"
540 )
541 })?;
542 numeric.minutes = Some(minutes);
543
544 if !self.subminute {
546 if input.get(0).map_or(false, |&b| b == b':') {
553 return Err(err!(
554 "subminute precision for UTC numeric offset {original:?} \
555 is not enabled in this context (must provide only \
556 integral minutes)",
557 ));
558 }
559 return Ok(Parsed { value: numeric, input });
560 }
561
562 let Parsed { value: has_seconds, input } =
564 self.parse_separator(input, extended).with_context(|| {
565 err!(
566 "failed to parse separator after minutes in \
567 UTC numeric offset {original:?}"
568 )
569 })?;
570 if !has_seconds {
571 if self.require_second {
572 return Err(err!(
573 "parsed hour and minute components of time zone offset \
574 from {original:?}, but could not find required second \
575 component",
576 ));
577 }
578 return Ok(Parsed { value: numeric, input });
579 }
580
581 let Parsed { value: seconds, input } =
583 self.parse_seconds(input).with_context(|| {
584 err!(
585 "failed to parse seconds in UTC numeric offset \
586 {original:?}"
587 )
588 })?;
589 numeric.seconds = Some(seconds);
590
591 if !self.subsecond {
593 if input.get(0).map_or(false, |&b| b == b'.' || b == b',') {
594 return Err(err!(
595 "subsecond precision for UTC numeric offset {original:?} \
596 is not enabled in this context (must provide only \
597 integral minutes or seconds)",
598 ));
599 }
600 return Ok(Parsed { value: numeric, input });
601 }
602
603 let Parsed { value: nanoseconds, input } =
605 parse_temporal_fraction(input).with_context(|| {
606 err!(
607 "failed to parse fractional nanoseconds in \
608 UTC numeric offset {original:?}",
609 )
610 })?;
611 numeric.nanoseconds =
613 nanoseconds.map(|n| t::SubsecNanosecond::new(n).unwrap());
614 Ok(Parsed { value: numeric, input })
615 }
616
617 #[cfg_attr(feature = "perf-inline", inline(always))]
618 fn parse_sign<'i>(
619 &self,
620 input: &'i [u8],
621 ) -> Result<Parsed<'i, t::Sign>, Error> {
622 let sign = input.get(0).copied().ok_or_else(|| {
623 err!("expected UTC numeric offset, but found end of input")
624 })?;
625 let sign = if sign == b'+' {
626 t::Sign::N::<1>()
627 } else if sign == b'-' {
628 t::Sign::N::<-1>()
629 } else {
630 return Err(err!(
631 "expected '+' or '-' sign at start of UTC numeric offset, \
632 but found {found:?} instead",
633 found = escape::Byte(sign),
634 ));
635 };
636 Ok(Parsed { value: sign, input: &input[1..] })
637 }
638
639 #[cfg_attr(feature = "perf-inline", inline(always))]
640 fn parse_hours<'i>(
641 &self,
642 input: &'i [u8],
643 ) -> Result<Parsed<'i, ParsedOffsetHours>, Error> {
644 let (hours, input) = parse::split(input, 2).ok_or_else(|| {
645 err!("expected two digit hour after sign, but found end of input",)
646 })?;
647 let hours = parse::i64(hours).with_context(|| {
648 err!(
649 "failed to parse {hours:?} as hours (a two digit integer)",
650 hours = escape::Bytes(hours),
651 )
652 })?;
653 let hours = ParsedOffsetHours::try_new("hours", hours)
659 .context("offset hours are not valid")?;
660 Ok(Parsed { value: hours, input })
661 }
662
663 #[cfg_attr(feature = "perf-inline", inline(always))]
664 fn parse_minutes<'i>(
665 &self,
666 input: &'i [u8],
667 ) -> Result<Parsed<'i, ParsedOffsetMinutes>, Error> {
668 let (minutes, input) = parse::split(input, 2).ok_or_else(|| {
669 err!(
670 "expected two digit minute after hours, \
671 but found end of input",
672 )
673 })?;
674 let minutes = parse::i64(minutes).with_context(|| {
675 err!(
676 "failed to parse {minutes:?} as minutes (a two digit integer)",
677 minutes = escape::Bytes(minutes),
678 )
679 })?;
680 let minutes = ParsedOffsetMinutes::try_new("minutes", minutes)
681 .context("minutes are not valid")?;
682 Ok(Parsed { value: minutes, input })
683 }
684
685 #[cfg_attr(feature = "perf-inline", inline(always))]
686 fn parse_seconds<'i>(
687 &self,
688 input: &'i [u8],
689 ) -> Result<Parsed<'i, ParsedOffsetSeconds>, Error> {
690 let (seconds, input) = parse::split(input, 2).ok_or_else(|| {
691 err!(
692 "expected two digit second after hours, \
693 but found end of input",
694 )
695 })?;
696 let seconds = parse::i64(seconds).with_context(|| {
697 err!(
698 "failed to parse {seconds:?} as seconds (a two digit integer)",
699 seconds = escape::Bytes(seconds),
700 )
701 })?;
702 let seconds = ParsedOffsetSeconds::try_new("seconds", seconds)
703 .context("time zone offset seconds are not valid")?;
704 Ok(Parsed { value: seconds, input })
705 }
706
707 #[cfg_attr(feature = "perf-inline", inline(always))]
718 fn parse_separator<'i>(
719 &self,
720 mut input: &'i [u8],
721 extended: bool,
722 ) -> Result<Parsed<'i, bool>, Error> {
723 if !extended {
724 let expected =
725 input.len() >= 2 && input[..2].iter().all(u8::is_ascii_digit);
726 return Ok(Parsed { value: expected, input });
727 }
728 let is_separator = input.get(0).map_or(false, |&b| b == b':');
729 if is_separator {
730 input = &input[1..];
731 }
732 Ok(Parsed { value: is_separator, input })
733 }
734}
735
736#[derive(Debug)]
738pub(crate) enum Colon {
739 Optional,
742 Required,
744 Absent,
746}
747
748#[cfg(test)]
749mod tests {
750 use crate::util::rangeint::RInto;
751
752 use super::*;
753
754 #[test]
755 fn ok_zulu() {
756 let p = |input| Parser::new().parse(input).unwrap();
757
758 insta::assert_debug_snapshot!(p(b"Z"), @r###"
759 Parsed {
760 value: ParsedOffset {
761 kind: Zulu,
762 },
763 input: "",
764 }
765 "###);
766 insta::assert_debug_snapshot!(p(b"z"), @r###"
767 Parsed {
768 value: ParsedOffset {
769 kind: Zulu,
770 },
771 input: "",
772 }
773 "###);
774 }
775
776 #[test]
777 fn ok_numeric() {
778 let p = |input| Parser::new().parse(input).unwrap();
779
780 insta::assert_debug_snapshot!(p(b"-05"), @r###"
781 Parsed {
782 value: ParsedOffset {
783 kind: Numeric(
784 -05,
785 ),
786 },
787 input: "",
788 }
789 "###);
790 }
791
792 #[test]
794 fn ok_numeric_complete() {
795 let p = |input| Parser::new().parse_numeric(input).unwrap();
796
797 insta::assert_debug_snapshot!(p(b"-05"), @r###"
798 Parsed {
799 value: -05,
800 input: "",
801 }
802 "###);
803 insta::assert_debug_snapshot!(p(b"+05"), @r###"
804 Parsed {
805 value: +05,
806 input: "",
807 }
808 "###);
809
810 insta::assert_debug_snapshot!(p(b"+25:59"), @r###"
811 Parsed {
812 value: +25:59,
813 input: "",
814 }
815 "###);
816 insta::assert_debug_snapshot!(p(b"+2559"), @r###"
817 Parsed {
818 value: +25:59,
819 input: "",
820 }
821 "###);
822
823 insta::assert_debug_snapshot!(p(b"+25:59:59"), @r###"
824 Parsed {
825 value: +25:59:59,
826 input: "",
827 }
828 "###);
829 insta::assert_debug_snapshot!(p(b"+255959"), @r###"
830 Parsed {
831 value: +25:59:59,
832 input: "",
833 }
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"+25:59:59,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 insta::assert_debug_snapshot!(p(b"+255959,999"), @r###"
855 Parsed {
856 value: +25:59:59.999,
857 input: "",
858 }
859 "###);
860
861 insta::assert_debug_snapshot!(p(b"+25:59:59.999999999"), @r###"
862 Parsed {
863 value: +25:59:59.999999999,
864 input: "",
865 }
866 "###);
867 }
868
869 #[test]
872 fn ok_numeric_incomplete() {
873 let p = |input| Parser::new().parse_numeric(input).unwrap();
874
875 insta::assert_debug_snapshot!(p(b"-05a"), @r###"
876 Parsed {
877 value: -05,
878 input: "a",
879 }
880 "###);
881 insta::assert_debug_snapshot!(p(b"-05:12a"), @r###"
882 Parsed {
883 value: -05:12,
884 input: "a",
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"-05:12,"), @r###"
894 Parsed {
895 value: -05:12,
896 input: ",",
897 }
898 "###);
899 insta::assert_debug_snapshot!(p(b"-0512a"), @r###"
900 Parsed {
901 value: -05:12,
902 input: "a",
903 }
904 "###);
905 insta::assert_debug_snapshot!(p(b"-0512:"), @r###"
906 Parsed {
907 value: -05:12,
908 input: ":",
909 }
910 "###);
911 insta::assert_debug_snapshot!(p(b"-05:12:34a"), @r###"
912 Parsed {
913 value: -05:12:34,
914 input: "a",
915 }
916 "###);
917 insta::assert_debug_snapshot!(p(b"-05:12:34.9a"), @r###"
918 Parsed {
919 value: -05:12:34.9,
920 input: "a",
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 insta::assert_debug_snapshot!(p(b"-05:12:34.9,"), @r###"
930 Parsed {
931 value: -05:12:34.9,
932 input: ",",
933 }
934 "###);
935 }
936
937 #[test]
941 fn err_numeric_empty() {
942 insta::assert_snapshot!(
943 Parser::new().parse_numeric(b"").unwrap_err(),
944 @r###"failed to parse sign in UTC numeric offset "": expected UTC numeric offset, but found end of input"###,
945 );
946 }
947
948 #[test]
950 fn err_numeric_notsign() {
951 insta::assert_snapshot!(
952 Parser::new().parse_numeric(b"*").unwrap_err(),
953 @r###"failed to parse sign in UTC numeric offset "*": expected '+' or '-' sign at start of UTC numeric offset, but found "*" instead"###,
954 );
955 }
956
957 #[test]
959 fn err_numeric_hours_too_short() {
960 insta::assert_snapshot!(
961 Parser::new().parse_numeric(b"+a").unwrap_err(),
962 @r###"failed to parse hours in UTC numeric offset "+a": expected two digit hour after sign, but found end of input"###,
963 );
964 }
965
966 #[test]
968 fn err_numeric_hours_invalid_digits() {
969 insta::assert_snapshot!(
970 Parser::new().parse_numeric(b"+ab").unwrap_err(),
971 @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"###,
972 );
973 }
974
975 #[test]
977 fn err_numeric_hours_out_of_range() {
978 insta::assert_snapshot!(
979 Parser::new().parse_numeric(b"-26").unwrap_err(),
980 @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"###,
981 );
982 }
983
984 #[test]
986 fn err_numeric_minutes_too_short() {
987 insta::assert_snapshot!(
988 Parser::new().parse_numeric(b"+05:a").unwrap_err(),
989 @r###"failed to parse minutes in UTC numeric offset "+05:a": expected two digit minute after hours, but found end of input"###,
990 );
991 }
992
993 #[test]
995 fn err_numeric_minutes_invalid_digits() {
996 insta::assert_snapshot!(
997 Parser::new().parse_numeric(b"+05:ab").unwrap_err(),
998 @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"###,
999 );
1000 }
1001
1002 #[test]
1004 fn err_numeric_minutes_out_of_range() {
1005 insta::assert_snapshot!(
1006 Parser::new().parse_numeric(b"-05:60").unwrap_err(),
1007 @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"###,
1008 );
1009 }
1010
1011 #[test]
1013 fn err_numeric_seconds_too_short() {
1014 insta::assert_snapshot!(
1015 Parser::new().parse_numeric(b"+05:30:a").unwrap_err(),
1016 @r###"failed to parse seconds in UTC numeric offset "+05:30:a": expected two digit second after hours, but found end of input"###,
1017 );
1018 }
1019
1020 #[test]
1022 fn err_numeric_seconds_invalid_digits() {
1023 insta::assert_snapshot!(
1024 Parser::new().parse_numeric(b"+05:30:ab").unwrap_err(),
1025 @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"###,
1026 );
1027 }
1028
1029 #[test]
1031 fn err_numeric_seconds_out_of_range() {
1032 insta::assert_snapshot!(
1033 Parser::new().parse_numeric(b"-05:30:60").unwrap_err(),
1034 @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"###,
1035 );
1036 }
1037
1038 #[test]
1041 fn err_numeric_fraction_non_empty() {
1042 insta::assert_snapshot!(
1043 Parser::new().parse_numeric(b"-05:30:44.").unwrap_err(),
1044 @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"###,
1045 );
1046 insta::assert_snapshot!(
1047 Parser::new().parse_numeric(b"-05:30:44,").unwrap_err(),
1048 @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"###,
1049 );
1050
1051 insta::assert_snapshot!(
1053 Parser::new().parse_numeric(b"-05:30:44.a").unwrap_err(),
1054 @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"###,
1055 );
1056 insta::assert_snapshot!(
1057 Parser::new().parse_numeric(b"-05:30:44,a").unwrap_err(),
1058 @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"###,
1059 );
1060
1061 insta::assert_snapshot!(
1063 Parser::new().parse_numeric(b"-053044.a").unwrap_err(),
1064 @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"###,
1065 );
1066 insta::assert_snapshot!(
1067 Parser::new().parse_numeric(b"-053044,a").unwrap_err(),
1068 @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"###,
1069 );
1070 }
1071
1072 #[test]
1076 fn err_numeric_subminute_disabled_but_desired() {
1077 insta::assert_snapshot!(
1078 Parser::new().subminute(false).parse_numeric(b"-05:59:32").unwrap_err(),
1079 @r###"subminute precision for UTC numeric offset "-05:59:32" is not enabled in this context (must provide only integral minutes)"###,
1080 );
1081 }
1082
1083 #[test]
1086 fn err_zulu_disabled_but_desired() {
1087 insta::assert_snapshot!(
1088 Parser::new().zulu(false).parse(b"Z").unwrap_err(),
1089 @r###"found "Z" in "Z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###,
1090 );
1091 insta::assert_snapshot!(
1092 Parser::new().zulu(false).parse(b"z").unwrap_err(),
1093 @r###"found "z" in "z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###,
1094 );
1095 }
1096
1097 #[test]
1102 fn err_numeric_too_big_for_offset() {
1103 let numeric = Numeric {
1104 sign: t::Sign::MAX_SELF,
1105 hours: ParsedOffsetHours::MAX_SELF,
1106 minutes: Some(ParsedOffsetMinutes::MAX_SELF),
1107 seconds: Some(ParsedOffsetSeconds::MAX_SELF),
1108 nanoseconds: Some(C(499_999_999).rinto()),
1109 };
1110 assert_eq!(numeric.to_offset().unwrap(), Offset::MAX);
1111
1112 let numeric = Numeric {
1113 sign: t::Sign::MAX_SELF,
1114 hours: ParsedOffsetHours::MAX_SELF,
1115 minutes: Some(ParsedOffsetMinutes::MAX_SELF),
1116 seconds: Some(ParsedOffsetSeconds::MAX_SELF),
1117 nanoseconds: Some(C(500_000_000).rinto()),
1118 };
1119 insta::assert_snapshot!(
1120 numeric.to_offset().unwrap_err(),
1121 @"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",
1122 );
1123 }
1124
1125 #[test]
1127 fn err_numeric_too_small_for_offset() {
1128 let numeric = Numeric {
1129 sign: t::Sign::MIN_SELF,
1130 hours: ParsedOffsetHours::MAX_SELF,
1131 minutes: Some(ParsedOffsetMinutes::MAX_SELF),
1132 seconds: Some(ParsedOffsetSeconds::MAX_SELF),
1133 nanoseconds: Some(C(499_999_999).rinto()),
1134 };
1135 assert_eq!(numeric.to_offset().unwrap(), Offset::MIN);
1136
1137 let numeric = Numeric {
1138 sign: t::Sign::MIN_SELF,
1139 hours: ParsedOffsetHours::MAX_SELF,
1140 minutes: Some(ParsedOffsetMinutes::MAX_SELF),
1141 seconds: Some(ParsedOffsetSeconds::MAX_SELF),
1142 nanoseconds: Some(C(500_000_000).rinto()),
1143 };
1144 insta::assert_snapshot!(
1145 numeric.to_offset().unwrap_err(),
1146 @"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",
1147 );
1148 }
1149}