You've already forked parse_datetime
mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
Don't use Durations for calculating relative times
This commit is contained in:
+191
-129
@@ -1,7 +1,7 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
use crate::ParseDateTimeError;
|
||||
use chrono::{Duration, Local, NaiveDate, Utc};
|
||||
use chrono::{DateTime, Days, Duration, Months, Utc};
|
||||
use regex::Regex;
|
||||
/// Parses a relative time string and returns a `Duration` representing the
|
||||
/// relative time.
|
||||
@@ -40,7 +40,9 @@ use regex::Regex;
|
||||
///
|
||||
/// ```
|
||||
pub fn parse_relative_time(s: &str) -> Result<Duration, ParseDateTimeError> {
|
||||
parse_relative_time_at_date(Utc::now().date_naive(), s)
|
||||
let now = Utc::now();
|
||||
let parsed = parse_relative_time_at_date(now, s)?;
|
||||
Ok(parsed - now)
|
||||
}
|
||||
|
||||
/// Parses a duration string and returns a `Duration` instance, with the duration
|
||||
@@ -56,10 +58,10 @@ pub fn parse_relative_time(s: &str) -> Result<Duration, ParseDateTimeError> {
|
||||
/// This function will return `Err(ParseDateTimeError::InvalidInput)` if the input string
|
||||
/// cannot be parsed as a relative time.
|
||||
/// ```
|
||||
pub fn parse_relative_time_at_date(
|
||||
date: NaiveDate,
|
||||
fn parse_relative_time_at_date(
|
||||
mut datetime: DateTime<Utc>,
|
||||
s: &str,
|
||||
) -> Result<Duration, ParseDateTimeError> {
|
||||
) -> Result<DateTime<Utc>, ParseDateTimeError> {
|
||||
let time_pattern: Regex = Regex::new(
|
||||
r"(?x)
|
||||
(?:(?P<value>[-+]?\d*)\s*)?
|
||||
@@ -69,7 +71,6 @@ pub fn parse_relative_time_at_date(
|
||||
(\s*(?P<ago>ago)?)?",
|
||||
)?;
|
||||
|
||||
let mut total_duration = Duration::seconds(0);
|
||||
let mut is_ago = s.contains(" ago");
|
||||
let mut captures_processed = 0;
|
||||
let mut total_length = 0;
|
||||
@@ -104,26 +105,28 @@ pub fn parse_relative_time_at_date(
|
||||
is_ago = true;
|
||||
}
|
||||
|
||||
let duration = match unit {
|
||||
"years" | "year" => 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 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),
|
||||
_ => return Err(ParseDateTimeError::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(ParseDateTimeError::InvalidInput),
|
||||
};
|
||||
datetime = match new_datetime {
|
||||
Some(dt) => dt,
|
||||
None => return Err(ParseDateTimeError::InvalidInput),
|
||||
};
|
||||
|
||||
// Calculate the total length of the matched substring
|
||||
if let Some(m) = capture.get(0) {
|
||||
@@ -139,61 +142,102 @@ pub fn parse_relative_time_at_date(
|
||||
if captures_processed == 0 {
|
||||
Err(ParseDateTimeError::InvalidInput)
|
||||
} else {
|
||||
let time_now = Local::now().date_naive();
|
||||
let date_duration = date - time_now;
|
||||
|
||||
Ok(total_duration + date_duration)
|
||||
Ok(datetime)
|
||||
}
|
||||
}
|
||||
|
||||
fn add_months(datetime: DateTime<Utc>, months: i64, mut is_ago: bool) -> Option<DateTime<Utc>> {
|
||||
let months = if months < 0 {
|
||||
is_ago = !is_ago;
|
||||
u32::try_from(-months).ok()?
|
||||
} else {
|
||||
u32::try_from(months).ok()?
|
||||
};
|
||||
if is_ago {
|
||||
datetime.checked_sub_months(Months::new(months))
|
||||
} else {
|
||||
datetime.checked_add_months(Months::new(months))
|
||||
}
|
||||
}
|
||||
|
||||
fn add_days(datetime: DateTime<Utc>, days: i64, mut is_ago: bool) -> Option<DateTime<Utc>> {
|
||||
let days = if days < 0 {
|
||||
is_ago = !is_ago;
|
||||
u64::try_from(-days).ok()?
|
||||
} else {
|
||||
u64::try_from(days).ok()?
|
||||
};
|
||||
if is_ago {
|
||||
datetime.checked_sub_days(Days::new(days))
|
||||
} else {
|
||||
datetime.checked_add_days(Days::new(days))
|
||||
}
|
||||
}
|
||||
|
||||
fn add_duration(
|
||||
datetime: DateTime<Utc>,
|
||||
duration: Duration,
|
||||
is_ago: bool,
|
||||
) -> Option<DateTime<Utc>> {
|
||||
let duration = if is_ago { -duration } else { duration };
|
||||
datetime.checked_add_signed(duration)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::ParseDateTimeError;
|
||||
use super::{parse_relative_time, parse_relative_time_at_date};
|
||||
use chrono::{Duration, Local, NaiveDate, Utc};
|
||||
use chrono::{Days, Duration, Months, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
|
||||
|
||||
#[test]
|
||||
fn test_years() {
|
||||
let now = Utc::now();
|
||||
assert_eq!(
|
||||
parse_relative_time("1 year").unwrap(),
|
||||
Duration::seconds(31_536_000)
|
||||
now + parse_relative_time("1 year").unwrap(),
|
||||
now.checked_add_months(Months::new(12)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("-2 years").unwrap(),
|
||||
Duration::seconds(-63_072_000)
|
||||
now + parse_relative_time("-2 years").unwrap(),
|
||||
now.checked_sub_months(Months::new(24)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("2 years ago").unwrap(),
|
||||
Duration::seconds(-63_072_000)
|
||||
now + parse_relative_time("2 years ago").unwrap(),
|
||||
now.checked_sub_months(Months::new(24)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("year").unwrap(),
|
||||
Duration::seconds(31_536_000)
|
||||
now + parse_relative_time("year").unwrap(),
|
||||
now.checked_add_months(Months::new(12)).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_months() {
|
||||
let now = Utc::now();
|
||||
assert_eq!(
|
||||
parse_relative_time("1 month").unwrap(),
|
||||
Duration::seconds(2_592_000)
|
||||
now + parse_relative_time("1 month").unwrap(),
|
||||
now.checked_add_months(Months::new(1)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("1 month and 2 weeks").unwrap(),
|
||||
Duration::seconds(3_801_600)
|
||||
now + parse_relative_time("1 month and 2 weeks").unwrap(),
|
||||
now.checked_add_months(Months::new(1))
|
||||
.unwrap()
|
||||
.checked_add_days(Days::new(14))
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("1 month and 2 weeks ago").unwrap(),
|
||||
Duration::seconds(-3_801_600)
|
||||
now + parse_relative_time("1 month and 2 weeks ago").unwrap(),
|
||||
now.checked_sub_months(Months::new(1))
|
||||
.unwrap()
|
||||
.checked_sub_days(Days::new(14))
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("2 months").unwrap(),
|
||||
Duration::seconds(5_184_000)
|
||||
now + parse_relative_time("2 months").unwrap(),
|
||||
now.checked_add_months(Months::new(2)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("month").unwrap(),
|
||||
Duration::seconds(2_592_000)
|
||||
now + parse_relative_time("month").unwrap(),
|
||||
now.checked_add_months(Months::new(1)).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -323,6 +367,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_no_spaces() {
|
||||
let now = Utc::now();
|
||||
assert_eq!(parse_relative_time("-1hour").unwrap(), Duration::hours(-1));
|
||||
assert_eq!(parse_relative_time("+3days").unwrap(), Duration::days(3));
|
||||
assert_eq!(parse_relative_time("2weeks").unwrap(), Duration::weeks(2));
|
||||
@@ -335,12 +380,12 @@ mod tests {
|
||||
Duration::seconds(-1_213_200)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("+4months").unwrap(),
|
||||
Duration::days(4 * 30)
|
||||
now + parse_relative_time("+4months").unwrap(),
|
||||
now.checked_add_months(Months::new(4)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("-2years").unwrap(),
|
||||
Duration::days(-2 * 365)
|
||||
now + parse_relative_time("-2years").unwrap(),
|
||||
now.checked_sub_months(Months::new(24)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("15minutes").unwrap(),
|
||||
@@ -374,131 +419,139 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_relative_time_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!(
|
||||
parse_relative_time_at_date(date, "1 day").unwrap(),
|
||||
Duration::days(days_diff + 1)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_relative_time_at_date(date, "2 hours").unwrap(),
|
||||
Duration::days(days_diff) + Duration::hours(2)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_input_at_date() {
|
||||
let date = NaiveDate::from_ymd_opt(2014, 9, 5).unwrap();
|
||||
assert!(matches!(
|
||||
parse_relative_time_at_date(date, "invalid"),
|
||||
Err(ParseDateTimeError::InvalidInput)
|
||||
let datetime = Utc.from_utc_datetime(&NaiveDateTime::new(
|
||||
NaiveDate::from_ymd_opt(2014, 9, 5).unwrap(),
|
||||
NaiveTime::from_hms_opt(0, 2, 3).unwrap(),
|
||||
));
|
||||
let now = Utc::now();
|
||||
let diff = datetime - now;
|
||||
|
||||
assert_eq!(
|
||||
parse_relative_time_at_date(datetime, "1 day").unwrap(),
|
||||
now + diff + Duration::days(1)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_relative_time_at_date(datetime, "2 hours").unwrap(),
|
||||
now + diff + Duration::hours(2)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_direction() {
|
||||
let now = Utc::now();
|
||||
assert_eq!(
|
||||
parse_relative_time("last hour").unwrap(),
|
||||
Duration::seconds(-3600)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("next year").unwrap(),
|
||||
Duration::days(365)
|
||||
now + parse_relative_time("next year").unwrap(),
|
||||
now.checked_add_months(Months::new(12)).unwrap()
|
||||
);
|
||||
assert_eq!(parse_relative_time("next week").unwrap(), Duration::days(7));
|
||||
assert_eq!(
|
||||
parse_relative_time("last month").unwrap(),
|
||||
Duration::days(-30)
|
||||
now + parse_relative_time("last month").unwrap(),
|
||||
now.checked_sub_months(Months::new(1)).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_parsing() {
|
||||
let now = Utc::now();
|
||||
assert_eq!(
|
||||
parse_relative_time("1 year").unwrap(),
|
||||
Duration::seconds(31_536_000)
|
||||
now + parse_relative_time("1 year").unwrap(),
|
||||
now.checked_add_months(Months::new(12)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("-2 years").unwrap(),
|
||||
Duration::seconds(-63_072_000)
|
||||
now + parse_relative_time("-2 years").unwrap(),
|
||||
now.checked_sub_months(Months::new(24)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("2 years ago").unwrap(),
|
||||
Duration::seconds(-63_072_000)
|
||||
now + parse_relative_time("2 years ago").unwrap(),
|
||||
now.checked_sub_months(Months::new(24)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("year").unwrap(),
|
||||
Duration::seconds(31_536_000)
|
||||
now + parse_relative_time("year").unwrap(),
|
||||
now.checked_add_months(Months::new(12)).unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_relative_time("1 month").unwrap(),
|
||||
Duration::seconds(2_592_000)
|
||||
now + parse_relative_time("1 month").unwrap(),
|
||||
now.checked_add_months(Months::new(1)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("1 month and 2 weeks").unwrap(),
|
||||
Duration::seconds(3_801_600)
|
||||
now + parse_relative_time("1 month and 2 weeks").unwrap(),
|
||||
now.checked_add_months(Months::new(1))
|
||||
.unwrap()
|
||||
.checked_add_days(Days::new(14))
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("1 month, 2 weeks").unwrap(),
|
||||
Duration::seconds(3_801_600)
|
||||
now + parse_relative_time("1 month, 2 weeks").unwrap(),
|
||||
now.checked_add_months(Months::new(1))
|
||||
.unwrap()
|
||||
.checked_add_days(Days::new(14))
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("1 months 2 weeks").unwrap(),
|
||||
Duration::seconds(3_801_600)
|
||||
now + parse_relative_time("1 months 2 weeks").unwrap(),
|
||||
now.checked_add_months(Months::new(1))
|
||||
.unwrap()
|
||||
.checked_add_days(Days::new(14))
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("1 month and 2 weeks ago").unwrap(),
|
||||
Duration::seconds(-3_801_600)
|
||||
now + parse_relative_time("1 month and 2 weeks ago").unwrap(),
|
||||
now.checked_sub_months(Months::new(1))
|
||||
.unwrap()
|
||||
.checked_sub_days(Days::new(14))
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("2 months").unwrap(),
|
||||
Duration::seconds(5_184_000)
|
||||
now + parse_relative_time("2 months").unwrap(),
|
||||
now.checked_add_months(Months::new(2)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("month").unwrap(),
|
||||
Duration::seconds(2_592_000)
|
||||
now + parse_relative_time("month").unwrap(),
|
||||
now.checked_add_months(Months::new(1)).unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_relative_time("1 fortnight").unwrap(),
|
||||
Duration::seconds(1_209_600)
|
||||
now + parse_relative_time("1 fortnight").unwrap(),
|
||||
now.checked_add_days(Days::new(14)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("3 fortnights").unwrap(),
|
||||
Duration::seconds(3_628_800)
|
||||
now + parse_relative_time("3 fortnights").unwrap(),
|
||||
now.checked_add_days(Days::new(3 * 14)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("fortnight").unwrap(),
|
||||
Duration::seconds(1_209_600)
|
||||
now + parse_relative_time("fortnight").unwrap(),
|
||||
now.checked_add_days(Days::new(14)).unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_relative_time("1 week").unwrap(),
|
||||
Duration::seconds(604_800)
|
||||
now + parse_relative_time("1 week").unwrap(),
|
||||
now.checked_add_days(Days::new(7)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("1 week 3 days").unwrap(),
|
||||
Duration::seconds(864_000)
|
||||
now + parse_relative_time("1 week 3 days").unwrap(),
|
||||
now.checked_add_days(Days::new(7 + 3)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("1 week 3 days ago").unwrap(),
|
||||
Duration::seconds(-864_000)
|
||||
now + parse_relative_time("1 week 3 days ago").unwrap(),
|
||||
now.checked_sub_days(Days::new(7 + 3)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("-2 weeks").unwrap(),
|
||||
Duration::seconds(-1_209_600)
|
||||
now + parse_relative_time("-2 weeks").unwrap(),
|
||||
now.checked_sub_days(Days::new(14)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("2 weeks ago").unwrap(),
|
||||
Duration::seconds(-1_209_600)
|
||||
now + parse_relative_time("2 weeks ago").unwrap(),
|
||||
now.checked_sub_days(Days::new(14)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("week").unwrap(),
|
||||
Duration::seconds(604_800)
|
||||
now + parse_relative_time("week").unwrap(),
|
||||
now.checked_add_days(Days::new(7)).unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -572,12 +625,22 @@ mod tests {
|
||||
assert_eq!(parse_relative_time("today").unwrap(), Duration::seconds(0));
|
||||
|
||||
assert_eq!(
|
||||
parse_relative_time("1 year 2 months 4 weeks 3 days and 2 seconds").unwrap(),
|
||||
Duration::seconds(39_398_402)
|
||||
now + parse_relative_time("1 year 2 months 4 weeks 3 days and 2 seconds").unwrap(),
|
||||
now.checked_add_months(Months::new(12 + 2))
|
||||
.unwrap()
|
||||
.checked_add_days(Days::new(4 * 7 + 3))
|
||||
.unwrap()
|
||||
.checked_add_signed(Duration::seconds(2))
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_relative_time("1 year 2 months 4 weeks 3 days and 2 seconds ago").unwrap(),
|
||||
Duration::seconds(-39_398_402)
|
||||
now + parse_relative_time("1 year 2 months 4 weeks 3 days and 2 seconds ago").unwrap(),
|
||||
now.checked_sub_months(Months::new(12 + 2))
|
||||
.unwrap()
|
||||
.checked_sub_days(Days::new(4 * 7 + 3))
|
||||
.unwrap()
|
||||
.checked_sub_signed(Duration::seconds(2))
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -601,22 +664,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_relative_time_at_date_day() {
|
||||
let today = Utc::now().date_naive();
|
||||
let yesterday = today - Duration::days(1);
|
||||
let now = Utc::now();
|
||||
let now_yesterday = now - Duration::days(1);
|
||||
assert_eq!(
|
||||
parse_relative_time_at_date(yesterday, "2 days").unwrap(),
|
||||
Duration::days(1)
|
||||
parse_relative_time_at_date(now_yesterday, "2 days").unwrap(),
|
||||
now + Duration::days(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_input_at_date_relative() {
|
||||
let today = Utc::now().date_naive();
|
||||
let result = parse_relative_time_at_date(today, "foobar");
|
||||
println!("{result:?}");
|
||||
let now = Utc::now();
|
||||
let result = parse_relative_time_at_date(now, "foobar");
|
||||
assert_eq!(result, Err(ParseDateTimeError::InvalidInput));
|
||||
|
||||
let result = parse_relative_time_at_date(today, "invalid 1r");
|
||||
let result = parse_relative_time_at_date(now, "invalid 1r");
|
||||
assert_eq!(result, Err(ParseDateTimeError::InvalidInput));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user