diff --git a/Cargo.lock b/Cargo.lock index 33ef30e..87eb966 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -493,7 +493,7 @@ dependencies = [ [[package]] name = "ttdl" -version = "4.4.1" +version = "4.5.0" dependencies = [ "anyhow", "caseless", diff --git a/Cargo.toml b/Cargo.toml index 7b5dd3e..fc89f1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ttdl" -version = "4.4.1" +version = "4.5.0" authors = ["Vladimir Markelov "] edition = "2021" keywords = ["todotxt", "terminal", "cli", "todo", "tasks"] diff --git a/src/conf.rs b/src/conf.rs index d14e018..9a97b0a 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -448,21 +448,21 @@ fn parse_todo(matches: &Matches, c: &mut todo::Conf) -> Result<(), terr::TodoErr let s = if s.is_empty() { "none".to_owned() } else { s.to_lowercase() }; match s.as_str() { "-" => { - c.priority_act = todo::Action::Decrease; + c.priority.action = todo::Action::Decrease; } "+" => { - c.priority_act = todo::Action::Increase; + c.priority.action = todo::Action::Increase; } "none" => { - c.priority_act = todo::Action::Delete; + c.priority.action = todo::Action::Delete; } _ => { let p = s.as_bytes()[0]; if !p.is_ascii_lowercase() { return Err(terr::TodoError::InvalidValue(s, "priority".to_string())); } - c.priority = p - b'a'; - c.priority_act = todo::Action::Set; + c.priority.value = p - b'a'; + c.priority.action = todo::Action::Set; } } } @@ -471,12 +471,12 @@ fn parse_todo(matches: &Matches, c: &mut todo::Conf) -> Result<(), terr::TodoErr let s = s.to_lowercase(); match s.as_str() { "-" | "none" => { - c.recurrence_act = todo::Action::Delete; + c.recurrence.action = todo::Action::Delete; } _ => match todotxt::Recurrence::from_str(&s) { Ok(r) => { - c.recurrence = Some(r); - c.recurrence_act = todo::Action::Set; + c.recurrence.value = Some(r); + c.recurrence.action = todo::Action::Set; } Err(_) => { return Err(terr::TodoError::InvalidValue(s, "recurrence".to_string())); @@ -488,7 +488,7 @@ fn parse_todo(matches: &Matches, c: &mut todo::Conf) -> Result<(), terr::TodoErr if let Some(s) = matches.opt_str("set-due") { match s.as_str() { "-" | "none" => { - c.due_act = todo::Action::Delete; + c.due.action = todo::Action::Delete; } "soon" => { return Err(terr::TodoError::InvalidValue(s, "set-due".to_string())); @@ -496,16 +496,17 @@ fn parse_todo(matches: &Matches, c: &mut todo::Conf) -> Result<(), terr::TodoErr _ => { let dt = Local::now().date_naive(); if let Ok(new_date) = human_date::human_to_date(dt, &s, 0) { - c.due = Some(new_date); - c.due_act = todo::Action::Set; + c.due.value = todo::NewDateValue::Date(new_date); + c.due.action = todo::Action::Set; } else { match chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d") { Ok(d) => { - c.due = Some(d); - c.due_act = todo::Action::Set; + c.due.value = todo::NewDateValue::Date(d); + c.due.action = todo::Action::Set; } Err(_) => { - return Err(terr::TodoError::InvalidValue(s, "set-due".to_string())); + c.due.action = todo::Action::Set; + c.due.value = todo::NewDateValue::Expr(s); } } } @@ -516,7 +517,7 @@ fn parse_todo(matches: &Matches, c: &mut todo::Conf) -> Result<(), terr::TodoErr if let Some(s) = matches.opt_str("set-threshold") { match s.as_str() { "-" | "none" => { - c.recurrence_act = todo::Action::Delete; + c.thr.action = todo::Action::Delete; } "soon" => { return Err(terr::TodoError::InvalidValue(s, "set-threshold".to_string())); @@ -524,16 +525,17 @@ fn parse_todo(matches: &Matches, c: &mut todo::Conf) -> Result<(), terr::TodoErr _ => { let dt = Local::now().date_naive(); if let Ok(new_date) = human_date::human_to_date(dt, &s, 0) { - c.thr = Some(new_date); - c.thr_act = todo::Action::Set; + c.thr.value = todo::NewDateValue::Date(new_date); + c.thr.action = todo::Action::Set; } else { match chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d") { Ok(d) => { - c.thr = Some(d); - c.thr_act = todo::Action::Set; + c.thr.value = todo::NewDateValue::Date(d); + c.thr.action = todo::Action::Set; } Err(_) => { - return Err(terr::TodoError::InvalidValue(s, "set-threshold".to_string())); + c.thr.action = todo::Action::Set; + c.thr.value = todo::NewDateValue::Expr(s); } } } @@ -543,44 +545,44 @@ fn parse_todo(matches: &Matches, c: &mut todo::Conf) -> Result<(), terr::TodoErr if let Some(s) = matches.opt_str("set-proj") { for st in s.split(',') { - c.projects.push(st.to_string()); + c.projects.value.push(st.to_string()); } - c.project_act = todo::Action::Set; + c.projects.action = todo::Action::Set; } if let Some(s) = matches.opt_str("del-proj") { for st in s.split(',') { - c.projects.push(st.to_string()); + c.projects.value.push(st.to_string()); } - c.project_act = todo::Action::Delete; + c.projects.action = todo::Action::Delete; } if let Some(s) = matches.opt_str("repl-proj") { for st in s.split(',') { - c.projects.push(st.to_string()); + c.projects.value.push(st.to_string()); } - c.project_act = todo::Action::Replace; + c.projects.action = todo::Action::Replace; } if let Some(s) = matches.opt_str("set-ctx") { for st in s.split(',') { - c.contexts.push(st.to_string()); + c.contexts.value.push(st.to_string()); } - c.context_act = todo::Action::Set; + c.contexts.action = todo::Action::Set; } if let Some(s) = matches.opt_str("del-ctx") { for st in s.split(',') { - c.contexts.push(st.to_string()); + c.contexts.value.push(st.to_string()); } - c.context_act = todo::Action::Delete; + c.contexts.action = todo::Action::Delete; } if let Some(s) = matches.opt_str("repl-ctx") { for st in s.split(',') { - c.contexts.push(st.to_string()); + c.contexts.value.push(st.to_string()); } - c.context_act = todo::Action::Replace; + c.contexts.action = todo::Action::Replace; } if let Some(s) = matches.opt_str("set-tag") { @@ -599,8 +601,10 @@ fn parse_todo(matches: &Matches, c: &mut todo::Conf) -> Result<(), terr::TodoErr } } if !hmap.is_empty() { - c.tags = Some(hmap); - c.tags_act = todo::Action::Set; + c.tags = todo::TagValuesChange{ + value: Some(hmap), + action: todo::Action::Set, + }; } } @@ -626,8 +630,10 @@ fn parse_todo(matches: &Matches, c: &mut todo::Conf) -> Result<(), terr::TodoErr } } if !hmap.is_empty() { - c.tags = Some(hmap); - c.tags_act = todo::Action::Delete; + c.tags = todo::TagValuesChange{ + value: Some(hmap), + action: todo::Action::Delete, + }; } } @@ -637,8 +643,10 @@ fn parse_todo(matches: &Matches, c: &mut todo::Conf) -> Result<(), terr::TodoErr v.push(st.to_string()); } if !v.is_empty() { - c.hashtags = Some(v); - c.hashtags_act = todo::Action::Set; + c.hashtags = todo::ListTagChange{ + value: v, + action: todo::Action::Set, + }; } } @@ -648,8 +656,10 @@ fn parse_todo(matches: &Matches, c: &mut todo::Conf) -> Result<(), terr::TodoErr v.push(st.to_string()); } if !v.is_empty() { - c.hashtags = Some(v); - c.hashtags_act = todo::Action::Delete; + c.hashtags = todo::ListTagChange{ + value: v, + action: todo::Action::Delete, + } } } @@ -663,8 +673,10 @@ fn parse_todo(matches: &Matches, c: &mut todo::Conf) -> Result<(), terr::TodoErr } } if !v.is_empty() { - c.hashtags = Some(v); - c.hashtags_act = todo::Action::Replace; + c.hashtags = todo::ListTagChange{ + value: v, + action: todo::Action::Replace, + }; } } diff --git a/src/date_expr.rs b/src/date_expr.rs deleted file mode 100644 index 99d14e3..0000000 --- a/src/date_expr.rs +++ /dev/null @@ -1,808 +0,0 @@ -use chrono::{Duration, NaiveDate}; -use todo_lib::todotxt; - -use crate::human_date; - -// pub fn dd(dt: NaiveDate) -> String { -// todotxt::format_date(dt) -// } - -#[derive(Debug)] -pub struct ExprItem<'a> { - pub sign: char, - pub val: &'a str, -} - -#[derive(Default, Clone)] -pub struct TaskTag { - pub name: String, - pub svalue: Option, - pub dvalue: Option, -} - -pub struct TaskTagList(Vec); - -#[derive(Debug, PartialEq)] -pub enum TagValueType { - Raw(String), - Calc(NaiveDate), - None, -} - -impl TaskTagList { - // TODO: from task - pub fn from_task(task: &todotxt::Task) -> Self { - let mut v = Vec::new(); - if let Some(dt) = task.due_date { - let tg = TaskTag { name: "due".to_string(), dvalue: Some(dt), svalue: None }; - v.push(tg); - } - if let Some(dt) = task.create_date { - let tg = TaskTag { name: "created".to_string(), dvalue: Some(dt), svalue: None }; - v.push(tg); - } - if let Some(dt) = task.threshold_date { - let tg = TaskTag { name: "t".to_string(), dvalue: Some(dt), svalue: None }; - v.push(tg); - } - for (key, value) in task.tags.iter() { - let tg = TaskTag { name: key.clone(), svalue: Some(value.clone()), dvalue: None }; - v.push(tg); - } - TaskTagList(v) - } - pub fn from_str(s: &str, dt: NaiveDate) -> Self { - let hmap = todotxt::extract_tags(s); - let mut v = Vec::new(); - for (key, val) in hmap.iter() { - let tg = TaskTag { name: key.to_string(), svalue: Some(val.to_string()), dvalue: None }; - v.push(tg); - } - let tg = TaskTag { name: "created".to_string(), dvalue: None, svalue: Some(todotxt::format_date(dt)) }; - v.push(tg); - TaskTagList(v) - } - pub fn set_tag(&mut self, tag: &str, value: NaiveDate) { - for v in self.0.iter_mut() { - if v.name.as_str() == tag { - v.dvalue = Some(value); - return; - } - } - } - pub fn tag_value(&self, tag: &str) -> TagValueType { - for v in self.0.iter() { - if v.name.as_str() == tag { - if let Some(dt) = v.dvalue { - return TagValueType::Calc(dt); - } else if let Some(ref s) = v.svalue { - return TagValueType::Raw(s.clone()); - } - } - } - TagValueType::None - } -} - -// Full: YYYY-MM-DD -fn parse_full_date(s: &str) -> Option<&str> { - let mut st = s; - match s.find(|c: char| !c.is_ascii_digit()) { - None => return None, - Some(i) => { - if i != 4 { - return None; - } else { - st = &st[i..]; - } - } - } - if !st.starts_with('-') { - return None; - } - st = &st[1..]; - match st.find(|c: char| !c.is_ascii_digit()) { - None => return None, - Some(i) => { - if i != 2 { - return None; - } else { - st = &st[i..]; - } - } - } - if !st.starts_with('-') { - return None; - } - st = &st[1..]; - match st.find(|c: char| !c.is_ascii_digit()) { - None => { - if st.len() == 2 { - Some(s) - } else { - None - } - } - Some(i) => { - if i != 2 { - None - } else { - let l = "2020-01-01".len(); - let rest = &s[l..]; - if !rest.starts_with('-') && !rest.starts_with('+') { - return None; - } - Some(&s[..l]) - } - } - } -} - -// Short: MM-DD -fn parse_short_date(s: &str) -> Option<&str> { - let mut st = s; - match s.find(|c: char| !c.is_ascii_digit()) { - None => return None, - Some(i) => { - if i != 2 { - return None; - } else { - st = &st[i..]; - } - } - } - if !st.starts_with('-') { - return None; - } - st = &st[1..]; - match st.find(|c: char| !c.is_ascii_digit()) { - None => { - if st.len() == 2 { - Some(s) - } else { - None - } - } - Some(i) => { - if i != 2 { - None - } else { - let l = "01-01".len(); - let rest = &s[l..]; - if !rest.starts_with('-') && !rest.starts_with('+') { - return None; - } - Some(&s[..l]) - } - } - } -} - -// Single day:DD -fn parse_single_day(s: &str) -> Option<&str> { - match s.find(|c: char| !c.is_ascii_digit()) { - None => { - if s.len() < 3 { - Some(s) - } else { - None - } - } - Some(i) => { - if i > 2 || i == 0 { - None - } else { - let rest = &s[i..]; - if !rest.starts_with('-') && !rest.starts_with('+') { - return None; - } - Some(&s[..i]) - } - } - } -} - -// Special: WORD (tue/today/tomorrow/etc) -fn parse_special(s: &str) -> Option<&str> { - let c = match s.chars().next() { - None => return None, - Some(cc) => cc, - }; - if !('a'..='z').contains(&c) && !('A'..='Z').contains(&c) { - return None; - } - match s.find(|c: char| !('a'..='z').contains(&c) && !('A'..='Z').contains(&c)) { - None => Some(s), - Some(idx) => { - let rest = &s[idx..]; - if !rest.starts_with('-') && !rest.starts_with('+') { - None - } else { - Some(&s[..idx]) - } - } - } -} - -// Duration: ##L (1-2 digits and duration type DWMY) -fn parse_duration(s: &str) -> Option<&str> { - let c = match s.chars().next() { - None => return None, - Some(cc) => cc, - }; - let durs = vec!['d', 'D', 'w', 'W', 'm', 'M', 'y', 'Y']; - if c.is_ascii_digit() { - let idxl = match s.find(|c: char| !c.is_ascii_digit()) { - None => return Some(s), - Some(i) => i, - }; - let rest = &s[idxl..]; - if rest.starts_with('-') || rest.starts_with('+') { - Some(&s[..idxl]) - } else { - if let Some(cc) = rest.chars().next() { - if durs.contains(&cc) { - if s.len() == idxl + 1 { - Some(s) - } else { - let rest = &s[idxl + 1..]; - if rest.starts_with('-') || rest.starts_with('+') { - Some(&s[..idxl + 1]) - } else { - None - } - } - } else { - None - } - } else { - None - } - } - } else if durs.contains(&c) { - match s.find(|c: char| !('a'..'z').contains(&c) && !('A'..'Z').contains(&c)) { - None => { - if s.len() == 1 { - Some(s) - } else { - None - } - } - Some(idx) => { - let rest = &s[idx..]; - if rest.starts_with('-') || rest.starts_with('+') { - Some(&s[..idx]) - } else { - None - } - } - } - } else { - None - } -} - -fn parse_base_date(s: &str) -> Result { - if let Some(st) = parse_special(s) { - return Ok(ExprItem { sign: '+', val: st }); - } - if let Some(st) = parse_full_date(s) { - return Ok(ExprItem { sign: '+', val: st }); - } - if let Some(st) = parse_short_date(s) { - return Ok(ExprItem { sign: '+', val: st }); - } - if let Some(st) = parse_single_day(s) { - return Ok(ExprItem { sign: '+', val: st }); - } - Err("Failed to parse base date".to_string()) -} - -pub fn parse_expression(s: &str) -> Result, String> { - let mut items = Vec::new(); - let mut st = match parse_base_date(s) { - Err(e) => return Err(e), - Ok(ei) => { - let sc = &s[ei.val.len()..]; - items.push(ei); - sc - } - }; - while !st.is_empty() { - if st.len() < 2 { - return Err(format!("Incomplete expression: '{s}'")); - } - let c = match st.chars().next() { - Some(cc) => cc, - None => return Err("Internal error".to_string()), - }; - if c != '-' && c != '+' { - return Err(format!("Invalid character '{0}'", c)); - } - st = &st[1..]; - match parse_duration(st) { - None => return Err(format!("Invalid duration: '{st}'")), - Some(v) => { - let ei = ExprItem { sign: c, val: v }; - st = &st[ei.val.len()..]; - items.push(ei); - } - } - } - Ok(items) -} - -fn parse_abs_date(base: NaiveDate, s: &str, soon_days: u8) -> Result { - match human_date::human_to_date(base, s, soon_days) { - Ok(d) => Ok(d), - Err(e) => { - if e == human_date::NO_CHANGE { - match NaiveDate::parse_from_str(s, "%Y-%m-%d") { - Ok(d) => Ok(d), - Err(e) => Err(format!("Invalid date [{s}]: {e}")), - } - } else { - Err(e) - } - } - } -} - -fn calc_base( - base: NaiveDate, - s: &str, - tags: &mut TaskTagList, - soon_days: u8, - counter: usize, -) -> Result { - let mut dt = base; - if s.find(|c: char| !('a'..='z').contains(&c) && !('A'..='Z').contains(&c)).is_none() { - // Special date case - let spec = s.to_lowercase(); - let tval = tags.tag_value(spec.as_str()); - match tval { - TagValueType::None => { - dt = parse_abs_date(dt, &s, soon_days)?; - } - TagValueType::Calc(d) => { - dt = d; - } - TagValueType::Raw(s) => { - let d = calc_expr(base, &s, tags, soon_days, counter + 1)?; - tags.set_tag(&spec, d); - dt = d; - } - } - } else { - // Absolute date - dt = parse_abs_date(dt, s, soon_days)?; - } - Ok(dt) -} - -fn calc_expr( - base: NaiveDate, - s: &str, - tags: &mut TaskTagList, - soon_days: u8, - counter: usize, -) -> Result { - if counter > 10 { - return Err("Recursion stack overflow".to_string()); - } - - let items = parse_expression(s)?; - if items.is_empty() { - return Err("Empty expression".to_string()); - } - - let mut dt = base; - for (idx, item) in items.iter().enumerate() { - match idx { - 0 => { - dt = calc_base(base, item.val, tags, soon_days, counter)?; - } - _ => { - let rec_str = if item.val.find(|c: char| !('0'..='9').contains(&c)).is_none() { - format!("{0}d", item.val) - } else { - item.val.to_string() - }; - let rc = match todotxt::Recurrence::parse(&rec_str) { - Ok(r) => r, - Err(e) => { - return Err(format!("Invalid duration '{0}': {e}", item.val)); - } - }; - match rc.period { - todotxt::Period::Day => { - let dur = if item.sign == '-' { - Duration::days(-(rc.count as i64)) - } else { - Duration::days(rc.count as i64) - }; - dt += dur; - } - todotxt::Period::Week => { - let dur = if item.sign == '-' { - Duration::days(-(rc.count as i64) * 7) - } else { - Duration::days(rc.count as i64 * 7) - }; - dt += dur; - } - todotxt::Period::Month => { - dt = human_date::add_months(dt, rc.count.into(), item.sign == '-'); - } - todotxt::Period::Year => { - dt = human_date::add_years(dt, rc.count.into(), item.sign == '-'); - } - _ => {} - } - } - } - } - - Ok(dt) -} - -pub fn calculate_expr(base: NaiveDate, s: &str, tags: &mut TaskTagList, soon_days: u8) -> Result { - calc_expr(base, s, tags, soon_days, 1) -} - -pub fn calculate_main_tags(base: NaiveDate, tags: &mut TaskTagList, soon_days: u8) -> Result { - let mut anything_changed = false; - for tag in ["due", "t"].into_iter() { - let t = tags.tag_value(tag); - if let TagValueType::Raw(s) = t { - let cval = calculate_expr(base, &s, tags, soon_days)?; - let fdate = todotxt::format_date(cval); - if fdate.as_str() != s { - tags.set_tag(tag, cval); - anything_changed = true; - } - } - } - Ok(anything_changed) -} - -pub fn update_tags_in_str(tags: &TaskTagList, s: &str) -> String { - let mut st = s.to_string(); - for tag in tags.0.iter() { - if tag.name.as_str() != "due" && tag.name.as_str() != "t" { - continue; - } - if let (Some(sval), Some(dval)) = (tag.svalue.clone(), tag.dvalue) { - let old = format!("{0}:{sval}", tag.name); - let new = format!("{0}:{1}", tag.name, todotxt::format_date(dval)); - todotxt::replace_word(&mut st, &old, &new); - } - } - st -} - -#[cfg(test)] -mod date_expr_test { - use super::*; - - struct Test { - txt: &'static str, - err: bool, - res: &'static str, - } - - #[test] - fn parse_full_date_test() { - let tests: Vec = vec![ - Test { txt: "1999-20-20", err: false, res: "1999-20-20" }, - Test { txt: "1999-20-20+1d", err: false, res: "1999-20-20" }, - Test { txt: "1999-20-20-2", err: false, res: "1999-20-20" }, - Test { txt: "1999-20-20z", err: true, res: "" }, - Test { txt: "21999-20-20", err: true, res: "" }, - Test { txt: "1999-2-20", err: true, res: "" }, - Test { txt: "1999-20-0", err: true, res: "" }, - Test { txt: "19a9-20-20", err: true, res: "" }, - Test { txt: "1999-20-0a", err: true, res: "" }, - Test { txt: "cccccccccc", err: true, res: "" }, - Test { txt: "19992020", err: true, res: "" }, - Test { txt: "", err: true, res: "" }, - Test { txt: "-1999-20-20", err: true, res: "" }, - ]; - for test in tests.iter() { - let r = parse_full_date(test.txt); - match r { - Some(rr) => { - if test.err { - assert!(false, "Test [{0}] must fail", test.txt); - } - assert_eq!(test.res, rr, "Failed [{0}]: {:?}", test.txt); - } - None => { - if !test.err { - assert!(false, "Test [{0}] must pass", test.txt); - } - } - } - } - } - - #[test] - fn parse_special_test() { - let tests: Vec = vec![ - Test { txt: "today", err: false, res: "today" }, - Test { txt: "Today", err: false, res: "Today" }, - Test { txt: "tODAY", err: false, res: "tODAY" }, - Test { txt: "tue", err: false, res: "tue" }, - Test { txt: "tue-2", err: false, res: "tue" }, - Test { txt: "today%2", err: true, res: "" }, - Test { txt: "2+today", err: true, res: "" }, - ]; - for test in tests.iter() { - let r = parse_special(test.txt); - match r { - Some(rr) => { - if test.err { - assert!(false, "Test [{0}] must fail", test.txt); - } - assert_eq!(test.res, rr, "Failed [{0}]: {:?}", test.txt); - } - None => { - if !test.err { - assert!(false, "Test [{0}] must pass", test.txt); - } - } - } - } - } - - #[test] - fn parse_short_date_test() { - let tests: Vec = vec![ - Test { txt: "20-20", err: false, res: "20-20" }, - Test { txt: "20-20+1d", err: false, res: "20-20" }, - Test { txt: "20-20-2", err: false, res: "20-20" }, - Test { txt: "20-20z", err: true, res: "" }, - Test { txt: "320-20", err: true, res: "" }, - Test { txt: "2-20", err: true, res: "" }, - Test { txt: "20-0", err: true, res: "" }, - Test { txt: "2a-20", err: true, res: "" }, - Test { txt: "20-0a", err: true, res: "" }, - Test { txt: "ccccc", err: true, res: "" }, - Test { txt: "2020", err: true, res: "" }, - Test { txt: "", err: true, res: "" }, - Test { txt: "-20-20", err: true, res: "" }, - ]; - for test in tests.iter() { - let r = parse_short_date(test.txt); - match r { - Some(rr) => { - if test.err { - assert!(false, "Test [{0}] must fail", test.txt); - } - assert_eq!(test.res, rr, "Failed [{0}]: {:?}", test.txt); - } - None => { - if !test.err { - assert!(false, "Test [{0}] must pass", test.txt); - } - } - } - } - } - - #[test] - fn parse_duration_test() { - let tests: Vec = vec![ - Test { txt: "w+1", err: false, res: "w" }, - Test { txt: "200d-1", err: false, res: "200d" }, - Test { txt: "15w", err: false, res: "15w" }, - Test { txt: "y", err: false, res: "y" }, - Test { txt: "2+3", err: false, res: "2" }, - Test { txt: "day", err: true, res: "" }, - Test { txt: "", err: true, res: "" }, - Test { txt: "a20", err: true, res: "" }, - Test { txt: "20days", err: true, res: "" }, - Test { txt: "20/4", err: true, res: "" }, - Test { txt: "20w/4", err: true, res: "" }, - ]; - for test in tests.iter() { - let r = parse_duration(test.txt); - match r { - Some(rr) => { - if test.err { - assert!(false, "Test [{0}] must fail", test.txt); - } - assert_eq!(test.res, rr, "Failed [{0}]: {:?}", test.txt); - } - None => { - if !test.err { - assert!(false, "Test [{0}] must pass", test.txt); - } - } - } - } - } - - #[test] - fn parse_expression_test() { - struct ETest { - txt: &'static str, - l: usize, - err: bool, - last: &'static str, - } - let tests: Vec = vec![ - ETest { txt: "2003-01-01", err: false, l: 1, last: "2003-01-01" }, - ETest { txt: "2003-01-01+2d", err: false, l: 2, last: "2d" }, - ETest { txt: "2003-01-01+2d-9", err: false, l: 3, last: "9" }, - ETest { txt: "2003-01-01+9-10m", err: false, l: 3, last: "10m" }, - ETest { txt: "tue+67", err: false, l: 2, last: "67" }, - ETest { txt: "2003-01-01+abcd", err: true, l: 1, last: "" }, - ETest { txt: "tue+tue", err: true, l: 1, last: "" }, - ETest { txt: "tue/2", err: true, l: 1, last: "" }, - ETest { txt: "2d", err: true, l: 1, last: "" }, - ]; - for test in tests.iter() { - let r = parse_expression(test.txt); - match r { - Ok(rr) => { - if test.err { - assert!(false, "Test [{0}] must fail", test.txt); - } - assert_eq!(test.l, rr.len(), "{0} expected {1} items, got {2}", test.txt, test.l, rr.len()); - assert_eq!( - test.last, - rr[rr.len() - 1].val, - "Failed [{0}]: {:?}, [{1}] != [{2}]", - test.txt, - test.last, - rr[rr.len() - 1].val - ); - } - Err(e) => { - if !test.err { - assert!(false, "Test [{0}] must pass: {e:?}", test.txt); - } - } - } - } - } - - #[test] - fn parse_str_expression_test() { - struct ETest { - txt: &'static str, - err: bool, - res: NaiveDate, - } - let tests: Vec = vec![ - ETest { txt: "2021-05-07", err: false, res: NaiveDate::from_ymd_opt(2021, 5, 7).unwrap() }, - ETest { txt: "2021-05-07+10d", err: false, res: NaiveDate::from_ymd_opt(2021, 5, 17).unwrap() }, - ETest { txt: "2021-05-07+2w", err: false, res: NaiveDate::from_ymd_opt(2021, 5, 21).unwrap() }, - ETest { txt: "2021-05-07-7d", err: false, res: NaiveDate::from_ymd_opt(2021, 4, 30).unwrap() }, - ETest { txt: "2021-05-07-2m", err: false, res: NaiveDate::from_ymd_opt(2021, 3, 07).unwrap() }, - ETest { txt: "2021-05-07+1y", err: false, res: NaiveDate::from_ymd_opt(2022, 5, 07).unwrap() }, - ETest { txt: "2021-05-07+12d-2d", err: false, res: NaiveDate::from_ymd_opt(2021, 5, 17).unwrap() }, - ETest { txt: "2021-05-07+12d-1w", err: false, res: NaiveDate::from_ymd_opt(2021, 5, 12).unwrap() }, - ETest { txt: "today", err: false, res: NaiveDate::from_ymd_opt(2020, 3, 15).unwrap() }, - ETest { txt: "yesterday+2d", err: false, res: NaiveDate::from_ymd_opt(2020, 3, 16).unwrap() }, - ETest { txt: "first+1w", err: false, res: NaiveDate::from_ymd_opt(2020, 4, 8).unwrap() }, - ETest { txt: "due+1d", err: false, res: NaiveDate::from_ymd_opt(2020, 4, 9).unwrap() }, - ETest { txt: "t-1d", err: false, res: NaiveDate::from_ymd_opt(2020, 4, 3).unwrap() }, - ETest { txt: "extra+1w", err: false, res: NaiveDate::from_ymd_opt(2022, 9, 23).unwrap() }, - ETest { txt: "2021-05-07*2", err: true, res: NaiveDate::from_ymd_opt(2021, 5, 7).unwrap() }, - ETest { txt: "2021-05-07+1t", err: true, res: NaiveDate::from_ymd_opt(2021, 5, 7).unwrap() }, - ETest { txt: "someday", err: true, res: NaiveDate::from_ymd_opt(2021, 5, 7).unwrap() }, - ]; - - let base = NaiveDate::from_ymd_opt(2020, 3, 15).unwrap(); - let task = todotxt::Task::parse("create something due:2020-04-08 t:due-4 extra:2022-09-16", base); - let mut tlist = TaskTagList::from_task(&task); - for (idx, test) in tests.iter().enumerate() { - let d = calculate_expr(base, test.txt, &mut tlist, 8); - if test.err { - if d.is_ok() { - assert!(false, "Test {idx}.[{0}] must fail", test.txt); - } - } else { - if d.is_err() { - assert!(false, "Test {idx}.[{0}] must pass: {1:?}", test.txt, d); - } else { - assert_eq!(d.unwrap(), test.res, "Test {idx}.[{0}]", test.txt); - } - } - } - } - #[test] - fn tag_list_from_str_test() { - struct ETest { - txt: &'static str, - count: usize, - values: Vec<&'static str>, - } - let base = NaiveDate::from_ymd_opt(2020, 3, 15).unwrap(); - // Do not forget to add +1 - for "created" - let tests: Vec = vec![ - ETest { txt: "", count: 1, values: vec!["created", "2020-03-15"] }, - ETest { - txt: "house due:2015-08-12 was done:due+5 t:2015-07-30 .", - count: 4, - values: vec!["created", "2020-03-15", "due", "2015-08-12", "done", "due+5", "t", "2015-07-30"], - }, - ]; - for (idx, test) in tests.iter().enumerate() { - let tlist = TaskTagList::from_str(test.txt, base); - assert_eq!(test.count, tlist.0.len()); - for vidx in 0..test.values.len() / 2 { - let v = tlist.tag_value(test.values[vidx * 2]); - assert_eq!( - v, - TagValueType::Raw(test.values[vidx * 2 + 1].to_string()), - "{idx}. Tag [{0}] must get value [{1}] instead of [{2:?}]", - test.values[vidx * 2], - test.values[vidx * 2 + 1], - v - ); - } - } - } - #[test] - fn tag_calculate_done_test() { - struct ETest { - txt: &'static str, - is_err: bool, - value: &'static str, - } - let base = NaiveDate::from_ymd_opt(2020, 3, 15).unwrap(); - // Do not forget to add +1 - for "created" - let tests: Vec = vec![ - ETest { txt: "", is_err: true, value: "" }, - ETest { txt: "no done tag, just t:2023-09-11", is_err: true, value: "" }, - ETest { txt: "exists normal done:2023-06-24 tag", is_err: false, value: "2023-06-24" }, - ETest { txt: "house due:2015-08-12 was done:due+5 t:2015-07-30 .", is_err: false, value: "2015-08-17" }, - ]; - for (idx, test) in tests.iter().enumerate() { - let mut tlist = TaskTagList::from_str(test.txt, base); - let res = calculate_expr(base, "done", &mut tlist, 7); - match res { - Err(e) => { - if !test.is_err { - assert!(false, "{idx}. The test must not fail. Got {0:?}", e); - } - } - Ok(d) => { - if test.is_err { - assert!(false, "{idx}. The test must fail"); - } else { - let ds = todotxt::format_date(d); - assert_eq!(test.value, ds.as_str(), "Expected date: {0}, got {1}", test.value, d); - } - } - } - } - } - #[test] - fn tag_fix_str_test() { - struct ETest { - txt: &'static str, - val: &'static str, - fixed: bool, - } - let base = NaiveDate::from_ymd_opt(2020, 3, 15).unwrap(); - let tests: Vec = vec![ - ETest { txt: "exists normal due:2023-06-24 tag", val: "exists normal due:2023-06-24 tag", fixed: false }, - ETest { - txt: "house done:2015-08-12 was due:done-5 t:2015-07-30 .", - val: "house done:2015-08-12 was due:2015-08-07 t:2015-07-30 .", - fixed: true, - }, - ]; - for (idx, test) in tests.iter().enumerate() { - let mut tlist = TaskTagList::from_str(test.txt, base); - let fixed = calculate_main_tags(base, &mut tlist, 7).unwrap(); - assert_eq!(fixed, test.fixed); - let new_str = update_tags_in_str(&tlist, test.txt); - assert_eq!(new_str.as_str(), test.val, "{idx}. Must be equal [{0}], got [{new_str}]", test.val); - } - } -} diff --git a/src/human_date.rs b/src/human_date.rs deleted file mode 100644 index c130b77..0000000 --- a/src/human_date.rs +++ /dev/null @@ -1,1535 +0,0 @@ -use chrono::{Datelike, Duration, NaiveDate, Weekday}; -use std::mem; - -use todo_lib::{terr, tfilter}; - -pub const NO_CHANGE: &str = "no change"; -const DAYS_PER_WEEK: u32 = 7; -const FAR_PAST: i64 = -100 * 365; // far in the past - -type HumanResult = Result; - -#[derive(Debug, Clone, PartialEq, Eq, Copy)] -pub enum CalendarRangeType { - Days(i8), - Weeks(i8), - Months(i8), - Years(i8), - DayRange(i8, i8), - WeekRange(i8, i8), - MonthRange(i8, i8), - YearRange(i8, 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 !c.is_ascii_digit() { - break; - } - res.push(c); - } - (&s[res.len()..], res) -} - -impl CalendarRange { - pub(crate) fn parse(s: &str) -> Result { - if s.contains("..") || s.contains(':') { - CalendarRange::parse_range(s) - } else { - CalendarRange::parse_single(s) - } - } - - fn parse_single_num(s_in: &str) -> Result<(&str, i8, bool), terr::TodoError> { - let (s, strict) = if s_in.starts_with('+') { (&s_in["+".len()..], true) } else { (s_in, false) }; - let (s, sgn) = if s.starts_with('-') { (&s["-".len()..], -1i8) } else { (s, 1i8) }; - let (s, num_str) = parse_int(s); - let num = if num_str.is_empty() { - 1 - } else { - match num_str.parse::() { - Ok(n) => n, - Err(_) => { - return Err(terr::TodoError::InvalidValue(s_in.to_string(), "calendar range value".to_string())) - } - } - }; - let num = num * sgn; - match s { - "" | "d" | "D" => { - if num.abs() > 100 { - return Err(terr::TodoError::InvalidValue( - s_in.to_string(), - "number of days(range -100..100)".to_string(), - )); - } - } - "w" | "W" => { - if num.abs() > 16 { - return Err(terr::TodoError::InvalidValue( - s_in.to_string(), - "number of weeks(range -16..16)".to_string(), - )); - } - } - "m" | "M" => { - if num.abs() > 24 { - return Err(terr::TodoError::InvalidValue( - s_in.to_string(), - "number of months(range -24..24)".to_string(), - )); - } - } - "y" | "Y" => { - if num.abs() > 2 { - return Err(terr::TodoError::InvalidValue( - s_in.to_string(), - "number of years(range -2..2)".to_string(), - )); - } - } - _ => return Err(terr::TodoError::InvalidValue(s_in.to_string(), "calendar range type".to_string())), - } - Ok((s, num, strict)) - } - - fn parse_range(s: &str) -> Result { - let ends: Vec<&str> = if s.contains("..") { s.split("..").collect() } else { s.split(':').collect() }; - if ends.len() > 2 { - return Err(terr::TodoError::InvalidValue( - s.to_string(), - "calendar range cannot contain more than 2 values".to_string(), - )); - } - let (ltp, lnum, lstrict) = CalendarRange::parse_single_num(ends[0])?; - let (rtp, rnum, rstrict) = CalendarRange::parse_single_num(ends[1])?; - if ltp != rtp { - return Err(terr::TodoError::InvalidValue( - s.to_string(), - "both range ends must use the same dimensions".to_string(), - )); - } - let (lnum, rnum) = if lnum > rnum { (rnum, lnum) } else { (lnum, rnum) }; - let rng = CalendarRange { - strict: lstrict || rstrict, - rng: match ltp { - "" | "d" | "D" => CalendarRangeType::DayRange(lnum, rnum), - "w" | "W" => CalendarRangeType::WeekRange(lnum, rnum), - "m" | "M" => CalendarRangeType::MonthRange(lnum, rnum), - "y" | "Y" => CalendarRangeType::YearRange(lnum, rnum), - _ => { - return Err(terr::TodoError::InvalidValue(ltp.to_string(), "date range type".to_string())); - } - }, - }; - Ok(rng) - } - - fn parse_single(s: &str) -> Result { - let (tp, num, strict) = CalendarRange::parse_single_num(s)?; - let rng = CalendarRange { - strict, - rng: match tp { - "" | "d" | "D" => CalendarRangeType::Days(num), - "w" | "W" => CalendarRangeType::Weeks(num), - "m" | "M" => CalendarRangeType::Months(num), - "y" | "Y" => CalendarRangeType::Years(num), - _ => { - return Err(terr::TodoError::InvalidValue(tp.to_string(), "date 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, - } -} - -pub 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) - } -} - -pub fn add_years(dt: NaiveDate, num: u32, back: bool) -> NaiveDate { - let mut y = dt.year(); - let m = dt.month(); - let mut d = dt.day(); - if back { - y -= num as i32; - } else { - y += num as i32; - } - if d > days_in_month(y, m) { - d = days_in_month(y, m); - } - NaiveDate::from_ymd_opt(y, m, d).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 { - // bad due date - 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 { - // this week - base + Duration::days((wn - bn) as i64) - } else { - // next week - 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 { - // this week - base - Duration::days(bn as i64 - wn as i64) - } else { - // week before - base + Duration::days(wn as i64 - bn as i64 - DAYS_PER_WEEK as i64) - } -} - -// Converts "human" which is a string contains a number to a date. -// "human" is a day of a date. If today's day is less than "human", the function returns the -// "human" date of the next month, otherwise of this month. -// E.g: today=2022-06-20, human="24" --> 2022-06-24 -// today=2022-06-20, human="19" --> 2022-07-19 -fn day_of_first_month(base: NaiveDate, human: &str) -> HumanResult { - match human.parse::() { - 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::() { - 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::() { - 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) - } -} - -// Returns if a special day is always either in the future or in the past. E.g., `today` cannot be in -// the past and `yesterday` cannot be in the future, so the function returns `true` for both. -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}'")), - } -} - -// Converts human-readable date to an absolute date in todo-txt format. If the date is already an -// absolute value, the function returns None. In case of any error None is returned as well. -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| !c.is_ascii_digit()).is_none() { - if back { - return Err("negative day of month".to_string()); - } - return day_of_first_month(base, human); - } - if human.find(|c: char| !c.is_ascii_digit() && c != '-').is_none() { - if back { - return Err("negative absolute date".to_string()); - } - if human.matches('-').count() == 1 { - // month-day case - return no_year_date(base, human); - } - // normal date, nothing to fix - 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); - } - - // some "special" word like "tomorrow", "tue" - special_time_point(base, human, back, soon_days) -} - -// Replace a special word in due date with a real date. -// E.g, "due:sat" ==> "due:2022-07-09" for today between 2022-07-03 and 2022-07-09 -pub fn fix_date(base: NaiveDate, orig: &str, look_for: &str, soon_days: u8) -> Option { - 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(_) => 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 { - 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 { - 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::DayRange(n, _) => today.checked_add_signed(Duration::days(n.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::WeekRange(n, _) => { - 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 - } - CalendarRangeType::MonthRange(n, _) => { - let (today, diff) = if rng.strict { - (today, n) - } else { - ( - NaiveDate::from_ymd_opt(today.year(), today.month(), 1).unwrap_or(today), - if n > 0 { n - 1 } else { n + 1 }, - ) - }; - add_months(today, diff.unsigned_abs() as u32, n < 0) - } - CalendarRangeType::Years(n) => { - if n >= 0 { - if rng.strict { - return today; - } - return NaiveDate::from_ymd_opt(today.year(), 1, 1).unwrap_or(today); - } - let (today, diff) = if rng.strict { - (today, -n) - } else { - (NaiveDate::from_ymd_opt(today.year(), 1, 1).unwrap_or(today), -n - 1) - }; - add_years(today, diff as u32, n < 0) - } - CalendarRangeType::YearRange(n, _) => { - let (today, diff) = - if rng.strict { (today, n) } else { (NaiveDate::from_ymd_opt(today.year(), 1, 1).unwrap_or(today), n) }; - add_years(today, diff.unsigned_abs() as u32, n < 0) - } - } -} - -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::DayRange(_, n) => 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::WeekRange(_, n) => 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.unsigned_abs() as u32, n < 0); - 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.unsigned_abs() as u32, diff < 0) - } - CalendarRangeType::MonthRange(_, n) => { - let dt = add_months(today, n.unsigned_abs() as u32, n < 0); - if rng.strict { - dt - } else { - let y = dt.year(); - let m = dt.month(); - let d = days_in_month(y, m); - NaiveDate::from_ymd_opt(y, m, d).unwrap_or(dt) - } - } - CalendarRangeType::Years(n) => { - if rng.strict { - if n <= 0 { - return today; - } - return add_years(today, n as u32, false); - } - let dt = NaiveDate::from_ymd_opt(today.year(), 12, 31).unwrap_or(today); - if n <= 1 { - dt - } else { - add_years(dt, (n - 1) as u32, false) - } - } - CalendarRangeType::YearRange(_, n) => { - if rng.strict { - return add_years(today, n.unsigned_abs() as u32, n < 0); - } - let dt = add_years(today, n.unsigned_abs() as u32, n < 0); - NaiveDate::from_ymd_opt(dt.year(), 12, 31).unwrap_or(today) - } - } -} - -#[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 = 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 = 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 = 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(); // thursday - let tests: Vec = 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 = 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, - } - let tests: Vec = 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 = vec![ - TestCal { - td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday - 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(), // Monday - 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(), // Sunday - 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(), // Monday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), - }, - // --- - TestCal { - td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), - rng: CalendarRange { strict: false, rng: CalendarRangeType::Years(-1) }, - sunday: false, - res: NaiveDate::from_ymd_opt(2022, 01, 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 = vec![ - TestCal { - td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday - 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(), // Monday - 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(), // Sunday - 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(), // Monday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), // Sunday - 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(), - }, - // --- - TestCal { - td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), - rng: CalendarRange { strict: false, rng: CalendarRangeType::Years(-1) }, - sunday: false, - res: NaiveDate::from_ymd_opt(2022, 12, 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); - } - } -} diff --git a/src/main.rs b/src/main.rs index 2553473..d807a8c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,9 +7,7 @@ mod cal; mod colauto; mod conf; mod conv; -mod date_expr; mod fmt; -mod human_date; mod stats; mod subj_clean; mod tml; @@ -335,7 +333,7 @@ fn task_done(stdout: &mut StandardStream, tasks: &mut todo::TaskVec, conf: &conf writeln!(stdout, "Warning: you are going to mark all the tasks 'done'. Please specify tasks to complete.")?; std::process::exit(1); } - let processed = process_tasks(stdout, tasks, conf, "completed", todo::done_with_config)?; + let processed = process_tasks(stdout, tasks, conf, "completed", todo::done)?; if processed { if let Err(e) = todo::save(tasks, Path::new(&conf.todo_file)) { eprintln!("Failed to save to '{0:?}': {e}", &conf.todo_file);