Restore the old API and add the missing combinators

This commit is contained in:
Daniel Hilst
2024-08-29 14:21:15 -03:00
committed by yuankunzhang
parent 07d4b80ea5
commit 4c3d2221da
14 changed files with 1356 additions and 158 deletions
Generated
+257 -29
View File
@@ -26,12 +26,73 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is-terminal",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c"
dependencies = [
"anstyle",
"windows-sys 0.48.0",
]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bumpalo"
version = "3.13.0"
@@ -62,12 +123,40 @@ dependencies = [
"windows-link",
]
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "errno"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hermit-abi"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
@@ -91,6 +180,28 @@ dependencies = [
"cc",
]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi 0.3.9",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "is-terminal"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.0",
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "js-sys"
version = "0.3.70"
@@ -106,6 +217,12 @@ version = "0.2.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]]
name = "log"
version = "0.4.18"
@@ -129,9 +246,9 @@ dependencies = [
[[package]]
name = "num-traits"
version = "0.2.15"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
@@ -146,8 +263,10 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
name = "parse_datetime"
version = "0.9.0"
dependencies = [
"anyhow",
"chrono",
"nom",
"num-traits",
"regex",
"winnow",
]
@@ -199,6 +318,20 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustix"
version = "0.37.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.48.0",
]
[[package]]
name = "syn"
version = "2.0.18"
@@ -210,12 +343,28 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "terminal_size"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237"
dependencies = [
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "unicode-ident"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
@@ -277,7 +426,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@@ -287,68 +436,143 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-targets"
version = "0.52.5"
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
@@ -356,5 +580,9 @@ version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"anstream",
"anstyle",
"is-terminal",
"memchr",
"terminal_size",
]
+14
View File
@@ -12,3 +12,17 @@ regex = "1.10.4"
chrono = { version="0.4.38", default-features=false, features=["std", "alloc", "clock"] }
nom = "8.0.0"
winnow = "0.5.34"
num-traits = "0.2.19"
[dev-dependencies]
anyhow = "1.0.86"
#winnow = { version="0.5.34", features = ["debug"] }
[features]
debug = ["winnow/debug"]
[[bin]]
path = "bin/main.rs"
name = "parse_datetime"
test = false
bench = false
+6
View File
@@ -0,0 +1,6 @@
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("%+"))
}
+3
View File
@@ -14,6 +14,9 @@ chrono = { version="0.4", default-features=false, features=["std", "alloc", "clo
[dependencies.parse_datetime]
path = "../"
[features]
debug = ["parse_datetime/debug"]
[[bin]]
name = "fuzz_parse_datetime"
path = "fuzz_targets/parse_datetime.rs"
+130 -4
View File
@@ -1,8 +1,134 @@
#![no_main]
#![allow(dead_code)]
use libfuzzer_sys::fuzz_target;
use std::fmt::{Debug, Display};
use std::io::{self, Write};
fuzz_target!(|data: &[u8]| {
let s = std::str::from_utf8(data).unwrap_or("");
let _ = parse_datetime::parse_datetime(s);
use libfuzzer_sys::arbitrary::{self, Arbitrary};
#[macro_use]
extern crate libfuzzer_sys;
#[derive(Debug)]
struct Format(&'static str);
// These are formats to test the compatibility with GNU
const FORMATS: &[&str] = &["%G-%m-%d %H:%M:%S", "%b %d %Y %H:%M:%S"];
impl<'a> Arbitrary<'a> for Format {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
Ok(Format(u.choose(FORMATS)?))
}
}
struct Input {
year: u32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
format: Format,
}
impl<'a> Arbitrary<'a> for Input {
fn arbitrary(
u: &mut libfuzzer_sys::arbitrary::Unstructured<'a>,
) -> libfuzzer_sys::arbitrary::Result<Self> {
let year = u.arbitrary::<u32>()?;
let month = u.arbitrary::<u32>()?;
let day = u.arbitrary::<u32>()?;
let hour = u.arbitrary::<u32>()?;
let minute = u.arbitrary::<u32>()?;
let second = u.arbitrary::<u32>()?;
let format = u.arbitrary::<Format>()?;
// GNU max 2147485547
// chrono max 262143
// chrono outputs + before the year if it is >9999
if !(1..=9999).contains(&year)
|| !(1..=12).contains(&month)
|| !(1..=31).contains(&day)
|| !(0..24).contains(&hour)
|| !(0..60).contains(&minute)
|| !(0..60).contains(&second)
{
return Err(crate::arbitrary::Error::IncorrectFormat);
}
Ok(Input {
year,
month,
day,
hour,
minute,
second,
format,
})
}
}
impl Input {
fn format(&self) -> String {
let Input {
year,
month,
day,
hour,
minute,
second,
format,
} = self;
let as_string = format!("{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}");
std::process::Command::new("date")
.arg("-d")
.arg(as_string)
.arg(format!("+{}", &format.0))
.output()
.map(|mut output| {
output.stdout.pop(); // remove trailing \n
String::from_utf8(output.stdout).expect("from_utf8")
})
.expect("gnu date")
}
}
impl Display for Input {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.format())
}
}
impl Debug for Input {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.format())
}
}
fuzz_target!(|input: Input| {
let fmt = input.format.0;
let gnu = std::process::Command::new("date")
.arg("-d")
.arg(input.format())
.arg(format!("+{fmt}"))
.output()
.map(|mut output| {
output.stdout.pop(); // remove trailing \n
String::from_utf8(output.stdout).expect("from_utf8")
});
let us = parse_datetime::parse_datetime(&input.format()).map(|d| d.format(fmt).to_string());
match (us, gnu) {
(Ok(us), Ok(gnu)) => assert_eq!(
us, gnu,
"\n\nGNU Incompatibility found for the input: {input}\nExpected: {gnu}\nFound: {us}\n\n"
),
(Err(_), Err(_)) => (),
(Ok(us), Err(e)) => {
panic!("Expecting to fail, but succeeded for input `{input}`, gnu error: {e}, parsed date: {us}")
}
(Err(_), Ok(gnu)) => {
panic!("Expecting to succeed, but failed for input `{input}`, gnu output: {gnu}")
}
};
});
+44 -6
View File
@@ -13,9 +13,13 @@
//! > seconds are allowed, with either comma or period preceding the fraction.
//! > ISO 8601 fractional minutes and hours are not supported. Typically, hosts
//! > support nanosecond timestamp resolution; excess precision is silently discarded.
#![allow(deprecated)]
use winnow::{combinator::alt, seq, PResult, Parser};
use winnow::ascii::dec_uint;
use winnow::token::take;
use winnow::{combinator::alt, seq, trace::trace, PResult, Parser};
use crate::items::combined;
use crate::items::space;
use super::{
@@ -24,22 +28,56 @@ use super::{
time::{self, Time},
};
#[derive(PartialEq, Debug, Clone)]
#[derive(PartialEq, Debug, Clone, Default)]
pub struct DateTime {
date: Date,
time: Time,
pub(crate) date: Date,
pub(crate) time: Time,
}
pub fn parse(input: &mut &str) -> PResult<DateTime> {
alt((
parse_basic,
//parse_8digits
))
.parse_next(input)
}
fn parse_basic(input: &mut &str) -> PResult<DateTime> {
seq!(DateTime {
date: date::iso,
date: trace("date iso", date::iso),
// Note: the `T` is lowercased by the main parse function
_: alt((s('t').void(), (' ', space).void())),
time: time::iso,
time: trace("time iso", time::iso),
})
.parse_next(input)
}
#[allow(dead_code)]
fn parse_8digits(input: &mut &str) -> PResult<DateTime> {
s((
take(2usize).and_then(dec_uint),
take(2usize).and_then(dec_uint),
take(2usize).and_then(dec_uint),
take(2usize).and_then(dec_uint),
))
.map(
|(hour, minute, day, month): (u32, u32, u32, u32)| combined::DateTime {
date: date::Date {
day,
month,
year: None,
},
time: time::Time {
hour,
minute,
second: 0.0,
offset: None,
},
},
)
.parse_next(input)
}
#[cfg(test)]
mod tests {
use super::{parse, DateTime};
+36 -18
View File
@@ -30,14 +30,14 @@ use winnow::{
ascii::{alpha1, dec_uint},
combinator::{alt, opt, preceded},
seq,
token::take,
trace::trace,
PResult, Parser,
};
use super::s;
use crate::ParseDateTimeError;
#[derive(PartialEq, Eq, Clone, Debug)]
#[derive(PartialEq, Eq, Clone, Debug, Default)]
pub struct Date {
pub day: u32,
pub month: u32,
@@ -96,27 +96,24 @@ fn literal2(input: &mut &str) -> PResult<Date> {
.parse_next(input)
}
fn year(input: &mut &str) -> PResult<u32> {
s(alt((
take(4usize).try_map(|x: &str| x.parse()),
take(3usize).try_map(|x: &str| x.parse()),
take(2usize).try_map(|x: &str| x.parse()).map(
|x: u32| {
if x <= 68 {
x + 2000
} else {
x + 1900
}
},
),
)))
pub fn year(input: &mut &str) -> PResult<u32> {
trace(
"year",
dec_uint.try_map(|x| {
(0..=2147485547)
.contains(&x)
.then_some(x)
.ok_or(ParseDateTimeError::InvalidInput)
}),
)
.parse_next(input)
}
fn month(input: &mut &str) -> PResult<u32> {
s(dec_uint)
.try_map(|x| {
(x >= 1 && x <= 12)
(1..=12)
.contains(&x)
.then_some(x)
.ok_or(ParseDateTimeError::InvalidInput)
})
@@ -126,7 +123,8 @@ fn month(input: &mut &str) -> PResult<u32> {
fn day(input: &mut &str) -> PResult<u32> {
s(dec_uint)
.try_map(|x| {
(x >= 1 && x <= 31)
(1..=31)
.contains(&x)
.then_some(x)
.ok_or(ParseDateTimeError::InvalidInput)
})
@@ -229,4 +227,24 @@ mod tests {
assert_eq!(parse(&mut s).unwrap(), reference);
}
}
#[test]
fn test_year() {
use super::year;
// the minimun input length is 2
assert!(year(&mut "0").is_err());
// 2-characters are converted to 19XX/20XX
assert_eq!(year(&mut "00").unwrap(), 2000u32);
assert_eq!(year(&mut "68").unwrap(), 2068u32);
assert_eq!(year(&mut "69").unwrap(), 1969u32);
assert_eq!(year(&mut "99").unwrap(), 1999u32);
// 3,4-characters are converted verbatim
assert_eq!(year(&mut "468").unwrap(), 468u32);
assert_eq!(year(&mut "469").unwrap(), 469u32);
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);
}
}
+276 -19
View File
@@ -26,32 +26,56 @@
//! - [`relative`]
//! - [`number]
#![allow(deprecated)]
mod combined;
mod date;
mod ordinal;
mod relative;
mod time;
mod time_zone;
mod weekday;
mod number {}
mod epoch {
use winnow::{ascii::dec_int, combinator::preceded, PResult, Parser};
use super::s;
pub fn parse(input: &mut &str) -> PResult<i32> {
s(preceded("@", dec_int)).parse_next(input)
}
}
mod timezone {
use super::time;
use winnow::PResult;
pub(crate) fn parse(input: &mut &str) -> PResult<time::Offset> {
time::timezone(input)
}
}
use chrono::NaiveDate;
use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike};
use winnow::error::ParseError;
use winnow::error::ParserError;
use winnow::trace::trace;
use winnow::{
ascii::multispace0,
combinator::{alt, delimited, not, peek, preceded, repeat, separated, terminated},
error::ParserError,
stream::AsChar,
token::{none_of, take_while},
PResult, Parser,
};
use crate::ParseDateTimeError;
#[derive(PartialEq, Debug)]
pub enum Item {
Timestamp(i32),
Year(u32),
DateTime(combined::DateTime),
Date(date::Date),
Time(time::Time),
Weekday(weekday::Weekday),
Relative(relative::Relative),
TimeZone(()),
TimeZone(time::Offset),
}
/// Allow spaces and comments before a parser
@@ -115,32 +139,228 @@ where
.parse_next(input)
}
/// Parse an item
// Parse an item
pub fn parse_one(input: &mut &str) -> PResult<Item> {
alt((
combined::parse.map(Item::DateTime),
date::parse.map(Item::Date),
time::parse.map(Item::Time),
relative::parse.map(Item::Relative),
weekday::parse.map(Item::Weekday),
// time_zone::parse.map(Item::TimeZone),
))
.parse_next(input)
//eprintln!("parsing_one -> {input}");
let result = trace(
"parse_one",
alt((
combined::parse.map(Item::DateTime),
date::parse.map(Item::Date),
time::parse.map(Item::Time),
relative::parse.map(Item::Relative),
weekday::parse.map(Item::Weekday),
epoch::parse.map(Item::Timestamp),
timezone::parse.map(Item::TimeZone),
s(date::year).map(Item::Year),
)),
)
.parse_next(input)?;
//eprintln!("parsing_one <- {input} {result:?}");
Ok(result)
}
pub fn parse(input: &mut &str) -> Option<Vec<Item>> {
terminated(repeat(0.., parse_one), space).parse(input).ok()
pub fn parse<'a>(
input: &'a mut &str,
) -> Result<Vec<Item>, ParseError<&'a str, winnow::error::ContextError>> {
terminated(repeat(0.., parse_one), space).parse(input)
}
fn new_date(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
offset: FixedOffset,
) -> Option<DateTime<FixedOffset>> {
let newdate = NaiveDate::from_ymd_opt(year, month, day)
.and_then(|naive| naive.and_hms_opt(hour, minute, second))?;
Some(DateTime::<FixedOffset>::from_local(newdate, offset))
}
/// Restores year, month, day, etc after applying the timezone
/// returns None if timezone overflows the date
fn with_timezone_restore(
offset: time::Offset,
at: DateTime<FixedOffset>,
) -> Option<DateTime<FixedOffset>> {
let offset: FixedOffset = chrono::FixedOffset::from(offset);
let copy = at;
let x = at
.with_timezone(&offset)
.with_day(copy.day())?
.with_month(copy.month())?
.with_year(copy.year())?
.with_hour(copy.hour())?
.with_minute(copy.minute())?
.with_second(copy.second())?;
Some(x)
}
fn last_day_of_month(year: i32, month: u32) -> u32 {
NaiveDate::from_ymd_opt(year, month + 1, 1)
.unwrap_or(NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap())
.pred_opt()
.unwrap()
.day()
}
fn at_date_inner(date: Vec<Item>, mut d: DateTime<FixedOffset>) -> Option<DateTime<FixedOffset>> {
d = d.with_hour(0).unwrap();
d = d.with_minute(0).unwrap();
d = d.with_second(0).unwrap();
d = d.with_nanosecond(0).unwrap();
for item in date {
match item {
Item::Timestamp(ts) => {
d = chrono::Utc
.timestamp_opt(ts.into(), 0)
.unwrap()
.with_timezone(&d.timezone())
}
Item::Date(date::Date { day, month, year }) => {
d = new_date(
year.map(|x| x as i32).unwrap_or(d.year()),
month,
day,
d.hour(),
d.minute(),
d.second(),
*d.offset(),
)?;
}
Item::DateTime(combined::DateTime {
date: date::Date { day, month, year },
time:
time::Time {
hour,
minute,
second,
offset,
},
..
}) => {
let offset = offset.map(chrono::FixedOffset::from).unwrap_or(*d.offset());
d = new_date(
year.map(|x| x as i32).unwrap_or(d.year()),
month,
day,
hour,
minute,
second as u32,
offset,
)?;
}
Item::Year(year) => d = d.with_year(year as i32).unwrap_or(d),
Item::Time(time::Time {
hour,
minute,
second,
offset,
}) => {
let offset = offset.map(chrono::FixedOffset::from).unwrap_or(*d.offset());
d = new_date(
d.year(),
d.month(),
d.day(),
hour,
minute,
second as u32,
offset,
)?;
}
Item::Weekday(weekday::Weekday {
offset: _, // TODO: use the offset
day,
}) => {
let mut beginning_of_day = d
.with_hour(0)
.unwrap()
.with_minute(0)
.unwrap()
.with_second(0)
.unwrap()
.with_nanosecond(0)
.unwrap();
let day = day.into();
while beginning_of_day.weekday() != day {
beginning_of_day += chrono::Duration::days(1);
}
d = beginning_of_day
}
Item::Relative(relative::Relative::Years(x)) => {
d = d.with_year(d.year() + x)?;
}
Item::Relative(relative::Relative::Months(x)) => {
// *NOTE* This is done in this way to conform to
// GNU behavior.
let days = last_day_of_month(d.year(), d.month());
d += d
.date_naive()
.checked_add_days(chrono::Days::new((days * x as u32) as u64))?
.signed_duration_since(d.date_naive());
}
Item::Relative(relative::Relative::Days(x)) => d += chrono::Duration::days(x.into()),
Item::Relative(relative::Relative::Hours(x)) => d += chrono::Duration::hours(x.into()),
Item::Relative(relative::Relative::Minutes(x)) => {
d += chrono::Duration::minutes(x.into());
}
// Seconds are special because they can be given as a float
Item::Relative(relative::Relative::Seconds(x)) => {
d += chrono::Duration::seconds(x as i64);
}
Item::TimeZone(offset) => {
d = with_timezone_restore(offset, d)?;
}
}
}
Some(d)
}
pub(crate) fn at_date(
date: Vec<Item>,
d: DateTime<FixedOffset>,
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
at_date_inner(date, d).ok_or(ParseDateTimeError::InvalidInput)
}
pub(crate) fn at_local(date: Vec<Item>) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
at_date(date, chrono::Local::now().into())
}
#[cfg(test)]
mod tests {
use super::{date::Date, parse, time::Time, Item};
use super::{at_date, date::Date, parse, time::Time, Item};
use chrono::{DateTime, FixedOffset};
fn at_utc(date: Vec<Item>) -> DateTime<FixedOffset> {
at_date(date, chrono::Utc::now().fixed_offset()).unwrap()
}
fn test_eq_fmt(fmt: &str, input: &str) -> String {
let input = input.to_ascii_lowercase();
parse(&mut input.as_str())
.map(at_utc)
.map_err(|e| eprintln!("TEST FAILED AT:\n{}", anyhow::format_err!("{e}")))
.expect("parsing failed during tests")
.format(fmt)
.to_string()
}
#[test]
fn date_and_time() {
assert_eq!(
parse(&mut " 10:10 2022-12-12 "),
Some(vec![
Ok(vec![
Item::Time(Time {
hour: 10,
minute: 10,
@@ -153,6 +373,43 @@ mod tests {
year: Some(2022)
})
])
)
);
// format, expected output, input
assert_eq!("2024-01-02", test_eq_fmt("%Y-%m-%d", "2024-01-02"));
// https://github.com/uutils/coreutils/issues/6662
assert_eq!("2005-01-02", test_eq_fmt("%Y-%m-%d", "2005-01-01 +1 day"));
// https://github.com/uutils/coreutils/issues/6644
assert_eq!("Jul 16", test_eq_fmt("%b %d", "Jul 16"));
assert_eq!("0718061449", test_eq_fmt("%m%d%H%M%S", "Jul 18 06:14:49"));
assert_eq!(
"07182024061449",
test_eq_fmt("%m%d%Y%H%M%S", "Jul 18, 2024 06:14:49")
);
assert_eq!(
"07182024061449",
test_eq_fmt("%m%d%Y%H%M%S", "Jul 18 06:14:49 2024")
);
// https://github.com/uutils/coreutils/issues/5177
assert_eq!(
"2023-07-27T13:53:54+00:00",
test_eq_fmt("%+", "@1690466034")
);
// https://github.com/uutils/coreutils/issues/6398
assert_eq!("1111 1111 00", test_eq_fmt("%m%d %H%M %S", "11111111"));
assert_eq!(
"2024-07-17 06:14:49 +00:00",
test_eq_fmt("%Y-%m-%d %H:%M:%S %Z", "Jul 17 06:14:49 2024 GMT"),
);
assert_eq!(
"2024-07-17 06:14:49 -03:00",
test_eq_fmt("%Y-%m-%d %H:%M:%S %Z", "Jul 17 06:14:49 2024 BRT"),
);
}
}
-45
View File
@@ -1,45 +0,0 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Numbers without other symbols
//!
//! The GNU docs state:
//!
//! > If the decimal number is of the form yyyymmdd and no other calendar date
//! > item (see Calendar date items) appears before it in the date string, then
//! > yyyy is read as the year, mm as the month number and dd as the day of the
//! > month, for the specified calendar date.
//! >
//! > If the decimal number is of the form hhmm and no other time of day item
//! > appears before it in the date string, then hh is read as the hour of the
//! > day and mm as the minute of the hour, for the specified time of day. mm
//! > can also be omitted.
use winnow::{combinator::cond, PResult};
enum Number {
Date,
Time,
Year,
}
pub fn parse(seen_date: bool, seen_time: bool, input: &mut &str) -> PResult<Number> {
alt((
cond(!seen_date, date_number),
cond(!seen_time, time_number),
cond(seen_date && seen_time, year_number),
))
.parse_next(input)
}
fn date_number(input: &mut &str) -> PResult<Number> {
todo!()
}
fn time_number(input: &mut &str) -> PResult<Number> {
todo!()
}
fn year_number(input: &mut &str) -> PResult<Number> {
todo!()
}
+1 -1
View File
@@ -99,7 +99,7 @@ fn ago(input: &mut &str) -> PResult<bool> {
fn integer_unit(input: &mut &str) -> PResult<Relative> {
s(alpha1)
.verify_map(|s: &str| {
Some(match s.strip_suffix('s').unwrap_or(&s) {
Some(match s.strip_suffix('s').unwrap_or(s) {
"year" => Relative::Years(1),
"month" => Relative::Months(1),
"fortnight" => Relative::Days(14),
+380 -13
View File
@@ -37,9 +37,13 @@
//! >
//! > Either am/pm or a time zone correction may be specified, but not both.
use std::fmt::Display;
use chrono::FixedOffset;
use winnow::{
ascii::{dec_uint, float},
combinator::{alt, opt, preceded},
error::{AddContext, ContextError, ErrMode, StrContext},
seq,
stream::AsChar,
token::take_while,
@@ -48,7 +52,14 @@ use winnow::{
use super::s;
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Debug, Clone, Default)]
pub struct Offset {
pub(crate) negative: bool,
pub(crate) hours: u32,
pub(crate) minutes: u32,
}
#[derive(PartialEq, Clone, Debug, Default)]
pub struct Time {
pub hour: u32,
pub minute: u32,
@@ -56,11 +67,55 @@ pub struct Time {
pub offset: Option<Offset>,
}
#[derive(PartialEq, Debug, Clone)]
pub struct Offset {
negative: bool,
hours: u32,
minutes: u32,
impl Offset {
fn merge(self, offset: Offset) -> Option<Offset> {
let Offset { negative, .. } = offset;
fn combine(a: u32, b: u32, negative: bool) -> Option<u32> {
if negative {
a.checked_sub(b)
} else {
a.checked_add(b)
}
}
let hours = combine(self.hours, offset.hours, negative)?;
let minutes = combine(self.minutes, offset.minutes, negative)?;
Some(Offset {
negative,
hours,
minutes,
})
}
}
impl From<Offset> for chrono::FixedOffset {
fn from(
Offset {
negative,
hours,
minutes,
}: Offset,
) -> Self {
let secs = hours * 3600 + minutes * 60;
if negative {
FixedOffset::west_opt(secs.try_into().expect("secs overflow"))
.expect("timezone overflow")
} else {
FixedOffset::east_opt(secs.try_into().unwrap()).unwrap()
}
}
}
impl Display for Offset {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(
fmt,
"{}{:02}:{:02}",
if self.negative { "-" } else { "+" },
self.hours,
self.minutes
)
}
}
#[derive(Clone)]
@@ -150,15 +205,39 @@ fn second(input: &mut &str) -> PResult<f64> {
s(float).verify(|x| *x < 60.0).parse_next(input)
}
pub(crate) fn timezone(input: &mut &str) -> PResult<Offset> {
alt((timezone_num, timezone_name_offset)).parse_next(input)
}
/// Parse a timezone starting with `+` or `-`
fn timezone(input: &mut &str) -> PResult<Offset> {
fn timezone_num(input: &mut &str) -> PResult<Offset> {
seq!(plus_or_minus, alt((timezone_colon, timezone_colonless)))
.map(|(negative, (hours, minutes))| Offset {
negative,
hours,
minutes,
})
.parse_next(input)
.and_then(|(negative, (hours, minutes))| {
if !(0..=12).contains(&hours) {
return Err(ErrMode::Cut(ContextError::new().add_context(
&input,
StrContext::Expected(winnow::error::StrContextValue::Description(
"timezone hour between 0 and 12",
)),
)));
}
if !(0..=60).contains(&minutes) {
return Err(ErrMode::Cut(ContextError::new().add_context(
&input,
StrContext::Expected(winnow::error::StrContextValue::Description(
"timezone minute between 0 and 60",
)),
)));
}
Ok(Offset {
negative,
hours,
minutes,
})
})
}
/// Parse a timezone offset with a colon separating hours and minutes
@@ -166,7 +245,7 @@ fn timezone_colon(input: &mut &str) -> PResult<(u32, u32)> {
seq!(
s(take_while(1..=2, AsChar::is_dec_digit)).try_map(|x: &str| {
// parse will fail on empty input
if x == "" {
if x.is_empty() {
Ok(0)
} else {
x.parse()
@@ -194,6 +273,262 @@ fn timezone_colonless(input: &mut &str) -> PResult<(u32, u32)> {
.parse_next(input)
}
/// Parse a timezone by name
fn timezone_name_offset(input: &mut &str) -> PResult<Offset> {
/// I'm assuming there are no timezone abbreviations with more
/// than 6 charactres
const MAX_TZ_SIZE: usize = 6;
let nextword = s(take_while(1..=MAX_TZ_SIZE, AsChar::is_alpha)).parse_next(input)?;
let tz = tzname_to_offset(nextword)?;
if let Ok(other_tz) = timezone_num.parse_next(input) {
let newtz = tz
.merge(other_tz)
.ok_or(ErrMode::Cut(ContextError::new()))?;
return Ok(newtz);
};
Ok(tz)
}
/// Timezone list extracted from:
/// https://www.timeanddate.com/time/zones/
fn tzname_to_offset(input: &str) -> PResult<Offset> {
let mut offset_str = match input {
"z" => Ok("+0"),
"yekt" => Ok("+5"),
"yekst" => Ok("+6"),
"yapt" => Ok("+10"),
"yakt" => Ok("+9"),
"yakst" => Ok("+10"),
"y" => Ok("-12"),
"x" => Ok("-11"),
"wt" => Ok("+0"),
"wst" => Ok("+13"),
"wita" => Ok("+8"),
"wit" => Ok("+9"),
"wib" => Ok("+7"),
"wgt" => Ok("-2"),
"wgst" => Ok("-1"),
"wft" => Ok("+12"),
"wet" => Ok("+0"),
"west" => Ok("+1"),
"wat" => Ok("+1"),
"wast" => Ok("+2"),
"warst" => Ok("-3"),
"wakt" => Ok("+12"),
"w" => Ok("-10"),
"vut" => Ok("+11"),
"vost" => Ok("+6"),
"vlat" => Ok("+10"),
"vlast" => Ok("+11"),
"vet" => Ok("-4"),
"v" => Ok("-9"),
"uzt" => Ok("+5"),
"uyt" => Ok("-3"),
"uyst" => Ok("-2"),
"utc" => Ok("+0"),
"ulat" => Ok("+8"),
"ulast" => Ok("+9"),
"u" => Ok("-8"),
"tvt" => Ok("+12"),
"trt" => Ok("+3"),
"tot" => Ok("+13"),
"tost" => Ok("+14"),
"tmt" => Ok("+5"),
"tlt" => Ok("+9"),
"tkt" => Ok("+13"),
"tjt" => Ok("+5"),
"tft" => Ok("+5"),
"taht" => Ok("-10"),
"t" => Ok("-7"),
"syot" => Ok("+3"),
"sst" => Ok("-11"),
"srt" => Ok("-3"),
"sret" => Ok("+11"),
"sgt" => Ok("+8"),
"sct" => Ok("+4"),
"sbt" => Ok("+11"),
"sast" => Ok("+2"),
"samt" => Ok("+4"),
"sakt" => Ok("+11"),
"s" => Ok("-6"),
"rott" => Ok("-3"),
"ret" => Ok("+4"),
"r" => Ok("-5"),
"qyzt" => Ok("+6"),
"q" => Ok("-4"),
"pyt" => Ok("-4"),
"pyst" => Ok("-3"),
"pwt" => Ok("+9"),
"pt" => Ok("-7"),
"pst" => Ok("-8"),
"pont" => Ok("+11"),
"pmst" => Ok("-3"),
"pmdt" => Ok("-2"),
"pkt" => Ok("+5"),
"pht" => Ok("+8"),
"phot" => Ok("+13"),
"pgt" => Ok("+10"),
"pett" => Ok("+12"),
"petst" => Ok("+12"),
"pet" => Ok("-5"),
"pdt" => Ok("-7"),
"p" => Ok("-3"),
"orat" => Ok("+5"),
"omst" => Ok("+6"),
"omsst" => Ok("+7"),
"o" => Ok("-2"),
"nzst" => Ok("+12"),
"nzdt" => Ok("+13"),
"nut" => Ok("-11"),
"nst" => Ok("-3:30"),
"nrt" => Ok("+12"),
"npt" => Ok("+5:45"),
"novt" => Ok("+7"),
"novst" => Ok("+7"),
"nft" => Ok("+11"),
"nfdt" => Ok("+12"),
"ndt" => Ok("-2:30"),
"nct" => Ok("+11"),
"n" => Ok("-1"),
"myt" => Ok("+8"),
"mvt" => Ok("+5"),
"mut" => Ok("+4"),
"mt" => Ok("-6"),
"mst" => Ok("-7"),
"msk" => Ok("+3"),
"msd" => Ok("+4"),
"mmt" => Ok("+6:30"),
"mht" => Ok("+12"),
"mdt" => Ok("-6"),
"mawt" => Ok("+5"),
"mart" => Ok("-9:30"),
"magt" => Ok("+11"),
"magst" => Ok("+12"),
"m" => Ok("+12"),
"lint" => Ok("+14"),
"lhst" => Ok("+10:30"),
"lhdt" => Ok("+11"),
"l" => Ok("+11"),
"kuyt" => Ok("+4"),
"kst" => Ok("+9"),
"krat" => Ok("+7"),
"krast" => Ok("+8"),
"kost" => Ok("+11"),
"kgt" => Ok("+6"),
"k" => Ok("+10"),
"jst" => Ok("+9"),
"ist" => Ok("+5:30"),
"irst" => Ok("+3:30"),
"irkt" => Ok("+8"),
"irkst" => Ok("+9"),
"irdt" => Ok("+4:30"),
"iot" => Ok("+6"),
"idt" => Ok("+3"),
"ict" => Ok("+7"),
"i" => Ok("+9"),
"hst" => Ok("-10"),
"hovt" => Ok("+7"),
"hovst" => Ok("+8"),
"hkt" => Ok("+8"),
"hdt" => Ok("-9"),
"h" => Ok("+8"),
"gyt" => Ok("-4"),
"gst" => Ok("+4"),
"gmt" => Ok("+0"),
"gilt" => Ok("+12"),
"gft" => Ok("-3"),
"get" => Ok("+4"),
"gamt" => Ok("-9"),
"galt" => Ok("-6"),
"g" => Ok("+7"),
"fnt" => Ok("-2"),
"fkt" => Ok("-4"),
"fkst" => Ok("-3"),
"fjt" => Ok("+12"),
"fjst" => Ok("+13"),
"fet" => Ok("+3"),
"f" => Ok("+6"),
"et" => Ok("-4"),
"est" => Ok("-5"),
"egt" => Ok("-1"),
"egst" => Ok("+0"),
"eet" => Ok("+2"),
"eest" => Ok("+3"),
"edt" => Ok("-4"),
"ect" => Ok("-5"),
"eat" => Ok("+3"),
"east" => Ok("-6"),
"easst" => Ok("-5"),
"e" => Ok("+5"),
"ddut" => Ok("+10"),
"davt" => Ok("+7"),
"d" => Ok("+4"),
"chst" => Ok("+10"),
"cxt" => Ok("+7"),
"cvt" => Ok("-1"),
"ct" => Ok("-5"),
"cst" => Ok("-6"),
"cot" => Ok("-5"),
"clt" => Ok("-4"),
"clst" => Ok("-3"),
"ckt" => Ok("-10"),
"cist" => Ok("-5"),
"cidst" => Ok("-4"),
"chut" => Ok("+10"),
"chot" => Ok("+8"),
"chost" => Ok("+9"),
"chast" => Ok("+12:45"),
"chadt" => Ok("+13:45"),
"cet" => Ok("+1"),
"cest" => Ok("+2"),
"cdt" => Ok("-5"),
"cct" => Ok("+6:30"),
"cat" => Ok("+2"),
"cast" => Ok("+8"),
"c" => Ok("+3"),
"btt" => Ok("+6"),
"bst" => Ok("+6"),
"brt" => Ok("-3"),
"brst" => Ok("-2"),
"bot" => Ok("-4"),
"bnt" => Ok("+8"),
"b" => Ok("+2"),
"aoe" => Ok("-12"),
"azt" => Ok("+4"),
"azst" => Ok("+5"),
"azot" => Ok("-1"),
"azost" => Ok("+0"),
"awst" => Ok("+8"),
"awdt" => Ok("+9"),
"at" => Ok("-4:00"),
"ast" => Ok("-3"),
"art" => Ok("-3"),
"aqtt" => Ok("+5"),
"anat" => Ok("+12"),
"anast" => Ok("+12"),
"amt" => Ok("-4"),
"amst" => Ok("-3"),
"almt" => Ok("+6"),
"akst" => Ok("-9"),
"akdt" => Ok("-8"),
"aft" => Ok("+4:30"),
"aet" => Ok("+11"),
"aest" => Ok("+10"),
"aedt" => Ok("+11"),
"adt" => Ok("+4"),
"acwst" => Ok("+8:45"),
"act" => Ok("-5"),
"acst" => Ok("+9:30"),
"acdt" => Ok("+10:30"),
"a" => Ok("+1"),
_ => Err(ErrMode::Backtrack(ContextError::new())),
}?;
timezone_num(&mut offset_str)
}
/// Parse the plus or minus character and return whether it was negative
fn plus_or_minus(input: &mut &str) -> PResult<bool> {
s(alt(("+".value(false), "-".value(true)))).parse_next(input)
@@ -435,4 +770,36 @@ mod tests {
);
}
}
#[test]
fn test_timezone_colonless() {
use super::timezone_colonless;
fn aux(inp: &mut &str) -> String {
format!("{:?}", timezone_colonless(inp).expect("timezone_colonless"))
}
assert_eq!("(0, 0)", aux(&mut "0000"));
assert_eq!("(12, 34)", aux(&mut "1234"));
}
#[test]
fn test_timezone() {
use super::timezone;
let make_timezone = |input: &mut &str| {
timezone(input)
.map_err(|e| eprintln!("TEST FAILED AT:\n{}", anyhow::format_err!("{e}")))
.map(|offset| format!("{}", offset))
.expect("expect tests to succeed")
};
assert_eq!("+00:00", make_timezone(&mut "+00:00"));
assert_eq!("+00:00", make_timezone(&mut "+0000"));
assert_eq!("-00:00", make_timezone(&mut "-0000"));
assert_eq!("+00:00", make_timezone(&mut "gmt"));
assert_eq!("+01:00", make_timezone(&mut "a"));
assert_eq!("+00:00", make_timezone(&mut "utc"));
assert_eq!("-02:00", make_timezone(&mut "brst"));
assert_eq!("-03:00", make_timezone(&mut "brt"));
}
}
-14
View File
@@ -1,14 +0,0 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Parse a time zone items
//!
//! The GNU docs state:
//!
//! > Normally, dates are interpreted using the rules of the current time zone,
//! > which in turn are specified by the TZ environment variable, or by a
//! > system default if TZ is not set. To specify a different set of default
//! > time zone rules that apply just to one date, start the date with a string
//! > of the form TZ="rule". The two quote characters (") must be present
//! > in the date, and any quotes or backslashes within rule must be escaped by
//! > a backslash.
+16 -3
View File
@@ -26,7 +26,7 @@ use winnow::{ascii::alpha1, combinator::opt, seq, PResult, Parser};
use super::{ordinal::ordinal, s};
#[derive(PartialEq, Eq, Debug)]
enum Day {
pub(crate) enum Day {
Monday,
Tuesday,
Wednesday,
@@ -38,10 +38,23 @@ enum Day {
#[derive(PartialEq, Eq, Debug)]
pub struct Weekday {
offset: i32,
day: Day,
pub(crate) offset: i32,
pub(crate) day: Day,
}
impl From<Day> for chrono::Weekday {
fn from(value: Day) -> Self {
match value {
Day::Monday => chrono::Weekday::Mon,
Day::Tuesday => chrono::Weekday::Tue,
Day::Wednesday => chrono::Weekday::Wed,
Day::Thursday => chrono::Weekday::Thu,
Day::Friday => chrono::Weekday::Fri,
Day::Saturday => chrono::Weekday::Sat,
Day::Sunday => chrono::Weekday::Sun,
}
}
}
pub fn parse(input: &mut &str) -> PResult<Weekday> {
seq!(Weekday {
offset: opt(ordinal).map(|o| o.unwrap_or_default()),
+193 -6
View File
@@ -11,7 +11,7 @@
use std::error::Error;
use std::fmt::{self, Display};
use items::Item;
use chrono::{DateTime, FixedOffset, Local};
mod items;
@@ -35,11 +35,80 @@ impl Display for ParseDateTimeError {
impl Error for ParseDateTimeError {}
pub fn parse_datetime(input: &str) -> Result<Vec<Item>, ParseDateTimeError> {
let input = input.to_ascii_lowercase();
match items::parse(&mut input.as_ref()) {
Some(x) => Ok(x),
None => Err(ParseDateTimeError::InvalidInput),
/// Parses a time string and returns a `DateTime` representing the
/// absolute time of the string.
///
/// # Arguments
///
/// * `s` - A string slice representing the time.
///
/// # Examples
///
/// ```
/// use chrono::{DateTime, Utc, TimeZone};
/// let time = parse_datetime::parse_datetime("2023-06-03 12:00:01Z");
/// assert_eq!(time.unwrap(), Utc.with_ymd_and_hms(2023, 06, 03, 12, 00, 01).unwrap());
/// ```
///
///
/// # Returns
///
/// * `Ok(DateTime<FixedOffset>)` - If the input string can be parsed as a time
/// * `Err(ParseDateTimeError)` - If the input string cannot be parsed as a relative time
///
/// # Errors
///
/// This function will return `Err(ParseDateTimeError::InvalidInput)` if the input string
/// cannot be parsed as a relative time.
pub fn parse_datetime<S: AsRef<str> + Clone>(
input: S,
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
let input = input.as_ref().to_ascii_lowercase();
match items::parse(&mut input.as_str()) {
Ok(x) => items::at_local(x),
Err(_) => Err(ParseDateTimeError::InvalidInput),
}
}
/// Parses a time string at a specific date and returns a `DateTime` representing the
/// absolute time of the string.
///
/// # Arguments
///
/// * date - The date represented in local time
/// * `s` - A string slice representing the time.
///
/// # Examples
///
/// ```
/// use chrono::{Duration, Local};
/// use parse_datetime::parse_datetime_at_date;
///
/// let now = Local::now();
/// let after = parse_datetime_at_date(now, "2024-09-13 +3 days");
///
/// assert_eq!(
/// "2024-09-16",
/// after.unwrap().naive_utc().format("%F").to_string()
/// );
/// ```
///
/// # Returns
///
/// * `Ok(DateTime<FixedOffset>)` - If the input string can be parsed as a time
/// * `Err(ParseDateTimeError)` - If the input string cannot be parsed as a relative time
///
/// # Errors
///
/// This function will return `Err(ParseDateTimeError::InvalidInput)` if the input string
/// cannot be parsed as a relative time.
pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
date: DateTime<Local>,
input: S,
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
let input = input.as_ref().to_ascii_lowercase();
match items::parse(&mut input.as_str()) {
Ok(x) => items::at_date(x, date.into()),
Err(_) => Err(ParseDateTimeError::InvalidInput),
}
}
@@ -547,4 +616,122 @@ mod tests {
assert_eq!(actual, expected);
}
}
mod test_relative {
use chrono::NaiveDate;
use crate::{items, parse_datetime};
use std::{
env,
io::{self, Write},
};
#[test]
fn test_month() {
env::set_var("TZ", "UTC");
assert_eq!(
parse_datetime("28 feb + 1 month")
.expect("parse_datetime")
.format("%+")
.to_string(),
"2024-03-28T00:00:00+00:00"
);
// 29 feb 2025 is invalid
assert!(parse_datetime("29 feb + 1 year").is_err());
// 29 feb 2025 is an invalid date
assert!(parse_datetime("29 feb 2025").is_err());
// because 29 feb 2025 is invalid, 29 feb 2025 + 1 day is invalid
// arithmetic does not operate on invalid dates
assert!(parse_datetime("29 feb 2025 + 1 day").is_err());
// 28 feb 2023 + 1 day = 1 mar
assert_eq!(
parse_datetime("28 feb 2023 + 1 day")
.unwrap()
.format("%+")
.to_string(),
"2023-03-01T00:00:00+00:00"
);
}
#[test]
fn month_overflow() {
env::set_var("TZ", "UTC");
assert_eq!(
parse_datetime("2024-01-31 + 1 month")
.unwrap()
.format("%+")
.to_string(),
"2024-03-02T00:00:00+00:00",
);
assert_eq!(
parse_datetime("2024-02-29 + 1 month")
.unwrap()
.format("%+")
.to_string(),
"2024-03-29T00:00:00+00:00",
);
}
fn make_gnu_date(input: &str, fmt: &str) -> String {
std::process::Command::new("date")
.arg("-d")
.arg(input)
.arg(format!("+{fmt}"))
.output()
.map(|mut output| {
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);
}
#[test]
fn gnu_compat() {
const FMT: &str = "%Y-%m-%d %H:%M:%S";
let input = "0000-03-02 00:00:00";
assert_eq!(
make_gnu_date(input, FMT),
parse_datetime(input).unwrap().format(FMT).to_string()
);
let input = "262144-03-10 00:00:00";
assert_eq!(
make_gnu_date(input, FMT),
parse_datetime(input)
.expect("parse_datetime")
.format(FMT)
.to_string()
);
let input = "10384-03-10 00:00:00";
assert_eq!(
make_gnu_date(input, FMT),
parse_datetime(input)
.expect("parse_datetime")
.format(FMT)
.to_string()
);
}
}
}