diff --git a/src/date_expr.rs b/src/date_expr.rs index ab23e0b..99d14e3 100644 --- a/src/date_expr.rs +++ b/src/date_expr.rs @@ -1,5 +1,5 @@ use chrono::{Duration, NaiveDate}; -use todo_lib::{terr, tfilter, todotxt}; +use todo_lib::todotxt; use crate::human_date; @@ -13,7 +13,7 @@ pub struct ExprItem<'a> { pub val: &'a str, } -#[derive(Default,Clone)] +#[derive(Default, Clone)] pub struct TaskTag { pub name: String, pub svalue: Option, @@ -22,7 +22,7 @@ pub struct TaskTag { pub struct TaskTagList(Vec); -#[derive(Debug,PartialEq)] +#[derive(Debug, PartialEq)] pub enum TagValueType { Raw(String), Calc(NaiveDate), @@ -34,35 +34,19 @@ impl TaskTagList { 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, - }; + 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, - }; + 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, - }; + 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, - }; + let tg = TaskTag { name: key.clone(), svalue: Some(value.clone()), dvalue: None }; v.push(tg); } TaskTagList(v) @@ -71,18 +55,10 @@ impl TaskTagList { 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, - }; + 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)), - }; + let tg = TaskTag { name: "created".to_string(), dvalue: None, svalue: Some(todotxt::format_date(dt)) }; v.push(tg); TaskTagList(v) } @@ -113,11 +89,13 @@ 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..]; - }, + Some(i) => { + if i != 4 { + return None; + } else { + st = &st[i..]; + } + } } if !st.starts_with('-') { return None; @@ -125,32 +103,38 @@ fn parse_full_date(s: &str) -> Option<&str> { 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..]; - }, + 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; + None => { + if st.len() == 2 { + Some(s) + } else { + None } - Some(&s[..l]) - }, + } + 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]) + } + } } } @@ -159,52 +143,62 @@ 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..]; - }, + 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; + None => { + if st.len() == 2 { + Some(s) + } else { + None } - Some(&s[..l]) - }, + } + 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; + 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]) } - Some(&s[..i]) - }, + } } } @@ -248,12 +242,12 @@ fn parse_duration(s: &str) -> Option<&str> { } else { if let Some(cc) = rest.chars().next() { if durs.contains(&cc) { - if s.len() == idxl+1 { + if s.len() == idxl + 1 { Some(s) } else { - let rest = &s[idxl+1..]; + let rest = &s[idxl + 1..]; if rest.starts_with('-') || rest.starts_with('+') { - Some(&s[..idxl+1]) + Some(&s[..idxl + 1]) } else { None } @@ -267,11 +261,13 @@ fn parse_duration(s: &str) -> Option<&str> { } } 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 - }, + None => { + if s.len() == 1 { + Some(s) + } else { + None + } + } Some(idx) => { let rest = &s[idx..]; if rest.starts_with('-') || rest.starts_with('+') { @@ -279,7 +275,7 @@ fn parse_duration(s: &str) -> Option<&str> { } else { None } - }, + } } } else { None @@ -288,29 +284,29 @@ fn parse_duration(s: &str) -> Option<&str> { fn parse_base_date(s: &str) -> Result { if let Some(st) = parse_special(s) { - return Ok(ExprItem{sign: '+', val: st}); + return Ok(ExprItem { sign: '+', val: st }); } if let Some(st) = parse_full_date(s) { - return Ok(ExprItem{sign: '+', val: st}); + return Ok(ExprItem { sign: '+', val: st }); } if let Some(st) = parse_short_date(s) { - return Ok(ExprItem{sign: '+', val: st}); + return Ok(ExprItem { sign: '+', val: st }); } if let Some(st) = parse_single_day(s) { - return Ok(ExprItem{sign: '+', val: st}); + 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 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 { @@ -327,10 +323,10 @@ pub fn parse_expression(s: &str) -> Result, String> { match parse_duration(st) { None => return Err(format!("Invalid duration: '{st}'")), Some(v) => { - let ei = ExprItem{sign: c, val: v}; + let ei = ExprItem { sign: c, val: v }; st = &st[ei.val.len()..]; items.push(ei); - }, + } } } Ok(items) @@ -339,18 +335,26 @@ pub fn parse_expression(s: &str) -> Result, String> { 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}")), + 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) } - } else { - Err(e) } } } -fn calc_base(base: NaiveDate, s: &str, tags: &mut TaskTagList, soon_days: u8, counter: usize) -> Result { +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 @@ -359,13 +363,15 @@ fn calc_base(base: NaiveDate, s: &str, tags: &mut TaskTagList, soon_days: u8, co match tval { TagValueType::None => { dt = parse_abs_date(dt, &s, soon_days)?; - }, - TagValueType::Calc(d) => {dt = d;}, + } + TagValueType::Calc(d) => { + dt = d; + } TagValueType::Raw(s) => { - let d = calc_expr(base, &spec, tags, soon_days, counter+1)?; + let d = calc_expr(base, &s, tags, soon_days, counter + 1)?; tags.set_tag(&spec, d); dt = d; - }, + } } } else { // Absolute date @@ -374,7 +380,13 @@ fn calc_base(base: NaiveDate, s: &str, tags: &mut TaskTagList, soon_days: u8, co Ok(dt) } -fn calc_expr(base: NaiveDate, s: &str, tags: &mut TaskTagList, soon_days: u8, counter: usize) -> Result { +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()); } @@ -389,7 +401,7 @@ fn calc_expr(base: NaiveDate, s: &str, tags: &mut TaskTagList, soon_days: u8, co 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) @@ -400,7 +412,7 @@ fn calc_expr(base: NaiveDate, s: &str, tags: &mut TaskTagList, soon_days: u8, co Ok(r) => r, Err(e) => { return Err(format!("Invalid duration '{0}': {e}", item.val)); - }, + } }; match rc.period { todotxt::Period::Day => { @@ -410,7 +422,7 @@ fn calc_expr(base: NaiveDate, s: &str, tags: &mut TaskTagList, soon_days: u8, co Duration::days(rc.count as i64) }; dt += dur; - }, + } todotxt::Period::Week => { let dur = if item.sign == '-' { Duration::days(-(rc.count as i64) * 7) @@ -418,14 +430,14 @@ fn calc_expr(base: NaiveDate, s: &str, tags: &mut TaskTagList, soon_days: u8, co 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 == '-'); - }, - _ => {}, + } + _ => {} } } } @@ -438,10 +450,40 @@ pub fn calculate_expr(base: NaiveDate, s: &str, tags: &mut TaskTagList, soon_day 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::*; - use chrono::Local; struct Test { txt: &'static str, @@ -452,19 +494,19 @@ mod date_expr_test { #[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: ""}, + 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); @@ -474,12 +516,12 @@ mod date_expr_test { 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); } - }, + } } } } @@ -487,13 +529,13 @@ mod date_expr_test { #[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: ""}, + 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); @@ -503,12 +545,12 @@ mod date_expr_test { 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); } - }, + } } } } @@ -516,19 +558,19 @@ mod date_expr_test { #[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: ""}, + 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); @@ -538,12 +580,12 @@ mod date_expr_test { 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); } - }, + } } } } @@ -551,17 +593,17 @@ mod date_expr_test { #[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: ""}, + 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); @@ -571,12 +613,12 @@ mod date_expr_test { 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); } - }, + } } } } @@ -590,15 +632,15 @@ mod date_expr_test { 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: ""}, + 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); @@ -608,13 +650,20 @@ mod date_expr_test { 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); - }, + 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); } - }, + } } } } @@ -627,26 +676,23 @@ mod date_expr_test { 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()}, + 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(); @@ -677,11 +723,7 @@ mod date_expr_test { 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: "", 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, @@ -691,10 +733,16 @@ mod date_expr_test { for (idx, test) in tests.iter().enumerate() { let tlist = TaskTagList::from_str(test.txt, base); assert_eq!(test.count, tlist.0.len()); - for idx in 0..test.values.len()/2 { - let v = tlist.tag_value(test.values[idx*2]); - assert_eq!(v, TagValueType::Raw(test.values[idx*2+1].to_string()), "{idx}. Tag [{0}] must get value [{1}] instead of [{2:?}]", - test.values[idx*2], test.values[idx*2+1], v); + 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 + ); } } } @@ -708,41 +756,53 @@ mod date_expr_test { 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", - }, + 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); - }, + 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 index 3a0eeec..c130b77 100644 --- a/src/human_date.rs +++ b/src/human_date.rs @@ -130,11 +130,8 @@ impl CalendarRange { "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(), - )); - }, + return Err(terr::TodoError::InvalidValue(ltp.to_string(), "date range type".to_string())); + } }, }; Ok(rng) @@ -150,11 +147,8 @@ impl CalendarRange { "m" | "M" => CalendarRangeType::Months(num), "y" | "Y" => CalendarRangeType::Years(num), _ => { - return Err(terr::TodoError::InvalidValue( - tp.to_string(), - "date range type".to_string(), - )); - }, + return Err(terr::TodoError::InvalidValue(tp.to_string(), "date range type".to_string())); + } }, }; Ok(rng) @@ -546,12 +540,7 @@ pub fn fix_date(base: NaiveDate, orig: &str, look_for: &str, soon_days: u8) -> O 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 - } + 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(); diff --git a/src/main.rs b/src/main.rs index 6b1a0fb..874c381 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,12 +7,12 @@ mod cal; mod colauto; mod conf; mod conv; +mod date_expr; mod fmt; mod human_date; mod stats; mod subj_clean; mod tml; -mod date_expr; use std::collections::HashMap; use std::env; @@ -175,6 +175,18 @@ fn task_add(stdout: &mut StandardStream, tasks: &mut todo::TaskVec, conf: &conf: Some(s) => s.clone(), }; let now = chrono::Local::now().date_naive(); + let mut tag_list = date_expr::TaskTagList::from_str(&subj, now); + let subj = match date_expr::calculate_main_tags(now, &mut tag_list, conf.fmt.colors.soon_days) { + Err(e) => { + eprintln!("{e:?}"); + std::process::exit(1); + } + Ok(changed) => match changed { + false => subj, + true => date_expr::update_tags_in_str(&tag_list, &subj), + }, + }; + if conf.dry { let t = todotxt::Task::parse(&subj, now); let (cols, widths) = cols_with_width(&[t.clone()], &[0], conf);