use crate::{
error::{err, Error, ErrorContext},
fmt::{
temporal::{PiecesNumericOffset, PiecesOffset},
util::{parse_temporal_fraction, FractionalFormatter},
Parsed,
},
tz::Offset,
util::{
escape, parse,
rangeint::{ri8, RFrom},
t::{self, C},
},
};
type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>;
type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>;
type ParsedOffsetSeconds = ri8<0, { t::SpanZoneOffsetSeconds::MAX }>;
#[derive(Debug)]
pub(crate) struct ParsedOffset {
kind: ParsedOffsetKind,
}
impl ParsedOffset {
pub(crate) fn to_offset(&self) -> Result<Offset, Error> {
match self.kind {
ParsedOffsetKind::Zulu => Ok(Offset::UTC),
ParsedOffsetKind::Numeric(ref numeric) => numeric.to_offset(),
}
}
pub(crate) fn to_pieces_offset(&self) -> Result<PiecesOffset, Error> {
match self.kind {
ParsedOffsetKind::Zulu => Ok(PiecesOffset::Zulu),
ParsedOffsetKind::Numeric(ref numeric) => {
let mut off = PiecesNumericOffset::from(numeric.to_offset()?);
if numeric.sign < C(0) {
off = off.with_negative_zero();
}
Ok(PiecesOffset::from(off))
}
}
}
pub(crate) fn is_zulu(&self) -> bool {
matches!(self.kind, ParsedOffsetKind::Zulu)
}
pub(crate) fn has_subminute(&self) -> bool {
let ParsedOffsetKind::Numeric(ref numeric) = self.kind else {
return false;
};
numeric.seconds.is_some()
}
}
#[derive(Debug)]
enum ParsedOffsetKind {
Zulu,
Numeric(Numeric),
}
struct Numeric {
sign: t::Sign,
hours: ParsedOffsetHours,
minutes: Option<ParsedOffsetMinutes>,
seconds: Option<ParsedOffsetSeconds>,
nanoseconds: Option<t::SubsecNanosecond>,
}
impl Numeric {
fn to_offset(&self) -> Result<Offset, Error> {
let mut seconds = t::SpanZoneOffset::rfrom(C(3_600) * self.hours);
if let Some(part_minutes) = self.minutes {
seconds += C(60) * part_minutes;
}
if let Some(part_seconds) = self.seconds {
seconds += part_seconds;
}
if let Some(part_nanoseconds) = self.nanoseconds {
if part_nanoseconds >= C(500_000_000) {
seconds = seconds
.try_checked_add("offset-seconds", C(1))
.with_context(|| {
err!(
"due to precision loss, UTC offset '{}' is \
rounded to a value that is out of bounds",
self,
)
})?;
}
}
Ok(Offset::from_seconds_ranged(seconds * self.sign))
}
}
impl core::fmt::Display for Numeric {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
if self.sign == C(-1) {
write!(f, "-")?;
} else {
write!(f, "+")?;
}
write!(f, "{:02}", self.hours)?;
if let Some(minutes) = self.minutes {
write!(f, ":{:02}", minutes)?;
}
if let Some(seconds) = self.seconds {
write!(f, ":{:02}", seconds)?;
}
if let Some(nanos) = self.nanoseconds {
static FMT: FractionalFormatter = FractionalFormatter::new();
write!(
f,
".{}",
FMT.format(i32::from(nanos).unsigned_abs()).as_str()
)?;
}
Ok(())
}
}
impl core::fmt::Debug for Numeric {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(self, f)
}
}
#[derive(Debug)]
pub(crate) struct Parser {
zulu: bool,
require_minute: bool,
require_second: bool,
subminute: bool,
subsecond: bool,
colon: Colon,
}
impl Parser {
pub(crate) const fn new() -> Parser {
Parser {
zulu: true,
require_minute: false,
require_second: false,
subminute: true,
subsecond: true,
colon: Colon::Optional,
}
}
pub(crate) const fn zulu(self, yes: bool) -> Parser {
Parser { zulu: yes, ..self }
}
pub(crate) const fn require_minute(self, yes: bool) -> Parser {
Parser { require_minute: yes, ..self }
}
pub(crate) const fn require_second(self, yes: bool) -> Parser {
Parser { require_second: yes, ..self }
}
pub(crate) const fn subminute(self, yes: bool) -> Parser {
Parser { subminute: yes, ..self }
}
pub(crate) const fn subsecond(self, yes: bool) -> Parser {
Parser { subsecond: yes, ..self }
}
pub(crate) const fn colon(self, colon: Colon) -> Parser {
Parser { colon, ..self }
}
pub(crate) fn parse<'i>(
&self,
mut input: &'i [u8],
) -> Result<Parsed<'i, ParsedOffset>, Error> {
if input.is_empty() {
return Err(err!("expected UTC offset, but found end of input"));
}
if input[0] == b'Z' || input[0] == b'z' {
if !self.zulu {
return Err(err!(
"found {z:?} in {original:?} where a numeric UTC offset \
was expected (this context does not permit \
the Zulu offset)",
z = escape::Byte(input[0]),
original = escape::Bytes(input),
));
}
input = &input[1..];
let value = ParsedOffset { kind: ParsedOffsetKind::Zulu };
return Ok(Parsed { value, input });
}
let Parsed { value: numeric, input } = self.parse_numeric(input)?;
let value = ParsedOffset { kind: ParsedOffsetKind::Numeric(numeric) };
Ok(Parsed { value, input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn parse_optional<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Option<ParsedOffset>>, Error> {
let Some(first) = input.first().copied() else {
return Ok(Parsed { value: None, input });
};
if !matches!(first, b'z' | b'Z' | b'+' | b'-') {
return Ok(Parsed { value: None, input });
}
let Parsed { value, input } = self.parse(input)?;
Ok(Parsed { value: Some(value), input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_numeric<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Numeric>, Error> {
let original = escape::Bytes(input);
let Parsed { value: sign, input } =
self.parse_sign(input).with_context(|| {
err!("failed to parse sign in UTC numeric offset {original:?}")
})?;
let Parsed { value: hours, input } =
self.parse_hours(input).with_context(|| {
err!(
"failed to parse hours in UTC numeric offset {original:?}"
)
})?;
let extended = match self.colon {
Colon::Optional => input.starts_with(b":"),
Colon::Required => {
if !input.is_empty() && !input.starts_with(b":") {
return Err(err!(
"parsed hour component of time zone offset from \
{original:?}, but could not find required colon \
separator",
));
}
true
}
Colon::Absent => {
if !input.is_empty() && input.starts_with(b":") {
return Err(err!(
"parsed hour component of time zone offset from \
{original:?}, but found colon after hours which \
is not allowed",
));
}
false
}
};
let mut numeric = Numeric {
sign,
hours,
minutes: None,
seconds: None,
nanoseconds: None,
};
let Parsed { value: has_minutes, input } =
self.parse_separator(input, extended).with_context(|| {
err!(
"failed to parse separator after hours in \
UTC numeric offset {original:?}"
)
})?;
if !has_minutes {
if self.require_minute || (self.subminute && self.require_second) {
return Err(err!(
"parsed hour component of time zone offset from \
{original:?}, but could not find required minute \
component",
));
}
return Ok(Parsed { value: numeric, input });
}
let Parsed { value: minutes, input } =
self.parse_minutes(input).with_context(|| {
err!(
"failed to parse minutes in UTC numeric offset \
{original:?}"
)
})?;
numeric.minutes = Some(minutes);
if !self.subminute {
if input.get(0).map_or(false, |&b| b == b':') {
return Err(err!(
"subminute precision for UTC numeric offset {original:?} \
is not enabled in this context (must provide only \
integral minutes)",
));
}
return Ok(Parsed { value: numeric, input });
}
let Parsed { value: has_seconds, input } =
self.parse_separator(input, extended).with_context(|| {
err!(
"failed to parse separator after minutes in \
UTC numeric offset {original:?}"
)
})?;
if !has_seconds {
if self.require_second {
return Err(err!(
"parsed hour and minute components of time zone offset \
from {original:?}, but could not find required second \
component",
));
}
return Ok(Parsed { value: numeric, input });
}
let Parsed { value: seconds, input } =
self.parse_seconds(input).with_context(|| {
err!(
"failed to parse seconds in UTC numeric offset \
{original:?}"
)
})?;
numeric.seconds = Some(seconds);
if !self.subsecond {
if input.get(0).map_or(false, |&b| b == b'.' || b == b',') {
return Err(err!(
"subsecond precision for UTC numeric offset {original:?} \
is not enabled in this context (must provide only \
integral minutes or seconds)",
));
}
return Ok(Parsed { value: numeric, input });
}
let Parsed { value: nanoseconds, input } =
parse_temporal_fraction(input).with_context(|| {
err!(
"failed to parse fractional nanoseconds in \
UTC numeric offset {original:?}",
)
})?;
numeric.nanoseconds =
nanoseconds.map(|n| t::SubsecNanosecond::new(n).unwrap());
Ok(Parsed { value: numeric, input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_sign<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Sign>, Error> {
let sign = input.get(0).copied().ok_or_else(|| {
err!("expected UTC numeric offset, but found end of input")
})?;
let sign = if sign == b'+' {
t::Sign::N::<1>()
} else if sign == b'-' {
t::Sign::N::<-1>()
} else {
return Err(err!(
"expected '+' or '-' sign at start of UTC numeric offset, \
but found {found:?} instead",
found = escape::Byte(sign),
));
};
Ok(Parsed { value: sign, input: &input[1..] })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_hours<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedOffsetHours>, Error> {
let (hours, input) = parse::split(input, 2).ok_or_else(|| {
err!("expected two digit hour after sign, but found end of input",)
})?;
let hours = parse::i64(hours).with_context(|| {
err!(
"failed to parse {hours:?} as hours (a two digit integer)",
hours = escape::Bytes(hours),
)
})?;
let hours = ParsedOffsetHours::try_new("hours", hours)
.context("offset hours are not valid")?;
Ok(Parsed { value: hours, input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_minutes<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedOffsetMinutes>, Error> {
let (minutes, input) = parse::split(input, 2).ok_or_else(|| {
err!(
"expected two digit minute after hours, \
but found end of input",
)
})?;
let minutes = parse::i64(minutes).with_context(|| {
err!(
"failed to parse {minutes:?} as minutes (a two digit integer)",
minutes = escape::Bytes(minutes),
)
})?;
let minutes = ParsedOffsetMinutes::try_new("minutes", minutes)
.context("minutes are not valid")?;
Ok(Parsed { value: minutes, input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_seconds<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedOffsetSeconds>, Error> {
let (seconds, input) = parse::split(input, 2).ok_or_else(|| {
err!(
"expected two digit second after hours, \
but found end of input",
)
})?;
let seconds = parse::i64(seconds).with_context(|| {
err!(
"failed to parse {seconds:?} as seconds (a two digit integer)",
seconds = escape::Bytes(seconds),
)
})?;
let seconds = ParsedOffsetSeconds::try_new("seconds", seconds)
.context("time zone offset seconds are not valid")?;
Ok(Parsed { value: seconds, input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_separator<'i>(
&self,
mut input: &'i [u8],
extended: bool,
) -> Result<Parsed<'i, bool>, Error> {
if !extended {
let expected =
input.len() >= 2 && input[..2].iter().all(u8::is_ascii_digit);
return Ok(Parsed { value: expected, input });
}
let is_separator = input.get(0).map_or(false, |&b| b == b':');
if is_separator {
input = &input[1..];
}
Ok(Parsed { value: is_separator, input })
}
}
#[derive(Debug)]
pub(crate) enum Colon {
Optional,
Required,
Absent,
}
#[cfg(test)]
mod tests {
use crate::util::rangeint::RInto;
use super::*;
#[test]
fn ok_zulu() {
let p = |input| Parser::new().parse(input).unwrap();
insta::assert_debug_snapshot!(p(b"Z"), @r###"
Parsed {
value: ParsedOffset {
kind: Zulu,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"z"), @r###"
Parsed {
value: ParsedOffset {
kind: Zulu,
},
input: "",
}
"###);
}
#[test]
fn ok_numeric() {
let p = |input| Parser::new().parse(input).unwrap();
insta::assert_debug_snapshot!(p(b"-05"), @r###"
Parsed {
value: ParsedOffset {
kind: Numeric(
-05,
),
},
input: "",
}
"###);
}
#[test]
fn ok_numeric_complete() {
let p = |input| Parser::new().parse_numeric(input).unwrap();
insta::assert_debug_snapshot!(p(b"-05"), @r###"
Parsed {
value: -05,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"+05"), @r###"
Parsed {
value: +05,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"+25:59"), @r###"
Parsed {
value: +25:59,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"+2559"), @r###"
Parsed {
value: +25:59,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"+25:59:59"), @r###"
Parsed {
value: +25:59:59,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"+255959"), @r###"
Parsed {
value: +25:59:59,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"+25:59:59.999"), @r###"
Parsed {
value: +25:59:59.999,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"+25:59:59,999"), @r###"
Parsed {
value: +25:59:59.999,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"+255959.999"), @r###"
Parsed {
value: +25:59:59.999,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"+255959,999"), @r###"
Parsed {
value: +25:59:59.999,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"+25:59:59.999999999"), @r###"
Parsed {
value: +25:59:59.999999999,
input: "",
}
"###);
}
#[test]
fn ok_numeric_incomplete() {
let p = |input| Parser::new().parse_numeric(input).unwrap();
insta::assert_debug_snapshot!(p(b"-05a"), @r###"
Parsed {
value: -05,
input: "a",
}
"###);
insta::assert_debug_snapshot!(p(b"-05:12a"), @r###"
Parsed {
value: -05:12,
input: "a",
}
"###);
insta::assert_debug_snapshot!(p(b"-05:12."), @r###"
Parsed {
value: -05:12,
input: ".",
}
"###);
insta::assert_debug_snapshot!(p(b"-05:12,"), @r###"
Parsed {
value: -05:12,
input: ",",
}
"###);
insta::assert_debug_snapshot!(p(b"-0512a"), @r###"
Parsed {
value: -05:12,
input: "a",
}
"###);
insta::assert_debug_snapshot!(p(b"-0512:"), @r###"
Parsed {
value: -05:12,
input: ":",
}
"###);
insta::assert_debug_snapshot!(p(b"-05:12:34a"), @r###"
Parsed {
value: -05:12:34,
input: "a",
}
"###);
insta::assert_debug_snapshot!(p(b"-05:12:34.9a"), @r###"
Parsed {
value: -05:12:34.9,
input: "a",
}
"###);
insta::assert_debug_snapshot!(p(b"-05:12:34.9."), @r###"
Parsed {
value: -05:12:34.9,
input: ".",
}
"###);
insta::assert_debug_snapshot!(p(b"-05:12:34.9,"), @r###"
Parsed {
value: -05:12:34.9,
input: ",",
}
"###);
}
#[test]
fn err_numeric_empty() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"").unwrap_err(),
@r###"failed to parse sign in UTC numeric offset "": expected UTC numeric offset, but found end of input"###,
);
}
#[test]
fn err_numeric_notsign() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"*").unwrap_err(),
@r###"failed to parse sign in UTC numeric offset "*": expected '+' or '-' sign at start of UTC numeric offset, but found "*" instead"###,
);
}
#[test]
fn err_numeric_hours_too_short() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+a").unwrap_err(),
@r###"failed to parse hours in UTC numeric offset "+a": expected two digit hour after sign, but found end of input"###,
);
}
#[test]
fn err_numeric_hours_invalid_digits() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+ab").unwrap_err(),
@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"###,
);
}
#[test]
fn err_numeric_hours_out_of_range() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-26").unwrap_err(),
@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"###,
);
}
#[test]
fn err_numeric_minutes_too_short() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+05:a").unwrap_err(),
@r###"failed to parse minutes in UTC numeric offset "+05:a": expected two digit minute after hours, but found end of input"###,
);
}
#[test]
fn err_numeric_minutes_invalid_digits() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+05:ab").unwrap_err(),
@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"###,
);
}
#[test]
fn err_numeric_minutes_out_of_range() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:60").unwrap_err(),
@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"###,
);
}
#[test]
fn err_numeric_seconds_too_short() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+05:30:a").unwrap_err(),
@r###"failed to parse seconds in UTC numeric offset "+05:30:a": expected two digit second after hours, but found end of input"###,
);
}
#[test]
fn err_numeric_seconds_invalid_digits() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+05:30:ab").unwrap_err(),
@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"###,
);
}
#[test]
fn err_numeric_seconds_out_of_range() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:60").unwrap_err(),
@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"###,
);
}
#[test]
fn err_numeric_fraction_non_empty() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:44.").unwrap_err(),
@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"###,
);
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:44,").unwrap_err(),
@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"###,
);
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:44.a").unwrap_err(),
@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"###,
);
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:44,a").unwrap_err(),
@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"###,
);
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-053044.a").unwrap_err(),
@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"###,
);
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-053044,a").unwrap_err(),
@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"###,
);
}
#[test]
fn err_numeric_subminute_disabled_but_desired() {
insta::assert_snapshot!(
Parser::new().subminute(false).parse_numeric(b"-05:59:32").unwrap_err(),
@r###"subminute precision for UTC numeric offset "-05:59:32" is not enabled in this context (must provide only integral minutes)"###,
);
}
#[test]
fn err_zulu_disabled_but_desired() {
insta::assert_snapshot!(
Parser::new().zulu(false).parse(b"Z").unwrap_err(),
@r###"found "Z" in "Z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###,
);
insta::assert_snapshot!(
Parser::new().zulu(false).parse(b"z").unwrap_err(),
@r###"found "z" in "z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###,
);
}
#[test]
fn err_numeric_too_big_for_offset() {
let numeric = Numeric {
sign: t::Sign::MAX_SELF,
hours: ParsedOffsetHours::MAX_SELF,
minutes: Some(ParsedOffsetMinutes::MAX_SELF),
seconds: Some(ParsedOffsetSeconds::MAX_SELF),
nanoseconds: Some(C(499_999_999).rinto()),
};
assert_eq!(numeric.to_offset().unwrap(), Offset::MAX);
let numeric = Numeric {
sign: t::Sign::MAX_SELF,
hours: ParsedOffsetHours::MAX_SELF,
minutes: Some(ParsedOffsetMinutes::MAX_SELF),
seconds: Some(ParsedOffsetSeconds::MAX_SELF),
nanoseconds: Some(C(500_000_000).rinto()),
};
insta::assert_snapshot!(
numeric.to_offset().unwrap_err(),
@"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",
);
}
#[test]
fn err_numeric_too_small_for_offset() {
let numeric = Numeric {
sign: t::Sign::MIN_SELF,
hours: ParsedOffsetHours::MAX_SELF,
minutes: Some(ParsedOffsetMinutes::MAX_SELF),
seconds: Some(ParsedOffsetSeconds::MAX_SELF),
nanoseconds: Some(C(499_999_999).rinto()),
};
assert_eq!(numeric.to_offset().unwrap(), Offset::MIN);
let numeric = Numeric {
sign: t::Sign::MIN_SELF,
hours: ParsedOffsetHours::MAX_SELF,
minutes: Some(ParsedOffsetMinutes::MAX_SELF),
seconds: Some(ParsedOffsetSeconds::MAX_SELF),
nanoseconds: Some(C(500_000_000).rinto()),
};
insta::assert_snapshot!(
numeric.to_offset().unwrap_err(),
@"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",
);
}
}