2021-06-30 13:27:42 -05:00
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 ) ;
2021-06-30 18:51:02 -05:00
let note = entry . note . unwrap_or ( " " . into ( ) ) ;
2021-06-30 13:27:42 -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-30 18:51:02 -05:00
lines . push ( [ id , date , start , end , duration , note ] ) ;
2021-06-30 13:27:42 -05:00
} else {
2021-06-30 18:51:02 -05:00
lines . push ( [ id , " " . into ( ) , start , end , duration , note ] ) ;
2021-06-30 13:27:42 -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 ,
" {} {} {} {} {} {} " ,
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 ( & note , 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 + 2 d 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 ( ) ) ,
2021-06-30 13:27:42 -05:00
} ,
] ;
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 ( ) ) ,
2021-06-30 13:27:42 -05:00
} ,
] ;
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 \n and a second line " . into ( ) ) ,
2021-06-30 13:27:42 -05:00
} ,
] ;
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
" ));
}
}