1use super::cli::{EnumOutputTypes, EnumReportTypes};
2use super::{CalTempsDateRange, CalTempsEntry};
3use std::cmp::min;
4use std::io;
5
6use chrono::{DateTime, Datelike, Days, Local, Months, Timelike};
7use cli_table::{Cell, Style, Table, format::Justify, print_stdout};
8use itertools::Itertools;
9
10fn reset_day(date: DateTime<Local>) -> DateTime<Local> {
11 date.with_hour(0)
12 .unwrap_or(date)
13 .with_minute(0)
14 .unwrap_or(date)
15 .with_second(0)
16 .unwrap_or(date)
17 .with_nanosecond(0)
18 .unwrap_or(date)
19}
20pub fn advance_years(date: DateTime<Local>, count: u32) -> DateTime<Local> {
21 reset_day(date)
22 .with_day(1)
23 .unwrap_or(date)
24 .with_month(1)
25 .unwrap_or(date)
26 .checked_add_months(Months::new(count * 12))
27 .unwrap_or(date)
28}
29pub fn advance_quarters(date: DateTime<Local>, count: u32) -> DateTime<Local> {
30 advance_months(date, 3 * count - (date.month0() % 3))
31}
32pub fn advance_months(date: DateTime<Local>, count: u32) -> DateTime<Local> {
33 reset_day(date)
34 .with_day(1)
35 .unwrap_or(date)
36 .checked_add_months(Months::new(count))
37 .unwrap_or(date)
38}
39pub fn advance_mondays(date: DateTime<Local>, count: u32) -> DateTime<Local> {
40 date.with_hour(0)
41 .unwrap_or(date)
42 .with_minute(0)
43 .unwrap_or(date)
44 .with_second(0)
45 .unwrap_or(date)
46 .with_nanosecond(0)
47 .unwrap_or(date)
48 .checked_add_days(Days::new(
49 (count * 7 - date.weekday().num_days_from_monday()).into(),
50 ))
51 .unwrap_or(date)
52}
53
54pub fn format_datetime(datetime: Option<DateTime<Local>>, report_type: EnumReportTypes) -> String {
55 match datetime {
56 Some(dt) => match report_type {
57 EnumReportTypes::Totals => format!("{}", dt),
58 EnumReportTypes::Yearly => format!("{:0>4}", dt.year()),
59 EnumReportTypes::Quarterly | EnumReportTypes::Monthly => {
60 format!("{:0>4}-{:0>2}", dt.year(), dt.month())
61 }
62 EnumReportTypes::Fortnightly | EnumReportTypes::Weekly => {
63 format!("{:0>4}-W{:0>2}", dt.year(), dt.iso_week().week())
64 }
65 },
66 _ => "".into(),
67 }
68}
69
70pub fn split_date_range(
71 date_range: &CalTempsDateRange,
72 report_type: EnumReportTypes,
73) -> Vec<CalTempsDateRange> {
74 match date_range.end {
75 Some(dre) => match date_range.start {
76 Some(drs) => match report_type {
77 EnumReportTypes::Totals => vec![],
79 EnumReportTypes::Yearly
80 | EnumReportTypes::Quarterly
81 | EnumReportTypes::Monthly
82 | EnumReportTypes::Fortnightly
83 | EnumReportTypes::Weekly => {
84 let mut o = Vec::<CalTempsDateRange>::new();
85 let mut prev_start = drs;
86 let get_next_day = match report_type {
87 EnumReportTypes::Totals => todo!(), EnumReportTypes::Yearly => |d| advance_years(d, 1),
89 EnumReportTypes::Quarterly => |d| advance_quarters(d, 1),
90 EnumReportTypes::Monthly => |d| advance_months(d, 1),
91 EnumReportTypes::Fortnightly => |d| advance_mondays(d, 2),
92 EnumReportTypes::Weekly => |d| advance_mondays(d, 1),
93 };
94 let mut next_day = get_next_day(drs);
95 while next_day < dre {
96 o.push(CalTempsDateRange {
98 start: Some(prev_start),
99 end: Some(next_day),
100 });
101 prev_start = next_day;
103 next_day = get_next_day(next_day);
104 }
105 o.push(CalTempsDateRange {
106 start: Some(prev_start),
107 end: Some(dre),
108 });
109 o
110 }
111 },
112 _ => vec![],
113 },
114 _ => vec![],
115 }
116}
117
118pub fn report_summary(
119 active_filter: String,
120 date_range: &CalTempsDateRange,
121 report_type: EnumReportTypes,
122 output_type: EnumOutputTypes,
123 entries: Vec<CalTempsEntry>,
124) {
125 let total = entries
126 .clone()
127 .into_iter()
128 .filter_map(|e| e.duration_minutes())
129 .sum::<i64>() as f64
130 / 60.0;
131 let last_date = entries
132 .clone()
133 .into_iter()
134 .filter_map(|e| match date_range.end {
135 Some(_) => min(date_range.end, e.end),
136 _ => e.end,
137 })
138 .max();
139 let covered_range = CalTempsDateRange {
140 start: date_range.start,
141 end: last_date,
142 };
143
144 let date_slots = split_date_range(&covered_range, report_type);
145
146 let data_header = [active_filter]
147 .into_iter()
148 .chain(
149 date_slots
150 .clone()
151 .into_iter()
152 .map(|dsl| format_datetime(dsl.start, report_type)),
153 )
154 .chain(["Hours".into()]);
155 let data_footer = [["TOTAL".to_string()]
156 .into_iter()
157 .chain(date_slots.clone().into_iter().map(|dsl| {
158 format!(
159 "{:.2}",
160 entries
161 .clone()
162 .into_iter()
163 .filter_map(|e| e.date_range_intersection_minutes(&dsl.clone()))
164 .sum::<i64>() as f64
165 / 60.0
166 )
167 }))
168 .chain([format!("{:.2}", total)])
169 .collect::<Vec<_>>()];
170 let mut es = entries.clone();
171 es.sort_by_cached_key(|e| e.full_tags().join(" ").to_lowercase());
172 let data_body = es
173 .into_iter()
174 .chunk_by(|e| e.full_tags().join(" "))
175 .into_iter()
176 .map(|c| {
177 let matched_entries = c.1.collect::<Vec<_>>();
178 let matched_total_hours = matched_entries
179 .clone()
180 .into_iter()
181 .filter_map(|e| e.duration_minutes())
182 .sum::<i64>() as f64
183 / 60.0;
184 [c.0]
185 .into_iter()
186 .chain(date_slots.clone().into_iter().map(move |dsl| {
187 let dsl_mins = matched_entries
188 .clone()
189 .into_iter()
190 .filter_map(|e| e.date_range_intersection_minutes(&dsl.clone()))
191 .sum::<i64>() as f64
192 / 60.0;
193 if dsl_mins > 0.0 {
194 format!("{:.2}", dsl_mins)
195 } else {
196 "".into()
197 }
198 }))
199 .chain([format!("{:.2}", matched_total_hours)])
200 })
201 .collect::<Vec<_>>();
202 match output_type {
203 EnumOutputTypes::Cli => {
204 let mut header_iter = data_header.into_iter();
206 let header_first = header_iter.next().unwrap();
207 let header_last = header_iter.next_back().unwrap();
208 let table_header = [header_first.cell().bold(true)]
209 .into_iter()
210 .chain(header_iter.map(|c| c.cell().bold(true).justify(Justify::Right)))
211 .chain([header_last.cell().bold(true).justify(Justify::Right)]);
212 let table_body = data_body.into_iter().map(|r| {
214 let mut r_iter = r.into_iter();
215 let r_first = r_iter.next().unwrap();
216 [r_first.cell()]
217 .into_iter()
218 .chain(r_iter.map(|c| c.cell().justify(Justify::Right)))
219 .collect::<Vec<_>>()
220 });
221 let table_footer = data_footer.into_iter().map(|r| {
223 let mut r_iter = r.into_iter();
224 let r_first = r_iter.next().unwrap();
225 [r_first.cell().bold(true)]
226 .into_iter()
227 .chain(r_iter.map(|c| c.cell().bold(true).justify(Justify::Right)))
228 .collect::<Vec<_>>()
229 });
230 let table = table_body
231 .chain(table_footer)
232 .table()
233 .title(table_header)
234 .bold(true);
235 assert!(print_stdout(table).is_ok())
236 }
237 EnumOutputTypes::Csv => {
238 let mut wtr = csv::Writer::from_writer(io::stdout());
239 let _ = wtr.write_record(data_header);
240 let _ = data_body
241 .into_iter()
242 .map(|r| wtr.write_record(r))
243 .collect::<Vec<_>>();
244 let _ = data_footer
245 .into_iter()
246 .map(|r| wtr.write_record(r))
247 .collect::<Vec<_>>();
248 assert!(wtr.flush().is_ok());
249 }
250 }
251}