use crate::{ data::{Delta, Invoice, Session}, error::{UserError, UserResult}, Date, Time, }; /// A timeline entry of sessions and invoices #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub(crate) enum Entry { Session(Session), Invoice(Invoice), } impl From for Entry { fn from(s: Session) -> Self { Self::Session(s) } } impl From for Entry { fn from(i: Invoice) -> Self { Self::Invoice(i) } } /// A timeline of sessions and invoices, ordered chronologically #[derive(Debug, Default, Clone)] pub(crate) struct Timeline { inner: Vec, } impl Timeline { /// Take a set of sessions and invoices to sort into a timeline pub(crate) fn build(s: Vec, i: Vec) -> Self { let mut inner: Vec<_> = s.into_iter().map(|s| Entry::Session(s)).collect(); inner.append(&mut i.into_iter().map(|i| Entry::Invoice(i)).collect()); Self { inner } } /// Utility function to get the last session in the timeline fn last_session(&mut self) -> Option<&mut Session> { self.inner .iter_mut() .find(|e| match e { Entry::Session(_) => true, _ => false, }) .map(|e| match e { Entry::Session(ref mut s) => s, _ => unreachable!(), }) } /// Utility function to get the last invoice in the timeline fn last_invoice(&self) -> Option<&Invoice> { self.inner .iter() .find(|e| match e { Entry::Invoice(_) => true, _ => false, }) .map(|e| match e { Entry::Invoice(ref s) => s, _ => unreachable!(), }) } /// Get a list of sessions that happened up to a certain invoice date /// /// **WARNING** If there is no invoice with the given date, this /// function will return garbage data, so don't call it with /// invoice dates that don't exist. /// /// Because: if the date passes other invoices on the way, the accumulator /// will be discarded and a new count will be started. pub(crate) fn session_iter(&self, date: &Date) -> Vec<&Session> { self.inner .iter() .fold((false, vec![]), |(mut done, mut acc), entry| { match (done, entry) { // Put sessions into the accumulator (false, Entry::Session(ref s)) => acc.push(s), // When we reach the target invoice, terminate the iterator (false, Entry::Invoice(ref i)) if &i.date == date => done = true, // When we hit another invoice, empty accumulator (false, Entry::Invoice(_)) => acc.clear(), // When we are ever "done", skip all other entries (true, _) => {} } (done, acc) }) .1 } /// Start a new session, if no active session is already in progress pub(crate) fn start(&mut self, time: Time) -> UserResult { match self.last_session() { Some(s) if !s.finished() => Err(UserError::ActiveSessionExists), _ => Ok(()), }?; self.inner.push(Session::start(time.clone()).into()); Ok(Delta::Start(time)) } /// Stop an ongoing session, if one exists pub(crate) fn stop(&mut self, time: Time) -> UserResult { match self.last_session() { Some(s) if s.finished() => Err(UserError::NoActiveSession), _ => Ok(()), }?; self.last_session().unwrap().stop(time.clone()); Ok(Delta::Stop(time)) } /// Create a new invoice on the given day pub(crate) fn invoice(&mut self, date: Date) -> UserResult { match self.last_invoice() { // If an invoice on the same day exists already Some(i) if i.date == date => Err(UserError::SameDayInvoice), // If there was no work since the last invoice Some(ref i) if self.session_iter(&i.date).len() == 0 => Err(UserError::NoWorkInvoice), // Otherwise everything is coolio _ => Ok(()), }?; self.inner.push(Invoice::new(date.clone()).into()); Ok(Delta::Invoice(date)) } }