todo_lib 11.1.0

Collection of utilities for todo.txt format
Documentation
use chrono;
use todo_lib::tfilter::TodoStatus;
use todo_lib::{tfilter, todo, todotxt, tsort};

fn init_tasks() -> todo::TaskVec {
    let mut t = Vec::new();
    let now = chrono::Local::now().date_naive();

    t.push(todotxt::Task::parse("call mother #tagone +family @parents", now));
    t.push(todotxt::Task::parse(
        "x (C) 2018-10-05 2018-10-01 call to car service and schedule repair +car @repair",
        now,
    ));
    t.push(todotxt::Task::parse(
        "(B) 2018-10-15 repair #tagtwo family car +Car @repair due:2018-12-01 t:2019-01-02",
        now,
    ));
    t.push(todotxt::Task::parse("(A) Kid's art #tagthree school lesson +Family @Kids due:2018-11-10 rec:1w", now));
    t.push(todotxt::Task::parse("take kid to hockey #tagone #tagtwo game +Family @kids due:2018-11-18", now));
    t.push(todotxt::Task::parse("xmas vacations +FamilyHoliday due:2018-12-24", now));

    t
}

fn init_tasks_with_empty() -> todo::TaskVec {
    let mut t = Vec::new();
    let now = chrono::Local::now().date_naive();

    t.push(todotxt::Task::parse("call mother #tagone +family @parents", now));
    t.push(todotxt::Task::parse("", now));
    t.push(todotxt::Task::parse("xmas vacations +FamilyHoliday due:2018-12-24", now));
    t.push(todotxt::Task::parse("", now));
    t
}

#[test]
fn one_item() {
    let t = init_tasks();
    let mut cflt = tfilter::Conf::default();

    // invalid ranges
    cflt.range = tfilter::ItemRange::One(t.len());
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids.len(), 0);

    cflt.range = tfilter::ItemRange::One(t.len() + 1);
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids.len(), 0);
}

#[test]
fn item_range() {
    let t = init_tasks();
    let mut cflt = tfilter::Conf::default();

    // both ends are out of range
    cflt.range = tfilter::ItemRange::Range(t.len(), t.len() + 5);
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids.len(), 0);

    // one item that is completed
    cflt.range = tfilter::ItemRange::One(1);
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids.len(), 0);

    // short range only active
    cflt.range = tfilter::ItemRange::Range(1, 3);
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![2, 3]);
    cflt.range = tfilter::ItemRange::None;
}

#[test]
fn item_status() {
    let t = init_tasks();
    let mut cflt = tfilter::Conf::default();

    // one incomplete
    cflt.range = tfilter::ItemRange::One(0);
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![0]);

    // full range all
    cflt.all = tfilter::TodoStatus::All;
    cflt.range = tfilter::ItemRange::Range(0, 10);
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![0, 1, 2, 3, 4, 5]);

    // full range only completed
    cflt.all = tfilter::TodoStatus::Done;
    cflt.range = tfilter::ItemRange::Range(0, 10);
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![1]);

    // full range only active
    cflt.all = tfilter::TodoStatus::Active;
    cflt.range = tfilter::ItemRange::Range(0, 10);
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![0, 2, 3, 4, 5]);
}

#[test]
fn item_regex() {
    let t = init_tasks();
    let mut cflt = tfilter::Conf::default();

    // all with 'car' anywhere
    cflt.all = tfilter::TodoStatus::All;
    cflt.regex = Some("car".to_owned());
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![1, 2]);
    cflt.all = tfilter::TodoStatus::Active;

    // active with <regex> anywhere
    cflt.use_regex = true;
    cflt.regex = Some("CA[rl]".to_owned());
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![0, 2]);
}

#[test]
fn item_projects() {
    let t = init_tasks();
    let mut cflt = tfilter::Conf::default();

    // active with 'car' project
    cflt.include.projects.push("car".to_owned());
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![2]);
    cflt.include.projects.clear();

    // active with 'family' project
    cflt.include.projects.push("FAMILY".to_owned());
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![0, 3, 4]);
    cflt.include.projects.clear();

    // active with 'family' project
    cflt.include.projects.push("FAMILY*".to_owned());
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![0, 3, 4, 5]);
    cflt.include.projects.clear();

    // active with 'holiday' project
    cflt.include.projects.push("*holiday".to_owned());
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![5]);
    cflt.include.projects.clear();

    // active with 'family' related projects
    cflt.include.projects.push("*family*".to_owned());
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![0, 3, 4, 5]);
    cflt.include.projects.clear();

    // Exclude filtering
    cflt.exclude.projects.push("*family*".to_owned());
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![2]);
    cflt.exclude.projects.clear();

    cflt.exclude.projects.push("FAMILY".to_owned());
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![2, 5]);
    cflt.exclude.projects.clear();

    // Exclude has higher priority
    cflt.include.projects.push("*family*".to_owned());
    cflt.exclude.projects.push("FAMILY".to_owned());
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![5]);
}

#[test]
fn item_contexts() {
    let t = init_tasks();
    let mut cflt = tfilter::Conf::default();

    // active with 'kids' context
    cflt.include.contexts.push("kids".to_owned());
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![3, 4]);
    cflt.include.contexts.clear();

    cflt.exclude.contexts.push("kids".to_owned());
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![0, 2, 5]);
}

