410 lines
17 KiB
Rust
410 lines
17 KiB
Rust
use std::io::Write;
|
|
|
|
use itertools::Itertools;
|
|
use chrono::{ Duration, Local, NaiveTime, Timelike, NaiveDateTime };
|
|
|
|
use crate::models::Entry;
|
|
use crate::error::Result;
|
|
use crate::tabulate::{Tabulate, Col, Align::*};
|
|
use crate::commands::Facts;
|
|
|
|
pub fn format_duration(dur: Duration) -> String {
|
|
format!("{}:{:02}:{:02}", dur.num_hours(), dur.num_minutes() % 60, dur.num_seconds() % 60)
|
|
}
|
|
|
|
pub fn format_hours(dur: Duration) -> String {
|
|
let hours = dur.num_hours();
|
|
let minutes = dur.num_minutes() % 60;
|
|
let seconds = dur.num_seconds() % 60;
|
|
|
|
let time = [
|
|
if hours > 0 { Some(format!("{hours}h")) } else { None },
|
|
if minutes > 0 { Some(format!("{minutes}m")) } else { None },
|
|
if seconds > 0 { Some(format!("{seconds}s")) } else { None },
|
|
].into_iter().flatten().collect::<Vec<_>>().join(" ");
|
|
|
|
if time.is_empty() {
|
|
String::from("less than one second")
|
|
} else {
|
|
time
|
|
}
|
|
}
|
|
|
|
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, facts: &Facts, ids: bool) -> Result<()> {
|
|
let grouped_entries = entries.into_iter().group_by(|e| e.sheet.to_string());
|
|
let mut num_sheets = 0;
|
|
let mut grand_total = Duration::seconds(0);
|
|
|
|
for (i, (key, group)) in grouped_entries.into_iter().enumerate() {
|
|
num_sheets += 1;
|
|
if i != 0 {
|
|
writeln!(out)?;
|
|
}
|
|
|
|
writeln!(out, "Timesheet: {}", key)?;
|
|
|
|
// A vector of lines to be printed, with all the components
|
|
let mut tabs = Tabulate::with_columns(vec![
|
|
Col::new().min_width(3).and_alignment(Right),
|
|
Col::new().min_width(18).and_alignment(Left),
|
|
Col::new().min_width(21).and_alignment(Left),
|
|
Col::new().min_width(8).and_alignment(Right),
|
|
Col::new().min_width(5).max_width(44).and_alignment(Left),
|
|
]);
|
|
|
|
if ids {
|
|
tabs.feed(vec![
|
|
"ID", "Day", "Start End", "Duration", "Notes",
|
|
]);
|
|
} else {
|
|
tabs.feed(vec![
|
|
"", "Day", "Start End", "Duration", "Notes",
|
|
]);
|
|
}
|
|
|
|
let entries_by_date = group.group_by(|e| e.start.with_timezone(&Local).date_naive());
|
|
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 startend = format!("{} - {}",
|
|
format_start(entry.start.with_timezone(&Local).time()),
|
|
entry.end.map(|t| {
|
|
format_end(
|
|
entry.start.with_timezone(&Local).naive_local(),
|
|
t.with_timezone(&Local).naive_local()
|
|
)
|
|
}).unwrap_or_else(|| "".into())
|
|
);
|
|
let duration = entry.end.unwrap_or(facts.now) - entry.start;
|
|
daily = daily + duration;
|
|
let duration = format_duration(duration);
|
|
let note = entry.note.unwrap_or_else(|| "".into());
|
|
|
|
let id = if ids { entry.id.to_string() } else { "".into() };
|
|
|
|
if i == 0 {
|
|
let date = date.format("%a %b %d, %Y").to_string();
|
|
|
|
tabs.feed(vec![id, date, startend, duration, note]);
|
|
} else {
|
|
tabs.feed(vec![id, "".into(), startend, duration, note]);
|
|
}
|
|
}
|
|
|
|
total = total + daily;
|
|
|
|
tabs.feed(vec!["".to_string(), "".to_string(), "".to_string(), format_duration(daily), "".to_string()]);
|
|
}
|
|
|
|
tabs.separator('-');
|
|
tabs.feed(vec!["".into(), "Total".into(), "".into(), format_duration(total)]);
|
|
grand_total = grand_total + total;
|
|
|
|
out.write_all(tabs.print(facts.env.stdout_is_tty).as_bytes())?;
|
|
}
|
|
|
|
if num_sheets > 1 {
|
|
let mut tabs = Tabulate::with_columns(vec![
|
|
Col::new().min_width(3).and_alignment(Right),
|
|
Col::new().min_width(18).and_alignment(Left),
|
|
Col::new().min_width(21).and_alignment(Left),
|
|
Col::new().min_width(8).and_alignment(Right),
|
|
Col::new().min_width(5).max_width(44).and_alignment(Left),
|
|
]);
|
|
|
|
tabs.separator('-');
|
|
|
|
tabs.feed(vec!["".to_string(), "Grand total".to_string(), "".to_string(), format_duration(grand_total)]);
|
|
|
|
out.write_all(tabs.print(facts.env.stdout_is_tty).as_bytes())?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use pretty_assertions::assert_eq;
|
|
use chrono::{Utc, TimeZone};
|
|
|
|
use super::*;
|
|
|
|
const LONG_NOTE: &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", "CST+6");
|
|
let mut output = Vec::new();
|
|
let entries = vec![
|
|
Entry::new_sample(1, Utc.with_ymd_and_hms(2008, 10, 3, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap())),
|
|
Entry::new_sample(2, Utc.with_ymd_and_hms(2008, 10, 3, 16, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 18, 0, 0).unwrap())),
|
|
Entry::new_sample(3, Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap())),
|
|
Entry::new_sample(4, Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap(), None),
|
|
];
|
|
|
|
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
|
let facts = Facts::new().with_now(now);
|
|
|
|
print_formatted(entries, &mut output, &facts, false).unwrap();
|
|
|
|
assert_eq!(&String::from_utf8_lossy(&output), "Timesheet: default
|
|
Day Start End Duration Notes
|
|
Fri Oct 03, 2008 06:00:00 - 08:00:00 2:00:00 entry 1
|
|
10:00:00 - 12:00:00 2:00:00 entry 2
|
|
4:00:00
|
|
Sun Oct 05, 2008 10:00:00 - 12:00:00 2:00:00 entry 3
|
|
12: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", "CST+6");
|
|
let mut output = Vec::new();
|
|
let entries = vec![
|
|
Entry::new_sample(
|
|
1,
|
|
Utc.with_ymd_and_hms(2008, 10, 3, 12, 0, 0).unwrap().with_nanosecond(432_000_000).unwrap(),
|
|
Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap().with_nanosecond(312_000_000).unwrap())),
|
|
];
|
|
|
|
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
|
let facts = Facts::new().with_now(now);
|
|
|
|
print_formatted(entries, &mut output, &facts, false).unwrap();
|
|
|
|
assert_eq!(&String::from_utf8_lossy(&output), "Timesheet: default
|
|
Day Start End Duration Notes
|
|
Fri Oct 03, 2008 06:00:00 - 08:00:00 1:59:59 entry 1
|
|
1:59:59
|
|
-------------------------------------------------------------
|
|
Total 1:59:59
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn test_text_output_long_duration() {
|
|
std::env::set_var("TZ", "CST+6");
|
|
let mut output = Vec::new();
|
|
let entries = vec![
|
|
Entry::new_sample(1, Utc.with_ymd_and_hms(2008, 10, 1, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap())),
|
|
Entry::new_sample(2, Utc.with_ymd_and_hms(2008, 10, 3, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap())),
|
|
];
|
|
|
|
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
|
let facts = Facts::new().with_now(now);
|
|
|
|
print_formatted(entries, &mut output, &facts, false).unwrap();
|
|
|
|
assert_eq!(&String::from_utf8_lossy(&output), "Timesheet: default
|
|
Day Start End Duration Notes
|
|
Wed Oct 01, 2008 06:00:00 - 08:00:00+2d 50:00:00 entry 1
|
|
50:00:00
|
|
Fri Oct 03, 2008 06:00:00 - 08:00:00 2:00:00 entry 2
|
|
2:00:00
|
|
--------------------------------------------------------------
|
|
Total 52:00:00
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn test_text_output_with_ids() {
|
|
std::env::set_var("TZ", "CST+6");
|
|
let mut output = Vec::new();
|
|
let entries = vec![
|
|
Entry::new_sample(1, Utc.with_ymd_and_hms(2008, 10, 3, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap())),
|
|
Entry::new_sample(2, Utc.with_ymd_and_hms(2008, 10, 3, 16, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 18, 0, 0).unwrap())),
|
|
Entry::new_sample(3, Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap())),
|
|
Entry::new_sample(4, Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap(), None),
|
|
];
|
|
|
|
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
|
let facts = Facts::new().with_now(now);
|
|
|
|
print_formatted(entries, &mut output, &facts, true).unwrap();
|
|
|
|
assert_eq!(&String::from_utf8_lossy(&output), "Timesheet: default
|
|
ID Day Start End Duration Notes
|
|
1 Fri Oct 03, 2008 06:00:00 - 08:00:00 2:00:00 entry 1
|
|
2 10:00:00 - 12:00:00 2:00:00 entry 2
|
|
4:00:00
|
|
3 Sun Oct 05, 2008 10:00:00 - 12:00:00 2:00:00 entry 3
|
|
4 12:00:00 - 2:00:00 entry 4
|
|
4:00:00
|
|
-------------------------------------------------------------
|
|
Total 8:00:00
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn test_text_output_long_note_with_ids() {
|
|
std::env::set_var("TZ", "CST+6");
|
|
let mut output = Vec::new();
|
|
let entries = vec![
|
|
Entry {
|
|
id: 60000,
|
|
sheet: "default".into(),
|
|
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
|
|
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
|
|
note: Some(LONG_NOTE.into()),
|
|
},
|
|
];
|
|
|
|
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
|
let facts = Facts::new().with_now(now);
|
|
|
|
print_formatted(entries, &mut output, &facts, true).unwrap();
|
|
|
|
assert_eq!(&String::from_utf8_lossy(&output), "Timesheet: default
|
|
ID Day Start End Duration Notes
|
|
60000 Sun Oct 05, 2008 10:00:00 - 12: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() {
|
|
std::env::set_var("TZ", "CST+6");
|
|
let mut output = Vec::new();
|
|
let entries = vec![
|
|
Entry {
|
|
id: 1,
|
|
sheet: "default".into(),
|
|
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
|
|
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
|
|
note: Some("first line\nand a second line".into()),
|
|
},
|
|
];
|
|
|
|
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
|
let facts = Facts::new().with_now(now);
|
|
|
|
print_formatted(entries, &mut output, &facts, false).unwrap();
|
|
|
|
assert_eq!(&String::from_utf8_lossy(&output), "Timesheet: default
|
|
Day Start End Duration Notes
|
|
Sun Oct 05, 2008 10:00:00 - 12:00:00 2:00:00 first line
|
|
and a second line
|
|
2:00:00
|
|
-----------------------------------------------------------------------
|
|
Total 2:00:00
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn note_with_accents() {
|
|
std::env::set_var("TZ", "CST+6");
|
|
let mut output = Vec::new();
|
|
let entries = vec![
|
|
Entry {
|
|
id: 1,
|
|
sheet: "default".into(),
|
|
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
|
|
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
|
|
note: Some("quiúbole".into()),
|
|
},
|
|
];
|
|
|
|
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
|
let facts = Facts::new().with_now(now);
|
|
|
|
print_formatted(entries, &mut output, &facts, false).unwrap();
|
|
|
|
assert_eq!(&String::from_utf8_lossy(&output), "Timesheet: default
|
|
Day Start End Duration Notes
|
|
Sun Oct 05, 2008 10:00:00 - 12:00:00 2:00:00 quiúbole
|
|
2:00:00
|
|
--------------------------------------------------------------
|
|
Total 2:00:00
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn displays_grand_total_when_multiple_sheets() {
|
|
std::env::set_var("TZ", "CST+6");
|
|
|
|
let mut output = Vec::new();
|
|
let entries = vec![
|
|
Entry {
|
|
id: 1,
|
|
sheet: "sheet1".to_string(),
|
|
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
|
|
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
|
|
note: Some("quiúbole".to_string()),
|
|
},
|
|
Entry {
|
|
id: 2,
|
|
sheet: "sheet2".to_string(),
|
|
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
|
|
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
|
|
note: Some("quiúbole".to_string()),
|
|
},
|
|
];
|
|
|
|
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
|
let facts = Facts::new().with_now(now);
|
|
|
|
print_formatted(entries, &mut output, &facts, false).unwrap();
|
|
|
|
assert_eq!(&String::from_utf8_lossy(&output), "Timesheet: sheet1
|
|
Day Start End Duration Notes
|
|
Sun Oct 05, 2008 10:00:00 - 12:00:00 2:00:00 quiúbole
|
|
2:00:00
|
|
--------------------------------------------------------------
|
|
Total 2:00:00
|
|
|
|
Timesheet: sheet2
|
|
Day Start End Duration Notes
|
|
Sun Oct 05, 2008 10:00:00 - 12:00:00 2:00:00 quiúbole
|
|
2:00:00
|
|
--------------------------------------------------------------
|
|
Total 2:00:00
|
|
-----------------------------------------------------------
|
|
Grand total 4:00:00
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn hour_formatting() {
|
|
assert_eq!(format_hours(Duration::hours(3) + Duration::minutes(30)), "3h 30m");
|
|
assert_eq!(format_hours(Duration::hours(3) + Duration::minutes(1)), "3h 1m");
|
|
assert_eq!(format_hours(Duration::hours(3) + Duration::minutes(5) + Duration::seconds(59)), "3h 5m 59s");
|
|
assert_eq!(format_hours(Duration::hours(3)), "3h");
|
|
assert_eq!(format_hours(Duration::milliseconds(543)), "less than one second")
|
|
}
|
|
}
|