organize formatters into their own files
This commit is contained in:
parent
af2a18c8db
commit
6d6eeead55
|
@ -1,54 +1,13 @@
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use itertools::Itertools;
|
use chrono::{DateTime, Utc};
|
||||||
use chrono::{
|
|
||||||
DateTime, Utc, TimeZone, Duration, NaiveTime, Timelike, NaiveDateTime,
|
|
||||||
Local,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::error;
|
use crate::error;
|
||||||
use crate::models::Entry;
|
use crate::models::Entry;
|
||||||
|
|
||||||
fn format_duration(dur: Duration) -> String {
|
pub mod text;
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn constrained_lines(text: &str, width: usize) -> Vec<Cow<'_, str>> {
|
|
||||||
textwrap::wrap(text, width)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
|
@ -72,132 +31,13 @@ impl Formatter {
|
||||||
/// also makes it easier to test.
|
/// also makes it easier to test.
|
||||||
pub fn print_formatted<W: Write>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, ids: bool, term_width: usize) -> error::Result<()> {
|
pub fn print_formatted<W: Write>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, ids: bool, term_width: usize) -> error::Result<()> {
|
||||||
match &self {
|
match &self {
|
||||||
Formatter::Text => self.print_formatted_text(entries, out, now, ids, term_width)?,
|
Formatter::Text => text::print_formatted(entries, out, now, ids, term_width)?,
|
||||||
Formatter::Custom(name) => {
|
Formatter::Custom(name) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print in the default text format. Assume entries are sorted by sheet and
|
|
||||||
/// then by start
|
|
||||||
fn print_formatted_text<W: Write>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, ids: bool, term_width: usize) -> error::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)?;
|
|
||||||
|
|
||||||
// End Duration Notes")?;
|
|
||||||
// A vector of lines to be printed, with all the components
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
let id = if ids { entry.id.to_string() } else { "".into() };
|
|
||||||
|
|
||||||
if i == 0 {
|
|
||||||
let date = date.format("%a %b %d, %Y").to_string();
|
|
||||||
|
|
||||||
lines.push([id, date, start, end, duration, entry.note]);
|
|
||||||
} else {
|
|
||||||
lines.push([id, "".into(), start, end, duration, entry.note]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
"{} {} {} {} {} {}",
|
|
||||||
if ids { lpad("ID", 3.max(lengths[0])) } else { lpad(" ", 3.max(lengths[0])) },
|
|
||||||
rpad("Day", 18),
|
|
||||||
rpad("Start", 10),
|
|
||||||
rpad("End", 10.max(lengths[3])),
|
|
||||||
rpad("Duration", 8.max(lengths[4])),
|
|
||||||
"Notes",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut max_note_length = 0;
|
|
||||||
|
|
||||||
for [id, date, start, end, duration, note] in lines {
|
|
||||||
let first_line = format!(
|
|
||||||
"{} {} {} {} {}",
|
|
||||||
lpad(&id, 3.max(lengths[0])),
|
|
||||||
rpad(&date, 18),
|
|
||||||
rpad(&start, 10),
|
|
||||||
rpad(&end, 10.max(lengths[3])),
|
|
||||||
lpad(&duration, 8.max(lengths[4])),
|
|
||||||
);
|
|
||||||
|
|
||||||
let space_left = term_width.saturating_sub(first_line.len() + 1).max(40);
|
|
||||||
let note_lines = constrained_lines(¬e, space_left);
|
|
||||||
|
|
||||||
for (i, note_line) in note_lines.into_iter().enumerate() {
|
|
||||||
if i == 0 {
|
|
||||||
if note_line.len() != 0 {
|
|
||||||
writeln!(out, "{} {}", first_line, note_line)?;
|
|
||||||
} else {
|
|
||||||
writeln!(out, "{}", first_line)?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
writeln!(out, "{} {}", " ".repeat(first_line.len()), note_line)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if note_line.len() > max_note_length {
|
|
||||||
max_note_length = note_line.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeln!(out,
|
|
||||||
"{} {}-{}-{}-{}-{}",
|
|
||||||
lpad(" ", 3.max(lengths[0])),
|
|
||||||
"-".repeat(18),
|
|
||||||
"-".repeat(10),
|
|
||||||
"-".repeat(10.max(lengths[3])),
|
|
||||||
"-".repeat(8.max(lengths[4])),
|
|
||||||
"-".repeat(4.max(max_note_length)),
|
|
||||||
)?;
|
|
||||||
writeln!(out,
|
|
||||||
"{} {} {} {} {}",
|
|
||||||
lpad(" ", 3.max(lengths[0])),
|
|
||||||
rpad("Total", 18),
|
|
||||||
rpad("", 10),
|
|
||||||
rpad("", 10.max(lengths[3])),
|
|
||||||
lpad(&format_duration(total), 8.max(lengths[4])),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Formatter {
|
impl FromStr for Formatter {
|
||||||
|
@ -212,234 +52,3 @@ impl FromStr for Formatter {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
use chrono::TimeZone;
|
|
||||||
|
|
||||||
use crate::test_utils::PrettyString;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
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_constrained_lines_long_text() {
|
|
||||||
std::env::set_var("TZ", "UTC");
|
|
||||||
assert_eq!(constrained_lines(LONG_NOTE, 46), vec![
|
|
||||||
"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 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);
|
|
||||||
|
|
||||||
formatter.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 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);
|
|
||||||
|
|
||||||
formatter.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 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;
|
|
||||||
|
|
||||||
formatter.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 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, 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 formatter = Formatter::Text;
|
|
||||||
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: LONG_NOTE.into(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
|
||||||
let offset = Utc;
|
|
||||||
|
|
||||||
formatter.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 formatter = Formatter::Text;
|
|
||||||
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)),
|
|
||||||
note: LONG_NOTE.into(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
|
||||||
let offset = Utc;
|
|
||||||
|
|
||||||
formatter.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 formatter = Formatter::Text;
|
|
||||||
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: "first line\nand a second line".into(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
|
||||||
let offset = Utc;
|
|
||||||
|
|
||||||
formatter.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
|
|
||||||
"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,383 @@
|
||||||
|
use std::io::Write;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
use chrono::{
|
||||||
|
DateTime, Utc, TimeZone, Duration, Local, NaiveTime, Timelike,
|
||||||
|
NaiveDateTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::models::Entry;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn constrained_lines(text: &str, width: usize) -> Vec<Cow<'_, str>> {
|
||||||
|
textwrap::wrap(text, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)?;
|
||||||
|
|
||||||
|
// End Duration Notes")?;
|
||||||
|
// A vector of lines to be printed, with all the components
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
let id = if ids { entry.id.to_string() } else { "".into() };
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
let date = date.format("%a %b %d, %Y").to_string();
|
||||||
|
|
||||||
|
lines.push([id, date, start, end, duration, entry.note]);
|
||||||
|
} else {
|
||||||
|
lines.push([id, "".into(), start, end, duration, entry.note]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
"{} {} {} {} {} {}",
|
||||||
|
if ids { lpad("ID", 3.max(lengths[0])) } else { lpad(" ", 3.max(lengths[0])) },
|
||||||
|
rpad("Day", 18),
|
||||||
|
rpad("Start", 10),
|
||||||
|
rpad("End", 10.max(lengths[3])),
|
||||||
|
rpad("Duration", 8.max(lengths[4])),
|
||||||
|
"Notes",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut max_note_length = 0;
|
||||||
|
|
||||||
|
for [id, date, start, end, duration, note] in lines {
|
||||||
|
let first_line = format!(
|
||||||
|
"{} {} {} {} {}",
|
||||||
|
lpad(&id, 3.max(lengths[0])),
|
||||||
|
rpad(&date, 18),
|
||||||
|
rpad(&start, 10),
|
||||||
|
rpad(&end, 10.max(lengths[3])),
|
||||||
|
lpad(&duration, 8.max(lengths[4])),
|
||||||
|
);
|
||||||
|
|
||||||
|
let space_left = term_width.saturating_sub(first_line.len() + 1).max(40);
|
||||||
|
let note_lines = constrained_lines(¬e, space_left);
|
||||||
|
|
||||||
|
for (i, note_line) in note_lines.into_iter().enumerate() {
|
||||||
|
if i == 0 {
|
||||||
|
if note_line.len() != 0 {
|
||||||
|
writeln!(out, "{} {}", first_line, note_line)?;
|
||||||
|
} else {
|
||||||
|
writeln!(out, "{}", first_line)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writeln!(out, "{} {}", " ".repeat(first_line.len()), note_line)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if note_line.len() > max_note_length {
|
||||||
|
max_note_length = note_line.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(out,
|
||||||
|
"{} {}-{}-{}-{}-{}",
|
||||||
|
lpad(" ", 3.max(lengths[0])),
|
||||||
|
"-".repeat(18),
|
||||||
|
"-".repeat(10),
|
||||||
|
"-".repeat(10.max(lengths[3])),
|
||||||
|
"-".repeat(8.max(lengths[4])),
|
||||||
|
"-".repeat(4.max(max_note_length)),
|
||||||
|
)?;
|
||||||
|
writeln!(out,
|
||||||
|
"{} {} {} {} {}",
|
||||||
|
lpad(" ", 3.max(lengths[0])),
|
||||||
|
rpad("Total", 18),
|
||||||
|
rpad("", 10),
|
||||||
|
rpad("", 10.max(lengths[3])),
|
||||||
|
lpad(&format_duration(total), 8.max(lengths[4])),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_constrained_lines_long_text() {
|
||||||
|
std::env::set_var("TZ", "UTC");
|
||||||
|
assert_eq!(constrained_lines(LONG_NOTE, 46), vec![
|
||||||
|
"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)),
|
||||||
|
note: 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)),
|
||||||
|
note: 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)),
|
||||||
|
note: "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
|
||||||
|
"));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue