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.
This commit is contained in:
Patrick Klitzke
2023-08-23 10:00:43 +09:00
parent 2737b4a0f2
commit 96db1a281e
5 changed files with 196 additions and 1 deletions
Generated
+17
View File
@@ -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",
]
+1
View File
@@ -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"
+77 -1
View File
@@ -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<S: AsRef<str> + 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::<FixedOffset>::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<Local>, 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;
+2
View File
@@ -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;
+99
View File
@@ -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<Weekday> {
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());
}
}
}