1use std::cmp::Ordering;
24use std::fmt::{self, Display};
25use std::io::Write;
26use std::time::Duration;
27
28use xml::writer::{EventWriter, XmlEvent};
29
30#[doc(inline)]
31#[rustfmt::skip]
32use crate::chart::{
33 BarGraph, ColorIter, DayHours, Legend, Percent, Percentages, PieChart
34};
35use crate::emit_xml;
36use crate::Day;
37use crate::TaskEvent;
38
39pub struct DetailReport<'a>(&'a Day);
41
42impl<'a> DetailReport<'a> {
43 pub fn new(day: &'a Day) -> Self { Self(day) }
44}
45
46impl<'a> Display for DetailReport<'a> {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 let day = self.0;
60 let mut last_proj = String::new();
61 writeln!(f)?;
62 day._format_stamp_line(f, "")?;
63
64 let mut tasks: Vec<(String, &str, &TaskEvent)> = day
65 .tasks
66 .iter()
67 .map(|(t, tsk)| (tsk.project(), t.as_str(), tsk))
68 .collect();
69 tasks.sort();
70
71 for (cur_proj, tname, task) in tasks {
72 if cur_proj != last_proj {
73 day._format_project_line(
74 f,
75 &cur_proj,
76 day.proj_dur.get(&cur_proj).unwrap_or(&Duration::default())
77 )?;
78 last_proj = cur_proj;
79 }
80 day._format_task_line(f, tname, &task.duration())?;
81 }
82 Ok(())
83 }
84}
85
86#[must_use]
88pub struct SummaryReport<'a>(&'a Day);
89
90impl<'a> SummaryReport<'a> {
91 pub fn new(day: &'a Day) -> Self { Self(day) }
92}
93
94impl<'a> Display for SummaryReport<'a> {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 let day = self.0;
105
106 let proj_dur = &day.proj_dur;
107 let mut keys: Vec<&str> = proj_dur.keys().map(String::as_str).collect();
108 keys.sort();
109
110 day._format_stamp_line(f, "")?;
111 for proj in keys {
112 if let Some(dur) = proj_dur.get(proj) {
114 day._format_project_line(f, proj, dur)?;
115 }
116 }
117 Ok(())
118 }
119}
120
121#[must_use]
123pub struct HoursReport<'a>(&'a Day);
124
125impl<'a> HoursReport<'a> {
126 pub fn new(day: &'a Day) -> Self { Self(day) }
127}
128
129impl<'a> Display for HoursReport<'a> {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0._format_stamp_line(f, ":") }
134}
135
136#[must_use]
138pub struct EventReport<'a> {
139 day: &'a Day,
140 compact: bool
141}
142
143impl<'a> EventReport<'a> {
144 pub fn new(day: &'a Day, compact: bool) -> Self { Self { day, compact } }
145}
146
147impl<'a> Display for EventReport<'a> {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 let day = self.day;
157 if !day.has_events() {
158 return Ok(());
159 }
160 if self.compact {
161 for ev in day.events() {
162 #[rustfmt::skip]
163 writeln!(f, "{} {} {}", ev.date(), ev.date_time().hhmm(), ev.entry_text())?;
164 }
165 }
166 else {
167 writeln!(f, "{}", day.date_stamp())?;
168 for ev in day.events() {
169 #[rustfmt::skip]
170 writeln!(f, " {} {}", ev.date_time().hhmm(), ev.entry_text())?;
171 }
172 }
173 Ok(())
174 }
175}
176
177#[must_use]
179pub struct DailyChart<'a>(&'a Day);
180
181impl<'a> DailyChart<'a> {
182 pub fn new(day: &'a Day) -> Self { Self(day) }
183}
184
185impl<'a> DailyChart<'a> {
186 pub fn project_percentages(&self) -> Percentages { self.0.project_percentages() }
189
190 pub fn task_percentages(&self, proj: &str) -> Percentages { self.0.task_percentages(proj) }
193
194 pub fn project_pie<W: Write>(&self, w: &mut EventWriter<W>) -> crate::Result<()> {
201 const R: f32 = 100.0;
202 let legend = Legend::new(14.0, ColorIter::default());
203 let pie = PieChart::new(R, legend);
204
205 let mut percents = self.project_percentages();
206 percents.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
207 emit_xml!(w, div, class: "project" => {
208 emit_xml!(w, h2; &format!("{} ({}) Projects", self.0.date_stamp(), self.0.date().weekday()))?;
209 pie.write_pie(w, &percents)?;
210 self.project_hours(w)
211 })
212 }
213
214 pub fn project_hours<W: Write>(&self, w: &mut EventWriter<W>) -> crate::Result<()> {
221 emit_xml!(w, div, class: "hours" => {
222 emit_xml!(w, h3; "Hourly")?;
223 emit_xml!(w, div, class: "hist" => {
224 let mut day_hours = DayHours::default();
225 for entry in self.0.entries() {
226 day_hours.add(entry.clone());
227 }
228 let bar_graph = BarGraph::new(&self.project_percentages());
229 bar_graph.write(w, &day_hours)
230 })
231 })
232 }
233
234 pub fn task_pie<W: Write>(
241 &self, w: &mut EventWriter<W>, proj: &str, percent: &Percent
242 ) -> crate::Result<()> {
243 const R: f32 = 60.0;
244 let legend = Legend::new(12.0, ColorIter::default());
245 let pie = PieChart::new(R, legend);
246
247 let mut percents = self.task_percentages(proj);
248 percents.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
249 emit_xml!(w, div => {
250 emit_xml!(w, h3 => {
251 emit_xml!(w; "Tasks for ")?;
252 emit_xml!(w, em; &format!("{proj} ({percent})"))
253 })?;
254 pie.write_pie(w, &percents)
255 })
256 }
257
258 pub fn write<W: Write>(&self, w: &mut EventWriter<W>) -> crate::Result<()> {
264 emit_xml!(w, div, class: "day" => {
265 self.project_pie(w)?;
266 emit_xml!(w, div, class: "tasks" => {
267 let mut percentages = self.project_percentages();
268 percentages.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
269 for percent in percentages {
270 self.task_pie(w, percent.label(), percent.percent())?;
271 }
272 Ok(())
273 })
274 })
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use regex::Regex;
281 use spectral::prelude::*;
282
283 use super::*;
284 use crate::chart::TagPercent;
285 use crate::date::DateTime;
286 use crate::day::tests::{add_entries, add_extra_entries, add_some_events};
287 use crate::day::Day;
288 use crate::entry::Entry;
289
290 #[test]
291 fn test_detail_report_empty() {
292 let day_result = Day::new("2021-06-10");
293 assert_that!(&day_result).is_ok();
294
295 let day = day_result.unwrap();
296 let expect = String::from("\n2021-06-10 0:00\n");
297 assert_that!(format!("{}", day.detail_report())).is_equal_to(expect);
298 }
299
300 #[test]
301 fn test_summary_report_empty() {
302 let day_result = Day::new("2021-06-10");
303 assert_that!(&day_result).is_ok();
304
305 let day = day_result.unwrap();
306 let detail = format!("{}", day.summary_report());
307 let expected = String::from("2021-06-10 0:00\n");
308 assert_that!(detail).is_equal_to(expected);
309 }
310
311 #[test]
312 fn test_hours_report_empty() {
313 let day_result = Day::new("2021-06-10");
314 assert_that!(&day_result).is_ok();
315
316 let day = day_result.unwrap();
317 let expect = String::from("2021-06-10: 0:00\n");
318 assert_that!(format!("{}", day.hours_report())).is_equal_to(expect);
319 }
320
321 #[test]
322 fn test_detail_report_events_only() {
323 let day_result = Day::new("2021-06-10");
324 assert_that!(&day_result).is_ok();
325
326 let mut day = day_result.unwrap();
327 let _ = add_some_events(&mut day);
328
329 let expect = String::from("\n2021-06-10 0:00\n");
330 assert_that!(format!("{}", day.detail_report())).is_equal_to(expect);
331 }
332
333 #[test]
334 fn test_summary_report_events_only() {
335 let day_result = Day::new("2021-06-10");
336 assert_that!(&day_result).is_ok();
337
338 let mut day = day_result.unwrap();
339 let _ = add_some_events(&mut day);
340
341 let expect = String::from("2021-06-10 0:00\n");
342 assert_that!(format!("{}", day.summary_report())).is_equal_to(expect);
343 }
344
345 #[test]
346 fn test_hours_report_events_only() {
347 let day_result = Day::new("2021-06-10");
348 assert_that!(&day_result).is_ok();
349
350 let mut day = day_result.unwrap();
351 let _ = add_some_events(&mut day);
352
353 let expect = String::from("2021-06-10: 0:00\n");
354 assert_that!(format!("{}", day.hours_report())).is_equal_to(expect);
355 }
356
357 #[test]
358 fn test_detail_report_with_one() {
359 let day_result = Day::new("2021-06-10");
360 assert_that!(&day_result).is_ok();
361
362 let mut day = day_result.unwrap();
363 let entry = Entry::from_line("2021-06-10 08:00:00 +foo @task").unwrap();
364 let stamp = entry.date_time();
365 let _ = day.add_entry(entry);
366 let _ = day.update_dur(&(stamp + DateTime::minutes(45)).unwrap());
367 let expect = String::from(
368 "\n2021-06-10 0:45\n foo 0:45\n task 0:45\n"
369 );
370 assert_that!(format!("{}", day.detail_report())).is_equal_to(expect);
371 }
372
373 #[test]
374 fn test_detail_report_tasks() {
375 let day_result = Day::new("2021-06-10");
376 assert_that!(&day_result).is_ok();
377
378 let mut day = day_result.unwrap();
379 add_entries(&mut day).expect("Entries out of order");
380
381 let lines = [
382 "\n",
383 "2021-06-10 0:06\n",
384 " proj1 0:05\n",
385 " Final 0:01\n",
386 " Make 0:02 (changes)\n",
387 " Stuff 0:02 (Other changes)\n",
388 " proj2 0:01\n",
389 " Start 0:01 (work)\n"
390 ];
391 let expect = lines.iter().fold(String::new(), |mut acc, s| {
392 acc.push_str(s);
393 acc
394 });
395 assert_that!(format!("{}", day.detail_report())).is_equal_to(expect);
396 }
397
398 #[test]
399 fn test_summary_report_tasks() {
400 let day_result = Day::new("2021-06-10");
401 assert_that!(&day_result).is_ok();
402
403 let mut day = day_result.unwrap();
404 add_entries(&mut day).expect("Entries out of order");
405
406 let lines = [
407 "2021-06-10 0:06\n",
408 " proj1 0:05\n",
409 " proj2 0:01\n"
410 ];
411
412 let expected = lines.iter().fold(String::new(), |mut acc, s| {
413 acc.push_str(s);
414 acc
415 });
416 assert_that!(format!("{}", day.summary_report())).is_equal_to(expected);
417 }
418
419 #[test]
420 fn test_events_report() {
421 let day_result = Day::new("2021-06-10");
422 assert_that!(&day_result).is_ok();
423
424 let mut day = day_result.unwrap();
425 let _ = add_entries(&mut day);
426 let _ = add_some_events(&mut day);
427
428 let expect = String::from("2021-06-10\n 08:30 +foo thing1\n 08:35 +foo thing2\n");
429 assert_that!(format!("{}", day.event_report(false))).is_equal_to(expect);
430 }
431
432 #[test]
433 fn test_compact_events_report() {
434 let day_result = Day::new("2021-06-10");
435 assert_that!(&day_result).is_ok();
436
437 let mut day = day_result.unwrap();
438 let _ = add_entries(&mut day);
439 let _ = add_some_events(&mut day);
440
441 let expect = String::from("2021-06-10 08:30 +foo thing1\n2021-06-10 08:35 +foo thing2\n");
442 assert_that!(format!("{}", day.event_report(true))).is_equal_to(expect);
443 }
444
445 #[test]
446 fn test_project_percentages() {
447 let day_result = Day::new("2021-06-10");
448 assert_that!(&day_result).is_ok();
449
450 let mut day = day_result.unwrap();
451 add_extra_entries(&mut day).expect("Entries out of order");
452
453 let expect: Percentages = vec![
454 TagPercent::new("proj1", 50.0).unwrap(),
455 TagPercent::new("", 20.0).unwrap(),
456 TagPercent::new("proj2", 10.0).unwrap(),
457 TagPercent::new("proj3", 10.0).unwrap(),
458 TagPercent::new("proj4", 10.0).unwrap(),
459 ];
460 let mut actual = day.daily_chart().project_percentages();
461 actual.sort_by(|lhs, rhs| lhs.partial_cmp(rhs).unwrap());
462 assert_that!(actual).is_equal_to(expect);
463 }
464
465 #[test]
466 fn test_hours_report_tasks() {
467 let day_result = Day::new("2021-06-10");
468 assert_that!(&day_result).is_ok();
469
470 let mut day = day_result.unwrap();
471 add_entries(&mut day).expect("Entries out of order");
472 let expect = String::from("2021-06-10: 0:06\n");
473 assert_that!(format!("{}", day.hours_report())).is_equal_to(expect);
474 }
475
476 #[test]
477 fn test_project_filter_regex() {
478 let day_result = Day::new("2021-06-10");
479 assert_that!(&day_result).is_ok();
480
481 let mut day = day_result.unwrap();
482 add_entries(&mut day).expect("Entries out of order");
483 let regex = Regex::new(r"^\w+1$").expect("Invalid project regex");
484 let day2 = day.filtered_by_project(®ex);
485
486 assert_that!(day2.proj_dur.len()).is_equal_to(&1);
487 assert_that!(day2.proj_dur.contains_key("proj1")).is_true();
488
489 let expected = String::from("2021-06-10 0:05\n proj1 0:05\n");
490 assert_that!(format!("{}", day2.summary_report())).is_equal_to(expected);
491 }
492
493 #[test]
494 fn test_day_crossing() {
495 let mut day = Day::new("2021-06-10").unwrap();
496
497 let line = "2021-06-10 23:20:00 +project Task";
498 day.add_entry(Entry::from_line(line).expect("Entry failed to parse"))
499 .expect("Failed add");
500 day.finish().expect("Unable to close day");
501 let expected = r#"
5022021-06-10 0:40
503 project 0:40
504 Task 0:40
505"#;
506 let actual = format!("{}", day.detail_report());
507 assert_that!(actual.as_str()).is_equal_to(expected);
508 }
509}