diff options
Diffstat (limited to '')
-rw-r--r-- | apps/cassiopeia/src/timeline.rs | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/apps/cassiopeia/src/timeline.rs b/apps/cassiopeia/src/timeline.rs new file mode 100644 index 000000000000..70119b30a725 --- /dev/null +++ b/apps/cassiopeia/src/timeline.rs @@ -0,0 +1,132 @@ +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<Session> for Entry { + fn from(s: Session) -> Self { + Self::Session(s) + } +} + +impl From<Invoice> 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<Entry>, +} + +impl Timeline { + /// Take a set of sessions and invoices to sort into a timeline + pub(crate) fn build(s: Vec<Session>, i: Vec<Invoice>) -> 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<Delta> { + 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<Delta> { + 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<Delta> { + 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)) + } +} |