use regex::Regex;
use crate::timer;
use crate::todo;
pub const INCLUDE_NONE: i64 = -9_999_998;
const NONE_TITLE: &str = "none";
const ANY_TITLE: &str = "any";
#[derive(Debug, Clone)]
pub enum ItemRange {
None,
One(usize),
Range(usize, usize),
List(Vec<usize>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum TodoStatus {
Active,
All,
Done,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ValueRange {
pub low: i64,
pub high: i64,
}
impl Default for ValueRange {
fn default() -> ValueRange {
ValueRange { low: 0, high: 0 }
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ValueSpan {
None,
Equal,
Lower,
Higher,
Any,
Range,
Active,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DateRange {
pub days: ValueRange,
pub span: ValueSpan,
}
impl Default for DateRange {
fn default() -> DateRange {
DateRange {
span: ValueSpan::None,
days: Default::default(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Recurrence {
pub span: ValueSpan,
}
impl Default for Recurrence {
fn default() -> Recurrence {
Recurrence { span: ValueSpan::None }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Priority {
pub value: u8,
pub span: ValueSpan,
}
impl Default for Priority {
fn default() -> Priority {
Priority {
value: todo::NO_PRIORITY,
span: ValueSpan::None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Timer {
pub span: ValueSpan,
pub value: usize,
}
impl Default for Timer {
fn default() -> Timer {
Timer {
value: 0,
span: ValueSpan::None,
}
}
}
#[derive(Debug, Clone)]
pub struct Conf {
pub range: ItemRange,
pub projects: Vec<String>,
pub contexts: Vec<String>,
pub tags: Vec<String>,
pub regex: Option<String>,
pub use_regex: bool,
pub all: TodoStatus,
pub due: Option<DateRange>,
pub thr: Option<DateRange>,
pub rec: Option<Recurrence>,
pub pri: Option<Priority>,
pub tmr: Option<Timer>,
pub created: Option<DateRange>,
pub finished: Option<DateRange>,
}
impl Default for Conf {
fn default() -> Conf {
Conf {
range: ItemRange::None,
projects: Vec::new(),
contexts: Vec::new(),
tags: Vec::new(),
regex: None,
use_regex: false,
all: TodoStatus::Active,
due: None,
thr: None,
rec: None,
pri: None,
tmr: None,
created: None,
finished: None,
}
}
}
fn filter_regex(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVec {
let rx = match &c.regex {
None => return v,
Some(s) => s,
};
let mut new_v: todo::IDVec = Vec::new();
if c.use_regex {
let rx = match Regex::new(&format!("(?i){}", rx)) {
Err(_) => {
println!("Invalid regex");
return v;
}
Ok(v) => v,
};
for i in v.iter() {
let idx = *i;
if idx >= tasks.len() {
continue;
}
if rx.is_match(&tasks[idx].subject) {
new_v.push(idx);
}
}
return new_v;
}
let rstr = rx.to_lowercase();
for i in v.iter() {
let idx = *i;
let low = tasks[idx].subject.to_lowercase();
if low.find(&rstr).is_some() {
new_v.push(idx);
continue;
}
}
new_v
}
fn vec_match(task_list: &[String], filter: &[String]) -> bool {
if filter.is_empty() {
return true;
}
for f in filter.iter() {
if (f == NONE_TITLE && task_list.is_empty()) || (f == ANY_TITLE && !task_list.is_empty()) {
return true;
}
}
for ctx in task_list.iter() {
let low = ctx.to_lowercase();
for tag in filter.iter() {
let ltag = tag.to_lowercase();
if str_matches(&low, <ag) {
return true;
}
}
}
false
}
fn filter_context(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVec {
if c.contexts.is_empty() {
return v;
}
let mut new_v: todo::IDVec = Vec::new();
for i in v.iter() {
let idx = *i;
if vec_match(&tasks[idx].contexts, &c.contexts) {
new_v.push(idx);
}
}
new_v
}
fn filter_project(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVec {
if c.projects.is_empty() {
return v;
}
let mut new_v: todo::IDVec = Vec::new();
for i in v.iter() {
let idx = *i;
if vec_match(&tasks[idx].projects, &c.projects) {
new_v.push(idx);
}
}
new_v
}
fn filter_tag(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVec {
if c.tags.is_empty() {
return v;
}
let mut new_v: todo::IDVec = Vec::new();
for i in v.iter() {
let idx = *i;
let mut tag_list: Vec<String> = Vec::new();
for (k, _v) in tasks[idx].tags.iter() {
tag_list.push(k.to_string());
}
if vec_match(&tag_list, &c.tags) {
new_v.push(idx);
}
}
new_v
}
fn filter_priority(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVec {
match &c.pri {
None => v,
Some(p) => {
let mut new_v: todo::IDVec = Vec::new();
for i in v.iter() {
let idx = *i;
match p.span {
ValueSpan::None => {
if tasks[idx].priority == todo::NO_PRIORITY {
new_v.push(idx);
}
}
ValueSpan::Equal => {
if p.value == tasks[idx].priority {
new_v.push(idx);
}
}
ValueSpan::Lower => {
if p.value <= tasks[idx].priority {
new_v.push(idx);
}
}
ValueSpan::Higher => {
if p.value >= tasks[idx].priority {
new_v.push(idx);
}
}
ValueSpan::Any => {
if tasks[idx].priority < todo::NO_PRIORITY {
new_v.push(idx);
}
}
_ => {}
}
}
new_v
}
}
}
fn filter_recurrence(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVec {
match &c.rec {
None => v,
Some(r) => {
let mut new_v: todo::IDVec = Vec::new();
for i in v.iter() {
let idx = *i;
match r.span {
ValueSpan::None => {
if tasks[idx].recurrence.is_none() {
new_v.push(idx);
}
}
ValueSpan::Any => {
if tasks[idx].recurrence.is_some() {
new_v.push(idx);
}
}
_ => {}
}
}
new_v
}
}
}
fn filter_due(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVec {
match &c.due {
None => v,
Some(due) => {
let today = chrono::Local::now().date().naive_local();
let mut new_v: todo::IDVec = Vec::new();
for i in v.iter() {
let idx = *i;
match due.span {
ValueSpan::None => {
if tasks[idx].due_date.is_none() {
new_v.push(idx);
}
}
ValueSpan::Any => {
if tasks[idx].due_date.is_some() {
new_v.push(idx);
}
}
ValueSpan::Higher => {
if let Some(d) = tasks[idx].due_date {
let diff = d - today;
if diff.num_days() > due.days.high {
new_v.push(idx);
}
} else if due.days.low == INCLUDE_NONE {
new_v.push(idx);
}
}
ValueSpan::Lower => {
if let Some(d) = tasks[idx].due_date {
let diff = d - today;
if diff.num_days() < due.days.low {
new_v.push(idx);
}
} else if due.days.high == INCLUDE_NONE {
new_v.push(idx);
}
}
ValueSpan::Range => {
if let Some(d) = tasks[idx].due_date {
let diff = d - today;
if diff.num_days() >= due.days.low && diff.num_days() <= due.days.high {
new_v.push(idx);
}
}
}
_ => {}
}
}
new_v
}
}
}
fn filter_created(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVec {
match &c.created {
None => v,
Some(created) => {
let mut new_v: todo::IDVec = Vec::new();
for i in v.iter() {
let idx = *i;
if date_in_range(&tasks[idx].create_date, &created) {
new_v.push(idx);
}
}
new_v
}
}
}
fn filter_finished(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVec {
match &c.finished {
None => v,
Some(finished) => {
let mut new_v: todo::IDVec = Vec::new();
for i in v.iter() {
let idx = *i;
if date_in_range(&tasks[idx].finish_date, &finished) {
new_v.push(idx);
}
}
new_v
}
}
}
fn filter_threshold(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVec {
match &c.thr {
None => v,
Some(thr) => {
let mut new_v: todo::IDVec = Vec::new();
for i in v.iter() {
let idx = *i;
if date_in_range(&tasks[idx].threshold_date, &thr) {
new_v.push(idx);
}
}
new_v
}
}
}
fn date_in_range(date: &Option<chrono::NaiveDate>, range: &DateRange) -> bool {
let today = chrono::Local::now().date().naive_local();
match range.span {
ValueSpan::None => date.is_none(),
ValueSpan::Any => date.is_some(),
ValueSpan::Higher => {
if let Some(d) = date {
let diff = *d - today;
diff.num_days() > range.days.high
} else {
range.days.low == INCLUDE_NONE
}
}
ValueSpan::Lower => {
if let Some(d) = date {
let diff = *d - today;
diff.num_days() < range.days.low
} else {
range.days.high == INCLUDE_NONE
}
}
ValueSpan::Range => {
if let Some(d) = date {
let diff = *d - today;
diff.num_days() >= range.days.low && diff.num_days() <= range.days.high
} else {
false
}
}
_ => false,
}
}
fn filter_timer(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVec {
match &c.tmr {
None => v,
Some(r) => {
let mut new_v: todo::IDVec = Vec::new();
for i in v.iter() {
let idx = *i;
match r.span {
ValueSpan::None => {
if !timer::is_timer_on(&tasks[idx]) {
new_v.push(idx);
}
}
ValueSpan::Active => {
if timer::is_timer_on(&tasks[idx]) {
new_v.push(idx);
}
}
_ => {}
}
}
new_v
}
}
}
fn is_status_ok(task: &todo_txt::task::Extended, status: &TodoStatus) -> bool {
!((*status == TodoStatus::Active && task.finished) || (*status == TodoStatus::Done && !task.finished))
}
pub fn filter(tasks: &todo::TaskSlice, c: &Conf) -> todo::IDVec {
let mut v: todo::IDVec = Vec::new();
match c.range {
ItemRange::One(i) => {
if i < tasks.len() && is_status_ok(&tasks[i], &c.all) {
v.push(i);
}
}
ItemRange::Range(min, max) => {
let mut start = min;
while start <= max {
if start >= tasks.len() {
break;
}
if is_status_ok(&tasks[start], &c.all) {
v.push(start);
}
start += 1;
}
}
ItemRange::List(ref lst) => {
for idx in lst.iter() {
if *idx == 0 || *idx >= tasks.len() {
continue;
}
if is_status_ok(&tasks[*idx], &c.all) {
v.push(*idx);
}
}
}
_ => {
for (i, ref item) in tasks.iter().enumerate() {
if is_status_ok(item, &c.all) {
v.push(i);
}
}
}
}
v = filter_regex(tasks, v, c);
v = filter_tag(tasks, v, c);
v = filter_project(tasks, v, c);
v = filter_context(tasks, v, c);
v = filter_priority(tasks, v, c);
v = filter_recurrence(tasks, v, c);
v = filter_due(tasks, v, c);
v = filter_created(tasks, v, c);
v = filter_finished(tasks, v, c);
v = filter_threshold(tasks, v, c);
v = filter_timer(tasks, v, c);
v
}
fn str_matches(orig: &str, patt: &str) -> bool {
if patt.starts_with('*') && patt.ends_with('*') {
let p = patt.trim_matches('*');
orig.find(p).is_some()
} else if patt.starts_with('*') {
let p = patt.trim_start_matches('*');
orig.ends_with(p)
} else if patt.ends_with('*') {
let p = patt.trim_end_matches('*');
orig.starts_with(p)
} else {
orig == patt
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn matches() {
assert!(!str_matches("abcd", "abc"));
assert!(str_matches("abcd", "abcd"));
assert!(!str_matches("abcd", "abcde"));
assert!(str_matches("abcd", "abc*"));
assert!(str_matches("abcd", "*bcd"));
assert!(str_matches("abcd", "*b*"));
assert!(!str_matches("abcd", "bc*"));
assert!(!str_matches("abcd", "*bc"));
assert!(!str_matches("abcd", ""));
assert!(!str_matches("", "abcd"));
assert!(str_matches("abcd", "*d*"));
assert!(str_matches("abcd", "*a*"));
}
}