caltemps 0.3.1

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;

fn reset_day(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)
}
pub fn advance_years(date: DateTime<Local>, count: u32) -> DateTime<Local> {
    reset_day(date)
        .with_day(1)
        .unwrap_or(date)
        .with_month(1)
        .unwrap_or(date)
        .checked_add_months(Months::new(count * 12))
        .unwrap_or(date)
}
pub fn advance_quarters(date: DateTime<Local>, count: u32) -> DateTime<Local> {
    advance_months(date, 3 * count - (date.month0() % 3))
}
pub fn advance_months(date: DateTime<Local>, count: u32) -> DateTime<Local> {
    reset_day(date)
        .with_day(1)
        .unwrap_or(date)
        .checked_add_months(Months::new(count))
        .unwrap_or(date)
}
pub fn advance_mondays(date: DateTime<Local>, count: u32) -> 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(
            (count * 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::Yearly => format!("{:0>4}", dt.year()),
            EnumReportTypes::Quarterly | EnumReportTypes::Monthly => {
                format!("{:0>4}-{:0>2}", dt.year(), dt.month())
            }
            EnumReportTypes::Fortnightly | 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![],
                EnumReportTypes::Yearly
                | EnumReportTypes::Quarterly
                | EnumReportTypes::Monthly
                | EnumReportTypes::Fortnightly
                | EnumReportTypes::Weekly => {
                    let mut o = Vec::<CalTempsDateRange>::new();
                    let mut prev_start = drs;
                    let get_next_day = match report_type {
                        EnumReportTypes::Totals => todo!(), // This shouldn't be hit
                        EnumReportTypes::Yearly => |d| advance_years(d, 1),
                        EnumReportTypes::Quarterly => |d| advance_quarters(d, 1),
                        EnumReportTypes::Monthly => |d| advance_months(d, 1),
                        EnumReportTypes::Fortnightly => |d| advance_mondays(d, 2),
                        EnumReportTypes::Weekly => |d| advance_mondays(d, 1),
                    };
                    let mut next_day = get_next_day(drs);
                    while next_day < dre {
                        // Add piece
                        o.push(CalTempsDateRange {
                            start: Some(prev_start),
                            end: Some(next_day),
                        });
                        // Advance period
                        prev_start = next_day;
                        next_day = get_next_day(next_day);
                    }
                    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());
        }
    }
}