use crate::{
civil::{Date, DateTime, Time, Weekday},
error::{fmt::rfc2822::Error as E, ErrorContext},
fmt::{buffer::BorrowedBuffer, Parsed, Write},
tz::{Offset, TimeZone},
util::{b, parse},
Error, Timestamp, Zoned,
};
pub(crate) static DEFAULT_DATETIME_PARSER: DateTimeParser =
DateTimeParser::new();
pub(crate) static DEFAULT_DATETIME_PRINTER: DateTimePrinter =
DateTimePrinter::new();
const PRINTER_MAX_BYTES_RFC2822: usize = 31;
const PRINTER_MAX_BYTES_RFC9110: usize = 29;
#[cfg(feature = "alloc")]
#[inline]
pub fn to_string(zdt: &Zoned) -> Result<alloc::string::String, Error> {
let mut buf = alloc::string::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(E::FailedZoned)?
.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(E::FailedTimestamp)?
.into_full()?;
Ok(ts)
}
#[cfg_attr(feature = "perf-inline", 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)?;
let zdt = ts.to_zoned(TimeZone::fixed(offset));
Ok(Parsed { value: zdt, input })
}
#[cfg_attr(feature = "perf-inline", 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)?;
Ok(Parsed { value: ts, input })
}
#[cfg_attr(feature = "perf-inline", 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 })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_datetime<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, DateTime>, Error> {
if input.is_empty() {
return Err(Error::from(E::Empty));
}
let Parsed { input, .. } = self.skip_whitespace(input);
if input.is_empty() {
return Err(Error::from(E::EmptyAfterWhitespace));
}
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.skip_whitespace(input);
let Parsed { input, .. } = self.parse_time_separator(input)?;
let Parsed { input, .. } = self.skip_whitespace(input);
let Parsed { value: minute, input } = self.parse_minute(input)?;
let Parsed { value: whitespace_after_minute, input } =
self.skip_whitespace(input);
let (second, input) = if !input.starts_with(b":") {
if !whitespace_after_minute {
return Err(Error::from(E::WhitespaceAfterTime));
}
(0, input)
} else {
let Parsed { input, .. } = self.parse_time_separator(input)?;
let Parsed { input, .. } = self.skip_whitespace(input);
let Parsed { value: second, input } = self.parse_second(input)?;
let Parsed { input, .. } = self.parse_whitespace(input)?;
(second, input)
};
let date = Date::new(year, month, day).context(E::InvalidDate)?;
let time = Time::new(hour, minute, second, 0).unwrap();
let dt = DateTime::from_parts(date, time);
if let Some(wd) = wd {
if !self.relaxed_weekday && wd != dt.weekday() {
return Err(Error::from(E::InconsistentWeekday {
parsed: wd,
from_date: dt.weekday(),
}));
}
}
Ok(Parsed { value: dt, input })
}
#[cfg_attr(feature = "perf-inline", 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 let Ok(len) = u8::try_from(input.len()) {
if len < 4 {
return Err(Error::from(E::TooShortWeekday {
got_non_digit: input[0],
len,
}));
}
}
let b1 = input[0];
let b2 = input[1];
let b3 = input[2];
let wd = match &[
b1.to_ascii_lowercase(),
b2.to_ascii_lowercase(),
b3.to_ascii_lowercase(),
] {
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(Error::from(E::InvalidWeekday {
got_non_digit: input[0],
}));
}
};
let Parsed { input, .. } = self.skip_whitespace(&input[3..]);
let Some(should_be_comma) = input.get(0).copied() else {
return Err(Error::from(E::EndOfInputComma));
};
if should_be_comma != b',' {
return Err(Error::from(E::UnexpectedByteComma {
byte: should_be_comma,
}));
}
let Parsed { input, .. } = self.skip_whitespace(&input[1..]);
Ok(Parsed { value: Some(wd), input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_day<'i>(&self, input: &'i [u8]) -> Result<Parsed<'i, i8>, Error> {
if input.is_empty() {
return Err(Error::from(E::EndOfInputDay));
}
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 = b::Day::parse(day).context(E::ParseDay)?;
let Parsed { input, .. } =
self.parse_whitespace(input).context(E::WhitespaceAfterDay)?;
Ok(Parsed { value: day, input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_month<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, i8>, Error> {
if input.is_empty() {
return Err(Error::from(E::EndOfInputMonth));
}
if let Ok(len) = u8::try_from(input.len()) {
if len < 3 {
return Err(Error::from(E::TooShortMonth { 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(Error::from(E::InvalidMonth)),
};
let Parsed { input, .. } = self
.parse_whitespace(&input[3..])
.context(E::WhitespaceAfterMonth)?;
Ok(Parsed { value: month, input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_year<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, i16>, Error> {
let mut digits = 0;
while digits <= 3
&& !input[digits..].is_empty()
&& matches!(input[digits], b'0'..=b'9')
{
digits += 1;
}
if let Ok(len) = u8::try_from(digits) {
if len <= 1 {
return Err(Error::from(E::TooShortYear { len }));
}
}
let (year, input) = input.split_at(digits);
let year = b::Year::parse(year).context(E::ParseYear)?;
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 Parsed { input, .. } =
self.parse_whitespace(input).context(E::WhitespaceAfterYear)?;
Ok(Parsed { value: year, input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_hour<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, i8>, Error> {
let (hour, input) = parse::split(input, 2).ok_or(E::EndOfInputHour)?;
let hour = b::Hour::parse(hour).context(E::ParseHour)?;
Ok(Parsed { value: hour, input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_minute<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, i8>, Error> {
let (minute, input) =
parse::split(input, 2).ok_or(E::EndOfInputMinute)?;
let minute = b::Minute::parse(minute).context(E::ParseMinute)?;
Ok(Parsed { value: minute, input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_second<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, i8>, Error> {
let (second, input) =
parse::split(input, 2).ok_or(E::EndOfInputSecond)?;
let mut second =
b::LeapSecond::parse(second).context(E::ParseSecond)?;
if second == 60 {
second = 59;
}
Ok(Parsed { value: second, input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_offset<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Offset>, Error> {
let sign = input.get(0).copied().ok_or(E::EndOfInputOffset)?;
let sign = if sign == b'+' {
b::Sign::Positive
} else if sign == b'-' {
b::Sign::Negative
} else {
return self.parse_offset_obsolete(input);
};
let input = &input[1..];
let (hhmm, input) = parse::split(input, 4).ok_or(E::TooShortOffset)?;
let hh =
b::OffsetHours::parse(&hhmm[0..2]).context(E::ParseOffsetHour)?;
let mm = b::OffsetMinutes::parse(&hhmm[2..4])
.context(E::ParseOffsetMinute)?;
let seconds = sign * (i32::from(hh) * 3_600 + i32::from(mm) * 60);
let offset = Offset::from_seconds(seconds).unwrap();
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(Error::from(E::WhitespaceAfterTimeForObsoleteOffset));
}
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(Error::from(E::InvalidObsoleteOffset));
}
}
};
Ok(Parsed { value: offset, input: &input[len..] })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_time_separator<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
if input.is_empty() {
return Err(Error::from(E::EndOfInputTimeSeparator));
}
if input[0] != b':' {
return Err(Error::from(E::UnexpectedByteTimeSeparator {
byte: input[0],
}));
}
Ok(Parsed { value: (), input: &input[1..] })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_whitespace<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let Parsed { input, value: had_whitespace } =
self.skip_whitespace(input);
if !had_whitespace {
return Err(Error::from(E::WhitespaceAfterTime));
}
Ok(Parsed { value: (), input })
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn skip_whitespace<'i>(&self, mut input: &'i [u8]) -> Parsed<'i, bool> {
let mut found_whitespace = false;
while input.first().map_or(false, |&b| is_whitespace(b)) {
input = &input[1..];
found_whitespace = true;
}
Parsed { value: found_whitespace, 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(E::CommentClosingParenWithoutOpen)?;
if depth == 0 {
break;
}
} else if byte == b'(' {
depth = depth
.checked_add(1)
.ok_or(E::CommentTooManyNestedParens)?;
}
}
if depth > 0 {
return Err(Error::from(E::CommentOpeningParenWithoutClose));
}
let Parsed { input, .. } = self.skip_whitespace(input);
Ok(Parsed { value: (), input })
}
}
#[derive(Debug)]
pub struct DateTimePrinter {
_private: (),
}
impl DateTimePrinter {
#[inline]
pub const fn new() -> DateTimePrinter {
DateTimePrinter { _private: () }
}
#[cfg(feature = "alloc")]
pub fn zoned_to_string(
&self,
zdt: &Zoned,
) -> Result<alloc::string::String, Error> {
let mut buf =
alloc::string::String::with_capacity(PRINTER_MAX_BYTES_RFC2822);
self.print_zoned(zdt, &mut buf)?;
Ok(buf)
}
#[cfg(feature = "alloc")]
pub fn timestamp_to_string(
&self,
timestamp: &Timestamp,
) -> Result<alloc::string::String, Error> {
let mut buf =
alloc::string::String::with_capacity(PRINTER_MAX_BYTES_RFC2822);
self.print_timestamp(timestamp, &mut buf)?;
Ok(buf)
}
#[cfg(feature = "alloc")]
pub fn timestamp_to_rfc9110_string(
&self,
timestamp: &Timestamp,
) -> Result<alloc::string::String, Error> {
let mut buf =
alloc::string::String::with_capacity(PRINTER_MAX_BYTES_RFC9110);
self.print_timestamp_rfc9110(timestamp, &mut buf)?;
Ok(buf)
}
pub fn print_zoned<W: Write>(
&self,
zdt: &Zoned,
mut wtr: W,
) -> Result<(), Error> {
BorrowedBuffer::with_writer::<PRINTER_MAX_BYTES_RFC2822>(
&mut wtr,
PRINTER_MAX_BYTES_RFC2822,
|bbuf| {
self.print_civil_with_offset(
zdt.datetime(),
Some(zdt.offset()),
bbuf,
)
},
)
}
pub fn print_timestamp<W: Write>(
&self,
timestamp: &Timestamp,
mut wtr: W,
) -> Result<(), Error> {
let dt = TimeZone::UTC.to_datetime(*timestamp);
BorrowedBuffer::with_writer::<PRINTER_MAX_BYTES_RFC2822>(
&mut wtr,
PRINTER_MAX_BYTES_RFC2822,
|bbuf| self.print_civil_with_offset(dt, None, bbuf),
)
}
pub fn print_timestamp_rfc9110<W: Write>(
&self,
timestamp: &Timestamp,
mut wtr: W,
) -> Result<(), Error> {
let dt = TimeZone::UTC.to_datetime(*timestamp);
BorrowedBuffer::with_writer::<PRINTER_MAX_BYTES_RFC9110>(
&mut wtr,
PRINTER_MAX_BYTES_RFC9110,
|bbuf| self.print_civil_always_utc(dt, bbuf),
)
}
#[inline(never)]
fn print_civil_with_offset(
&self,
dt: DateTime,
offset: Option<Offset>,
buf: &mut BorrowedBuffer<'_>,
) -> Result<(), Error> {
if dt.year() < 0 {
return Err(Error::from(E::NegativeYear));
}
buf.write_str(weekday_abbrev(dt.weekday()));
buf.write_str(", ");
buf.write_int(dt.day().unsigned_abs());
buf.write_ascii_char(b' ');
buf.write_str(month_name(dt.month()));
buf.write_ascii_char(b' ');
buf.write_int_pad4(dt.year().unsigned_abs());
buf.write_ascii_char(b' ');
buf.write_int_pad2(dt.hour().unsigned_abs());
buf.write_ascii_char(b':');
buf.write_int_pad2(dt.minute().unsigned_abs());
buf.write_ascii_char(b':');
buf.write_int_pad2(dt.second().unsigned_abs());
buf.write_ascii_char(b' ');
let Some(offset) = offset else {
buf.write_str("-0000");
return Ok(());
};
buf.write_ascii_char(if offset.is_negative() { b'-' } else { b'+' });
let (offset_hours, offset_minutes) = offset.round_to_nearest_minute();
buf.write_int_pad2(offset_hours);
buf.write_int_pad2(offset_minutes);
Ok(())
}
#[inline(never)]
fn print_civil_always_utc(
&self,
dt: DateTime,
buf: &mut BorrowedBuffer<'_>,
) -> Result<(), Error> {
if dt.year() < 0 {
return Err(Error::from(E::NegativeYear));
}
buf.write_str(weekday_abbrev(dt.weekday()));
buf.write_str(", ");
buf.write_int_pad2(dt.day().unsigned_abs());
buf.write_str(" ");
buf.write_str(month_name(dt.month()));
buf.write_str(" ");
buf.write_int_pad4(dt.year().unsigned_abs());
buf.write_str(" ");
buf.write_int_pad2(dt.hour().unsigned_abs());
buf.write_str(":");
buf.write_int_pad2(dt.minute().unsigned_abs());
buf.write_str(":");
buf.write_int_pad2(dt.second().unsigned_abs());
buf.write_str(" ");
buf.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(feature = "alloc")]
#[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]",
);
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("Wed , 10 Jan 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]",
);
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("Wed, 10 Jan 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 `Thursday`, but parsed datetime has weekday `Wednesday`",
);
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' for `2023-02` is invalid, must be in range `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' for `2024-06` is invalid, must be in range `1..=30`",
);
insta::assert_snapshot!(
p("Tue, 32 Jun 2024 05:34:45 -0500"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: failed to parse day: parameter 'day' 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: failed to parse hour (expects a two digit integer): parameter 'hour' is not in the required range of 0..=23",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024 05:34MST"),
@r###"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"###,
);
}
#[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 leading whitespace",
);
insta::assert_snapshot!(
p("Wat"),
@"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"),
@"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 "),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected comma after parsed weekday in RFC 2822 datetime, but found end of input instead",
);
insta::assert_snapshot!(
p("Wed ,"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
);
insta::assert_snapshot!(
p("Wed , "),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
);
insta::assert_snapshot!(
p("Wat, "),
@"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 a valid weekday abbreviation",
);
insta::assert_snapshot!(
p("Wed, "),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric 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: expected whitespace after parsing time: 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: expected whitespace after parsing time: 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"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize a valid abbreviated month name",
);
insta::assert_snapshot!(
p("Wed, 10 Jan"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing abbreviated month name: expected whitespace after parsing time: 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 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"),
@"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"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but did not recognize a valid abbreviation",
);
}
#[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)
.in_tz("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)
.in_tz("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)
.in_tz("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).in_tz("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)
.in_tz("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)
.in_tz("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)
.in_tz("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)
.in_tz("UTC")
.unwrap()
.timestamp();
insta::assert_snapshot!(p(ts), @"Tue, 5 Mar 2024 05:34:45 -0000");
}
#[test]
fn ok_minimum_offset_roundtrip() {
let zdt = date(2025, 12, 25)
.at(17, 0, 0, 0)
.to_zoned(TimeZone::fixed(Offset::MIN))
.unwrap();
let string = DateTimePrinter::new().zoned_to_string(&zdt).unwrap();
assert_eq!(string, "Thu, 25 Dec 2025 17:00:00 -2559");
let got: Zoned = DateTimeParser::new().parse_zoned(&string).unwrap();
let expected = date(2025, 12, 25)
.at(17, 0, 0, 0)
.to_zoned(TimeZone::fixed(-Offset::hms(25, 59, 0)))
.unwrap();
assert_eq!(expected, got);
}
#[test]
fn ok_maximum_offset_roundtrip() {
let zdt = date(2025, 12, 25)
.at(17, 0, 0, 0)
.to_zoned(TimeZone::fixed(Offset::MAX))
.unwrap();
let string = DateTimePrinter::new().zoned_to_string(&zdt).unwrap();
assert_eq!(string, "Thu, 25 Dec 2025 17:00:00 +2559");
let got: Zoned = DateTimeParser::new().parse_zoned(&string).unwrap();
let expected = date(2025, 12, 25)
.at(17, 0, 0, 0)
.to_zoned(TimeZone::fixed(Offset::hms(25, 59, 0)))
.unwrap();
assert_eq!(expected, got);
}
#[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)
.in_tz("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)
.in_tz("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)
.in_tz("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)
.in_tz("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)
.in_tz("America/New_York")
.unwrap();
insta::assert_snapshot!(p(&zdt), @"datetime 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)
.in_tz("America/New_York")
.unwrap()
.timestamp();
insta::assert_snapshot!(p(ts), @"datetime has negative year, which cannot be formatted with RFC 2822");
}
}