Skip to content

Commit 33d0670

Browse files
committed
Parse relative weekdays like "next monday"
1 parent 9184428 commit 33d0670

File tree

3 files changed

+211
-27
lines changed

3 files changed

+211
-27
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "parse_datetime"
33
description = "parsing human-readable time strings and converting them to a DateTime"
4-
version = "0.8.0"
4+
version = "0.9.0"
55
edition = "2021"
66
license = "MIT"
77
repository = "https://github.com/uutils/parse_datetime"

src/parse_relative_time.rs

Lines changed: 209 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// For the full copyright and license information, please view the LICENSE
22
// file that was distributed with this source code.
3-
use crate::ParseDateTimeError;
3+
use crate::{parse_weekday::parse_weekday, ParseDateTimeError};
44
use chrono::{
55
DateTime, Datelike, Days, Duration, LocalResult, Months, NaiveDate, NaiveDateTime, TimeZone,
6+
Weekday,
67
};
78
use regex::Regex;
89

@@ -61,7 +62,7 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
6162
r"(?x)
6263
(?:(?P<value>[-+]?\d*)\s*)?
6364
(\s*(?P<direction>next|this|last)?\s*)?
64-
(?P<unit>years?|months?|fortnights?|weeks?|days?|hours?|h|minutes?|mins?|m|seconds?|secs?|s|yesterday|tomorrow|now|today)
65+
(?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
6566
(\s*(?P<separator>and|,)?\s*)?
6667
(\s*(?P<ago>ago)?)?",
6768
)?;
@@ -77,16 +78,19 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
7778
.name("value")
7879
.ok_or(ParseDateTimeError::InvalidInput)?
7980
.as_str();
81+
let direction = capture.name("direction").map_or("", |d| d.as_str());
8082
let value = if value_str.is_empty() {
81-
1
83+
if direction == "this" {
84+
0
85+
} else {
86+
1
87+
}
8288
} else {
8389
value_str
8490
.parse::<i64>()
8591
.map_err(|_| ParseDateTimeError::InvalidInput)?
8692
};
8793

88-
let direction = capture.name("direction").map_or("", |d| d.as_str());
89-
9094
if direction == "last" {
9195
is_ago = true;
9296
}
@@ -100,27 +104,26 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
100104
is_ago = true;
101105
}
102106

