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 {
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!(), 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 {
o.push(CalTempsDateRange {
start: Some(prev_start),
end: Some(next_day),
});
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 => {
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)]);
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<_>>()
});
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());
}
}
}