tiempo-rs/src/formatters/text.rs

339 lines
14 KiB
Rust
Raw Normal View History

use std::io::Write;
use itertools::Itertools;
use chrono::{
DateTime, Utc, TimeZone, Duration, Local, NaiveTime, Timelike,
NaiveDateTime,
};
use crate::models::Entry;
use crate::error::Result;
2021-07-15 00:22:38 -05:00
use crate::tabulate::{Tabulate, Col, Align::*};
2021-07-15 00:22:38 -05:00
pub 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 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
)
}
/// Print in the default text format. Assume entries are sorted by sheet and
/// then by start
pub fn print_formatted<W: Write>(entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, ids: bool, term_width: usize) -> Result<()> {
let grouped_entries = entries.into_iter().group_by(|e| e.sheet.to_string());
for (key, group) in grouped_entries.into_iter() {
writeln!(out, "Timesheet: {}", key)?;
// A vector of lines to be printed, with all the components
2021-07-15 00:22:38 -05:00
let mut tabs = if ids {
Tabulate::with_columns(vec![
Col::min_width(2).and_alignment(Right),
Col::min_width(18).and_alignment(Left),
Col::min_width(21).and_alignment(Left),
Col::min_width(8).and_alignment(Right),
Col::min_width(5).and_alignment(Left),
])
} else {
Tabulate::with_columns(vec![
Col::min_width(18).and_alignment(Left),
Col::min_width(21).and_alignment(Left),
Col::min_width(8).and_alignment(Right),
Col::min_width(5).and_alignment(Left),
])
};
if ids {
tabs.feed(vec![
"ID".into(), "Day".into(), "Start".into(), "End".into(), "Duration".into(), "Notes".into(),
]);
} else {
tabs.feed(vec![
"Day".into(), "Start".into(), "End".into(), "Duration".into(), "Notes".into(),
]);
}
let entries_by_date = group.group_by(|e| Local.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);
for (i, entry) in entries.into_iter().enumerate() {
let start = format_start(Local.from_utc_datetime(&entry.start.naive_utc()).time());
let end = entry.end.map(|t| {
format_end(
Local.from_utc_datetime(&entry.start.naive_utc()).naive_local(),
Local.from_utc_datetime(&t.naive_utc()).naive_local()
)
}).unwrap_or(" ".into());
let duration = entry.end.unwrap_or(now) - entry.start;
daily = daily + duration;
let duration = format_duration(duration);
2021-06-30 18:51:02 -05:00
let note = entry.note.unwrap_or("".into());
let id = if ids { entry.id.to_string() } else { "".into() };
if i == 0 {
let date = date.format("%a %b %d, %Y").to_string();
2021-07-15 00:22:38 -05:00
tabs.feed(vec![id, date, start, end, duration, note]);
} else {
2021-07-15 00:22:38 -05:00
tabs.feed(vec![id, "".into(), start, end, duration, note]);
}
}
total = total + daily;
2021-07-15 00:22:38 -05:00
tabs.feed(vec!["".into(), "".into(), "".into(), "".into(), format_duration(daily), "".into()]);
}
2021-07-15 00:22:38 -05:00
tabs.separator('-');
2021-07-15 00:22:38 -05:00
out.write_all(tabs.print().as_bytes())?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::PrettyString;
const LONG_NOTE: &'static str = "chatting with bob about upcoming task, district sharing of images, how the user settings currently works etc. Discussing the fingerprinting / cache busting issue with CKEDITOR, suggesting perhaps looking into forking the rubygem and seeing if we can work in our own changes, however hard that might be.";
#[test]
fn test_text_output() {
std::env::set_var("TZ", "UTC");
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);
print_formatted(entries, &mut output, now, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
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
"));
}
#[test]
fn test_text_output_with_millis() {
std::env::set_var("TZ", "UTC");
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);
print_formatted(entries, &mut output, now, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
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 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);
print_formatted(entries, &mut output, now, false, 100).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
"));
}
#[test]
fn test_text_output_with_ids() {
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);
print_formatted(entries, &mut output, now, true, 100).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
"));
}
#[test]
fn test_text_output_long_note_default_with() {
let mut output = Vec::new();
let entries = vec![
Entry {
id: 1,
sheet: "default".into(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
2021-06-30 18:51:02 -05:00
note: Some(LONG_NOTE.into()),
},
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
print_formatted(entries, &mut output, now, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
Day Start End Duration Notes
Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 chatting with bob about upcoming task,
district sharing of images, how the user
settings currently works etc. Discussing the
fingerprinting / cache busting issue with
CKEDITOR, suggesting perhaps looking into
forking the rubygem and seeing if we can work
in our own changes, however hard that might
be.
2:00:00
-----------------------------------------------------------------------------------------------
Total 2:00:00
"));
}
#[test]
fn test_text_output_long_note_with_ids() {
let mut output = Vec::new();
let entries = vec![
Entry {
id: 60000,
sheet: "default".into(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
2021-06-30 18:51:02 -05:00
note: Some(LONG_NOTE.into()),
},
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
print_formatted(entries, &mut output, now, true, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
ID Day Start End Duration Notes
60000 Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 chatting with bob about upcoming task,
district sharing of images, how the user
settings currently works etc. Discussing the
fingerprinting / cache busting issue with
CKEDITOR, suggesting perhaps looking into
forking the rubygem and seeing if we can
work in our own changes, however hard that
might be.
2:00:00
----------------------------------------------------------------------------------------------
Total 2:00:00
"));
}
#[test]
fn test_text_output_note_with_line_breaks() {
let mut output = Vec::new();
let entries = vec![
Entry {
id: 1,
sheet: "default".into(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
2021-06-30 18:51:02 -05:00
note: Some("first line\nand a second line".into()),
},
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
print_formatted(entries, &mut output, now, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
Day Start End Duration Notes
Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 first line
and a second line
2:00:00
-------------------------------------------------------------------
Total 2:00:00
2021-07-01 11:45:04 -05:00
"));
}
#[test]
fn note_with_accents() {
let mut output = Vec::new();
let entries = vec![
Entry {
id: 1,
sheet: "default".into(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
note: Some("quiúbole".into()),
},
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
print_formatted(entries, &mut output, now, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
Day Start End Duration Notes
Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 quiúbole
2:00:00
----------------------------------------------------------
Total 2:00:00
"));
}
}