#[test]
fn item_priority() {
    let t = init_tasks();
    let mut cflt = tfilter::Conf::default();
    cflt.all = tfilter::TodoStatus::All;

    // only B priority
    cflt.pri = Some(tfilter::Priority { value: 'b' as u8 - 'a' as u8, span: tfilter::ValueSpan::Equal });
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![2]);

    // B priority and higher
    cflt.pri = Some(tfilter::Priority { value: 'b' as u8 - 'a' as u8, span: tfilter::ValueSpan::Higher });
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![2, 3]);

    //  B priority and lower
    cflt.pri = Some(tfilter::Priority { value: 'b' as u8 - 'a' as u8, span: tfilter::ValueSpan::Lower });
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![0, 1, 2, 4, 5]);

    // any priority except no priority
    cflt.pri = Some(tfilter::Priority { value: todotxt::NO_PRIORITY, span: tfilter::ValueSpan::Any });
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![1, 2, 3]);

    // no priority
    cflt.pri = Some(tfilter::Priority { value: todotxt::NO_PRIORITY, span: tfilter::ValueSpan::None });
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![0, 4, 5]);
}

#[test]
fn item_recurrence() {
    let t = init_tasks();
    let mut cflt = tfilter::Conf::default();
    cflt.all = tfilter::TodoStatus::All;

    // with recurrence
    cflt.rec = Some(tfilter::Recurrence { span: tfilter::ValueSpan::Any });
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![3]);

    // without recurrence
    cflt.rec = Some(tfilter::Recurrence { span: tfilter::ValueSpan::None });
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![0, 1, 2, 4, 5]);
}

#[test]
fn item_due() {
    let t = init_tasks();

    let mut cflt = tfilter::Conf::default();
    cflt.all = tfilter::TodoStatus::All;

    // with due
    cflt.due = Some(tfilter::DateRange { span: tfilter::ValueSpan::Any, days: Default::default() });
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![2, 3, 4, 5]);

    // without due
    cflt.due = Some(tfilter::DateRange { span: tfilter::ValueSpan::None, days: Default::default() });
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![0, 1]);

    let sconf = tsort::Conf { fields: Some("due".to_string()), rev: true };
    let mut ids: todo::IDVec = vec![0, 1, 2, 3, 4, 5];
    tsort::sort(&mut ids, &t, &sconf);
    assert_eq!(ids, vec![1, 0, 5, 2, 4, 3]);
}

#[test]
fn item_threshold() {
    let t = init_tasks();

    let mut cflt = tfilter::Conf::default();

    // with thr
    cflt.thr = Some(tfilter::DateRange { span: tfilter::ValueSpan::Any, days: Default::default() });
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![2]);

    // without thr
    cflt.thr = Some(tfilter::DateRange { span: tfilter::ValueSpan::None, days: Default::default() });
    let ids = tfilter::filter(&t, &cflt);
    assert_eq!(ids, vec![0, 3, 4, 5]);

    let sconf = tsort::Conf { fields: Some("thr".to_string()), rev: false };
    let mut ids: todo::IDVec = vec![0, 1, 2, 3, 4, 5];
    tsort::sort(&mut ids, &t, &sconf);
    assert_eq!(ids, vec![2, 0, 1, 3, 4, 5]);
}

#[test]
fn item_tags() {
    let t = init_tasks();

    struct Test {
        inc: Vec<&'static str>,
        exc: Vec<&'static str>,
        res: todo::IDVec,
    }
    let tests: Vec<Test> = vec![
        Test { inc: vec!["tagone"], exc: Vec::new(), res: vec![0, 4] },
        Test { inc: vec!["tagone", "tagtwo"], exc: Vec::new(), res: vec![0, 2, 4] },
        Test { inc: vec!["tagone"], exc: vec!["tagtwo"], res: vec![0] },
        Test { inc: vec!["any"], exc: Vec::new(), res: vec![0, 2, 3, 4] },
        Test { inc: vec!["none"], exc: Vec::new(), res: vec![5] },
    ];

    for (idx, test) in tests.iter().enumerate() {
        let mut cflt = tfilter::Conf::default();
        for tg in test.inc.iter() {
            cflt.include.hashtags.push(tg.to_string());
        }
        for tg in test.exc.iter() {
            cflt.exclude.hashtags.push(tg.to_string());
        }
        let ids = tfilter::filter(&t, &cflt);
        assert_eq!(ids, test.res, "{}. {:?} != {:?}", idx, ids, test.res);
    }
}

#[test]
fn empty_tasks() {
    let t = init_tasks_with_empty();
    struct Test {
        inc: TodoStatus,
        res: todo::IDVec,
    }
    let tests: Vec<Test> = vec![
        Test { inc: TodoStatus::Active, res: vec![0, 2] },
        Test { inc: TodoStatus::Empty, res: vec![1, 3] },
        Test { inc: TodoStatus::All, res: vec![0, 1, 2, 3] },
    ];
    for (idx, test) in tests.iter().enumerate() {
        let mut cflt = tfilter::Conf::default();
        cflt.all = test.inc;
        let ids = tfilter::filter(&t, &cflt);
        assert_eq!(ids, test.res, "{idx}. {ids:?} != {:?}", test.res);
    }
}

#[test]
fn custom_filter_test() {
    let t = init_tasks();
    struct Test {
        flt: &'static str,
        res: todo::IDVec,
    }
    let tests: Vec<Test> = vec![
        Test { flt: "subj=HOCKEY", res: vec![4] },
        Test { flt: "subj=abcde", res: vec![] },
        Test { flt: "prj=Family", res: vec![0, 3, 4] },
        Test { flt: "prj=Family|prj=car", res: vec![0, 2, 3, 4] },
        Test { flt: "prj=Family;ctx=Kids", res: vec![3, 4] },
    ];
    for (idx, test) in tests.iter().enumerate() {
        let mut cflt = tfilter::Conf::default();
        cflt.custom_filter = Some(test.flt.to_string());
        let mut ids = tfilter::filter(&t, &cflt);
        ids.sort();
        assert_eq!(ids, test.res, "{idx}. {ids:?} != {:?}", test.res);
    }
}