ttdl 0.7.3

TTDL - Terminal ToDo List manager
use chrono;
use std::cmp::Ordering;

use crate::fmt;
use caseless::default_caseless_match_str;
use todo_lib::*;

const TOTAL: &str = "";
const PRJ_WIDTH: usize = 15;
const CTX_WIDTH: usize = 10;
const NUM_WIDTH: usize = 10;

pub fn show_stats(tasks: &todo::TaskSlice, conf: &fmt::Conf) {
    show_short_stats(tasks);
    if conf.fmt != fmt::Format::Short {
        println!();
        show_full_stats(tasks);
    }
}

fn show_short_stats(tasks: &todo::TaskSlice) {
    let mut overdue: usize = 0;
    let mut threshold: usize = 0;
    let mut recurrent: usize = 0;
    let mut done: usize = 0;

    for t in tasks.iter() {
        if t.finished {
            done += 1;
            continue;
        }
        if t.recurrence.is_some() {
            recurrent += 1;
        }
        if let Some(d) = t.due_date {
            if d < chrono::Utc::now().naive_utc().date() {
                overdue += 1;
            }
        }
        if let Some(d) = t.threshold_date {
            if d < chrono::Utc::now().naive_utc().date() {
                threshold += 1;
            }
        }
    }

    let width = 19;
    let length = tasks.len();
    println!("{:wid$}{:4}", "Total todos:", length, wid = width);
    if done > 0 {
        println!("{:wid$}{:>4} ({}%)", "Done:", done, done * 100 / length, wid = width);
    }
    if threshold > 0 {
        println!(
            "{:wid$}{:4} ({}%)",
            "Missed theshold:",
            threshold,
            threshold * 100 / length,
            wid = width
        );
    }
    if overdue > 0 {
        println!(
            "{:wid$}{:4} ({}%)",
            "Overdue:",
            overdue,
            overdue * 100 / length,
            wid = width
        );
    }
    if recurrent > 0 {
        println!("{:wid$}{:4}", "Recurrent:", recurrent, wid = width);
    }
}

struct Stat {
    prj: String,
    ctx: String,
    total: usize,
    done: usize,
    overdue: usize,
    spent: chrono::Duration,
}
struct Stats {
    stats: Vec<Stat>,
}
impl Stats {
    fn update_stat(&mut self, prj: &str, ctx: &str, done: bool, overdue: bool, spent: chrono::Duration) {
        let mut found = false;
        for t in self.stats.iter_mut() {
            if default_caseless_match_str(&t.prj, prj) && default_caseless_match_str(&t.ctx, ctx) {
                t.total += 1;
                if done {
                    t.done += 1;
                }
                if overdue {
                    t.overdue += 1;
                }
                t.spent = t.spent + spent;
                found = true;
            }
        }

        if !found {
            let s = Stat {
                prj: prj.to_lowercase(),
                ctx: ctx.to_lowercase(),
                total: 1,
                done: if done { 1 } else { 0 },
                overdue: if overdue { 1 } else { 0 },
                spent,
            };
            self.stats.push(s);
        }
    }
    fn sort(&mut self) {
        self.stats.sort_by(|a, b| {
            if a.prj == TOTAL {
                return Ordering::Less;
            }
            if b.prj == TOTAL {
                return Ordering::Greater;
            }

            let c = a.prj.to_lowercase().cmp(&b.prj.to_lowercase());
            if c != Ordering::Equal {
                return c;
            }
            if a.ctx == TOTAL {
                return Ordering::Less;
            }
            if b.ctx == TOTAL {
                return Ordering::Greater;
            }
            a.ctx.to_lowercase().cmp(&b.ctx.to_lowercase())
        });
    }
}

fn show_full_stats(tasks: &todo::TaskSlice) {
    let mut st = Stats { stats: Vec::new() };

    for t in tasks.iter() {
        let done = t.finished;
        let overdue = if let Some(d) = t.due_date {
            d < chrono::Utc::now().naive_utc().date()
        } else {
            false
        };
        let spent = timer::spent_time(t);
        st.update_stat(TOTAL, TOTAL, done, overdue, spent);
        for p in t.projects.iter() {
            st.update_stat(p, TOTAL, done, overdue, spent);
            for c in t.contexts.iter() {
                st.update_stat(p, c, done, overdue, spent);
            }
        }
    }

    st.sort();
    let mut last_prj = "".to_string();
    let mut proj_total: usize = 0;

    let length = tasks.len();
    let header = format!(
        "{:pw$} {:cw$} {:nw$} {:nw$} {:nw$} {}",
        "Project",
        "Context",
        "Total",
        "Done",
        "Overdue",
        "Spent",
        pw = PRJ_WIDTH,
        cw = CTX_WIDTH,
        nw = NUM_WIDTH
    );
    println!("{}", header);
    let sep = "-".repeat(header.len());

    for s in st.stats.iter() {
        let prj = if s.prj == last_prj {
            String::new()
        } else {
            println!("{}", sep);
            last_prj = s.prj.clone();
            proj_total = s.total;
            if s.prj.len() > PRJ_WIDTH {
                format!("{:.w$}", s.prj, w = PRJ_WIDTH)
            } else {
                s.prj.clone()
            }
        };
        print!("{:w$} ", prj, w = PRJ_WIDTH);

        let ctx = if s.ctx.len() > CTX_WIDTH {
            format!("{:.w$}", s.ctx, w = CTX_WIDTH)
        } else {
            s.ctx.clone()
        };
        print!("{:w$} ", ctx, w = CTX_WIDTH);

        let div_by = if s.ctx.is_empty() { length } else { proj_total };
        let sn = format!("{}({:>3}%)", s.total, s.total * 100 / div_by);
        print!("{:>w$} ", &sn, w = NUM_WIDTH);

        let sn = if s.done == 0 {
            String::new()
        } else {
            format!("{}({:>3}%)", s.done, s.done * 100 / s.total)
        };
        print!("{:>w$} ", &sn, w = NUM_WIDTH);

        let sn = if s.overdue == 0 {
            String::new()
        } else {
            format!("{}({:>3}%)", s.overdue, s.overdue * 100 / s.total)
        };
        print!("{:>w$} ", &sn, w = NUM_WIDTH);

        if s.spent.num_seconds() == 0 {
            println!();
        } else {
            println!("{}", &fmt::duration_str(s.spent));
        }
    }
}