tiempo-rs/src/formatters.rs

313 lines
12 KiB
Rust
Raw Normal View History

2021-06-21 17:38:51 -05:00
use std::str::FromStr;
use std::io::Write;
2021-06-18 11:27:19 -05:00
use serde::{Serialize, Deserialize};
use itertools::Itertools;
use chrono::{
DateTime, Utc, Offset, TimeZone, Duration, NaiveTime, Timelike,
NaiveDateTime,
};
2021-06-18 11:27:19 -05:00
2021-06-21 17:38:51 -05:00
use crate::error;
use crate::models::Entry;
2021-06-24 00:28:10 -05:00
fn format_duration(dur: Duration) -> String {
format!("{}:{:02}:{:02}", dur.num_hours(), dur.num_minutes() % 60, dur.num_seconds() % 60)
}
fn format_start(t: NaiveTime) -> String {
format!("{:02}:{:02}:{:02} -", t.hour(), t.minute(), t.second())
}
fn lpad(s: &str, len: usize) -> String {
let padding = " ".repeat(len.saturating_sub(s.len()));
padding + s
}
fn rpad(s: &str, len: usize) -> String {
let padding = " ".repeat(len.saturating_sub(s.len()));
s.to_string() + &padding
}
fn format_end(start: NaiveDateTime, end: NaiveDateTime) -> String {
let extra_days = (end - start).num_days();
let d = if extra_days > 0 { format!("+{}d", extra_days) } else { "".into() };
format!(
"{:02}:{:02}:{:02}{}",
end.hour(),
end.minute() % 60,
end.second() % 60,
d
)
2021-06-24 09:02:28 -05:00
}
2021-06-18 11:27:19 -05:00
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Formatter {
Text,
2021-06-21 17:38:51 -05:00
Custom(String),
}
impl Formatter {
/// Prints the given entries to the specified output device.
///
/// the current time is given as the `now` argument and the offset from UTC
/// to the local timezone is given in `offset` to prevent this function from
/// using a secondary effect to retrieve the time and conver dates. This
/// also makes it easier to test.
2021-06-25 14:05:27 -05:00
pub fn print_formatted<W: Write, O: Offset>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, offset: O, ids: bool) -> error::Result<()> {
2021-06-21 17:38:51 -05:00
match &self {
2021-06-25 14:05:27 -05:00
Formatter::Text => self.print_formatted_text(entries, out, now, offset, ids)?,
2021-06-21 17:38:51 -05:00
Formatter::Custom(name) => {
}
}
Ok(())
}
/// Print in the default text format. Assume entries are sorted by sheet and
/// then by start
2021-06-25 14:05:27 -05:00
fn print_formatted_text<W: Write, O: Offset>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, offset: O, ids: bool) -> error::Result<()> {
let grouped_entries = entries.into_iter().group_by(|e| e.sheet.to_string());
// Build a timezone based on the offset given to this function. This
// will later be used to properly group the entries by date in the local
// timezone.
let fixed_offset = offset.fix();
for (key, group) in grouped_entries.into_iter() {
writeln!(out, "Timesheet: {}", key)?;
// End Duration Notes")?;
// A vector of lines to be printed, with all the components
let mut lines = Vec::new();
2021-06-24 00:28:10 -05:00
let entries_by_date = group.group_by(|e| fixed_offset.from_utc_datetime(&e.start.naive_utc()).date());
let mut total = Duration::seconds(0);
for (date, entries) in entries_by_date.into_iter() {
let mut daily = Duration::seconds(0);
2021-06-25 14:05:27 -05:00
for (i, entry) in entries.into_iter().enumerate() {
let start = format_start(fixed_offset.from_utc_datetime(&entry.start.naive_utc()).time());
let end = entry.end.map(|t| {
format_end(
fixed_offset.from_utc_datetime(&entry.start.naive_utc()).naive_local(),
fixed_offset.from_utc_datetime(&t.naive_utc()).naive_local()
)
}).unwrap_or(" ".into());
let duration = entry.end.unwrap_or(now) - entry.start;
2021-06-24 00:28:10 -05:00
daily = daily + duration;
let duration = format_duration(duration);
2021-06-25 14:05:27 -05:00
let id = if ids { entry.id.to_string() } else { "".into() };
if i == 0 {
let date = date.format("%a %b %d, %Y").to_string();
2021-06-25 14:05:27 -05:00
lines.push([id, date, start, end, duration, entry.note]);
} else {
2021-06-25 14:05:27 -05:00
lines.push([id, "".into(), start, end, duration, entry.note]);
}
}
2021-06-24 00:28:10 -05:00
total = total + daily;
lines.push(["".into(), "".into(), "".into(), "".into(), format_duration(daily), "".into()]);
}
// compute some column widths before printing
// When array_map is stabilized this can be shortened
let lengths = lines
.iter()
.map(|[i, d, s, e, du, n]| [i.len(), d.len(), s.len(), e.len(), du.len(), n.len()])
.reduce(|[a, b, c, d, e, f], [g, h, i, j, k, l]| {
[a.max(g), b.max(h), c.max(i), d.max(j), e.max(k), f.max(l)]
}).unwrap();
writeln!(out,
"{} {} {} {} {} {}",
2021-06-25 14:05:27 -05:00
if ids { lpad("ID", 3.max(lengths[0])) } else { lpad(" ", 3.max(lengths[0])) },
rpad("Day", 18),
rpad("Start", 10),
2021-06-25 13:56:48 -05:00
rpad("End", 10.max(lengths[3])),
rpad("Duration", 8.max(lengths[4])),
"Notes",
)?;
for [id, date, start, end, duration, note] in lines {
writeln!(
out,
"{} {} {} {} {} {}",
lpad(&id, 3.max(lengths[0])),
rpad(&date, 18),
rpad(&start, 10),
2021-06-25 13:56:48 -05:00
rpad(&end, 10.max(lengths[3])),
lpad(&duration, 8.max(lengths[4])),
note,
)?;
}
2021-06-24 00:28:10 -05:00
writeln!(out,
"{} {}-{}-{}-{}-{}",
lpad(" ", 3.max(lengths[0])),
"-".repeat(18),
"-".repeat(10),
2021-06-25 13:56:48 -05:00
"-".repeat(10.max(lengths[3])),
"-".repeat(8.max(lengths[4])),
"-".repeat(4.max(lengths[5])),
)?;
writeln!(out,
"{} {} {} {} {}",
lpad(" ", 3.max(lengths[0])),
rpad("Total", 18),
rpad("", 10),
2021-06-25 13:56:48 -05:00
rpad("", 10.max(lengths[3])),
lpad(&format_duration(total), 8.max(lengths[4])),
)?;
}
Ok(())
}
2021-06-21 17:38:51 -05:00
}
impl FromStr for Formatter {
type Err = error::Error;
fn from_str(s: &str) -> error::Result<Formatter> {
let lower = s.to_lowercase();
Ok(match &*lower {
"text" => Formatter::Text,
custom_format => Formatter::Custom(custom_format.into()),
})
}
2021-06-18 11:27:19 -05:00
}
#[cfg(test)]
mod tests {
use super::*;
2021-06-22 10:18:16 -05:00
use std::fmt;
use pretty_assertions::assert_eq;
use chrono::TimeZone;
2021-06-22 10:18:16 -05:00
#[derive(PartialEq, Eq)]
pub struct PrettyString<'a>(pub &'a str);
/// Make diff to display string as multi-line string
impl<'a> fmt::Debug for PrettyString<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.0)
}
}
#[test]
fn test_text_output() {
let formatter = Formatter::Text;
let mut output = Vec::new();
let entries = vec![
Entry::new_sample(1, Utc.ymd(2008, 10, 3).and_hms(12, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(14, 0, 0))),
2021-06-24 00:28:10 -05:00
Entry::new_sample(2, Utc.ymd(2008, 10, 3).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(18, 0, 0))),
Entry::new_sample(3, Utc.ymd(2008, 10, 5).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0))),
Entry::new_sample(4, Utc.ymd(2008, 10, 5).and_hms(18, 0, 0), None),
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
2021-06-25 13:56:48 -05:00
formatter.print_formatted(entries, &mut output, now, offset, false).unwrap();
2021-06-22 10:18:16 -05:00
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
2021-06-25 13:56:48 -05:00
Day Start End Duration Notes
Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 1
16:00:00 - 18:00:00 2:00:00 entry 2
4:00:00
Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 entry 3
18:00:00 - 2:00:00 entry 4
4:00:00
---------------------------------------------------------
Total 8:00:00
2021-06-24 09:02:28 -05:00
"));
}
#[test]
fn test_text_output_with_millis() {
let formatter = Formatter::Text;
let mut output = Vec::new();
let entries = vec![
Entry::new_sample(1, Utc.ymd(2008, 10, 3).and_hms_milli(12, 0, 0, 432), Some(Utc.ymd(2008, 10, 3).and_hms_milli(14, 0, 0, 312))),
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
2021-06-25 13:56:48 -05:00
formatter.print_formatted(entries, &mut output, now, offset, false).unwrap();
2021-06-24 09:02:28 -05:00
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
2021-06-25 13:56:48 -05:00
Day Start End Duration Notes
Fri Oct 03, 2008 12:00:00 - 14:00:00 1:59:59 entry 1
1:59:59
---------------------------------------------------------
Total 1:59:59
"));
}
#[test]
fn test_text_output_long_duration() {
let formatter = Formatter::Text;
let mut output = Vec::new();
let entries = vec![
Entry::new_sample(1, Utc.ymd(2008, 10, 1).and_hms(12, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(14, 0, 0))),
Entry::new_sample(2, Utc.ymd(2008, 10, 3).and_hms(12, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(14, 0, 0))),
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
2021-06-25 13:56:48 -05:00
formatter.print_formatted(entries, &mut output, now, offset, false).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
Day Start End Duration Notes
Wed Oct 01, 2008 12:00:00 - 14:00:00+2d 50:00:00 entry 1
50:00:00
Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 2
2:00:00
----------------------------------------------------------
Total 52:00:00
2021-06-25 14:05:27 -05:00
"));
}
#[test]
fn test_text_output_with_ids() {
let formatter = Formatter::Text;
let mut output = Vec::new();
let entries = vec![
Entry::new_sample(1, Utc.ymd(2008, 10, 3).and_hms(12, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(14, 0, 0))),
Entry::new_sample(2, Utc.ymd(2008, 10, 3).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(18, 0, 0))),
Entry::new_sample(3, Utc.ymd(2008, 10, 5).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0))),
Entry::new_sample(4, Utc.ymd(2008, 10, 5).and_hms(18, 0, 0), None),
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, true).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
ID Day Start End Duration Notes
1 Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 1
2 16:00:00 - 18:00:00 2:00:00 entry 2
4:00:00
3 Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 entry 3
4 18:00:00 - 2:00:00 entry 4
4:00:00
---------------------------------------------------------
Total 8:00:00
2021-06-22 10:18:16 -05:00
"));
}
}