use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
use nom::{
branch::alt,
bytes::complete::{is_a, tag, tag_no_case, take_while_m_n},
character,
character::complete::{alphanumeric1, digit0},
combinator::{map, opt, value},
sequence::{delimited, preceded, terminated, tuple},
IResult,
};
use crate::text::whitespace::{cfws, fws};
const MIN: i32 = 60;
const HOUR: i32 = 60 * MIN;
pub fn section(input: &[u8]) -> IResult<&[u8], Option<DateTime<FixedOffset>>> {
map(
terminated(
alt((
tuple((
opt(terminated(strict_day_of_week, tag(","))),
strict_date,
strict_time_of_day,
strict_zone,
)),
tuple((
opt(terminated(obs_day_of_week, tag(","))),
obs_date,
obs_time_of_day,
alt((strict_zone, obs_zone)),
)),
)),
opt(cfws),
),
|res| match res {
(_, Some(date), Some(time), Some(tz)) => {
date.and_time(time).and_local_timezone(tz).earliest()
}
_ => None,
},
)(input)
}
fn strict_day_of_week(input: &[u8]) -> IResult<&[u8], &[u8]> {
preceded(opt(fws), day_name)(input)
}
fn obs_day_of_week(input: &[u8]) -> IResult<&[u8], &[u8]> {
delimited(opt(cfws), day_name, opt(cfws))(input)
}
fn day_name(input: &[u8]) -> IResult<&[u8], &[u8]> {
alt((
tag_no_case(b"Mon"),
tag_no_case(b"Tue"),
tag_no_case(b"Wed"),
tag_no_case(b"Thu"),
tag_no_case(b"Fri"),
tag_no_case(b"Sat"),
tag_no_case(b"Sun"),
))(input)
}
fn strict_date(input: &[u8]) -> IResult<&[u8], Option<NaiveDate>> {
map(tuple((strict_day, month, strict_year)), |(d, m, y)| {
NaiveDate::from_ymd_opt(y, m, d)
})(input)
}
fn obs_date(input: &[u8]) -> IResult<&[u8], Option<NaiveDate>> {
map(tuple((obs_day, month, obs_year)), |(d, m, y)| {
NaiveDate::from_ymd_opt(y, m, d)
})(input)
}
fn strict_day(input: &[u8]) -> IResult<&[u8], u32> {
delimited(opt(fws), character::complete::u32, fws)(input)
}
fn obs_day(input: &[u8]) -> IResult<&[u8], u32> {
delimited(opt(cfws), character::complete::u32, opt(cfws))(input)
}
fn month(input: &[u8]) -> IResult<&[u8], u32> {
alt((
value(1, tag_no_case(b"Jan")),
value(2, tag_no_case(b"Feb")),
value(3, tag_no_case(b"Mar")),
value(4, tag_no_case(b"Apr")),
value(5, tag_no_case(b"May")),
value(6, tag_no_case(b"Jun")),
value(7, tag_no_case(b"Jul")),
value(8, tag_no_case(b"Aug")),
value(9, tag_no_case(b"Sep")),
value(10, tag_no_case(b"Oct")),
value(11, tag_no_case(b"Nov")),
value(12, tag_no_case(b"Dec")),
))(input)
}
fn strict_year(input: &[u8]) -> IResult<&[u8], i32> {
delimited(
fws,
map(
terminated(take_while_m_n(4, 9, |c| (0x30..=0x39).contains(&c)), digit0),
|d: &[u8]| {
encoding_rs::UTF_8
.decode_without_bom_handling(d)
.0
.parse::<i32>()
.unwrap_or(0)
},
),
fws,
)(input)
}
fn obs_year(input: &[u8]) -> IResult<&[u8], i32> {
map(
delimited(
opt(cfws),
terminated(take_while_m_n(2, 7, |c| (0x30..=0x39).contains(&c)), digit0),
opt(cfws),
),
|cap: &[u8]| {
let year_txt = encoding_rs::UTF_8.decode_without_bom_handling(cap).0;
let d = year_txt.parse::<i32>().unwrap_or(0);
if (0..=49).contains(&d) {
2000 + d
} else if (50..=999).contains(&d) {
1900 + d
} else {
d
}
},
)(input)
}
fn strict_time_of_day(input: &[u8]) -> IResult<&[u8], Option<NaiveTime>> {
map(
tuple((
strict_time_digit,
tag(":"),
strict_time_digit,
opt(preceded(tag(":"), strict_time_digit)),
)),
|(hour, _, minute, maybe_sec)| {
NaiveTime::from_hms_opt(hour, minute, maybe_sec.unwrap_or(0))
},
)(input)
}
fn obs_time_of_day(input: &[u8]) -> IResult<&[u8], Option<NaiveTime>> {
map(
tuple((
obs_time_digit,
tag(":"),
obs_time_digit,
opt(preceded(tag(":"), obs_time_digit)),
)),
|(hour, _, minute, maybe_sec)| {
NaiveTime::from_hms_opt(hour, minute, maybe_sec.unwrap_or(0))
},
)(input)
}
fn strict_time_digit(input: &[u8]) -> IResult<&[u8], u32> {
character::complete::u32(input)
}
fn obs_time_digit(input: &[u8]) -> IResult<&[u8], u32> {
delimited(opt(cfws), character::complete::u32, opt(cfws))(input)
}
fn strict_zone(input: &[u8]) -> IResult<&[u8], Option<FixedOffset>> {
map(
tuple((
opt(fws),
is_a("+-"),
take_while_m_n(2, 2, |c| (0x30..=0x39).contains(&c)),
take_while_m_n(2, 2, |c| (0x30..=0x39).contains(&c)),
)),
|(_, op, dig_zone_hour, dig_zone_min)| {
let zone_hour: i32 =
((dig_zone_hour[0] - 0x30) * 10 + (dig_zone_hour[1] - 0x30)) as i32 * HOUR;
let zone_min: i32 =
((dig_zone_min[0] - 0x30) * 10 + (dig_zone_min[1] - 0x30)) as i32 * MIN;
match op {
b"+" => FixedOffset::east_opt(zone_hour + zone_min),
b"-" => FixedOffset::west_opt(zone_hour + zone_min),
_ => unreachable!(),
}
},
)(input)
}
fn obs_zone(input: &[u8]) -> IResult<&[u8], Option<FixedOffset>> {
preceded(
opt(fws),
alt((
value(
FixedOffset::west_opt(0 * HOUR),
alt((tag_no_case(b"UTC"), tag_no_case(b"UT"), tag_no_case(b"GMT"))),
),
value(FixedOffset::west_opt(4 * HOUR), tag_no_case(b"EDT")),
value(
FixedOffset::west_opt(5 * HOUR),
alt((tag_no_case(b"EST"), tag_no_case(b"CDT"))),
),
value(
FixedOffset::west_opt(6 * HOUR),
alt((tag_no_case(b"CST"), tag_no_case(b"MDT"))),
),
value(
FixedOffset::west_opt(7 * HOUR),
alt((tag_no_case(b"MST"), tag_no_case(b"PDT"))),
),
value(FixedOffset::west_opt(8 * HOUR), tag_no_case(b"PST")),
value(FixedOffset::west_opt(0 * HOUR), tag_no_case(b"Z")),
alt((
value(FixedOffset::east_opt(HOUR), tag_no_case(b"A")),
value(FixedOffset::east_opt(2 * HOUR), tag_no_case(b"B")),
value(FixedOffset::east_opt(3 * HOUR), tag_no_case(b"C")),
value(FixedOffset::east_opt(4 * HOUR), tag_no_case(b"D")),
value(FixedOffset::east_opt(5 * HOUR), tag_no_case(b"E")),
value(FixedOffset::east_opt(6 * HOUR), tag_no_case(b"F")),
value(FixedOffset::east_opt(7 * HOUR), tag_no_case(b"G")),
value(FixedOffset::east_opt(8 * HOUR), tag_no_case(b"H")),
value(FixedOffset::east_opt(9 * HOUR), tag_no_case(b"I")),
value(FixedOffset::east_opt(10 * HOUR), tag_no_case(b"K")),
value(FixedOffset::east_opt(11 * HOUR), tag_no_case(b"L")),
value(FixedOffset::east_opt(12 * HOUR), tag_no_case(b"M")),
)),
alt((
value(FixedOffset::west_opt(HOUR), tag_no_case(b"N")),
value(FixedOffset::west_opt(2 * HOUR), tag_no_case(b"O")),
value(FixedOffset::west_opt(3 * HOUR), tag_no_case(b"P")),
value(FixedOffset::west_opt(4 * HOUR), tag_no_case(b"Q")),
value(FixedOffset::west_opt(5 * HOUR), tag_no_case(b"R")),
value(FixedOffset::west_opt(6 * HOUR), tag_no_case(b"S")),
value(FixedOffset::west_opt(7 * HOUR), tag_no_case(b"T")),
value(FixedOffset::west_opt(8 * HOUR), tag_no_case(b"U")),
value(FixedOffset::west_opt(9 * HOUR), tag_no_case(b"V")),
value(FixedOffset::west_opt(10 * HOUR), tag_no_case(b"W")),
value(FixedOffset::west_opt(11 * HOUR), tag_no_case(b"X")),
value(FixedOffset::west_opt(12 * HOUR), tag_no_case(b"Y")),
)),
value(FixedOffset::west_opt(0 * HOUR), alphanumeric1),
)),
)(input)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn test_section_rfc_strict() {
assert_eq!(
section(b"Fri, 21 Nov 1997 09:55:06 -0600"),
Ok((
&b""[..],
Some(
FixedOffset::west_opt(6 * HOUR)
.unwrap()
.with_ymd_and_hms(1997, 11, 21, 9, 55, 6)
.unwrap()
)
)),
);
}
#[test]
fn test_section_received() {
assert_eq!(
section(b"Sun, 18 Jun 2023 15:39:08 +0200 (CEST)"),
Ok((
&b""[..],
Some(
FixedOffset::east_opt(2 * HOUR)
.unwrap()
.with_ymd_and_hms(2023, 6, 18, 15, 39, 8)
.unwrap()
)
)),
);
}
#[test]
fn test_section_rfc_ws() {
assert_eq!(
section(
r#"Thu,
13
Feb
1969
23:32
-0330 (Newfoundland Time)"#
.as_bytes()
),
Ok((
&b""[..],
Some(
FixedOffset::west_opt(3 * HOUR + 30 * MIN)
.unwrap()
.with_ymd_and_hms(1969, 2, 13, 23, 32, 00)
.unwrap()
)
)),
);
}
#[test]
fn test_section_rfc_obs() {
assert_eq!(
section(b"21 Nov 97 09:55:06 GMT"),
Ok((
&b""[..],
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(1997, 11, 21, 9, 55, 6)
.unwrap()
)
)),
);
}
#[test]
fn test_section_3digit_year() {
assert_eq!(
section(b"21 Nov 103 09:55:06 UT"),
Ok((
&b""[..],
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2003, 11, 21, 9, 55, 6)
.unwrap()
)
)),
);
}
#[test]
fn test_section_rfc_obs_ws() {
assert_eq!(
section(b"Fri, 21 Nov 1997 09(comment): 55 : 06 -0600"),
Ok((
&b""[..],
Some(
FixedOffset::west_opt(6 * HOUR)
.unwrap()
.with_ymd_and_hms(1997, 11, 21, 9, 55, 6)
.unwrap()
)
)),
);
}
#[test]
fn test_section_2digit_year() {
assert_eq!(
section(b"21 Nov 23 09:55:06Z"),
Ok((
&b""[..],
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 9, 55, 6)
.unwrap()
)
)),
);
}
#[test]
fn test_section_military_zone_east() {
["a", "B", "c", "D", "e", "F", "g", "H", "i", "K", "l", "M"]
.iter()
.enumerate()
.for_each(|(i, x)| {
assert_eq!(
section(format!("1 Jan 22 08:00:00 {}", x).as_bytes()),
Ok((
&b""[..],
Some(
FixedOffset::east_opt((i as i32 + 1) * HOUR)
.unwrap()
.with_ymd_and_hms(2022, 01, 01, 8, 0, 0)
.unwrap()
)
))
);
});
}
#[test]
fn test_section_military_zone_west() {
["N", "O", "P", "q", "r", "s", "T", "U", "V", "w", "x", "y"]
.iter()
.enumerate()
.for_each(|(i, x)| {
assert_eq!(
section(format!("1 Jan 22 08:00:00 {}", x).as_bytes()),
Ok((
&b""[..],
Some(
FixedOffset::west_opt((i as i32 + 1) * HOUR)
.unwrap()
.with_ymd_and_hms(2022, 01, 01, 8, 0, 0)
.unwrap()
)
))
);
});
}
#[test]
fn test_section_gmt() {
assert_eq!(
section(b"21 Nov 2023 07:07:07 +0000"),
Ok((
&b""[..],
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
.unwrap()
)
)),
);
assert_eq!(
section(b"21 Nov 2023 07:07:07 -0000"),
Ok((
&b""[..],
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
.unwrap()
)
)),
);
assert_eq!(
section(b"21 Nov 2023 07:07:07 Z"),
Ok((
&b""[..],
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
.unwrap()
)
)),
);
assert_eq!(
section(b"21 Nov 2023 07:07:07 GMT"),
Ok((
&b""[..],
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
.unwrap()
)
)),
);
assert_eq!(
section(b"21 Nov 2023 07:07:07 UT"),
Ok((
&b""[..],
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
.unwrap()
)
)),
);
assert_eq!(
section(b"21 Nov 2023 07:07:07 UTC"),
Ok((
&b""[..],
Some(
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
.unwrap()
)
)),
);
}
#[test]
fn test_section_usa() {
assert_eq!(
section(b"21 Nov 2023 4:4:4 CST"),
Ok((
&b""[..],
Some(
FixedOffset::west_opt(6 * HOUR)
.unwrap()
.with_ymd_and_hms(2023, 11, 21, 4, 4, 4)
.unwrap()
)
)),
);
}
}