cassiopeia 0.3.0

Simple, low effort time tracking tool for the kookie-office ecosystem
Documentation
//! Typed time file for cassiopeia
//!
//! This data gets generated by the `format` module, and can later be
//! used to generate new files, and perform various lookups and
//! analysis tasks.

use crate::{
    format::ir::{IrItem, IrType, MakeIr},
    Date, Time,
};
use chrono::{DateTime, Duration, FixedOffset as Offset, Local, NaiveDate};
use std::collections::BTreeMap;

#[derive(Debug, Default)]
pub struct TimeFile {
    /// A parsed header structure
    header: BTreeMap<String, String>,
    /// A parsed session structure
    sessions: Vec<Session>,
    /// A parsed invoice list
    invoices: Vec<Invoice>,
}

impl TimeFile {
    pub(crate) fn append(&mut self, line: IrItem) {
        match line {
            IrItem {
                tt: IrType::Header(ref header),
                ..
            } => self.header = header.clone(),
            IrItem {
                tt: IrType::Start(time),
                lo,
            } => self.sessions.push(Session::start(time.into())),
            IrItem {
                tt: IrType::Stop(time),
                lo,
            } => self.get_last_session().unwrap().stop(time.into()),
            IrItem {
                tt: IrType::Invoice(date),
                lo,
            } => self.invoices.push(Invoice::new(date.into())),
            _ => {}
        }
    }

    fn get_last_session(&mut self) -> Option<&mut Session> {
        self.sessions.last_mut()
    }

    fn get_last_invoice(&mut self) -> Option<&mut Invoice> {
        self.invoices.last_mut()
    }

    /// Start a new session (optionally 15-minute rounded)
    ///
    /// This function returns the new session object that will have to
    /// be turned into an IR line to be written back into the file
    pub(crate) fn start(&mut self, round: bool) -> Option<Session> {
        // Check if the last session was closed
        match self.get_last_session() {
            Some(s) if !s.finished() => return None,
            _ => {}
        }

        // Create a new time
        let now = if round {
            Time::now().round()
        } else {
            Time::now()
        };

        Some(Session::start(now))
    }

    /// Stop the last session that was started, returning a completed
    /// session
    pub(crate) fn stop(&mut self, round: bool) -> Option<Session> {
        match self.get_last_session() {
            Some(s) if s.finished() => return None,
            None => return None,
            _ => {}
        }

        // Create a new time
        let now = if round {
            Time::now().round()
        } else {
            Time::now()
        };

        self.get_last_session().cloned().map(|mut s| {
            s.stop(now);
            s
        })
    }

    /// Add a new invoice block to the time file
    pub(crate) fn invoice(&mut self) -> Option<Invoice> {
        let today = Date::today();

        let last_sess = self.get_last_session().cloned();

        match self.get_last_invoice() {
            // Check if _today_ there has been already an invoice
            Some(i) if i.date == today => return None,

            // Check if since the last invoice there has been at least
            // _one_ terminated session.
            Some(i)
                if !last_sess
                    .map(|s| !s.stop.map(|s| s.after(&i.date)).unwrap_or(false))
                    .unwrap_or(false) =>
            {
                return None
            }

            // Otherwise, we create an invoice
            _ => {}
        }

        Some(Invoice::new(today))
    }
}

#[derive(Clone, Debug)]
pub struct Session {
    start: Time,
    stop: Option<Time>,
}

impl Session {
    /// Create a new session with a start time
    fn start(start: Time) -> Self {
        Self { start, stop: None }
    }

    /// Finalise a session with a stop time
    fn stop(&mut self, stop: Time) {
        self.stop = Some(stop);
    }

    /// Check whether this session was already finished
    pub fn finished(&self) -> bool {
        self.stop.is_some()
    }

    /// Get the length of the session, if it was already finished
    pub fn length(&self) -> Option<Duration> {
        self.stop.as_ref().map(|stop| stop - &self.start)
    }
}

impl MakeIr for Session {
    fn make_ir(&self) -> IrType {
        match self.stop {
            Some(ref time) => IrType::Stop(time.clone()),
            None => IrType::Start(self.start.clone()),
        }
    }
}

#[derive(Debug)]
pub struct Invoice {
    date: Date,
}

impl Invoice {
    fn new(date: Date) -> Self {
        Self { date }
    }
}

impl MakeIr for Invoice {
    fn make_ir(&self) -> IrType {
        IrType::Invoice(self.date.clone())
    }
}