From 96db1a281e85b511a573482d9e7a0dfef63fb46b Mon Sep 17 00:00:00 2001 From: Patrick Klitzke Date: Wed, 23 Aug 2023 10:00:43 +0900 Subject: [PATCH] Support weekdays in parse_datetime This commit resolves issue #23. Adds parse_weekday function that uses chrono weekday parser with a map for edge cases. Adds tests cases to make sure it works correctly. Use nom for parsing. --- Cargo.lock | 17 +++++++ Cargo.toml | 1 + src/lib.rs | 78 +++++++++++++++++++++++++++++- src/parse_relative_time.rs | 2 + src/parse_weekday.rs | 99 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 src/parse_weekday.rs diff --git a/Cargo.lock b/Cargo.lock index 0d099a3..76586d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,22 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -138,6 +154,7 @@ name = "parse_datetime" version = "0.4.0" dependencies = [ "chrono", + "nom", "regex", ] diff --git a/Cargo.toml b/Cargo.toml index 9e9fd40..59f01ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ readme = "README.md" [dependencies] regex = "1.9" chrono = { version="0.4", default-features=false, features=["std", "alloc", "clock"] } +nom = "7.1.3" diff --git a/src/lib.rs b/src/lib.rs index 90e191b..7d9437e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,12 @@ use std::fmt::{self, Display}; // Expose parse_datetime mod parse_relative_time; -use chrono::{DateTime, FixedOffset, Local, LocalResult, NaiveDateTime, TimeZone}; +mod parse_weekday; + +use chrono::{ + DateTime, Datelike, Duration, FixedOffset, Local, LocalResult, NaiveDateTime, TimeZone, + Timelike, +}; use parse_relative_time::parse_relative_time; @@ -168,6 +173,27 @@ pub fn parse_datetime_at_date + Clone>( } } + // parse weekday + if let Some(weekday) = parse_weekday::parse_weekday(s.as_ref()) { + let mut beginning_of_day = date + .with_hour(0) + .unwrap() + .with_minute(0) + .unwrap() + .with_second(0) + .unwrap() + .with_nanosecond(0) + .unwrap(); + + while beginning_of_day.weekday() != weekday { + beginning_of_day += Duration::days(1); + } + + let dt = DateTime::::from(beginning_of_day); + + return Ok(dt); + } + // Parse epoch seconds if s.as_ref().bytes().next() == Some(b'@') { if let Ok(parsed) = NaiveDateTime::parse_from_str(&s.as_ref()[1..], "%s") { @@ -353,6 +379,56 @@ mod tests { } } + #[cfg(test)] + mod weekday { + use chrono::{DateTime, Local, TimeZone}; + + use crate::parse_datetime_at_date; + + fn get_formatted_date(date: DateTime, weekday: &str) -> String { + let result = parse_datetime_at_date(date, weekday).unwrap(); + + return result.format("%F %T %f").to_string(); + } + #[test] + fn test_weekday() { + // add some constant hours and minutes and seconds to check its reset + let date = Local.with_ymd_and_hms(2023, 02, 28, 10, 12, 3).unwrap(); + + // 2023-2-28 is tuesday + assert_eq!( + get_formatted_date(date, "tuesday"), + "2023-02-28 00:00:00 000000000" + ); + + // 2023-3-01 is wednesday + assert_eq!( + get_formatted_date(date, "wed"), + "2023-03-01 00:00:00 000000000" + ); + + assert_eq!( + get_formatted_date(date, "thu"), + "2023-03-02 00:00:00 000000000" + ); + + assert_eq!( + get_formatted_date(date, "fri"), + "2023-03-03 00:00:00 000000000" + ); + + assert_eq!( + get_formatted_date(date, "sat"), + "2023-03-04 00:00:00 000000000" + ); + + assert_eq!( + get_formatted_date(date, "sun"), + "2023-03-05 00:00:00 000000000" + ); + } + } + #[cfg(test)] mod timestamp { use crate::parse_datetime; diff --git a/src/parse_relative_time.rs b/src/parse_relative_time.rs index afa47b5..7bc0840 100644 --- a/src/parse_relative_time.rs +++ b/src/parse_relative_time.rs @@ -1,3 +1,5 @@ +// 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 regex::Regex; diff --git a/src/parse_weekday.rs b/src/parse_weekday.rs new file mode 100644 index 0000000..d61aca7 --- /dev/null +++ b/src/parse_weekday.rs @@ -0,0 +1,99 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +use chrono::Weekday; +use nom::branch::alt; +use nom::bytes::complete::tag; +use nom::combinator::value; +use nom::{self, IResult}; + +// Helper macro to simplify tag matching +macro_rules! tag_match { + ($day:expr, $($pattern:expr),+) => { + value($day, alt(($(tag($pattern)),+))) + }; +} + +pub(crate) fn parse_weekday(s: &str) -> Option { + let s = s.trim().to_lowercase(); + let s = s.as_str(); + + let parse_result: IResult<&str, Weekday> = nom::combinator::all_consuming(alt(( + tag_match!(Weekday::Mon, "monday", "mon"), + tag_match!(Weekday::Tue, "tuesday", "tues", "tue"), + tag_match!(Weekday::Wed, "wednesday", "wednes", "wed"), + tag_match!(Weekday::Thu, "thursday", "thurs", "thur", "thu"), + tag_match!(Weekday::Fri, "friday", "fri"), + tag_match!(Weekday::Sat, "saturday", "sat"), + tag_match!(Weekday::Sun, "sunday", "sun"), + )))(s); + + match parse_result { + Ok((_, weekday)) => Some(weekday), + Err(_) => None, + } +} + +#[cfg(test)] +mod tests { + + use chrono::Weekday::*; + + use crate::parse_weekday::parse_weekday; + + #[test] + fn test_valid_weekdays() { + let days = [ + ("mon", Mon), + ("monday", Mon), + ("tue", Tue), + ("tues", Tue), + ("tuesday", Tue), + ("wed", Wed), + ("wednes", Wed), + ("wednesday", Wed), + ("thu", Thu), + ("thursday", Thu), + ("fri", Fri), + ("friday", Fri), + ("sat", Sat), + ("saturday", Sat), + ("sun", Sun), + ("sunday", Sun), + ]; + + 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)); + + let (left, right) = name.split_at(1); + let (test_str1, test_str2) = ( + format!("{}{}", left.to_uppercase(), right.to_lowercase()), + format!("{}{}", left.to_lowercase(), right.to_uppercase()), + ); + + assert_eq!(parse_weekday(&test_str1), Some(weekday)); + assert_eq!(parse_weekday(&test_str2), Some(weekday)); + } + } + + #[test] + fn test_invalid_weekdays() { + let days = [ + "mond", + "tuesda", + "we", + "th", + "fr", + "sa", + "su", + "garbageday", + "tomorrow", + "yesterday", + ]; + for day in days { + assert!(parse_weekday(day).is_none()); + } + } +}