use crate::{
civil::{Date, DateTime, Time},
error::{err, Error, ErrorContext},
fmt::{
offset::{self, ParsedOffset},
rfc9557::{self, ParsedAnnotations},
temporal::Pieces,
util::{
fractional_time_to_duration, fractional_time_to_span,
parse_temporal_fraction,
},
Parsed,
},
span::Span,
tz::{
AmbiguousZoned, Disambiguation, Offset, OffsetConflict, TimeZone,
TimeZoneDatabase,
},
util::{
escape, parse,
t::{self, C},
},
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_pieces(&self) -> Result<Pieces<'i>, Error> {
let mut pieces = Pieces::from(self.date.date);
if let Some(ref time) = self.time {
pieces = pieces.with_time(time.time);
}
if let Some(ref offset) = self.offset {
pieces = pieces.with_offset(offset.to_pieces_offset()?);
}
if let Some(ann) = self.annotations.to_time_zone_annotation()? {
pieces = pieces.with_time_zone_annotation(ann);
}
Ok(pieces)
}
#[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_annotation =
self.annotations.to_time_zone_annotation()?.ok_or_else(|| {
err!(
"failed to find time zone in square brackets \
in {:?}, which is required for parsing a zoned instant",
self.input,
)
})?;
let tz = tz_annotation.to_time_zone_with(db)?;
let Some(ref parsed_offset) = self.offset else {
return Ok(tz.into_ambiguous_zoned(dt));
};
if parsed_offset.is_zulu() {
return OffsetConflict::AlwaysOffset
.resolve(dt, Offset::UTC, tz)
.with_context(|| {
err!("parsing {input:?} failed", input = self.input)
});
}
let offset = parsed_offset.to_offset()?;
let is_equal = |parsed: Offset, candidate: Offset| {
if parsed == candidate {
return true;
}
if candidate.part_seconds_ranged() == C(0) {
return parsed == candidate;
}
let Ok(candidate) = candidate.round(Unit::Minute) else {
return parsed == candidate;
};
parsed == candidate
};
offset_conflict.resolve_with(dt, offset, tz, is_equal).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 ParsedTimeZone<'i> {
input: escape::Bytes<'i>,
kind: ParsedTimeZoneKind<'i>,
}
impl<'i> core::fmt::Display for ParsedTimeZone<'i> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(&self.input, f)
}
}
#[derive(Debug)]
pub(super) enum ParsedTimeZoneKind<'i> {
Named(&'i str),
Offset(ParsedOffset),
#[cfg(feature = "alloc")]
Posix(crate::tz::posix::PosixTimeZoneOwned),
}
impl<'i> ParsedTimeZone<'i> {
pub(super) fn into_time_zone(
self,
db: &TimeZoneDatabase,
) -> Result<TimeZone, Error> {
match self.kind {
ParsedTimeZoneKind::Named(iana_name) => {
let tz = db.get(iana_name).with_context(|| {
err!(
"parsed apparent IANA time zone identifier \
{iana_name} from {input}, but the tzdb lookup \
failed",
input = self.input,
)
})?;
Ok(tz)
}
ParsedTimeZoneKind::Offset(poff) => {
let offset = poff.to_offset().with_context(|| {
err!(
"offset successfully parsed from {input}, \
but failed to convert to numeric `Offset`",
input = self.input,
)
})?;
Ok(TimeZone::fixed(offset))
}
#[cfg(feature = "alloc")]
ParsedTimeZoneKind::Posix(posix_tz) => {
Ok(TimeZone::from_posix_tz(posix_tz))
}
}
}
}
#[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)]
pub(super) fn parse_time_zone<'i>(
&self,
mut input: &'i [u8],
) -> Result<Parsed<'i, ParsedTimeZone<'i>>, Error> {
let Some(first) = input.first().copied() else {
return Err(err!("an empty string is not a valid time zone"));
};
let original = escape::Bytes(input);
if matches!(first, b'+' | b'-') {
static P: offset::Parser = offset::Parser::new()
.zulu(false)
.subminute(true)
.subsecond(false);
let Parsed { value: offset, input } = P.parse(input)?;
let kind = ParsedTimeZoneKind::Offset(offset);
let value = ParsedTimeZone { input: original, kind };
return Ok(Parsed { value, input });
}
let mknamed = |consumed, remaining| {
let Ok(tzid) = core::str::from_utf8(consumed) else {
return Err(err!(
"found plausible IANA time zone identifier \
{input:?}, but it is not valid UTF-8",
input = escape::Bytes(consumed),
));
};
let kind = ParsedTimeZoneKind::Named(tzid);
let value = ParsedTimeZone { input: original, kind };
Ok(Parsed { value, input: remaining })
};
let mkconsumed = parse::slicer(input);
let mut saw_number = false;
loop {
let Some(byte) = input.first().copied() else { break };
if byte.is_ascii_whitespace() {
break;
}
saw_number = saw_number || byte.is_ascii_digit();
input = &input[1..];
}
let consumed = mkconsumed(input);
if !saw_number {
return mknamed(consumed, input);
}
#[cfg(not(feature = "alloc"))]
{
Err(err!(
"cannot parsed time zones other than fixed offsets \
without the `alloc` crate feature enabled",
))
}
#[cfg(feature = "alloc")]
{
use crate::tz::posix::PosixTimeZone;
match PosixTimeZone::parse_prefix(consumed) {
Ok((posix_tz, input)) => {
let kind = ParsedTimeZoneKind::Posix(posix_tz);
let value = ParsedTimeZone { input: original, kind };
Ok(Parsed { value, input })
}
Err(_) => mknamed(consumed, 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 == C(0) && sign < C(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();
if input.is_empty() || input[0] != b'[' {
let value = ParsedAnnotations::none();
return Ok(Parsed { input, value });
}
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 < C(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 == C(-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 = fractional_time_to_span(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(_) => fractional_time_to_span(
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 =
fractional_time_to_duration(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..] }
}
}
#[cfg(feature = "alloc")]
#[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: 1m,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1m"), @r###"
Parsed {
value: 1m,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1m0.000000001s"), @r###"
Parsed {
value: 1m 1ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1.25m"), @r###"
Parsed {
value: 1m 15s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1h"), @r###"
Parsed {
value: 1h,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1h0.000000001s"), @r###"
Parsed {
value: 1h 1ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1.25h"), @r###"
Parsed {
value: 1h 15m,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"-PT2562047788015215h30m8.999999999s"), @r###"
Parsed {
value: 2562047788015215h 30m 8s 999ms 999µs 999ns ago,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT2562047788015215h30m7.999999999s"), @r###"
Parsed {
value: 2562047788015215h 30m 7s 999ms 999µs 999ns,
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 1m",
);
insta::assert_snapshot!(
p(b"PT2562047788015215.6h"),
@"failed to parse ISO 8601 duration string into `SignedDuration`: adding fractional duration 36m from unit hour to 2562047788015215h 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: 5d,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"-P5d"), @r###"
Parsed {
value: 5d ago,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"+P5d"), @r###"
Parsed {
value: 5d,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"P5DT1s"), @r###"
Parsed {
value: 5d 1s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1S"), @r###"
Parsed {
value: 1s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT0S"), @r###"
Parsed {
value: 0s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"P0Y"), @r###"
Parsed {
value: 0s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"P1Y1M1W1DT1H1M1S"), @r###"
Parsed {
value: 1y 1mo 1w 1d 1h 1m 1s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"P1y1m1w1dT1h1m1s"), @r###"
Parsed {
value: 1y 1mo 1w 1d 1h 1m 1s,
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: 30m,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT0.123456789h"), @r###"
Parsed {
value: 7m 24s 444ms 440µs 400ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1.123456789h"), @r###"
Parsed {
value: 1h 7m 24s 444ms 440µs 400ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT0.5m"), @r###"
Parsed {
value: 30s,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT0.123456789m"), @r###"
Parsed {
value: 7s 407ms 407µs 340ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1.123456789m"), @r###"
Parsed {
value: 1m 7s 407ms 407µs 340ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT0.5s"), @r###"
Parsed {
value: 500ms,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT0.123456789s"), @r###"
Parsed {
value: 123ms 456µs 789ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1.123456789s"), @r###"
Parsed {
value: 1s 123ms 456µs 789ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT1902545624836.854775807s"), @r###"
Parsed {
value: 631107417600s 631107417600000ms 631107417600000000µs 9223372036854775807ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"PT175307616h10518456960m640330789636.854775807s"), @r###"
Parsed {
value: 175307616h 10518456960m 631107417600s 9223372036854ms 775µs 807ns,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"-PT1902545624836.854775807s"), @r###"
Parsed {
value: 631107417600s 631107417600000ms 631107417600000000µs 9223372036854775807ns ago,
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"-PT175307616h10518456960m640330789636.854775807s"), @r###"
Parsed {
value: 175307616h 10518456960m 631107417600s 9223372036854ms 775µs 807ns ago,
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: 175307616h 10518456960m 631107417600s 631107417600000ms 512231821560000000µs,
input: "",
}
"###);
insta::assert_debug_snapshot!(
p(b"Pt843517082H"), @r###"
Parsed {
value: 175307616h 10518456960m 631107417600s 631107417600000ms 512231824800000000µs,
input: "",
}
"###);
insta::assert_debug_snapshot!(
p(b"Pt843517081H"), @r###"
Parsed {
value: 175307616h 10518456960m 631107417600s 631107417600000ms 512231821200000000µs,
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"###,
);
}
}