use chrono::Weekday;
use crate::date::{self, Date, DateError, DateRange};
pub struct DateParser {
base: Date
}
impl Default for DateParser {
fn default() -> Self { Self { base: Date::today() } }
}
impl DateParser {
fn new(base: Date) -> Self { Self { base } }
pub fn parse(&self, datespec: &str) -> date::Result<Date> {
match datespec {
"today" => Ok(self.base),
"yesterday" => Ok(self.base.pred()),
"sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" => {
Ok(self.base.find_previous(weekday_from_str(datespec)?))
}
_ => datespec.parse()
}
}
}
pub struct RangeParser {
base: Date
}
impl Default for RangeParser {
fn default() -> Self { Self { base: Date::today() } }
}
fn weekday_from_str(day: &str) -> date::Result<Weekday> {
match day {
"sunday" => Ok(Weekday::Sun),
"monday" => Ok(Weekday::Mon),
"tuesday" => Ok(Weekday::Tue),
"wednesday" => Ok(Weekday::Wed),
"thursday" => Ok(Weekday::Thu),
"friday" => Ok(Weekday::Fri),
"saturday" => Ok(Weekday::Sat),
_ => Err(DateError::InvalidDaySpec(day.to_string()))
}
}
fn month_from_name(name: &str) -> Option<u32> {
let month = match name {
"january" | "jan" => 1,
"february" | "feb" => 2,
"march" | "mar" => 3,
"april" | "apr" => 4,
"may" => 5,
"june" | "jun" => 6,
"july" | "jul" => 7,
"august" | "aug" => 8,
"september" | "sep" | "sept" => 9,
"october" | "oct" => 10,
"november" | "nov" => 11,
"december" | "dec" => 12,
_ => return None
};
Some(month)
}
impl RangeParser {
#[cfg(test)]
pub fn new(base: Date) -> Self { Self { base } }
pub fn parse_from_str(&self, datespec: &str) -> date::Result<DateRange> {
if datespec.is_empty() { return Ok(self.base.into()); }
let mut iter = datespec.split_ascii_whitespace();
self.parse(&mut iter).map(|(r, _)| r)
}
pub fn parse<'a, I>(&self, datespec: &mut I) -> date::Result<(DateRange, &'a str)>
where
I: Iterator<Item = &'a str>
{
let Some(token) = datespec.next() else {
return Ok((self.base.into(), ""));
};
let ltoken = token.to_ascii_lowercase();
if let Some(range) = self.month_range(ltoken.as_str()) {
return Ok((range, ""));
}
let base = self.base;
let range_opt = match ltoken.as_str() {
"ytd" => {
let start = base.year_start();
DateRange::new_opt(start, base.succ())
}
"this" => {
let Some(token) = datespec.next() else {
return Err(DateError::InvalidDaySpec(token.into()));
};
let ltoken = token.to_ascii_lowercase();
match ltoken.as_str() {
"week" => DateRange::new_opt(base.week_start(), base.week_end().succ()),
"month" => DateRange::new_opt(base.month_start(), base.month_end().succ()),
"year" => DateRange::new_opt(base.year_start(), base.year_end().succ()),
_ => return Err(DateError::InvalidDaySpec(token.into()))
}
}
"last" => {
let Some(token) = datespec.next() else {
return Err(DateError::InvalidDate);
};
let ltoken = token.to_ascii_lowercase();
match ltoken.as_str() {
"week" => {
let date = base.week_start();
DateRange::new_opt(date.pred().week_start(), date)
}
"month" => {
let date = base.month_start().pred().month_start();
DateRange::new_opt(date, date.month_end().succ())
}
"year" => {
let date = Date::new(base.year() - 1, 1, 1)?;
DateRange::new_opt(date, date.year_end().succ())
}
_ => return Err(DateError::InvalidDaySpec(token.into()))
}
}
_ => None
};
if let Some(date_range) = range_opt {
return Ok((date_range, ""));
}
let dparser = DateParser::new(self.base);
let Ok(start) = dparser.parse(<oken) else {
return Ok((self.base.into(), token));
};
if let Some(token) = datespec.next() {
let ltoken = token.to_ascii_lowercase();
if let Ok(end) = dparser.parse(<oken) {
let range = DateRange::new_opt(start, end.succ())
.ok_or(DateError::WrongDateOrder)?;
return Ok((range, ""));
}
else {
return Ok((start.into(), token));
}
}
Ok((start.into(), ""))
}
fn month_range(&self, token: &str) -> Option<DateRange> {
let month = month_from_name(token)?;
let this = self.base.month();
let year = self.base.year();
let year = if month < this { year } else { year - 1 };
let start = Date::new(year, month, 1).ok()?;
Some(DateRange { start, end: start.month_end().succ() })
}
}
#[cfg(test)]
mod tests {
use once_cell::sync::Lazy;
use assert2::{assert, let_assert};
use rstest::rstest;
use super::*;
use crate::date::{DateTime, Weekday};
static BASE_DATE: Lazy<Date> = Lazy::new(
|| Date::new(2022, 11, 15).expect("Hardcoded value") );
static YESTERDAY: Lazy<Date> = Lazy::new(
||Date::new(2022, 11, 14).expect("Hardcoded date must work")
);
static HARD_DATE: Lazy<Date> = Lazy::new(
||Date::new(2022, 10, 20).expect("Hardcoded date must work")
);
#[rstest]
#[case("today", Date::today(), "today")]
#[case("yesterday", Date::today().pred(), "yesterday")]
fn test_date_parse(#[case]input: &str, #[case]expected: Date, #[case]msg: &str) {
let p = DateParser::default();
let_assert!(Ok(actual) = p.parse(input));
assert!(actual == expected, "{msg}");
}
#[test]
fn test_date_parse_weekdays() {
let max_dur = DateTime::days(7);
#[rustfmt::skip]
let days: [&str; 7] = [
"sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"
];
let today = Date::today();
let midnight = Date::today().day_end();
let p = DateParser::default();
days.iter().for_each(|&day| {
let_assert!(Ok(date) = p.parse(day), "parse {day}");
assert!(date < today);
let end_of_date = date.day_end();
let_assert!(Ok(dur) = midnight - end_of_date, "end {day}");
assert!(dur <= max_dur);
});
}
fn test_range_parser() -> RangeParser { RangeParser::new(*BASE_DATE) }
#[test]
fn test_parse_default() {
let p = RangeParser::default();
let expect: DateRange = DateRange::default();
let_assert!(Ok(actual) = p.parse_from_str(""));
assert!(actual == expect);
}
#[rstest]
#[case("", (*BASE_DATE).into(), "new")]
#[case("today", (*BASE_DATE).into(), "today")]
#[case("yesterday", (*YESTERDAY).into(), "yesterday")]
#[case("2022-10-20", (*HARD_DATE).into(), "actual date")]
#[case("monday", DateRange::from(BASE_DATE.find_previous(Weekday::Mon)), "dayname")]
#[case("wednesday", DateRange::from(BASE_DATE.find_previous(Weekday::Wed)), "later dayname")]
fn test_date_range_parser_one_day(#[case]input: &str, #[case]expect: DateRange, #[case]msg: &str) {
let p = test_range_parser();
let_assert!(Ok(actual) = p.parse_from_str(input));
assert!(actual == expect, "{msg}");
}
#[test]
fn test_dates_both_dates() {
let_assert!(Ok(start) = Date::new(2021, 12, 1));
let_assert!(Ok(end) = Date::new(2021, 12, 8));
let expected = DateRange::new(start, end);
let p = test_range_parser();
let_assert!(Ok(actual) = p.parse_from_str("2021-12-01 2021-12-07"));
assert!(actual == expected);
}
#[test]
fn test_dates_both_dates_desc() {
let_assert!(Ok(start) = Date::new(2022, 11, 13));
let expected = DateRange::new(start, BASE_DATE.succ());
let p = test_range_parser();
let_assert!(Ok(actual) = p.parse_from_str("sunday today"));
assert!(actual == expected);
}
#[rstest]
#[case("january", Date::new(2022, 1, 1), Date::new(2022, 2, 1))]
#[case("jan", Date::new(2022, 1, 1), Date::new(2022, 2, 1))]
#[case("february", Date::new(2022, 2, 1), Date::new(2022, 3, 1))]
#[case("feb", Date::new(2022, 2, 1), Date::new(2022, 3, 1))]
#[case("march", Date::new(2022, 3, 1), Date::new(2022, 4, 1))]
#[case("mar", Date::new(2022, 3, 1), Date::new(2022, 4, 1))]
#[case("april", Date::new(2022, 4, 1), Date::new(2022, 5, 1))]
#[case("apr", Date::new(2022, 4, 1), Date::new(2022, 5, 1))]
#[case("may", Date::new(2022, 5, 1), Date::new(2022, 6, 1))]
#[case("june", Date::new(2022, 6, 1), Date::new(2022, 7, 1))]
#[case("jun", Date::new(2022, 6, 1), Date::new(2022, 7, 1))]
#[case("july", Date::new(2022, 7, 1), Date::new(2022, 8, 1))]
#[case("jul", Date::new(2022, 7, 1), Date::new(2022, 8, 1))]
#[case("august", Date::new(2022, 8, 1), Date::new(2022, 9, 1))]
#[case("aug", Date::new(2022, 8, 1), Date::new(2022, 9, 1))]
#[case("september", Date::new(2022, 9, 1), Date::new(2022, 10, 1))]
#[case("sep", Date::new(2022, 9, 1), Date::new(2022, 10, 1))]
#[case("october", Date::new(2022, 10, 1), Date::new(2022, 11, 1))]
#[case("oct", Date::new(2022, 10, 1), Date::new(2022, 11, 1))]
#[case("november", Date::new(2021, 11, 1), Date::new(2021, 12, 1))]
#[case("nov", Date::new(2021, 11, 1), Date::new(2021, 12, 1))]
#[case("december", Date::new(2021, 12, 1), Date::new(2022, 1, 1))]
#[case("dec", Date::new(2021, 12, 1), Date::new(2022, 1, 1))]
fn test_month_name(
#[case]name: &str,
#[case]start_opt: Result<Date, DateError>,
#[case]end_opt: Result<Date, DateError>
) {
let p = test_range_parser();
let_assert!(Ok(start) = start_opt.as_ref());
let_assert!(Ok(end) = end_opt.as_ref());
let expected = DateRange::new(*start, *end);
let_assert!(Ok(actual) = p.parse_from_str(name));
assert!(actual == expected);
}
#[rstest]
#[case("this week", Date::new(2022, 11, 13), Date::new(2022, 11, 20))]
#[case("this month", Date::new(2022, 11, 1), Date::new(2022, 12, 1))]
#[case("this year", Date::new(2022, 1, 1), Date::new(2023, 1, 1))]
#[case("ytd", Date::new(2022, 1, 1), Ok(BASE_DATE.succ()))]
#[case("last week", Date::new(2022, 11, 6), Date::new(2022, 11, 13))]
#[case("last month", Date::new(2022, 10, 1), Date::new(2022, 11, 1))]
#[case("last year", Date::new(2021, 1, 1), Date::new(2022, 1, 1))]
fn test_special_range(
#[case]input: &str,
#[case]start_opt: Result<Date, DateError>,
#[case]end_opt: Result<Date, DateError>
) {
let_assert!(Ok(start) = start_opt);
let_assert!(Ok(end) = end_opt);
let expected = DateRange::new(start, end);
let p = test_range_parser();
let_assert!(Ok(actual) = p.parse_from_str(input), "parsing '{input}'");
assert!(actual == expected);
}
}