mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
Find a compatibility intersection between chrono and GNU
This commit is contained in:
committed by
yuankunzhang
parent
4c3d2221da
commit
2479c61dd5
@@ -20,9 +20,3 @@ anyhow = "1.0.86"
|
||||
|
||||
[features]
|
||||
debug = ["winnow/debug"]
|
||||
|
||||
[[bin]]
|
||||
path = "bin/main.rs"
|
||||
name = "parse_datetime"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
@@ -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("%+"))
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::io::{self, Write};
|
||||
|
||||
use libfuzzer_sys::arbitrary::{self, Arbitrary};
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user