You've already forked parse_datetime
mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
Restore the old API and add the missing combinators
This commit is contained in:
committed by
yuankunzhang
parent
07d4b80ea5
commit
4c3d2221da
Generated
+257
-29
@@ -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
@@ -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
|
||||
|
||||
@@ -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("%+"))
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!()
|
||||
}
|
||||
@@ -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
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user