51 Commits

Author SHA1 Message Date
Daniel Hofstetter b9630d4bd3 Merge pull request #128 from dan-hipschman/parse-relative-weekdays
Parse relative weekdays like "next monday"
2025-04-25 09:24:49 +02:00
Dan Hipschman 65fb9140d2 Parse relative weekdays like "next monday" 2025-04-22 10:00:13 -07:00
Sylvestre Ledru 9b22a18bce Merge pull request #130 from cakebaker/fix_some_clippy_pedantic_issues
clippy: fix some warnings from clippy::pedantic
2025-04-22 10:42:20 +02:00
Daniel Hofstetter 28b788a300 clippy: fix warnings from redundant_else lint 2025-04-22 10:34:39 +02:00
Daniel Hofstetter f6b6b3e4ea clippy: fix warning from manual_let_else lint 2025-04-22 10:31:02 +02:00
Daniel Hofstetter 1a93bb4e3e clippy: fix warning from needless_continue lint 2025-04-22 10:27:13 +02:00
Daniel Hofstetter 9fc966308e clippy: remove unnecessary semicolon 2025-04-22 10:12:20 +02:00
Sylvestre Ledru 7119769488 Merge pull request #121 from cakebaker/rename_const
Rename `TWELVEHOUR` to `TWELVE_HOUR`
2025-04-22 09:56:39 +02:00
Daniel Hofstetter 46a82b70b3 Rename TWELVEHOUR to TWELVE_HOUR 2025-04-22 09:52:19 +02:00
Dan Hipschman e7ed14e8a4 Allow spaces between +/- and number (#129)
* Allow spaces between +/- and number

* Remove space to make fmt job pass

---------

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>
2025-04-22 09:44:09 +02:00
Daniel Hofstetter 9184428407 Merge pull request #126 from uutils/renovate/chrono-0.x-lockfile
Update Rust crate chrono to v0.4.40
2025-02-26 13:44:20 +01:00
renovate[bot] 25825df8b7 Update Rust crate chrono to v0.4.40 2025-02-26 11:56:48 +00:00
Sylvestre Ledru 0dbce58014 Merge pull request #120 from cakebaker/readme_fix_sentence
README.md: fix incorrect sentence
2025-02-17 20:31:53 +01:00
Daniel Hofstetter 0c0a054f70 README.md: fix incorrect sentence 2025-02-17 14:51:34 +01:00
Sylvestre Ledru 52dca03c56 README.md: adjust the doc to avoid the hardcoded version 2025-02-17 11:24:36 +01:00
Daniel Hofstetter d4384ceec9 Merge pull request #119 from sylvestre/new-release
Prepare the new relase + clippy
2025-02-17 08:58:27 +01:00
Sylvestre Ledru 46cc4147c7 fix clippy pedantic 2025-02-16 21:52:29 +01:00
Sylvestre Ledru 93e1c9bfe7 prepare 0.8.0 2025-02-16 21:32:13 +01:00
Daniel Hofstetter e7a7a0b07f Merge pull request #110 from jfinkels/parse-ref-date-and-delta
Parse reference datetime and timedelta from the same string
2025-02-16 17:20:29 +01:00
jfinkels d4353d0888 Use str.ends_with for patterns ending in Z
Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>
2025-02-16 11:16:23 -05:00
Jeffrey Finkelstein 8aee979de5 Parse reference date and delta from same string
Change the `parse_datetime()` function so that it parses both a
reference date and a time delta from one string. The new implementation
attempts to parse the datetime from the longest possible prefix of the
string. The remainder of the string is parsed as the time delta. This
allows us to parse more combinations of reference dates and time deltas
more easily.

Fixes #104
2025-02-14 22:00:09 -05:00
Jeffrey Finkelstein f1fada62a6 Add additional pattern for datetime w/o timezone 2025-02-14 22:00:09 -05:00
Daniel Hofstetter ca6a39c0a7 Merge pull request #117 from jfinkels/add-time-to-date-after-parsing
Add time to parsed date after parsing not before
2025-02-14 10:10:14 +01:00
Jeffrey Finkelstein db18db4e1d Add time to parsed date after parsing not before
Change the way dates without times are parsed by just parsing the date
as-is and applying the placeholder time (00:00:00) after parsing
instead of before.
2025-02-13 18:48:47 -05:00
Sylvestre Ledru c337aba9cb Merge pull request #116 from jfinkels/remove-add-zero-months
Remove more useless calls to add zero months to date
2025-02-13 07:55:18 +01:00
Sylvestre Ledru 63cfc01e80 Merge pull request #111 from cakebaker/fuzz_remove_rand_dependency
fuzz: remove `rand` dependency
2025-02-13 07:54:53 +01:00
Sylvestre Ledru 1daaeb42a2 Merge pull request #118 from jfinkels/add-more-tests
Add a lot more unit tests
2025-02-13 07:54:36 +01:00
Jeffrey Finkelstein 3fcbac6fae Add a lot more unit tests
Add more unit tests for `parse_datetime()` when given an input without
a time delta (like "+1 year" or similar).
2025-02-12 22:04:09 -05:00
Jeffrey Finkelstein 65cd15dba9 Remove useless call to add zero months to date 2025-02-12 21:49:23 -05:00
Daniel Hofstetter 65c88f3586 Merge pull request #115 from jfinkels/patterns-with-length
Greedy parsing of datetime before time delta
2025-02-12 07:15:23 +01:00
jfinkels 2c55db21fa Avoid unnecessary slicing when parsing offset
Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>
2025-02-11 20:05:55 -05:00
Jeffrey Finkelstein badc887606 Greedy parsing of datetime before time delta
Simulate greedy parsing of an absolute datetime in the prefix of a
string before parsing a subsequent time delta in the suffix of the
string. This does not change the behavior of `parse_datetime`, it just
prepares the code for a future change that allows parsing both the
absolute datetime and the time delta from the same string.

Greedy parsing is implemented by iterating over a list of patterns in
decreasing order of length so that longer patterns are tried before
shorter patterns. This guarantees that if there is an absolute datetime
present at the beginning of the string, then it will definitely be
parsed and the remaining part of the string is assumed to contain a time
delta.
2025-02-09 17:31:25 -05:00
Jeffrey Finkelstein f8adcb6c88 Add passing test for parsing datetime ending in Z
Add a passing unit test for parsing a datetime ending in the letter Z,
as in

    2023-06-03 12:00:01Z

This is treated as a datetime in the UTC time zone.
2025-02-09 17:31:25 -05:00
Daniel Hofstetter d0dceb6eaf Merge pull request #114 from jfinkels/parse-empty-string
Allow empty string in parse_relative_time_at_date
2025-02-09 16:18:43 +01:00
Jeffrey Finkelstein 45b9d9f09a Allow empty string in parse_relative_time_at_date
Allow the empty string when parsing the relative time in
`parse_relative_time_at_date()`. The empty string represents zero time
delta.
2025-02-08 15:05:50 -05:00
Daniel Hofstetter 0965130c1b fuzz: remove rand dependency 2025-02-08 20:59:19 +01:00
Sylvestre Ledru 51ddcb793f Merge pull request #113 from jfinkels/set-utc-tz-in-test
Explicitly set timezone UTC0 in a unit test
2025-02-08 20:59:03 +01:00
Sylvestre Ledru 7985aa04ba Merge pull request #112 from jfinkels/fix-add-months-tests
Make tests of adding months more resilient
2025-02-08 20:58:39 +01:00
Jeffrey Finkelstein 5c0c9c4405 Explicitly set timezone UTC0 in a unit test 2025-02-08 14:48:06 -05:00
Jeffrey Finkelstein 8dd8051eb7 Make tests of adding months more resilient
Protect against a situation where adding one month to the current day
would cause an overflow in the next month in some unit tests of
`parse_relative_time`. For example, adding one month to March 31
should overflow to May 1 because April only has 30 days. This is a
difference in the behavior we want for our library compared with the
behavior of `chrono`.
2025-02-08 14:42:38 -05:00
Daniel Hofstetter 073b9f7b70 Merge pull request #109 from uutils/renovate/libfuzzer-sys-0.x-lockfile
fix(deps): update rust crate libfuzzer-sys to v0.4.9
2025-01-29 07:16:20 +01:00
renovate[bot] 0318dabe3a fix(deps): update rust crate libfuzzer-sys to v0.4.9 2025-01-28 18:33:49 +00:00
Sylvestre Ledru 57fe123e21 Merge pull request #107 from cakebaker/bump_nom
Bump `nom` & adapt to API changes
2025-01-27 11:14:31 +01:00
Daniel Hofstetter 2fe83cabf1 Adapt to API changes in nom 8.0.0 2025-01-27 10:53:08 +01:00
Daniel Hofstetter 94d4112707 Bump nom from 7.1.3 to 8.0.0 2025-01-27 10:48:23 +01:00
Sylvestre Ledru eda2422bb3 Merge pull request #103 from jfinkels/parse-datetime-with-single-digit-offset-t-sep
Parse datetime with T separator and single digit offset
2025-01-19 23:07:12 +01:00
Jeffrey Finkelstein c948274c42 Parse datetime with T separator and digit offset
Parse a datetime with T separator between date and time, single digit
timezone offset, and no space between time and offset.
2025-01-19 16:38:06 -05:00
Sylvestre Ledru b209341259 Merge pull request #105 from sylvestre/0.7.0
prepare new release 0.7.0
2025-01-19 22:29:25 +01:00
Sylvestre Ledru 9cd22afa29 prepare new release 0.7.0 2025-01-19 22:25:25 +01:00
Sylvestre Ledru a7061bce12 Merge pull request #102 from jfinkels/parse-datetime-with-single-digit-offset
Parse datetimes with single digit timezone offset
2025-01-19 22:25:10 +01:00
Jeffrey Finkelstein ea49dc94f0 Parse datetimes with single digit timezone offset
Parse full datetimes with single digit timezone offsets like +3 or -5.
2025-01-18 18:45:22 -05:00
10 changed files with 742 additions and 230 deletions
Generated
+12 -13
View File
@@ -52,14 +52,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.39"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-targets",
"windows-link",
]
[[package]]
@@ -118,20 +118,13 @@ version = "2.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
@@ -151,7 +144,7 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "parse_datetime"
version = "0.6.0"
version = "0.9.0"
dependencies = [
"chrono",
"nom",
@@ -286,6 +279,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "windows-link"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-targets"
version = "0.52.5"
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "parse_datetime"
description = "parsing human-readable time strings and converting them to a DateTime"
version = "0.6.0"
version = "0.9.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/uutils/parse_datetime"
@@ -10,4 +10,4 @@ readme = "README.md"
[dependencies]
regex = "1.10.4"
chrono = { version="0.4.38", default-features=false, features=["std", "alloc", "clock"] }
nom = "7.1.3"
nom = "8.0.0"
+3 -4
View File
@@ -16,11 +16,10 @@ A Rust crate for parsing human-readable relative time strings and human-readable
## Usage
Add this to your `Cargo.toml`:
Add `parse_datetime` to your `Cargo.toml` with:
```toml
[dependencies]
parse_datetime = "0.6.0"
```
cargo add parse_datetime
```
Then, import the crate and use the `parse_datetime_at_date` function:
+26 -73
View File
@@ -46,11 +46,13 @@ checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "cc"
version = "1.0.79"
version = "1.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
dependencies = [
"jobserver",
"libc",
"shlex",
]
[[package]]
@@ -61,14 +63,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.39"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-targets",
"windows-link",
]
[[package]]
@@ -84,21 +86,9 @@ dependencies = [
"chrono",
"libfuzzer-sys",
"parse_datetime",
"rand",
"regex",
]
[[package]]
name = "getrandom"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
@@ -124,9 +114,9 @@ dependencies = [
[[package]]
name = "jobserver"
version = "0.1.26"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
@@ -148,9 +138,9 @@ checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
[[package]]
name = "libfuzzer-sys"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa"
checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75"
dependencies = [
"arbitrary",
"cc",
@@ -168,20 +158,13 @@ version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
@@ -201,19 +184,13 @@ checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "parse_datetime"
version = "0.6.0"
version = "0.8.0"
dependencies = [
"chrono",
"nom",
"regex",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.59"
@@ -232,36 +209,6 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "regex"
version = "1.11.1"
@@ -291,6 +238,12 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.18"
@@ -308,12 +261,6 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
@@ -378,6 +325,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "windows-link"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-targets"
version = "0.52.6"
-1
View File
@@ -7,7 +7,6 @@ edition = "2021"
cargo-fuzz = true
[dependencies]
rand = "0.8.5"
libfuzzer-sys = "0.4.7"
regex = "1.10.4"
chrono = { version="0.4", default-features=false, features=["std", "alloc", "clock"] }
+394 -68
View File
File diff suppressed because it is too large Load Diff
+283 -46
View File
@@ -1,8 +1,9 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use crate::ParseDateTimeError;
use crate::{parse_weekday::parse_weekday, ParseDateTimeError};
use chrono::{
DateTime, Datelike, Days, Duration, LocalResult, Months, NaiveDate, NaiveDateTime, TimeZone,
DateTime, Datelike, Days, Duration, LocalResult, Months, NaiveDate, NaiveDateTime, NaiveTime,
TimeZone, Weekday,
};
use regex::Regex;
@@ -20,6 +21,8 @@ const DAYS_PER_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 3
/// * `date` - A `Date` instance representing the base date for the calculation
/// * `s` - A string slice representing the relative time.
///
/// If `s` is empty, the `date` is returned as-is.
///
/// # Supported formats
///
/// The function supports the following formats for relative time:
@@ -51,11 +54,15 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
mut datetime: DateTime<T>,
s: &str,
) -> Result<DateTime<T>, ParseDateTimeError> {
let s = s.trim();
if s.is_empty() {
return Ok(datetime);
}
let time_pattern: Regex = Regex::new(
r"(?x)
(?:(?P<value>[-+]?\d*)\s*)?
(?:(?P<value>[-+]?\s*\d*)\s*)?
(\s*(?P<direction>next|this|last)?\s*)?
(?P<unit>years?|months?|fortnights?|weeks?|days?|hours?|h|minutes?|mins?|m|seconds?|secs?|s|yesterday|tomorrow|now|today)
(?P<unit>years?|months?|fortnights?|weeks?|days?|hours?|h|minutes?|mins?|m|seconds?|secs?|s|yesterday|tomorrow|now|today|(?P<weekday>[a-z]{3,9}))\b
(\s*(?P<separator>and|,)?\s*)?
(\s*(?P<ago>ago)?)?",
)?;
@@ -67,20 +74,26 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
for capture in time_pattern.captures_iter(s) {
captures_processed += 1;
let value_str = capture
let value_str: String = capture
.name("value")
.ok_or(ParseDateTimeError::InvalidInput)?
.as_str();
.as_str()
.chars()
.filter(|c| !c.is_whitespace()) // Remove potential space between +/- and number
.collect();
let direction = capture.name("direction").map_or("", |d| d.as_str());
let value = if value_str.is_empty() {
1
if direction == "this" {
0
} else {
1
}
} else {
value_str
.parse::<i64>()
.map_err(|_| ParseDateTimeError::InvalidInput)?
};
let direction = capture.name("direction").map_or("", |d| d.as_str());
if direction == "last" {
is_ago = true;
}
@@ -94,27 +107,26 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
is_ago = true;
}
let new_datetime = if direction == "this" {
add_days(datetime, 0, is_ago)
} else {
match unit {
"years" | "year" => add_months(datetime, value * 12, is_ago),
"months" | "month" => add_months(datetime, value, is_ago),
"fortnights" | "fortnight" => add_days(datetime, value * 14, is_ago),
"weeks" | "week" => add_days(datetime, value * 7, is_ago),
"days" | "day" => add_days(datetime, value, is_ago),
"hours" | "hour" | "h" => add_duration(datetime, Duration::hours(value), is_ago),
"minutes" | "minute" | "mins" | "min" | "m" => {
add_duration(datetime, Duration::minutes(value), is_ago)
}
"seconds" | "second" | "secs" | "sec" | "s" => {
add_duration(datetime, Duration::seconds(value), is_ago)
}
"yesterday" => add_days(datetime, 1, true),
"tomorrow" => add_days(datetime, 1, false),
"now" | "today" => Some(datetime),
_ => None,
let new_datetime = match unit {
"years" | "year" => add_months(datetime, value * 12, is_ago),
"months" | "month" => add_months(datetime, value, is_ago),
"fortnights" | "fortnight" => add_days(datetime, value * 14, is_ago),
"weeks" | "week" => add_days(datetime, value * 7, is_ago),
"days" | "day" => add_days(datetime, value, is_ago),
"hours" | "hour" | "h" => add_duration(datetime, Duration::hours(value), is_ago),
"minutes" | "minute" | "mins" | "min" | "m" => {
add_duration(datetime, Duration::minutes(value), is_ago)
}
"seconds" | "second" | "secs" | "sec" | "s" => {
add_duration(datetime, Duration::seconds(value), is_ago)
}
"yesterday" => add_days(datetime, 1, true),
"tomorrow" => add_days(datetime, 1, false),
"now" | "today" => Some(datetime),
_ => capture
.name("weekday")
.and_then(|weekday| parse_weekday(weekday.as_str()))
.and_then(|weekday| adjust_for_weekday(datetime, weekday, value, is_ago)),
};
datetime = match new_datetime {
Some(dt) => dt,
@@ -139,6 +151,25 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
}
}
fn adjust_for_weekday<T: TimeZone>(
mut datetime: DateTime<T>,
weekday: Weekday,
mut amount: i64,
is_ago: bool,
) -> Option<DateTime<T>> {
let mut same_day = true;
// last/this/next <weekday> truncates the time to midnight
datetime = datetime.with_time(NaiveTime::MIN).unwrap();
while datetime.weekday() != weekday {
datetime = add_days(datetime, 1, is_ago)?;
same_day = false;
}
if !same_day && 0 < amount {
amount -= 1;
}
add_days(datetime, amount * 7, is_ago)
}
fn add_months<T: TimeZone>(
datetime: DateTime<T>,
months: i64,
@@ -278,6 +309,12 @@ mod tests {
Ok(parsed - now)
}
#[test]
fn test_empty_string() {
let now = Utc::now();
assert_eq!(parse_relative_time_at_date(now, "").unwrap(), now);
}
#[test]
fn test_years() {
let now = Utc::now();
@@ -285,10 +322,7 @@ mod tests {
parse_relative_time_at_date(now, "1 year").unwrap(),
now.checked_add_months(Months::new(12)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "this year").unwrap(),
now.checked_add_months(Months::new(0)).unwrap()
);
assert_eq!(parse_relative_time_at_date(now, "this year").unwrap(), now);
assert_eq!(
parse_relative_time_at_date(now, "-2 years").unwrap(),
now.checked_sub_months(Months::new(24)).unwrap()
@@ -319,25 +353,24 @@ mod tests {
#[test]
fn test_months() {
use crate::parse_relative_time::add_months;
let now = Utc::now();
assert_eq!(
parse_relative_time_at_date(now, "1 month").unwrap(),
now.checked_add_months(Months::new(1)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "this month").unwrap(),
now.checked_add_months(Months::new(0)).unwrap()
add_months(now, 1, false).unwrap(),
);
assert_eq!(parse_relative_time_at_date(now, "this month").unwrap(), now);
assert_eq!(
parse_relative_time_at_date(now, "1 month and 2 weeks").unwrap(),
now.checked_add_months(Months::new(1))
add_months(now, 1, false)
.unwrap()
.checked_add_days(Days::new(14))
.unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "1 month and 2 weeks ago").unwrap(),
now.checked_sub_months(Months::new(1))
add_months(now, 1, true)
.unwrap()
.checked_sub_days(Days::new(14))
.unwrap()
@@ -348,7 +381,7 @@ mod tests {
);
assert_eq!(
parse_relative_time_at_date(now, "month").unwrap(),
now.checked_add_months(Months::new(1)).unwrap()
add_months(now, 1, false).unwrap(),
);
}
@@ -502,6 +535,19 @@ mod tests {
);
}
#[test]
fn test_spaces() {
let now = Utc::now();
assert_eq!(
parse_relative_time_at_date(now, "+ 1 hour").unwrap(),
now.checked_add_signed(Duration::hours(1)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "- 1 hour").unwrap(),
now.checked_sub_signed(Duration::hours(1)).unwrap()
);
}
#[test]
fn test_invalid_input() {
let result = parse_duration("foobar");
@@ -562,6 +608,8 @@ mod tests {
#[test]
fn test_duration_parsing() {
use crate::parse_relative_time::add_months;
let now = Utc::now();
assert_eq!(
parse_relative_time_at_date(now, "1 year").unwrap(),
@@ -582,25 +630,25 @@ mod tests {
assert_eq!(
parse_relative_time_at_date(now, "1 month").unwrap(),
now.checked_add_months(Months::new(1)).unwrap()
add_months(now, 1, false).unwrap(),
);
assert_eq!(
parse_relative_time_at_date(now, "1 month and 2 weeks").unwrap(),
now.checked_add_months(Months::new(1))
add_months(now, 1, false)
.unwrap()
.checked_add_days(Days::new(14))
.unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "1 month, 2 weeks").unwrap(),
now.checked_add_months(Months::new(1))
add_months(now, 1, false)
.unwrap()
.checked_add_days(Days::new(14))
.unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "1 months 2 weeks").unwrap(),
now.checked_add_months(Months::new(1))
add_months(now, 1, false)
.unwrap()
.checked_add_days(Days::new(14))
.unwrap()
@@ -618,7 +666,7 @@ mod tests {
);
assert_eq!(
parse_relative_time_at_date(now, "month").unwrap(),
now.checked_add_months(Months::new(1)).unwrap()
add_months(now, 1, false).unwrap(),
);
assert_eq!(
@@ -784,4 +832,193 @@ mod tests {
let result = parse_relative_time_at_date(now, "invalid 1r");
assert_eq!(result, Err(ParseDateTimeError::InvalidInput));
}
#[test]
fn test_parse_relative_time_at_date_this_weekday() {
// Jan 1 2025 is a Wed
let now = Utc.from_utc_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
));
// Check "this <same weekday>"
assert_eq!(
parse_relative_time_at_date(now, "this wednesday").unwrap(),
now
);
assert_eq!(parse_relative_time_at_date(now, "this wed").unwrap(), now);
// Other days
assert_eq!(
parse_relative_time_at_date(now, "this thursday").unwrap(),
now.checked_add_days(Days::new(1)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "this thur").unwrap(),
now.checked_add_days(Days::new(1)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "this thu").unwrap(),
now.checked_add_days(Days::new(1)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "this friday").unwrap(),
now.checked_add_days(Days::new(2)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "this fri").unwrap(),
now.checked_add_days(Days::new(2)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "this saturday").unwrap(),
now.checked_add_days(Days::new(3)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "this sat").unwrap(),
now.checked_add_days(Days::new(3)).unwrap()
);
// "this" with a day of the week that comes before today should return the next instance of
// that day
assert_eq!(
parse_relative_time_at_date(now, "this sunday").unwrap(),
now.checked_add_days(Days::new(4)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "this sun").unwrap(),
now.checked_add_days(Days::new(4)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "this monday").unwrap(),
now.checked_add_days(Days::new(5)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "this mon").unwrap(),
now.checked_add_days(Days::new(5)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "this tuesday").unwrap(),
now.checked_add_days(Days::new(6)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "this tue").unwrap(),
now.checked_add_days(Days::new(6)).unwrap()
);
}
#[test]
fn test_parse_relative_time_at_date_last_weekday() {
// Jan 1 2025 is a Wed
let now = Utc.from_utc_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
));
// Check "last <same weekday>"
assert_eq!(
parse_relative_time_at_date(now, "last wed").unwrap(),
now.checked_sub_days(Days::new(7)).unwrap()
);
// Check "last <day after today>"
assert_eq!(
parse_relative_time_at_date(now, "last thu").unwrap(),
now.checked_sub_days(Days::new(6)).unwrap()
);
// Check "last <day before today>"
assert_eq!(
parse_relative_time_at_date(now, "last tue").unwrap(),
now.checked_sub_days(Days::new(1)).unwrap()
);
}
#[test]
fn test_parse_relative_time_at_date_next_weekday() {
// Jan 1 2025 is a Wed
let now = Utc.from_utc_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
));
// Check "next <same weekday>"
assert_eq!(
parse_relative_time_at_date(now, "next wed").unwrap(),
now.checked_add_days(Days::new(7)).unwrap()
);
// Check "next <day after today>"
assert_eq!(
parse_relative_time_at_date(now, "next thu").unwrap(),
now.checked_add_days(Days::new(1)).unwrap()
);
// Check "next <day before today>"
assert_eq!(
parse_relative_time_at_date(now, "next tue").unwrap(),
now.checked_add_days(Days::new(6)).unwrap()
);
}
#[test]
fn test_parse_relative_time_at_date_number_weekday() {
// Jan 1 2025 is a Wed
let now = Utc.from_utc_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
));
assert_eq!(
parse_relative_time_at_date(now, "1 wed").unwrap(),
now.checked_add_days(Days::new(7)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "1 thu").unwrap(),
now.checked_add_days(Days::new(1)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "1 tue").unwrap(),
now.checked_add_days(Days::new(6)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "2 wed").unwrap(),
now.checked_add_days(Days::new(14)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "2 thu").unwrap(),
now.checked_add_days(Days::new(8)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "2 tue").unwrap(),
now.checked_add_days(Days::new(13)).unwrap()
);
}
#[test]
fn test_parse_relative_time_at_date_weekday_truncates_time() {
// Jan 1 2025 is a Wed
let now = Utc.from_utc_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
));
let now_midnight = Utc.from_utc_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
));
assert_eq!(
parse_relative_time_at_date(now, "this wed").unwrap(),
now_midnight
);
assert_eq!(
parse_relative_time_at_date(now, "last wed").unwrap(),
now_midnight.checked_sub_days(Days::new(7)).unwrap()
);
assert_eq!(
parse_relative_time_at_date(now, "next wed").unwrap(),
now_midnight.checked_add_days(Days::new(7)).unwrap()
);
}
#[test]
fn test_parse_relative_time_at_date_invalid_weekday() {
// Jan 1 2025 is a Wed
let now = Utc.from_utc_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
));
assert_eq!(
parse_relative_time_at_date(now, "this fooday"),
Err(ParseDateTimeError::InvalidInput)
);
}
}
+11 -13
View File
@@ -4,7 +4,7 @@ use regex::Regex;
mod time_only_formats {
pub const HH_MM: &str = "%R";
pub const HH_MM_SS: &str = "%T";
pub const TWELVEHOUR: &str = "%r";
pub const TWELVE_HOUR: &str = "%r";
}
/// Convert a military time zone string to a time zone offset.
@@ -55,16 +55,14 @@ fn parse_time_with_offset_multi(
for fmt in [
time_only_formats::HH_MM,
time_only_formats::HH_MM_SS,
time_only_formats::TWELVEHOUR,
time_only_formats::TWELVE_HOUR,
] {
let parsed = match NaiveTime::parse_from_str(s, fmt) {
Ok(t) => t,
Err(_) => continue,
let Ok(parsed) = NaiveTime::parse_from_str(s, fmt) else {
continue;
};
let parsed_dt = date.date_naive().and_time(parsed);
match offset.from_local_datetime(&parsed_dt).single() {
Some(dt) => return Some(dt),
None => continue,
if let Some(dt) = offset.from_local_datetime(&parsed_dt).single() {
return Some(dt);
}
}
None
@@ -84,7 +82,7 @@ pub(crate) fn parse_time_only(date: DateTime<Local>, s: &str) -> Option<DateTime
offset_in_sec += minutes.as_str().parse::<i32>().unwrap() * 60;
}
_ => (),
};
}
offset_in_sec *= if &captures["sign"] == "-" { -1 } else { 1 };
FixedOffset::east_opt(offset_in_sec)
}
@@ -140,7 +138,7 @@ mod tests {
let parsed_time = parse_time_only(get_test_date(), "21:04")
.unwrap()
.timestamp();
assert_eq!(parsed_time, 1709499840)
assert_eq!(parsed_time, 1709499840);
}
#[test]
@@ -187,7 +185,7 @@ mod tests {
let parsed_time = parse_time_only(get_test_date(), "21:04:30")
.unwrap()
.timestamp();
assert_eq!(parsed_time, 1709499870)
assert_eq!(parsed_time, 1709499870);
}
#[test]
@@ -196,7 +194,7 @@ mod tests {
let parsed_time = parse_time_only(get_test_date(), "21:04:30 +0530")
.unwrap()
.timestamp();
assert_eq!(parsed_time, 1709480070)
assert_eq!(parsed_time, 1709480070);
}
#[test]
@@ -205,6 +203,6 @@ mod tests {
let parsed_time = parse_time_only(get_test_date(), "9:04:00 PM")
.unwrap()
.timestamp();
assert_eq!(parsed_time, 1709499840)
assert_eq!(parsed_time, 1709499840);
}
}
+5 -5
View File
@@ -10,8 +10,7 @@ use nom::character::complete::{char, digit1};
use nom::combinator::all_consuming;
use nom::multi::fold_many0;
use nom::sequence::preceded;
use nom::sequence::tuple;
use nom::{self, IResult};
use nom::{self, IResult, Parser};
#[derive(Debug, PartialEq)]
pub enum ParseTimestampError {
@@ -55,7 +54,7 @@ pub(crate) fn parse_timestamp(s: &str) -> Result<i64, ParseTimestampError> {
let res: IResult<&str, (char, &str)> = all_consuming(preceded(
char('@'),
tuple((
(
// Note: to stay compatible with gnu date this code allows
// multiple + and - and only considers the last one
fold_many0(
@@ -67,8 +66,9 @@ pub(crate) fn parse_timestamp(s: &str) -> Result<i64, ParseTimestampError> {
|_, c| c,
),
digit1,
)),
))(s);
),
))
.parse(s);
let (_, (sign, number_str)) = res?;
+6 -5
View File
@@ -4,7 +4,7 @@ use chrono::Weekday;
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::combinator::value;
use nom::{self, IResult};
use nom::{self, IResult, Parser};
// Helper macro to simplify tag matching
macro_rules! tag_match {
@@ -25,7 +25,8 @@ pub(crate) fn parse_weekday(s: &str) -> Option<Weekday> {
tag_match!(Weekday::Fri, "friday", "fri"),
tag_match!(Weekday::Sat, "saturday", "sat"),
tag_match!(Weekday::Sun, "sunday", "sun"),
)))(s);
)))
.parse(s);
match parse_result {
Ok((_, weekday)) => Some(weekday),
@@ -63,9 +64,9 @@ mod tests {
for (name, weekday) in days {
assert_eq!(parse_weekday(name), Some(weekday));
assert_eq!(parse_weekday(&format!(" {}", name)), Some(weekday));
assert_eq!(parse_weekday(&format!(" {} ", name)), Some(weekday));
assert_eq!(parse_weekday(&format!("{} ", name)), Some(weekday));
assert_eq!(parse_weekday(&format!(" {name}")), Some(weekday));
assert_eq!(parse_weekday(&format!(" {name} ")), Some(weekday));
assert_eq!(parse_weekday(&format!("{name} ")), Some(weekday));
let (left, right) = name.split_at(1);
let (test_str1, test_str2) = (