103-
let new_datetime = if direction == "this" {
104-
add_days(datetime, 0, is_ago)
105-
} else {
106-
match unit {
107-
"years" | "year" => add_months(datetime, value * 12, is_ago),
108-
"months" | "month" => add_months(datetime, value, is_ago),
109-
"fortnights" | "fortnight" => add_days(datetime, value * 14, is_ago),
110-
"weeks" | "week" => add_days(datetime, value * 7, is_ago),
111-
"days" | "day" => add_days(datetime, value, is_ago),
112-
"hours" | "hour" | "h" => add_duration(datetime, Duration::hours(value), is_ago),
113-
"minutes" | "minute" | "mins" | "min" | "m" => {
114-
add_duration(datetime, Duration::minutes(value), is_ago)
115-
}
116-
"seconds" | "second" | "secs" | "sec" | "s" => {
117-
add_duration(datetime, Duration::seconds(value), is_ago)
118-
}
119-
"yesterday" => add_days(datetime, 1, true),
120-
"tomorrow" => add_days(datetime, 1, false),
121-
"now" | "today" => Some(datetime),
122-
_ => None,
107+
let new_datetime = match unit {
108+
"years" | "year" => add_months(datetime, value * 12, is_ago),
109+
"months" | "month" => add_months(datetime, value, is_ago),
110+
"fortnights" | "fortnight" => add_days(datetime, value * 14, is_ago),
111+
"weeks" | "week" => add_days(datetime, value * 7, is_ago),
112+
"days" | "day" => add_days(datetime, value, is_ago),
113+
"hours" | "hour" | "h" => add_duration(datetime, Duration::hours(value), is_ago),
114+
"minutes" | "minute" | "mins" | "min" | "m" => {
115+
add_duration(datetime, Duration::minutes(value), is_ago)
116+
}
117+
"seconds" | "second" | "secs" | "sec" | "s" => {
118+
add_duration(datetime, Duration::seconds(value), is_ago)
123119
}
120+
"yesterday" => add_days(datetime, 1, true),
121+
"tomorrow" => add_days(datetime, 1, false),
122+
"now" | "today" => Some(datetime),
123+
_ => capture
124+
.name("weekday")
125+
.and_then(|weekday| parse_weekday(weekday.as_str()))
126+
.and_then(|weekday| adjust_for_weekday(datetime, weekday, value, is_ago)),
124127
};
125128
datetime = match new_datetime {
126129
Some(dt) => dt,
@@ -145,6 +148,23 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
145148
}
146149
}
147150

151+
fn adjust_for_weekday<T: TimeZone>(
152+
mut datetime: DateTime<T>,
153+
weekday: Weekday,
154+
mut amount: i64,
155+
is_ago: bool,
156+
) -> Option<DateTime<T>> {
157+
let mut same_day = true;
158+
while datetime.weekday() != weekday {
159+
datetime = add_days(datetime, 1, is_ago)?;
160+
same_day = false;
161+
}
162+
if !same_day && 0 < amount {
163+
amount -= 1;
164+
}
165+
add_days(datetime, amount * 7, is_ago)
166+
}
167+
148168
fn add_months<T: TimeZone>(
149169
datetime: DateTime<T>,
150170
months: i64,
@@ -794,4 +814,168 @@ mod tests {
794814
let result = parse_relative_time_at_date(now, "invalid 1r");
795815
assert_eq!(result, Err(ParseDateTimeError::InvalidInput));
796816
}
817+
818+
#[test]
819+
fn test_parse_relative_time_at_date_this_weekday() {
820+
// Jan 1 2025 is a Wed
821+
let now = Utc.from_utc_datetime(&NaiveDateTime::new(
822+
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
823+
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
824+
));
825+
// Check "this <same weekday>"
826+
assert_eq!(
827+
parse_relative_time_at_date(now, "this wednesday").unwrap(),
828+
now
829+
);
830+
assert_eq!(parse_relative_time_at_date(now, "this wed").unwrap(), now);
831+
// Other days
832+
assert_eq!(
833+
parse_relative_time_at_date(now, "this thursday").unwrap(),
834+
now.checked_add_days(Days::new(1)).unwrap()
835+
);
836+
assert_eq!(
837+
parse_relative_time_at_date(now, "this thur").unwrap(),
838+
now.checked_add_days(Days::new(1)).unwrap()
839+
);
840+
assert_eq!(
841+
parse_relative_time_at_date(now, "this thu").unwrap(),
842+
now.checked_add_days(Days::new(1)).unwrap()
843+
);
844+
assert_eq!(
845+
parse_relative_time_at_date(now, "this friday").unwrap(),
846+
now.checked_add_days(Days::new(2)).unwrap()
847+
);
848+
assert_eq!(
849+
parse_relative_time_at_date(now, "this fri").unwrap(),
850+
now.checked_add_days(Days::new(2)).unwrap()
851+
);
852+
assert_eq!(
853+
parse_relative_time_at_date(now, "this saturday").unwrap(),
854+
now.checked_add_days(Days::new(3)).unwrap()
855+
);
856+
assert_eq!(
857+
parse_relative_time_at_date(now, "this sat").unwrap(),
858+
now.checked_add_days(Days::new(3)).unwrap()
859+
);
860+
// "this" with a day of the week that comes before today should return the next instance of
861+
// that day
862+
assert_eq!(
863+
parse_relative_time_at_date(now, "this sunday").unwrap(),
864+
now.checked_add_days(Days::new(4)).unwrap()
865+
);
866+
assert_eq!(
867+
parse_relative_time_at_date(now, "this sun").unwrap(),
868+
now.checked_add_days(Days::new(4)).unwrap()
869+
);
870+
assert_eq!(
871+
parse_relative_time_at_date(now, "this monday").unwrap(),
872+
now.checked_add_days(Days::new(5)).unwrap()
873+
);
874+
assert_eq!(
875+
parse_relative_time_at_date(now, "this mon").unwrap(),
876+
now.checked_add_days(Days::new(5)).unwrap()
877+
);
878+
assert_eq!(
879+
parse_relative_time_at_date(now, "this tuesday").unwrap(),
880+
now.checked_add_days(Days::new(6)).unwrap()
881+
);
882+
assert_eq!(
883+
parse_relative_time_at_date(now, "this tue").unwrap(),
884+
now.checked_add_days(Days::new(6)).unwrap()
885+
);
886+
}
887+
888+
#[test]
889+
fn test_parse_relative_time_at_date_last_weekday() {
890+
// Jan 1 2025 is a Wed
891+
let now = Utc.from_utc_datetime(&NaiveDateTime::new(
892+
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
893+
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
894+
));
895+
// Check "last <same weekday>"
896+
assert_eq!(
897+
parse_relative_time_at_date(now, "last wed").unwrap(),
898+
now.checked_sub_days(Days::new(7)).unwrap()
899+
);
900+
// Check "last <day after today>"
901+
assert_eq!(
902+
parse_relative_time_at_date(now, "last thu").unwrap(),
903+
now.checked_sub_days(Days::new(6)).unwrap()
904+
);
905+
// Check "last <day before today>"
906+
assert_eq!(
907+
parse_relative_time_at_date(now, "last tue").unwrap(),
908+
now.checked_sub_days(Days::new(1)).unwrap()
909+
);
910+
}
911+
912+
#[test]
913+
fn test_parse_relative_time_at_date_next_weekday() {
914+
// Jan 1 2025 is a Wed
915+
let now = Utc.from_utc_datetime(&NaiveDateTime::new(
916+
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
917+
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
918+
));
919+
// Check "next <same weekday>"
920+
assert_eq!(
921+
parse_relative_time_at_date(now, "next wed").unwrap(),
922+
now.checked_add_days(Days::new(7)).unwrap()
923+
);
924+
// Check "next <day after today>"
925+
assert_eq!(
926+
parse_relative_time_at_date(now, "next thu").unwrap(),
927+
now.checked_add_days(Days::new(1)).unwrap()
928+
);
929+
// Check "next <day before today>"
930+
assert_eq!(
931+
parse_relative_time_at_date(now, "next tue").unwrap(),
932+
now.checked_add_days(Days::new(6)).unwrap()
933+
);
934+
}
935+
936+
#[test]
937+
fn test_parse_relative_time_at_date_number_weekday() {
938+
// Jan 1 2025 is a Wed
939+
let now = Utc.from_utc_datetime(&NaiveDateTime::new(
940+
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
941+
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
942+
));
943+
assert_eq!(
944+
parse_relative_time_at_date(now, "1 wed").unwrap(),
945+
now.checked_add_days(Days::new(7)).unwrap()
946+
);
947+
assert_eq!(
948+
parse_relative_time_at_date(now, "1 thu").unwrap(),
949+
now.checked_add_days(Days::new(1)).unwrap()
950+
);
951+
assert_eq!(
952+
parse_relative_time_at_date(now, "1 tue").unwrap(),
953+
now.checked_add_days(Days::new(6)).unwrap()
954+
);
955+
assert_eq!(
956+
parse_relative_time_at_date(now, "2 wed").unwrap(),
957+
now.checked_add_days(Days::new(14)).unwrap()
958+
);
959+
assert_eq!(
960+
parse_relative_time_at_date(now, "2 thu").unwrap(),
961+
now.checked_add_days(Days::new(8)).unwrap()
962+
);
963+
assert_eq!(
964+
parse_relative_time_at_date(now, "2 tue").unwrap(),
965+
now.checked_add_days(Days::new(13)).unwrap()
966+
);
967+
}
968+
969+
#[test]
970+
fn test_parse_relative_time_at_date_invalid_weekday() {
971+
// Jan 1 2025 is a Wed
972+
let now = Utc.from_utc_datetime(&NaiveDateTime::new(
973+
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
974+
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
975+
));
976+
assert_eq!(
977+
parse_relative_time_at_date(now, "this fooday"),
978+
Err(ParseDateTimeError::InvalidInput)
979+
);
980+
}
797981
}

0 commit comments

Comments
 (0)