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-23 23:19:13 +02:00
const TIME_FORMAT : & str = " %H:%M " ;
2020-04-20 15:31:43 +02:00
lazy_static! {
2020-04-23 23:19:13 +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 {
2020-04-23 23:19:13 +02:00
let time = OffsetDateTime ::now_local ( ) . time ( ) ;
2020-01-25 22:15:41 +01:00
Self {
2020-04-23 23:19:13 +02:00
time ,
text : format ! ( " [{}] {} " , time . format ( TIME_FORMAT ) , text ) ,
2020-01-25 22:15:41 +01:00
}
}
}
impl fmt ::Display for TimePoint {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> fmt ::Result {
2020-04-23 23:19:13 +02:00
write! ( f , " {} " , 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-23 23:19:13 +02:00
. map ( | ( prev , next ) | {
(
prev . text . clone ( ) . splitn ( 2 , " " ) . last ( ) . unwrap ( ) . to_string ( ) ,
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-23 23:19:13 +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-23 23:19:13 +02:00
let old_text = self . current ( ) . text . clone ( ) ;
let parts : Vec < _ > = old_text . splitn ( 2 , " " ) . collect ( ) ;
if parts . len ( ) < 2 {
2020-01-25 22:15:41 +01:00
self . remove_current ( ) ;
self . selected = self . selected . saturating_sub ( 1 ) ;
2020-04-23 23:19:13 +02:00
return ;
}
let current = & mut self . times [ self . selected ] ;
// if we have a parse error, just keep the old time
if let Ok ( t ) = Time ::parse ( parts [ 0 ] . replace ( '[' , " " ) . replace ( ']' , " " ) , TIME_FORMAT ) {
current . time = t ;
2020-01-25 22:15:41 +01:00
}
2020-04-23 23:19:13 +02:00
current . text = format! ( " [ {} ] {} " , current . time . format ( TIME_FORMAT ) , parts [ 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-23 23:19:13 +02:00
// noop for this
fn toggle_current ( & mut self ) { }
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
}