You've already forked parse_datetime
mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46a5ef89c9 | |||
| 226d6a0588 | |||
| 28b9f11f2e | |||
| b68c325abd | |||
| 27a626d3d5 | |||
| 1630fd025e | |||
| cf5770838c | |||
| db9ff29c02 | |||
| 4cfe8db9be | |||
| 1e7a541528 | |||
| b2bb6dfe91 | |||
| d71c6ea41c | |||
| 3b6362e7c8 | |||
| 897a50f4d5 | |||
| 96f477121f | |||
| c47ce6870a | |||
| 77e1ab2d00 | |||
| a48ad6aec3 | |||
| 0ddeec70b4 | |||
| 2bb8ac8203 | |||
| 5b52929c91 | |||
| 3d6f79d826 | |||
| 324d0bca34 | |||
| 9f75f895e0 | |||
| 5ba4b18655 | |||
| 5d869b5264 | |||
| 793d18dce7 | |||
| 7182760266 | |||
| e7a8a52a21 | |||
| 0674254bc6 | |||
| 8bebfab576 | |||
| 6feeddfea1 | |||
| 6499842e15 | |||
| 949e0613d6 | |||
| 6e6ffc2dff | |||
| 67364fc6ba | |||
| d92f5eeb27 | |||
| 5c4a60fadf | |||
| 45bda33a6b | |||
| 64069cdbf2 | |||
| 4d6568b326 | |||
| f5c1a5e399 | |||
| 706ab45937 | |||
| 4d9afba2d4 | |||
| 05677a803d | |||
| 98d4931b53 | |||
| 0b5d3ffdf3 | |||
| 24fa78a927 | |||
| c0ada32ce1 | |||
| b5b1fd2b5e | |||
| 8d78de85a5 | |||
| 33ac3b75cc | |||
| 832942c019 | |||
| e031c8ce49 | |||
| 856a13ddb2 | |||
| f1f2c47120 | |||
| 2c0fa5a264 | |||
| 937572d557 | |||
| a7d9d1ab59 | |||
| 3be37c4d82 | |||
| 95897465b2 | |||
| 7260b0ae73 | |||
| 92daee09de | |||
| 73f69b1da0 | |||
| 30cb26d75c | |||
| 6e64f21967 | |||
| 9b67307512 | |||
| 76d8f17350 | |||
| c05b8217e0 | |||
| 41a53a2610 | |||
| 03cff2ce1e | |||
| 12f73c3d57 | |||
| c694587fdd | |||
| 3fe06c0b94 | |||
| 1a6a6f688a | |||
| 294bd5eeb2 | |||
| 60745300ad |
Generated
+169
-3
@@ -71,6 +71,67 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-macro",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
@@ -95,6 +156,16 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
@@ -140,14 +211,36 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "parse_datetime"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"num-traits",
|
||||
"regex",
|
||||
"rstest",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
@@ -195,18 +288,74 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "relative-path"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
|
||||
|
||||
[[package]]
|
||||
name = "rstest"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49"
|
||||
dependencies = [
|
||||
"futures-timer",
|
||||
"futures-util",
|
||||
"rstest_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstest_macros"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"glob",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"relative-path",
|
||||
"rustc_version",
|
||||
"syn",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
@@ -218,6 +367,23 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
@@ -343,9 +509,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.10"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
|
||||
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
+4
-1
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "parse_datetime"
|
||||
description = "parsing human-readable time strings and converting them to a DateTime"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/uutils/parse_datetime"
|
||||
@@ -13,3 +13,6 @@ regex = "1.10.4"
|
||||
chrono = { version="0.4.38", default-features=false, features=["std", "alloc", "clock"] }
|
||||
winnow = "0.7.10"
|
||||
num-traits = "0.2.19"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.26"
|
||||
|
||||
@@ -10,7 +10,7 @@ A Rust crate for parsing human-readable relative time strings and human-readable
|
||||
|
||||
- Parses a variety of human-readable and standard time formats.
|
||||
- Supports positive and negative durations.
|
||||
- Allows for chaining time units (e.g., "1 hour 2 minutes" or "2 days and 2 hours").
|
||||
- Allows for chaining time units (e.g., "1 hour 2 minutes" or "2 days 2 hours ago").
|
||||
- Calculate durations relative to a specified date.
|
||||
- Relies on Chrono
|
||||
|
||||
@@ -58,7 +58,6 @@ The `parse_datetime` and `parse_datetime_at_date` functions support absolute dat
|
||||
- "tomorrow"
|
||||
- use "ago" for the past
|
||||
- use "next" or "last" with `unit` (e.g., "next week", "last year")
|
||||
- combined units with "and" or "," (e.g., "2 years and 1 month", "1 day, 2 hours" or "2 weeks 1 second")
|
||||
- unix timestamps (for example "@0" "@1344000")
|
||||
|
||||
`num` can be a positive or negative integer.
|
||||
|
||||
Generated
+2
-2
@@ -138,9 +138,9 @@ checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.9"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75"
|
||||
checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, TimeZone, Timelike};
|
||||
|
||||
use super::{date, relative, time, weekday};
|
||||
|
||||
/// The builder is used to construct a DateTime object from various components.
|
||||
/// The parser creates a `DateTimeBuilder` object with the parsed components,
|
||||
/// but without the baseline date and time. So you normally need to set the base
|
||||
/// date and time using the `set_base()` method before calling `build()`, or
|
||||
/// leave it unset to use the current date and time as the base.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DateTimeBuilder {
|
||||
base: Option<DateTime<FixedOffset>>,
|
||||
timestamp: Option<f64>,
|
||||
date: Option<date::Date>,
|
||||
time: Option<time::Time>,
|
||||
weekday: Option<weekday::Weekday>,
|
||||
timezone: Option<time::Offset>,
|
||||
relative: Vec<relative::Relative>,
|
||||
}
|
||||
|
||||
impl DateTimeBuilder {
|
||||
pub(super) fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Sets the base date and time for the builder. If not set, the current
|
||||
/// date and time will be used.
|
||||
pub(super) fn set_base(mut self, base: DateTime<FixedOffset>) -> Self {
|
||||
self.base = Some(base);
|
||||
self
|
||||
}
|
||||
|
||||
/// Timestamp value is exclusive to other date/time components. Caller of
|
||||
/// the builder must ensure that it is not combined with other items.
|
||||
pub(super) fn set_timestamp(mut self, ts: f64) -> Result<Self, &'static str> {
|
||||
self.timestamp = Some(ts);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub(super) fn set_year(mut self, year: u32) -> Result<Self, &'static str> {
|
||||
if let Some(date) = self.date.as_mut() {
|
||||
if date.year.is_some() {
|
||||
Err("year cannot appear more than once")
|
||||
} else {
|
||||
date.year = Some(year);
|
||||
Ok(self)
|
||||
}
|
||||
} else {
|
||||
self.date = Some(date::Date {
|
||||
day: 1,
|
||||
month: 1,
|
||||
year: Some(year),
|
||||
});
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_date(mut self, date: date::Date) -> Result<Self, &'static str> {
|
||||
if self.date.is_some() || self.timestamp.is_some() {
|
||||
Err("date cannot appear more than once")
|
||||
} else {
|
||||
self.date = Some(date);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_time(mut self, time: time::Time) -> Result<Self, &'static str> {
|
||||
if self.time.is_some() || self.timestamp.is_some() {
|
||||
Err("time cannot appear more than once")
|
||||
} else if self.timezone.is_some() && time.offset.is_some() {
|
||||
Err("time offset and timezone are mutually exclusive")
|
||||
} else {
|
||||
self.time = Some(time);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_weekday(mut self, weekday: weekday::Weekday) -> Result<Self, &'static str> {
|
||||
if self.weekday.is_some() {
|
||||
Err("weekday cannot appear more than once")
|
||||
} else {
|
||||
self.weekday = Some(weekday);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_timezone(mut self, timezone: time::Offset) -> Result<Self, &'static str> {
|
||||
if self.timezone.is_some() {
|
||||
Err("timezone cannot appear more than once")
|
||||
} else if self.time.as_ref().and_then(|t| t.offset.as_ref()).is_some() {
|
||||
Err("time offset and timezone are mutually exclusive")
|
||||
} else {
|
||||
self.timezone = Some(timezone);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn push_relative(mut self, relative: relative::Relative) -> Self {
|
||||
self.relative.push(relative);
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn build(self) -> Option<DateTime<FixedOffset>> {
|
||||
let base = self.base.unwrap_or_else(|| chrono::Local::now().into());
|
||||
|
||||
// If any of the following items are set, we truncate the time portion
|
||||
// of the base date to zero; otherwise, we use the base date as is.
|
||||
let mut dt = if self.timestamp.is_none()
|
||||
&& self.date.is_none()
|
||||
&& self.time.is_none()
|
||||
&& self.weekday.is_none()
|
||||
&& self.timezone.is_none()
|
||||
{
|
||||
base
|
||||
} else {
|
||||
new_date(
|
||||
base.year(),
|
||||
base.month(),
|
||||
base.day(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
*base.offset(),
|
||||
)?
|
||||
};
|
||||
|
||||
if let Some(ts) = self.timestamp {
|
||||
// TODO: How to make the fract -> nanosecond conversion more precise?
|
||||
// Maybe considering using the
|
||||
// [rust_decimal](https://crates.io/crates/rust_decimal) crate?
|
||||
match chrono::Utc.timestamp_opt(ts as i64, (ts.fract() * 10f64.powi(9)).round() as u32)
|
||||
{
|
||||
chrono::MappedLocalTime::Single(t) => {
|
||||
// If the timestamp is valid, we can use it directly.
|
||||
dt = t.with_timezone(&dt.timezone());
|
||||
}
|
||||
chrono::MappedLocalTime::Ambiguous(earliest, _latest) => {
|
||||
// TODO: When there is a fold in the local time, which value
|
||||
// do we choose? For now, we use the earliest one.
|
||||
dt = earliest.with_timezone(&dt.timezone());
|
||||
}
|
||||
chrono::MappedLocalTime::None => {
|
||||
return None; // Invalid timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(date::Date { year, month, day }) = self.date {
|
||||
dt = new_date(
|
||||
year.map(|x| x as i32).unwrap_or(dt.year()),
|
||||
month,
|
||||
day,
|
||||
dt.hour(),
|
||||
dt.minute(),
|
||||
dt.second(),
|
||||
dt.nanosecond(),
|
||||
*dt.offset(),
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(time::Time {
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
ref offset,
|
||||
}) = self.time
|
||||
{
|
||||
let offset = offset
|
||||
.clone()
|
||||
.and_then(|o| chrono::FixedOffset::try_from(o).ok())
|
||||
.unwrap_or(*dt.offset());
|
||||
|
||||
dt = new_date(
|
||||
dt.year(),
|
||||
dt.month(),
|
||||
dt.day(),
|
||||
hour,
|
||||
minute,
|
||||
second as u32,
|
||||
(second.fract() * 10f64.powi(9)).round() as u32,
|
||||
offset,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(weekday::Weekday { offset, day }) = self.weekday {
|
||||
if self.time.is_none() {
|
||||
dt = new_date(dt.year(), dt.month(), dt.day(), 0, 0, 0, 0, *dt.offset())?;
|
||||
}
|
||||
|
||||
let mut offset = offset;
|
||||
let day = day.into();
|
||||
|
||||
// If the current day is not the target day, we need to adjust
|
||||
// the x value to ensure we find the correct day.
|
||||
//
|
||||
// Consider this:
|
||||
// Assuming today is Monday, next Friday is actually THIS Friday;
|
||||
// but next Monday is indeed NEXT Monday.
|
||||
if dt.weekday() != day && offset > 0 {
|
||||
offset -= 1;
|
||||
}
|
||||
|
||||
// Calculate the delta to the target day.
|
||||
//
|
||||
// Assuming today is Thursday, here are some examples:
|
||||
//
|
||||
// Example 1: last Thursday (x = -1, day = Thursday)
|
||||
// delta = (3 - 3) % 7 + (-1) * 7 = -7
|
||||
//
|
||||
// Example 2: last Monday (x = -1, day = Monday)
|
||||
// delta = (0 - 3) % 7 + (-1) * 7 = -3
|
||||
//
|
||||
// Example 3: next Monday (x = 1, day = Monday)
|
||||
// delta = (0 - 3) % 7 + (0) * 7 = 4
|
||||
// (Note that we have adjusted the x value above)
|
||||
//
|
||||
// Example 4: next Thursday (x = 1, day = Thursday)
|
||||
// delta = (3 - 3) % 7 + (1) * 7 = 7
|
||||
let delta = (day.num_days_from_monday() as i32
|
||||
- dt.weekday().num_days_from_monday() as i32)
|
||||
.rem_euclid(7)
|
||||
+ offset.checked_mul(7)?;
|
||||
|
||||
dt = if delta < 0 {
|
||||
dt.checked_sub_days(chrono::Days::new((-delta) as u64))?
|
||||
} else {
|
||||
dt.checked_add_days(chrono::Days::new(delta as u64))?
|
||||
}
|
||||
}
|
||||
|
||||
for rel in self.relative {
|
||||
// TODO: Handle potential overflows in the addition operations.
|
||||
match rel {
|
||||
relative::Relative::Years(x) => {
|
||||
dt = dt.with_year(dt.year() + x)?;
|
||||
}
|
||||
relative::Relative::Months(x) => {
|
||||
// *NOTE* This is done in this way to conform to
|
||||
// GNU behavior.
|
||||
let days = last_day_of_month(dt.year(), dt.month());
|
||||
if x >= 0 {
|
||||
dt += dt
|
||||
.date_naive()
|
||||
.checked_add_days(chrono::Days::new((days * x as u32) as u64))?
|
||||
.signed_duration_since(dt.date_naive());
|
||||
} else {
|
||||
dt += dt
|
||||
.date_naive()
|
||||
.checked_sub_days(chrono::Days::new((days * -x as u32) as u64))?
|
||||
.signed_duration_since(dt.date_naive());
|
||||
}
|
||||
}
|
||||
relative::Relative::Days(x) => dt += chrono::Duration::days(x.into()),
|
||||
relative::Relative::Hours(x) => dt += chrono::Duration::hours(x.into()),
|
||||
relative::Relative::Minutes(x) => {
|
||||
dt += chrono::Duration::try_minutes(x.into())?;
|
||||
}
|
||||
// Seconds are special because they can be given as a float.
|
||||
relative::Relative::Seconds(x) => {
|
||||
dt += chrono::Duration::try_seconds(x as i64)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(offset) = self.timezone {
|
||||
dt = with_timezone_restore(offset, dt)?;
|
||||
}
|
||||
|
||||
Some(dt)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new_date(
|
||||
year: i32,
|
||||
month: u32,
|
||||
day: u32,
|
||||
hour: u32,
|
||||
minute: u32,
|
||||
second: u32,
|
||||
nano: u32,
|
||||
offset: FixedOffset,
|
||||
) -> Option<DateTime<FixedOffset>> {
|
||||
let newdate = NaiveDate::from_ymd_opt(year, month, day)
|
||||
.and_then(|naive| naive.and_hms_nano_opt(hour, minute, second, nano))?;
|
||||
|
||||
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::try_from(offset).ok()?;
|
||||
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())?
|
||||
.with_nanosecond(copy.nanosecond())?;
|
||||
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()
|
||||
}
|
||||
@@ -12,7 +12,8 @@
|
||||
//! > 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.
|
||||
//! > support nanosecond timestamp resolution; excess precision is silently
|
||||
//! > discarded.
|
||||
use winnow::{
|
||||
combinator::{alt, trace},
|
||||
seq, ModalResult, Parser,
|
||||
@@ -22,22 +23,22 @@ use crate::items::space;
|
||||
|
||||
use super::{
|
||||
date::{self, Date},
|
||||
s,
|
||||
primitive::s,
|
||||
time::{self, Time},
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Default)]
|
||||
pub struct DateTime {
|
||||
pub(crate) struct DateTime {
|
||||
pub(crate) date: Date,
|
||||
pub(crate) time: Time,
|
||||
}
|
||||
|
||||
pub fn parse(input: &mut &str) -> ModalResult<DateTime> {
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTime> {
|
||||
seq!(DateTime {
|
||||
date: trace("date iso", alt((date::iso1, date::iso2))),
|
||||
date: trace("iso_date", 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),
|
||||
time: trace("iso_time", time::iso),
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
+423
-124
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use winnow::{combinator::preceded, ModalResult, Parser};
|
||||
|
||||
use super::primitive::{float, s};
|
||||
|
||||
/// Parse a timestamp in the form of `@1234567890`.
|
||||
pub fn parse(input: &mut &str) -> ModalResult<f64> {
|
||||
s(preceded("@", float)).parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse;
|
||||
|
||||
fn float_eq(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < f64::EPSILON
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float() {
|
||||
let mut input = "@1234567890";
|
||||
assert!(float_eq(parse(&mut input).unwrap(), 1234567890.0));
|
||||
|
||||
let mut input = "@1234567890.12345";
|
||||
assert!(float_eq(parse(&mut input).unwrap(), 1234567890.12345));
|
||||
|
||||
let mut input = "@1234567890,12345";
|
||||
assert!(float_eq(parse(&mut input).unwrap(), 1234567890.12345));
|
||||
|
||||
let mut input = "@-1234567890.12345";
|
||||
assert_eq!(parse(&mut input).unwrap(), -1234567890.12345);
|
||||
}
|
||||
}
|
||||
+362
-404
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,15 @@
|
||||
// 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},
|
||||
ascii::alpha1,
|
||||
combinator::{alt, opt},
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
pub fn ordinal(input: &mut &str) -> ModalResult<i32> {
|
||||
use super::primitive::{dec_uint, s};
|
||||
|
||||
pub(super) fn ordinal(input: &mut &str) -> ModalResult<i32> {
|
||||
alt((text_ordinal, number_ordinal)).parse_next(input)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Primitive combinators.
|
||||
|
||||
use winnow::{
|
||||
ascii::{digit1, multispace0},
|
||||
combinator::{alt, delimited, not, opt, peek, preceded, repeat, separated},
|
||||
error::{ContextError, ParserError, StrContext, StrContextValue},
|
||||
stream::AsChar,
|
||||
token::{none_of, one_of, take_while},
|
||||
Parser,
|
||||
};
|
||||
|
||||
/// Allow spaces and comments before a parser
|
||||
///
|
||||
/// Every token parser should be wrapped in this to allow spaces and comments.
|
||||
/// It is only preceding, because that allows us to check mandatory whitespace
|
||||
/// after running the parser.
|
||||
pub(super) fn s<'a, O, E>(p: impl Parser<&'a str, O, E>) -> impl Parser<&'a str, O, E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
preceded(space, p)
|
||||
}
|
||||
|
||||
/// Parse the space in-between tokens
|
||||
///
|
||||
/// You probably want to use the [`s`] combinator instead.
|
||||
pub(super) fn space<'a, E>(input: &mut &'a str) -> winnow::Result<(), E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
separated(0.., multispace0, alt((comment, ignored_hyphen_or_plus))).parse_next(input)
|
||||
}
|
||||
|
||||
/// A hyphen or plus is ignored when it is not followed by a digit
|
||||
///
|
||||
/// This includes being followed by a comment! Compare these inputs:
|
||||
/// ```txt
|
||||
/// - 12 weeks
|
||||
/// - (comment) 12 weeks
|
||||
/// ```
|
||||
/// The last comment should be ignored.
|
||||
///
|
||||
/// The plus is undocumented, but it seems to be ignored.
|
||||
fn ignored_hyphen_or_plus<'a, E>(input: &mut &'a str) -> winnow::Result<(), E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
(
|
||||
alt(('-', '+')),
|
||||
multispace0,
|
||||
peek(not(take_while(1, AsChar::is_dec_digit))),
|
||||
)
|
||||
.void()
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a comment
|
||||
///
|
||||
/// A comment is given between parentheses, which must be balanced. Any other
|
||||
/// tokens can be within the comment.
|
||||
fn comment<'a, E>(input: &mut &'a str) -> winnow::Result<(), E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
delimited(
|
||||
'(',
|
||||
repeat(0.., alt((none_of(['(', ')']).void(), comment))),
|
||||
')',
|
||||
)
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a signed decimal integer.
|
||||
///
|
||||
/// Rationale for not using `winnow::ascii::dec_int`: When upgrading winnow from
|
||||
/// 0.5 to 0.7, we discovered that `winnow::ascii::dec_int` now accepts only the
|
||||
/// following two forms:
|
||||
///
|
||||
/// - 0
|
||||
/// - [+-]?[1-9][0-9]*
|
||||
///
|
||||
/// Inputs like [+-]?0[0-9]* (e.g., `+012`) are therefore rejected. We provide a
|
||||
/// custom implementation to support such zero-prefixed integers.
|
||||
#[allow(unused)]
|
||||
pub(super) fn dec_int<'a, E>(input: &mut &'a str) -> winnow::Result<i32, E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
(opt(one_of(['+', '-'])), digit1)
|
||||
.void()
|
||||
.take()
|
||||
.verify_map(|s: &str| s.parse().ok())
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse an unsigned decimal integer.
|
||||
///
|
||||
/// See the rationale for `dec_int` for why we don't use
|
||||
/// `winnow::ascii::dec_uint`.
|
||||
pub(super) fn dec_uint<'a, E>(input: &mut &'a str) -> winnow::Result<u32, E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
digit1
|
||||
.void()
|
||||
.take()
|
||||
.verify_map(|s: &str| s.parse().ok())
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a float number.
|
||||
///
|
||||
/// Rationale for not using `winnow::ascii::float`: the `float` parser provided
|
||||
/// by winnow accepts E-notation numbers (e.g., `1.23e4`), whereas GNU date
|
||||
/// rejects such numbers. To remain compatible with GNU date, we provide a
|
||||
/// custom implementation that only accepts inputs like [+-]?[0-9]+(\.[0-9]+)?.
|
||||
pub(super) fn float<'a, E>(input: &mut &'a str) -> winnow::Result<f64, E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
(
|
||||
opt(one_of(['+', '-'])),
|
||||
digit1,
|
||||
opt(preceded(one_of(['.', ',']), digit1)),
|
||||
)
|
||||
.void()
|
||||
.take()
|
||||
.verify_map(|s: &str| s.replace(",", ".").parse().ok())
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Create a context error with a reason.
|
||||
pub(super) fn ctx_err(reason: &'static str) -> ContextError {
|
||||
let mut err = ContextError::new();
|
||||
err.push(StrContext::Expected(StrContextValue::Description(reason)));
|
||||
err
|
||||
}
|
||||
+12
-7
@@ -32,12 +32,15 @@
|
||||
//! > ‘this thursday’.
|
||||
|
||||
use winnow::{
|
||||
ascii::{alpha1, float},
|
||||
ascii::alpha1,
|
||||
combinator::{alt, opt},
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
use super::{ordinal::ordinal, s};
|
||||
use super::{
|
||||
ordinal::ordinal,
|
||||
primitive::{float, s},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Relative {
|
||||
@@ -51,13 +54,15 @@ pub enum Relative {
|
||||
}
|
||||
|
||||
impl Relative {
|
||||
// TODO: determine how to handle multiplication overflows,
|
||||
// using saturating_mul for now.
|
||||
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::Years(x) => Self::Years(n.saturating_mul(x)),
|
||||
Self::Months(x) => Self::Months(n.saturating_mul(x)),
|
||||
Self::Days(x) => Self::Days(n.saturating_mul(x)),
|
||||
Self::Hours(x) => Self::Hours(n.saturating_mul(x)),
|
||||
Self::Minutes(x) => Self::Minutes(n.saturating_mul(x)),
|
||||
Self::Seconds(x) => Self::Seconds(f64::from(n) * x),
|
||||
}
|
||||
}
|
||||
|
||||
+83
-71
@@ -41,9 +41,8 @@ use std::fmt::Display;
|
||||
|
||||
use chrono::FixedOffset;
|
||||
use winnow::{
|
||||
ascii::{digit1, float},
|
||||
combinator::{alt, opt, peek, preceded},
|
||||
error::{ContextError, ErrMode, StrContext, StrContextValue},
|
||||
error::{ContextError, ErrMode},
|
||||
seq,
|
||||
stream::AsChar,
|
||||
token::take_while,
|
||||
@@ -52,7 +51,10 @@ use winnow::{
|
||||
|
||||
use crate::ParseDateTimeError;
|
||||
|
||||
use super::{dec_uint, relative, s};
|
||||
use super::{
|
||||
primitive::{ctx_err, dec_uint, float, s},
|
||||
relative,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Default)]
|
||||
pub struct Offset {
|
||||
@@ -70,7 +72,7 @@ pub struct Time {
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
fn merge(self, offset: Offset) -> Option<Offset> {
|
||||
fn merge(self, offset: Offset) -> Offset {
|
||||
fn combine(a: u32, neg_a: bool, b: u32, neg_b: bool) -> (u32, bool) {
|
||||
if neg_a == neg_b {
|
||||
(a + b, neg_a)
|
||||
@@ -89,11 +91,11 @@ impl Offset {
|
||||
let hours = hours_minutes / 60;
|
||||
let minutes = hours_minutes % 60;
|
||||
|
||||
Some(Offset {
|
||||
Offset {
|
||||
negative,
|
||||
hours,
|
||||
minutes,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +142,7 @@ impl Display for Offset {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Suffix {
|
||||
enum Meridiem {
|
||||
Am,
|
||||
Pm,
|
||||
}
|
||||
@@ -154,7 +156,7 @@ pub fn parse(input: &mut &str) -> ModalResult<Time> {
|
||||
/// Also used by the [`combined`](super::combined) module
|
||||
pub fn iso(input: &mut &str) -> ModalResult<Time> {
|
||||
alt((
|
||||
(hour24, timezone).map(|(hour, offset)| Time {
|
||||
(hour24, timezone_num).map(|(hour, offset)| Time {
|
||||
hour,
|
||||
minute: 0,
|
||||
second: 0.0,
|
||||
@@ -165,7 +167,7 @@ pub fn iso(input: &mut &str) -> ModalResult<Time> {
|
||||
_: colon,
|
||||
minute: minute,
|
||||
second: opt(preceded(colon, second)).map(|s| s.unwrap_or(0.0)),
|
||||
offset: opt(timezone)
|
||||
offset: opt(timezone_num)
|
||||
}),
|
||||
))
|
||||
.parse_next(input)
|
||||
@@ -175,30 +177,35 @@ pub fn iso(input: &mut &str) -> ModalResult<Time> {
|
||||
///
|
||||
/// The hours are restricted to 12 or lower in this format
|
||||
fn am_pm_time(input: &mut &str) -> ModalResult<Time> {
|
||||
seq!(
|
||||
let (h, m, s, meridiem) = (
|
||||
hour12,
|
||||
opt(preceded(colon, minute)),
|
||||
opt(preceded(colon, second)),
|
||||
alt((
|
||||
s("am").value(Suffix::Am),
|
||||
s("a.m.").value(Suffix::Am),
|
||||
s("pm").value(Suffix::Pm),
|
||||
s("p.m.").value(Suffix::Pm)
|
||||
s("am").value(Meridiem::Am),
|
||||
s("a.m.").value(Meridiem::Am),
|
||||
s("pm").value(Meridiem::Pm),
|
||||
s("p.m.").value(Meridiem::Pm),
|
||||
)),
|
||||
)
|
||||
.map(|(h, m, s, suffix)| {
|
||||
let mut h = h % 12;
|
||||
if let Suffix::Pm = suffix {
|
||||
h += 12;
|
||||
}
|
||||
Time {
|
||||
hour: h,
|
||||
minute: m.unwrap_or(0),
|
||||
second: s.unwrap_or(0.0),
|
||||
offset: None,
|
||||
}
|
||||
.parse_next(input)?;
|
||||
|
||||
if h == 0 {
|
||||
return Err(ErrMode::Cut(ctx_err(
|
||||
"hour must be greater than 0 when meridiem is specified",
|
||||
)));
|
||||
}
|
||||
|
||||
let mut h = h % 12;
|
||||
if let Meridiem::Pm = meridiem {
|
||||
h += 12;
|
||||
}
|
||||
Ok(Time {
|
||||
hour: h,
|
||||
minute: m.unwrap_or(0),
|
||||
second: s.unwrap_or(0.0),
|
||||
offset: None,
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a colon preceded by whitespace
|
||||
@@ -223,11 +230,18 @@ fn minute(input: &mut &str) -> ModalResult<u32> {
|
||||
|
||||
/// Parse a number of seconds (preceded by whitespace)
|
||||
fn second(input: &mut &str) -> ModalResult<f64> {
|
||||
s(float).verify(|x| *x < 60.0).parse_next(input)
|
||||
s(float)
|
||||
.verify(|x| *x < 60.0)
|
||||
.map(|x| {
|
||||
// Truncates the fractional part of seconds to 9 digits.
|
||||
let factor = 10f64.powi(9);
|
||||
(x * factor).trunc() / factor
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
pub(crate) fn timezone(input: &mut &str) -> ModalResult<Offset> {
|
||||
alt((timezone_num, timezone_name_offset)).parse_next(input)
|
||||
timezone_name_offset.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a timezone starting with `+` or `-`
|
||||
@@ -245,19 +259,11 @@ fn timezone_num(input: &mut &str) -> ModalResult<Offset> {
|
||||
.parse_next(input)
|
||||
.and_then(|(negative, (hours, minutes))| {
|
||||
if !(0..=12).contains(&hours) {
|
||||
let mut ctx_err = ContextError::new();
|
||||
ctx_err.push(StrContext::Expected(StrContextValue::Description(
|
||||
"timezone hour between 0 and 12",
|
||||
)));
|
||||
return Err(ErrMode::Cut(ctx_err));
|
||||
return Err(ErrMode::Cut(ctx_err("timezone hour between 0 and 12")));
|
||||
}
|
||||
|
||||
if !(0..=60).contains(&minutes) {
|
||||
let mut ctx_err = ContextError::new();
|
||||
ctx_err.push(StrContext::Expected(StrContextValue::Description(
|
||||
"timezone minute between 0 and 60",
|
||||
)));
|
||||
return Err(ErrMode::Cut(ctx_err));
|
||||
return Err(ErrMode::Cut(ctx_err("timezone minute between 0 and 60")));
|
||||
}
|
||||
|
||||
Ok(Offset {
|
||||
@@ -271,14 +277,7 @@ fn timezone_num(input: &mut &str) -> ModalResult<Offset> {
|
||||
/// Parse a timezone offset with a colon separating hours and minutes
|
||||
fn timezone_colon(input: &mut &str) -> ModalResult<(u32, u32)> {
|
||||
seq!(
|
||||
s(take_while(1..=2, AsChar::is_dec_digit)).try_map(|x: &str| {
|
||||
// parse will fail on empty input
|
||||
if x.is_empty() {
|
||||
Ok(0)
|
||||
} else {
|
||||
x.parse()
|
||||
}
|
||||
}),
|
||||
s(take_while(1..=2, AsChar::is_dec_digit)).try_map(|x: &str| x.parse()),
|
||||
_: colon,
|
||||
s(take_while(1..=2, AsChar::is_dec_digit)).try_map(|x: &str| x.parse()),
|
||||
)
|
||||
@@ -287,26 +286,21 @@ fn timezone_colon(input: &mut &str) -> ModalResult<(u32, u32)> {
|
||||
|
||||
/// Parse a timezone offset without colon
|
||||
fn timezone_colonless(input: &mut &str) -> ModalResult<(u32, u32)> {
|
||||
if let Ok(x) = peek(s(digit1::<&str, ContextError>)).parse_next(input) {
|
||||
if x.len() > 4 {
|
||||
let mut ctx_err = ContextError::new();
|
||||
ctx_err.push(StrContext::Expected(StrContextValue::Description(
|
||||
"timezone offset cannot be more than 4 digits",
|
||||
)));
|
||||
return Err(ErrMode::Cut(ctx_err));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: GNU date supports number strings with leading zeroes, e.g.,
|
||||
// `UTC+000001100` is valid.
|
||||
s(take_while(0..=4, AsChar::is_dec_digit))
|
||||
.verify_map(|x: &str| {
|
||||
Some(match x.len() {
|
||||
s(take_while(0.., AsChar::is_dec_digit))
|
||||
.verify_map(|s: &str| {
|
||||
// GNU date supports number strings with leading zeroes, e.g.,
|
||||
// `UTC+000001100` is valid.
|
||||
let s = if s.len() > 4 {
|
||||
s.trim_start_matches('0')
|
||||
} else {
|
||||
s
|
||||
};
|
||||
Some(match s.len() {
|
||||
0 => (0, 0),
|
||||
1 | 2 => (x.parse().ok()?, 0),
|
||||
1 | 2 => (s.parse().ok()?, 0),
|
||||
// The minutes are the last two characters here, for some reason.
|
||||
3 => (x[..1].parse().ok()?, x[1..].parse().ok()?),
|
||||
4 => (x[..2].parse().ok()?, x[2..].parse().ok()?),
|
||||
3 => (s[..1].parse().ok()?, s[1..].parse().ok()?),
|
||||
4 => (s[..2].parse().ok()?, s[2..].parse().ok()?),
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
@@ -328,9 +322,7 @@ fn timezone_name_offset(input: &mut &str) -> ModalResult<Offset> {
|
||||
// Only process if the input cannot be parsed as a relative time.
|
||||
if peek(relative::parse).parse_next(input).is_err() {
|
||||
if let Ok(other_tz) = timezone_num.parse_next(input) {
|
||||
let newtz = tz
|
||||
.merge(other_tz)
|
||||
.ok_or(ErrMode::Cut(ContextError::new()))?;
|
||||
let newtz = tz.merge(other_tz);
|
||||
|
||||
return Ok(newtz);
|
||||
};
|
||||
@@ -623,6 +615,12 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid() {
|
||||
assert!(parse(&mut "00:00am").is_err());
|
||||
assert!(parse(&mut "00:00:00am").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hours_only() {
|
||||
let reference = Time {
|
||||
@@ -829,6 +827,8 @@ mod tests {
|
||||
|
||||
assert_eq!("(0, 0)", aux(&mut "0000"));
|
||||
assert_eq!("(12, 34)", aux(&mut "1234"));
|
||||
assert_eq!("(12, 34)", aux(&mut "00001234"));
|
||||
assert!(timezone_colonless(&mut "12345").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -837,17 +837,29 @@ mod tests {
|
||||
let make_timezone = |input: &mut &str| {
|
||||
timezone(input)
|
||||
.map_err(|e| eprintln!("TEST FAILED AT:\n{e}"))
|
||||
.map(|offset| format!("{}", offset))
|
||||
.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"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timezone_num() {
|
||||
use super::timezone_num;
|
||||
let make_timezone = |input: &mut &str| {
|
||||
timezone_num(input)
|
||||
.map_err(|e| eprintln!("TEST FAILED AT:\n{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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use winnow::ModalResult;
|
||||
|
||||
use super::time;
|
||||
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<time::Offset> {
|
||||
time::timezone(input)
|
||||
}
|
||||
+24
-5
@@ -21,11 +21,15 @@
|
||||
//! >
|
||||
//! > A comma following a day of the week item is ignored.
|
||||
|
||||
use winnow::{ascii::alpha1, combinator::opt, seq, ModalResult, Parser};
|
||||
use winnow::{
|
||||
ascii::alpha1,
|
||||
combinator::{opt, terminated},
|
||||
seq, ModalResult, Parser,
|
||||
};
|
||||
|
||||
use super::{ordinal::ordinal, s};
|
||||
use super::{ordinal::ordinal, primitive::s};
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub(crate) enum Day {
|
||||
Monday,
|
||||
Tuesday,
|
||||
@@ -55,10 +59,12 @@ impl From<Day> for chrono::Weekday {
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn parse(input: &mut &str) -> ModalResult<Weekday> {
|
||||
|
||||
/// Parse a weekday item.
|
||||
pub(super) fn parse(input: &mut &str) -> ModalResult<Weekday> {
|
||||
seq!(Weekday {
|
||||
offset: opt(ordinal).map(|o| o.unwrap_or_default()),
|
||||
day: day,
|
||||
day: terminated(day, opt(s(","))),
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
@@ -134,4 +140,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_comma() {
|
||||
for mut s in ["monday,", "mon,", "mon.,", "mon. ,"] {
|
||||
assert_eq!(
|
||||
parse(&mut s).unwrap(),
|
||||
Weekday {
|
||||
offset: 0,
|
||||
day: Day::Monday,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Parse a year from a string.
|
||||
//!
|
||||
//! The year must be parsed to a string first, this is to handle a specific GNU
|
||||
//! compatibility quirk. According to the GNU documentation: "if the year is 68
|
||||
//! or smaller, then 2000 is added to it; otherwise, if year is less than 100,
|
||||
//! then 1900 is added to it." This adjustment only applies to two-digit year
|
||||
//! strings. For example, `"00"` is interpreted as `2000`, whereas `"0"`,
|
||||
//! `"000"`, or `"0000"` are interpreted as `0`.
|
||||
|
||||
use winnow::{error::ErrMode, stream::AsChar, token::take_while, ModalResult, Parser};
|
||||
|
||||
use super::primitive::{ctx_err, s};
|
||||
|
||||
pub(super) fn parse(input: &mut &str) -> ModalResult<u32> {
|
||||
year_from_str(year_str(input)?).map_err(|e| ErrMode::Cut(ctx_err(e)))
|
||||
}
|
||||
|
||||
// TODO: Leverage `TryFrom` trait.
|
||||
pub(super) fn year_from_str(year_str: &str) -> Result<u32, &'static str> {
|
||||
let mut year = year_str
|
||||
.parse::<u32>()
|
||||
.map_err(|_| "year must be a valid number")?;
|
||||
|
||||
// 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.
|
||||
//
|
||||
// GNU quirk: this only applies to two-digit years. For example,
|
||||
// "98-01-01" will be parsed as "1998-01-01", whereas "098-01-01" will be
|
||||
// parsed as "0098-01-01".
|
||||
if year_str.len() == 2 {
|
||||
if year <= 68 {
|
||||
year += 2000
|
||||
} else {
|
||||
year += 1900
|
||||
}
|
||||
}
|
||||
|
||||
// 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].
|
||||
//
|
||||
// See discussion in https://github.com/uutils/parse_datetime/issues/160.
|
||||
if year > 9999 {
|
||||
return Err("year must be no greater than 9999");
|
||||
}
|
||||
|
||||
Ok(year)
|
||||
}
|
||||
|
||||
pub(super) fn year_str<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
|
||||
s(take_while(1.., AsChar::is_dec_digit)).parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse;
|
||||
|
||||
#[test]
|
||||
fn test_year() {
|
||||
// 2-characters are converted to 19XX/20XX
|
||||
assert_eq!(parse(&mut "10").unwrap(), 2010u32);
|
||||
assert_eq!(parse(&mut "68").unwrap(), 2068u32);
|
||||
assert_eq!(parse(&mut "69").unwrap(), 1969u32);
|
||||
assert_eq!(parse(&mut "99").unwrap(), 1999u32);
|
||||
|
||||
// 3,4-characters are converted verbatim
|
||||
assert_eq!(parse(&mut "468").unwrap(), 468u32);
|
||||
assert_eq!(parse(&mut "469").unwrap(), 469u32);
|
||||
assert_eq!(parse(&mut "1568").unwrap(), 1568u32);
|
||||
assert_eq!(parse(&mut "1569").unwrap(), 1569u32);
|
||||
|
||||
// years greater than 9999 are not accepted
|
||||
assert!(parse(&mut "10000").is_err());
|
||||
}
|
||||
}
|
||||
+7
-50
@@ -165,7 +165,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn invalid_formats() {
|
||||
let invalid_dts = vec!["NotADate", "202104", "202104-12T22:37:47"];
|
||||
let invalid_dts = vec![
|
||||
"NotADate",
|
||||
"202104",
|
||||
"202104-12T22:37:47",
|
||||
"a774e26sec", // 774e26 is not a valid seconds value (we don't accept E-notation)
|
||||
"12.", // Invalid floating point number
|
||||
];
|
||||
for dt in invalid_dts {
|
||||
assert_eq!(parse_datetime(dt), Err(ParseDateTimeError::InvalidInput));
|
||||
}
|
||||
@@ -216,7 +222,6 @@ mod tests {
|
||||
use chrono::{Local, NaiveDate};
|
||||
|
||||
use crate::parse_datetime;
|
||||
use crate::ParseDateTimeError;
|
||||
|
||||
#[test]
|
||||
fn test_positive_offsets() {
|
||||
@@ -227,8 +232,6 @@ mod tests {
|
||||
"Z+07:00",
|
||||
"Z+0700",
|
||||
"Z+07",
|
||||
"+07",
|
||||
"+7",
|
||||
];
|
||||
|
||||
let expected = format!("{}{}", Local::now().format("%Y%m%d"), "0000+0700");
|
||||
@@ -248,15 +251,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_offset_format() {
|
||||
let offset = "UTC+01005";
|
||||
assert_eq!(
|
||||
parse_datetime(offset),
|
||||
Err(ParseDateTimeError::InvalidInput)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datetime_with_offset() {
|
||||
let actual = parse_datetime("1997-01-19 08:17:48 +2").unwrap();
|
||||
@@ -383,21 +377,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod timeonly {
|
||||
use crate::parse_datetime_at_date;
|
||||
use chrono::{Local, TimeZone};
|
||||
use std::env;
|
||||
#[test]
|
||||
fn test_time_only() {
|
||||
env::set_var("TZ", "UTC");
|
||||
let test_date = Local.with_ymd_and_hms(2024, 3, 3, 0, 0, 0).unwrap();
|
||||
let parsed_time = parse_datetime_at_date(test_date, "9:04:30 PM +0530")
|
||||
.unwrap()
|
||||
.timestamp();
|
||||
assert_eq!(parsed_time, 1709480070);
|
||||
}
|
||||
}
|
||||
/// Used to test example code presented in the README.
|
||||
mod readme_test {
|
||||
use crate::parse_datetime;
|
||||
@@ -463,9 +442,6 @@ mod tests {
|
||||
for s in [
|
||||
"1997-01-01 00:00:00 +0000",
|
||||
"1997-01-01 00:00:00 +00",
|
||||
"199701010000 +0000",
|
||||
"199701010000UTC+0000",
|
||||
"199701010000Z+0000",
|
||||
"1997-01-01 00:00 +0000",
|
||||
"1997-01-01 00:00:00 +0000",
|
||||
"1997-01-01T00:00:00+0000",
|
||||
@@ -531,9 +507,6 @@ mod tests {
|
||||
for s in [
|
||||
"1997-01-01 00:00:00 +0000 +1 year",
|
||||
"1997-01-01 00:00:00 +00 +1 year",
|
||||
"199701010000 +0000 +1 year",
|
||||
"199701010000UTC+0000 +1 year",
|
||||
"199701010000Z+0000 +1 year",
|
||||
"1997-01-01T00:00:00Z +1 year",
|
||||
"1997-01-01 00:00 +0000 +1 year",
|
||||
"1997-01-01 00:00:00 +0000 +1 year",
|
||||
@@ -596,22 +569,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_only() {
|
||||
use chrono::{FixedOffset, Local};
|
||||
std::env::set_var("TZ", "UTC");
|
||||
|
||||
let offset = FixedOffset::east_opt(5 * 60 * 60 + 1800).unwrap();
|
||||
let expected = Local::now()
|
||||
.date_naive()
|
||||
.and_hms_opt(21, 4, 30)
|
||||
.unwrap()
|
||||
.and_local_timezone(offset)
|
||||
.unwrap();
|
||||
let actual = crate::parse_datetime("9:04:30 PM +0530").unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weekday_only() {
|
||||
use chrono::{Datelike, Days, Local, MappedLocalTime, NaiveTime, Weekday};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use std::env;
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use parse_datetime::{parse_datetime, parse_datetime_at_date};
|
||||
|
||||
pub fn check_absolute(input: &str, expected: &str) {
|
||||
env::set_var("TZ", "UTC0");
|
||||
|
||||
let parsed = match parse_datetime(input) {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("Failed to parse date from value '{input}': {e}"),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
&parsed.to_rfc3339().replace("T", " "),
|
||||
expected,
|
||||
"Input value: {input}"
|
||||
);
|
||||
}
|
||||
|
||||
pub fn check_relative(now: DateTime<FixedOffset>, input: &str, expected: &str) {
|
||||
env::set_var("TZ", "UTC0");
|
||||
|
||||
let parsed = match parse_datetime_at_date(now.into(), input) {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("Failed to parse date from value '{input}': {e}"),
|
||||
};
|
||||
let expected_parsed = DateTime::parse_from_rfc3339(expected).unwrap();
|
||||
assert_eq!(parsed, expected_parsed, "Input value: {input}");
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use rstest::rstest;
|
||||
|
||||
mod common;
|
||||
use common::check_absolute;
|
||||
|
||||
// The expected values are produced by GNU date version 8.32
|
||||
// export LC_TIME=en_US.UTF-8
|
||||
// export TZ=UTC
|
||||
// date --rfc-3339=seconds --date="2022-11-14"
|
||||
//
|
||||
// Documentation for the date format can be found at:
|
||||
// https://www.gnu.org/software/coreutils/manual/html_node/Calendar-date-items.html
|
||||
|
||||
#[rstest]
|
||||
#[case::iso8601("2022-11-14", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::short_year_22("22-11-14", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::short_year_68("68-11-14", "2068-11-14 00:00:00+00:00")]
|
||||
#[case::short_year_00("00-11-14", "2000-11-14 00:00:00+00:00")]
|
||||
#[case::short_year_69("69-11-14", "1969-11-14 00:00:00+00:00")]
|
||||
#[case::short_year_99("99-11-14", "1999-11-14 00:00:00+00:00")]
|
||||
#[case::us_style("11/14/2022", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::us_style_short_year("11/14/22", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::year_zero("0000-01-01", "0000-01-01 00:00:00+00:00")]
|
||||
#[case::year_001("001-11-14", "0001-11-14 00:00:00+00:00")]
|
||||
#[case::year_100("100-11-14", "0100-11-14 00:00:00+00:00")]
|
||||
#[case::year_999("999-11-14", "0999-11-14 00:00:00+00:00")]
|
||||
#[case::year_9999("9999-11-14", "9999-11-14 00:00:00+00:00")]
|
||||
/** TODO: https://github.com/uutils/parse_datetime/issues/160
|
||||
#[case::year_10000("10000-12-31", "10000-12-31 00:00:00+00:00")]
|
||||
#[case::year_100000("100000-12-31", "100000-12-31 00:00:00+00:00")]
|
||||
#[case::year_1000000("1000000-12-31", "1000000-12-31 00:00:00+00:00")]
|
||||
#[case::year_10000000("10000000-12-31", "10000000-12-31 00:00:00+00:00")]
|
||||
#[case::max_date("2147485547-12-31", "2147485547-12-31 00:00:00+00:00")]
|
||||
**/
|
||||
#[case::long_month_in_the_middle("14 November 2022", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_lowercase("14 november 2022", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_uppercase("14 NOVEMBER 2022", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle("14 nov 2022", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_uppercase("14 NOV 2022", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_hyphened("14-november-2022", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_nospace("14november2022", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_hyphened("14-nov-2022", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_nospace("14nov2022", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::long_month_at_start("November 14 2022", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::long_month_at_start_with_comma("November 14, 2022", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::short_month_at_start("nov 14 2022", "2022-11-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_jan("14 January 2022", "2022-01-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_feb("14 February 2022", "2022-02-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_mar("14 March 2022", "2022-03-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_apr("14 April 2022", "2022-04-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_may("14 May 2022", "2022-05-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_jun("14 June 2022", "2022-06-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_jul("14 July 2022", "2022-07-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_aug("14 August 2022", "2022-08-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_sep("14 September 2022", "2022-09-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_oct("14 October 2022", "2022-10-14 00:00:00+00:00")]
|
||||
#[case::long_month_in_the_middle_dec("14 December 2022", "2022-12-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_jan("14 jan 2022", "2022-01-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_feb("14 feb 2022", "2022-02-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_mar("14 mar 2022", "2022-03-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_apr("14 apr 2022", "2022-04-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_may("14 may 2022", "2022-05-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_jun("14 jun 2022", "2022-06-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_jul("14 jul 2022", "2022-07-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_aug("14 aug 2022", "2022-08-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_sep("14 sep 2022", "2022-09-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_sept("14 sept 2022", "2022-09-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_oct("14 oct 2022", "2022-10-14 00:00:00+00:00")]
|
||||
#[case::short_month_in_the_middle_dec("14 dec 2022", "2022-12-14 00:00:00+00:00")]
|
||||
fn test_absolute_date_numeric(#[case] input: &str, #[case] expected: &str) {
|
||||
check_absolute(input, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::us_style("11/14", 2022, "2022-11-14 00:00:00+00:00")]
|
||||
#[case::alphabetical_full_month_in_front("november 14", 2022, "2022-11-14 00:00:00+00:00")]
|
||||
#[case::alphabetical_full_month_at_back("14 november", 2022, "2022-11-14 00:00:00+00:00")]
|
||||
#[case::alphabetical_short_month_in_front("nov 14", 2022, "2022-11-14 00:00:00+00:00")]
|
||||
#[case::alphabetical_short_month_at_back("14 nov", 2022, "2022-11-14 00:00:00+00:00")]
|
||||
#[case::alphabetical_full_month_in_front("november 14", 2022, "2022-11-14 00:00:00+00:00")]
|
||||
#[case::alphabetical_full_month_at_back("14 november", 2022, "2022-11-14 00:00:00+00:00")]
|
||||
#[case::alphabetical_short_month_in_front("nov 14", 2022, "2022-11-14 00:00:00+00:00")]
|
||||
#[case::alphabetical_short_month_at_back("14 nov", 2022, "2022-11-14 00:00:00+00:00")]
|
||||
#[case::alphabetical_long_month_at_back_hyphen("14-november", 2022, "2022-11-14 00:00:00+00:00")]
|
||||
#[case::alphabetical_short_month_at_back_hyphen("14-nov", 2022, "2022-11-14 00:00:00+00:00")]
|
||||
fn test_date_omitting_year(#[case] input: &str, #[case] year: u32, #[case] expected: &str) {
|
||||
use chrono::DateTime;
|
||||
use common::check_relative;
|
||||
|
||||
let now = DateTime::parse_from_rfc3339(&format!("{year}-06-01T00:00:00+00:00")).unwrap();
|
||||
check_relative(now, input, expected);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user