use std::cmp::Ordering;
use std::fmt::{self, Display};
use std::io::Write;
use std::time::Duration;
use xml::writer::{EventWriter, XmlEvent};
#[doc(inline)]
#[rustfmt::skip]
use crate::chart::{
BarGraph, ColorIter, DayHours, Legend, Percent, Percentages, PieChart
};
use crate::emit_xml;
use crate::Day;
use crate::TaskEvent;
pub struct DetailReport<'a>(&'a Day);
impl<'a> DetailReport<'a> {
pub fn new(day: &'a Day) -> Self { Self(day) }
}
impl<'a> Display for DetailReport<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let day = self.0;
let mut last_proj = String::new();
writeln!(f)?;
day._format_stamp_line(f, "")?;
let mut tasks: Vec<(String, &str, &TaskEvent)> = day
.tasks
.iter()
.map(|(t, tsk)| (tsk.project(), t.as_str(), tsk))
.collect();
tasks.sort();
for (cur_proj, tname, task) in tasks {
if cur_proj != last_proj {
day._format_project_line(
f,
&cur_proj,
day.proj_dur.get(&cur_proj).unwrap_or(&Duration::default())
)?;
last_proj = cur_proj;
}
day._format_task_line(f, tname, &task.duration())?;
}
Ok(())
}
}
#[must_use]
pub struct SummaryReport<'a>(&'a Day);
impl<'a> SummaryReport<'a> {
pub fn new(day: &'a Day) -> Self { Self(day) }
}
impl<'a> Display for SummaryReport<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let day = self.0;
let proj_dur = &day.proj_dur;
let mut keys: Vec<&str> = proj_dur.keys().map(String::as_str).collect();
keys.sort();
day._format_stamp_line(f, "")?;
for proj in keys {
if let Some(dur) = proj_dur.get(proj) {
day._format_project_line(f, proj, dur)?;
}
}
Ok(())
}
}
#[must_use]
pub struct HoursReport<'a>(&'a Day);
impl<'a> HoursReport<'a> {
pub fn new(day: &'a Day) -> Self { Self(day) }
}
impl<'a> Display for HoursReport<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0._format_stamp_line(f, ":") }
}
#[must_use]
pub struct EventReport<'a> {
day: &'a Day,
compact: bool
}
impl<'a> EventReport<'a> {
pub fn new(day: &'a Day, compact: bool) -> Self { Self { day, compact } }
}
impl<'a> Display for EventReport<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let day = self.day;
if !day.has_events() {
return Ok(());
}
if self.compact {
for ev in day.events() {
#[rustfmt::skip]
writeln!(f, "{} {} {}", ev.date(), ev.date_time().hhmm(), ev.entry_text())?;
}
}
else {
writeln!(f, "{}", day.date_stamp())?;
for ev in day.events() {
#[rustfmt::skip]
writeln!(f, " {} {}", ev.date_time().hhmm(), ev.entry_text())?;
}
}
Ok(())
}
}
#[must_use]
pub struct DailyChart<'a>(&'a Day);
impl<'a> DailyChart<'a> {
pub fn new(day: &'a Day) -> Self { Self(day) }
}
impl<'a> DailyChart<'a> {
pub fn project_percentages(&self) -> Percentages { self.0.project_percentages() }
pub fn task_percentages(&self, proj: &str) -> Percentages { self.0.task_percentages(proj) }
pub fn project_pie<W: Write>(&self, w: &mut EventWriter<W>) -> crate::Result<()> {
const R: f32 = 100.0;
let legend = Legend::new(14.0, ColorIter::default());
let pie = PieChart::new(R, legend);
let mut percents = self.project_percentages();
percents.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
emit_xml!(w, div, class: "project" => {
emit_xml!(w, h2; &format!("{} ({}) Projects", self.0.date_stamp(), self.0.date().weekday()))?;
pie.write_pie(w, &percents)?;
self.project_hours(w)
})
}
pub fn project_hours<W: Write>(&self, w: &mut EventWriter<W>) -> crate::Result<()> {
emit_xml!(w, div, class: "hours" => {
emit_xml!(w, h3; "Hourly")?;
emit_xml!(w, div, class: "hist" => {
let mut day_hours = DayHours::default();
for entry in self.0.entries() {
day_hours.add(entry.clone());
}
let bar_graph = BarGraph::new(&self.project_percentages());
bar_graph.write(w, &day_hours)
})
})
}
pub fn task_pie<W: Write>(
&self, w: &mut EventWriter<W>, proj: &str, percent: &Percent
) -> crate::Result<()> {
const R: f32 = 60.0;
let legend = Legend::new(12.0, ColorIter::default());
let pie = PieChart::new(R, legend);
let mut percents = self.task_percentages(proj);
percents.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
emit_xml!(w, div => {
emit_xml!(w, h3 => {
emit_xml!(w; "Tasks for ")?;
emit_xml!(w, em; &format!("{proj} ({percent})"))
})?;
pie.write_pie(w, &percents)
})
}
pub fn write<W: Write>(&self, w: &mut EventWriter<W>) -> crate::Result<()> {
emit_xml!(w, div, class: "day" => {
self.project_pie(w)?;
emit_xml!(w, div, class: "tasks" => {
let mut percentages = self.project_percentages();
percentages.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
for percent in percentages {
self.task_pie(w, percent.label(), percent.percent())?;
}
Ok(())
})
})
}
}
#[cfg(test)]
mod tests {
use assert2::{assert, let_assert};
use regex::Regex;
use super::*;
use crate::chart::TagPercent;
use crate::date::DateTime;
use crate::day::tests::{add_entries, add_extra_entries, add_some_events};
use crate::day::Day;
use crate::entry::Entry;
#[test]
fn test_detail_report_empty() {
let_assert!(Ok(day) = Day::new("2021-06-10"));
let expect = String::from("\n2021-06-10 0:00\n");
assert!(format!("{}", day.detail_report()) == expect);
}
#[test]
fn test_summary_report_empty() {
let_assert!(Ok(day) = Day::new("2021-06-10"));
let detail = format!("{}", day.summary_report());
let expected = String::from("2021-06-10 0:00\n");
assert!(detail == expected);
}
#[test]
fn test_hours_report_empty() {
let_assert!(Ok(day) = Day::new("2021-06-10"));
let expect = String::from("2021-06-10: 0:00\n");
assert!(format!("{}", day.hours_report()) == expect);
}
#[test]
fn test_detail_report_events_only() {
let_assert!(Ok(mut day) = Day::new("2021-06-10"));
let _ = add_some_events(&mut day);
let expect = String::from("\n2021-06-10 0:00\n");
assert!(format!("{}", day.detail_report()) == expect);
}
#[test]
fn test_summary_report_events_only() {
let_assert!(Ok(mut day) = Day::new("2021-06-10"));
let _ = add_some_events(&mut day);
let expect = String::from("2021-06-10 0:00\n");
assert!(format!("{}", day.summary_report()) == expect);
}
#[test]
fn test_hours_report_events_only() {
let_assert!(Ok(mut day) = Day::new("2021-06-10"));
let _ = add_some_events(&mut day);
let expect = String::from("2021-06-10: 0:00\n");
assert!(format!("{}", day.hours_report()) == expect);
}
#[test]
fn test_detail_report_with_one() {
let_assert!(Ok(mut day) = Day::new("2021-06-10"));
let_assert!(Ok(entry) = Entry::from_line("2021-06-10 08:00:00 +foo @task"));
let stamp = entry.date_time();
let _ = day.add_entry(entry);
let_assert!(Ok(dur) = stamp + DateTime::minutes(45));
let _ = day.update_dur(&dur);
let expect = String::from(
"\n2021-06-10 0:45\n foo 0:45\n task 0:45\n"
);
assert!(format!("{}", day.detail_report()) == expect);
}
#[test]
fn test_detail_report_tasks() {
let_assert!(Ok(mut day) = Day::new("2021-06-10"));
let_assert!(Ok(_) = add_entries(&mut day), "Entries out of order");
let lines = [
"\n",
"2021-06-10 0:06\n",
" proj1 0:05\n",
" Final 0:01\n",
" Make 0:02 (changes)\n",
" Stuff 0:02 (Other changes)\n",
" proj2 0:01\n",
" Start 0:01 (work)\n"
];
let expect = lines.iter().fold(String::new(), |mut acc, s| {
acc.push_str(s);
acc
});
assert!(format!("{}", day.detail_report()) == expect);
}
#[test]
fn test_summary_report_tasks() {
let_assert!(Ok(mut day) = Day::new("2021-06-10"));
let_assert!(Ok(_) = add_entries(&mut day), "Entries out of order");
let lines = [
"2021-06-10 0:06\n",
" proj1 0:05\n",
" proj2 0:01\n"
];
let expected = lines.iter().fold(String::new(), |mut acc, s| {
acc.push_str(s);
acc
});
assert!(format!("{}", day.summary_report()) == expected);
}
#[test]
fn test_events_report() {
let_assert!(Ok(mut day) = Day::new("2021-06-10"));
let _ = add_entries(&mut day);
let _ = add_some_events(&mut day);
let expect = String::from("2021-06-10\n 08:30 +foo thing1\n 08:35 +foo thing2\n");
assert!(format!("{}", day.event_report(false)) == expect);
}
#[test]
fn test_compact_events_report() {
let_assert!(Ok(mut day) = Day::new("2021-06-10"));
let _ = add_entries(&mut day);
let _ = add_some_events(&mut day);
let expect = String::from("2021-06-10 08:30 +foo thing1\n2021-06-10 08:35 +foo thing2\n");
assert!(format!("{}", day.event_report(true)) == expect);
}
#[test]
fn test_project_percentages() {
let_assert!(Ok(mut day) = Day::new("2021-06-10"));
let_assert!(Ok(_) = add_extra_entries(&mut day), "Entries out of order");
let expect: Percentages = vec![
TagPercent::new("proj1", 50.0).expect("Hardcoded value"),
TagPercent::new("", 20.0).expect("Hardcoded value"),
TagPercent::new("proj2", 10.0).expect("Hardcoded value"),
TagPercent::new("proj3", 10.0).expect("Hardcoded value"),
TagPercent::new("proj4", 10.0).expect("Hardcoded value"),
];
let mut actual = day.daily_chart().project_percentages();
actual.sort_by(|lhs, rhs| lhs.partial_cmp(rhs).expect("Hardcoded value"));
assert!(actual == expect);
}
#[test]
fn test_hours_report_tasks() {
let_assert!(Ok(mut day) = Day::new("2021-06-10"));
let_assert!(Ok(_) = add_entries(&mut day));
let expect = String::from("2021-06-10: 0:06\n");
assert!(format!("{}", day.hours_report()) == expect);
}
#[test]
fn test_project_filter_regex() {
let_assert!(Ok(mut day) = Day::new("2021-06-10"));
let_assert!(Ok(_) = add_entries(&mut day), "Entries out of order");
let_assert!(Ok(regex) = Regex::new(r"^\w+1$"));
let day2 = day.filtered_by_project(®ex);
assert!(day2.proj_dur.len() == 1);
assert!(day2.proj_dur.contains_key("proj1"));
let expected = String::from("2021-06-10 0:05\n proj1 0:05\n");
assert!(format!("{}", day2.summary_report()) == expected);
}
#[test]
fn test_day_crossing() {
let_assert!(Ok(mut day) = Day::new("2021-06-10"));
let line = "2021-06-10 23:20:00 +project Task";
let_assert!(Ok(entry) = Entry::from_line(line));
let_assert!(Ok(_) = day.add_entry(entry));
let_assert!(Ok(_) = day.finish());
let expected = r#"
2021-06-10 0:40
project 0:40
Task 0:40
"#;
let actual = format!("{}", day.detail_report());
assert!(actual.as_str() == expected);
}
}