caltemps 0.3.0

A tool to query and report on your iCalendar data from vDirs.
Documentation
use super::cli::{EnumOutputTypes, EnumReportTypes};
use super::{CalTempsDateRange, CalTempsEntry};
use std::cmp::min;
use std::io;

use chrono::{DateTime, Datelike, Days, Local, Months, Timelike};
use cli_table::{Cell, Style, Table, format::Justify, print_stdout};
use itertools::Itertools;

pub fn get_next_1st(date: DateTime<Local>) -> DateTime<Local> {
    date.with_day(1)
        .unwrap_or(date)
        .with_hour(0)
        .unwrap_or(date)
        .with_minute(0)
        .unwrap_or(date)
        .with_second(0)
        .unwrap_or(date)
        .with_nanosecond(0)
        .unwrap_or(date)
        .checked_add_months(Months::new(1))
        .unwrap_or(date)
}
pub fn get_next_monday(date: DateTime<Local>) -> DateTime<Local> {
    date.with_hour(0)
        .unwrap_or(date)
        .with_minute(0)
        .unwrap_or(date)
        .with_second(0)
        .unwrap_or(date)
        .with_nanosecond(0)
        .unwrap_or(date)
        .checked_add_days(Days::new(
            (7 - date.weekday().num_days_from_monday()).into(),
        ))
        .unwrap_or(date)
}

pub fn format_datetime(datetime: Option<DateTime<Local>>, report_type: EnumReportTypes) -> String {
    match datetime {
        Some(dt) => match report_type {
            EnumReportTypes::Totals => format!("{}", dt),
            EnumReportTypes::Monthly => format!("{:0>4}-{:0>2}", dt.year(), dt.month()),
            EnumReportTypes::Weekly => format!("{:0>4}-W{:0>2}", dt.year(), dt.iso_week().week()),
        },
        _ => "".into(),
    }
}

pub fn split_date_range(
    date_range: &CalTempsDateRange,
    report_type: EnumReportTypes,
) -> Vec<CalTempsDateRange> {
    match date_range.end {
        Some(dre) => match date_range.start {
            Some(drs) => match report_type {
                // Only list totals
                EnumReportTypes::Totals => vec![],
                // Split on month change
                EnumReportTypes::Monthly => {
                    let mut o = Vec::<CalTempsDateRange>::new();
                    let mut prev_start = drs;
                    let mut next_1st = get_next_1st(drs);
                    while next_1st < dre {
                        // Add piece
                        o.push(CalTempsDateRange {
                            start: Some(prev_start),
                            end: Some(next_1st),
                        });
                        // Advance one month
                        prev_start = next_1st;
                        next_1st = get_next_1st(next_1st);
                    }
                    o.push(CalTempsDateRange {
                        start: Some(prev_start),
                        end: Some(dre),
                    });
                    o
                }
                // Split Monday to Sunday
                EnumReportTypes::Weekly => {
                    let mut o = Vec::<CalTempsDateRange>::new();
                    let mut prev_start = drs;
                    let mut next_monday = get_next_monday(drs);
                    while next_monday < dre {
                        // Add piece
                        o.push(CalTempsDateRange {
                            start: Some(prev_start),
                            end: Some(next_monday),
                        });
                        // Advance one week
                        prev_start = next_monday;
                        next_monday = get_next_monday(next_monday);
                    }
                    o.push(CalTempsDateRange {
                        start: Some(prev_start),
                        end: Some(dre),
                    });
                    o
                }
            },

            _ => vec![],
        },
        _ => vec![],
    }
}

