aboutsummaryrefslogtreecommitdiff
path: root/apps/cassiopeia
diff options
context:
space:
mode:
authorMx Kookie <kookie@spacekookie.de>2020-12-13 12:08:55 +0000
committerMx Kookie <kookie@spacekookie.de>2020-12-21 05:19:49 +0100
commit236cf191b90a428325c8c179d595d4b1cd36f776 (patch)
tree5d6c67cb0b7ef980aad47ee35e264b4dfdb5422d /apps/cassiopeia
parent4c97f3208a0ba185264a169e01d0b0d922266ea6 (diff)
cassiopeia: changing parser output to more generic IR structure
This allows a few things: a, it's a persistant format that we can mirror to disk again, AND can adapt because all type information is known, and it allows for new entries to be added to the IR more easily, without having to worry about exact formatting, or re-inferring order from the TimeFile abstraction.
Diffstat (limited to 'apps/cassiopeia')
-rw-r--r--apps/cassiopeia/src/data.rs159
-rw-r--r--apps/cassiopeia/src/date.rs26
-rw-r--r--apps/cassiopeia/src/format/ir.rs63
-rw-r--r--apps/cassiopeia/src/format/mod.rs32
-rw-r--r--apps/cassiopeia/src/format/parser.rs3
-rw-r--r--apps/cassiopeia/src/lib.rs30
-rw-r--r--apps/cassiopeia/src/time.rs29
7 files changed, 289 insertions, 53 deletions
diff --git a/apps/cassiopeia/src/data.rs b/apps/cassiopeia/src/data.rs
index 7442d699be88..47bb6e35b6e5 100644
--- a/apps/cassiopeia/src/data.rs
+++ b/apps/cassiopeia/src/data.rs
@@ -4,14 +4,15 @@
//! used to generate new files, and perform various lookups and
//! analysis tasks.
-use crate::format::LineCfg;
-use chrono::{DateTime, Duration, Local, FixedOffset as Offset, NaiveDate};
+use crate::{
+ format::{IrItem, IrType, MakeIr},
+ Date, Time,
+};
+use chrono::{DateTime, Duration, FixedOffset as Offset, Local, NaiveDate};
use std::collections::BTreeMap;
#[derive(Debug, Default)]
pub struct TimeFile {
- /// Raw line buffers to echo back into the file
- lines: Vec<LineCfg>,
/// A parsed header structure
header: BTreeMap<String, String>,
/// A parsed session structure
@@ -21,70 +22,158 @@ pub struct TimeFile {
}
impl TimeFile {
- pub(crate) fn append(mut self, line: LineCfg) -> Self {
- let lo = self.lines.len();
+ pub(crate) fn append(&mut self, line: IrItem) {
match line {
- LineCfg::Header(ref header) => self.header = header.clone(),
- LineCfg::Start(Some(time)) => self.sessions.push(Session::start(time, lo)),
- LineCfg::Stop(Some(time)) => self.get_last_session().stop(time, lo),
- LineCfg::Invoice(Some(date)) => self.invoices.push(Invoice::new(date, lo)),
+ 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())),
_ => {}
}
+ }
- self.lines.push(line);
- self
+ fn get_last_session(&mut self) -> Option<&mut Session> {
+ self.sessions.last_mut()
}
- fn get_last_session(&mut self) -> &mut Session {
- self.sessions.last_mut().unwrap()
+ fn get_last_invoice(&mut self) -> Option<&mut Invoice> {
+ self.invoices.last_mut()
}
/// Start a new session (optionally 15-minute rounded)
- pub fn start(&mut self, round: bool) -> Option<()> {
- let now = Local::now();
-
+ ///
+ /// 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<()> {
+ 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
+ _ => {}
+ }
+
+ self.invoices.push(Invoice::new(today));
Some(())
}
}
-#[derive(Debug)]
+#[derive(Clone, Debug)]
pub struct Session {
- start: DateTime<Offset>,
- stop: Option<DateTime<Offset>>,
- /// Track the lines this session took place in
- lines: (usize, usize),
+ start: Time,
+ stop: Option<Time>,
}
impl Session {
/// Create a new session with a start time
- fn start(start: DateTime<Offset>, line: usize) -> Self {
- Self {
- start,
- stop: None,
- lines: (line, 0),
- }
+ fn start(start: Time) -> Self {
+ Self { start, stop: None }
}
/// Finalise a session with a stop time
- fn stop(&mut self, stop: DateTime<Offset>, line: usize) {
+ fn stop(&mut self, stop: Time) {
self.stop = Some(stop);
- self.lines.1 = line;
+ }
+
+ /// 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.map(|stop| stop - self.start)
+ 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: NaiveDate,
- line: usize,
+ date: Date,
}
impl Invoice {
- fn new(date: NaiveDate, line: usize) -> Self {
- Self { date, line }
+ fn new(date: Date) -> Self {
+ Self { date }
+ }
+}
+
+impl MakeIr for Invoice {
+ fn make_ir(&self) -> IrType {
+ IrType::Invoice(self.date.clone())
}
}
diff --git a/apps/cassiopeia/src/date.rs b/apps/cassiopeia/src/date.rs
new file mode 100644
index 000000000000..00d8a700859a
--- /dev/null
+++ b/apps/cassiopeia/src/date.rs
@@ -0,0 +1,26 @@
+use crate::Time;
+use chrono::{FixedOffset as Offset, NaiveDate};
+
+/// A convenienc wrapper around [chrono::NaiveDate](chrono::NaiveDate)
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Date {
+ inner: NaiveDate,
+}
+
+impl Date {
+ pub fn today() -> Self {
+ Self::from(Time::now().date())
+ }
+
+ pub(crate) fn from(d: chrono::Date<Offset>) -> Self {
+ Self {
+ inner: d.naive_local(),
+ }
+ }
+}
+
+impl From<NaiveDate> for Date {
+ fn from(inner: NaiveDate) -> Self {
+ Self { inner }
+ }
+}
diff --git a/apps/cassiopeia/src/format/ir.rs b/apps/cassiopeia/src/format/ir.rs
new file mode 100644
index 000000000000..32922ec079e7
--- /dev/null
+++ b/apps/cassiopeia/src/format/ir.rs
@@ -0,0 +1,63 @@
+use crate::{format::LineCfg, Date, Time, TimeFile};
+use std::collections::BTreeMap;
+
+/// A set of IR parsed items that makes up a whole cass file
+pub(crate) type IrStream = Vec<IrItem>;
+
+/// Intermediate representation for parsing and generating files
+///
+/// The CASS IR is largely based on the output of the parser's
+/// [`LineCfg`](crate::format::LineCfg), but with concrete types used
+/// in the data layer (namely [`Date`][date] and [`Time`][time]),
+/// while also keeping track of the line numbers to allow idempotent
+/// file changes.
+///
+/// Something not yet implemented is comment pass-through (this needs
+/// to happen in the parser first), but will likely be implemented in
+/// a future version.
+///
+/// [date]: crate::Date
+/// [time]: crate::Time
+#[derive(Debug, Clone)]
+pub(crate) struct IrItem {
+ pub(crate) tt: IrType,
+ pub(crate) lo: usize,
+}
+
+/// Disambiguate between different IR line types with their payload
+#[derive(Debug, Clone)]
+pub(crate) enum IrType {
+ /// A line with parsed header information
+ Header(BTreeMap<String, String>),
+ /// Start a session at a given timestapm
+ Start(Time),
+ /// Stop a session at a given timestamp
+ Stop(Time),
+ /// Invoice a block of previous work
+ Invoice(Date),
+ /// An item that gets ignored
+ Ignore,
+}
+
+/// Generate a stream of IR items from the raw parser output
+pub(crate) fn generate_ir(buf: impl Iterator<Item = LineCfg>) -> IrStream {
+ buf.enumerate().fold(vec![], |mut buf, (lo, item)| {
+ #[cfg_attr(rustfmt, rustfmt_skip)]
+ buf.push(match item {
+ LineCfg::Header(map) => IrItem { tt: IrType::Header(map), lo },
+ LineCfg::Start(Some(time)) => IrItem { tt: IrType::Start(time.into()), lo },
+ LineCfg::Stop(Some(time)) => IrItem { tt: IrType::Stop(time.into()), lo },
+ LineCfg::Invoice(Some(date)) => IrItem { tt: IrType::Invoice(date.into()), lo },
+ LineCfg::Ignore => IrItem { tt: IrType::Ignore, lo },
+ _ => IrItem { tt: IrType::Ignore, lo },
+ });
+
+ buf
+ })
+}
+
+
+pub(crate) trait MakeIr {
+ /// Make a new IR line from an object
+ fn make_ir(&self) -> IrType;
+}
diff --git a/apps/cassiopeia/src/format/mod.rs b/apps/cassiopeia/src/format/mod.rs
index e55658d515ff..814c08656dbe 100644
--- a/apps/cassiopeia/src/format/mod.rs
+++ b/apps/cassiopeia/src/format/mod.rs
@@ -1,17 +1,33 @@
//! cassiopeia file format
+mod ir;
mod lexer;
mod parser;
+pub(crate) use ir::{IrItem, IrStream, IrType, MakeIr};
pub(crate) use lexer::{LineLexer, LineToken, Token};
pub(crate) use parser::LineCfg;
use crate::TimeFile;
use std::{fs::File, io::Read};
+#[derive(Default)]
+pub struct ParseOutput {
+ pub(crate) ir: IrStream,
+ pub(crate) tf: TimeFile,
+}
+
+impl ParseOutput {
+ fn append(mut self, ir: IrItem) -> Self {
+ self.tf.append(ir.clone());
+ self.ir.push(ir);
+ self
+ }
+}
+
/// Load a file from disk and parse it into a
/// [`TimeFile`](crate::TimeFile)
-pub fn load_file(path: &str) -> Option<TimeFile> {
+pub fn load_file(path: &str) -> Option<ParseOutput> {
let mut f = File::open(path).ok()?;
let mut content = String::new();
f.read_to_string(&mut content).ok()?;
@@ -19,11 +35,13 @@ pub fn load_file(path: &str) -> Option<TimeFile> {
let mut lines: Vec<String> = content.split("\n").map(|l| l.to_owned()).collect();
Some(
- lines
- .iter_mut()
- .map(|line| lexer::lex(line))
- .map(|lex| parser::parse(lex))
- .filter(|line| line.valid())
- .fold(TimeFile::default(), |file, line| file.append(line)),
+ ir::generate_ir(
+ lines
+ .iter_mut()
+ .map(|line| lexer::lex(line))
+ .map(|lex| parser::parse(lex)),
+ )
+ .into_iter()
+ .fold(ParseOutput::default(), |output, ir| output.append(ir)),
)
}
diff --git a/apps/cassiopeia/src/format/parser.rs b/apps/cassiopeia/src/format/parser.rs
index 430fee6332a7..d1f0bcdebc68 100644
--- a/apps/cassiopeia/src/format/parser.rs
+++ b/apps/cassiopeia/src/format/parser.rs
@@ -53,6 +53,9 @@ pub(crate) fn parse<'l>(lex: LineLexer<'l>) -> LineCfg {
(Stop(_), LineToken { tt: T::Date, slice }) => Stop(parse_datetime(slice)),
(Invoice(_), LineToken { tt: T::Date, slice }) => Invoice(parse_date(slice)),
+ // Pass empty lines through,
+ (Empty, _) => Empty,
+
// Ignore everything else (which will be filtered)
_ => Ignore,
})
diff --git a/apps/cassiopeia/src/lib.rs b/apps/cassiopeia/src/lib.rs
index 1ea50fb8bc17..d4ebf5355bb9 100644
--- a/apps/cassiopeia/src/lib.rs
+++ b/apps/cassiopeia/src/lib.rs
@@ -9,28 +9,32 @@
//! https://git.spacekookie.de/kookienomicon/tree/apps/cassiopeia
mod data;
+mod date;
mod format;
pub mod meta;
mod time;
-pub use data::{Session, TimeFile};
+pub use date::Date;
pub use format::load_file;
+pub use time::Time;
-/// A state handler for all cass interactions
+use data::{Invoice, Session, TimeFile};
+use format::{ir, IrStream, ParseOutput};
+
+/// A state handler and primary API for all cass interactions
+///
///
-/// This could be a stateless API, but I like being able to refer to
-/// fields that need to be saved for later here. This API wraps
-/// around [`TimeFile`](crate::TimeFile), so that you don't have to! ✨
pub struct Cassiopeia {
path: String,
tf: TimeFile,
+ ir: IrStream,
}
impl Cassiopeia {
/// Load a cass file from disk, parsing it into a [`TimeFile`](crate::TimeFile)
pub fn load(path: &str) -> Option<Self> {
let path = path.to_owned();
- load_file(path.as_str()).map(|tf| Self { path, tf })
+ load_file(path.as_str()).map(|ParseOutput { tf, ir }| Self { path, tf, ir })
}
/// Store the modified time file back to disk
@@ -40,11 +44,15 @@ impl Cassiopeia {
/// Start a new work session (with optional 15 minute rounding)
pub fn start(&mut self, round: bool) -> Option<()> {
- self.tf.start(round)
+ self.tf.start(round)?;
+
+ Some(())
}
/// Stop the existing work session (with optional 15 minute rounding)
pub fn stop(&mut self, round: bool) -> Option<()> {
+ self.tf.stop(round)?;
+
Some(())
}
@@ -96,7 +104,7 @@ impl<'cass> Invoicer<'cass> {
}
}
- /// S
+ /// Enable the invoice generation feature
pub fn generate(self) -> Self {
Self {
generate: true,
@@ -120,10 +128,12 @@ impl<'cass> Invoicer<'cass> {
pub fn run(self) -> Option<()> {
if self.generate {
- eprintln!("Integration with invoice(1) is currently not implemented. Sorry :()");
+ eprintln!("Integration with invoice(1) is currently not implemented. Sorry :(");
return None;
}
- None
+ self.tf.tf.invoice()?;
+
+ Some(())
}
}
diff --git a/apps/cassiopeia/src/time.rs b/apps/cassiopeia/src/time.rs
index 56804713033b..4ac4ce7db900 100644
--- a/apps/cassiopeia/src/time.rs
+++ b/apps/cassiopeia/src/time.rs
@@ -1,14 +1,32 @@
+use crate::Date;
use chrono::{
- DateTime, FixedOffset as Offset, Local, NaiveDateTime, NaiveTime, TimeZone, Timelike, Utc,
+ DateTime, Duration, FixedOffset as Offset, Local, NaiveDateTime, NaiveTime, TimeZone, Timelike,
+ Utc,
};
+use std::{cmp::Ordering, ops::Sub};
/// A convenience wrapper around [DateTime][t] with fixed timezone
///
/// [t]: chrono::DateTime
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Time {
inner: DateTime<Offset>,
}
+impl From<DateTime<Offset>> for Time {
+ fn from(inner: DateTime<Offset>) -> Self {
+ Self { inner }
+ }
+}
+
+impl<'t> Sub for &'t Time {
+ type Output = Duration;
+
+ fn sub(self, o: &'t Time) -> Self::Output {
+ self.inner - o.inner
+ }
+}
+
impl Time {
/// Get the current local time and pin it to a fixed Tz offset
pub fn now() -> Self {
@@ -18,6 +36,15 @@ impl Time {
}
}
+ pub(crate) fn date(&self) -> chrono::Date<Offset> {
+ self.inner.date()
+ }
+
+ /// Check if a time stamp happened _after_ a date
+ pub fn after(&self, date: &Date) -> bool {
+ &Date::from(self.date()) > date
+ }
+
#[cfg(test)]
pub(crate) fn fixed(hour: u32, min: u32, sec: u32) -> Self {
Self {