37 Commits

Author SHA1 Message Date
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 462 additions and 185 deletions
Generated
+3 -10
View File
@@ -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.7.0"
version = "0.8.0"
dependencies = [
"chrono",
"nom",
+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.7.0"
version = "0.8.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"
+2 -3
View File
@@ -18,9 +18,8 @@ A Rust crate for parsing human-readable relative time strings and human-readable
Add this to your `Cargo.toml`:
```toml
[dependencies]
parse_datetime = "0.6.0"
```
cargo add parse_datetime
```
Then, import the crate and use the `parse_datetime_at_date` function:
+17 -70
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]]
@@ -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.7.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"
-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"] }
+396 -68
View File
File diff suppressed because it is too large Load Diff
+27 -17
View File
@@ -20,6 +20,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,6 +53,10 @@ 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*)?
@@ -278,6 +284,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 +297,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 +328,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 +356,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(),
);
}
@@ -562,6 +570,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 +592,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 +628,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!(
+4 -4
View File
@@ -140,7 +140,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 +187,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 +196,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 +205,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) = (