pub fn report_summary(
    active_filter: String,
    date_range: &CalTempsDateRange,
    report_type: EnumReportTypes,
    output_type: EnumOutputTypes,
    entries: Vec<CalTempsEntry>,
) {
    let total = entries
        .clone()
        .into_iter()
        .filter_map(|e| e.duration_minutes())
        .sum::<i64>() as f64
        / 60.0;
    let last_date = entries
        .clone()
        .into_iter()
        .filter_map(|e| match date_range.end {
            Some(_) => min(date_range.end, e.end),
            _ => e.end,
        })
        .max();
    let covered_range = CalTempsDateRange {
        start: date_range.start,
        end: last_date,
    };

    let date_slots = split_date_range(&covered_range, report_type);

    let data_header = [active_filter]
        .into_iter()
        .chain(
            date_slots
                .clone()
                .into_iter()
                .map(|dsl| format_datetime(dsl.start, report_type)),
        )
        .chain(["Hours".into()]);
    let data_footer = [["TOTAL".to_string()]
        .into_iter()
        .chain(date_slots.clone().into_iter().map(|dsl| {
            format!(
                "{:.2}",
                entries
                    .clone()
                    .into_iter()
                    .filter_map(|e| e.date_range_intersection_minutes(&dsl.clone()))
                    .sum::<i64>() as f64
                    / 60.0
            )
        }))
        .chain([format!("{:.2}", total)])
        .collect::<Vec<_>>()];
    let mut es = entries.clone();
    es.sort_by_cached_key(|e| e.full_tags().join(" ").to_lowercase());
    let data_body = es
        .into_iter()
        .chunk_by(|e| e.full_tags().join(" "))
        .into_iter()
        .map(|c| {
            let matched_entries = c.1.collect::<Vec<_>>();
            let matched_total_hours = matched_entries
                .clone()
                .into_iter()
                .filter_map(|e| e.duration_minutes())
                .sum::<i64>() as f64
                / 60.0;
            [c.0]
                .into_iter()
                .chain(date_slots.clone().into_iter().map(move |dsl| {
                    let dsl_mins = matched_entries
                        .clone()
                        .into_iter()
                        .filter_map(|e| e.date_range_intersection_minutes(&dsl.clone()))
                        .sum::<i64>() as f64
                        / 60.0;
                    if dsl_mins > 0.0 {
                        format!("{:.2}", dsl_mins)
                    } else {
                        "".into()
                    }
                }))
                .chain([format!("{:.2}", matched_total_hours)])
        })
        .collect::<Vec<_>>();
    match output_type {
        EnumOutputTypes::CLI => {
            // Format header row
            let mut header_iter = data_header.into_iter();
            let header_first = header_iter.next().unwrap();
            let header_last = header_iter.next_back().unwrap();
            let table_header = [header_first.cell().bold(true)]
                .into_iter()
                .chain(header_iter.map(|c| c.cell().bold(true).justify(Justify::Right)))
                .chain([header_last.cell().bold(true).justify(Justify::Right)]);
            // Format data rows
            let table_body = data_body.into_iter().map(|r| {
                let mut r_iter = r.into_iter();
                let r_first = r_iter.next().unwrap();
                [r_first.cell()]
                    .into_iter()
                    .chain(r_iter.map(|c| c.cell().justify(Justify::Right)))
                    .collect::<Vec<_>>()
            });
            // Format footer rows
            let table_footer = data_footer.into_iter().map(|r| {
                let mut r_iter = r.into_iter();
                let r_first = r_iter.next().unwrap();
                [r_first.cell().bold(true)]
                    .into_iter()
                    .chain(r_iter.map(|c| c.cell().bold(true).justify(Justify::Right)))
                    .collect::<Vec<_>>()
            });
            let table = table_body
                .chain(table_footer)
                .table()
                .title(table_header)
                .bold(true);
            assert!(print_stdout(table).is_ok())
        }
        EnumOutputTypes::CSV => {
            let mut wtr = csv::Writer::from_writer(io::stdout());
            let _ = wtr.write_record(data_header);
            let _ = data_body
                .into_iter()
                .map(|r| wtr.write_record(r))
                .collect::<Vec<_>>();
            let _ = data_footer
                .into_iter()
                .map(|r| wtr.write_record(r))
                .collect::<Vec<_>>();
            assert!(wtr.flush().is_ok());
        }
    }
}