diff --git a/README.md b/README.md index 05e955f..2ead819 100644 --- a/README.md +++ b/README.md @@ -23,34 +23,22 @@ Add this to your `Cargo.toml`: parse_datetime = "0.4.0" ``` -Then, import the crate and use the `from_str` and `from_str_at_date` functions: -```rs -use parse_datetime::{from_str, from_str_at_date}; -use chrono::Duration; - -let duration = from_str("+3 days"); -assert_eq!(duration.unwrap(), Duration::days(3)); +Then, import the crate and use the `add_relative_str` function: -let today = Utc::today().naive_utc(); -let yesterday = today - Duration::days(1); +```rs +use chrono::{DateTime, Utc}; +use parse_datetime::{add_relative_str}; +let date: DateTime = "2014-09-05 15:43:21Z".parse::>().unwrap(); assert_eq!( - from_str_at_date(yesterday, "2 days").unwrap(), - Duration::days(1) + add_relative_str(date, "4 months 25 days").unwrap().to_string(), + "2015-01-30 15:43:21 UTC" ); -``` - -For DateTime parsing, import the `parse_datetime` module: -```rs -use parse_datetime::parse_datetime::from_str; -use chrono::{Local, TimeZone}; -let dt = from_str("2021-02-14 06:37:47"); -assert_eq!(dt.unwrap(), Local.with_ymd_and_hms(2021, 2, 14, 6, 37, 47).unwrap()); ``` ### Supported Formats -The `from_str` and `from_str_at_date` functions support the following formats for relative time: +The `add_relative_str` function supports the following formats for relative time: - `num` `unit` (e.g., "-1 hour", "+3 days") - `unit` (e.g., "hour", "day") @@ -64,28 +52,20 @@ The `from_str` and `from_str_at_date` functions support the following formats fo `num` can be a positive or negative integer. `unit` can be one of the following: "fortnight", "week", "day", "hour", "minute", "min", "second", "sec" and their plural forms. -## Return Values +### Return Values -### Duration +The `add_relative_str` function returns: -The `from_str` and `from_str_at_date` functions return: - -- `Ok(Duration)` - If the input string can be parsed as a relative time +- `Ok(DateTime)` - If the input string can be parsed as a relative time - `Err(ParseDurationError)` - If the input string cannot be parsed as a relative time This function will return `Err(ParseDurationError::InvalidInput)` if the input string cannot be parsed as a relative time. -### parse_datetime - -The `from_str` function returns: - -- `Ok(DateTime)` - If the input string can be parsed as a datetime -- `Err(ParseDurationError::InvalidInput)` - If the input string cannot be parsed - ## Fuzzer To run the fuzzer: + ``` $ cargo fuzz run fuzz_from_str ``` diff --git a/src/lib.rs b/src/lib.rs index 614b355..c7fae2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ // Expose parse_datetime pub mod parse_datetime; -use chrono::{Duration, Local, NaiveDate, Utc}; +use chrono::{DateTime, Days, Duration, Months, TimeZone}; use regex::{Error as RegexError, Regex}; use std::error::Error; use std::fmt::{self, Display}; @@ -39,21 +39,13 @@ impl From for ParseDurationError { } } -/// Parses a relative time string and returns a `Duration` representing the -/// relative time. +/// Adds a relative duration to the given date and returns the obtained date. /// /// # Arguments /// +/// * `date` - A `DateTime` instance representing the base date for the calculation /// * `s` - A string slice representing the relative time. /// -/// # Examples -/// -/// ``` -/// use chrono::Duration; -/// let duration = parse_datetime::from_str("+3 days"); -/// assert_eq!(duration.unwrap(), Duration::days(3)); -/// ``` -/// /// # Supported formats /// /// The function supports the following formats for relative time: @@ -71,11 +63,6 @@ impl From for ParseDurationError { /// /// It is also possible to pass "1 hour 2 minutes" or "2 days and 2 hours" /// -/// # Returns -/// -/// * `Ok(Duration)` - If the input string can be parsed as a relative time -/// * `Err(ParseDurationError)` - If the input string cannot be parsed as a relative time -/// /// # Errors /// /// This function will return `Err(ParseDurationError::InvalidInput)` if the input string @@ -84,43 +71,18 @@ impl From for ParseDurationError { /// # Examples /// /// ``` -/// use chrono::Duration; -/// use parse_datetime::{from_str, ParseDurationError}; -/// -/// assert_eq!(from_str("1 hour, 30 minutes").unwrap(), Duration::minutes(90)); -/// assert_eq!(from_str("tomorrow").unwrap(), Duration::days(1)); -/// assert!(matches!(from_str("invalid"), Err(ParseDurationError::InvalidInput))); -/// ``` -pub fn from_str(s: &str) -> Result { - from_str_at_date(Utc::now().date_naive(), s) -} - -/// Parses a duration string and returns a `Duration` instance, with the duration -/// calculated from the specified date. -/// -/// # Arguments -/// -/// * `date` - A `Date` instance representing the base date for the calculation -/// * `s` - A string slice representing the relative time. -/// -/// # Errors -/// -/// This function will return `Err(ParseDurationError::InvalidInput)` if the input string -/// cannot be parsed as a relative time. -/// -/// # Examples -/// -/// ``` -/// use chrono::{Duration, NaiveDate, Utc, Local}; -/// use parse_datetime::{from_str_at_date, ParseDurationError}; -/// let today = Local::now().date().naive_local(); -/// let yesterday = today - Duration::days(1); +/// use chrono::{DateTime, Utc}; +/// use parse_datetime::{add_relative_str}; +/// let date: DateTime = "2014-09-05 15:43:21Z".parse::>().unwrap(); /// assert_eq!( -/// from_str_at_date(yesterday, "2 days").unwrap(), -/// Duration::days(1) // 1 day from the specified date + 1 day from the input string +/// add_relative_str(date, "4 months 25 days").unwrap().to_string(), +/// "2015-01-30 15:43:21 UTC" /// ); /// ``` -pub fn from_str_at_date(date: NaiveDate, s: &str) -> Result { +pub fn add_relative_str(date: DateTime, s: &str) -> Result, ParseDurationError> +where + Tz: TimeZone, +{ let time_pattern: Regex = Regex::new( r"(?x) (?:(?P[-+]?\d*)\s*)? @@ -130,7 +92,7 @@ pub fn from_str_at_date(date: NaiveDate, s: &str) -> Resultago)?)?", )?; - let mut total_duration = Duration::seconds(0); + let mut date = date.clone(); let mut is_ago = s.contains(" ago"); let mut captures_processed = 0; let mut total_length = 0; @@ -164,27 +126,37 @@ pub fn from_str_at_date(date: NaiveDate, s: &str) -> Result Duration::days(value * 365), - "months" | "month" => Duration::days(value * 30), - "fortnights" | "fortnight" => Duration::weeks(value * 2), - "weeks" | "week" => Duration::weeks(value), - "days" | "day" => Duration::days(value), - "hours" | "hour" | "h" => Duration::hours(value), - "minutes" | "minute" | "mins" | "min" | "m" => Duration::minutes(value), - "seconds" | "second" | "secs" | "sec" | "s" => Duration::seconds(value), - "yesterday" => Duration::days(-1), - "tomorrow" => Duration::days(1), - "now" | "today" => Duration::zero(), + let add_months = |date: DateTime, months: i64| { + if months.is_negative() { + date - Months::new(months.unsigned_abs() as u32) + } else { + date + Months::new(months.unsigned_abs() as u32) + } + }; + let add_days = |date: DateTime, days: i64| { + if days.is_negative() { + date - Days::new(days.unsigned_abs()) + } else { + date + Days::new(days.unsigned_abs()) + } + }; + + date = match unit { + "years" | "year" => add_months(date, 12 * value), + "months" | "month" => add_months(date, value), + "fortnights" | "fortnight" => add_days(date, 14 * value), + "weeks" | "week" => add_days(date, 7 * value), + "days" | "day" => add_days(date, value), + "hours" | "hour" | "h" => date + Duration::hours(value), + "minutes" | "minute" | "mins" | "min" | "m" => date + Duration::minutes(value), + "seconds" | "second" | "secs" | "sec" | "s" => date + Duration::seconds(value), + "yesterday" => add_days(date, -1), + "tomorrow" => add_days(date, 1), + "now" | "today" => date, _ => return Err(ParseDurationError::InvalidInput), }; - let neg_duration = -duration; - total_duration = - match total_duration.checked_add(if is_ago { &neg_duration } else { &duration }) { - Some(duration) => duration, - None => return Err(ParseDurationError::InvalidInput), - }; // Calculate the total length of the matched substring if let Some(m) = capture.get(0) { @@ -200,149 +172,113 @@ pub fn from_str_at_date(date: NaiveDate, s: &str) -> Result = "2014-09-05 15:43:21Z".parse::>().unwrap(); + let result = add_relative_str(date, "foobar"); assert_eq!(result, Err(ParseDurationError::InvalidInput)); - let result = from_str("invalid 1"); + let result = add_relative_str(date, "invalid 1"); assert_eq!(result, Err(ParseDurationError::InvalidInput)); // Fails for now with a panic - /* let result = from_str("777777777777777771m"); + /* let result = add_relative_str(date, "777777777777777771m"); match result { Err(ParseDurationError::InvalidInput) => assert!(true), _ => assert!(false), @@ -350,36 +286,44 @@ mod tests { } #[test] - fn test_from_str_at_date() { - let date = NaiveDate::from_ymd_opt(2014, 9, 5).unwrap(); - let now = Local::now().date_naive(); - let days_diff = (date - now).num_days(); - - assert_eq!( - from_str_at_date(date, "1 day").unwrap(), - Duration::days(days_diff + 1) + fn test_add_relative_str() { + assert_add_relative_str_eq("0 seconds", "2014-09-05 15:43:21 UTC"); + assert_add_relative_str_eq("1 day", "2014-09-06 15:43:21 UTC"); + assert_add_relative_str_eq("2 hours", "2014-09-05 17:43:21 UTC"); + assert_add_relative_str_eq("1 year ago", "2013-09-05 15:43:21 UTC"); + assert_add_relative_str_eq("1 year", "2015-09-05 15:43:21 UTC"); + assert_add_relative_str_eq("4 years", "2018-09-05 15:43:21 UTC"); + assert_add_relative_str_eq("2 months ago", "2014-07-05 15:43:21 UTC"); + assert_add_relative_str_eq("15 days ago", "2014-08-21 15:43:21 UTC"); + assert_add_relative_str_eq("1 week ago", "2014-08-29 15:43:21 UTC"); + assert_add_relative_str_eq("5 hours ago", "2014-09-05 10:43:21 UTC"); + assert_add_relative_str_eq("30 minutes ago", "2014-09-05 15:13:21 UTC"); + assert_add_relative_str_eq("10 seconds", "2014-09-05 15:43:31 UTC"); + assert_add_relative_str_eq("last hour", "2014-09-05 14:43:21 UTC"); + assert_add_relative_str_eq("next year", "2015-09-05 15:43:21 UTC"); + assert_add_relative_str_eq("next week", "2014-09-12 15:43:21 UTC"); + assert_add_relative_str_eq("last month", "2014-08-05 15:43:21 UTC"); + assert_add_relative_str_eq("4 months 25 days", "2015-01-30 15:43:21 UTC"); + assert_add_relative_str_eq("4 months 25 days 1 month", "2015-02-28 15:43:21 UTC"); + assert_add_relative_str_eq( + "1 year 2 months 4 weeks 3 days and 2 seconds", + "2015-12-06 15:43:23 UTC", ); - - assert_eq!( - from_str_at_date(date, "2 hours").unwrap(), - Duration::days(days_diff) + Duration::hours(2) + assert_add_relative_str_eq( + "1 year 2 months 4 weeks 3 days and 2 seconds ago", + "2013-06-04 15:43:19 UTC", ); } - #[test] - fn test_invalid_input_at_date() { - let date = NaiveDate::from_ymd_opt(2014, 9, 5).unwrap(); - assert!(matches!( - from_str_at_date(date, "invalid"), - Err(ParseDurationError::InvalidInput) - )); - } - - #[test] - fn test_direction() { - assert_eq!(from_str("last hour").unwrap(), Duration::seconds(-3600)); - assert_eq!(from_str("next year").unwrap(), Duration::days(365)); - assert_eq!(from_str("next week").unwrap(), Duration::days(7)); - assert_eq!(from_str("last month").unwrap(), Duration::days(-30)); + /// Adds the given relative string to the date `2014-09-05 15:43:21 UTC` and compares it with the expected result. + fn assert_add_relative_str_eq(str: &str, expected: &str) { + let date: DateTime = "2014-09-05 15:43:21 UTC".parse::>().unwrap(); + assert_eq!( + (add_relative_str(date, str).unwrap()).to_string(), + expected, + "'{}' relative from {}", + str, + date + ); } } diff --git a/tests/simple.rs b/tests/simple.rs index a538f9d..9f10277 100644 --- a/tests/simple.rs +++ b/tests/simple.rs @@ -12,6 +12,7 @@ fn test_invalid_input() { } #[test] +#[ignore] fn test_duration_parsing() { assert_eq!(from_str("1 year").unwrap(), Duration::seconds(31_536_000)); assert_eq!( @@ -69,7 +70,18 @@ fn test_duration_parsing() { Duration::seconds(-1_209_600) ); assert_eq!(from_str("week").unwrap(), Duration::seconds(604_800)); + assert_eq!( + from_str("1 year 2 months 4 weeks 3 days and 2 seconds").unwrap(), + Duration::seconds(39_398_402) + ); + assert_eq!( + from_str("1 year 2 months 4 weeks 3 days and 2 seconds ago").unwrap(), + Duration::seconds(-39_398_402) + ); +} +#[test] +fn test_duration_parsing_exact() { assert_eq!(from_str("1 day").unwrap(), Duration::seconds(86_400)); assert_eq!(from_str("2 days ago").unwrap(), Duration::seconds(-172_800)); assert_eq!(from_str("-2 days").unwrap(), Duration::seconds(-172_800)); @@ -97,15 +109,6 @@ fn test_duration_parsing() { assert_eq!(from_str("now").unwrap(), Duration::seconds(0)); assert_eq!(from_str("today").unwrap(), Duration::seconds(0)); - - assert_eq!( - from_str("1 year 2 months 4 weeks 3 days and 2 seconds").unwrap(), - Duration::seconds(39_398_402) - ); - assert_eq!( - from_str("1 year 2 months 4 weeks 3 days and 2 seconds ago").unwrap(), - Duration::seconds(-39_398_402) - ); } #[test]