Find a compatibility intersection between chrono and GNU

This commit is contained in:
Daniel Hilst
2025-04-30 19:53:34 +08:00
committed by yuankunzhang
parent 4c3d2221da
commit 2479c61dd5
8 changed files with 80 additions and 60 deletions
-6
View File
@@ -20,9 +20,3 @@ anyhow = "1.0.86"
[features]
debug = ["winnow/debug"]
[[bin]]
path = "bin/main.rs"
name = "parse_datetime"
test = false
bench = false
-6
View File
@@ -1,6 +0,0 @@
use parse_datetime::parse_datetime;
fn main() {
let date: String = std::env::args().nth(1).unwrap_or("".to_string());
println!("{}", parse_datetime(&date).unwrap().format("%+"))
}
-1
View File
@@ -2,7 +2,6 @@
#![allow(dead_code)]
use std::fmt::{Debug, Display};
use std::io::{self, Write};
use libfuzzer_sys::arbitrary::{self, Arbitrary};
+1 -6
View File
@@ -35,11 +35,7 @@ pub struct DateTime {
}
pub fn parse(input: &mut &str) -> PResult<DateTime> {
alt((
parse_basic,
//parse_8digits
))
.parse_next(input)
alt((parse_basic, parse_8digits)).parse_next(input)
}
fn parse_basic(input: &mut &str) -> PResult<DateTime> {
@@ -52,7 +48,6 @@ fn parse_basic(input: &mut &str) -> PResult<DateTime> {
.parse_next(input)
}
#[allow(dead_code)]
fn parse_8digits(input: &mut &str) -> PResult<DateTime> {
s((
take(2usize).and_then(dec_uint),
+27 -9
View File
@@ -30,6 +30,8 @@ use winnow::{
ascii::{alpha1, dec_uint},
combinator::{alt, opt, preceded},
seq,
stream::AsChar,
token::take_while,
trace::trace,
PResult, Parser,
};
@@ -97,14 +99,26 @@ fn literal2(input: &mut &str) -> PResult<Date> {
}
pub fn year(input: &mut &str) -> PResult<u32> {
// 2147485547 is the maximum value accepted
// by GNU, but chrono only behave like GNU
// for years in the range: [0, 9999], so we
// keep in the range [0, 9999]
trace(
"year",
dec_uint.try_map(|x| {
(0..=2147485547)
.contains(&x)
.then_some(x)
.ok_or(ParseDateTimeError::InvalidInput)
}),
s(
take_while(1..=4, AsChar::is_dec_digit).map(|number_str: &str| {
let year = number_str.parse::<u32>().unwrap();
if number_str.len() == 2 {
if year <= 68 {
year + 2000
} else {
year + 1900
}
} else {
year
}
}),
),
)
.parse_next(input)
}
@@ -233,9 +247,13 @@ mod tests {
use super::year;
// the minimun input length is 2
assert!(year(&mut "0").is_err());
// assert!(year(&mut "0").is_err());
// -> GNU accepts year 0
// test $(date -d '1-1-1' '+%Y') -eq '0001'
// test $(date -d '68-1-1' '+%Y') -eq '2068'
// 2-characters are converted to 19XX/20XX
assert_eq!(year(&mut "00").unwrap(), 2000u32);
assert_eq!(year(&mut "10").unwrap(), 2010u32);
assert_eq!(year(&mut "68").unwrap(), 2068u32);
assert_eq!(year(&mut "69").unwrap(), 1969u32);
assert_eq!(year(&mut "99").unwrap(), 1999u32);
@@ -245,6 +263,6 @@ mod tests {
assert_eq!(year(&mut "1568").unwrap(), 1568u32);
assert_eq!(year(&mut "1569").unwrap(), 1569u32);
// consumes at most 4 characters from the input
assert_eq!(year(&mut "1234567").unwrap(), 1234u32);
//assert_eq!(year(&mut "1234567").unwrap(), 1234u32);
}
}
+15 -4
View File
@@ -53,8 +53,8 @@ mod timezone {
use chrono::NaiveDate;
use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike};
use winnow::error::ParseError;
use winnow::error::ParserError;
use winnow::error::{ContextError, ErrMode, ParseError};
use winnow::trace::trace;
use winnow::{
ascii::multispace0,
@@ -100,6 +100,17 @@ where
separated(0.., multispace0, alt((comment, ignored_hyphen_or_plus))).parse_next(input)
}
/// Check for the end of a token, without consuming the input
/// succeedes if the next character in the input is a space or
/// if the input is empty
pub(crate) fn eotoken(input: &mut &str) -> PResult<()> {
if input.is_empty() || input.chars().next().unwrap().is_space() {
return Ok(());
}
Err(ErrMode::Backtrack(ContextError::new()))
}
/// A hyphen or plus is ignored when it is not followed by a digit
///
/// This includes being followed by a comment! Compare these inputs:
@@ -141,7 +152,7 @@ where
// Parse an item
pub fn parse_one(input: &mut &str) -> PResult<Item> {
//eprintln!("parsing_one -> {input}");
// eprintln!("parsing_one -> {input}");
let result = trace(
"parse_one",
alt((
@@ -152,11 +163,11 @@ pub fn parse_one(input: &mut &str) -> PResult<Item> {
weekday::parse.map(Item::Weekday),
epoch::parse.map(Item::Timestamp),
timezone::parse.map(Item::TimeZone),
s(date::year).map(Item::Year),
date::year.map(Item::Year),
)),
)
.parse_next(input)?;
//eprintln!("parsing_one <- {input} {result:?}");
// eprintln!("parsing_one <- {input} {result:?}");
Ok(result)
}
+7 -3
View File
@@ -42,7 +42,7 @@ use std::fmt::Display;
use chrono::FixedOffset;
use winnow::{
ascii::{dec_uint, float},
combinator::{alt, opt, preceded},
combinator::{alt, opt, preceded, terminated},
error::{AddContext, ContextError, ErrMode, StrContext},
seq,
stream::AsChar,
@@ -50,7 +50,7 @@ use winnow::{
PResult, Parser,
};
use super::s;
use super::{eotoken, s};
#[derive(PartialEq, Debug, Clone, Default)]
pub struct Offset {
@@ -206,7 +206,11 @@ fn second(input: &mut &str) -> PResult<f64> {
}
pub(crate) fn timezone(input: &mut &str) -> PResult<Offset> {
alt((timezone_num, timezone_name_offset)).parse_next(input)
let result =
terminated(alt((timezone_num, timezone_name_offset)), eotoken).parse_next(input)?;
// space_or_eof(input, result)
Ok(result)
}
/// Parse a timezone starting with `+` or `-`
+30 -25
View File
@@ -120,6 +120,8 @@ mod tests {
mod iso_8601 {
use std::env;
use chrono::{TimeZone, Utc};
use crate::ParseDateTimeError;
use crate::{parse_datetime, tests::TEST_TIME};
@@ -127,8 +129,11 @@ mod tests {
fn test_t_sep() {
env::set_var("TZ", "UTC");
let dt = "2021-02-15T06:37:47";
let actual = parse_datetime(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
let actual = parse_datetime(dt).unwrap();
assert_eq!(
actual,
Utc.timestamp_opt(TEST_TIME, 0).unwrap().fixed_offset()
);
}
#[test]
@@ -400,7 +405,6 @@ mod tests {
#[test]
fn test_invalid_input() {
let result = parse_datetime("foobar");
println!("{result:?}");
assert_eq!(result, Err(ParseDateTimeError::InvalidInput));
let result = parse_datetime("invalid 1");
@@ -618,13 +622,9 @@ mod tests {
}
mod test_relative {
use chrono::NaiveDate;
use crate::{items, parse_datetime};
use std::{
env,
io::{self, Write},
};
use crate::parse_datetime;
use std::env;
#[test]
fn test_month() {
@@ -677,6 +677,11 @@ mod tests {
"2024-03-29T00:00:00+00:00",
);
}
}
mod test_gnu {
use crate::parse_datetime;
fn make_gnu_date(input: &str, fmt: &str) -> String {
std::process::Command::new("date")
.arg("-d")
@@ -684,30 +689,30 @@ mod tests {
.arg(format!("+{fmt}"))
.output()
.map(|mut output| {
io::stdout().write_all(&output.stdout).unwrap();
//io::stdout().write_all(&output.stdout).unwrap();
output.stdout.pop(); // remove trailing \n
String::from_utf8(output.stdout).expect("from_utf8")
})
.unwrap()
}
#[test]
fn chrono_date() {
const FMT: &str = "%Y-%m-%d %H:%M:%S";
let year = 262144;
let input = format!("{year}-01-01 00:00:00");
assert!(NaiveDate::from_ymd_opt(year, 1, 1).is_none());
assert!(chrono::DateTime::parse_from_str(&input, FMT).is_err());
// the parsing works, but hydration fails
assert!(items::parse(&mut input.to_string().as_str()).is_ok());
assert!(parse_datetime(&input).is_err());
// GNU date works
assert_eq!(make_gnu_date(&input, FMT), input);
fn has_gnu_date() -> bool {
std::process::Command::new("date")
.arg("--version")
.output()
.map(|output| String::from_utf8(output.stdout).unwrap())
.map(|output| output.starts_with("date (GNU coreutils)"))
.unwrap_or(false)
}
#[test]
fn gnu_compat() {
// skip if GNU date is not present
if !has_gnu_date() {
eprintln!("GNU date not found, skipping gnu_compat tests");
return;
}
const FMT: &str = "%Y-%m-%d %H:%M:%S";
let input = "0000-03-02 00:00:00";
assert_eq!(
@@ -715,7 +720,7 @@ mod tests {
parse_datetime(input).unwrap().format(FMT).to_string()
);
let input = "262144-03-10 00:00:00";
let input = "2621-03-10 00:00:00";
assert_eq!(
make_gnu_date(input, FMT),
parse_datetime(input)
@@ -724,7 +729,7 @@ mod tests {
.to_string()
);
let input = "10384-03-10 00:00:00";
let input = "1038-03-10 00:00:00";
assert_eq!(
make_gnu_date(input, FMT),
parse_datetime(input)