use core::fmt::Write;
use crate::{
civil::Weekday,
error::{err, ErrorContext},
fmt::strtime::{BrokenDownTime, Extension, Flag, Meridiem},
tz::Offset,
util::{
escape, parse,
rangeint::{ri8, RFrom},
t::{self, C},
},
Error, Timestamp,
};
type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>;
type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>;
type ParsedOffsetSeconds = ri8<0, { t::SpanZoneOffsetSeconds::MAX }>;
pub(super) struct Parser<'f, 'i, 't> {
pub(super) fmt: &'f [u8],
pub(super) inp: &'i [u8],
pub(super) tm: &'t mut BrokenDownTime,
}
impl<'f, 'i, 't> Parser<'f, 'i, 't> {
pub(super) fn parse(&mut self) -> Result<(), Error> {
while !self.fmt.is_empty() {
if self.f() != b'%' {
self.parse_literal()?;
continue;
}
if !self.bump_fmt() {
return Err(err!(
"invalid format string, expected byte after '%', \
but found end of format string",
));
}
if self.inp.is_empty() && self.f() != b'.' {
return Err(err!(
"expected non-empty input for directive %{directive}, \
but found end of input",
directive = escape::Byte(self.f()),
));
}
let ext = self.parse_extension()?;
match self.f() {
b'%' => self.parse_percent().context("%% failed")?,
b'A' => self.parse_weekday_full().context("%A failed")?,
b'a' => self.parse_weekday_abbrev().context("%a failed")?,
b'B' => self.parse_month_name_full().context("%B failed")?,
b'b' => self.parse_month_name_abbrev().context("%b failed")?,
b'C' => self.parse_century(ext).context("%C failed")?,
b'D' => self.parse_american_date().context("%D failed")?,
b'd' => self.parse_day(ext).context("%d failed")?,
b'e' => self.parse_day(ext).context("%e failed")?,
b'F' => self.parse_iso_date().context("%F failed")?,
b'f' => self.parse_fractional(ext).context("%f failed")?,
b'G' => self.parse_iso_week_year(ext).context("%G failed")?,
b'g' => self.parse_iso_week_year2(ext).context("%g failed")?,
b'H' => self.parse_hour24(ext).context("%H failed")?,
b'h' => self.parse_month_name_abbrev().context("%h failed")?,
b'I' => self.parse_hour12(ext).context("%I failed")?,
b'j' => self.parse_day_of_year(ext).context("%j failed")?,
b'k' => self.parse_hour24(ext).context("%k failed")?,
b'l' => self.parse_hour12(ext).context("%l failed")?,
b'M' => self.parse_minute(ext).context("%M failed")?,
b'm' => self.parse_month(ext).context("%m failed")?,
b'n' => self.parse_whitespace().context("%n failed")?,
b'P' => self.parse_ampm().context("%P failed")?,
b'p' => self.parse_ampm().context("%p failed")?,
b'Q' => self.parse_iana_nocolon().context("%Q failed")?,
b'R' => self.parse_clock_nosecs().context("%R failed")?,
b'S' => self.parse_second(ext).context("%S failed")?,
b's' => self.parse_timestamp(ext).context("%s failed")?,
b'T' => self.parse_clock_secs().context("%T failed")?,
b't' => self.parse_whitespace().context("%t failed")?,
b'U' => self.parse_week_sun(ext).context("%U failed")?,
b'u' => self.parse_weekday_mon(ext).context("%u failed")?,
b'V' => self.parse_week_iso(ext).context("%V failed")?,
b'W' => self.parse_week_mon(ext).context("%W failed")?,
b'w' => self.parse_weekday_sun(ext).context("%w failed")?,
b'Y' => self.parse_year(ext).context("%Y failed")?,
b'y' => self.parse_year2(ext).context("%y failed")?,
b'z' => self.parse_offset_nocolon().context("%z failed")?,
b':' => {
if !self.bump_fmt() {
return Err(err!(
"invalid format string, expected directive \
after '%:'",
));
}
match self.f() {
b'Q' => {
self.parse_iana_colon().context("%:Q failed")?
}
b'z' => {
self.parse_offset_colon().context("%:z failed")?
}
unk => {
return Err(err!(
"found unrecognized directive %{unk} \
following %:",
unk = escape::Byte(unk),
));
}
}
}
b'Z' => {
return Err(err!("cannot parse time zone abbreviations"));
}
b'.' => {
if !self.bump_fmt() {
return Err(err!(
"invalid format string, expected directive \
after '%.'",
));
}
let (width, fmt) = Extension::parse_width(self.fmt)?;
let ext = Extension { width, ..ext };
self.fmt = fmt;
match self.f() {
b'f' => self
.parse_dot_fractional(ext)
.context("%.f failed")?,
unk => {
return Err(err!(
"found unrecognized directive %{unk} \
following %.",
unk = escape::Byte(unk),
));
}
}
}
unk => {
return Err(err!(
"found unrecognized directive %{unk}",
unk = escape::Byte(unk),
));
}
}
}
Ok(())
}
fn f(&self) -> u8 {
self.fmt[0]
}
fn i(&self) -> u8 {
self.inp[0]
}
fn bump_fmt(&mut self) -> bool {
self.fmt = &self.fmt[1..];
!self.fmt.is_empty()
}
fn bump_input(&mut self) -> bool {
self.inp = &self.inp[1..];
!self.inp.is_empty()
}
fn parse_extension(&mut self) -> Result<Extension, Error> {
let (flag, fmt) = Extension::parse_flag(self.fmt)?;
let (width, fmt) = Extension::parse_width(fmt)?;
self.fmt = fmt;
Ok(Extension { flag, width })
}
fn parse_literal(&mut self) -> Result<(), Error> {
if self.f().is_ascii_whitespace() {
if !self.inp.is_empty() {
while self.i().is_ascii_whitespace() && self.bump_input() {}
}
} else if self.inp.is_empty() {
return Err(err!(
"expected to match literal byte {byte:?} from \
format string, but found end of input",
byte = escape::Byte(self.fmt[0]),
));
} else if self.f() != self.i() {
return Err(err!(
"expected to match literal byte {expect:?} from \
format string, but found byte {found:?} in input",
expect = escape::Byte(self.f()),
found = escape::Byte(self.i()),
));
} else {
self.bump_input();
}
self.bump_fmt();
Ok(())
}
fn parse_whitespace(&mut self) -> Result<(), Error> {
if !self.inp.is_empty() {
while self.i().is_ascii_whitespace() && self.bump_input() {}
}
self.bump_fmt();
Ok(())
}
fn parse_percent(&mut self) -> Result<(), Error> {
if self.i() != b'%' {
return Err(err!(
"expected '%' due to '%%' in format string, \
but found {byte:?} in input",
byte = escape::Byte(self.inp[0]),
));
}
self.bump_fmt();
self.bump_input();
Ok(())
}
fn parse_american_date(&mut self) -> Result<(), Error> {
let mut p = Parser { fmt: b"%m/%d/%y", inp: self.inp, tm: self.tm };
p.parse()?;
self.inp = p.inp;
self.bump_fmt();
Ok(())
}
fn parse_ampm(&mut self) -> Result<(), Error> {
let (index, inp) = parse_ampm(self.inp)?;
self.inp = inp;
self.tm.meridiem = Some(match index {
0 => Meridiem::AM,
1 => Meridiem::PM,
index => unreachable!("unknown AM/PM index {index}"),
});
self.bump_fmt();
Ok(())
}
fn parse_clock_secs(&mut self) -> Result<(), Error> {
let mut p = Parser { fmt: b"%H:%M:%S", inp: self.inp, tm: self.tm };
p.parse()?;
self.inp = p.inp;
self.bump_fmt();
Ok(())
}
fn parse_clock_nosecs(&mut self) -> Result<(), Error> {
let mut p = Parser { fmt: b"%H:%M", inp: self.inp, tm: self.tm };
p.parse()?;
self.inp = p.inp;
self.bump_fmt();
Ok(())
}
fn parse_day(&mut self, ext: Extension) -> Result<(), Error> {
let (day, inp) = ext
.parse_number(2, Flag::PadZero, self.inp)
.context("failed to parse day")?;
self.inp = inp;
let day =
t::Day::try_new("day", day).context("day number is invalid")?;
self.tm.day = Some(day);
self.bump_fmt();
Ok(())
}
fn parse_day_of_year(&mut self, ext: Extension) -> Result<(), Error> {
let (day, inp) = ext
.parse_number(3, Flag::PadZero, self.inp)
.context("failed to parse day of year")?;
self.inp = inp;
let day = t::DayOfYear::try_new("day-of-year", day)
.context("day of year number is invalid")?;
self.tm.day_of_year = Some(day);
self.bump_fmt();
Ok(())
}
fn parse_hour24(&mut self, ext: Extension) -> Result<(), Error> {
let (hour, inp) = ext
.parse_number(2, Flag::PadZero, self.inp)
.context("failed to parse hour")?;
self.inp = inp;
let hour = t::Hour::try_new("hour", hour)
.context("hour number is invalid")?;
self.tm.hour = Some(hour);
self.bump_fmt();
Ok(())
}
fn parse_hour12(&mut self, ext: Extension) -> Result<(), Error> {
type Hour12 = ri8<1, 12>;
let (hour, inp) = ext
.parse_number(2, Flag::PadZero, self.inp)
.context("failed to parse hour")?;
self.inp = inp;
let hour =
Hour12::try_new("hour", hour).context("hour number is invalid")?;
self.tm.hour = Some(t::Hour::rfrom(hour));
self.bump_fmt();
Ok(())
}
fn parse_iso_date(&mut self) -> Result<(), Error> {
let mut p = Parser { fmt: b"%Y-%m-%d", inp: self.inp, tm: self.tm };
p.parse()?;
self.inp = p.inp;
self.bump_fmt();
Ok(())
}
fn parse_minute(&mut self, ext: Extension) -> Result<(), Error> {
let (minute, inp) = ext
.parse_number(2, Flag::PadZero, self.inp)
.context("failed to parse minute")?;
self.inp = inp;
let minute = t::Minute::try_new("minute", minute)
.context("minute number is invalid")?;
self.tm.minute = Some(minute);
self.bump_fmt();
Ok(())
}
fn parse_iana_nocolon(&mut self) -> Result<(), Error> {
#[cfg(not(feature = "alloc"))]
{
Err(err!(
"cannot parse `%Q` without Jiff's `alloc` feature enabled"
))
}
#[cfg(feature = "alloc")]
{
use alloc::string::ToString;
if !self.inp.is_empty() && matches!(self.inp[0], b'+' | b'-') {
return self.parse_offset_nocolon();
}
let (iana, inp) = parse_iana(self.inp)?;
self.inp = inp;
self.tm.iana = Some(iana.to_string());
self.bump_fmt();
Ok(())
}
}
fn parse_iana_colon(&mut self) -> Result<(), Error> {
#[cfg(not(feature = "alloc"))]
{
Err(err!(
"cannot parse `%:Q` without Jiff's `alloc` feature enabled"
))
}
#[cfg(feature = "alloc")]
{
use alloc::string::ToString;
if !self.inp.is_empty() && matches!(self.inp[0], b'+' | b'-') {
return self.parse_offset_colon();
}
let (iana, inp) = parse_iana(self.inp)?;
self.inp = inp;
self.tm.iana = Some(iana.to_string());
self.bump_fmt();
Ok(())
}
}
fn parse_offset_nocolon(&mut self) -> Result<(), Error> {
let (sign, inp) = parse_required_sign(self.inp)
.context("sign is required for time zone offset")?;
let (hhmm, inp) = parse::split(inp, 4).ok_or_else(|| {
err!(
"expected at least 4 digits for time zone offset \
after sign, but found only {len} bytes remaining",
len = inp.len(),
)
})?;
let hh = parse::i64(&hhmm[0..2]).with_context(|| {
err!(
"failed to parse hours from time zone offset {hhmm}",
hhmm = escape::Bytes(hhmm)
)
})?;
let hh = ParsedOffsetHours::try_new("zone-offset-hours", hh)
.context("time zone offset hours are not valid")?;
let hh = t::SpanZoneOffset::rfrom(hh);
let mm = parse::i64(&hhmm[2..4]).with_context(|| {
err!(
"failed to parse minutes from time zone offset {hhmm}",
hhmm = escape::Bytes(hhmm)
)
})?;
let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes", mm)
.context("time zone offset minutes are not valid")?;
let mm = t::SpanZoneOffset::rfrom(mm);
let (ss, inp) = if inp.len() < 2
|| !inp[..2].iter().all(u8::is_ascii_digit)
{
(t::SpanZoneOffset::N::<0>(), inp)
} else {
let (ss, inp) = parse::split(inp, 2).unwrap();
let ss = parse::i64(ss).with_context(|| {
err!(
"failed to parse seconds from time zone offset {ss}",
ss = escape::Bytes(ss)
)
})?;
let ss = ParsedOffsetSeconds::try_new("zone-offset-seconds", ss)
.context("time zone offset seconds are not valid")?;
if inp.starts_with(b".") {
return Err(err!(
"parsing fractional seconds in time zone offset \
is not supported",
));
}
(t::SpanZoneOffset::rfrom(ss), inp)
};
let seconds = hh * C(3_600) + mm * C(60) + ss;
let offset = Offset::from_seconds_ranged(seconds * sign);
self.tm.offset = Some(offset);
self.inp = inp;
self.bump_fmt();
Ok(())
}
fn parse_offset_colon(&mut self) -> Result<(), Error> {
let (sign, inp) = parse_required_sign(self.inp)
.context("sign is required for time zone offset")?;
let (hhmm, inp) = parse::split(inp, 5).ok_or_else(|| {
err!(
"expected at least HH:MM digits for time zone offset \
after sign, but found only {len} bytes remaining",
len = inp.len(),
)
})?;
if hhmm[2] != b':' {
return Err(err!(
"expected colon after between HH and MM in time zone \
offset, but found {found:?} instead",
found = escape::Byte(hhmm[2]),
));
}
let hh = parse::i64(&hhmm[0..2]).with_context(|| {
err!(
"failed to parse hours from time zone offset {hhmm}",
hhmm = escape::Bytes(hhmm)
)
})?;
let hh = ParsedOffsetHours::try_new("zone-offset-hours", hh)
.context("time zone offset hours are not valid")?;
let hh = t::SpanZoneOffset::rfrom(hh);
let mm = parse::i64(&hhmm[3..5]).with_context(|| {
err!(
"failed to parse minutes from time zone offset {hhmm}",
hhmm = escape::Bytes(hhmm)
)
})?;
let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes", mm)
.context("time zone offset minutes are not valid")?;
let mm = t::SpanZoneOffset::rfrom(mm);
let (ss, inp) = if inp.len() < 3
|| inp[0] != b':'
|| !inp[1..3].iter().all(u8::is_ascii_digit)
{
(t::SpanZoneOffset::N::<0>(), inp)
} else {
let (ss, inp) = parse::split(&inp[1..], 2).unwrap();
let ss = parse::i64(ss).with_context(|| {
err!(
"failed to parse seconds from time zone offset {ss}",
ss = escape::Bytes(ss)
)
})?;
let ss = ParsedOffsetSeconds::try_new("zone-offset-seconds", ss)
.context("time zone offset seconds are not valid")?;
if inp.starts_with(b".") {
return Err(err!(
"parsing fractional seconds in time zone offset \
is not supported",
));
}
(t::SpanZoneOffset::rfrom(ss), inp)
};
let seconds = hh * C(3_600) + mm * C(60) + ss;
let offset = Offset::from_seconds_ranged(seconds * sign);
self.tm.offset = Some(offset);
self.inp = inp;
self.bump_fmt();
Ok(())
}
fn parse_second(&mut self, ext: Extension) -> Result<(), Error> {
let (mut second, inp) = ext
.parse_number(2, Flag::PadZero, self.inp)
.context("failed to parse second")?;
self.inp = inp;
if second == 60 {
second = 59;
}
let second = t::Second::try_new("second", second)
.context("second number is invalid")?;
self.tm.second = Some(second);
self.bump_fmt();
Ok(())
}
fn parse_timestamp(&mut self, ext: Extension) -> Result<(), Error> {
let (sign, inp) = parse_optional_sign(self.inp);
let (timestamp, inp) = ext
.parse_number(19, Flag::PadSpace, inp)
.context("failed to parse Unix timestamp (in seconds)")?;
let timestamp = timestamp.checked_mul(sign).ok_or_else(|| {
err!(
"parsed Unix timestamp `{timestamp}` with a \
leading `-` sign, which causes overflow",
)
})?;
let timestamp =
Timestamp::from_second(timestamp).with_context(|| {
err!(
"parsed Unix timestamp `{timestamp}`, \
but out of range of valid Jiff `Timestamp`",
)
})?;
self.inp = inp;
let dt = Offset::UTC.to_datetime(timestamp);
let (d, t) = (dt.date(), dt.time());
self.tm.offset = Some(Offset::UTC);
self.tm.year = Some(d.year_ranged());
self.tm.month = Some(d.month_ranged());
self.tm.day = Some(d.day_ranged());
self.tm.hour = Some(t.hour_ranged());
self.tm.minute = Some(t.minute_ranged());
self.tm.second = Some(t.second_ranged());
self.tm.subsec = Some(t.subsec_nanosecond_ranged());
self.tm.meridiem = Some(Meridiem::from(t));
self.bump_fmt();
Ok(())
}
fn parse_fractional(&mut self, _ext: Extension) -> Result<(), Error> {
let mkdigits = parse::slicer(self.inp);
while mkdigits(self.inp).len() < 9
&& self.inp.first().map_or(false, u8::is_ascii_digit)
{
self.inp = &self.inp[1..];
}
let digits = mkdigits(self.inp);
if digits.is_empty() {
return Err(err!(
"expected at least one fractional decimal digit, \
but did not find any",
));
}
let nanoseconds = parse::fraction(digits, 9).map_err(|err| {
err!(
"failed to parse {digits:?} as fractional second component \
(up to 9 digits, nanosecond precision): {err}",
digits = escape::Bytes(digits),
)
})?;
let nanoseconds =
t::SubsecNanosecond::try_new("nanoseconds", nanoseconds).map_err(
|err| err!("fractional nanoseconds are not valid: {err}"),
)?;
self.tm.subsec = Some(nanoseconds);
self.bump_fmt();
Ok(())
}
fn parse_dot_fractional(&mut self, ext: Extension) -> Result<(), Error> {
if !self.inp.starts_with(b".") {
self.bump_fmt();
return Ok(());
}
self.inp = &self.inp[1..];
self.parse_fractional(ext)
}
fn parse_month(&mut self, ext: Extension) -> Result<(), Error> {
let (month, inp) = ext
.parse_number(2, Flag::PadZero, self.inp)
.context("failed to parse month")?;
self.inp = inp;
let month = t::Month::try_new("month", month)
.context("month number is invalid")?;
self.tm.month = Some(month);
self.bump_fmt();
Ok(())
}
fn parse_month_name_abbrev(&mut self) -> Result<(), Error> {
let (index, inp) = parse_month_name_abbrev(self.inp)?;
self.inp = inp;
let index = i8::try_from(index).unwrap();
self.tm.month = Some(t::Month::new(index + 1).unwrap());
self.bump_fmt();
Ok(())
}
fn parse_month_name_full(&mut self) -> Result<(), Error> {
static CHOICES: &'static [&'static [u8]] = &[
b"January",
b"February",
b"March",
b"April",
b"May",
b"June",
b"July",
b"August",
b"September",
b"October",
b"November",
b"December",
];
let (index, inp) = parse_choice(self.inp, CHOICES)
.context("unrecognized month name")?;
self.inp = inp;
let index = i8::try_from(index).unwrap();
self.tm.month = Some(t::Month::new(index + 1).unwrap());
self.bump_fmt();
Ok(())
}
fn parse_weekday_abbrev(&mut self) -> Result<(), Error> {
let (index, inp) = parse_weekday_abbrev(self.inp)?;
self.inp = inp;
let index = i8::try_from(index).unwrap();
self.tm.weekday =
Some(Weekday::from_sunday_zero_offset(index).unwrap());
self.bump_fmt();
Ok(())
}
fn parse_weekday_full(&mut self) -> Result<(), Error> {
static CHOICES: &'static [&'static [u8]] = &[
b"Sunday",
b"Monday",
b"Tueday",
b"Wednesday",
b"Thursday",
b"Friday",
b"Saturday",
];
let (index, inp) = parse_choice(self.inp, CHOICES)
.context("unrecognized weekday abbreviation")?;
self.inp = inp;
let index = i8::try_from(index).unwrap();
self.tm.weekday =
Some(Weekday::from_sunday_zero_offset(index).unwrap());
self.bump_fmt();
Ok(())
}
fn parse_weekday_mon(&mut self, ext: Extension) -> Result<(), Error> {
let (weekday, inp) = ext
.parse_number(1, Flag::NoPad, self.inp)
.context("failed to parse weekday number")?;
self.inp = inp;
let weekday = i8::try_from(weekday).map_err(|_| {
err!("parsed weekday number `{weekday}` is invalid")
})?;
let weekday = Weekday::from_monday_one_offset(weekday)
.context("weekday number is invalid")?;
self.tm.weekday = Some(weekday);
self.bump_fmt();
Ok(())
}
fn parse_weekday_sun(&mut self, ext: Extension) -> Result<(), Error> {
let (weekday, inp) = ext
.parse_number(1, Flag::NoPad, self.inp)
.context("failed to parse weekday number")?;
self.inp = inp;
let weekday = i8::try_from(weekday).map_err(|_| {
err!("parsed weekday number `{weekday}` is invalid")
})?;
let weekday = Weekday::from_sunday_zero_offset(weekday)
.context("weekday number is invalid")?;
self.tm.weekday = Some(weekday);
self.bump_fmt();
Ok(())
}
fn parse_week_sun(&mut self, ext: Extension) -> Result<(), Error> {
let (week, inp) = ext
.parse_number(2, Flag::PadZero, self.inp)
.context("failed to parse Sunday-based week number")?;
self.inp = inp;
let week = t::WeekNum::try_new("week", week)
.context("Sunday-based week number is invalid")?;
self.tm.week_sun = Some(week);
self.bump_fmt();
Ok(())
}
fn parse_week_iso(&mut self, ext: Extension) -> Result<(), Error> {
let (week, inp) = ext
.parse_number(2, Flag::PadZero, self.inp)
.context("failed to parse ISO 8601 week number")?;
self.inp = inp;
let week = t::ISOWeek::try_new("week", week)
.context("ISO 8601 week number is invalid")?;
self.tm.iso_week = Some(week);
self.bump_fmt();
Ok(())
}
fn parse_week_mon(&mut self, ext: Extension) -> Result<(), Error> {
let (week, inp) = ext
.parse_number(2, Flag::PadZero, self.inp)
.context("failed to parse Monday-based week number")?;
self.inp = inp;
let week = t::WeekNum::try_new("week", week)
.context("Monday-based week number is invalid")?;
self.tm.week_mon = Some(week);
self.bump_fmt();
Ok(())
}
fn parse_year(&mut self, ext: Extension) -> Result<(), Error> {
let (sign, inp) = parse_optional_sign(self.inp);
let (year, inp) = ext
.parse_number(4, Flag::PadZero, inp)
.context("failed to parse year")?;
self.inp = inp;
let year = sign.checked_mul(year).unwrap();
let year = t::Year::try_new("year", year)
.context("year number is invalid")?;
self.tm.year = Some(year);
self.bump_fmt();
Ok(())
}
fn parse_year2(&mut self, ext: Extension) -> Result<(), Error> {
type Year2Digit = ri8<0, 99>;
let (year, inp) = ext
.parse_number(2, Flag::PadZero, self.inp)
.context("failed to parse 2-digit year")?;
self.inp = inp;
let year = Year2Digit::try_new("year (2 digits)", year)
.context("year number is invalid")?;
let mut year = t::Year::rfrom(year);
if year <= C(68) {
year += C(2000);
} else {
year += C(1900);
}
self.tm.year = Some(year);
self.bump_fmt();
Ok(())
}
fn parse_century(&mut self, ext: Extension) -> Result<(), Error> {
let (sign, inp) = parse_optional_sign(self.inp);
let (century, inp) = ext
.parse_number(2, Flag::NoPad, inp)
.context("failed to parse century")?;
self.inp = inp;
let century = sign.checked_mul(century).unwrap();
let year = century.checked_mul(100).unwrap();
let year = t::Year::try_new("year", year)
.context("year number (from century) is invalid")?;
self.tm.year = Some(year);
self.bump_fmt();
Ok(())
}
fn parse_iso_week_year(&mut self, ext: Extension) -> Result<(), Error> {
let (sign, inp) = parse_optional_sign(self.inp);
let (year, inp) = ext
.parse_number(4, Flag::PadZero, inp)
.context("failed to parse ISO 8601 week-based year")?;
self.inp = inp;
let year = sign.checked_mul(year).unwrap();
let year = t::ISOYear::try_new("year", year)
.context("ISO 8601 week-based year number is invalid")?;
self.tm.iso_week_year = Some(year);
self.bump_fmt();
Ok(())
}
fn parse_iso_week_year2(&mut self, ext: Extension) -> Result<(), Error> {
type Year2Digit = ri8<0, 99>;
let (year, inp) = ext
.parse_number(2, Flag::PadZero, self.inp)
.context("failed to parse 2-digit ISO 8601 week-based year")?;
self.inp = inp;
let year = Year2Digit::try_new("year (2 digits)", year)
.context("ISO 8601 week-based year number is invalid")?;
let mut year = t::ISOYear::rfrom(year);
if year <= C(68) {
year += C(2000);
} else {
year += C(1900);
}
self.tm.iso_week_year = Some(year);
self.bump_fmt();
Ok(())
}
}
impl Extension {
#[inline(always)]
fn parse_number<'i>(
self,
default_pad_width: usize,
default_flag: Flag,
mut inp: &'i [u8],
) -> Result<(i64, &'i [u8]), Error> {
let flag = self.flag.unwrap_or(default_flag);
let zero_pad_width = match flag {
Flag::PadSpace | Flag::NoPad => 0,
_ => self.width.map(usize::from).unwrap_or(default_pad_width),
};
let max_digits = default_pad_width.max(zero_pad_width);
while inp.get(0).map_or(false, |b| b.is_ascii_whitespace()) {
inp = &inp[1..];
}
let mut digits = 0;
while digits < inp.len()
&& digits < zero_pad_width
&& inp[digits] == b'0'
{
digits += 1;
}
let mut n: i64 = 0;
while digits < inp.len()
&& digits < max_digits
&& inp[digits].is_ascii_digit()
{
let byte = inp[digits];
digits += 1;
let digit = i64::from(byte - b'0');
n = n
.checked_mul(10)
.and_then(|n| n.checked_add(digit))
.ok_or_else(|| {
err!(
"number '{}' too big to parse into 64-bit integer",
escape::Bytes(&inp[..digits]),
)
})?;
}
if digits == 0 {
return Err(err!("invalid number, no digits found"));
}
Ok((n, &inp[digits..]))
}
}
#[inline(always)]
fn parse_optional_sign<'i>(input: &'i [u8]) -> (i64, &'i [u8]) {
if input.is_empty() {
(1, input)
} else if input[0] == b'-' {
(-1, &input[1..])
} else if input[0] == b'+' {
(1, &input[1..])
} else {
(1, input)
}
}
#[inline(always)]
fn parse_required_sign<'i>(
input: &'i [u8],
) -> Result<(t::Sign, &'i [u8]), Error> {
if input.is_empty() {
Err(err!("expected +/- sign, but found end of input"))
} else if input[0] == b'-' {
Ok((t::Sign::N::<-1>(), &input[1..]))
} else if input[0] == b'+' {
Ok((t::Sign::N::<1>(), &input[1..]))
} else {
Err(err!(
"expected +/- sign, but found {found:?} instead",
found = escape::Byte(input[0])
))
}
}
fn parse_choice<'i>(
input: &'i [u8],
choices: &[&'static [u8]],
) -> Result<(usize, &'i [u8]), Error> {
for (i, choice) in choices.into_iter().enumerate() {
if input.len() < choice.len() {
continue;
}
let (candidate, input) = input.split_at(choice.len());
if candidate.eq_ignore_ascii_case(choice) {
return Ok((i, input));
}
}
#[cfg(feature = "alloc")]
{
let mut err = alloc::format!(
"failed to find expected choice at beginning of {input:?}, \
available choices are: ",
input = escape::Bytes(input),
);
for (i, choice) in choices.iter().enumerate() {
if i > 0 {
write!(err, ", ").unwrap();
}
write!(err, "{}", escape::Bytes(choice)).unwrap();
}
Err(Error::adhoc(err))
}
#[cfg(not(feature = "alloc"))]
{
Err(err!(
"failed to find expected value from a set of allowed choices"
))
}
}
#[inline(always)]
fn parse_ampm<'i>(input: &'i [u8]) -> Result<(usize, &'i [u8]), Error> {
if input.len() < 2 {
return Err(err!(
"expected to find AM or PM, \
but the remaining input, {input:?}, is too short \
to contain one",
input = escape::Bytes(input),
));
}
let (x, input) = input.split_at(2);
let candidate = &[x[0].to_ascii_lowercase(), x[1].to_ascii_lowercase()];
let index = match candidate {
b"am" => 0,
b"pm" => 1,
_ => {
return Err(err!(
"expected to find AM or PM, but found \
{candidate:?} instead",
candidate = escape::Bytes(x),
))
}
};
Ok((index, input))
}
#[inline(always)]
fn parse_weekday_abbrev<'i>(
input: &'i [u8],
) -> Result<(usize, &'i [u8]), Error> {
if input.len() < 3 {
return Err(err!(
"expected to find a weekday abbreviation, \
but the remaining input, {input:?}, is too short \
to contain one",
input = escape::Bytes(input),
));
}
let (x, input) = input.split_at(3);
let candidate = &[
x[0].to_ascii_lowercase(),
x[1].to_ascii_lowercase(),
x[2].to_ascii_lowercase(),
];
let index = match candidate {
b"sun" => 0,
b"mon" => 1,
b"tue" => 2,
b"wed" => 3,
b"thu" => 4,
b"fri" => 5,
b"sat" => 6,
_ => {
return Err(err!(
"expected to find weekday abbreviation, but found \
{candidate:?} instead",
candidate = escape::Bytes(x),
))
}
};
Ok((index, input))
}
#[inline(always)]
fn parse_month_name_abbrev<'i>(
input: &'i [u8],
) -> Result<(usize, &'i [u8]), Error> {
if input.len() < 3 {
return Err(err!(
"expected to find a month name abbreviation, \
but the remaining input, {input:?}, is too short \
to contain one",
input = escape::Bytes(input),
));
}
let (x, input) = input.split_at(3);
let candidate = &[
x[0].to_ascii_lowercase(),
x[1].to_ascii_lowercase(),
x[2].to_ascii_lowercase(),
];
let index = match candidate {
b"jan" => 0,
b"feb" => 1,
b"mar" => 2,
b"apr" => 3,
b"may" => 4,
b"jun" => 5,
b"jul" => 6,
b"aug" => 7,
b"sep" => 8,
b"oct" => 9,
b"nov" => 10,
b"dec" => 11,
_ => {
return Err(err!(
"expected to find month name abbreviation, but found \
{candidate:?} instead",
candidate = escape::Bytes(x),
))
}
};
Ok((index, input))
}
#[inline(always)]
fn parse_iana<'i>(input: &'i [u8]) -> Result<(&'i str, &'i [u8]), Error> {
let mkiana = parse::slicer(input);
let (_, mut input) = parse_iana_component(input)?;
while input.starts_with(b"/") {
input = &input[1..];
let (_, unconsumed) = parse_iana_component(input)?;
input = unconsumed;
}
let iana = core::str::from_utf8(mkiana(input)).expect("ASCII");
Ok((iana, input))
}
#[inline(always)]
fn parse_iana_component<'i>(
mut input: &'i [u8],
) -> Result<(&'i [u8], &'i [u8]), Error> {
let mkname = parse::slicer(input);
if input.is_empty() {
return Err(err!(
"expected the start of an IANA time zone identifier \
name or component, but found end of input instead",
));
}
if !matches!(input[0], b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') {
return Err(err!(
"expected the start of an IANA time zone identifier \
name or component, but found {:?} instead",
escape::Byte(input[0]),
));
}
input = &input[1..];
let is_iana_char = |byte| {
matches!(
byte,
b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
)
};
while !input.is_empty() && is_iana_char(input[0]) {
input = &input[1..];
}
Ok((mkname(input), input))
}
#[cfg(feature = "alloc")]
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use super::*;
#[test]
fn ok_parse_zoned() {
if crate::tz::db().is_definitively_empty() {
return;
}
let p = |fmt: &str, input: &str| {
BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
.unwrap()
.to_zoned()
.unwrap()
};
insta::assert_debug_snapshot!(
p("%h %d, %Y %H:%M:%S %z", "Apr 1, 2022 20:46:15 -0400"),
@"2022-04-01T20:46:15-04:00[-04:00]",
);
insta::assert_debug_snapshot!(
p("%h %d, %Y %H:%M:%S %Q", "Apr 1, 2022 20:46:15 -0400"),
@"2022-04-01T20:46:15-04:00[-04:00]",
);
insta::assert_debug_snapshot!(
p("%h %d, %Y %H:%M:%S [%Q]", "Apr 1, 2022 20:46:15 [America/New_York]"),
@"2022-04-01T20:46:15-04:00[America/New_York]",
);
insta::assert_debug_snapshot!(
p("%h %d, %Y %H:%M:%S %Q", "Apr 1, 2022 20:46:15 America/New_York"),
@"2022-04-01T20:46:15-04:00[America/New_York]",
);
insta::assert_debug_snapshot!(
p("%h %d, %Y %H:%M:%S %:z %:Q", "Apr 1, 2022 20:46:15 -08:00 -04:00"),
@"2022-04-01T20:46:15-04:00[-04:00]",
);
}
#[test]
fn ok_parse_timestamp() {
let p = |fmt: &str, input: &str| {
BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
.unwrap()
.to_timestamp()
.unwrap()
};
insta::assert_debug_snapshot!(
p("%h %d, %Y %H:%M:%S %z", "Apr 1, 2022 20:46:15 -0400"),
@"2022-04-02T00:46:15Z",
);
insta::assert_debug_snapshot!(
p("%h %d, %Y %H:%M:%S %z", "Apr 1, 2022 20:46:15 +0400"),
@"2022-04-01T16:46:15Z",
);
insta::assert_debug_snapshot!(
p("%h %d, %Y %H:%M:%S %z", "Apr 1, 2022 20:46:15 -040059"),
@"2022-04-02T00:47:14Z",
);
insta::assert_debug_snapshot!(
p("%h %d, %Y %H:%M:%S %:z", "Apr 1, 2022 20:46:15 -04:00"),
@"2022-04-02T00:46:15Z",
);
insta::assert_debug_snapshot!(
p("%h %d, %Y %H:%M:%S %:z", "Apr 1, 2022 20:46:15 +04:00"),
@"2022-04-01T16:46:15Z",
);
insta::assert_debug_snapshot!(
p("%h %d, %Y %H:%M:%S %:z", "Apr 1, 2022 20:46:15 -04:00:59"),
@"2022-04-02T00:47:14Z",
);
insta::assert_debug_snapshot!(
p("%s", "0"),
@"1970-01-01T00:00:00Z",
);
insta::assert_debug_snapshot!(
p("%s", "-0"),
@"1970-01-01T00:00:00Z",
);
insta::assert_debug_snapshot!(
p("%s", "-1"),
@"1969-12-31T23:59:59Z",
);
insta::assert_debug_snapshot!(
p("%s", "1"),
@"1970-01-01T00:00:01Z",
);
insta::assert_debug_snapshot!(
p("%s", "+1"),
@"1970-01-01T00:00:01Z",
);
insta::assert_debug_snapshot!(
p("%s", "1737396540"),
@"2025-01-20T18:09:00Z",
);
insta::assert_debug_snapshot!(
p("%s", "-377705023201"),
@"-009999-01-02T01:59:59Z",
);
insta::assert_debug_snapshot!(
p("%s", "253402207200"),
@"9999-12-30T22:00:00Z",
);
}
#[test]
fn ok_parse_datetime() {
let p = |fmt: &str, input: &str| {
BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
.unwrap()
.to_datetime()
.unwrap()
};
insta::assert_debug_snapshot!(
p("%h %d, %Y %H:%M:%S", "Apr 1, 2022 20:46:15"),
@"2022-04-01T20:46:15",
);
insta::assert_debug_snapshot!(
p("%h %05d, %Y %H:%M:%S", "Apr 1, 2022 20:46:15"),
@"2022-04-01T20:46:15",
);
}
#[test]
fn ok_parse_date() {
let p = |fmt: &str, input: &str| {
BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
.unwrap()
.to_date()
.unwrap()
};
insta::assert_debug_snapshot!(
p("%m/%d/%y", "1/1/99"),
@"1999-01-01",
);
insta::assert_debug_snapshot!(
p("%m/%d/%04y", "1/1/0099"),
@"1999-01-01",
);
insta::assert_debug_snapshot!(
p("%D", "1/1/99"),
@"1999-01-01",
);
insta::assert_debug_snapshot!(
p("%m/%d/%Y", "1/1/0099"),
@"0099-01-01",
);
insta::assert_debug_snapshot!(
p("%m/%d/%Y", "1/1/1999"),
@"1999-01-01",
);
insta::assert_debug_snapshot!(
p("%m/%d/%Y", "12/31/9999"),
@"9999-12-31",
);
insta::assert_debug_snapshot!(
p("%m/%d/%Y", "01/01/-9999"),
@"-009999-01-01",
);
insta::assert_snapshot!(
p("%a %m/%d/%Y", "sun 7/14/2024"),
@"2024-07-14",
);
insta::assert_snapshot!(
p("%A %m/%d/%Y", "sUnDaY 7/14/2024"),
@"2024-07-14",
);
insta::assert_snapshot!(
p("%b %d %Y", "Jul 14 2024"),
@"2024-07-14",
);
insta::assert_snapshot!(
p("%B %d, %Y", "July 14, 2024"),
@"2024-07-14",
);
insta::assert_snapshot!(
p("%A, %B %d, %Y", "Wednesday, dEcEmBeR 25, 2024"),
@"2024-12-25",
);
insta::assert_debug_snapshot!(
p("%Y%m%d", "20240730"),
@"2024-07-30",
);
insta::assert_debug_snapshot!(
p("%Y%m%d", "09990730"),
@"0999-07-30",
);
insta::assert_debug_snapshot!(
p("%Y%m%d", "9990111"),
@"9990-11-01",
);
insta::assert_debug_snapshot!(
p("%3Y%m%d", "09990111"),
@"0999-01-11",
);
insta::assert_debug_snapshot!(
p("%5Y%m%d", "09990111"),
@"9990-11-01",
);
insta::assert_debug_snapshot!(
p("%5Y%m%d", "009990111"),
@"0999-01-11",
);
insta::assert_debug_snapshot!(
p("%C-%m-%d", "20-07-01"),
@"2000-07-01",
);
insta::assert_debug_snapshot!(
p("%C-%m-%d", "-20-07-01"),
@"-002000-07-01",
);
insta::assert_debug_snapshot!(
p("%C-%m-%d", "9-07-01"),
@"0900-07-01",
);
insta::assert_debug_snapshot!(
p("%C-%m-%d", "-9-07-01"),
@"-000900-07-01",
);
insta::assert_debug_snapshot!(
p("%C-%m-%d", "09-07-01"),
@"0900-07-01",
);
insta::assert_debug_snapshot!(
p("%C-%m-%d", "-09-07-01"),
@"-000900-07-01",
);
insta::assert_debug_snapshot!(
p("%C-%m-%d", "0-07-01"),
@"0000-07-01",
);
insta::assert_debug_snapshot!(
p("%C-%m-%d", "-0-07-01"),
@"0000-07-01",
);
insta::assert_snapshot!(
p("%u %m/%d/%Y", "7 7/14/2024"),
@"2024-07-14",
);
insta::assert_snapshot!(
p("%w %m/%d/%Y", "0 7/14/2024"),
@"2024-07-14",
);
insta::assert_snapshot!(
p("%Y-%U-%u", "2025-00-6"),
@"2025-01-04",
);
insta::assert_snapshot!(
p("%Y-%U-%u", "2025-01-7"),
@"2025-01-05",
);
insta::assert_snapshot!(
p("%Y-%U-%u", "2025-01-1"),
@"2025-01-06",
);
insta::assert_snapshot!(
p("%Y-%W-%u", "2025-00-6"),
@"2025-01-04",
);
insta::assert_snapshot!(
p("%Y-%W-%u", "2025-00-7"),
@"2025-01-05",
);
insta::assert_snapshot!(
p("%Y-%W-%u", "2025-01-1"),
@"2025-01-06",
);
insta::assert_snapshot!(
p("%Y-%W-%u", "2025-01-2"),
@"2025-01-07",
);
}
#[test]
fn ok_parse_time() {
let p = |fmt: &str, input: &str| {
BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
.unwrap()
.to_time()
.unwrap()
};
insta::assert_debug_snapshot!(
p("%H:%M", "15:48"),
@"15:48:00",
);
insta::assert_debug_snapshot!(
p("%H:%M:%S", "15:48:59"),
@"15:48:59",
);
insta::assert_debug_snapshot!(
p("%H:%M:%S", "15:48:60"),
@"15:48:59",
);
insta::assert_debug_snapshot!(
p("%T", "15:48:59"),
@"15:48:59",
);
insta::assert_debug_snapshot!(
p("%R", "15:48"),
@"15:48:00",
);
insta::assert_debug_snapshot!(
p("%H %p", "5 am"),
@"05:00:00",
);
insta::assert_debug_snapshot!(
p("%H%p", "5am"),
@"05:00:00",
);
insta::assert_debug_snapshot!(
p("%H%p", "11pm"),
@"23:00:00",
);
insta::assert_debug_snapshot!(
p("%I%p", "11pm"),
@"23:00:00",
);
insta::assert_debug_snapshot!(
p("%I%p", "12am"),
@"00:00:00",
);
insta::assert_debug_snapshot!(
p("%H%p", "23pm"),
@"23:00:00",
);
insta::assert_debug_snapshot!(
p("%H%p", "23am"),
@"11:00:00",
);
insta::assert_debug_snapshot!(
p("%H:%M:%S%.f", "15:48:01.1"),
@"15:48:01.1",
);
insta::assert_debug_snapshot!(
p("%H:%M:%S%.255f", "15:48:01.1"),
@"15:48:01.1",
);
insta::assert_debug_snapshot!(
p("%H:%M:%S%255.255f", "15:48:01.1"),
@"15:48:01.1",
);
insta::assert_debug_snapshot!(
p("%H:%M:%S%.f", "15:48:01"),
@"15:48:01",
);
insta::assert_debug_snapshot!(
p("%H:%M:%S%.fa", "15:48:01a"),
@"15:48:01",
);
insta::assert_debug_snapshot!(
p("%H:%M:%S%.f", "15:48:01.123456789"),
@"15:48:01.123456789",
);
insta::assert_debug_snapshot!(
p("%H:%M:%S%.f", "15:48:01.000000001"),
@"15:48:01.000000001",
);
insta::assert_debug_snapshot!(
p("%H:%M:%S.%f", "15:48:01.1"),
@"15:48:01.1",
);
insta::assert_debug_snapshot!(
p("%H:%M:%S.%3f", "15:48:01.123"),
@"15:48:01.123",
);
insta::assert_debug_snapshot!(
p("%H:%M:%S.%3f", "15:48:01.123456"),
@"15:48:01.123456",
);
insta::assert_debug_snapshot!(
p("%H", "09"),
@"09:00:00",
);
insta::assert_debug_snapshot!(
p("%H", " 9"),
@"09:00:00",
);
insta::assert_debug_snapshot!(
p("%H", "15"),
@"15:00:00",
);
insta::assert_debug_snapshot!(
p("%k", "09"),
@"09:00:00",
);
insta::assert_debug_snapshot!(
p("%k", " 9"),
@"09:00:00",
);
insta::assert_debug_snapshot!(
p("%k", "15"),
@"15:00:00",
);
insta::assert_debug_snapshot!(
p("%I", "09"),
@"09:00:00",
);
insta::assert_debug_snapshot!(
p("%I", " 9"),
@"09:00:00",
);
insta::assert_debug_snapshot!(
p("%l", "09"),
@"09:00:00",
);
insta::assert_debug_snapshot!(
p("%l", " 9"),
@"09:00:00",
);
}
#[test]
fn ok_parse_whitespace() {
let p = |fmt: &str, input: &str| {
BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
.unwrap()
.to_time()
.unwrap()
};
insta::assert_debug_snapshot!(
p("%H%M", "1548"),
@"15:48:00",
);
insta::assert_debug_snapshot!(
p("%H%M", "15\n48"),
@"15:48:00",
);
insta::assert_debug_snapshot!(
p("%H%M", "15\t48"),
@"15:48:00",
);
insta::assert_debug_snapshot!(
p("%H%n%M", "1548"),
@"15:48:00",
);
insta::assert_debug_snapshot!(
p("%H%n%M", "15\n48"),
@"15:48:00",
);
insta::assert_debug_snapshot!(
p("%H%n%M", "15\t48"),
@"15:48:00",
);
insta::assert_debug_snapshot!(
p("%H%t%M", "1548"),
@"15:48:00",
);
insta::assert_debug_snapshot!(
p("%H%t%M", "15\n48"),
@"15:48:00",
);
insta::assert_debug_snapshot!(
p("%H%t%M", "15\t48"),
@"15:48:00",
);
}
#[test]
fn err_parse() {
let p = |fmt: &str, input: &str| {
BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
.unwrap_err()
.to_string()
};
insta::assert_snapshot!(
p("%M", ""),
@"strptime parsing failed: expected non-empty input for directive %M, but found end of input",
);
insta::assert_snapshot!(
p("%M", "a"),
@"strptime parsing failed: %M failed: failed to parse minute: invalid number, no digits found",
);
insta::assert_snapshot!(
p("%M%S", "15"),
@"strptime parsing failed: expected non-empty input for directive %S, but found end of input",
);
insta::assert_snapshot!(
p("%M%a", "Sun"),
@"strptime parsing failed: %M failed: failed to parse minute: invalid number, no digits found",
);
insta::assert_snapshot!(
p("%y", "999"),
@r###"strptime expects to consume the entire input, but "9" remains unparsed"###,
);
insta::assert_snapshot!(
p("%Y", "-10000"),
@r###"strptime expects to consume the entire input, but "0" remains unparsed"###,
);
insta::assert_snapshot!(
p("%Y", "10000"),
@r###"strptime expects to consume the entire input, but "0" remains unparsed"###,
);
insta::assert_snapshot!(
p("%A %m/%d/%y", "Mon 7/14/24"),
@r###"strptime parsing failed: %A failed: unrecognized weekday abbreviation: failed to find expected choice at beginning of "Mon 7/14/24", available choices are: Sunday, Monday, Tueday, Wednesday, Thursday, Friday, Saturday"###,
);
insta::assert_snapshot!(
p("%b", "Bad"),
@r###"strptime parsing failed: %b failed: expected to find month name abbreviation, but found "Bad" instead"###,
);
insta::assert_snapshot!(
p("%h", "July"),
@r###"strptime expects to consume the entire input, but "y" remains unparsed"###,
);
insta::assert_snapshot!(
p("%B", "Jul"),
@r###"strptime parsing failed: %B failed: unrecognized month name: failed to find expected choice at beginning of "Jul", available choices are: January, February, March, April, May, June, July, August, September, October, November, December"###,
);
insta::assert_snapshot!(
p("%H", "24"),
@"strptime parsing failed: %H failed: hour number is invalid: parameter 'hour' with value 24 is not in the required range of 0..=23",
);
insta::assert_snapshot!(
p("%M", "60"),
@"strptime parsing failed: %M failed: minute number is invalid: parameter 'minute' with value 60 is not in the required range of 0..=59",
);
insta::assert_snapshot!(
p("%S", "61"),
@"strptime parsing failed: %S failed: second number is invalid: parameter 'second' with value 61 is not in the required range of 0..=59",
);
insta::assert_snapshot!(
p("%I", "0"),
@"strptime parsing failed: %I failed: hour number is invalid: parameter 'hour' with value 0 is not in the required range of 1..=12",
);
insta::assert_snapshot!(
p("%I", "13"),
@"strptime parsing failed: %I failed: hour number is invalid: parameter 'hour' with value 13 is not in the required range of 1..=12",
);
insta::assert_snapshot!(
p("%p", "aa"),
@r###"strptime parsing failed: %p failed: expected to find AM or PM, but found "aa" instead"###,
);
insta::assert_snapshot!(
p("%_", " "),
@r###"strptime parsing failed: expected to find specifier directive after flag "_", but found end of format string"###,
);
insta::assert_snapshot!(
p("%-", " "),
@r###"strptime parsing failed: expected to find specifier directive after flag "-", but found end of format string"###,
);
insta::assert_snapshot!(
p("%0", " "),
@r###"strptime parsing failed: expected to find specifier directive after flag "0", but found end of format string"###,
);
insta::assert_snapshot!(
p("%^", " "),
@r###"strptime parsing failed: expected to find specifier directive after flag "^", but found end of format string"###,
);
insta::assert_snapshot!(
p("%#", " "),
@r###"strptime parsing failed: expected to find specifier directive after flag "#", but found end of format string"###,
);
insta::assert_snapshot!(
p("%_1", " "),
@"strptime parsing failed: expected to find specifier directive after width 1, but found end of format string",
);
insta::assert_snapshot!(
p("%_23", " "),
@"strptime parsing failed: expected to find specifier directive after width 23, but found end of format string",
);
insta::assert_snapshot!(
p("%H:%M:%S%.f", "15:59:01."),
@"strptime parsing failed: %.f failed: expected at least one fractional decimal digit, but did not find any",
);
insta::assert_snapshot!(
p("%H:%M:%S%.f", "15:59:01.a"),
@"strptime parsing failed: %.f failed: expected at least one fractional decimal digit, but did not find any",
);
insta::assert_snapshot!(
p("%H:%M:%S%.f", "15:59:01.1234567891"),
@r###"strptime expects to consume the entire input, but "1" remains unparsed"###,
);
insta::assert_snapshot!(
p("%H:%M:%S.%f", "15:59:01."),
@"strptime parsing failed: expected non-empty input for directive %f, but found end of input",
);
insta::assert_snapshot!(
p("%H:%M:%S.%f", "15:59:01"),
@r###"strptime parsing failed: expected to match literal byte "." from format string, but found end of input"###,
);
insta::assert_snapshot!(
p("%H:%M:%S.%f", "15:59:01.a"),
@"strptime parsing failed: %f failed: expected at least one fractional decimal digit, but did not find any",
);
insta::assert_snapshot!(
p("%Q", "+America/New_York"),
@"strptime parsing failed: %Q failed: failed to parse hours from time zone offset Amer: invalid digit, expected 0-9 but got A",
);
insta::assert_snapshot!(
p("%Q", "-America/New_York"),
@"strptime parsing failed: %Q failed: failed to parse hours from time zone offset Amer: invalid digit, expected 0-9 but got A",
);
insta::assert_snapshot!(
p("%:Q", "+0400"),
@"strptime parsing failed: %:Q failed: expected at least HH:MM digits for time zone offset after sign, but found only 4 bytes remaining",
);
insta::assert_snapshot!(
p("%Q", "+04:00"),
@"strptime parsing failed: %Q failed: failed to parse minutes from time zone offset 04:0: invalid digit, expected 0-9 but got :",
);
insta::assert_snapshot!(
p("%Q", "America/"),
@"strptime parsing failed: %Q failed: expected the start of an IANA time zone identifier name or component, but found end of input instead",
);
insta::assert_snapshot!(
p("%Q", "America/+"),
@r###"strptime parsing failed: %Q failed: expected the start of an IANA time zone identifier name or component, but found "+" instead"###,
);
insta::assert_snapshot!(
p("%s", "-377705023202"),
@"strptime parsing failed: %s failed: parsed Unix timestamp `-377705023202`, but out of range of valid Jiff `Timestamp`: parameter 'second' with value -377705023202 is not in the required range of -377705023201..=253402207200",
);
insta::assert_snapshot!(
p("%s", "253402207201"),
@"strptime parsing failed: %s failed: parsed Unix timestamp `253402207201`, but out of range of valid Jiff `Timestamp`: parameter 'second' with value 253402207201 is not in the required range of -377705023201..=253402207200",
);
insta::assert_snapshot!(
p("%s", "-9999999999999999999"),
@"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number '9999999999999999999' too big to parse into 64-bit integer",
);
insta::assert_snapshot!(
p("%s", "9999999999999999999"),
@"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number '9999999999999999999' too big to parse into 64-bit integer",
);
insta::assert_snapshot!(
p("%u", "0"),
@"strptime parsing failed: %u failed: weekday number is invalid: parameter 'weekday' with value 0 is not in the required range of 1..=7",
);
insta::assert_snapshot!(
p("%w", "7"),
@"strptime parsing failed: %w failed: weekday number is invalid: parameter 'weekday' with value 7 is not in the required range of 0..=6",
);
insta::assert_snapshot!(
p("%u", "128"),
@r###"strptime expects to consume the entire input, but "28" remains unparsed"###,
);
insta::assert_snapshot!(
p("%w", "128"),
@r###"strptime expects to consume the entire input, but "28" remains unparsed"###,
);
}
#[test]
fn err_parse_date() {
let p = |fmt: &str, input: &str| {
BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
.unwrap()
.to_date()
.unwrap_err()
.to_string()
};
insta::assert_snapshot!(
p("%Y", "2024"),
@"a month/day, day-of-year or week date must be present to create a date, but none were found",
);
insta::assert_snapshot!(
p("%m", "7"),
@"missing year, date cannot be created",
);
insta::assert_snapshot!(
p("%d", "25"),
@"missing year, date cannot be created",
);
insta::assert_snapshot!(
p("%Y-%m", "2024-7"),
@"a month/day, day-of-year or week date must be present to create a date, but none were found",
);
insta::assert_snapshot!(
p("%Y-%d", "2024-25"),
@"a month/day, day-of-year or week date must be present to create a date, but none were found",
);
insta::assert_snapshot!(
p("%m-%d", "7-25"),
@"missing year, date cannot be created",
);
insta::assert_snapshot!(
p("%m/%d/%y", "6/31/24"),
@"invalid date: parameter 'day' with value 31 is not in the required range of 1..=30",
);
insta::assert_snapshot!(
p("%m/%d/%y", "2/29/23"),
@"invalid date: parameter 'day' with value 29 is not in the required range of 1..=28",
);
insta::assert_snapshot!(
p("%a %m/%d/%y", "Mon 7/14/24"),
@"parsed weekday Monday does not match weekday Sunday from parsed date 2024-07-14",
);
insta::assert_snapshot!(
p("%A %m/%d/%y", "Monday 7/14/24"),
@"parsed weekday Monday does not match weekday Sunday from parsed date 2024-07-14",
);
insta::assert_snapshot!(
p("%Y-%U-%u", "2025-00-2"),
@"weekday `Tuesday` is not valid for Sunday based week number `0` in year `2025`",
);
insta::assert_snapshot!(
p("%Y-%W-%u", "2025-00-2"),
@"weekday `Tuesday` is not valid for Monday based week number `0` in year `2025`",
);
}
#[test]
fn err_parse_time() {
let p = |fmt: &str, input: &str| {
BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
.unwrap()
.to_time()
.unwrap_err()
.to_string()
};
insta::assert_snapshot!(
p("%M", "59"),
@"parsing format did not include hour directive, but did include minute directive (cannot have smaller time units with bigger time units missing)",
);
insta::assert_snapshot!(
p("%S", "59"),
@"parsing format did not include hour directive, but did include second directive (cannot have smaller time units with bigger time units missing)",
);
insta::assert_snapshot!(
p("%M:%S", "59:59"),
@"parsing format did not include hour directive, but did include minute directive (cannot have smaller time units with bigger time units missing)",
);
insta::assert_snapshot!(
p("%H:%S", "15:59"),
@"parsing format did not include minute directive, but did include second directive (cannot have smaller time units with bigger time units missing)",
);
}
}