tested datetime
This commit is contained in:
parent
dd6f127fa2
commit
a88c4d2dcb
2 changed files with 259 additions and 112 deletions
257
src/datetime.rs
257
src/datetime.rs
|
@ -6,7 +6,7 @@ use nom::{
|
||||||
bytes::complete::{tag, tag_no_case, take_while, take_while1, take_while_m_n, is_a},
|
bytes::complete::{tag, tag_no_case, take_while, take_while1, take_while_m_n, is_a},
|
||||||
character,
|
character,
|
||||||
character::is_digit,
|
character::is_digit,
|
||||||
character::complete::{one_of, alphanumeric1},
|
character::complete::{one_of, alphanumeric1, digit0},
|
||||||
combinator::{map, opt, value},
|
combinator::{map, opt, value},
|
||||||
sequence::{preceded, terminated, tuple, delimited },
|
sequence::{preceded, terminated, tuple, delimited },
|
||||||
};
|
};
|
||||||
|
@ -32,13 +32,15 @@ const HOUR: i32 = 60 * MIN;
|
||||||
/// due to an error in RFC0822 but are interpreted as their respective
|
/// due to an error in RFC0822 but are interpreted as their respective
|
||||||
/// timezone according to the RFC5322 definition
|
/// timezone according to the RFC5322 definition
|
||||||
pub fn section(input: &str) -> IResult<&str, Option<DateTime<FixedOffset>>> {
|
pub fn section(input: &str) -> IResult<&str, Option<DateTime<FixedOffset>>> {
|
||||||
map(tuple((
|
map(terminated(
|
||||||
opt(terminated(day_of_week, tag(","))),
|
alt((
|
||||||
date, time_of_day, zone,
|
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)
|
opt(cfws)
|
||||||
)), |res| {
|
), |res| {
|
||||||
match res {
|
match res {
|
||||||
(_, Some(date), Some(time), Some(tz), _) => {
|
(_, Some(date), Some(time), Some(tz)) => {
|
||||||
date.and_time(time).and_local_timezone(tz).earliest()
|
date.and_time(time).and_local_timezone(tz).earliest()
|
||||||
},
|
},
|
||||||
_ => None,
|
_ => None,
|
||||||
|
@ -47,21 +49,17 @@ pub fn section(input: &str) -> IResult<&str, Option<DateTime<FixedOffset>>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// day-of-week = ([FWS] day-name) / obs-day-of-week
|
/// day-of-week = ([FWS] day-name) / obs-day-of-week
|
||||||
fn day_of_week(input: &str) -> IResult<&str, &str> {
|
fn strict_day_of_week(input: &str) -> IResult<&str, &str> {
|
||||||
alt((day_of_week_strict, obs_day_of_week))(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn day_of_week_strict(input: &str) -> IResult<&str, &str> {
|
|
||||||
preceded(opt(fws), day_name)(input)
|
preceded(opt(fws), day_name)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// obs-day-of-week = [CFWS] day-name [CFWS]
|
||||||
fn obs_day_of_week(input: &str) -> IResult<&str, &str> {
|
fn obs_day_of_week(input: &str) -> IResult<&str, &str> {
|
||||||
delimited(opt(cfws), day_name, opt(cfws))(input)
|
delimited(opt(cfws), day_name, opt(cfws))(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// day-name = "Mon" / "Tue" / "Wed" / "Thu" /
|
/// day-name = "Mon" / "Tue" / "Wed" / "Thu" /
|
||||||
/// "Fri" / "Sat" / "Sun"
|
/// "Fri" / "Sat" / "Sun"
|
||||||
|
|
||||||
fn day_name(input: &str) -> IResult<&str, &str> {
|
fn day_name(input: &str) -> IResult<&str, &str> {
|
||||||
alt((
|
alt((
|
||||||
tag_no_case("Mon"),
|
tag_no_case("Mon"),
|
||||||
|
@ -75,23 +73,27 @@ fn day_name(input: &str) -> IResult<&str, &str> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// date = day month year
|
/// date = day month year
|
||||||
fn date(input: &str) -> IResult<&str, Option<NaiveDate>> {
|
fn strict_date(input: &str) -> IResult<&str, Option<NaiveDate>> {
|
||||||
map(
|
map(
|
||||||
tuple((day, month, year)),
|
tuple((strict_day, month, strict_year)),
|
||||||
|
|(d, m, y)| NaiveDate::from_ymd_opt(y, m, d)
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// date = day month year
|
||||||
|
fn obs_date(input: &str) -> IResult<&str, Option<NaiveDate>> {
|
||||||
|
map(
|
||||||
|
tuple((obs_day, month, obs_year)),
|
||||||
|(d, m, y)| NaiveDate::from_ymd_opt(y, m, d)
|
|(d, m, y)| NaiveDate::from_ymd_opt(y, m, d)
|
||||||
)(input)
|
)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// day = ([FWS] 1*2DIGIT FWS) / obs-day
|
/// day = ([FWS] 1*2DIGIT FWS) / obs-day
|
||||||
/// obs-day = [CFWS] 1*2DIGIT [CFWS]
|
fn strict_day(input: &str) -> IResult<&str, u32> {
|
||||||
fn day(input: &str) -> IResult<&str, u32> {
|
|
||||||
alt((day_strict, obs_day))(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn day_strict(input: &str) -> IResult<&str, u32> {
|
|
||||||
delimited(opt(fws), character::complete::u32, fws)(input)
|
delimited(opt(fws), character::complete::u32, fws)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// obs-day = [CFWS] 1*2DIGIT [CFWS]
|
||||||
fn obs_day(input: &str) -> IResult<&str, u32> {
|
fn obs_day(input: &str) -> IResult<&str, u32> {
|
||||||
delimited(opt(cfws), character::complete::u32, opt(cfws))(input)
|
delimited(opt(cfws), character::complete::u32, opt(cfws))(input)
|
||||||
}
|
}
|
||||||
|
@ -117,61 +119,63 @@ fn month(input: &str) -> IResult<&str, u32> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// year = (FWS 4*DIGIT FWS) / obs-year
|
/// year = (FWS 4*DIGIT FWS) / obs-year
|
||||||
/// obs-year = [CFWS] 2*DIGIT [CFWS]
|
|
||||||
fn year(input: &str) -> IResult<&str, i32> {
|
|
||||||
alt((strict_year, obs_year))(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strict_year(input: &str) -> IResult<&str, i32> {
|
fn strict_year(input: &str) -> IResult<&str, i32> {
|
||||||
delimited(fws, character::complete::i32, fws)(input)
|
delimited(
|
||||||
|
fws,
|
||||||
|
map(
|
||||||
|
terminated(take_while_m_n(4,9,|c| c >= '\x30' && c <= '\x39'), digit0),
|
||||||
|
|d: &str| d.parse::<i32>().unwrap()),
|
||||||
|
fws,
|
||||||
|
)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// obs-year = [CFWS] 2*DIGIT [CFWS]
|
||||||
fn obs_year(input: &str) -> IResult<&str, i32> {
|
fn obs_year(input: &str) -> IResult<&str, i32> {
|
||||||
map(delimited(opt(cfws), character::complete::i32, opt(cfws)),
|
map(delimited(
|
||||||
|d: i32| if d >= 0 && d <= 49 {
|
opt(cfws),
|
||||||
|
terminated(take_while_m_n(2,7,|c| c >= '\x30' && c <= '\x39'), digit0),
|
||||||
|
opt(cfws)
|
||||||
|
), |cap: &str| {
|
||||||
|
let d = cap.parse::<i32>().unwrap();
|
||||||
|
if d >= 0 && d <= 49 {
|
||||||
2000 + d
|
2000 + d
|
||||||
} else if d >= 50 && d <= 999 {
|
} else if d >= 50 && d <= 999 {
|
||||||
1900 + d
|
1900 + d
|
||||||
} else {
|
} else {
|
||||||
d
|
d
|
||||||
|
}
|
||||||
})(input)
|
})(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// time-of-day = hour ":" minute [ ":" second ]
|
/// time-of-day = hour ":" minute [ ":" second ]
|
||||||
///
|
fn strict_time_of_day(input: &str) -> IResult<&str, Option<NaiveTime>> {
|
||||||
fn time_of_day(input: &str) -> IResult<&str, Option<NaiveTime>> {
|
|
||||||
map(
|
map(
|
||||||
tuple((character::complete::u32, tag(":"), character::complete::u32, opt(preceded(tag(":"), character::complete::u32)))),
|
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)),
|
|(hour, _, minute, maybe_sec)| NaiveTime::from_hms_opt(hour, minute, maybe_sec.unwrap_or(0)),
|
||||||
)(input)
|
)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// time-of-day = hour ":" minute [ ":" second ]
|
||||||
|
fn obs_time_of_day(input: &str) -> IResult<&str, 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: &str) -> IResult<&str, u32> {
|
||||||
|
character::complete::u32(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn obs_time_digit(input: &str) -> IResult<&str, u32> {
|
||||||
|
delimited(opt(cfws), character::complete::u32, opt(cfws))(input)
|
||||||
|
}
|
||||||
|
|
||||||
/// Obsolete zones
|
/// Obsolete zones
|
||||||
///
|
///
|
||||||
/// ```abnf
|
/// ```abnf
|
||||||
/// zone = (FWS ( "+" / "-" ) 4DIGIT) / obs-zone
|
/// zone = (FWS ( "+" / "-" ) 4DIGIT) / (FWS obs-zone)
|
||||||
/// obs-zone = "UT" / "GMT" / ; Universal Time
|
|
||||||
/// ; North American UT
|
|
||||||
/// ; offsets
|
|
||||||
/// "EST" / "EDT" / ; Eastern: - 5/ - 4
|
|
||||||
/// "CST" / "CDT" / ; Central: - 6/ - 5
|
|
||||||
/// "MST" / "MDT" / ; Mountain: - 7/ - 6
|
|
||||||
/// "PST" / "PDT" / ; Pacific: - 8/ - 7
|
|
||||||
/// ;
|
|
||||||
/// %d65-73 / ; Military zones - "A"
|
|
||||||
/// %d75-90 / ; through "I" and "K"
|
|
||||||
/// %d97-105 / ; through "Z", both
|
|
||||||
/// %d107-122 / ; upper and lower case
|
|
||||||
/// ;
|
|
||||||
/// 1*(ALPHA / DIGIT) ; Unknown legacy timezones
|
|
||||||
/// ```
|
/// ```
|
||||||
///
|
|
||||||
///
|
|
||||||
|
|
||||||
fn zone(input: &str) -> IResult<&str, Option<FixedOffset>> {
|
|
||||||
alt((strict_zone, obs_zone))(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strict_zone(input: &str) -> IResult<&str, Option<FixedOffset>> {
|
fn strict_zone(input: &str) -> IResult<&str, Option<FixedOffset>> {
|
||||||
map(
|
map(
|
||||||
tuple((opt(fws), is_a("+-"), take_while_m_n(2,2,|c| c >= '\x30' && c <= '\x39'), take_while_m_n(2,2,|c| c >= '\x30' && c <= '\x39'))),
|
tuple((opt(fws), is_a("+-"), take_while_m_n(2,2,|c| c >= '\x30' && c <= '\x39'), take_while_m_n(2,2,|c| c >= '\x30' && c <= '\x39'))),
|
||||||
|
@ -186,15 +190,33 @@ fn strict_zone(input: &str) -> IResult<&str, Option<FixedOffset>> {
|
||||||
)(input)
|
)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// obsole zone
|
||||||
|
///
|
||||||
|
/// obs-zone = "UT" / "GMT" / ; Universal Time
|
||||||
|
/// ; North American UT
|
||||||
|
/// ; offsets
|
||||||
|
/// "EST" / "EDT" / ; Eastern: - 5/ - 4
|
||||||
|
/// "CST" / "CDT" / ; Central: - 6/ - 5
|
||||||
|
/// "MST" / "MDT" / ; Mountain: - 7/ - 6
|
||||||
|
/// "PST" / "PDT" / ; Pacific: - 8/ - 7
|
||||||
|
/// ;
|
||||||
|
/// %d65-73 / ; Military zones - "A"
|
||||||
|
/// %d75-90 / ; through "I" and "K"
|
||||||
|
/// %d97-105 / ; through "Z", both
|
||||||
|
/// %d107-122 / ; upper and lower case
|
||||||
|
/// ;
|
||||||
|
/// 1*(ALPHA / DIGIT) ; Unknown legacy timezones
|
||||||
fn obs_zone(input: &str) -> IResult<&str, Option<FixedOffset>> {
|
fn obs_zone(input: &str) -> IResult<&str, Option<FixedOffset>> {
|
||||||
// The writing of this function is volontarily verbose
|
// The writing of this function is volontarily verbose
|
||||||
// to keep it straightforward to understand.
|
// to keep it straightforward to understand.
|
||||||
// @FIXME: Could return a TimeZone and not an Option<TimeZone>
|
// @FIXME: Could return a TimeZone and not an Option<TimeZone>
|
||||||
// as it could be determined at compile time if values are correct
|
// as it could be determined at compile time if values are correct
|
||||||
// and panic at this time if not. But not sure how to do it without unwrap.
|
// and panic at this time if not. But not sure how to do it without unwrap.
|
||||||
|
preceded(
|
||||||
|
opt(fws),
|
||||||
alt((
|
alt((
|
||||||
// Legacy UTC/GMT
|
// Legacy UTC/GMT
|
||||||
value(FixedOffset::west_opt(0 * HOUR), alt((tag("UT"), tag("GMT")))),
|
value(FixedOffset::west_opt(0 * HOUR), alt((tag("UTC"), tag("UT"), tag("GMT")))),
|
||||||
|
|
||||||
// USA Timezones
|
// USA Timezones
|
||||||
value(FixedOffset::west_opt(4 * HOUR), tag("EDT")),
|
value(FixedOffset::west_opt(4 * HOUR), tag("EDT")),
|
||||||
|
@ -242,5 +264,130 @@ fn obs_zone(input: &str) -> IResult<&str, Option<FixedOffset>> {
|
||||||
|
|
||||||
// Unknown timezone
|
// Unknown timezone
|
||||||
value(FixedOffset::west_opt(0 * HOUR), alphanumeric1),
|
value(FixedOffset::west_opt(0 * HOUR), alphanumeric1),
|
||||||
))(input)
|
)),
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_section_rfc_strict() {
|
||||||
|
assert_eq!(
|
||||||
|
section("Fri, 21 Nov 1997 09:55:06 -0600"),
|
||||||
|
Ok(("", 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("Sun, 18 Jun 2023 15:39:08 +0200 (CEST)"),
|
||||||
|
Ok(("", 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)"#),
|
||||||
|
Ok(("", 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("21 Nov 97 09:55:06 GMT"),
|
||||||
|
Ok(("", 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("21 Nov 103 09:55:06 UT"),
|
||||||
|
Ok(("", 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("Fri, 21 Nov 1997 09(comment): 55 : 06 -0600"),
|
||||||
|
Ok(("", 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("21 Nov 23 09:55:06Z"),
|
||||||
|
Ok(("", 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_str()),
|
||||||
|
Ok(("", 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_str()),
|
||||||
|
Ok(("", 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("21 Nov 2023 07:07:07 +0000"),
|
||||||
|
Ok(("", Some(FixedOffset::east_opt(0).unwrap().with_ymd_and_hms(2023, 11, 21, 7, 7, 7).unwrap()))),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
section("21 Nov 2023 07:07:07 -0000"),
|
||||||
|
Ok(("", Some(FixedOffset::east_opt(0).unwrap().with_ymd_and_hms(2023, 11, 21, 7, 7, 7).unwrap()))),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
section("21 Nov 2023 07:07:07 Z"),
|
||||||
|
Ok(("", Some(FixedOffset::east_opt(0).unwrap().with_ymd_and_hms(2023, 11, 21, 7, 7, 7).unwrap()))),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
section("21 Nov 2023 07:07:07 GMT"),
|
||||||
|
Ok(("", Some(FixedOffset::east_opt(0).unwrap().with_ymd_and_hms(2023, 11, 21, 7, 7, 7).unwrap()))),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
section("21 Nov 2023 07:07:07 UT"),
|
||||||
|
Ok(("", Some(FixedOffset::east_opt(0).unwrap().with_ymd_and_hms(2023, 11, 21, 7, 7, 7).unwrap()))),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
section("21 Nov 2023 07:07:07 UTC"),
|
||||||
|
Ok(("", 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("21 Nov 2023 4:4:4 CST"),
|
||||||
|
Ok(("", Some(FixedOffset::west_opt(6 * HOUR).unwrap().with_ymd_and_hms(2023, 11, 21, 4, 4, 4).unwrap()))),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//use imf_codec::header;
|
use imf_codec::header;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let hdr = r#"Return-Path: <gitlab@framasoft.org>
|
let hdr = r#"Return-Path: <gitlab@framasoft.org>
|
||||||
|
@ -33,5 +33,5 @@ References: <1234@local.machine.example>
|
||||||
This is a reply to your hello.
|
This is a reply to your hello.
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
//println!("{:?}", header::section(hdr));
|
println!("{:?}", header::section(hdr));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue