aboutsummaryrefslogtreecommitdiff
path: root/apps/cassiopeia/src/timeline.rs
diff options
context:
space:
mode:
authorMx Kookie <kookie@spacekookie.de>2020-12-19 15:15:20 +0000
committerMx Kookie <kookie@spacekookie.de>2020-12-21 05:19:49 +0100
commit69eaad1c9f934bccaf7e28529a6b1657345f0184 (patch)
tree4e7517a16f7303f5f6efbd02e4aae85bdc5aec29 /apps/cassiopeia/src/timeline.rs
parentb9c988f42504c2e4cfa0715ac8f2d2a0db591cad (diff)
cassiopeia: changing internal data representation to timeline module
What this allows us to do is much better relationship tracking between sessions and invoices. The CASS file already has all the structure we need, and it would be silly to replicate it via complicated time association algorithms. This approach uses the linear nature of the data file to track the position relative to other entries. The timeline module is then responsible for making changes to the internal representation (in case it is being used as a library for multi-query commands), and emitting a `Delta` type that can be used to easily patch the IR in question, because the mapping between the timeline and IR representations is linear.
Diffstat (limited to '')
-rw-r--r--apps/cassiopeia/src/timeline.rs132
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))
+ }
+}