1use crate::{
105 error::{fmt::offset::Error as E, Error, ErrorContext},
106 fmt::{
107 buffer::ArrayBuffer,
108 temporal::{PiecesNumericOffset, PiecesOffset},
109 util::parse_temporal_fraction,
110 Parsed,
111 },
112 tz::Offset,
113 util::{b, parse},
114};
115
116#[derive(Debug)]
122pub(crate) struct ParsedOffset {
123 kind: ParsedOffsetKind,
125}
126
127impl ParsedOffset {
128 pub(crate) fn to_offset(&self) -> Result<Offset, Error> {
142 match self.kind {
143 ParsedOffsetKind::Zulu => Ok(Offset::UTC),
144 ParsedOffsetKind::Numeric(ref numeric) => numeric.to_offset(),
145 }
146 }
147
148 pub(crate) fn to_pieces_offset(&self) -> Result<PiecesOffset, Error> {
154 match self.kind {
155 ParsedOffsetKind::Zulu => Ok(PiecesOffset::Zulu),
156 ParsedOffsetKind::Numeric(ref numeric) => {
157 let mut off = PiecesNumericOffset::from(numeric.to_offset()?);
158 if numeric.sign.is_negative() {
159 off = off.with_negative_zero();
160 }
161 Ok(PiecesOffset::from(off))
162 }
163 }
164 }
165
166 pub(crate) fn is_zulu(&self) -> bool {
172 matches!(self.kind, ParsedOffsetKind::Zulu)
173 }
174
175 pub(crate) fn has_subminute(&self) -> bool {
177 let ParsedOffsetKind::Numeric(ref numeric) = self.kind else {
178 return false;
179 };
180 numeric.seconds.is_some()
181 }
182}
183
184impl core::fmt::Display for ParsedOffset {
185 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
186 match self.kind {
187 ParsedOffsetKind::Zulu => f.write_str("Z"),
188 ParsedOffsetKind::Numeric(ref numeric) => {
189 core::fmt::Display::fmt(numeric, f)
190 }
191 }
192 }
193}
194
195#[derive(Debug)]
197enum ParsedOffsetKind {
198 Zulu,
201 Numeric(Numeric),
203}
204
205struct Numeric {
207 sign: b::Sign,
210 hours: i8,
213 minutes: Option<i8>,
215 seconds: Option<i8>,
218 nanoseconds: Option<i32>,
221}
222
223impl Numeric {
224 fn to_offset(&self) -> Result<Offset, Error> {
230 let mut seconds = i32::from(self.hours) * b::SECS_PER_HOUR_32;
231 if let Some(part_minutes) = self.minutes {
232 seconds += i32::from(part_minutes) * b::SECS_PER_MIN_32;
233 }
234 if let Some(part_seconds) = self.seconds {
235 seconds += i32::from(part_seconds);
236 }
237 if let Some(part_nanoseconds) = self.nanoseconds {
238 if part_nanoseconds >= 500_000_000 {
239 seconds += 1;
240 }
241 }
242 Ok(Offset::from_seconds(self.sign * seconds)
246 .map_err(|_| E::PrecisionLoss)?)
247 }
248}
249
250impl core::fmt::Display for Numeric {
253 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
254 let mut buf = ArrayBuffer::<19>::default();
255 let mut bbuf = buf.as_borrowed();
256
257 bbuf.write_ascii_char(if self.sign.is_negative() {
258 b'-'
259 } else {
260 b'+'
261 });
262 bbuf.write_int_pad2(self.hours.unsigned_abs());
263 if let Some(minutes) = self.minutes {
264 bbuf.write_ascii_char(b':');
265 bbuf.write_int_pad2(minutes.unsigned_abs());
266 }
267 if let Some(seconds) = self.seconds {
268 if self.minutes.is_none() {
269 bbuf.write_str(":00");
270 }
271 bbuf.write_ascii_char(b':');
272 bbuf.write_int_pad2(seconds.unsigned_abs());
273 }
274 if let Some(nanos) = self.nanoseconds {
275 if nanos != 0 {
276 if self.minutes.is_none() {
277 bbuf.write_str(":00");
278 }
279 if self.seconds.is_none() {
280 bbuf.write_str(":00");
281 }
282 bbuf.write_ascii_char(b'.');
283 bbuf.write_fraction(None, nanos.unsigned_abs());
284 }
285 }
286 f.write_str(bbuf.filled())
287 }
288}
289
290impl core::fmt::Debug for Numeric {
293 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
294 core::fmt::Display::fmt(self, f)
295 }
296}
297
298#[derive(Debug)]
311pub(crate) struct Parser {
312 zulu: bool,
313 require_minute: bool,
314 require_second: bool,
315 subminute: bool,
316 subsecond: bool,
317 colon: Colon,
318}
319
320impl Parser {
321 pub(crate) const fn new() -> Parser {
323 Parser {
324 zulu: true,
325 require_minute: false,
326 require_second: false,
327 subminute: true,
328 subsecond: true,
329 colon: Colon::Optional,
330 }
331 }
332
333 pub(crate) const fn zulu(self, yes: bool) -> Parser {
341 Parser { zulu: yes, ..self }
342 }
343
344 pub(crate) const fn require_minute(self, yes: bool) -> Parser {
349 Parser { require_minute: yes, ..self }
350 }
351
352 pub(crate) const fn require_second(self, yes: bool) -> Parser {
359 Parser { require_second: yes, ..self }
360 }
361
362 pub(crate) const fn subminute(self, yes: bool) -> Parser {
369 Parser { subminute: yes, ..self }
370 }
371
372 pub(crate) const fn subsecond(self, yes: bool) -> Parser {
383 Parser { subsecond: yes, ..self }
384 }
385
386 pub(crate) const fn colon(self, colon: Colon) -> Parser {
390 Parser { colon, ..self }
391 }
392
393 pub(crate) fn parse<'i>(
421 &self,
422 mut input: &'i [u8],
423 ) -> Result<Parsed<'i, ParsedOffset>, Error> {
424 if input.is_empty() {
425 return Err(Error::from(E::EndOfInput));
426 }
427
428 if input[0] == b'Z' || input[0] == b'z' {
429 if !self.zulu {
430 return Err(Error::from(E::UnexpectedLetterOffsetNoZulu(
431 input[0],
432 )));
433 }
434 input = &input[1..];
435 let value = ParsedOffset { kind: ParsedOffsetKind::Zulu };
436 return Ok(Parsed { value, input });
437 }
438 let Parsed { value: numeric, input } = self.parse_numeric(input)?;
439 let value = ParsedOffset { kind: ParsedOffsetKind::Numeric(numeric) };
440 Ok(Parsed { value, input })
441 }
442
443 #[cfg_attr(feature = "perf-inline", inline(always))]
449 pub(crate) fn parse_optional<'i>(
450 &self,
451 input: &'i [u8],
452 ) -> Result<Parsed<'i, Option<ParsedOffset>>, Error> {
453 let Some(first) = input.first().copied() else {
454 return Ok(Parsed { value: None, input });
455 };
456 if !matches!(first, b'z' | b'Z' | b'+' | b'-') {
457 return Ok(Parsed { value: None, input });
458 }
459 let Parsed { value, input } = self.parse(input)?;
460 Ok(Parsed { value: Some(value), input })
461 }
462
463 #[cfg_attr(feature = "perf-inline", inline(always))]
468 fn parse_numeric<'i>(
469 &self,
470 input: &'i [u8],
471 ) -> Result<Parsed<'i, Numeric>, Error> {
472 let Parsed { value: sign, input } =
474 self.parse_sign(input).context(E::InvalidSign)?;
475
476 let Parsed { value: hours, input } =
478 self.parse_hours(input).context(E::InvalidHours)?;
479 let extended = match self.colon {
480 Colon::Optional => input.starts_with(b":"),
481 Colon::Required => {
482 if !input.is_empty() && !input.starts_with(b":") {
483 return Err(Error::from(E::NoColonAfterHours));
484 }
485 true
486 }
487 Colon::Absent => {
488 if !input.is_empty() && input.starts_with(b":") {
489 return Err(Error::from(E::ColonAfterHours));
490 }
491 false
492 }
493 };
494
495 let mut numeric = Numeric {
497 sign,
498 hours,
499 minutes: None,
500 seconds: None,
501 nanoseconds: None,
502 };
503
504 let Parsed { value: has_minutes, input } = self
506 .parse_separator(input, extended)
507 .context(E::SeparatorAfterHours)?;
508 if !has_minutes {
509 return if self.require_minute
510 || (self.subminute && self.require_second)
511 {
512 Err(Error::from(E::MissingMinuteAfterHour))
513 } else {
514 Ok(Parsed { value: numeric, input })
515 };
516 }
517
518 let Parsed { value: minutes, input } =
520 self.parse_minutes(input).context(E::InvalidMinutes)?;
521 numeric.minutes = Some(minutes);
522
523 if !self.subminute {
525 return if input.get(0).map_or(false, |&b| b == b':') {
532 Err(Error::from(E::SubminutePrecisionNotEnabled))
533 } else {
534 Ok(Parsed { value: numeric, input })
535 };
536 }
537
538 let Parsed { value: has_seconds, input } = self
540 .parse_separator(input, extended)
541 .context(E::SeparatorAfterMinutes)?;
542 if !has_seconds {
543 return if self.require_second {
544 Err(Error::from(E::MissingSecondAfterMinute))
545 } else {
546 Ok(Parsed { value: numeric, input })
547 };
548 }
549
550 let Parsed { value: seconds, input } =
552 self.parse_seconds(input).context(E::InvalidSeconds)?;
553 numeric.seconds = Some(seconds);
554
555 if !self.subsecond {
557 if input.get(0).map_or(false, |&b| b == b'.' || b == b',') {
558 return Err(Error::from(E::SubsecondPrecisionNotEnabled));
559 }
560 return Ok(Parsed { value: numeric, input });
561 }
562
563 let Parsed { value: nanoseconds, input } =
565 parse_temporal_fraction(input)
566 .context(E::InvalidSecondsFractional)?;
567 numeric.nanoseconds = nanoseconds.map(|n| i32::try_from(n).unwrap());
569 Ok(Parsed { value: numeric, input })
570 }
571
572 #[cfg_attr(feature = "perf-inline", inline(always))]
573 fn parse_sign<'i>(
574 &self,
575 input: &'i [u8],
576 ) -> Result<Parsed<'i, b::Sign>, Error> {
577 let sign = input.get(0).copied().ok_or(E::EndOfInputNumeric)?;
578 let sign = if sign == b'+' {
579 b::Sign::Positive
580 } else if sign == b'-' {
581 b::Sign::Negative
582 } else {
583 return Err(Error::from(E::InvalidSignPlusOrMinus));
584 };
585 Ok(Parsed { value: sign, input: &input[1..] })
586 }
587
588 #[cfg_attr(feature = "perf-inline", inline(always))]
589 fn parse_hours<'i>(
590 &self,
591 input: &'i [u8],
592 ) -> Result<Parsed<'i, i8>, Error> {
593 let (hours, input) =
594 parse::split(input, 2).ok_or(E::EndOfInputHour)?;
595 let hours = b::OffsetHours::parse(hours).context(E::ParseHours)?;
596 Ok(Parsed { value: hours, input })
597 }
598
599 #[cfg_attr(feature = "perf-inline", inline(always))]
600 fn parse_minutes<'i>(
601 &self,
602 input: &'i [u8],
603 ) -> Result<Parsed<'i, i8>, Error> {
604 let (minutes, input) =
605 parse::split(input, 2).ok_or(E::EndOfInputMinute)?;
606 let minutes =
607 b::OffsetMinutes::parse(minutes).context(E::ParseMinutes)?;
608 Ok(Parsed { value: minutes, input })
609 }
610
611 #[cfg_attr(feature = "perf-inline", inline(always))]
612 fn parse_seconds<'i>(
613 &self,
614 input: &'i [u8],
615 ) -> Result<Parsed<'i, i8>, Error> {
616 let (seconds, input) =
617 parse::split(input, 2).ok_or(E::EndOfInputSecond)?;
618 let seconds =
619 b::OffsetSeconds::parse(seconds).context(E::ParseSeconds)?;
620 Ok(Parsed { value: seconds, input })
621 }
622
623 #[cfg_attr(feature = "perf-inline", inline(always))]
634 fn parse_separator<'i>(
635 &self,
636 mut input: &'i [u8],
637 extended: bool,
638 ) -> Result<Parsed<'i, bool>, Error> {
639 if !extended {
640 let expected =
641 input.len() >= 2 && input[..2].iter().all(u8::is_ascii_digit);
642 return Ok(Parsed { value: expected, input });
643 }
644 let is_separator = input.get(0).map_or(false, |&b| b == b':');
645 if is_separator {
646 input = &input[1..];
647 }
648 Ok(Parsed { value: is_separator, input })
649 }
650}
651
652#[derive(Debug)]
654pub(crate) enum Colon {
655 Optional,
658 Required,
660 Absent,
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667
668 #[test]
669 fn ok_zulu() {
670 let p = |input| Parser::new().parse(input).unwrap();
671
672 insta::assert_debug_snapshot!(p(b"Z"), @r###"
673 Parsed {
674 value: ParsedOffset {
675 kind: Zulu,
676 },
677 input: "",
678 }
679 "###);
680 insta::assert_debug_snapshot!(p(b"z"), @r###"
681 Parsed {
682 value: ParsedOffset {
683 kind: Zulu,
684 },
685 input: "",
686 }
687 "###);
688 }
689
690 #[test]
691 fn ok_numeric() {
692 let p = |input| Parser::new().parse(input).unwrap();
693
694 insta::assert_debug_snapshot!(p(b"-05"), @r###"
695 Parsed {
696 value: ParsedOffset {
697 kind: Numeric(
698 -05,
699 ),
700 },
701 input: "",
702 }
703 "###);
704 }
705
706 #[test]
708 fn ok_numeric_complete() {
709 let p = |input| Parser::new().parse_numeric(input).unwrap();
710
711 insta::assert_debug_snapshot!(p(b"-05"), @r###"
712 Parsed {
713 value: -05,
714 input: "",
715 }
716 "###);
717 insta::assert_debug_snapshot!(p(b"+05"), @r###"
718 Parsed {
719 value: +05,
720 input: "",
721 }
722 "###);
723
724 insta::assert_debug_snapshot!(p(b"+25:59"), @r###"
725 Parsed {
726 value: +25:59,
727 input: "",
728 }
729 "###);
730 insta::assert_debug_snapshot!(p(b"+2559"), @r###"
731 Parsed {
732 value: +25:59,
733 input: "",
734 }
735 "###);
736
737 insta::assert_debug_snapshot!(p(b"+25:59:59"), @r###"
738 Parsed {
739 value: +25:59:59,
740 input: "",
741 }
742 "###);
743 insta::assert_debug_snapshot!(p(b"+255959"), @r###"
744 Parsed {
745 value: +25:59:59,
746 input: "",
747 }
748 "###);
749
750 insta::assert_debug_snapshot!(p(b"+25:59:59.999"), @r###"
751 Parsed {
752 value: +25:59:59.999,
753 input: "",
754 }
755 "###);
756 insta::assert_debug_snapshot!(p(b"+25:59:59,999"), @r###"
757 Parsed {
758 value: +25:59:59.999,
759 input: "",
760 }
761 "###);
762 insta::assert_debug_snapshot!(p(b"+255959.999"), @r###"
763 Parsed {
764 value: +25:59:59.999,
765 input: "",
766 }
767 "###);
768 insta::assert_debug_snapshot!(p(b"+255959,999"), @r###"
769 Parsed {
770 value: +25:59:59.999,
771 input: "",
772 }
773 "###);
774
775 insta::assert_debug_snapshot!(p(b"+25:59:59.999999999"), @r###"
776 Parsed {
777 value: +25:59:59.999999999,
778 input: "",
779 }
780 "###);
781 }
782
783 #[test]
786 fn ok_numeric_incomplete() {
787 let p = |input| Parser::new().parse_numeric(input).unwrap();
788
789 insta::assert_debug_snapshot!(p(b"-05a"), @r###"
790 Parsed {
791 value: -05,
792 input: "a",
793 }
794 "###);
795 insta::assert_debug_snapshot!(p(b"-05:12a"), @r###"
796 Parsed {
797 value: -05:12,
798 input: "a",
799 }
800 "###);
801 insta::assert_debug_snapshot!(p(b"-05:12."), @r###"
802 Parsed {
803 value: -05:12,
804 input: ".",
805 }
806 "###);
807 insta::assert_debug_snapshot!(p(b"-05:12,"), @r###"
808 Parsed {
809 value: -05:12,
810 input: ",",
811 }
812 "###);
813 insta::assert_debug_snapshot!(p(b"-0512a"), @r###"
814 Parsed {
815 value: -05:12,
816 input: "a",
817 }
818 "###);
819 insta::assert_debug_snapshot!(p(b"-0512:"), @r###"
820 Parsed {
821 value: -05:12,
822 input: ":",
823 }
824 "###);
825 insta::assert_debug_snapshot!(p(b"-05:12:34a"), @r###"
826 Parsed {
827 value: -05:12:34,
828 input: "a",
829 }
830 "###);
831 insta::assert_debug_snapshot!(p(b"-05:12:34.9a"), @r###"
832 Parsed {
833 value: -05:12:34.9,
834 input: "a",
835 }
836 "###);
837 insta::assert_debug_snapshot!(p(b"-05:12:34.9."), @r###"
838 Parsed {
839 value: -05:12:34.9,
840 input: ".",
841 }
842 "###);
843 insta::assert_debug_snapshot!(p(b"-05:12:34.9,"), @r###"
844 Parsed {
845 value: -05:12:34.9,
846 input: ",",
847 }
848 "###);
849 }
850
851 #[test]
855 fn err_numeric_empty() {
856 insta::assert_snapshot!(
857 Parser::new().parse_numeric(b"").unwrap_err(),
858 @"failed to parse sign in UTC numeric offset: expected UTC numeric offset, but found end of input",
859 );
860 }
861
862 #[test]
864 fn err_numeric_notsign() {
865 insta::assert_snapshot!(
866 Parser::new().parse_numeric(b"*").unwrap_err(),
867 @"failed to parse sign in UTC numeric offset: expected `+` or `-` sign at start of UTC numeric offset",
868 );
869 }
870
871 #[test]
873 fn err_numeric_hours_too_short() {
874 insta::assert_snapshot!(
875 Parser::new().parse_numeric(b"+a").unwrap_err(),
876 @"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input",
877 );
878 }
879
880 #[test]
882 fn err_numeric_hours_invalid_digits() {
883 insta::assert_snapshot!(
884 Parser::new().parse_numeric(b"+ab").unwrap_err(),
885 @"failed to parse hours in UTC numeric offset: failed to parse hours (requires a two digit integer): invalid digit, expected 0-9 but got a",
886 );
887 }
888
889 #[test]
891 fn err_numeric_hours_out_of_range() {
892 insta::assert_snapshot!(
893 Parser::new().parse_numeric(b"-26").unwrap_err(),
894 @"failed to parse hours in UTC numeric offset: failed to parse hours (requires a two digit integer): parameter 'time zone offset hours' is not in the required range of -25..=25",
895 );
896 }
897
898 #[test]
900 fn err_numeric_minutes_too_short() {
901 insta::assert_snapshot!(
902 Parser::new().parse_numeric(b"+05:a").unwrap_err(),
903 @"failed to parse minutes in UTC numeric offset: expected two digit minute after hours, but found end of input",
904 );
905 }
906
907 #[test]
909 fn err_numeric_minutes_invalid_digits() {
910 insta::assert_snapshot!(
911 Parser::new().parse_numeric(b"+05:ab").unwrap_err(),
912 @"failed to parse minutes in UTC numeric offset: failed to parse minutes (requires a two digit integer): invalid digit, expected 0-9 but got a",
913 );
914 }
915
916 #[test]
918 fn err_numeric_minutes_out_of_range() {
919 insta::assert_snapshot!(
920 Parser::new().parse_numeric(b"-05:60").unwrap_err(),
921 @"failed to parse minutes in UTC numeric offset: failed to parse minutes (requires a two digit integer): parameter 'time zone offset minutes' is not in the required range of -59..=59",
922 );
923 }
924
925 #[test]
927 fn err_numeric_seconds_too_short() {
928 insta::assert_snapshot!(
929 Parser::new().parse_numeric(b"+05:30:a").unwrap_err(),
930 @"failed to parse seconds in UTC numeric offset: expected two digit second after minutes, but found end of input",
931 );
932 }
933
934 #[test]
936 fn err_numeric_seconds_invalid_digits() {
937 insta::assert_snapshot!(
938 Parser::new().parse_numeric(b"+05:30:ab").unwrap_err(),
939 @"failed to parse seconds in UTC numeric offset: failed to parse seconds (requires a two digit integer): invalid digit, expected 0-9 but got a",
940 );
941 }
942
943 #[test]
945 fn err_numeric_seconds_out_of_range() {
946 insta::assert_snapshot!(
947 Parser::new().parse_numeric(b"-05:30:60").unwrap_err(),
948 @"failed to parse seconds in UTC numeric offset: failed to parse seconds (requires a two digit integer): parameter 'time zone offset seconds' is not in the required range of -59..=59",
949 );
950 }
951
952 #[test]
955 fn err_numeric_fraction_non_empty() {
956 insta::assert_snapshot!(
957 Parser::new().parse_numeric(b"-05:30:44.").unwrap_err(),
958 @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
959 );
960 insta::assert_snapshot!(
961 Parser::new().parse_numeric(b"-05:30:44,").unwrap_err(),
962 @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
963 );
964
965 insta::assert_snapshot!(
967 Parser::new().parse_numeric(b"-05:30:44.a").unwrap_err(),
968 @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
969 );
970 insta::assert_snapshot!(
971 Parser::new().parse_numeric(b"-05:30:44,a").unwrap_err(),
972 @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
973 );
974
975 insta::assert_snapshot!(
977 Parser::new().parse_numeric(b"-053044.a").unwrap_err(),
978 @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
979 );
980 insta::assert_snapshot!(
981 Parser::new().parse_numeric(b"-053044,a").unwrap_err(),
982 @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
983 );
984 }
985
986 #[test]
990 fn err_numeric_subminute_disabled_but_desired() {
991 insta::assert_snapshot!(
992 Parser::new().subminute(false).parse_numeric(b"-05:59:32").unwrap_err(),
993 @"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
994 );
995 }
996
997 #[test]
1000 fn err_zulu_disabled_but_desired() {
1001 insta::assert_snapshot!(
1002 Parser::new().zulu(false).parse(b"Z").unwrap_err(),
1003 @"found `Z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)",
1004 );
1005 insta::assert_snapshot!(
1006 Parser::new().zulu(false).parse(b"z").unwrap_err(),
1007 @"found `z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)",
1008 );
1009 }
1010
1011 #[test]
1016 fn err_numeric_too_big_for_offset() {
1017 let numeric = Numeric {
1018 sign: b::Sign::Positive,
1019 hours: b::OffsetHours::MAX,
1020 minutes: Some(b::OffsetMinutes::MAX),
1021 seconds: Some(b::OffsetSeconds::MAX),
1022 nanoseconds: Some(499_999_999),
1023 };
1024 assert_eq!(numeric.to_offset().unwrap(), Offset::MAX);
1025
1026 let numeric = Numeric {
1027 sign: b::Sign::Positive,
1028 hours: b::OffsetHours::MAX,
1029 minutes: Some(b::OffsetMinutes::MAX),
1030 seconds: Some(b::OffsetSeconds::MAX),
1031 nanoseconds: Some(500_000_000),
1032 };
1033 insta::assert_snapshot!(
1034 numeric.to_offset().unwrap_err(),
1035 @"due to precision loss from fractional seconds, time zone offset is rounded to a value that is out of bounds",
1036 );
1037 }
1038
1039 #[test]
1041 fn err_numeric_too_small_for_offset() {
1042 let numeric = Numeric {
1043 sign: b::Sign::Negative,
1044 hours: b::OffsetHours::MAX,
1045 minutes: Some(b::OffsetMinutes::MAX),
1046 seconds: Some(b::OffsetSeconds::MAX),
1047 nanoseconds: Some(499_999_999),
1048 };
1049 assert_eq!(numeric.to_offset().unwrap(), Offset::MIN);
1050
1051 let numeric = Numeric {
1052 sign: b::Sign::Negative,
1053 hours: b::OffsetHours::MAX,
1054 minutes: Some(b::OffsetMinutes::MAX),
1055 seconds: Some(b::OffsetSeconds::MAX),
1056 nanoseconds: Some(500_000_000),
1057 };
1058 insta::assert_snapshot!(
1059 numeric.to_offset().unwrap_err(),
1060 @"due to precision loss from fractional seconds, time zone offset is rounded to a value that is out of bounds",
1061 );
1062 }
1063}