use alloc::string::String;
use crate::{
civil::{Date, DateTime, Time, Weekday},
error::{err, ErrorContext},
fmt::{util::DecimalFormatter, Parsed, Write, WriteExt},
tz::{Offset, TimeZone},
util::{
escape, parse,
rangeint::{ri8, RFrom},
t::{self, C},
},
Error, Timestamp, Zoned,
};
pub(crate) static DEFAULT_DATETIME_PARSER: DateTimeParser =
DateTimeParser::new();
pub(crate) static DEFAULT_DATETIME_PRINTER: DateTimePrinter =
DateTimePrinter::new();
#[inline]
pub fn to_string(zdt: &Zoned) -> Result<String, Error> {
let mut buf = String::new();
DEFAULT_DATETIME_PRINTER.print_zoned(zdt, &mut buf)?;
Ok(buf)
}
#[inline]
pub fn parse(string: &str) -> Result<Zoned, Error> {
DEFAULT_DATETIME_PARSER.parse_zoned(string)
}
#[derive(Debug)]
pub struct DateTimeParser {
relaxed_weekday: bool,
}
impl DateTimeParser {
#[inline]
pub const fn new() -> DateTimeParser {
DateTimeParser { relaxed_weekday: false }
}
#[inline]
pub const fn relaxed_weekday(self, yes: bool) -> DateTimeParser {
DateTimeParser { relaxed_weekday: yes, ..self }
}
pub fn parse_zoned<I: AsRef<[u8]>>(
&self,
input: I,
) -> Result<Zoned, Error> {
let input = input.as_ref();
let zdt = self
.parse_zoned_internal(input)
.context(
"failed to parse RFC 2822 datetime into Jiff zoned datetime",
)?
.into_full()?;
Ok(zdt)
}
pub fn parse_timestamp<I: AsRef<[u8]>>(
&self,
input: I,
) -> Result<Timestamp, Error> {
let input = input.as_ref();
let ts = self
.parse_timestamp_internal(input)
.context("failed to parse RFC 2822 datetime into Jiff timestamp")?
.into_full()?;
Ok(ts)
}
#[inline(always)]
fn parse_zoned_internal<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Zoned>, Error> {
let Parsed { value: (dt, offset), input } =
self.parse_datetime_offset(input)?;
let ts = offset
.to_timestamp(dt)
.context("RFC 2822 datetime out of Jiff's range")?;
let zdt = ts.to_zoned(TimeZone::fixed(offset));
Ok(Parsed { value: zdt, input })
}
#[inline(always)]
fn parse_timestamp_internal<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Timestamp>, Error> {
let Parsed { value: (dt, offset), input } =
self.parse_datetime_offset(input)?;
let ts = offset
.to_timestamp(dt)
.context("RFC 2822 datetime out of Jiff's range")?;
Ok(Parsed { value: ts, input })
}
#[inline(always)]
fn parse_datetime_offset<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, (DateTime, Offset)>, Error> {
let input = input.as_ref();
let Parsed { value: dt, input } = self.parse_datetime(input)?;
let Parsed { value: offset, input } = self.parse_offset(input)?;
let Parsed { input, .. } = self.skip_whitespace(input);
let input = if input.is_empty() {
input
} else {
self.skip_comment(input)?.input
};
Ok(Parsed { value: (dt, offset), input })
}
#[inline(always)]
fn parse_datetime<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, DateTime>, Error> {
if input.is_empty() {
return Err(err!(
"expected RFC 2822 datetime, but got empty string"
));
}
let Parsed { input, .. } = self.skip_whitespace(input);
if input.is_empty() {
return Err(err!(
"expected RFC 2822 datetime, but got empty string after \
trimming whitespace",
));
}
let Parsed { value: wd, input } = self.parse_weekday(input)?;
let Parsed { value: day, input } = self.parse_day(input)?;
let Parsed { value: month, input } = self.parse_month(input)?;
let Parsed { value: year, input } = self.parse_year(input)?;
let Parsed { value: hour, input } = self.parse_hour(input)?;
let Parsed { input, .. } = self.parse_time_separator(input)?;
let Parsed { value: minute, input } = self.parse_minute(input)?;
let (second, input) = if !input.starts_with(b":") {
(t::Second::N::<0>(), input)
} else {
let Parsed { input, .. } = self.parse_time_separator(input)?;
let Parsed { value: second, input } = self.parse_second(input)?;
(second, input)
};
let Parsed { input, .. } = self
.parse_whitespace(input)
.with_context(|| err!("expected whitespace after parsing time"))?;
let date =
Date::new_ranged(year, month, day).context("invalid date")?;
let time = Time::new_ranged(
hour,
minute,
second,
t::SubsecNanosecond::N::<0>(),
);
let dt = DateTime::from_parts(date, time);
if let Some(wd) = wd {
if !self.relaxed_weekday && wd != dt.weekday() {
return Err(err!(
"found parsed weekday of {parsed}, \
but parsed datetime of {dt} has weekday \
{has}",
parsed = weekday_abbrev(wd),
has = weekday_abbrev(dt.weekday()),
));
}
}
Ok(Parsed { value: dt, input })
}
#[inline(always)]
fn parse_weekday<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Option<Weekday>>, Error> {
if matches!(input[0], b'0'..=b'9') {
return Ok(Parsed { value: None, input });
}
if input.len() < 4 {
return Err(err!(
"expected day at beginning of RFC 2822 datetime \
since first non-whitespace byte, {first:?}, \
is not a digit, but given string is too short \
(length is {length})",
first = escape::Byte(input[0]),
length = input.len(),
));
}
let b1 = input[0].to_ascii_lowercase();
let b2 = input[1].to_ascii_lowercase();
let b3 = input[2].to_ascii_lowercase();
let wd = match &[b1, b2, b3] {
b"sun" => Weekday::Sunday,
b"mon" => Weekday::Monday,
b"tue" => Weekday::Tuesday,
b"wed" => Weekday::Wednesday,
b"thu" => Weekday::Thursday,
b"fri" => Weekday::Friday,
b"sat" => Weekday::Saturday,
_ => {
return Err(err!(
"expected day at beginning of RFC 2822 datetime \
since first non-whitespace byte, {first:?}, \
is not a digit, but did not recognize {got:?} \
as a valid weekday abbreviation",
first = escape::Byte(input[0]),
got = escape::Bytes(&input[..3]),
));
}
};
if input[3] != b',' {
return Err(err!(
"expected day at beginning of RFC 2822 datetime \
since first non-whitespace byte, {first:?}, \
is not a digit, but found {got:?} after parsed \
weekday {wd:?} and expected a comma",
first = escape::Byte(input[0]),
got = escape::Byte(input[3]),
wd = escape::Bytes(&input[..3]),
));
}
let Parsed { input, .. } =
self.parse_whitespace(&input[4..]).with_context(|| {
err!(
"expected whitespace after parsing {got:?}",
got = escape::Bytes(&input[..4]),
)
})?;
Ok(Parsed { value: Some(wd), input })
}
#[inline(always)]
fn parse_day<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Day>, Error> {
if input.is_empty() {
return Err(err!("expected day, but found end of input"));
}
let mut digits = 1;
if input.len() >= 2 && matches!(input[1], b'0'..=b'9') {
digits = 2;
}
let (day, input) = input.split_at(digits);
let day = parse::i64(day).with_context(|| {
err!("failed to parse {day:?} as day", day = escape::Bytes(day))
})?;
let day = t::Day::try_new("day", day).context("day is not valid")?;
let Parsed { input, .. } =
self.parse_whitespace(input).with_context(|| {
err!("expected whitespace after parsing day {day}")
})?;
Ok(Parsed { value: day, input })
}
#[inline(always)]
fn parse_month<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Month>, Error> {
if input.is_empty() {
return Err(err!(
"expected abbreviated month name, but found end of input"
));
}
if input.len() < 3 {
return Err(err!(
"expected abbreviated month name, but remaining input \
is too short (remaining bytes is {length})",
length = input.len(),
));
}
let b1 = input[0].to_ascii_lowercase();
let b2 = input[1].to_ascii_lowercase();
let b3 = input[2].to_ascii_lowercase();
let month = match &[b1, b2, b3] {
b"jan" => 1,
b"feb" => 2,
b"mar" => 3,
b"apr" => 4,
b"may" => 5,
b"jun" => 6,
b"jul" => 7,
b"aug" => 8,
b"sep" => 9,
b"oct" => 10,
b"nov" => 11,
b"dec" => 12,
_ => {
return Err(err!(
"expected abbreviated month name, \
but did not recognize {got:?} \
as a valid month",
got = escape::Bytes(&input[..3]),
));
}
};
let month = t::Month::new(month).unwrap();
let Parsed { input, .. } =
self.parse_whitespace(&input[3..]).with_context(|| {
err!("expected whitespace after parsing month name")
})?;
Ok(Parsed { value: month, input })
}
#[inline(always)]
fn parse_year<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Year>, Error> {
let mut digits = 0;
while digits <= 3
&& !input[digits..].is_empty()
&& matches!(input[digits], b'0'..=b'9')
{
digits += 1;
}
if digits <= 1 {
return Err(err!(
"expected at least two ASCII digits for parsing \
a year, but only found {digits}",
));
}
let (year, input) = input.split_at(digits);
let year = parse::i64(year).with_context(|| {
err!(
"failed to parse {year:?} as year \
(a two, three or four digit integer)",
year = escape::Bytes(year),
)
})?;
let year = match digits {
2 if year <= 49 => year + 2000,
2 | 3 => year + 1900,
4 => year,
_ => unreachable!("digits={digits} must be 2, 3 or 4"),
};
let year =
t::Year::try_new("year", year).context("year is not valid")?;
let Parsed { input, .. } = self
.parse_whitespace(input)
.with_context(|| err!("expected whitespace after parsing year"))?;
Ok(Parsed { value: year, 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, Offset>, Error> {
type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>;
type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>;
let sign = input.get(0).copied().ok_or_else(|| {
err!(
"expected sign for time zone offset, \
(or a legacy time zone name abbreviation), \
but found end of input",
)
})?;
let sign = if sign == b'+' {
t::Sign::N::<1>()
} else if sign == b'-' {
t::Sign::N::<-1>()
} else {
return self.parse_offset_obsolete(input);
};
let input = &input[1..];
let (hhmm, input) = parse::split(input, 4).ok_or_else(|| {
err!(
"expected at least 4 digits for time zone offset \
after sign, but found only {len} bytes remaining",
len = input.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 seconds = hh * C(3_600) + mm * C(60);
let offset = Offset::from_seconds_ranged(seconds * sign);
Ok(Parsed { value: offset, input })
}
#[inline(never)]
fn parse_offset_obsolete<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Offset>, Error> {
let mut letters = [0; 5];
let mut len = 0;
while len <= 4
&& !input[len..].is_empty()
&& !is_whitespace(input[len])
{
letters[len] = input[len].to_ascii_lowercase();
len += 1;
}
if len == 0 {
return Err(err!(
"expected obsolete RFC 2822 time zone abbreviation, \
but found no remaining non-whitespace characters \
after time",
));
}
let offset = match &letters[..len] {
b"ut" | b"gmt" | b"z" => Offset::UTC,
b"est" => Offset::constant(-5),
b"edt" => Offset::constant(-4),
b"cst" => Offset::constant(-6),
b"cdt" => Offset::constant(-5),
b"mst" => Offset::constant(-7),
b"mdt" => Offset::constant(-6),
b"pst" => Offset::constant(-8),
b"pdt" => Offset::constant(-7),
name => {
if name.len() == 1
&& matches!(name[0], b'a'..=b'i' | b'k'..=b'z')
{
Offset::UTC
} else if name.len() >= 3
&& name.iter().all(|&b| matches!(b, b'a'..=b'z'))
{
Offset::UTC
} else {
return Err(err!(
"expected obsolete RFC 2822 time zone abbreviation, \
but found {found:?}",
found = escape::Bytes(&input[..len]),
));
}
}
};
Ok(Parsed { value: offset, input: &input[len..] })
}
#[inline(always)]
fn parse_time_separator<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
if input.is_empty() {
return Err(err!(
"expected time separator of ':', but found end of input",
));
}
if input[0] != b':' {
return Err(err!(
"expected time separator of ':', but found {got}",
got = escape::Byte(input[0]),
));
}
Ok(Parsed { value: (), input: &input[1..] })
}
#[inline(always)]
fn parse_whitespace<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let oldlen = input.len();
let parsed = self.skip_whitespace(input);
let newlen = parsed.input.len();
if oldlen == newlen {
return Err(err!(
"expected at least one whitespace character (space or tab), \
but found none",
));
}
Ok(parsed)
}
#[inline(always)]
fn skip_whitespace<'i>(&self, mut input: &'i [u8]) -> Parsed<'i, ()> {
while input.first().map_or(false, |&b| is_whitespace(b)) {
input = &input[1..];
}
Parsed { value: (), input }
}
#[inline(never)]
fn skip_comment<'i>(
&self,
mut input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
if !input.starts_with(b"(") {
return Ok(Parsed { value: (), input });
}
input = &input[1..];
let mut depth: u8 = 1;
let mut escape = false;
for byte in input.iter().copied() {
input = &input[1..];
if escape {
escape = false;
} else if byte == b'\\' {
escape = true;
} else if byte == b')' {
depth = depth.checked_sub(1).ok_or_else(|| {
err!(
"found closing parenthesis in comment with \
no matching opening parenthesis"
)
})?;
if depth == 0 {
break;
}
} else if byte == b'(' {
depth = depth.checked_add(1).ok_or_else(|| {
err!("found too many nested parenthesis in comment")
})?;
}
}
if depth > 0 {
return Err(err!(
"found opening parenthesis in comment with \
no matching closing parenthesis"
));
}
Ok(self.skip_whitespace(input))
}
}
#[derive(Debug)]
pub struct DateTimePrinter {
_private: (),
}
impl DateTimePrinter {
#[inline]
pub const fn new() -> DateTimePrinter {
DateTimePrinter { _private: () }
}
pub fn zoned_to_string(&self, zdt: &Zoned) -> Result<String, Error> {
let mut buf = String::with_capacity(4);
self.print_zoned(zdt, &mut buf)?;
Ok(buf)
}
pub fn timestamp_to_string(
&self,
timestamp: &Timestamp,
) -> Result<String, Error> {
let mut buf = String::with_capacity(4);
self.print_timestamp(timestamp, &mut buf)?;
Ok(buf)
}
pub fn timestamp_to_rfc9110_string(
&self,
timestamp: &Timestamp,
) -> Result<String, Error> {
let mut buf = String::with_capacity(4);
self.print_timestamp_rfc9110(timestamp, &mut buf)?;
Ok(buf)
}
pub fn print_zoned<W: Write>(
&self,
zdt: &Zoned,
wtr: W,
) -> Result<(), Error> {
self.print_civil_with_offset(zdt.datetime(), Some(zdt.offset()), wtr)
}
pub fn print_timestamp<W: Write>(
&self,
timestamp: &Timestamp,
wtr: W,
) -> Result<(), Error> {
let dt = TimeZone::UTC.to_datetime(*timestamp);
self.print_civil_with_offset(dt, None, wtr)
}
pub fn print_timestamp_rfc9110<W: Write>(
&self,
timestamp: &Timestamp,
wtr: W,
) -> Result<(), Error> {
self.print_civil_always_utc(timestamp, wtr)
}
fn print_civil_with_offset<W: Write>(
&self,
dt: DateTime,
offset: Option<Offset>,
mut wtr: W,
) -> Result<(), Error> {
static FMT_DAY: DecimalFormatter = DecimalFormatter::new();
static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4);
static FMT_TIME_UNIT: DecimalFormatter =
DecimalFormatter::new().padding(2);
if dt.year() < 0 {
return Err(err!(
"datetime {dt} has negative year, \
which cannot be formatted with RFC 2822",
));
}
wtr.write_str(weekday_abbrev(dt.weekday()))?;
wtr.write_str(", ")?;
wtr.write_int(&FMT_DAY, dt.day())?;
wtr.write_str(" ")?;
wtr.write_str(month_name(dt.month()))?;
wtr.write_str(" ")?;
wtr.write_int(&FMT_YEAR, dt.year())?;
wtr.write_str(" ")?;
wtr.write_int(&FMT_TIME_UNIT, dt.hour())?;
wtr.write_str(":")?;
wtr.write_int(&FMT_TIME_UNIT, dt.minute())?;
wtr.write_str(":")?;
wtr.write_int(&FMT_TIME_UNIT, dt.second())?;
wtr.write_str(" ")?;
let Some(offset) = offset else {
wtr.write_str("-0000")?;
return Ok(());
};
wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
let mut hours = offset.part_hours_ranged().abs().get();
let mut minutes = offset.part_minutes_ranged().abs().get();
if offset.part_seconds_ranged().abs() >= 30 {
if minutes == 59 {
hours = hours.saturating_add(1);
minutes = 0;
} else {
minutes = minutes.saturating_add(1);
}
}
wtr.write_int(&FMT_TIME_UNIT, hours)?;
wtr.write_int(&FMT_TIME_UNIT, minutes)?;
Ok(())
}
fn print_civil_always_utc<W: Write>(
&self,
timestamp: &Timestamp,
mut wtr: W,
) -> Result<(), Error> {
static FMT_DAY: DecimalFormatter = DecimalFormatter::new().padding(2);
static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4);
static FMT_TIME_UNIT: DecimalFormatter =
DecimalFormatter::new().padding(2);
let dt = TimeZone::UTC.to_datetime(*timestamp);
if dt.year() < 0 {
return Err(err!(
"datetime {dt} has negative year, \
which cannot be formatted with RFC 2822",
));
}
wtr.write_str(weekday_abbrev(dt.weekday()))?;
wtr.write_str(", ")?;
wtr.write_int(&FMT_DAY, dt.day())?;
wtr.write_str(" ")?;
wtr.write_str(month_name(dt.month()))?;
wtr.write_str(" ")?;
wtr.write_int(&FMT_YEAR, dt.year())?;
wtr.write_str(" ")?;
wtr.write_int(&FMT_TIME_UNIT, dt.hour())?;
wtr.write_str(":")?;
wtr.write_int(&FMT_TIME_UNIT, dt.minute())?;
wtr.write_str(":")?;
wtr.write_int(&FMT_TIME_UNIT, dt.second())?;
wtr.write_str(" ")?;
wtr.write_str("GMT")?;
Ok(())
}
}
fn weekday_abbrev(wd: Weekday) -> &'static str {
match wd {
Weekday::Sunday => "Sun",
Weekday::Monday => "Mon",
Weekday::Tuesday => "Tue",
Weekday::Wednesday => "Wed",
Weekday::Thursday => "Thu",
Weekday::Friday => "Fri",
Weekday::Saturday => "Sat",
}
}
fn month_name(month: i8) -> &'static str {
match month {
1 => "Jan",
2 => "Feb",
3 => "Mar",
4 => "Apr",
5 => "May",
6 => "Jun",
7 => "Jul",
8 => "Aug",
9 => "Sep",
10 => "Oct",
11 => "Nov",
12 => "Dec",
_ => unreachable!("invalid month value {month}"),
}
}
fn is_whitespace(byte: u8) -> bool {
byte.is_ascii_whitespace()
}
#[cfg(test)]
mod tests {
use alloc::string::{String, ToString};
use crate::civil::date;
use super::*;
#[test]
fn ok_parse_basic() {
let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 -0500"),
@"2024-01-10T05:34:45-05:00[-05:00]",
);
insta::assert_debug_snapshot!(
p("Tue, 9 Jan 2024 05:34:45 -0500"),
@"2024-01-09T05:34:45-05:00[-05:00]",
);
insta::assert_debug_snapshot!(
p("Tue, 09 Jan 2024 05:34:45 -0500"),
@"2024-01-09T05:34:45-05:00[-05:00]",
);
insta::assert_debug_snapshot!(
p("10 Jan 2024 05:34:45 -0500"),
@"2024-01-10T05:34:45-05:00[-05:00]",
);
insta::assert_debug_snapshot!(
p("10 Jan 2024 05:34 -0500"),
@"2024-01-10T05:34:00-05:00[-05:00]",
);
insta::assert_debug_snapshot!(
p("10 Jan 2024 05:34:45 +0500"),
@"2024-01-10T05:34:45+05:00[+05:00]",
);
insta::assert_debug_snapshot!(
p("Thu, 29 Feb 2024 05:34 -0500"),
@"2024-02-29T05:34:00-05:00[-05:00]",
);
insta::assert_debug_snapshot!(
p("10 Jan 2024 05:34:60 -0500"),
@"2024-01-10T05:34:59-05:00[-05:00]",
);
}
#[test]
fn ok_parse_obsolete_zone() {
let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 EST"),
@"2024-01-10T05:34:45-05:00[-05:00]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 EDT"),
@"2024-01-10T05:34:45-04:00[-04:00]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 CST"),
@"2024-01-10T05:34:45-06:00[-06:00]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 CDT"),
@"2024-01-10T05:34:45-05:00[-05:00]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 mst"),
@"2024-01-10T05:34:45-07:00[-07:00]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 mdt"),
@"2024-01-10T05:34:45-06:00[-06:00]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 pst"),
@"2024-01-10T05:34:45-08:00[-08:00]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 pdt"),
@"2024-01-10T05:34:45-07:00[-07:00]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 UT"),
@"2024-01-10T05:34:45+00:00[UTC]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 Z"),
@"2024-01-10T05:34:45+00:00[UTC]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 gmt"),
@"2024-01-10T05:34:45+00:00[UTC]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 XXX"),
@"2024-01-10T05:34:45+00:00[UTC]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 ABCDE"),
@"2024-01-10T05:34:45+00:00[UTC]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 FUCK"),
@"2024-01-10T05:34:45+00:00[UTC]",
);
}
#[test]
fn ok_parse_comment() {
let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 -0500 (wat)"),
@"2024-01-10T05:34:45-05:00[-05:00]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 -0500 (w(a)t)"),
@"2024-01-10T05:34:45-05:00[-05:00]",
);
insta::assert_debug_snapshot!(
p(r"Wed, 10 Jan 2024 05:34:45 -0500 (w\(a\)t)"),
@"2024-01-10T05:34:45-05:00[-05:00]",
);
}
#[test]
fn ok_parse_whitespace() {
let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
insta::assert_debug_snapshot!(
p("Wed, 10 \t Jan \n\r\n\n 2024 05:34:45 -0500"),
@"2024-01-10T05:34:45-05:00[-05:00]",
);
insta::assert_debug_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 -0500 "),
@"2024-01-10T05:34:45-05:00[-05:00]",
);
}
#[test]
fn err_parse_invalid() {
let p = |input| {
DateTimeParser::new().parse_zoned(input).unwrap_err().to_string()
};
insta::assert_snapshot!(
p("Thu, 10 Jan 2024 05:34:45 -0500"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: found parsed weekday of Thu, but parsed datetime of 2024-01-10T05:34:45 has weekday Wed",
);
insta::assert_snapshot!(
p("Wed, 29 Feb 2023 05:34:45 -0500"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: invalid date: parameter 'day' with value 29 is not in the required range of 1..=28",
);
insta::assert_snapshot!(
p("Mon, 31 Jun 2024 05:34:45 -0500"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: invalid date: parameter 'day' with value 31 is not in the required range of 1..=30",
);
insta::assert_snapshot!(
p("Tue, 32 Jun 2024 05:34:45 -0500"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: day is not valid: parameter 'day' with value 32 is not in the required range of 1..=31",
);
insta::assert_snapshot!(
p("Sun, 30 Jun 2024 24:00:00 -0500"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: hour is not valid: parameter 'hour' with value 24 is not in the required range of 0..=23",
);
}
#[test]
fn err_parse_incomplete() {
let p = |input| {
DateTimeParser::new().parse_zoned(input).unwrap_err().to_string()
};
insta::assert_snapshot!(
p(""),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string",
);
insta::assert_snapshot!(
p(" "),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming whitespace",
);
insta::assert_snapshot!(
p("Wat"),
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but given string is too short (length is 3)"###,
);
insta::assert_snapshot!(
p("Wed"),
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but given string is too short (length is 3)"###,
);
insta::assert_snapshot!(
p("Wat, "),
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but did not recognize "Wat" as a valid weekday abbreviation"###,
);
insta::assert_snapshot!(
p("Wed, "),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
);
insta::assert_snapshot!(
p("Wed, 1"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day 1: expected at least one whitespace character (space or tab), but found none",
);
insta::assert_snapshot!(
p("Wed, 10"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day 10: expected at least one whitespace character (space or tab), but found none",
);
insta::assert_snapshot!(
p("Wed, 10 J"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but remaining input is too short (remaining bytes is 1)",
);
insta::assert_snapshot!(
p("Wed, 10 Wat"),
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize "Wat" as a valid month"###,
);
insta::assert_snapshot!(
p("Wed, 10 Jan"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing month name: expected at least one whitespace character (space or tab), but found none",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected at least two ASCII digits for parsing a year, but only found 1",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing year: expected at least one whitespace character (space or tab), but found none",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024 05"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found end of input",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024 053"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found 3",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024 05:34"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024 05:34:"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected two digit second, but found end of input",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024 05:34:45"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 J"),
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but found "J""###,
);
}
#[test]
fn err_parse_comment() {
let p = |input| {
DateTimeParser::new().parse_zoned(input).unwrap_err().to_string()
};
insta::assert_snapshot!(
p(r"Wed, 10 Jan 2024 05:34:45 -0500 (wa)t)"),
@r###"parsed value '2024-01-10T05:34:45-05:00[-05:00]', but unparsed input "t)" remains (expected no unparsed input)"###,
);
insta::assert_snapshot!(
p(r"Wed, 10 Jan 2024 05:34:45 -0500 (wa(t)"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
);
insta::assert_snapshot!(
p(r"Wed, 10 Jan 2024 05:34:45 -0500 (w"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
);
insta::assert_snapshot!(
p(r"Wed, 10 Jan 2024 05:34:45 -0500 ("),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
);
insta::assert_snapshot!(
p(r"Wed, 10 Jan 2024 05:34:45 -0500 ( "),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
);
}
#[test]
fn ok_print_zoned() {
if crate::tz::db().is_definitively_empty() {
return;
}
let p = |zdt: &Zoned| -> String {
let mut buf = String::new();
DateTimePrinter::new().print_zoned(&zdt, &mut buf).unwrap();
buf
};
let zdt = date(2024, 1, 10)
.at(5, 34, 45, 0)
.intz("America/New_York")
.unwrap();
insta::assert_snapshot!(p(&zdt), @"Wed, 10 Jan 2024 05:34:45 -0500");
let zdt = date(2024, 2, 5)
.at(5, 34, 45, 0)
.intz("America/New_York")
.unwrap();
insta::assert_snapshot!(p(&zdt), @"Mon, 5 Feb 2024 05:34:45 -0500");
let zdt = date(2024, 7, 31)
.at(5, 34, 45, 0)
.intz("America/New_York")
.unwrap();
insta::assert_snapshot!(p(&zdt), @"Wed, 31 Jul 2024 05:34:45 -0400");
let zdt = date(2024, 3, 5).at(5, 34, 45, 0).intz("UTC").unwrap();
insta::assert_snapshot!(p(&zdt), @"Tue, 5 Mar 2024 05:34:45 +0000");
}
#[test]
fn ok_print_timestamp() {
if crate::tz::db().is_definitively_empty() {
return;
}
let p = |ts: Timestamp| -> String {
let mut buf = String::new();
DateTimePrinter::new().print_timestamp(&ts, &mut buf).unwrap();
buf
};
let ts = date(2024, 1, 10)
.at(5, 34, 45, 0)
.intz("America/New_York")
.unwrap()
.timestamp();
insta::assert_snapshot!(p(ts), @"Wed, 10 Jan 2024 10:34:45 -0000");
let ts = date(2024, 2, 5)
.at(5, 34, 45, 0)
.intz("America/New_York")
.unwrap()
.timestamp();
insta::assert_snapshot!(p(ts), @"Mon, 5 Feb 2024 10:34:45 -0000");
let ts = date(2024, 7, 31)
.at(5, 34, 45, 0)
.intz("America/New_York")
.unwrap()
.timestamp();
insta::assert_snapshot!(p(ts), @"Wed, 31 Jul 2024 09:34:45 -0000");
let ts =
date(2024, 3, 5).at(5, 34, 45, 0).intz("UTC").unwrap().timestamp();
insta::assert_snapshot!(p(ts), @"Tue, 5 Mar 2024 05:34:45 -0000");
}
#[test]
fn ok_print_rfc9110_timestamp() {
if crate::tz::db().is_definitively_empty() {
return;
}
let p = |ts: Timestamp| -> String {
let mut buf = String::new();
DateTimePrinter::new()
.print_timestamp_rfc9110(&ts, &mut buf)
.unwrap();
buf
};
let ts = date(2024, 1, 10)
.at(5, 34, 45, 0)
.intz("America/New_York")
.unwrap()
.timestamp();
insta::assert_snapshot!(p(ts), @"Wed, 10 Jan 2024 10:34:45 GMT");
let ts = date(2024, 2, 5)
.at(5, 34, 45, 0)
.intz("America/New_York")
.unwrap()
.timestamp();
insta::assert_snapshot!(p(ts), @"Mon, 05 Feb 2024 10:34:45 GMT");
let ts = date(2024, 7, 31)
.at(5, 34, 45, 0)
.intz("America/New_York")
.unwrap()
.timestamp();
insta::assert_snapshot!(p(ts), @"Wed, 31 Jul 2024 09:34:45 GMT");
let ts =
date(2024, 3, 5).at(5, 34, 45, 0).intz("UTC").unwrap().timestamp();
insta::assert_snapshot!(p(ts), @"Tue, 05 Mar 2024 05:34:45 GMT");
}
#[test]
fn err_print_zoned() {
if crate::tz::db().is_definitively_empty() {
return;
}
let p = |zdt: &Zoned| -> String {
let mut buf = String::new();
DateTimePrinter::new()
.print_zoned(&zdt, &mut buf)
.unwrap_err()
.to_string()
};
let zdt =
date(-1, 1, 10).at(5, 34, 45, 0).intz("America/New_York").unwrap();
insta::assert_snapshot!(p(&zdt), @"datetime -000001-01-10T05:34:45 has negative year, which cannot be formatted with RFC 2822");
}
#[test]
fn err_print_timestamp() {
if crate::tz::db().is_definitively_empty() {
return;
}
let p = |ts: Timestamp| -> String {
let mut buf = String::new();
DateTimePrinter::new()
.print_timestamp(&ts, &mut buf)
.unwrap_err()
.to_string()
};
let ts = date(-1, 1, 10)
.at(5, 34, 45, 0)
.intz("America/New_York")
.unwrap()
.timestamp();
insta::assert_snapshot!(p(ts), @"datetime -000001-01-10T10:30:47 has negative year, which cannot be formatted with RFC 2822");
}
}