Skip to content

Commit

Permalink
date calculation from tags
Browse files Browse the repository at this point in the history
  • Loading branch information
VladimirMarkelov committed Nov 16, 2024
1 parent 8bfb1bc commit fd703f5
Showing 1 changed file with 203 additions and 26 deletions.
229 changes: 203 additions & 26 deletions src/date_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,115 @@ pub struct ExprItem<'a> {
pub val: &'a str,
}

#[derive(Default)]
pub struct TaskTag {
pub name: String,

Check warning on line 18 in src/date_expr.rs

View workflow job for this annotation

GitHub Actions / Check

fields `name`, `svalue`, and `dvalue` are never read
pub svalue: Option<String>,
pub dvalue: Option<NaiveDate>,
}

pub struct TaskTagList(Vec<TaskTag>);

Check warning on line 23 in src/date_expr.rs

View workflow job for this annotation

GitHub Actions / Check

struct `TaskTagList` is never constructed

#[derive(Debug,PartialEq)]
pub enum TagValueType {
Raw(String),

Check warning on line 27 in src/date_expr.rs

View workflow job for this annotation

GitHub Actions / Check

variants `Raw`, `Calc`, and `None` are never constructed
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
}
// pub fn any_tag_value(&self, tags: &[&str]) -> TagValueType {
// for v in self.0.iter() {
// for tag in tags.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;
Expand Down Expand Up @@ -255,33 +364,22 @@ fn parse_abs_date(base: NaiveDate, s: &str, soon_days: u8) -> Result<NaiveDate,
}
}

fn calc_base(base: NaiveDate, s: &str, task: &todotxt::Task, soon_days: u8, counter: usize) -> Result<NaiveDate, String> {
fn calc_base(base: NaiveDate, s: &str, tags: &mut TaskTagList, soon_days: u8, counter: usize) -> Result<NaiveDate, String> {
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();
match spec.as_str() {
"due" if task.due_date.is_some() => {
if let Some(d) = task.due_date {
dt = d;
}
},
"created" => {
match task.create_date {
None => return Err("Task does not have creation date".to_string()),
Some(d) => dt = d,
}
let tval = tags.tag_value(spec.as_str());
match tval {
TagValueType::None => {
dt = parse_abs_date(dt, &s, soon_days)?;
},
"thr" | "t" | "threshold" if task.threshold_date.is_some() => {
if let Some(d) = task.threshold_date {
dt = d;
}
TagValueType::Calc(d) => {dt = d;},
TagValueType::Raw(s) => {

Check warning on line 378 in src/date_expr.rs

View workflow job for this annotation

GitHub Actions / Check

unused variable: `s`

Check warning on line 378 in src/date_expr.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused variable: `s`
let d = calc_expr(base, &spec, tags, soon_days, counter+1)?;
tags.set_tag(&spec, d);
dt = d;
},
_ => if let Some(val) = task.tags.get(s) {
dt = calc_expr(base, &val, task, soon_days, counter+1)?;
} else {
dt = parse_abs_date(dt, &s, soon_days)?;
},
}
} else {
// Absolute date
Expand All @@ -290,7 +388,7 @@ fn calc_base(base: NaiveDate, s: &str, task: &todotxt::Task, soon_days: u8, coun
Ok(dt)
}

fn calc_expr(base: NaiveDate, s: &str, task: &todotxt::Task, soon_days: u8, counter: usize) -> Result<NaiveDate, String> {
fn calc_expr(base: NaiveDate, s: &str, tags: &mut TaskTagList, soon_days: u8, counter: usize) -> Result<NaiveDate, String> {
if counter > 10 {
return Err("Recursion stack overflow".to_string());
}
Expand All @@ -304,7 +402,7 @@ fn calc_expr(base: NaiveDate, s: &str, task: &todotxt::Task, soon_days: u8, coun
for (idx, item) in items.iter().enumerate() {
match idx {
0 => {
dt = calc_base(base, item.val, task, soon_days, counter)?;
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() {
Expand Down Expand Up @@ -350,8 +448,8 @@ fn calc_expr(base: NaiveDate, s: &str, task: &todotxt::Task, soon_days: u8, coun
Ok(dt)
}

pub fn calculate_expr(base: NaiveDate, s: &str, task: &todotxt::Task, soon_days: u8) -> Result<NaiveDate, String> {
calc_expr(base, s, task, soon_days, 1)
pub fn calculate_expr(base: NaiveDate, s: &str, tags: &mut TaskTagList, soon_days: u8) -> Result<NaiveDate, String> {
calc_expr(base, s, tags, soon_days, 1)
}

#[cfg(test)]
Expand Down Expand Up @@ -567,8 +665,9 @@ mod date_expr_test {

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, &task, 8);
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);
Expand All @@ -582,4 +681,82 @@ mod date_expr_test {
}
}
}
#[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<ETest> = 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() {

Check warning on line 705 in src/date_expr.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused variable: `idx`
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);
}
}
}
#[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<ETest> = 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);
},
}
}
}
}

0 comments on commit fd703f5

Please sign in to comment.