use crate::{
civil::{Date, DateTime, Time},
error::{err, Error, ErrorContext},
fmt::{
offset::{self, ParsedOffset},
rfc9557::{self, ParsedAnnotations},
util::parse_temporal_fraction,
Parsed,
},
span::Span,
tz::{AmbiguousZoned, Disambiguation, OffsetConflict, TimeZoneDatabase},
util::{escape, parse, rangeint::RFrom, t},
SignedDuration, Timestamp, Unit, Zoned,
};
#[derive(Debug)]
pub(super) struct ParsedDateTime<'i> {
input: escape::Bytes<'i>,
date: ParsedDate<'i>,
time: Option<ParsedTime<'i>>,
offset: Option<ParsedOffset>,
annotations: ParsedAnnotations<'i>,
}
impl<'i> ParsedDateTime<'i> {
#[inline(always)]
pub(super) fn to_zoned(
&self,
db: &TimeZoneDatabase,
offset_conflict: OffsetConflict,
disambiguation: Disambiguation,
) -> Result<Zoned, Error> {
self.to_ambiguous_zoned(db, offset_conflict)?
.disambiguate(disambiguation)
}
#[inline(always)]
pub(super) fn to_ambiguous_zoned(
&self,
db: &TimeZoneDatabase,
offset_conflict: OffsetConflict,
) -> Result<AmbiguousZoned, Error> {
let time = self.time.as_ref().map_or(Time::midnight(), |p| p.time);
let dt = DateTime::from_parts(self.date.date, time);
let (tz, _critical) =
self.annotations.to_time_zone(db)?.ok_or_else(|| {
err!(
"failed to find time zone in square brackets \
in {:?}, which is required for parsing a zoned instant",
self.input,
)
})?;
let Some(ref parsed_offset) = self.offset else {
return Ok(tz.into_ambiguous_zoned(dt));
};
let offset = parsed_offset.to_offset()?;
offset_conflict.resolve(dt, offset, tz).with_context(|| {
err!("parsing {input:?} failed", input = self.input)
})
}
#[inline(always)]
pub(super) fn to_timestamp(&self) -> Result<Timestamp, Error> {
let time = self.time.as_ref().map(|p| p.time).ok_or_else(|| {
err!(
"failed to find time component in {:?}, \
which is required for parsing a timestamp",
self.input,
)
})?;
let parsed_offset = self.offset.as_ref().ok_or_else(|| {
err!(
"failed to find offset component in {:?}, \
which is required for parsing a timestamp",
self.input,
)
})?;
let offset = parsed_offset.to_offset()?;
let dt = DateTime::from_parts(self.date.date, time);
let timestamp = offset.to_timestamp(dt).with_context(|| {
err!(
"failed to convert civil datetime to timestamp \
with offset {offset}",
)
})?;
Ok(timestamp)
}
#[inline(always)]
pub(super) fn to_datetime(&self) -> Result<DateTime, Error> {
if self.offset.as_ref().map_or(false, |o| o.is_zulu()) {
return Err(err!(
"cannot parse civil date from string with a Zulu \
offset, parse as a `Timestamp` and convert to a civil \
datetime instead",
));
}
Ok(DateTime::from_parts(self.date.date, self.time()))
}
#[inline(always)]
pub(super) fn to_date(&self) -> Result<Date, Error> {
if self.offset.as_ref().map_or(false, |o| o.is_zulu()) {
return Err(err!(
"cannot parse civil date from string with a Zulu \
offset, parse as a `Timestamp` and convert to a civil \
date instead",
));
}
Ok(self.date.date)
}
#[inline(always)]
fn time(&self) -> Time {
self.time.as_ref().map(|p| p.time).unwrap_or(Time::midnight())
}
}
impl<'i> core::fmt::Display for ParsedDateTime<'i> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(&self.input, f)
}
}
#[derive(Debug)]
pub(super) struct ParsedDate<'i> {
input: escape::Bytes<'i>,
date: Date,
}
impl<'i> core::fmt::Display for ParsedDate<'i> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(&self.input, f)
}
}
#[derive(Debug)]
pub(super) struct ParsedTime<'i> {
input: escape::Bytes<'i>,
time: Time,
extended: bool,
}
impl<'i> ParsedTime<'i> {
pub(super) fn to_time(&self) -> Time {
self.time
}
}
impl<'i> core::fmt::Display for ParsedTime<'i> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(&self.input, f)
}
}
#[derive(Debug)]
pub(super) struct DateTimeParser {
_priv: (),
}
impl DateTimeParser {
pub(super) const fn new() -> DateTimeParser {
DateTimeParser { _priv: () }
}
#[inline(always)]
pub(super) fn parse_temporal_datetime<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedDateTime<'i>>, Error> {
let mkslice = parse::slicer(input);
let Parsed { value: date, input } = self.parse_date_spec(input)?;
if input.is_empty() {
let value = ParsedDateTime {
input: escape::Bytes(mkslice(input)),
date,
time: None,
offset: None,
annotations: ParsedAnnotations::none(),
};
return Ok(Parsed { value, input });
}
let (time, offset, input) = if !matches!(input[0], b' ' | b'T' | b't')
{
(None, None, input)
} else {
let input = &input[1..];
let Parsed { value: time, input } = self.parse_time_spec(input)?;
let Parsed { value: offset, input } = self.parse_offset(input)?;
(Some(time), offset, input)
};
let Parsed { value: annotations, input } =
self.parse_annotations(input)?;
let value = ParsedDateTime {
input: escape::Bytes(mkslice(input)),
date,
time,
offset,
annotations,
};
Ok(Parsed { value, input })
}
#[inline(always)]
pub(super) fn parse_temporal_time<'i>(
&self,
mut input: &'i [u8],
) -> Result<Parsed<'i, ParsedTime<'i>>, Error> {
let mkslice = parse::slicer(input);
if input.starts_with(b"T") || input.starts_with(b"t") {
input = &input[1..];
let Parsed { value: time, input } = self.parse_time_spec(input)?;
let Parsed { value: offset, input } = self.parse_offset(input)?;
if offset.map_or(false, |o| o.is_zulu()) {
return Err(err!(
"cannot parse civil time from string with a Zulu \
offset, parse as a `Timestamp` and convert to a civil \
time instead",
));
}
let Parsed { input, .. } = self.parse_annotations(input)?;
return Ok(Parsed { value: time, input });
}
if let Ok(parsed) = self.parse_temporal_datetime(input) {
let Parsed { value: dt, input } = parsed;
if dt.offset.map_or(false, |o| o.is_zulu()) {
return Err(err!(
"cannot parse plain time from full datetime string with a \
Zulu offset, parse as a `Timestamp` and convert to a \
plain time instead",
));
}
let Some(time) = dt.time else {
return Err(err!(
"successfully parsed date from {parsed:?}, but \
no time component was found",
parsed = dt.input,
));
};
return Ok(Parsed { value: time, input });
}
let Parsed { value: time, input } = self.parse_time_spec(input)?;
let Parsed { value: offset, input } = self.parse_offset(input)?;
if offset.map_or(false, |o| o.is_zulu()) {
return Err(err!(
"cannot parse plain time from string with a Zulu \
offset, parse as a `Timestamp` and convert to a plain \
time instead",
));
}
if !time.extended {
let possibly_ambiguous = mkslice(input);
if self.parse_month_day(possibly_ambiguous).is_ok() {
return Err(err!(
"parsed time from {parsed:?} is ambiguous \
with a month-day date",
parsed = escape::Bytes(possibly_ambiguous),
));
}
if self.parse_year_month(possibly_ambiguous).is_ok() {
return Err(err!(
"parsed time from {parsed:?} is ambiguous \
with a year-month date",
parsed = escape::Bytes(possibly_ambiguous),
));
}
}
let Parsed { input, .. } = self.parse_annotations(input)?;
Ok(Parsed { value: time, input })
}
#[inline(always)]
fn parse_date_spec<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedDate<'i>>, Error> {
let mkslice = parse::slicer(input);
let original = escape::Bytes(input);
let Parsed { value: year, input } =
self.parse_year(input).with_context(|| {
err!("failed to parse year in date {original:?}")
})?;
let extended = input.starts_with(b"-");
let Parsed { input, .. } = self
.parse_date_separator(input, extended)
.context("failed to parse separator after year")?;
let Parsed { value: month, input } =
self.parse_month(input).with_context(|| {
err!("failed to parse month in date {original:?}")
})?;
let Parsed { input, .. } = self
.parse_date_separator(input, extended)
.context("failed to parse separator after month")?;
let Parsed { value: day, input } =
self.parse_day(input).with_context(|| {
err!("failed to parse day in date {original:?}")
})?;
let date = Date::new_ranged(year, month, day).with_context(|| {
err!("date parsed from {original:?} is not valid")
})?;
let value = ParsedDate { input: escape::Bytes(mkslice(input)), date };
Ok(Parsed { value, input })
}
#[inline(always)]
fn parse_time_spec<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedTime<'i>>, Error> {
let mkslice = parse::slicer(input);
let original = escape::Bytes(input);
let Parsed { value: hour, input } =
self.parse_hour(input).with_context(|| {
err!("failed to parse hour in time {original:?}")
})?;
let extended = input.starts_with(b":");
let Parsed { value: has_minute, input } =
self.parse_time_separator(input, extended);
if !has_minute {
let time = Time::new_ranged(
hour,
t::Minute::N::<0>(),
t::Second::N::<0>(),
t::SubsecNanosecond::N::<0>(),
);
let value = ParsedTime {
input: escape::Bytes(mkslice(input)),
time,
extended,
};
return Ok(Parsed { value, input });
}
let Parsed { value: minute, input } =
self.parse_minute(input).with_context(|| {
err!("failed to parse minute in time {original:?}")
})?;
let Parsed { value: has_second, input } =
self.parse_time_separator(input, extended);
if !has_second {
let time = Time::new_ranged(
hour,
minute,
t::Second::N::<0>(),
t::SubsecNanosecond::N::<0>(),
);
let value = ParsedTime {
input: escape::Bytes(mkslice(input)),
time,
extended,
};
return Ok(Parsed { value, input });
}
let Parsed { value: second, input } =
self.parse_second(input).with_context(|| {
err!("failed to parse second in time {original:?}")
})?;
let Parsed { value: nanosecond, input } =
parse_temporal_fraction(input).with_context(|| {
err!(
"failed to parse fractional nanoseconds \
in time {original:?}",
)
})?;
let time = Time::new_ranged(
hour,
minute,
second,
nanosecond.unwrap_or(t::SubsecNanosecond::N::<0>()),
);
let value = ParsedTime {
input: escape::Bytes(mkslice(input)),
time,
extended,
};
Ok(Parsed { value, input })
}
#[inline(always)]
fn parse_month_day<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let original = escape::Bytes(input);
let Parsed { value: month, mut input } =
self.parse_month(input).with_context(|| {
err!("failed to parse month in month-day {original:?}")
})?;
if input.starts_with(b"-") {
input = &input[1..];
}
let Parsed { value: day, input } =
self.parse_day(input).with_context(|| {
err!("failed to parse day in month-day {original:?}")
})?;
let year = t::Year::N::<2024>();
let _ = Date::new_ranged(year, month, day).with_context(|| {
err!("month-day parsed from {original:?} is not valid")
})?;
Ok(Parsed { value: (), input })
}
#[inline(always)]
fn parse_year_month<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let original = escape::Bytes(input);
let Parsed { value: year, mut input } =
self.parse_year(input).with_context(|| {
err!("failed to parse year in date {original:?}")
})?;
if input.starts_with(b"-") {
input = &input[1..];
}
let Parsed { value: month, input } =
self.parse_month(input).with_context(|| {
err!("failed to parse month in month-day {original:?}")
})?;
let day = t::Day::N::<1>();
let _ = Date::new_ranged(year, month, day).with_context(|| {
err!("year-month parsed from {original:?} is not valid")
})?;
Ok(Parsed { value: (), input })
}
#[inline(always)]
fn parse_year<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Year>, Error> {
let Parsed { value: sign, input } = self.parse_year_sign(input);
if let Some(sign) = sign {
let (year, input) = parse::split(input, 6).ok_or_else(|| {
err!(
"expected six digit year (because of a leading sign), \
but found end of input",
)
})?;
let year = parse::i64(year).with_context(|| {
err!(
"failed to parse {year:?} as year (a six digit integer)",
year = escape::Bytes(year),
)
})?;
let year =
t::Year::try_new("year", year).context("year is not valid")?;
if year == 0 && sign < 0 {
return Err(err!(
"year zero must be written without a sign or a \
positive sign, but not a negative sign",
));
}
Ok(Parsed { value: year * sign, input })
} else {
let (year, input) = parse::split(input, 4).ok_or_else(|| {
err!(
"expected four digit year (or leading sign for \
six digit year), but found end of input",
)
})?;
let year = parse::i64(year).with_context(|| {
err!(
"failed to parse {year:?} as year (a four digit integer)",
year = escape::Bytes(year),
)
})?;
let year =
t::Year::try_new("year", year).context("year is not valid")?;
Ok(Parsed { value: year, input })
}
}
#[inline(always)]
fn parse_month<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Month>, Error> {
let (month, input) = parse::split(input, 2).ok_or_else(|| {
err!("expected two digit month, but found end of input")
})?;
let month = parse::i64(month).with_context(|| {
err!(
"failed to parse {month:?} as month (a two digit integer)",
month = escape::Bytes(month),
)
})?;
let month =
t::Month::try_new("month", month).context("month is not valid")?;
Ok(Parsed { value: month, input })
}
#[inline(always)]
fn parse_day<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Day>, Error> {
let (day, input) = parse::split(input, 2).ok_or_else(|| {
err!("expected two digit day, but found end of input")
})?;
let day = parse::i64(day).with_context(|| {
err!(
"failed to parse {day:?} as day (a two digit integer)",
day = escape::Bytes(day),
)
})?;
let day = t::Day::try_new("day", day).context("day is not valid")?;
Ok(Parsed { value: day, input })
}
#[inline(always)]
fn parse_hour<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Hour>, Error> {
let (hour, input) = parse::split(input, 2).ok_or_else(|| {
err!("expected two digit hour, but found end of input")
})?;
let hour = parse::i64(hour).with_context(|| {
err!(
"failed to parse {hour:?} as hour (a two digit integer)",
hour = escape::Bytes(hour),
)
})?;
let hour =
t::Hour::try_new("hour", hour).context("hour is not valid")?;
Ok(Parsed { value: hour, input })
}
#[inline(always)]
fn parse_minute<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Minute>, Error> {
let (minute, input) = parse::split(input, 2).ok_or_else(|| {
err!("expected two digit minute, but found end of input")
})?;
let minute = parse::i64(minute).with_context(|| {
err!(
"failed to parse {minute:?} as minute (a two digit integer)",
minute = escape::Bytes(minute),
)
})?;
let minute = t::Minute::try_new("minute", minute)
.context("minute is not valid")?;
Ok(Parsed { value: minute, input })
}
#[inline(always)]
fn parse_second<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Second>, Error> {
let (second, input) = parse::split(input, 2).ok_or_else(|| {
err!("expected two digit second, but found end of input",)
})?;
let mut second = parse::i64(second).with_context(|| {
err!(
"failed to parse {second:?} as second (a two digit integer)",
second = escape::Bytes(second),
)
})?;
if second == 60 {
second = 59;
}
let second = t::Second::try_new("second", second)
.context("second is not valid")?;
Ok(Parsed { value: second, input })
}
#[inline(always)]
fn parse_offset<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Option<ParsedOffset>>, Error> {
const P: offset::Parser =
offset::Parser::new().zulu(true).subminute(true);
P.parse_optional(input)
}
#[inline(always)]
fn parse_annotations<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedAnnotations<'i>>, Error> {
const P: rfc9557::Parser = rfc9557::Parser::new();
P.parse(input)
}
#[inline(always)]
fn parse_date_separator<'i>(
&self,
mut input: &'i [u8],
extended: bool,
) -> Result<Parsed<'i, ()>, Error> {
if !extended {
if input.starts_with(b"-") {
return Err(err!(
"expected no separator after month since none was \
found after the year, but found a '-' separator",
));
}
return Ok(Parsed { value: (), input });
}
if input.is_empty() {
return Err(err!(
"expected '-' separator, but found end of input"
));
}
if input[0] != b'-' {
return Err(err!(
"expected '-' separator, but found {found:?} instead",
found = escape::Byte(input[0]),
));
}
input = &input[1..];
Ok(Parsed { value: (), input })
}
#[inline(always)]
fn parse_time_separator<'i>(
&self,
mut input: &'i [u8],
extended: bool,
) -> Parsed<'i, bool> {
if !extended {
let expected =
input.len() >= 2 && input[..2].iter().all(u8::is_ascii_digit);
return Parsed { value: expected, input };
}
let is_separator = input.get(0).map_or(false, |&b| b == b':');
if is_separator {
input = &input[1..];
}
Parsed { value: is_separator, input }
}
#[inline(always)]
fn parse_year_sign<'i>(
&self,
mut input: &'i [u8],
) -> Parsed<'i, Option<t::Sign>> {
let Some(sign) = input.get(0).copied() else {
return Parsed { value: None, input };
};
let sign = if sign == b'+' {
t::Sign::N::<1>()
} else if sign == b'-' {
t::Sign::N::<-1>()
} else {
return Parsed { value: None, input };
};
input = &input[1..];
Parsed { value: Some(sign), input }
}
}
#[derive(Debug)]
pub(super) struct SpanParser {
_priv: (),
}
impl SpanParser {
pub(super) const fn new() -> SpanParser {
SpanParser { _priv: () }
}
#[inline(always)]
pub(super) fn parse_temporal_duration<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Span>, Error> {
self.parse_span(input).context(
"failed to parse ISO 8601 \
duration string into `Span`",
)
}
#[inline(always)]
pub(super) fn parse_signed_duration<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, SignedDuration>, Error> {
self.parse_duration(input).context(
"failed to parse ISO 8601 \
duration string into `SignedDuration`",
)
}
#[inline(always)]
fn parse_span<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Span>, Error> {
let original = escape::Bytes(input);
let Parsed { value: sign, input } = self.parse_sign(input);
let Parsed { input, .. } = self.parse_duration_designator(input)?;
let Parsed { value: (mut span, parsed_any_date), input } =
self.parse_date_units(input, Span::new())?;
let Parsed { value: has_time, mut input } =
self.parse_time_designator(input);
if has_time {
let parsed = self.parse_time_units(input, span)?;
input = parsed.input;
let (time_span, parsed_any_time) = parsed.value;
if !parsed_any_time {
return Err(err!(
"found a time designator (T or t) in an ISO 8601 \
duration string in {original:?}, but did not find \
any time units",
));
}
span = time_span;
} else if !parsed_any_date {
return Err(err!(
"found the start of a ISO 8601 duration string \
in {original:?}, but did not find any units",
));
}
if sign < 0 {
span = span.negate();
}
Ok(Parsed { value: span, input })
}
#[inline(always)]
fn parse_duration<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, SignedDuration>, Error> {
let Parsed { value: sign, input } = self.parse_sign(input);
let Parsed { input, .. } = self.parse_duration_designator(input)?;
let Parsed { value: has_time, input } =
self.parse_time_designator(input);
if !has_time {
return Err(err!(
"parsing ISO 8601 duration into SignedDuration requires \
that the duration contain a time component and no \
components of days or greater",
));
}
let Parsed { value: dur, input } =
self.parse_time_units_duration(input, sign == -1)?;
Ok(Parsed { value: dur, input })
}
#[inline(always)]
fn parse_date_units<'i>(
&self,
mut input: &'i [u8],
mut span: Span,
) -> Result<Parsed<'i, (Span, bool)>, Error> {
let mut parsed_any = false;
let mut prev_unit: Option<Unit> = None;
loop {
let parsed = self.parse_unit_value(input)?;
input = parsed.input;
let Some(value) = parsed.value else { break };
let parsed = self.parse_unit_date_designator(input)?;
input = parsed.input;
let unit = parsed.value;
if let Some(prev_unit) = prev_unit {
if prev_unit <= unit {
return Err(err!(
"found value {value:?} with unit {unit} \
after unit {prev_unit}, but units must be \
written from largest to smallest \
(and they can't be repeated)",
unit = unit.singular(),
prev_unit = prev_unit.singular(),
));
}
}
prev_unit = Some(unit);
span = span.try_units_ranged(unit, value).with_context(|| {
err!(
"failed to set value {value:?} as {unit} unit on span",
unit = Unit::from(unit).singular(),
)
})?;
parsed_any = true;
}
Ok(Parsed { value: (span, parsed_any), input })
}
#[inline(always)]
fn parse_time_units<'i>(
&self,
mut input: &'i [u8],
mut span: Span,
) -> Result<Parsed<'i, (Span, bool)>, Error> {
let mut parsed_any = false;
let mut prev_unit: Option<Unit> = None;
loop {
let parsed = self.parse_unit_value(input)?;
input = parsed.input;
let Some(value) = parsed.value else { break };
let parsed = parse_temporal_fraction(input)?;
input = parsed.input;
let fraction = parsed.value;
let parsed = self.parse_unit_time_designator(input)?;
input = parsed.input;
let unit = parsed.value;
if let Some(prev_unit) = prev_unit {
if prev_unit <= unit {
return Err(err!(
"found value {value:?} with unit {unit} \
after unit {prev_unit}, but units must be \
written from largest to smallest \
(and they can't be repeated)",
unit = unit.singular(),
prev_unit = prev_unit.singular(),
));
}
}
prev_unit = Some(unit);
parsed_any = true;
if let Some(fraction) = fraction {
span = span_fractional_time(unit, value, fraction, span)?;
break;
} else {
let result =
span.try_units_ranged(unit, value).with_context(|| {
err!(
"failed to set value {value:?} \
as {unit} unit on span",
unit = Unit::from(unit).singular(),
)
});
span = match result {
Ok(span) => span,
Err(_) => span_fractional_time(
unit,
value,
t::SubsecNanosecond::N::<0>(),
span,
)?,
};
}
}
Ok(Parsed { value: (span, parsed_any), input })
}
#[inline(always)]
fn parse_time_units_duration<'i>(
&self,
mut input: &'i [u8],
negative: bool,
) -> Result<Parsed<'i, SignedDuration>, Error> {
let mut parsed_any = false;
let mut prev_unit: Option<Unit> = None;
let mut dur = SignedDuration::ZERO;
loop {
let parsed = self.parse_unit_value(input)?;
input = parsed.input;
let Some(value) = parsed.value else { break };
let parsed = parse_temporal_fraction(input)?;
input = parsed.input;
let fraction = parsed.value;
let parsed = self.parse_unit_time_designator(input)?;
input = parsed.input;
let unit = parsed.value;
if let Some(prev_unit) = prev_unit {
if prev_unit <= unit {
return Err(err!(
"found value {value:?} with unit {unit} \
after unit {prev_unit}, but units must be \
written from largest to smallest \
(and they can't be repeated)",
unit = unit.singular(),
prev_unit = prev_unit.singular(),
));
}
}
prev_unit = Some(unit);
parsed_any = true;
let unit_secs = match unit {
Unit::Second => value.get(),
Unit::Minute => {
let mins = value.get();
mins.checked_mul(60).ok_or_else(|| {
err!(
"minute units {mins} overflowed i64 when \
converted to seconds"
)
})?
}
Unit::Hour => {
let hours = value.get();
hours.checked_mul(3_600).ok_or_else(|| {
err!(
"hour units {hours} overflowed i64 when \
converted to seconds"
)
})?
}
_ => unreachable!(),
};
let unit_dur = SignedDuration::new(unit_secs, 0);
let result = if negative {
dur.checked_sub(unit_dur)
} else {
dur.checked_add(unit_dur)
};
dur = result.ok_or_else(|| {
err!(
"adding value {value} from unit {unit} overflowed \
signed duration {dur:?}",
unit = unit.singular(),
)
})?;
if let Some(fraction) = fraction {
let fraction_dur = duration_fractional_time(unit, fraction);
let result = if negative {
dur.checked_sub(fraction_dur)
} else {
dur.checked_add(fraction_dur)
};
dur = result.ok_or_else(|| {
err!(
"adding fractional duration {fraction_dur:?} \
from unit {unit} to {dur:?} overflowed \
signed duration limits",
unit = unit.singular(),
)
})?;
break;
}
}
if !parsed_any {
return Err(err!(
"expected at least one unit of time (hours, minutes or \
seconds) in ISO 8601 duration when parsing into a \
`SignedDuration`",
));
}
Ok(Parsed { value: dur, input })
}
#[inline(always)]
fn parse_unit_value<'i>(
&self,
mut input: &'i [u8],
) -> Result<Parsed<'i, Option<t::NoUnits>>, Error> {
const MAX_I64_DIGITS: usize = 19;
let mkdigits = parse::slicer(input);
while mkdigits(input).len() <= MAX_I64_DIGITS
&& input.first().map_or(false, u8::is_ascii_digit)
{
input = &input[1..];
}
let digits = mkdigits(input);
if digits.is_empty() {
return Ok(Parsed { value: None, input });
}
let value = parse::i64(digits).with_context(|| {
err!(
"failed to parse {digits:?} as 64-bit signed integer",
digits = escape::Bytes(digits),
)
})?;
let value = t::NoUnits::new(value).unwrap();
Ok(Parsed { value: Some(value), input })
}
#[inline(always)]
fn parse_unit_date_designator<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Unit>, Error> {
if input.is_empty() {
return Err(err!(
"expected to find date unit designator suffix \
(Y, M, W or D), but found end of input",
));
}
let unit = match input[0] {
b'Y' | b'y' => Unit::Year,
b'M' | b'm' => Unit::Month,
b'W' | b'w' => Unit::Week,
b'D' | b'd' => Unit::Day,
unknown => {
return Err(err!(
"expected to find date unit designator suffix \
(Y, M, W or D), but found {found:?} instead",
found = escape::Byte(unknown),
));
}
};
Ok(Parsed { value: unit, input: &input[1..] })
}
#[inline(always)]
fn parse_unit_time_designator<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Unit>, Error> {
if input.is_empty() {
return Err(err!(
"expected to find time unit designator suffix \
(H, M or S), but found end of input",
));
}
let unit = match input[0] {
b'H' | b'h' => Unit::Hour,
b'M' | b'm' => Unit::Minute,
b'S' | b's' => Unit::Second,
unknown => {
return Err(err!(
"expected to find time unit designator suffix \
(H, M or S), but found {found:?} instead",
found = escape::Byte(unknown),
));
}
};
Ok(Parsed { value: unit, input: &input[1..] })
}
#[inline(always)]
fn parse_duration_designator<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
if input.is_empty() {
return Err(err!(
"expected to find duration beginning with 'P' or 'p', \
but found end of input",
));
}
if !matches!(input[0], b'P' | b'p') {
return Err(err!(
"expected 'P' or 'p' prefix to begin duration, \
but found {found:?} instead",
found = escape::Byte(input[0]),
));
}
Ok(Parsed { value: (), input: &input[1..] })
}
#[inline(always)]
fn parse_time_designator<'i>(&self, input: &'i [u8]) -> Parsed<'i, bool> {
if input.is_empty() || !matches!(input[0], b'T' | b't') {
return Parsed { value: false, input };
}
Parsed { value: true, input: &input[1..] }
}
#[inline(always)]
fn parse_sign<'i>(&self, input: &'i [u8]) -> Parsed<'i, t::Sign> {
let Some(sign) = input.get(0).copied() else {
return Parsed { value: t::Sign::N::<1>(), input };
};
let sign = if sign == b'+' {
t::Sign::N::<1>()
} else if sign == b'-' {
t::Sign::N::<-1>()
} else {
return Parsed { value: t::Sign::N::<1>(), input };
};
Parsed { value: sign, input: &input[1..] }
}
}
fn span_fractional_time(
unit: Unit,
value: t::NoUnits,
fraction: t::SubsecNanosecond,
mut span: Span,
) -> Result<Span, Error> {
let value = t::NoUnits128::rfrom(value);
let fraction = t::NoUnits128::rfrom(fraction);
let mut nanos = match unit {
Unit::Hour => {
(value * t::NANOS_PER_HOUR) + (fraction * t::SECONDS_PER_HOUR)
}
Unit::Minute => {
(value * t::NANOS_PER_MINUTE) + (fraction * t::SECONDS_PER_MINUTE)
}
Unit::Second => (value * t::NANOS_PER_SECOND) + fraction,
_ => unreachable!("unsupported unit: {unit:?}"),
};
if unit >= Unit::Hour && nanos > 0 {
let mut hours = nanos / t::NANOS_PER_HOUR;
nanos %= t::NANOS_PER_HOUR;
if hours > t::SpanHours::MAX_SELF {
nanos += (hours - t::SpanHours::MAX_SELF) * t::NANOS_PER_HOUR;
hours = t::NoUnits128::rfrom(t::SpanHours::MAX_SELF);
}
span = span.try_hours_ranged(hours).unwrap();
}
if unit >= Unit::Minute && nanos > 0 {
let mut minutes = nanos / t::NANOS_PER_MINUTE;
nanos %= t::NANOS_PER_MINUTE;
if minutes > t::SpanMinutes::MAX_SELF {
nanos +=
(minutes - t::SpanMinutes::MAX_SELF) * t::NANOS_PER_MINUTE;
minutes = t::NoUnits128::rfrom(t::SpanMinutes::MAX_SELF);
}
span = span.try_minutes_ranged(minutes).unwrap();
}
if nanos > 0 {
let mut seconds = nanos / t::NANOS_PER_SECOND;
nanos %= t::NANOS_PER_SECOND;
if seconds > t::SpanSeconds::MAX_SELF {
nanos +=
(seconds - t::SpanSeconds::MAX_SELF) * t::NANOS_PER_SECOND;
seconds = t::NoUnits128::rfrom(t::SpanSeconds::MAX_SELF);
}
span = span.try_seconds_ranged(seconds).unwrap();
}
if nanos > 0 {
let mut millis = nanos / t::NANOS_PER_MILLI;
nanos %= t::NANOS_PER_MILLI;
if millis > t::SpanMilliseconds::MAX_SELF {
nanos +=
(millis - t::SpanMilliseconds::MAX_SELF) * t::NANOS_PER_MILLI;
millis = t::NoUnits128::rfrom(t::SpanMilliseconds::MAX_SELF);
}
span = span.try_milliseconds_ranged(millis).unwrap();
}
if nanos > 0 {
let mut micros = nanos / t::NANOS_PER_MICRO;
nanos %= t::NANOS_PER_MICRO;
if micros > t::SpanMicroseconds::MAX_SELF {
nanos +=
(micros - t::SpanMicroseconds::MAX_SELF) * t::NANOS_PER_MICRO;
micros = t::NoUnits128::rfrom(t::SpanMicroseconds::MAX_SELF);
}
span = span.try_microseconds_ranged(micros).unwrap();
}
if nanos > 0 {
span = span.try_nanoseconds_ranged(nanos).with_context(|| {
err!(
"failed to set nanosecond value {nanos} on span \
determined from {value}.{fraction}",
)
})?;
}
Ok(span)
}
fn duration_fractional_time(
unit: Unit,
fraction: t::SubsecNanosecond,
) -> SignedDuration {
let fraction = t::NoUnits::rfrom(fraction);
let nanos = match unit {
Unit::Hour => fraction * t::SECONDS_PER_HOUR,
Unit::Minute => fraction * t::SECONDS_PER_MINUTE,
Unit::Second => fraction,
_ => unreachable!("unsupported unit: {unit:?}"),
};
SignedDuration::from_nanos(nanos.get())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ok_signed_duration() {
let p =
|input| SpanParser::new().parse_signed_duration(input).unwrap();
insta::assert_debug_snapshot!(p(b"PT0s"), @r###"
Parsed {
value: 0s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT0.000000001s"), @r###"
Parsed {
value: 1ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1s"), @r###"
Parsed {
value: 1s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT59s"), @r###"
Parsed {
value: 59s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT60s"), @r###"
Parsed {
value: 60s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1m"), @r###"
Parsed {
value: 60s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1m0.000000001s"), @r###"
Parsed {
value: 60s1ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1.25m"), @r###"
Parsed {
value: 75s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1h"), @r###"
Parsed {
value: 3600s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1h0.000000001s"), @r###"
Parsed {
value: 3600s1ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1.25h"), @r###"
Parsed {
value: 4500s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"-PT2562047788015215h30m8.999999999s"), @r###"
Parsed {
value: -9223372036854775808s999999999ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT2562047788015215h30m7.999999999s"), @r###"
Parsed {
value: 9223372036854775807s999999999ns,
input: "",
}
"###);
}
#[test]
fn err_signed_duration() {
let p = |input| {
SpanParser::new().parse_signed_duration(input).unwrap_err()
};
insta::assert_snapshot!(
p(b"P0d"),
@"failed to parse ISO 8601 duration string into `SignedDuration`: parsing ISO 8601 duration into SignedDuration requires that the duration contain a time component and no components of days or greater",
);
insta::assert_snapshot!(
p(b"PT0d"),
@r###"failed to parse ISO 8601 duration string into `SignedDuration`: expected to find time unit designator suffix (H, M or S), but found "d" instead"###,
);
insta::assert_snapshot!(
p(b"P0dT1s"),
@"failed to parse ISO 8601 duration string into `SignedDuration`: parsing ISO 8601 duration into SignedDuration requires that the duration contain a time component and no components of days or greater",
);
insta::assert_snapshot!(
p(b""),
@"failed to parse ISO 8601 duration string into `SignedDuration`: expected to find duration beginning with 'P' or 'p', but found end of input",
);
insta::assert_snapshot!(
p(b"P"),
@"failed to parse ISO 8601 duration string into `SignedDuration`: parsing ISO 8601 duration into SignedDuration requires that the duration contain a time component and no components of days or greater",
);
insta::assert_snapshot!(
p(b"PT"),
@"failed to parse ISO 8601 duration string into `SignedDuration`: expected at least one unit of time (hours, minutes or seconds) in ISO 8601 duration when parsing into a `SignedDuration`",
);
insta::assert_snapshot!(
p(b"PTs"),
@"failed to parse ISO 8601 duration string into `SignedDuration`: expected at least one unit of time (hours, minutes or seconds) in ISO 8601 duration when parsing into a `SignedDuration`",
);
insta::assert_snapshot!(
p(b"PT1s1m"),
@"failed to parse ISO 8601 duration string into `SignedDuration`: found value 1 with unit minute after unit second, but units must be written from largest to smallest (and they can't be repeated)",
);
insta::assert_snapshot!(
p(b"PT1s1h"),
@"failed to parse ISO 8601 duration string into `SignedDuration`: found value 1 with unit hour after unit second, but units must be written from largest to smallest (and they can't be repeated)",
);
insta::assert_snapshot!(
p(b"PT1m1h"),
@"failed to parse ISO 8601 duration string into `SignedDuration`: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)",
);
insta::assert_snapshot!(
p(b"-PT9223372036854775809s"),
@r###"failed to parse ISO 8601 duration string into `SignedDuration`: failed to parse "9223372036854775809" as 64-bit signed integer: number '9223372036854775809' too big to parse into 64-bit integer"###,
);
insta::assert_snapshot!(
p(b"PT9223372036854775808s"),
@r###"failed to parse ISO 8601 duration string into `SignedDuration`: failed to parse "9223372036854775808" as 64-bit signed integer: number '9223372036854775808' too big to parse into 64-bit integer"###,
);
insta::assert_snapshot!(
p(b"PT1m9223372036854775807s"),
@"failed to parse ISO 8601 duration string into `SignedDuration`: adding value 9223372036854775807 from unit second overflowed signed duration 60s",
);
insta::assert_snapshot!(
p(b"PT2562047788015215.6h"),
@"failed to parse ISO 8601 duration string into `SignedDuration`: adding fractional duration 2160s from unit hour to 9223372036854774000s overflowed signed duration limits",
);
}
#[test]
fn ok_temporal_duration_basic() {
let p =
|input| SpanParser::new().parse_temporal_duration(input).unwrap();
insta::assert_debug_snapshot!(p(b"P5d"), @r###"
Parsed {
value: P5d,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"-P5d"), @r###"
Parsed {
value: -P5d,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"+P5d"), @r###"
Parsed {
value: P5d,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"P5DT1s"), @r###"
Parsed {
value: P5dT1s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1S"), @r###"
Parsed {
value: PT1s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT0S"), @r###"
Parsed {
value: PT0s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"P0Y"), @r###"
Parsed {
value: PT0s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"P1Y1M1W1DT1H1M1S"), @r###"
Parsed {
value: P1y1m1w1dT1h1m1s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"P1y1m1w1dT1h1m1s"), @r###"
Parsed {
value: P1y1m1w1dT1h1m1s,
input: "",
}
"###);
}
#[test]
fn ok_temporal_duration_fractional() {
let p =
|input| SpanParser::new().parse_temporal_duration(input).unwrap();
insta::assert_debug_snapshot!(p(b"PT0.5h"), @r###"
Parsed {
value: PT30m,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT0.123456789h"), @r###"
Parsed {
value: PT7m24.4444404s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1.123456789h"), @r###"
Parsed {
value: PT1h7m24.4444404s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT0.5m"), @r###"
Parsed {
value: PT30s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT0.123456789m"), @r###"
Parsed {
value: PT7.40740734s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1.123456789m"), @r###"
Parsed {
value: PT1m7.40740734s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT0.5s"), @r###"
Parsed {
value: PT0.5s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT0.123456789s"), @r###"
Parsed {
value: PT0.123456789s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1.123456789s"), @r###"
Parsed {
value: PT1.123456789s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1902545624836.854775807s"), @r###"
Parsed {
value: PT1902545624836.854775807s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT175307616h10518456960m640330789636.854775807s"), @r###"
Parsed {
value: PT175307616h10518456960m640330789636.854775807s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"-PT1902545624836.854775807s"), @r###"
Parsed {
value: -PT1902545624836.854775807s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"-PT175307616h10518456960m640330789636.854775807s"), @r###"
Parsed {
value: -PT175307616h10518456960m640330789636.854775807s,
input: "",
}
"###);
}
#[test]
fn ok_temporal_duration_unbalanced() {
let p =
|input| SpanParser::new().parse_temporal_duration(input).unwrap();
insta::assert_debug_snapshot!(
p(b"PT175307616h10518456960m1774446656760s"), @r###"
Parsed {
value: PT175307616h10518456960m1774446656760s,
input: "",
}
"###);
insta::assert_debug_snapshot!(
p(b"Pt843517082H"), @r###"
Parsed {
value: PT175307616h10518456960m1774446660000s,
input: "",
}
"###);
insta::assert_debug_snapshot!(
p(b"Pt843517081H"), @r###"
Parsed {
value: PT175307616h10518456960m1774446656400s,
input: "",
}
"###);
}
#[test]
fn ok_temporal_datetime_basic() {
let p = |input| {
DateTimeParser::new().parse_temporal_datetime(input).unwrap()
};
insta::assert_debug_snapshot!(p(b"2024-06-01"), @r###"
Parsed {
value: ParsedDateTime {
input: "2024-06-01",
date: ParsedDate {
input: "2024-06-01",
date: 2024-06-01,
},
time: None,
offset: None,
annotations: ParsedAnnotations {
input: "",
time_zone: None,
},
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01[America/New_York]"), @r###"
Parsed {
value: ParsedDateTime {
input: "2024-06-01[America/New_York]",
date: ParsedDate {
input: "2024-06-01",
date: 2024-06-01,
},
time: None,
offset: None,
annotations: ParsedAnnotations {
input: "[America/New_York]",
time_zone: Some(
Named {
critical: false,
name: "America/New_York",
},
),
},
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03"), @r###"
Parsed {
value: ParsedDateTime {
input: "2024-06-01T01:02:03",
date: ParsedDate {
input: "2024-06-01",
date: 2024-06-01,
},
time: Some(
ParsedTime {
input: "01:02:03",
time: 01:02:03,
extended: true,
},
),
offset: None,
annotations: ParsedAnnotations {
input: "",
time_zone: None,
},
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-05"), @r###"
Parsed {
value: ParsedDateTime {
input: "2024-06-01T01:02:03-05",
date: ParsedDate {
input: "2024-06-01",
date: 2024-06-01,
},
time: Some(
ParsedTime {
input: "01:02:03",
time: 01:02:03,
extended: true,
},
),
offset: Some(
ParsedOffset {
kind: Numeric(
-05,
),
},
),
annotations: ParsedAnnotations {
input: "",
time_zone: None,
},
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-05[America/New_York]"), @r###"
Parsed {
value: ParsedDateTime {
input: "2024-06-01T01:02:03-05[America/New_York]",
date: ParsedDate {
input: "2024-06-01",
date: 2024-06-01,
},
time: Some(
ParsedTime {
input: "01:02:03",
time: 01:02:03,
extended: true,
},
),
offset: Some(
ParsedOffset {
kind: Numeric(
-05,
),
},
),
annotations: ParsedAnnotations {
input: "[America/New_York]",
time_zone: Some(
Named {
critical: false,
name: "America/New_York",
},
),
},
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03Z[America/New_York]"), @r###"
Parsed {
value: ParsedDateTime {
input: "2024-06-01T01:02:03Z[America/New_York]",
date: ParsedDate {
input: "2024-06-01",
date: 2024-06-01,
},
time: Some(
ParsedTime {
input: "01:02:03",
time: 01:02:03,
extended: true,
},
),
offset: Some(
ParsedOffset {
kind: Zulu,
},
),
annotations: ParsedAnnotations {
input: "[America/New_York]",
time_zone: Some(
Named {
critical: false,
name: "America/New_York",
},
),
},
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-01[America/New_York]"), @r###"
Parsed {
value: ParsedDateTime {
input: "2024-06-01T01:02:03-01[America/New_York]",
date: ParsedDate {
input: "2024-06-01",
date: 2024-06-01,
},
time: Some(
ParsedTime {
input: "01:02:03",
time: 01:02:03,
extended: true,
},
),
offset: Some(
ParsedOffset {
kind: Numeric(
-01,
),
},
),
annotations: ParsedAnnotations {
input: "[America/New_York]",
time_zone: Some(
Named {
critical: false,
name: "America/New_York",
},
),
},
},
input: "",
}
"###);
}
#[test]
fn ok_temporal_datetime_incomplete() {
let p = |input| {
DateTimeParser::new().parse_temporal_datetime(input).unwrap()
};
insta::assert_debug_snapshot!(p(b"2024-06-01T01"), @r###"
Parsed {
value: ParsedDateTime {
input: "2024-06-01T01",
date: ParsedDate {
input: "2024-06-01",
date: 2024-06-01,
},
time: Some(
ParsedTime {
input: "01",
time: 01:00:00,
extended: false,
},
),
offset: None,
annotations: ParsedAnnotations {
input: "",
time_zone: None,
},
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01T0102"), @r###"
Parsed {
value: ParsedDateTime {
input: "2024-06-01T0102",
date: ParsedDate {
input: "2024-06-01",
date: 2024-06-01,
},
time: Some(
ParsedTime {
input: "0102",
time: 01:02:00,
extended: false,
},
),
offset: None,
annotations: ParsedAnnotations {
input: "",
time_zone: None,
},
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01T01:02"), @r###"
Parsed {
value: ParsedDateTime {
input: "2024-06-01T01:02",
date: ParsedDate {
input: "2024-06-01",
date: 2024-06-01,
},
time: Some(
ParsedTime {
input: "01:02",
time: 01:02:00,
extended: true,
},
),
offset: None,
annotations: ParsedAnnotations {
input: "",
time_zone: None,
},
},
input: "",
}
"###);
}
#[test]
fn ok_temporal_datetime_separator() {
let p = |input| {
DateTimeParser::new().parse_temporal_datetime(input).unwrap()
};
insta::assert_debug_snapshot!(p(b"2024-06-01t01:02:03"), @r###"
Parsed {
value: ParsedDateTime {
input: "2024-06-01t01:02:03",
date: ParsedDate {
input: "2024-06-01",
date: 2024-06-01,
},
time: Some(
ParsedTime {
input: "01:02:03",
time: 01:02:03,
extended: true,
},
),
offset: None,
annotations: ParsedAnnotations {
input: "",
time_zone: None,
},
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01 01:02:03"), @r###"
Parsed {
value: ParsedDateTime {
input: "2024-06-01 01:02:03",
date: ParsedDate {
input: "2024-06-01",
date: 2024-06-01,
},
time: Some(
ParsedTime {
input: "01:02:03",
time: 01:02:03,
extended: true,
},
),
offset: None,
annotations: ParsedAnnotations {
input: "",
time_zone: None,
},
},
input: "",
}
"###);
}
#[test]
fn ok_temporal_time_basic() {
let p =
|input| DateTimeParser::new().parse_temporal_time(input).unwrap();
insta::assert_debug_snapshot!(p(b"01:02:03"), @r###"
Parsed {
value: ParsedTime {
input: "01:02:03",
time: 01:02:03,
extended: true,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"130113"), @r###"
Parsed {
value: ParsedTime {
input: "130113",
time: 13:01:13,
extended: false,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"T01:02:03"), @r###"
Parsed {
value: ParsedTime {
input: "01:02:03",
time: 01:02:03,
extended: true,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"T010203"), @r###"
Parsed {
value: ParsedTime {
input: "010203",
time: 01:02:03,
extended: false,
},
input: "",
}
"###);
}
#[test]
fn ok_temporal_time_from_full_datetime() {
let p =
|input| DateTimeParser::new().parse_temporal_time(input).unwrap();
insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03"), @r###"
Parsed {
value: ParsedTime {
input: "01:02:03",
time: 01:02:03,
extended: true,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03.123"), @r###"
Parsed {
value: ParsedTime {
input: "01:02:03.123",
time: 01:02:03.123,
extended: true,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01T01"), @r###"
Parsed {
value: ParsedTime {
input: "01",
time: 01:00:00,
extended: false,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01T0102"), @r###"
Parsed {
value: ParsedTime {
input: "0102",
time: 01:02:00,
extended: false,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01T010203"), @r###"
Parsed {
value: ParsedTime {
input: "010203",
time: 01:02:03,
extended: false,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2024-06-01T010203-05"), @r###"
Parsed {
value: ParsedTime {
input: "010203",
time: 01:02:03,
extended: false,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(
p(b"2024-06-01T010203-05[America/New_York]"), @r###"
Parsed {
value: ParsedTime {
input: "010203",
time: 01:02:03,
extended: false,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(
p(b"2024-06-01T010203[America/New_York]"), @r###"
Parsed {
value: ParsedTime {
input: "010203",
time: 01:02:03,
extended: false,
},
input: "",
}
"###);
}
#[test]
fn err_temporal_time_ambiguous() {
let p = |input| {
DateTimeParser::new().parse_temporal_time(input).unwrap_err()
};
insta::assert_snapshot!(
p(b"010203"),
@r###"parsed time from "010203" is ambiguous with a month-day date"###,
);
insta::assert_snapshot!(
p(b"130112"),
@r###"parsed time from "130112" is ambiguous with a year-month date"###,
);
}
#[test]
fn err_temporal_time_missing_time() {
let p = |input| {
DateTimeParser::new().parse_temporal_time(input).unwrap_err()
};
insta::assert_snapshot!(
p(b"2024-06-01[America/New_York]"),
@r###"successfully parsed date from "2024-06-01[America/New_York]", but no time component was found"###,
);
insta::assert_snapshot!(
p(b"2099-12-01[America/New_York]"),
@r###"successfully parsed date from "2099-12-01[America/New_York]", but no time component was found"###,
);
insta::assert_snapshot!(
p(b"2099-13-01[America/New_York]"),
@r###"failed to parse minute in time "2099-13-01[America/New_York]": minute is not valid: parameter 'minute' with value 99 is not in the required range of 0..=59"###,
);
}
#[test]
fn err_temporal_time_zulu() {
let p = |input| {
DateTimeParser::new().parse_temporal_time(input).unwrap_err()
};
insta::assert_snapshot!(
p(b"T00:00:00Z"),
@"cannot parse civil time from string with a Zulu offset, parse as a `Timestamp` and convert to a civil time instead",
);
insta::assert_snapshot!(
p(b"00:00:00Z"),
@"cannot parse plain time from string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead",
);
insta::assert_snapshot!(
p(b"000000Z"),
@"cannot parse plain time from string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead",
);
insta::assert_snapshot!(
p(b"2099-12-01T00:00:00Z"),
@"cannot parse plain time from full datetime string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead",
);
}
#[test]
fn ok_date_basic() {
let p = |input| DateTimeParser::new().parse_date_spec(input).unwrap();
insta::assert_debug_snapshot!(p(b"2010-03-14"), @r###"
Parsed {
value: ParsedDate {
input: "2010-03-14",
date: 2010-03-14,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"20100314"), @r###"
Parsed {
value: ParsedDate {
input: "20100314",
date: 2010-03-14,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"2010-03-14T01:02:03"), @r###"
Parsed {
value: ParsedDate {
input: "2010-03-14",
date: 2010-03-14,
},
input: "T01:02:03",
}
"###);
insta::assert_debug_snapshot!(p(b"-009999-03-14"), @r###"
Parsed {
value: ParsedDate {
input: "-009999-03-14",
date: -009999-03-14,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"+009999-03-14"), @r###"
Parsed {
value: ParsedDate {
input: "+009999-03-14",
date: 9999-03-14,
},
input: "",
}
"###);
}
#[test]
fn err_date_empty() {
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"").unwrap_err(),
@r###"failed to parse year in date "": expected four digit year (or leading sign for six digit year), but found end of input"###,
);
}
#[test]
fn err_date_year() {
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"123").unwrap_err(),
@r###"failed to parse year in date "123": expected four digit year (or leading sign for six digit year), but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"123a").unwrap_err(),
@r###"failed to parse year in date "123a": failed to parse "123a" as year (a four digit integer): invalid digit, expected 0-9 but got a"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"-9999").unwrap_err(),
@r###"failed to parse year in date "-9999": expected six digit year (because of a leading sign), but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"+9999").unwrap_err(),
@r###"failed to parse year in date "+9999": expected six digit year (because of a leading sign), but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"-99999").unwrap_err(),
@r###"failed to parse year in date "-99999": expected six digit year (because of a leading sign), but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"+99999").unwrap_err(),
@r###"failed to parse year in date "+99999": expected six digit year (because of a leading sign), but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"-99999a").unwrap_err(),
@r###"failed to parse year in date "-99999a": failed to parse "99999a" as year (a six digit integer): invalid digit, expected 0-9 but got a"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"+999999").unwrap_err(),
@r###"failed to parse year in date "+999999": year is not valid: parameter 'year' with value 999999 is not in the required range of -9999..=9999"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"-010000").unwrap_err(),
@r###"failed to parse year in date "-010000": year is not valid: parameter 'year' with value 10000 is not in the required range of -9999..=9999"###,
);
}
#[test]
fn err_date_month() {
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"2024-").unwrap_err(),
@r###"failed to parse month in date "2024-": expected two digit month, but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"2024").unwrap_err(),
@r###"failed to parse month in date "2024": expected two digit month, but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"2024-13-01").unwrap_err(),
@r###"failed to parse month in date "2024-13-01": month is not valid: parameter 'month' with value 13 is not in the required range of 1..=12"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"20241301").unwrap_err(),
@r###"failed to parse month in date "20241301": month is not valid: parameter 'month' with value 13 is not in the required range of 1..=12"###,
);
}
#[test]
fn err_date_day() {
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"2024-12-").unwrap_err(),
@r###"failed to parse day in date "2024-12-": expected two digit day, but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"202412").unwrap_err(),
@r###"failed to parse day in date "202412": expected two digit day, but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"2024-12-40").unwrap_err(),
@r###"failed to parse day in date "2024-12-40": day is not valid: parameter 'day' with value 40 is not in the required range of 1..=31"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"2024-11-31").unwrap_err(),
@r###"date parsed from "2024-11-31" is not valid: parameter 'day' with value 31 is not in the required range of 1..=30"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"2024-02-30").unwrap_err(),
@r###"date parsed from "2024-02-30" is not valid: parameter 'day' with value 30 is not in the required range of 1..=29"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"2023-02-29").unwrap_err(),
@r###"date parsed from "2023-02-29" is not valid: parameter 'day' with value 29 is not in the required range of 1..=28"###,
);
}
#[test]
fn err_date_separator() {
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"2024-1231").unwrap_err(),
@r###"failed to parse separator after month: expected '-' separator, but found "3" instead"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date_spec(b"202412-31").unwrap_err(),
@"failed to parse separator after month: expected no separator after month since none was found after the year, but found a '-' separator",
);
}
#[test]
fn ok_time_basic() {
let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap();
insta::assert_debug_snapshot!(p(b"01:02:03"), @r###"
Parsed {
value: ParsedTime {
input: "01:02:03",
time: 01:02:03,
extended: true,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"010203"), @r###"
Parsed {
value: ParsedTime {
input: "010203",
time: 01:02:03,
extended: false,
},
input: "",
}
"###);
}
#[test]
fn ok_time_fractional() {
let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap();
insta::assert_debug_snapshot!(p(b"01:02:03.123456789"), @r###"
Parsed {
value: ParsedTime {
input: "01:02:03.123456789",
time: 01:02:03.123456789,
extended: true,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"010203.123456789"), @r###"
Parsed {
value: ParsedTime {
input: "010203.123456789",
time: 01:02:03.123456789,
extended: false,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"01:02:03.9"), @r###"
Parsed {
value: ParsedTime {
input: "01:02:03.9",
time: 01:02:03.9,
extended: true,
},
input: "",
}
"###);
}
#[test]
fn ok_time_no_fractional() {
let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap();
insta::assert_debug_snapshot!(p(b"01:02.123456789"), @r###"
Parsed {
value: ParsedTime {
input: "01:02",
time: 01:02:00,
extended: true,
},
input: ".123456789",
}
"###);
}
#[test]
fn ok_time_leap() {
let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap();
insta::assert_debug_snapshot!(p(b"01:02:60"), @r###"
Parsed {
value: ParsedTime {
input: "01:02:60",
time: 01:02:59,
extended: true,
},
input: "",
}
"###);
}
#[test]
fn ok_time_mixed_format() {
let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap();
insta::assert_debug_snapshot!(p(b"01:0203"), @r###"
Parsed {
value: ParsedTime {
input: "01:02",
time: 01:02:00,
extended: true,
},
input: "03",
}
"###);
insta::assert_debug_snapshot!(p(b"0102:03"), @r###"
Parsed {
value: ParsedTime {
input: "0102",
time: 01:02:00,
extended: false,
},
input: ":03",
}
"###);
}
#[test]
fn err_time_empty() {
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"").unwrap_err(),
@r###"failed to parse hour in time "": expected two digit hour, but found end of input"###,
);
}
#[test]
fn err_time_hour() {
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"a").unwrap_err(),
@r###"failed to parse hour in time "a": expected two digit hour, but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"1a").unwrap_err(),
@r###"failed to parse hour in time "1a": failed to parse "1a" as hour (a two digit integer): invalid digit, expected 0-9 but got a"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"24").unwrap_err(),
@r###"failed to parse hour in time "24": hour is not valid: parameter 'hour' with value 24 is not in the required range of 0..=23"###,
);
}
#[test]
fn err_time_minute() {
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"01:").unwrap_err(),
@r###"failed to parse minute in time "01:": expected two digit minute, but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"01:a").unwrap_err(),
@r###"failed to parse minute in time "01:a": expected two digit minute, but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"01:1a").unwrap_err(),
@r###"failed to parse minute in time "01:1a": failed to parse "1a" as minute (a two digit integer): invalid digit, expected 0-9 but got a"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"01:60").unwrap_err(),
@r###"failed to parse minute in time "01:60": minute is not valid: parameter 'minute' with value 60 is not in the required range of 0..=59"###,
);
}
#[test]
fn err_time_second() {
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"01:02:").unwrap_err(),
@r###"failed to parse second in time "01:02:": expected two digit second, but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"01:02:a").unwrap_err(),
@r###"failed to parse second in time "01:02:a": expected two digit second, but found end of input"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"01:02:1a").unwrap_err(),
@r###"failed to parse second in time "01:02:1a": failed to parse "1a" as second (a two digit integer): invalid digit, expected 0-9 but got a"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"01:02:61").unwrap_err(),
@r###"failed to parse second in time "01:02:61": second is not valid: parameter 'second' with value 61 is not in the required range of 0..=59"###,
);
}
#[test]
fn err_time_fractional() {
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"01:02:03.").unwrap_err(),
@r###"failed to parse fractional nanoseconds in time "01:02:03.": found decimal after seconds component, but did not find any decimal digits after decimal"###,
);
insta::assert_snapshot!(
DateTimeParser::new().parse_time_spec(b"01:02:03.a").unwrap_err(),
@r###"failed to parse fractional nanoseconds in time "01:02:03.a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
);
}
}