mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
Merge pull request #137 from yuankunzhang/replace-the-regex-parser
Replace the regex parser with winnow-based parse
This commit is contained in:
Generated
+31
-20
@@ -129,9 +129,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",
|
||||
]
|
||||
@@ -148,7 +148,9 @@ version = "0.9.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"nom",
|
||||
"num-traits",
|
||||
"regex",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -287,9 +289,9 @@ checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
@@ -303,48 +305,57 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
|
||||
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.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
@@ -11,3 +11,5 @@ readme = "README.md"
|
||||
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"
|
||||
|
||||
Generated
+13
-2
@@ -169,9 +169,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",
|
||||
]
|
||||
@@ -188,7 +188,9 @@ version = "0.9.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"nom",
|
||||
"num-traits",
|
||||
"regex",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -394,3 +396,12 @@ name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Parse an ISO 8601 date and time item
|
||||
//!
|
||||
//! The GNU docs state:
|
||||
//!
|
||||
//! > The ISO 8601 date and time of day extended format consists of an ISO 8601
|
||||
//! > date, a ‘T’ character separator, and an ISO 8601 time of day. This format
|
||||
//! > is also recognized if the ‘T’ is replaced by a space.
|
||||
//! >
|
||||
//! > In this format, the time of day should use 24-hour notation. Fractional
|
||||
//! > 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.
|
||||
use winnow::{combinator::alt, seq, trace::trace, PResult, Parser};
|
||||
|
||||
use crate::items::space;
|
||||
|
||||
use super::{
|
||||
date::{self, Date},
|
||||
s,
|
||||
time::{self, Time},
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Default)]
|
||||
pub struct DateTime {
|
||||
pub(crate) date: Date,
|
||||
pub(crate) time: Time,
|
||||
}
|
||||
|
||||
pub fn parse(input: &mut &str) -> PResult<DateTime> {
|
||||
seq!(DateTime {
|
||||
date: trace("date iso", alt((date::iso1, date::iso2))),
|
||||
// Note: the `T` is lowercased by the main parse function
|
||||
_: alt((s('t').void(), (' ', space).void())),
|
||||
time: trace("time iso", time::iso),
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse, DateTime};
|
||||
use crate::items::{date::Date, time::Time};
|
||||
|
||||
#[test]
|
||||
fn some_date() {
|
||||
let reference = Some(DateTime {
|
||||
date: Date {
|
||||
day: 10,
|
||||
month: 10,
|
||||
year: Some(2022),
|
||||
},
|
||||
time: Time {
|
||||
hour: 10,
|
||||
minute: 10,
|
||||
second: 55.0,
|
||||
offset: None,
|
||||
},
|
||||
});
|
||||
|
||||
for mut s in [
|
||||
"2022-10-10t10:10:55",
|
||||
"2022-10-10 10:10:55",
|
||||
"2022-10-10 t 10:10:55",
|
||||
"2022-10-10 10:10:55",
|
||||
"2022-10-10 (A comment!) t 10:10:55",
|
||||
"2022-10-10 (A comment!) 10:10:55",
|
||||
] {
|
||||
let old_s = s.to_owned();
|
||||
assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Parse a date item (without time component)
|
||||
//!
|
||||
//! The GNU docs say:
|
||||
//!
|
||||
//! > A calendar date item specifies a day of the year. It is specified
|
||||
//! > differently, depending on whether the month is specified numerically
|
||||
//! > or literally.
|
||||
//! >
|
||||
//! > ...
|
||||
//! >
|
||||
//! > For numeric months, the ISO 8601 format ‘year-month-day’ is allowed,
|
||||
//! > where year is any positive number, month is a number between 01 and
|
||||
//! > 12, and day is a number between 01 and 31. A leading zero must be
|
||||
//! > present if a number is less than ten. If year is 68 or smaller, then
|
||||
//! > 2000 is added to it; otherwise, if year is less than 100, then 1900
|
||||
//! > is added to it. The construct ‘month/day/year’, popular in the United
|
||||
//! > States, is accepted. Also ‘month/day’, omitting the year.
|
||||
//! >
|
||||
//! > Literal months may be spelled out in full: ‘January’, ‘February’,
|
||||
//! > ‘March’, ‘April’, ‘May’, ‘June’, ‘July’, ‘August’, ‘September’,
|
||||
//! > ‘October’, ‘November’ or ‘December’. Literal months may be
|
||||
//! > abbreviated to their first three letters, possibly followed by an
|
||||
//! > abbreviating dot. It is also permitted to write ‘Sept’ instead of
|
||||
//! > ‘September’.
|
||||
|
||||
use winnow::{
|
||||
ascii::{alpha1, dec_uint},
|
||||
combinator::{alt, opt, preceded},
|
||||
seq,
|
||||
stream::AsChar,
|
||||
token::{take, take_while},
|
||||
trace::trace,
|
||||
PResult, Parser,
|
||||
};
|
||||
|
||||
use super::s;
|
||||
use crate::ParseDateTimeError;
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Default)]
|
||||
pub struct Date {
|
||||
pub day: u32,
|
||||
pub month: u32,
|
||||
pub year: Option<u32>,
|
||||
}
|
||||
|
||||
pub fn parse(input: &mut &str) -> PResult<Date> {
|
||||
alt((iso1, iso2, us, literal1, literal2)).parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse `YYYY-MM-DD` or `YY-MM-DD`
|
||||
///
|
||||
/// This is also used by [`combined`](super::combined).
|
||||
pub fn iso1(input: &mut &str) -> PResult<Date> {
|
||||
seq!(Date {
|
||||
year: year.map(Some),
|
||||
_: s('-'),
|
||||
month: month,
|
||||
_: s('-'),
|
||||
day: day,
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse `YYYYMMDD`
|
||||
///
|
||||
/// This is also used by [`combined`](super::combined).
|
||||
pub fn iso2(input: &mut &str) -> PResult<Date> {
|
||||
s((
|
||||
take(4usize).try_map(|s: &str| s.parse::<u32>()),
|
||||
take(2usize).try_map(|s: &str| s.parse::<u32>()),
|
||||
take(2usize).try_map(|s: &str| s.parse::<u32>()),
|
||||
))
|
||||
.map(|(year, month, day): (u32, u32, u32)| Date {
|
||||
day,
|
||||
month,
|
||||
year: Some(year),
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse `MM/DD/YYYY`, `MM/DD/YY` or `MM/DD`
|
||||
fn us(input: &mut &str) -> PResult<Date> {
|
||||
seq!(Date {
|
||||
month: month,
|
||||
_: s('/'),
|
||||
day: day,
|
||||
year: opt(preceded(s('/'), year)),
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse `14 November 2022`, `14 Nov 2022`, "14nov2022", "14-nov-2022", "14-nov2022", "14nov-2022"
|
||||
fn literal1(input: &mut &str) -> PResult<Date> {
|
||||
seq!(Date {
|
||||
day: day,
|
||||
_: opt(s('-')),
|
||||
month: literal_month,
|
||||
year: opt(preceded(opt(s('-')), year)),
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse `November 14, 2022` and `Nov 14, 2022`
|
||||
fn literal2(input: &mut &str) -> PResult<Date> {
|
||||
seq!(Date {
|
||||
month: literal_month,
|
||||
day: day,
|
||||
// FIXME: GNU requires _some_ space between the day and the year,
|
||||
// probably to distinguish with floats.
|
||||
year: opt(preceded(s(","), year)),
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
pub fn year(input: &mut &str) -> PResult<u32> {
|
||||
// 2147485547 is the maximum value accepted
|
||||
// by GNU, but chrono only behaves like GNU
|
||||
// for years in the range: [0, 9999], so we
|
||||
// keep in the range [0, 9999]
|
||||
trace(
|
||||
"year",
|
||||
s(
|
||||
take_while(1..=4, AsChar::is_dec_digit).map(|number_str: &str| {
|
||||
let year = number_str.parse::<u32>().unwrap();
|
||||
if number_str.len() == 2 {
|
||||
if year <= 68 {
|
||||
year + 2000
|
||||
} else {
|
||||
year + 1900
|
||||
}
|
||||
} else {
|
||||
year
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn month(input: &mut &str) -> PResult<u32> {
|
||||
s(dec_uint)
|
||||
.try_map(|x| {
|
||||
(1..=12)
|
||||
.contains(&x)
|
||||
.then_some(x)
|
||||
.ok_or(ParseDateTimeError::InvalidInput)
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn day(input: &mut &str) -> PResult<u32> {
|
||||
s(dec_uint)
|
||||
.try_map(|x| {
|
||||
(1..=31)
|
||||
.contains(&x)
|
||||
.then_some(x)
|
||||
.ok_or(ParseDateTimeError::InvalidInput)
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse the name of a month (case-insensitive)
|
||||
fn literal_month(input: &mut &str) -> PResult<u32> {
|
||||
s(alpha1)
|
||||
.verify_map(|s: &str| {
|
||||
Some(match s {
|
||||
"january" | "jan" => 1,
|
||||
"february" | "feb" => 2,
|
||||
"march" | "mar" => 3,
|
||||
"april" | "apr" => 4,
|
||||
"may" => 5,
|
||||
"june" | "jun" => 6,
|
||||
"july" | "jul" => 7,
|
||||
"august" | "aug" => 8,
|
||||
"september" | "sep" | "sept" => 9,
|
||||
"october" | "oct" => 10,
|
||||
"november" | "nov" => 11,
|
||||
"december" | "dec" => 12,
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse, Date};
|
||||
|
||||
// Test cases from the GNU docs:
|
||||
//
|
||||
// ```
|
||||
// 2022-11-14 # ISO 8601.
|
||||
// 22-11-14 # Assume 19xx for 69 through 99,
|
||||
// # 20xx for 00 through 68 (not recommended).
|
||||
// 11/14/2022 # Common U.S. writing.
|
||||
// 14 November 2022
|
||||
// 14 Nov 2022 # Three-letter abbreviations always allowed.
|
||||
// November 14, 2022
|
||||
// 14-nov-2022
|
||||
// 14nov2022
|
||||
// ```
|
||||
|
||||
#[test]
|
||||
fn with_year() {
|
||||
let reference = Date {
|
||||
year: Some(2022),
|
||||
month: 11,
|
||||
day: 14,
|
||||
};
|
||||
|
||||
for mut s in [
|
||||
"2022-11-14",
|
||||
"2022 - 11 - 14",
|
||||
"22-11-14",
|
||||
"2022---11----14",
|
||||
"22(comment 1)-11(comment 2)-14",
|
||||
"11/14/2022",
|
||||
"11--/14--/2022",
|
||||
"11(comment 1)/(comment 2)14(comment 3)/(comment 4)2022",
|
||||
"11 / 14 / 2022",
|
||||
"11/14/22",
|
||||
"14 november 2022",
|
||||
"14 nov 2022",
|
||||
"november 14, 2022",
|
||||
"november 14 , 2022",
|
||||
"nov 14, 2022",
|
||||
"14-nov-2022",
|
||||
"14nov2022",
|
||||
"14nov 2022",
|
||||
] {
|
||||
let old_s = s.to_owned();
|
||||
assert_eq!(parse(&mut s).unwrap(), reference, "Format string: {old_s}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_year() {
|
||||
let reference = Date {
|
||||
year: None,
|
||||
month: 11,
|
||||
day: 14,
|
||||
};
|
||||
for mut s in [
|
||||
"11/14",
|
||||
"14 november",
|
||||
"14 nov",
|
||||
"14(comment!)nov",
|
||||
"november 14",
|
||||
"november(comment!)14",
|
||||
"nov 14",
|
||||
"14-nov",
|
||||
"14nov",
|
||||
"14(comment????)nov",
|
||||
] {
|
||||
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());
|
||||
// -> GNU accepts year 0
|
||||
// test $(date -d '1-1-1' '+%Y') -eq '0001'
|
||||
|
||||
// test $(date -d '68-1-1' '+%Y') -eq '2068'
|
||||
// 2-characters are converted to 19XX/20XX
|
||||
assert_eq!(year(&mut "10").unwrap(), 2010u32);
|
||||
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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use super::s;
|
||||
use winnow::{
|
||||
ascii::{alpha1, dec_uint},
|
||||
combinator::{alt, opt},
|
||||
PResult, Parser,
|
||||
};
|
||||
|
||||
pub fn ordinal(input: &mut &str) -> PResult<i32> {
|
||||
alt((text_ordinal, number_ordinal)).parse_next(input)
|
||||
}
|
||||
|
||||
fn number_ordinal(input: &mut &str) -> PResult<i32> {
|
||||
let sign = opt(alt(('+'.value(1), '-'.value(-1)))).map(|s| s.unwrap_or(1));
|
||||
(s(sign), s(dec_uint))
|
||||
.verify_map(|(s, u): (i32, u32)| {
|
||||
let i: i32 = u.try_into().ok()?;
|
||||
Some(s * i)
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn text_ordinal(input: &mut &str) -> PResult<i32> {
|
||||
s(alpha1)
|
||||
.verify_map(|s: &str| {
|
||||
Some(match s {
|
||||
"last" => -1,
|
||||
"this" => 0,
|
||||
"next" | "first" => 1,
|
||||
"third" => 3,
|
||||
"fourth" => 4,
|
||||
"fifth" => 5,
|
||||
"sixth" => 6,
|
||||
"seventh" => 7,
|
||||
"eight" => 8,
|
||||
"ninth" => 9,
|
||||
"tenth" => 10,
|
||||
"eleventh" => 11,
|
||||
"twelfth" => 12,
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Parse a relative datetime item
|
||||
//!
|
||||
//! The GNU docs state:
|
||||
//!
|
||||
//! > The unit of time displacement may be selected by the string ‘year’ or
|
||||
//! > ‘month’ for moving by whole years or months. These are fuzzy units, as
|
||||
//! > years and months are not all of equal duration. More precise units are
|
||||
//! > ‘fortnight’ which is worth 14 days, ‘week’ worth 7 days, ‘day’ worth 24
|
||||
//! > hours, ‘hour’ worth 60 minutes, ‘minute’ or ‘min’ worth 60 seconds, and
|
||||
//! > ‘second’ or ‘sec’ worth one second. An ‘s’ suffix on these units is
|
||||
//! > accepted and ignored.
|
||||
//! >
|
||||
//! > The unit of time may be preceded by a multiplier, given as an optionally
|
||||
//! > signed number. Unsigned numbers are taken as positively signed. No number
|
||||
//! > at all implies 1 for a multiplier. Following a relative item by the
|
||||
//! > string ‘ago’ is equivalent to preceding the unit by a multiplier with
|
||||
//! > value -1.
|
||||
//! >
|
||||
//! > The string ‘tomorrow’ is worth one day in the future (equivalent to
|
||||
//! > ‘day’), the string ‘yesterday’ is worth one day in the past (equivalent
|
||||
//! > to ‘day ago’).
|
||||
//! >
|
||||
//! > The strings ‘now’ or ‘today’ are relative items corresponding to
|
||||
//! > zero-valued time displacement, these strings come from the fact a
|
||||
//! > zero-valued time displacement represents the current time when not
|
||||
//! > otherwise changed by previous items. They may be used to stress other
|
||||
//! > items, like in ‘12:00 today’. The string ‘this’ also has the meaning of a
|
||||
//! > zero-valued time displacement, but is preferred in date strings like
|
||||
//! > ‘this thursday’.
|
||||
|
||||
use winnow::{
|
||||
ascii::{alpha1, float},
|
||||
combinator::{alt, opt},
|
||||
PResult, Parser,
|
||||
};
|
||||
|
||||
use super::{ordinal::ordinal, s};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Relative {
|
||||
Years(i32),
|
||||
Months(i32),
|
||||
Days(i32),
|
||||
Hours(i32),
|
||||
Minutes(i32),
|
||||
// Seconds are special because they can be given as a float
|
||||
Seconds(f64),
|
||||
}
|
||||
|
||||
impl Relative {
|
||||
fn mul(self, n: i32) -> Self {
|
||||
match self {
|
||||
Self::Years(x) => Self::Years(n * x),
|
||||
Self::Months(x) => Self::Months(n * x),
|
||||
Self::Days(x) => Self::Days(n * x),
|
||||
Self::Hours(x) => Self::Hours(n * x),
|
||||
Self::Minutes(x) => Self::Minutes(n * x),
|
||||
Self::Seconds(x) => Self::Seconds(f64::from(n) * x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(input: &mut &str) -> PResult<Relative> {
|
||||
alt((
|
||||
s("tomorrow").value(Relative::Days(1)),
|
||||
s("yesterday").value(Relative::Days(-1)),
|
||||
// For "today" and "now", the unit is arbitrary
|
||||
s("today").value(Relative::Days(0)),
|
||||
s("now").value(Relative::Days(0)),
|
||||
seconds,
|
||||
other,
|
||||
))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn seconds(input: &mut &str) -> PResult<Relative> {
|
||||
(
|
||||
opt(alt((s(float), ordinal.map(|x| x as f64)))),
|
||||
s(alpha1).verify(|s: &str| matches!(s, "seconds" | "second" | "sec" | "secs")),
|
||||
ago,
|
||||
)
|
||||
.map(|(n, _, ago)| Relative::Seconds(n.unwrap_or(1.0) * if ago { -1.0 } else { 1.0 }))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn other(input: &mut &str) -> PResult<Relative> {
|
||||
(opt(ordinal), integer_unit, ago)
|
||||
.map(|(n, unit, ago)| unit.mul(n.unwrap_or(1) * if ago { -1 } else { 1 }))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn ago(input: &mut &str) -> PResult<bool> {
|
||||
opt(s("ago")).map(|o| o.is_some()).parse_next(input)
|
||||
}
|
||||
|
||||
fn integer_unit(input: &mut &str) -> PResult<Relative> {
|
||||
s(alpha1)
|
||||
.verify_map(|s: &str| {
|
||||
Some(match s.strip_suffix('s').unwrap_or(s) {
|
||||
"year" => Relative::Years(1),
|
||||
"month" => Relative::Months(1),
|
||||
"fortnight" => Relative::Days(14),
|
||||
"week" => Relative::Days(7),
|
||||
"day" => Relative::Days(1),
|
||||
"hour" => Relative::Hours(1),
|
||||
"minute" | "min" => Relative::Minutes(1),
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse, Relative};
|
||||
|
||||
#[test]
|
||||
fn all() {
|
||||
for (s, rel) in [
|
||||
// Seconds
|
||||
("second", Relative::Seconds(1.0)),
|
||||
("sec", Relative::Seconds(1.0)),
|
||||
("seconds", Relative::Seconds(1.0)),
|
||||
("secs", Relative::Seconds(1.0)),
|
||||
("second ago", Relative::Seconds(-1.0)),
|
||||
("3 seconds", Relative::Seconds(3.0)),
|
||||
("3.5 seconds", Relative::Seconds(3.5)),
|
||||
// ("+3.5 seconds", Relative::Seconds(3.5)),
|
||||
("3.5 seconds ago", Relative::Seconds(-3.5)),
|
||||
("-3.5 seconds ago", Relative::Seconds(3.5)),
|
||||
// Minutes
|
||||
("minute", Relative::Minutes(1)),
|
||||
("minutes", Relative::Minutes(1)),
|
||||
("min", Relative::Minutes(1)),
|
||||
("mins", Relative::Minutes(1)),
|
||||
("10 minutes", Relative::Minutes(10)),
|
||||
("-10 minutes", Relative::Minutes(-10)),
|
||||
("10 minutes ago", Relative::Minutes(-10)),
|
||||
("-10 minutes ago", Relative::Minutes(10)),
|
||||
// Hours
|
||||
("hour", Relative::Hours(1)),
|
||||
("hours", Relative::Hours(1)),
|
||||
("10 hours", Relative::Hours(10)),
|
||||
("+10 hours", Relative::Hours(10)),
|
||||
("-10 hours", Relative::Hours(-10)),
|
||||
("10 hours ago", Relative::Hours(-10)),
|
||||
("-10 hours ago", Relative::Hours(10)),
|
||||
// Days
|
||||
("day", Relative::Days(1)),
|
||||
("days", Relative::Days(1)),
|
||||
("10 days", Relative::Days(10)),
|
||||
("+10 days", Relative::Days(10)),
|
||||
("-10 days", Relative::Days(-10)),
|
||||
("10 days ago", Relative::Days(-10)),
|
||||
("-10 days ago", Relative::Days(10)),
|
||||
// Multiple days
|
||||
("fortnight", Relative::Days(14)),
|
||||
("fortnights", Relative::Days(14)),
|
||||
("2 fortnights ago", Relative::Days(-28)),
|
||||
("+2 fortnights ago", Relative::Days(-28)),
|
||||
("week", Relative::Days(7)),
|
||||
("weeks", Relative::Days(7)),
|
||||
("2 weeks ago", Relative::Days(-14)),
|
||||
// Other
|
||||
("year", Relative::Years(1)),
|
||||
("years", Relative::Years(1)),
|
||||
("month", Relative::Months(1)),
|
||||
("months", Relative::Months(1)),
|
||||
// Special
|
||||
("yesterday", Relative::Days(-1)),
|
||||
("tomorrow", Relative::Days(1)),
|
||||
("today", Relative::Days(0)),
|
||||
("now", Relative::Days(0)),
|
||||
// This something
|
||||
("this day", Relative::Days(0)),
|
||||
("this second", Relative::Seconds(0.0)),
|
||||
("this year", Relative::Years(0)),
|
||||
// Weird stuff
|
||||
("next week ago", Relative::Days(-7)),
|
||||
("last week ago", Relative::Days(7)),
|
||||
("this week ago", Relative::Days(0)),
|
||||
] {
|
||||
let mut t = s;
|
||||
assert_eq!(parse(&mut t).ok(), Some(rel), "Failed string: {s}")
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
// spell-checker:ignore wednes
|
||||
|
||||
//! The GNU docs state:
|
||||
//!
|
||||
//! > The explicit mention of a day of the week will forward the date (only if
|
||||
//! > necessary) to reach that day of the week in the future.
|
||||
//! >
|
||||
//! > Days of the week may be spelled out in full: ‘Sunday’, ‘Monday’,
|
||||
//! > ‘Tuesday’, ‘Wednesday’, ‘Thursday’, ‘Friday’ or ‘Saturday’. Days may be
|
||||
//! > abbreviated to their first three letters, optionally followed by a
|
||||
//! > period. The special abbreviations ‘Tues’ for ‘Tuesday’, ‘Wednes’ for
|
||||
//! > ‘Wednesday’ and ‘Thur’ or ‘Thurs’ for ‘Thursday’ are also allowed.
|
||||
//! >
|
||||
//! > A number may precede a day of the week item to move forward supplementary
|
||||
//! > weeks. It is best used in expression like ‘third monday’. In this
|
||||
//! > context, ‘last day’ or ‘next day’ is also acceptable; they move one week
|
||||
//! > before or after the day that day by itself would represent.
|
||||
//! >
|
||||
//! > A comma following a day of the week item is ignored.
|
||||
|
||||
use winnow::{ascii::alpha1, combinator::opt, seq, PResult, Parser};
|
||||
|
||||
use super::{ordinal::ordinal, s};
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub(crate) enum Day {
|
||||
Monday,
|
||||
Tuesday,
|
||||
Wednesday,
|
||||
Thursday,
|
||||
Friday,
|
||||
Saturday,
|
||||
Sunday,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Weekday {
|
||||
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()),
|
||||
day: day,
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn day(input: &mut &str) -> PResult<Day> {
|
||||
s(alpha1)
|
||||
.verify_map(|s: &str| {
|
||||
Some(match s {
|
||||
"monday" | "mon" | "mon." => Day::Monday,
|
||||
"tuesday" | "tue" | "tue." | "tues" => Day::Tuesday,
|
||||
"wednesday" | "wed" | "wed." | "wednes" => Day::Wednesday,
|
||||
"thursday" | "thu" | "thu." | "thur" | "thurs" => Day::Thursday,
|
||||
"friday" | "fri" | "fri." => Day::Friday,
|
||||
"saturday" | "sat" | "sat." => Day::Saturday,
|
||||
"sunday" | "sun" | "sun." => Day::Sunday,
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse, Day, Weekday};
|
||||
|
||||
#[test]
|
||||
fn this_monday() {
|
||||
for mut s in [
|
||||
"monday",
|
||||
"mon",
|
||||
"mon.",
|
||||
"this monday",
|
||||
"this mon",
|
||||
"this mon.",
|
||||
"this monday",
|
||||
"this - monday",
|
||||
"0 monday",
|
||||
] {
|
||||
assert_eq!(
|
||||
parse(&mut s).unwrap(),
|
||||
Weekday {
|
||||
offset: 0,
|
||||
day: Day::Monday,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_tuesday() {
|
||||
for s in ["tuesday", "tue", "tue.", "tues"] {
|
||||
let s = format!("next {s}");
|
||||
assert_eq!(
|
||||
parse(&mut s.as_ref()).unwrap(),
|
||||
Weekday {
|
||||
offset: 1,
|
||||
day: Day::Tuesday,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_wednesday() {
|
||||
for s in ["wednesday", "wed", "wed.", "wednes"] {
|
||||
let s = format!("last {s}");
|
||||
assert_eq!(
|
||||
parse(&mut s.as_ref()).unwrap(),
|
||||
Weekday {
|
||||
offset: -1,
|
||||
day: Day::Wednesday,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+115
-291
@@ -8,39 +8,22 @@
|
||||
//! * unix timestamps, e.g., "@12"
|
||||
//! * relative time to now, e.g. "+1 hour"
|
||||
//!
|
||||
use regex::Error as RegexError;
|
||||
use regex::Regex;
|
||||
use std::error::Error;
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
// Expose parse_datetime
|
||||
mod parse_relative_time;
|
||||
mod parse_timestamp;
|
||||
use chrono::{DateTime, FixedOffset, Local};
|
||||
|
||||
mod parse_time_only_str;
|
||||
mod parse_weekday;
|
||||
|
||||
use chrono::{
|
||||
DateTime, Datelike, Duration, FixedOffset, Local, LocalResult, MappedLocalTime, NaiveDate,
|
||||
NaiveDateTime, TimeZone, Timelike,
|
||||
};
|
||||
|
||||
use parse_relative_time::parse_relative_time_at_date;
|
||||
use parse_timestamp::parse_timestamp;
|
||||
mod items;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ParseDateTimeError {
|
||||
InvalidRegex(RegexError),
|
||||
InvalidInput,
|
||||
}
|
||||
|
||||
impl Display for ParseDateTimeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InvalidRegex(err) => {
|
||||
write!(f, "Invalid regex for time pattern: {err}")
|
||||
}
|
||||
Self::InvalidInput => {
|
||||
ParseDateTimeError::InvalidInput => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid input string: cannot be parsed as a relative time"
|
||||
@@ -52,118 +35,6 @@ impl Display for ParseDateTimeError {
|
||||
|
||||
impl Error for ParseDateTimeError {}
|
||||
|
||||
impl From<RegexError> for ParseDateTimeError {
|
||||
fn from(err: RegexError) -> Self {
|
||||
Self::InvalidRegex(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats that parse input can take.
|
||||
/// Taken from `touch` coreutils
|
||||
mod format {
|
||||
pub const ISO_8601: &str = "%Y-%m-%d";
|
||||
pub const ISO_8601_NO_SEP: &str = "%Y%m%d";
|
||||
// US format for calendar date items:
|
||||
// https://www.gnu.org/software/coreutils/manual/html_node/Calendar-date-items.html
|
||||
pub const MMDDYYYY_SLASH: &str = "%m/%d/%Y";
|
||||
pub const MMDDYY_SLASH: &str = "%m/%d/%y";
|
||||
pub const POSIX_LOCALE: &str = "%a %b %e %H:%M:%S %Y";
|
||||
pub const YYYYMMDDHHMM_DOT_SS: &str = "%Y%m%d%H%M.%S";
|
||||
pub const YYYYMMDDHHMMSS: &str = "%Y-%m-%d %H:%M:%S.%f";
|
||||
pub const YYYYMMDDHHMMS: &str = "%Y-%m-%d %H:%M:%S";
|
||||
pub const YYYY_MM_DD_HH_MM: &str = "%Y-%m-%d %H:%M";
|
||||
pub const YYYYMMDDHHMM: &str = "%Y%m%d%H%M";
|
||||
pub const YYYYMMDDHHMM_OFFSET: &str = "%Y%m%d%H%M %z";
|
||||
pub const YYYYMMDDHHMM_UTC_OFFSET: &str = "%Y%m%d%H%MUTC%z";
|
||||
pub const YYYYMMDDHHMM_ZULU_OFFSET: &str = "%Y%m%d%H%MZ%z";
|
||||
pub const YYYYMMDDHHMM_HYPHENATED_OFFSET: &str = "%Y-%m-%d %H:%M %z";
|
||||
pub const YYYYMMDDHHMMSS_HYPHENATED_OFFSET: &str = "%Y-%m-%d %H:%M:%S %#z";
|
||||
pub const YYYYMMDDHHMMSS_HYPHENATED_ZULU: &str = "%Y-%m-%d %H:%M:%SZ";
|
||||
pub const YYYYMMDDHHMMSS_T_SEP_HYPHENATED_OFFSET: &str = "%Y-%m-%dT%H:%M:%S%#z";
|
||||
pub const YYYYMMDDHHMMSS_T_SEP_HYPHENATED_ZULU: &str = "%Y-%m-%dT%H:%M:%SZ";
|
||||
pub const YYYYMMDDHHMMSS_T_SEP_HYPHENATED_SPACE_OFFSET: &str = "%Y-%m-%dT%H:%M:%S %#z";
|
||||
pub const YYYYMMDDHHMMS_T_SEP: &str = "%Y-%m-%dT%H:%M:%S";
|
||||
pub const UTC_OFFSET: &str = "UTC%#z";
|
||||
pub const ZULU_OFFSET: &str = "Z%#z";
|
||||
pub const NAKED_OFFSET: &str = "%#z";
|
||||
|
||||
/// Whether the pattern ends in the character `Z`.
|
||||
pub(crate) fn is_zulu(pattern: &str) -> bool {
|
||||
pattern.ends_with('Z')
|
||||
}
|
||||
|
||||
/// Patterns for datetimes with timezones.
|
||||
///
|
||||
/// These are in decreasing order of length. The same pattern may
|
||||
/// appear multiple times with different lengths if the pattern
|
||||
/// accepts input strings of different lengths. For example, the
|
||||
/// specifier `%#z` accepts two-digit time zone offsets (`+00`)
|
||||
/// and four-digit time zone offsets (`+0000`).
|
||||
pub(crate) const PATTERNS_TZ: [(&str, usize); 9] = [
|
||||
(YYYYMMDDHHMMSS_HYPHENATED_OFFSET, 25),
|
||||
(YYYYMMDDHHMMSS_T_SEP_HYPHENATED_SPACE_OFFSET, 25),
|
||||
(YYYYMMDDHHMMSS_T_SEP_HYPHENATED_OFFSET, 24),
|
||||
(YYYYMMDDHHMMSS_HYPHENATED_OFFSET, 23),
|
||||
(YYYYMMDDHHMMSS_T_SEP_HYPHENATED_OFFSET, 22),
|
||||
(YYYYMMDDHHMM_HYPHENATED_OFFSET, 22),
|
||||
(YYYYMMDDHHMM_UTC_OFFSET, 20),
|
||||
(YYYYMMDDHHMM_OFFSET, 18),
|
||||
(YYYYMMDDHHMM_ZULU_OFFSET, 18),
|
||||
];
|
||||
|
||||
/// Patterns for datetimes without timezones.
|
||||
///
|
||||
/// These are in decreasing order of length.
|
||||
pub(crate) const PATTERNS_NO_TZ: [(&str, usize); 9] = [
|
||||
(YYYYMMDDHHMMSS, 29),
|
||||
(POSIX_LOCALE, 24),
|
||||
(YYYYMMDDHHMMSS_HYPHENATED_ZULU, 20),
|
||||
(YYYYMMDDHHMMSS_T_SEP_HYPHENATED_ZULU, 20),
|
||||
(YYYYMMDDHHMMS_T_SEP, 19),
|
||||
(YYYYMMDDHHMMS, 19),
|
||||
(YYYY_MM_DD_HH_MM, 16),
|
||||
(YYYYMMDDHHMM_DOT_SS, 15),
|
||||
(YYYYMMDDHHMM, 12),
|
||||
];
|
||||
|
||||
/// Patterns for dates with neither times nor timezones.
|
||||
///
|
||||
/// These are in decreasing order of length. The same pattern may
|
||||
/// appear multiple times with different lengths if the pattern
|
||||
/// accepts input strings of different lengths. For example, the
|
||||
/// specifier `%m` accepts one-digit month numbers (like `2`) and
|
||||
/// two-digit month numbers (like `02` or `12`).
|
||||
pub(crate) const PATTERNS_DATE_NO_TZ: [(&str, usize); 8] = [
|
||||
(ISO_8601, 10),
|
||||
(MMDDYYYY_SLASH, 10),
|
||||
(ISO_8601, 9),
|
||||
(MMDDYYYY_SLASH, 9),
|
||||
(ISO_8601, 8),
|
||||
(MMDDYY_SLASH, 8),
|
||||
(MMDDYYYY_SLASH, 8),
|
||||
(ISO_8601_NO_SEP, 8),
|
||||
];
|
||||
|
||||
/// Patterns for lone timezone offsets.
|
||||
///
|
||||
/// These are in decreasing order of length. The same pattern may
|
||||
/// appear multiple times with different lengths if the pattern
|
||||
/// accepts input strings of different lengths. For example, the
|
||||
/// specifier `%#z` accepts two-digit time zone offsets (`+00`)
|
||||
/// and four-digit time zone offsets (`+0000`).
|
||||
pub(crate) const PATTERNS_OFFSET: [(&str, usize); 9] = [
|
||||
(UTC_OFFSET, 9),
|
||||
(UTC_OFFSET, 8),
|
||||
(ZULU_OFFSET, 7),
|
||||
(UTC_OFFSET, 6),
|
||||
(ZULU_OFFSET, 6),
|
||||
(NAKED_OFFSET, 6),
|
||||
(NAKED_OFFSET, 5),
|
||||
(ZULU_OFFSET, 4),
|
||||
(NAKED_OFFSET, 3),
|
||||
];
|
||||
}
|
||||
|
||||
/// Parses a time string and returns a `DateTime` representing the
|
||||
/// absolute time of the string.
|
||||
///
|
||||
@@ -190,11 +61,14 @@ mod format {
|
||||
/// 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>(
|
||||
s: S,
|
||||
input: S,
|
||||
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
|
||||
parse_datetime_at_date(Local::now(), s)
|
||||
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.
|
||||
///
|
||||
@@ -210,11 +84,11 @@ pub fn parse_datetime<S: AsRef<str> + Clone>(
|
||||
/// use parse_datetime::parse_datetime_at_date;
|
||||
///
|
||||
/// let now = Local::now();
|
||||
/// let after = parse_datetime_at_date(now, "+3 days");
|
||||
/// let after = parse_datetime_at_date(now, "2024-09-13UTC +3 days");
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// (now + Duration::days(3)).naive_utc(),
|
||||
/// after.unwrap().naive_utc()
|
||||
/// "2024-09-16",
|
||||
/// after.unwrap().naive_utc().format("%F").to_string()
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
@@ -229,150 +103,12 @@ pub fn parse_datetime<S: AsRef<str> + Clone>(
|
||||
/// cannot be parsed as a relative time.
|
||||
pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
|
||||
date: DateTime<Local>,
|
||||
s: S,
|
||||
input: S,
|
||||
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
|
||||
// TODO: Replace with a proper customiseable parsing solution using `nom`, `grmtools`, or
|
||||
// similar
|
||||
|
||||
// Try to parse a reference date first. Try parsing from longest
|
||||
// pattern to shortest pattern. If a reference date can be parsed,
|
||||
// then try to parse a time delta from the remaining slice. If no
|
||||
// reference date could be parsed, then try to parse the entire
|
||||
// string as a time delta. If no time delta could be parsed,
|
||||
// return an error.
|
||||
let (ref_date, n) = if let Some((ref_date, n)) = parse_reference_date(date, s.as_ref()) {
|
||||
(ref_date, n)
|
||||
} else {
|
||||
let tz = TimeZone::from_offset(date.offset());
|
||||
match date.naive_local().and_local_timezone(tz) {
|
||||
MappedLocalTime::Single(ref_date) => (ref_date, 0),
|
||||
_ => return Err(ParseDateTimeError::InvalidInput),
|
||||
}
|
||||
};
|
||||
parse_relative_time_at_date(ref_date, &s.as_ref()[n..])
|
||||
}
|
||||
|
||||
/// Parse an absolute datetime from a prefix of s, if possible.
|
||||
///
|
||||
/// Try to parse the longest possible absolute datetime at the beginning
|
||||
/// of string `s`. Return the parsed datetime and the index in `s` at
|
||||
/// which the datetime ended.
|
||||
fn parse_reference_date<S>(date: DateTime<Local>, s: S) -> Option<(DateTime<FixedOffset>, usize)>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
// HACK: if the string ends with a single digit preceded by a + or -
|
||||
// sign, then insert a 0 between the sign and the digit to make it
|
||||
// possible for `chrono` to parse it.
|
||||
let pattern = Regex::new(r"([\+-])(\d)$").unwrap();
|
||||
let tmp_s = pattern.replace(s.as_ref(), "${1}0${2}");
|
||||
for (fmt, n) in format::PATTERNS_TZ {
|
||||
if tmp_s.len() >= n {
|
||||
if let Ok(parsed) = DateTime::parse_from_str(&tmp_s[0..n], fmt) {
|
||||
if tmp_s == s.as_ref() {
|
||||
return Some((parsed, n));
|
||||
}
|
||||
return Some((parsed, n - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse formats with no offset, assume local time
|
||||
for (fmt, n) in format::PATTERNS_NO_TZ {
|
||||
if s.as_ref().len() >= n {
|
||||
if let Ok(parsed) = NaiveDateTime::parse_from_str(&s.as_ref()[0..n], fmt) {
|
||||
// Special case: `chrono` can only parse a datetime like
|
||||
// `2000-01-01 01:23:45Z` as a naive datetime, so we
|
||||
// manually force it to be in UTC.
|
||||
if format::is_zulu(fmt) {
|
||||
match FixedOffset::east_opt(0)
|
||||
.unwrap()
|
||||
.from_local_datetime(&parsed)
|
||||
{
|
||||
MappedLocalTime::Single(datetime) => return Some((datetime, n)),
|
||||
_ => return None,
|
||||
}
|
||||
} else if let Ok(dt) = naive_dt_to_fixed_offset(date, parsed) {
|
||||
return Some((dt, n));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse weekday
|
||||
if let Some(weekday) = parse_weekday::parse_weekday(s.as_ref()) {
|
||||
let mut beginning_of_day = date
|
||||
.with_hour(0)
|
||||
.unwrap()
|
||||
.with_minute(0)
|
||||
.unwrap()
|
||||
.with_second(0)
|
||||
.unwrap()
|
||||
.with_nanosecond(0)
|
||||
.unwrap();
|
||||
|
||||
while beginning_of_day.weekday() != weekday {
|
||||
beginning_of_day += Duration::days(1);
|
||||
}
|
||||
|
||||
let dt = DateTime::<FixedOffset>::from(beginning_of_day);
|
||||
|
||||
return Some((dt, s.as_ref().len()));
|
||||
}
|
||||
|
||||
// Parse epoch seconds
|
||||
if let Ok(timestamp) = parse_timestamp(s.as_ref()) {
|
||||
if let Some(timestamp_date) = DateTime::from_timestamp(timestamp, 0) {
|
||||
return Some((timestamp_date.into(), s.as_ref().len()));
|
||||
}
|
||||
}
|
||||
|
||||
// Parse date only formats - assume midnight local timezone
|
||||
for (fmt, n) in format::PATTERNS_DATE_NO_TZ {
|
||||
if s.as_ref().len() >= n {
|
||||
if let Ok(parsed) = NaiveDate::parse_from_str(&s.as_ref()[0..n], fmt) {
|
||||
let datetime = parsed.and_hms_opt(0, 0, 0).unwrap();
|
||||
if let Ok(dt) = naive_dt_to_fixed_offset(date, datetime) {
|
||||
return Some((dt, n));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse offsets. chrono doesn't provide any functionality to parse
|
||||
// offsets, so instead we replicate parse_date behaviour by getting
|
||||
// the current date with local, and create a date time string at midnight,
|
||||
// before trying offset suffixes
|
||||
let ts = format!("{}0000{}", date.format("%Y%m%d"), tmp_s.as_ref());
|
||||
for (fmt, n) in format::PATTERNS_OFFSET {
|
||||
if ts.len() == n + 12 {
|
||||
let f = format::YYYYMMDDHHMM.to_owned() + fmt;
|
||||
if let Ok(parsed) = DateTime::parse_from_str(&ts, &f) {
|
||||
if tmp_s == s.as_ref() {
|
||||
return Some((parsed, n));
|
||||
}
|
||||
return Some((parsed, n - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse time only dates
|
||||
if let Some(date_time) = parse_time_only_str::parse_time_only(date, s.as_ref()) {
|
||||
return Some((date_time, s.as_ref().len()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// Convert NaiveDateTime to DateTime<FixedOffset> by assuming the offset
|
||||
// is local time
|
||||
fn naive_dt_to_fixed_offset(
|
||||
local: DateTime<Local>,
|
||||
dt: NaiveDateTime,
|
||||
) -> Result<DateTime<FixedOffset>, ()> {
|
||||
match local.offset().from_local_datetime(&dt) {
|
||||
LocalResult::Single(dt) => Ok(dt),
|
||||
_ => Err(()),
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,6 +386,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_readme_code() {
|
||||
let dt = parse_datetime("2021-02-14 06:37:47");
|
||||
|
||||
assert_eq!(
|
||||
dt.unwrap(),
|
||||
Local.with_ymd_and_hms(2021, 2, 14, 6, 37, 47).unwrap()
|
||||
@@ -664,7 +401,6 @@ mod tests {
|
||||
#[test]
|
||||
fn test_invalid_input() {
|
||||
let result = parse_datetime("foobar");
|
||||
println!("{result:?}");
|
||||
assert_eq!(result, Err(ParseDateTimeError::InvalidInput));
|
||||
|
||||
let result = parse_datetime("invalid 1");
|
||||
@@ -738,8 +474,6 @@ mod tests {
|
||||
"1997-01-01T00:00:00",
|
||||
"1997-01-01 00:00:00",
|
||||
"1997-01-01 00:00",
|
||||
"199701010000.00",
|
||||
"199701010000",
|
||||
] {
|
||||
let actual = crate::parse_datetime(s).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
@@ -802,19 +536,25 @@ mod tests {
|
||||
.fixed_offset();
|
||||
|
||||
for s in [
|
||||
"1997-01-01 00:00:00.000000000 +1 year",
|
||||
"Wed Jan 1 00:00:00 1997 +1 year",
|
||||
"1997-01-01T00:00:00 +1 year",
|
||||
"1997-01-01 00:00:00 +1 year",
|
||||
"1997-01-01 00:00 +1 year",
|
||||
"199701010000.00 +1 year",
|
||||
"199701010000 +1 year",
|
||||
"1997-01-01 00:00:00.000000000 1 year",
|
||||
"Wed Jan 1 00:00:00 1997 1 year",
|
||||
"1997-01-01T00:00:00 1 year",
|
||||
"1997-01-01 00:00:00 1 year",
|
||||
"1997-01-01 00:00 1 year",
|
||||
] {
|
||||
let actual = crate::parse_datetime(s).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_datetime_notz_delta() {
|
||||
// GNU date does not accept the following formats.
|
||||
for s in ["199701010000.00 +1 year", "199701010000 +1 year"] {
|
||||
assert!(crate::parse_datetime(s).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_date_notz_delta() {
|
||||
std::env::set_var("TZ", "UTC0");
|
||||
@@ -880,4 +620,88 @@ mod tests {
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_relative {
|
||||
|
||||
use crate::parse_datetime;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_month() {
|
||||
env::set_var("TZ", "UTC");
|
||||
|
||||
assert_eq!(
|
||||
parse_datetime("28 feb + 1 month")
|
||||
.expect("parse_datetime")
|
||||
.format("%m%d")
|
||||
.to_string(),
|
||||
"0328"
|
||||
);
|
||||
|
||||
// 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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_gnu {
|
||||
use crate::parse_datetime;
|
||||
|
||||
#[test]
|
||||
fn gnu_compat() {
|
||||
const FMT: &str = "%Y-%m-%d %H:%M:%S";
|
||||
let input = "0000-03-02 00:00:00";
|
||||
assert_eq!(
|
||||
input,
|
||||
parse_datetime(input).unwrap().format(FMT).to_string()
|
||||
);
|
||||
|
||||
let input = "2621-03-10 00:00:00";
|
||||
assert_eq!(
|
||||
input,
|
||||
parse_datetime(input).unwrap().format(FMT).to_string()
|
||||
);
|
||||
|
||||
let input = "1038-03-10 00:00:00";
|
||||
assert_eq!(
|
||||
input,
|
||||
parse_datetime(input).unwrap().format(FMT).to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user