timelog/day/
report.rs

1//! Support for Day reports
2//!
3//! # Examples
4//!
5//! ```rust, no_run
6//! use timelog::{Day, Entry, Result};
7//!
8//! # fn main() -> Result<()> {
9//! # let entries: Vec<Entry> = vec![];
10//! # let mut entry_iter = entries.into_iter();
11//! let mut day = Day::new("2021-07-02")?;
12//! while let Some(entry) = entry_iter.next() {
13//!     day.add_entry(entry);
14//! }
15//! day.finish()?;
16//! print!("{}", day.detail_report());
17//! #   Ok(())
18//! #  }
19//! ```
20//!
21//! # Description
22
23use 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
39/// Representation of the full report about a [`Day`].
40pub 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    /// Format the [`Day`] information.
48    ///
49    /// The output starts with the current datestamp and duration for the day. Indented
50    /// under that are individual projects. Individual tasks are indented under the
51    /// projects.
52    ///
53    /// This is the most detailed report.
54    ///
55    /// # Errors
56    ///
57    /// Could return any formatting error
58    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/// Representation of the summary report about a [`Day`].
87#[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    /// Format the [`Day`] information.
96    ///
97    /// The output starts with the current datestamp and duration for the day. Indented
98    /// under that are individual projects.
99    ///
100    /// # Errors
101    ///
102    /// Could return any formatting error
103    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            // The None case should not be possible, ignore it if it happens.
113            if let Some(dur) = proj_dur.get(proj) {
114                day._format_project_line(f, proj, dur)?;
115            }
116        }
117        Ok(())
118    }
119}
120
121/// Representation of the hours report about a [`Day`].
122#[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    /// Format the [`Day`] information.
131    ///
132    /// The output only displays the current datestamp and duration for the day.
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0._format_stamp_line(f, ":") }
134}
135
136/// Representation of the event report about a [`Day`].
137#[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    /// Format the [`Day`] information.
149    ///
150    /// The output only displays the zero duration events associated with a [`Day`].
151    ///
152    /// # Errors
153    ///
154    /// Could return any formatting error
155    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/// Representation of chart report about a [`Day`].
178#[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    /// Return a [`Vec`] of tuples mapping project name to percentage of the overall
187    /// time this project took.
188    pub fn project_percentages(&self) -> Percentages { self.0.project_percentages() }
189
190    /// Return a [`Vec`] of tuples mapping the task name and percentage of the
191    /// supplied project.
192    pub fn task_percentages(&self, proj: &str) -> Percentages { self.0.task_percentages(proj) }
193
194    /// Write a pie chart representing the projects for the current day to the supplied
195    /// [`EventWriter`].
196    ///
197    /// # Errors
198    ///
199    /// Could return any formatting error
200    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    /// Write a bar-graph representation of the tasks by hour in the current day to the supplied
215    /// [`EventWriter`].
216    ///
217    /// # Errors
218    ///
219    /// Could return any formatting error
220    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    /// Write a pie chart representing the tasks for the supplied project to the supplied
235    /// [`EventWriter`].
236    ///
237    /// # Errors
238    ///
239    /// Could return any formatting error
240    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    /// Write the charts for the current day to the supplied [`EventWriter`].
259    ///
260    /// # Errors
261    ///
262    /// Could return any formatting error
263    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(&regex);
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}