2020-04-19 20:11:28 +02:00
use super ::listview ::ListView ;
2020-04-13 23:56:25 +02:00
use itertools ::Itertools ;
2020-01-25 22:15:41 +01:00
use serde ::{ Deserialize , Serialize } ;
use serde_json ::from_reader ;
2020-04-20 14:54:47 +02:00
use std ::{ collections , default , fmt , fs , io , iter } ;
2020-04-20 10:27:59 +02:00
use time ::{ Duration , OffsetDateTime , Time } ;
2020-01-25 22:15:41 +01:00
pub struct TimeSheet {
pub times : Vec < TimePoint > ,
pub selected : usize ,
pub register : Option < TimePoint > ,
}
2020-04-20 14:54:47 +02:00
const PAUSE_TEXTS : [ & str ; 3 ] = [ " lunch " , " mittag " , " pause " ] ;
2020-04-20 15:31:43 +02:00
lazy_static! {
2020-04-27 10:28:56 +02:00
static ref OVERRIDE_REGEX : regex ::Regex = regex ::Regex ::new ( " \\ [(.*) \\ ] " ) . unwrap ( ) ;
2020-04-20 15:31:43 +02:00
}
2020-04-20 14:54:47 +02:00
2020-04-13 23:56:25 +02:00
#[ derive(Serialize, Deserialize, Clone, Debug) ]
2020-01-25 22:15:41 +01:00
pub struct TimePoint {
text : String ,
2020-04-20 10:27:59 +02:00
time : Time ,
2020-01-25 22:15:41 +01:00
}
impl TimePoint {
pub fn new ( text : & str ) -> Self {
Self {
2020-04-27 10:28:56 +02:00
text : String ::from ( text ) ,
time : OffsetDateTime ::now_local ( ) . time ( ) ,
2020-01-25 22:15:41 +01:00
}
}
}
impl fmt ::Display for TimePoint {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> fmt ::Result {
2020-04-27 10:28:56 +02:00
write! ( f , " [{}] {} " , self . time . format ( " %H:%M " ) , self . text )
2020-01-25 22:15:41 +01:00
}
}
2020-04-18 18:15:23 +02:00
impl default ::Default for TimePoint {
fn default ( ) -> Self {
TimePoint ::new ( " " )
}
}
2020-04-13 23:56:25 +02:00
fn read_times ( path : & str ) -> Option < Vec < TimePoint > > {
2020-04-19 19:22:06 +02:00
fs ::File ::open ( path )
2020-04-13 23:56:25 +02:00
. ok ( )
2020-04-19 19:22:06 +02:00
. map ( io ::BufReader ::new )
2020-04-13 23:56:25 +02:00
. and_then ( | r | from_reader ( r ) . ok ( ) )
}
2020-04-20 15:31:43 +02:00
/**
* If a time text contains " [something] " ,
* only use the message inside the brackets .
* /
fn effective_text ( s : String ) -> String {
OVERRIDE_REGEX
. captures ( & s )
// index 0 is the entire string
. and_then ( | caps | caps . get ( 1 ) )
. map ( | m | m . as_str ( ) )
. unwrap_or ( & s )
. to_string ( )
}
2020-01-25 22:15:41 +01:00
impl TimeSheet {
2020-04-13 23:56:25 +02:00
pub fn open_or_create ( path : & str ) -> Self {
2020-01-25 22:15:41 +01:00
Self {
2020-04-20 10:52:45 +02:00
times : read_times ( path ) . unwrap_or_else ( | | vec! [ TimePoint ::new ( " start " ) ] ) ,
2020-01-25 22:15:41 +01:00
selected : 0 ,
register : None ,
}
}
pub fn printable ( & self ) -> Vec < String > {
self . times . iter ( ) . map ( TimePoint ::to_string ) . collect ( )
}
2020-04-13 23:56:25 +02:00
fn current ( & self ) -> & TimePoint {
& self . times [ self . selected ]
2020-01-25 22:15:41 +01:00
}
2020-04-20 14:54:47 +02:00
fn grouped_times ( & self ) -> impl Iterator < Item = ( String , Duration ) > {
2020-04-19 19:04:27 +02:00
self . times
. iter ( )
2020-04-20 14:54:47 +02:00
. chain ( iter ::once ( & TimePoint ::new ( " end " ) ) )
2020-04-19 19:04:27 +02:00
. tuple_windows ( )
2020-04-27 10:28:56 +02:00
. map ( | ( prev , next ) | ( prev . text . clone ( ) , next . time - prev . time ) )
2020-04-20 10:27:59 +02:00
// Fold into a map to group by description.
// I use a BTreeMap because I need a stable output order for the iterator
// (otherwise the summary list will jump around on every input).
2020-04-19 19:22:06 +02:00
. fold ( collections ::BTreeMap ::new ( ) , | mut map , ( text , duration ) | {
2020-04-20 15:31:43 +02:00
* map . entry ( effective_text ( text ) )
. or_insert_with ( Duration ::zero ) + = duration ;
2020-04-19 19:22:06 +02:00
map
} )
2020-04-13 23:56:25 +02:00
. into_iter ( )
2020-04-20 14:54:47 +02:00
. filter ( | ( text , _ ) | ! PAUSE_TEXTS . contains ( & text . as_str ( ) ) )
}
pub fn time_by_tasks ( & self ) -> String {
self . grouped_times ( )
2020-04-27 10:28:56 +02:00
. map ( | ( text , duration ) | format! ( " {} : {} " , text , format_duration ( & duration ) ) )
2020-04-19 19:04:27 +02:00
. join ( " | " )
2020-01-25 22:15:41 +01:00
}
2020-04-13 23:56:25 +02:00
pub fn sum_as_str ( & self ) -> String {
let total = self
2020-04-20 14:54:47 +02:00
. grouped_times ( )
. fold ( Duration ::zero ( ) , | total , ( _ , d ) | total + d ) ;
2020-04-13 23:56:25 +02:00
format_duration ( & total )
2020-01-25 22:15:41 +01:00
}
2020-04-13 23:56:25 +02:00
}
2020-04-19 19:22:06 +02:00
fn format_duration ( d : & Duration ) -> String {
2020-04-13 23:56:25 +02:00
format! ( " {} : {:02} " , d . whole_hours ( ) , d . whole_minutes ( ) . max ( 1 ) % 60 )
}
2020-01-25 22:15:41 +01:00
2020-04-13 23:56:25 +02:00
impl ListView < TimePoint > for TimeSheet {
fn selection_pointer ( & mut self ) -> & mut usize {
& mut self . selected
2020-01-25 22:15:41 +01:00
}
2020-04-13 23:56:25 +02:00
fn list ( & mut self ) -> & mut Vec < TimePoint > {
& mut self . times
2020-01-25 22:15:41 +01:00
}
2020-04-13 23:56:25 +02:00
fn register ( & mut self ) -> & mut Option < TimePoint > {
& mut self . register
}
fn normal_mode ( & mut self ) {
2020-04-27 10:28:56 +02:00
if self . current ( ) . text . is_empty ( ) {
2020-01-25 22:15:41 +01:00
self . remove_current ( ) ;
self . selected = self . selected . saturating_sub ( 1 ) ;
}
2020-04-13 23:56:25 +02:00
self . times . sort_by_key ( | t | t . time ) ;
2020-01-25 22:15:41 +01:00
}
2020-04-13 23:56:25 +02:00
fn append_to_current ( & mut self , chr : char ) {
self . times [ self . selected ] . text . push ( chr ) ;
2020-01-25 22:15:41 +01:00
}
2020-04-13 23:56:25 +02:00
fn backspace ( & mut self ) {
self . times [ self . selected ] . text . pop ( ) ;
}
2020-01-25 22:15:41 +01:00
}