use chrono::{Datelike, Duration, NaiveDate, Weekday};
use std::mem;
use todo_lib::{terr, tfilter};
const NO_CHANGE: &str = "no change";
const DAYS_PER_WEEK: u32 = 7;
const FAR_PAST: i64 = -100 * 365;
type HumanResult = Result<NaiveDate, String>;
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum CalendarRangeType {
Days(i8),
Weeks(i8),
Months(i8),
}
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub struct CalendarRange {
pub(crate) strict: bool,
pub(crate) rng: CalendarRangeType,
}
impl Default for CalendarRange {
fn default() -> CalendarRange {
CalendarRange { strict: false, rng: CalendarRangeType::Days(1) }
}
}
fn parse_int(s: &str) -> (&str, String) {
let mut res = String::new();
for c in s.chars() {
if !('0'..='9').contains(&c) {
break;
}
res.push(c);
}
(&s[res.len()..], res)
}
impl CalendarRange {
pub(crate) fn parse(s: &str) -> Result<CalendarRange, terr::TodoError> {
let mut rng = CalendarRange::default();
let (s, strict) = if s.starts_with('+') { (&s["+".len()..], true) } else { (s, false) };
let (s, sgn) = if s.starts_with('-') { (&s["-".len()..], -1) } else { (s, 1) };
rng.strict = strict;
let (s, num_str) = parse_int(s);
let num = if num_str.is_empty() {
1
} else {
match num_str.parse::<i8>() {
Ok(n) => n,
Err(_) => return Err(terr::TodoError::InvalidValue(s.to_string(), "calendar range value".to_string())),
}
};
let num = num * sgn;
rng.rng = match s {
"" | "d" | "D" => {
if num.abs() > 100 {
return Err(terr::TodoError::InvalidValue(num_str, "number of days(range -100..100)".to_string()));
}
CalendarRangeType::Days(num)
}
"w" | "W" => {
if num.abs() > 16 {
return Err(terr::TodoError::InvalidValue(num_str, "number of weeks(range -16..16)".to_string()));
}
CalendarRangeType::Weeks(num)
}
"m" | "M" => {
if num.abs() > 3 {
return Err(terr::TodoError::InvalidValue(num_str, "number of months(range -3..3)".to_string()));
}
CalendarRangeType::Months(num)
}
_ => return Err(terr::TodoError::InvalidValue(s.to_string(), "calendar range type".to_string())),
};
Ok(rng)
}
}
fn days_in_month(y: i32, m: u32) -> u32 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
2 => {
if y % 4 == 0 {
if y % 100 == 0 && y % 400 != 0 {
28
} else {
29
}
} else {
28
}
}
_ => 30,
}
}
fn add_months(dt: NaiveDate, num: u32, back: bool) -> NaiveDate {
let mut y = dt.year();
let mut m = dt.month();
let mut d = dt.day();
let mxd = days_in_month(y, m);
if back {
let full_years = num / 12;
let num = num % 12;
y -= full_years as i32;
m = if m > num {
m - num
} else {
y -= 1;
m + 12 - num
};
} else {
m += num;
if m > 12 {
m -= 1;
y += (m / 12) as i32;
m = (m % 12) + 1;
}
}
let new_mxd = days_in_month(y, m);
if mxd > d || d == mxd {
if d == mxd || new_mxd < d {
d = new_mxd
}
NaiveDate::from_ymd_opt(y, m, d).unwrap_or(dt)
} else {
NaiveDate::from_ymd_opt(y, m, new_mxd).unwrap_or(dt)
}
}
fn abs_time_diff(base: NaiveDate, human: &str, back: bool) -> HumanResult {
let mut num = 0u32;
let mut dt = base;
for c in human.chars() {
match c.to_digit(10) {
None => {
if num != 0 {
match c {
'd' => {
let dur = if back { Duration::days(-(num as i64)) } else { Duration::days(num as i64) };
dt += dur;
}
'w' => {
let dur = if back { Duration::weeks(-(num as i64)) } else { Duration::weeks(num as i64) };
dt += dur;
}
'm' => {
dt = add_months(dt, num, back);
}
'y' => {
let mut y = dt.year();
let m = dt.month();
let mut d = dt.day();
let mxd = days_in_month(y, m);
if back {
y -= num as i32;
} else {
y += num as i32;
};
let new_mxd = days_in_month(y, m);
if mxd > d || d == mxd {
if new_mxd < d || d == mxd {
d = new_mxd;
}
dt = NaiveDate::from_ymd_opt(y, m, d).unwrap_or(base);
} else {
dt = NaiveDate::from_ymd_opt(y, m, new_mxd).unwrap_or(base);
}
}
_ => {}
}
num = 0;
}
}
Some(i) => num = num * 10 + i,
}
}
if base == dt {
return Err(format!("invalid date '{human}'"));
}
Ok(dt)
}
pub(crate) fn next_weekday(base: NaiveDate, wd: Weekday) -> NaiveDate {
let base_wd = base.weekday();
let (bn, wn) = (base_wd.number_from_monday(), wd.number_from_monday());
if bn < wn {
base + Duration::days((wn - bn) as i64)
} else {
base + Duration::days((DAYS_PER_WEEK + wn - bn) as i64)
}
}
pub(crate) fn prev_weekday(base: NaiveDate, wd: Weekday) -> NaiveDate {
let base_wd = base.weekday();
let (bn, wn) = (base_wd.number_from_monday(), wd.number_from_monday());
if bn > wn {
base - Duration::days(bn as i64 - wn as i64)
} else {
base + Duration::days(wn as i64 - bn as i64 - DAYS_PER_WEEK as i64)
}
}
fn day_of_first_month(base: NaiveDate, human: &str) -> HumanResult {
match human.parse::<u32>() {
Err(e) => Err(format!("invalid day of month: {e:?}")),
Ok(n) => {
if n == 0 || n > 31 {
Err(format!("Day number too big: {n}"))
} else {
let mut m = base.month();
let mut y = base.year();
let mut d = base.day();
let bdays = days_in_month(y, m);
if d >= n {
if m == 12 {
m = 1;
y += 1;
} else {
m += 1;
}
}
d = if n >= days_in_month(y, m) || n >= bdays { days_in_month(y, m) } else { n };
Ok(NaiveDate::from_ymd_opt(y, m, d).unwrap_or(base))
}
}
}
}
fn no_year_date(base: NaiveDate, human: &str) -> HumanResult {
let parts: Vec<_> = human.split('-').collect();
if parts.len() != 2 {
return Err("expected date in format MONTH-DAY".to_string());
}
let y = base.year();
let m = match parts[0].parse::<u32>() {
Err(_) => return Err(format!("invalid month number: {}", parts[0])),
Ok(n) => {
if !(1..=12).contains(&n) {
return Err(format!("month number must be between 1 and 12 ({n})"));
}
n
}
};
let d = match parts[1].parse::<u32>() {
Err(_) => return Err(format!("invalid day number: {}", parts[1])),
Ok(n) => {
if !(1..=31).contains(&n) {
return Err(format!("day number must be between 1 and 31 ({n})"));
}
let mx = days_in_month(y, m);
if n > mx {
mx
} else {
n
}
}
};
let dt = NaiveDate::from_ymd_opt(y, m, d).unwrap_or(base);
if dt < base {
let y = y + 1;
let mx = days_in_month(y, m);
let d = if mx < d { mx } else { d };
Ok(NaiveDate::from_ymd_opt(y, m, d).unwrap_or(base))
} else {
Ok(dt)
}
}
fn is_absolute(name: &str) -> bool {
matches!(name, "today" | "tomorrow" | "tmr" | "tm" | "yesterday" | "overdue")
}
fn special_time_point(base: NaiveDate, human: &str, back: bool, soon_days: u8) -> HumanResult {
let s = human.replace(&['-', '_'][..], "").to_lowercase();
if back && is_absolute(human) {
return Err(format!("'{human}' cannot be back"));
}
match s.as_str() {
"today" => Ok(base),
"tomorrow" | "tmr" | "tm" => Ok(base.succ_opt().unwrap_or(base)),
"yesterday" => Ok(base.pred_opt().unwrap_or(base)),
"overdue" => Ok(base + Duration::days(FAR_PAST)),
"soon" => {
let dur = Duration::days(soon_days as i64);
Ok(if back { base - dur } else { base + dur })
}
"first" => {
let mut y = base.year();
let mut m = base.month();
let d = base.day();
if !back {
if m < 12 {
m += 1;
} else {
y += 1;
m = 1;
}
} else if d == 1 {
if m == 1 {
m = 12;
y -= 1;
} else {
m -= 1;
}
}
Ok(NaiveDate::from_ymd_opt(y, m, 1).unwrap_or(base))
}
"last" => {
let mut y = base.year();
let mut m = base.month();
let mut d = base.day();
let last_day = days_in_month(y, m);
if back {
if m == 1 {
m = 12;
y -= 1;
} else {
m -= 1;
}
} else if d == last_day {
if m < 12 {
m += 1;
} else {
m = 1;
y += 1;
}
}
d = days_in_month(y, m);
Ok(NaiveDate::from_ymd_opt(y, m, d).unwrap_or(base))
}
"monday" | "mon" | "mo" => {
if back {
Ok(prev_weekday(base, Weekday::Mon))
} else {
Ok(next_weekday(base, Weekday::Mon))
}
}
"tuesday" | "tue" | "tu" => {
if back {
Ok(prev_weekday(base, Weekday::Tue))
} else {
Ok(next_weekday(base, Weekday::Tue))
}
}
"wednesday" | "wed" | "we" => {
if back {
Ok(prev_weekday(base, Weekday::Wed))
} else {
Ok(next_weekday(base, Weekday::Wed))
}
}
"thursday" | "thu" | "th" => {
if back {
Ok(prev_weekday(base, Weekday::Thu))
} else {
Ok(next_weekday(base, Weekday::Thu))
}
}
"friday" | "fri" | "fr" => {
if back {
Ok(prev_weekday(base, Weekday::Fri))
} else {
Ok(next_weekday(base, Weekday::Fri))
}
}
"saturday" | "sat" | "sa" => {
if back {
Ok(prev_weekday(base, Weekday::Sat))
} else {
Ok(next_weekday(base, Weekday::Sat))
}
}
"sunday" | "sun" | "su" => {
if back {
Ok(prev_weekday(base, Weekday::Sun))
} else {
Ok(next_weekday(base, Weekday::Sun))
}
}
_ => Err(format!("invalid date '{human}'")),
}
}
pub fn human_to_date(base: NaiveDate, human: &str, soon_days: u8) -> HumanResult {
if human.is_empty() {
return Err("empty date".to_string());
}
let back = human.starts_with('-');
let human = if back { &human[1..] } else { human };
if human.find(|c: char| !('0'..='9').contains(&c)).is_none() {
if back {
return Err("negative day of month".to_string());
}
return day_of_first_month(base, human);
}
if human.find(|c: char| !('0'..='9').contains(&c) && c != '-').is_none() {
if back {
return Err("negative absolute date".to_string());
}
if human.matches('-').count() == 1 {
return no_year_date(base, human);
}
return Err(NO_CHANGE.to_string());
}
if human.find(|c: char| c < '0' || (c > '9' && c != 'd' && c != 'm' && c != 'w' && c != 'y')).is_none() {
return abs_time_diff(base, human, back);
}
special_time_point(base, human, back, soon_days)
}
pub fn fix_date(base: NaiveDate, orig: &str, look_for: &str, soon_days: u8) -> Option<String> {
if orig.is_empty() || look_for.is_empty() {
return None;
}
let spaced = " ".to_string() + look_for;
let start = if orig.starts_with(look_for) {
0
} else if let Some(p) = orig.find(&spaced) {
p + " ".len()
} else {
return None;
};
let substr = &orig[start + look_for.len()..];
let human = if let Some(p) = substr.find(' ') { &substr[..p] } else { substr };
match human_to_date(base, human, soon_days) {
Err(e) => {
if e != NO_CHANGE {
eprintln!("invalid due date: {human}");
}
None
}
Ok(new_date) => {
let what = look_for.to_string() + human;
let with = look_for.to_string() + new_date.format("%Y-%m-%d").to_string().as_str();
Some(orig.replace(what.as_str(), with.as_str()))
}
}
}
pub(crate) fn is_range_with_none(human: &str) -> bool {
if !is_range(human) {
return false;
}
human.starts_with("none..") || human.ends_with("..none") || human.starts_with("none:") || human.ends_with(":none")
}
pub(crate) fn human_to_range_with_none(
base: NaiveDate,
human: &str,
soon_days: u8,
) -> Result<tfilter::DateRange, terr::TodoError> {
let parts: Vec<&str> = if human.find(':').is_none() {
human.split("..").filter(|s| !s.is_empty()).collect()
} else {
human.split(':').filter(|s| !s.is_empty()).collect()
};
if parts.len() > 2 {
return Err(range_error(human));
}
if parts[1] == "none" {
match human_to_date(base, parts[0], soon_days) {
Err(e) => Err(range_error(&e)),
Ok(d) => Ok(tfilter::DateRange {
days: tfilter::ValueRange { high: tfilter::INCLUDE_NONE, low: (d - base).num_days() },
span: tfilter::ValueSpan::Range,
}),
}
} else if parts[0] == "none" {
match human_to_date(base, parts[1], soon_days) {
Err(e) => Err(range_error(&e)),
Ok(d) => Ok(tfilter::DateRange {
days: tfilter::ValueRange { low: tfilter::INCLUDE_NONE, high: (d - base).num_days() },
span: tfilter::ValueSpan::Range,
}),
}
} else {
Err(range_error(human))
}
}
pub(crate) fn is_range(human: &str) -> bool {
human.contains("..") || human.contains(':')
}
fn range_error(msg: &str) -> terr::TodoError {
terr::TodoError::InvalidValue(msg.to_string(), "date range".to_string())
}
pub(crate) fn human_to_range(
base: NaiveDate,
human: &str,
soon_days: u8,
) -> Result<tfilter::DateRange, terr::TodoError> {
let parts: Vec<&str> = if human.find(':').is_none() {
human.split("..").filter(|s| !s.is_empty()).collect()
} else {
human.split(':').filter(|s| !s.is_empty()).collect()
};
if parts.len() > 2 {
return Err(range_error(human));
}
let left_open = human.starts_with(':') || human.starts_with("..");
if parts.len() == 2 {
let mut begin = match human_to_date(base, parts[0], soon_days) {
Ok(d) => d,
Err(e) => return Err(range_error(&e)),
};
let mut end = match human_to_date(base, parts[1], soon_days) {
Ok(d) => d,
Err(e) => return Err(range_error(&e)),
};
if begin > end {
mem::swap(&mut begin, &mut end);
}
return Ok(tfilter::DateRange {
days: tfilter::ValueRange { low: (begin - base).num_days(), high: (end - base).num_days() },
span: tfilter::ValueSpan::Range,
});
}
if left_open {
let end = match human_to_date(base, parts[0], soon_days) {
Ok(d) => d,
Err(e) => return Err(range_error(&e)),
};
let diff = (end - base).num_days() + 1;
return Ok(tfilter::DateRange {
days: tfilter::ValueRange { low: diff, high: 0 },
span: tfilter::ValueSpan::Lower,
});
}
match human_to_date(base, parts[0], soon_days) {
Ok(begin) => {
let diff = (begin - base).num_days() - 1;
Ok(tfilter::DateRange {
days: tfilter::ValueRange { low: 0, high: diff },
span: tfilter::ValueSpan::Higher,
})
}
Err(e) => Err(range_error(&e)),
}
}
pub(crate) fn calendar_first_day(today: NaiveDate, rng: &CalendarRange, first_sunday: bool) -> NaiveDate {
match rng.rng {
CalendarRangeType::Days(n) => {
if n >= 0 {
today
} else {
let diff = n + 1;
today.checked_add_signed(Duration::days(diff.into())).unwrap_or(today)
}
}
CalendarRangeType::Weeks(n) => {
let is_first =
(today.weekday() == Weekday::Sun && first_sunday) || (today.weekday() == Weekday::Mon && !first_sunday);
let today = if rng.strict || is_first {
today
} else {
match first_sunday {
true => prev_weekday(today, Weekday::Sun),
false => prev_weekday(today, Weekday::Mon),
}
};
if rng.strict || n >= -1 {
return today;
}
let diff = if rng.strict {
n
} else if n > 0 {
n - 1
} else {
n + 1
};
today.checked_add_signed(Duration::weeks(diff.into())).unwrap_or(today)
}
CalendarRangeType::Months(n) => {
if n >= 0 {
if rng.strict {
return today;
}
return NaiveDate::from_ymd_opt(today.year(), today.month(), 1).unwrap_or(today);
}
let (today, diff) = if rng.strict {
(today, -n)
} else {
(NaiveDate::from_ymd_opt(today.year(), today.month(), 1).unwrap_or(today), -n - 1)
};
let today = add_months(today, diff as u32, true);
if rng.strict {
return today.checked_add_signed(Duration::days(1)).unwrap_or(today);
}
today
}
}
}
pub(crate) fn calendar_last_day(today: NaiveDate, rng: &CalendarRange, first_sunday: bool) -> NaiveDate {
match rng.rng {
CalendarRangeType::Days(n) => {
if n <= 0 {
return today;
}
let n = n - 1;
today.checked_add_signed(Duration::days(n.into())).unwrap_or(today)
}
CalendarRangeType::Weeks(n) => {
if rng.strict {
if n <= 0 {
return today;
}
return match today.checked_add_signed(Duration::weeks(n.into())) {
None => today,
Some(d) => d.checked_add_signed(Duration::days(-1)).unwrap_or(d),
};
}
let today = match first_sunday {
true => next_weekday(today, Weekday::Sat),
false => next_weekday(today, Weekday::Sun),
};
if n <= 1 {
return today;
}
let n = n - 1;
today.checked_add_signed(Duration::weeks(n.into())).unwrap_or(today)
}
CalendarRangeType::Months(n) => {
if rng.strict {
if n <= 0 {
return today;
}
let today = add_months(today, n as u32, false);
return today.checked_add_signed(Duration::days(-1)).unwrap_or(today);
}
let last = days_in_month(today.year(), today.month());
let today = NaiveDate::from_ymd_opt(today.year(), today.month(), last).unwrap_or(today);
if n <= 1 {
return today;
}
let diff = n - 1;
add_months(today, diff as u32, false)
}
}
}
#[cfg(test)]
mod humandate_test {
use super::*;
use chrono::Local;
struct Test {
txt: &'static str,
val: NaiveDate,
}
struct TestRange {
txt: &'static str,
val: tfilter::DateRange,
}
#[test]
fn no_change() {
let dt = Local::now().date_naive();
let res = human_to_date(dt, "2010-10-10", 0);
let must = Err(NO_CHANGE.to_string());
assert_eq!(res, must)
}
#[test]
fn month_day() {
let dt = NaiveDate::from_ymd_opt(2020, 7, 9).unwrap();
let tests: Vec<Test> = vec![
Test { txt: "7", val: NaiveDate::from_ymd_opt(2020, 8, 7).unwrap() },
Test { txt: "11", val: NaiveDate::from_ymd_opt(2020, 7, 11).unwrap() },
Test { txt: "31", val: NaiveDate::from_ymd_opt(2020, 7, 31).unwrap() },
];
for test in tests.iter() {
let nm = human_to_date(dt, test.txt, 0);
assert_eq!(nm, Ok(test.val), "{}", test.txt);
}
let dt = NaiveDate::from_ymd_opt(2020, 6, 9).unwrap();
let nm = human_to_date(dt, "31", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 6, 30).unwrap()));
let dt = NaiveDate::from_ymd_opt(2020, 2, 4).unwrap();
let nm = human_to_date(dt, "31", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 2, 29).unwrap()));
let dt = NaiveDate::from_ymd_opt(2020, 2, 29).unwrap();
let nm = human_to_date(dt, "29", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 3, 31).unwrap()));
let nm = human_to_date(dt, "32", 0);
assert!(nm.is_err());
let nm = human_to_date(dt, "0", 0);
assert!(nm.is_err());
}
#[test]
fn month_and_day() {
let dt = NaiveDate::from_ymd_opt(2020, 7, 9).unwrap();
let nm = human_to_date(dt, "07-08", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2021, 7, 8).unwrap()));
let nm = human_to_date(dt, "07-11", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 7, 11).unwrap()));
let nm = human_to_date(dt, "02-31", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2021, 2, 28).unwrap()));
}
#[test]
fn absolute() {
let dt = NaiveDate::from_ymd_opt(2020, 7, 9).unwrap();
let tests: Vec<Test> = vec![
Test { txt: "1w", val: NaiveDate::from_ymd_opt(2020, 7, 16).unwrap() },
Test { txt: "3d4d", val: NaiveDate::from_ymd_opt(2020, 7, 16).unwrap() },
Test { txt: "1y", val: NaiveDate::from_ymd_opt(2021, 7, 9).unwrap() },
Test { txt: "2w2d1m", val: NaiveDate::from_ymd_opt(2020, 8, 25).unwrap() },
Test { txt: "-1w", val: NaiveDate::from_ymd_opt(2020, 7, 2).unwrap() },
Test { txt: "-3d4d", val: NaiveDate::from_ymd_opt(2020, 7, 2).unwrap() },
Test { txt: "-1y", val: NaiveDate::from_ymd_opt(2019, 7, 9).unwrap() },
Test { txt: "-2w2d1m", val: NaiveDate::from_ymd_opt(2020, 5, 23).unwrap() },
];
for test in tests.iter() {
let nm = human_to_date(dt, test.txt, 0);
assert_eq!(nm, Ok(test.val), "{}", test.txt);
}
let dt = NaiveDate::from_ymd_opt(2021, 2, 28).unwrap();
let tests: Vec<Test> = vec![
Test { txt: "1m", val: NaiveDate::from_ymd_opt(2021, 3, 31).unwrap() },
Test { txt: "1y", val: NaiveDate::from_ymd_opt(2022, 2, 28).unwrap() },
Test { txt: "3y", val: NaiveDate::from_ymd_opt(2024, 2, 29).unwrap() },
Test { txt: "-1m", val: NaiveDate::from_ymd_opt(2021, 1, 31).unwrap() },
Test { txt: "-1y", val: NaiveDate::from_ymd_opt(2020, 2, 29).unwrap() },
Test { txt: "-3y", val: NaiveDate::from_ymd_opt(2018, 2, 28).unwrap() },
];
for test in tests.iter() {
let nm = human_to_date(dt, test.txt, 0);
assert_eq!(nm, Ok(test.val), "{}", test.txt);
}
}
#[test]
fn special() {
let dt = NaiveDate::from_ymd_opt(2020, 2, 29).unwrap();
let nm = human_to_date(dt, "last", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 3, 31).unwrap()));
let nm = human_to_date(dt, "-last", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 1, 31).unwrap()));
let dt = NaiveDate::from_ymd_opt(2020, 2, 10).unwrap();
let nm = human_to_date(dt, "last", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 2, 29).unwrap()));
let nm = human_to_date(dt, "-last", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 1, 31).unwrap()));
let dt = NaiveDate::from_ymd_opt(2020, 2, 1).unwrap();
let nm = human_to_date(dt, "first", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 3, 1).unwrap()));
let nm = human_to_date(dt, "-first", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()));
let dt = NaiveDate::from_ymd_opt(2020, 2, 10).unwrap();
let nm = human_to_date(dt, "first", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 3, 1).unwrap()));
let nm = human_to_date(dt, "-first", 0);
assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 2, 1).unwrap()));
let dt = NaiveDate::from_ymd_opt(2020, 7, 9).unwrap(); let tests: Vec<Test> = vec![
Test { txt: "tmr", val: NaiveDate::from_ymd_opt(2020, 7, 10).unwrap() },
Test { txt: "tm", val: NaiveDate::from_ymd_opt(2020, 7, 10).unwrap() },
Test { txt: "tomorrow", val: NaiveDate::from_ymd_opt(2020, 7, 10).unwrap() },
Test { txt: "today", val: NaiveDate::from_ymd_opt(2020, 7, 9).unwrap() },
Test { txt: "first", val: NaiveDate::from_ymd_opt(2020, 8, 1).unwrap() },
Test { txt: "last", val: NaiveDate::from_ymd_opt(2020, 7, 31).unwrap() },
Test { txt: "mon", val: NaiveDate::from_ymd_opt(2020, 7, 13).unwrap() },
Test { txt: "tu", val: NaiveDate::from_ymd_opt(2020, 7, 14).unwrap() },
Test { txt: "wed", val: NaiveDate::from_ymd_opt(2020, 7, 15).unwrap() },
Test { txt: "thursday", val: NaiveDate::from_ymd_opt(2020, 7, 16).unwrap() },
Test { txt: "fri", val: NaiveDate::from_ymd_opt(2020, 7, 10).unwrap() },
Test { txt: "sa", val: NaiveDate::from_ymd_opt(2020, 7, 11).unwrap() },
Test { txt: "sunday", val: NaiveDate::from_ymd_opt(2020, 7, 12).unwrap() },
Test { txt: "yesterday", val: NaiveDate::from_ymd_opt(2020, 7, 8).unwrap() },
Test { txt: "-mon", val: NaiveDate::from_ymd_opt(2020, 7, 6).unwrap() },
Test { txt: "-tu", val: NaiveDate::from_ymd_opt(2020, 7, 7).unwrap() },
Test { txt: "-wed", val: NaiveDate::from_ymd_opt(2020, 7, 8).unwrap() },
Test { txt: "-thursday", val: NaiveDate::from_ymd_opt(2020, 7, 2).unwrap() },
Test { txt: "-fri", val: NaiveDate::from_ymd_opt(2020, 7, 3).unwrap() },
Test { txt: "-sa", val: NaiveDate::from_ymd_opt(2020, 7, 4).unwrap() },
Test { txt: "-sunday", val: NaiveDate::from_ymd_opt(2020, 7, 5).unwrap() },
];
for test in tests.iter() {
let nm = human_to_date(dt, test.txt, 0);
assert_eq!(nm, Ok(test.val), "{}", test.txt);
}
}
#[test]
fn range_test() {
let dt = NaiveDate::from_ymd_opt(2020, 7, 9).unwrap();
let tests: Vec<TestRange> = vec![
TestRange {
txt: "..tue",
val: tfilter::DateRange {
days: tfilter::ValueRange { low: 6, high: 0 },
span: tfilter::ValueSpan::Lower,
},
},
TestRange {
txt: ":2d",
val: tfilter::DateRange {
days: tfilter::ValueRange { low: 3, high: 0 },
span: tfilter::ValueSpan::Lower,
},
},
TestRange {
txt: "tue..",
val: tfilter::DateRange {
days: tfilter::ValueRange { low: 0, high: 4 },
span: tfilter::ValueSpan::Higher,
},
},
TestRange {
txt: "3d:",
val: tfilter::DateRange {
days: tfilter::ValueRange { low: 0, high: 2 },
span: tfilter::ValueSpan::Higher,
},
},
TestRange {
txt: "-tue..we",
val: tfilter::DateRange {
days: tfilter::ValueRange { low: -2, high: 6 },
span: tfilter::ValueSpan::Range,
},
},
TestRange {
txt: "we..-tue",
val: tfilter::DateRange {
days: tfilter::ValueRange { low: -2, high: 6 },
span: tfilter::ValueSpan::Range,
},
},
TestRange {
txt: "-tue..-wed",
val: tfilter::DateRange {
days: tfilter::ValueRange { low: -2, high: -1 },
span: tfilter::ValueSpan::Range,
},
},
TestRange {
txt: "-1w:today",
val: tfilter::DateRange {
days: tfilter::ValueRange { low: -7, high: 0 },
span: tfilter::ValueSpan::Range,
},
},
TestRange {
txt: "..soon",
val: tfilter::DateRange {
days: tfilter::ValueRange { low: 7, high: 0 },
span: tfilter::ValueSpan::Lower,
},
},
TestRange {
txt: "soon..",
val: tfilter::DateRange {
days: tfilter::ValueRange { low: 0, high: 5 },
span: tfilter::ValueSpan::Higher,
},
},
TestRange {
txt: "-soon..soon",
val: tfilter::DateRange {
days: tfilter::ValueRange { low: -6, high: 6 },
span: tfilter::ValueSpan::Range,
},
},
];
for test in tests.iter() {
let rng = human_to_range(dt, test.txt, 6).unwrap();
assert_eq!(rng, test.val, "{}", test.txt);
}
}
#[test]
fn date_replace() {
let dt = NaiveDate::from_ymd_opt(2020, 7, 9).unwrap();
let s = fix_date(dt, "error due:xxxx next week", "due:", 0);
assert_eq!(s, None);
let s = fix_date(dt, "due: next week", "due:", 0);
assert_eq!(s, None);
let s = fix_date(dt, "due:1w next week", "due:", 0);
assert_eq!(s, Some("due:2020-07-16 next week".to_string()));
let s = fix_date(dt, "next day due:1d", "due:", 0);
assert_eq!(s, Some("next day due:2020-07-10".to_string()));
let s = fix_date(dt, "special due:sat in the middle", "due:", 0);
assert_eq!(s, Some("special due:2020-07-11 in the middle".to_string()));
}
#[test]
fn parse_calendar() {
struct TestCal {
txt: &'static str,
err: bool,
val: Option<CalendarRange>,
}
let tests: Vec<TestCal> = vec![
TestCal {
txt: "",
err: false,
val: Some(CalendarRange { strict: false, rng: CalendarRangeType::Days(1) }),
},
TestCal {
txt: "12",
err: false,
val: Some(CalendarRange { strict: false, rng: CalendarRangeType::Days(12) }),
},
TestCal {
txt: "w",
err: false,
val: Some(CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) }),
},
TestCal {
txt: "+m",
err: false,
val: Some(CalendarRange { strict: true, rng: CalendarRangeType::Months(1) }),
},
TestCal {
txt: "+-3d",
err: false,
val: Some(CalendarRange { strict: true, rng: CalendarRangeType::Days(-3) }),
},
TestCal { txt: "zzz", err: true, val: None },
TestCal { txt: "*2d", err: true, val: None },
TestCal { txt: "10r", err: true, val: None },
TestCal { txt: "100m", err: true, val: None },
];
for test in tests.iter() {
let res = CalendarRange::parse(test.txt);
if test.err {
assert!(res.is_err(), "{}", test.txt);
} else {
assert!(!res.is_err(), "{}", test.txt);
assert_eq!(res.unwrap(), test.val.unwrap(), "{}", test.txt);
}
}
}
#[test]
fn calendar_first_date() {
struct TestCal {
td: NaiveDate,
rng: CalendarRange,
sunday: bool,
res: NaiveDate,
}
let tests: Vec<TestCal> = vec![
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 06, 27).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 06, 27).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Weeks(1) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: true, rng: CalendarRangeType::Weeks(1) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Weeks(2) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(2) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 06, 27).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(2) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(15) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(15) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(15) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(15) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(-5) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 06, 29).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(-5) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 06, 29).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(-5) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 06, 29).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(-5) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 06, 29).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(2) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(2) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(2) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 01).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(2) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 01).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(-2) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 05, 04).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(-2) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 05, 04).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(-2) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 06, 01).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(-2) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 06, 01).unwrap(),
},
];
for test in tests.iter() {
let res = calendar_first_day(test.td, &test.rng, test.sunday);
assert_eq!(res, test.res, "{} - SUN: {}, RANGE: {:?}", test.td, test.sunday, test.rng);
}
}
#[test]
fn calendar_last_date() {
struct TestCal {
td: NaiveDate,
rng: CalendarRange,
sunday: bool,
res: NaiveDate,
}
let tests: Vec<TestCal> = vec![
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 09).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 10).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 09).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 10).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 09).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 10).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 09).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 10).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Weeks(1) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 09).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: true, rng: CalendarRangeType::Weeks(1) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 09).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 05).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(2) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 16).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: true, rng: CalendarRangeType::Weeks(2) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 16).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(15) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 17).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(15) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 17).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(15) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 17).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(15) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 17).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(-5) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(-5) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(-5) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(-5) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(2) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 09, 02).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(2) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 09, 02).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(2) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 08, 31).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(2) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 08, 31).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(-2) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(-2) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(-2) },
sunday: true,
res: NaiveDate::from_ymd_opt(2022, 07, 31).unwrap(),
},
TestCal {
td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(-2) },
sunday: false,
res: NaiveDate::from_ymd_opt(2022, 07, 31).unwrap(),
},
];
for test in tests.iter() {
let res = calendar_last_day(test.td, &test.rng, test.sunday);
assert_eq!(res, test.res, "{} - SUN: {}, RANGE: {:?}", test.td, test.sunday, test.rng);
}
}
}