use std::collections::HashMap; use chrono::NaiveDate; use crate::files::commands::{ self, BirthdaySpec, Command, Done, DoneDate, Note, Spec, Statement, Task, }; use crate::files::primitives::{Span, Spanned}; use crate::files::SourcedCommand; use super::date::Dates; use super::delta::Delta; use super::{DateRange, Entry, EntryKind, Error, Result}; mod birthday; mod date; mod formula; pub struct CommandState<'a> { command: SourcedCommand<'a>, range: DateRange, from: Option, until: Option, remind: Option>, dated: HashMap, undated: Vec, } impl<'a> CommandState<'a> { pub fn new(command: SourcedCommand<'a>, mut range: DateRange) -> Self { // If we don't calculate entries for the source of the move command, it // fails even though the user did nothing wrong. Also, move commands (or // chains thereof) may move an initially out-of-range entry into range. // // To fix this, we just expand the range to contain all move command // sources. This is a quick fix, but until it becomes a performance // issue (if ever), it's probably fine. for statement in command.command.statements() { if let Statement::Move { from, .. } = statement { range = range.containing(*from) } } Self { range, command, from: None, until: None, remind: None, dated: HashMap::new(), undated: Vec::new(), } } pub fn eval(mut self) -> Result { match self.command.command { Command::Task(task) => self.eval_task(task)?, Command::Note(note) => self.eval_note(note)?, } Ok(self) } pub fn entries(self) -> Vec { self.undated .into_iter() .chain(self.dated.into_values()) .collect() } // Helper functions fn kind(&self) -> EntryKind { match self.command.command { Command::Task(_) => EntryKind::Task, Command::Note(_) => EntryKind::Note, } } fn range_with_remind(&self) -> DateRange { match &self.remind { None => self.range, Some(delta) => self.range.expand_by(&delta.value), } } /// Last root date mentioned in any `DONE`. fn last_done_root(&self) -> Option { match self.command.command { Command::Task(task) => task .done .iter() .filter_map(|done| done.date.map(DoneDate::root)) .max(), Command::Note(_) => None, } } /// Last completion date mentioned in any `DONE`. fn last_done_completion(&self) -> Option { match self.command.command { Command::Task(task) => task.done.iter().map(|done| done.done_at).max(), Command::Note(_) => None, } } fn limit_from_until(&self, range: DateRange) -> Option { let range_from = range.from(); let from = self .from .filter(|&from| from > range_from) .unwrap_or(range_from); let range_until = range.until(); let until = self .until .filter(|&until| until < range_until) .unwrap_or(range_until); if from <= until { Some(DateRange::new(from, until)) } else { None } } fn entry_with_remind(&self, kind: EntryKind, dates: Option) -> Result { let remind = if let (Some(dates), Some(delta)) = (dates, &self.remind) { let index = self.command.source.file(); let start = dates.sorted().root(); let remind = delta.value.apply_date(index, dates.sorted().root())?; if remind >= start { return Err(Error::RemindDidNotMoveBackwards { index, span: delta.span, from: start, to: remind, }); } Some(remind) } else { None }; Ok(Entry::new(self.command.source, kind, dates, remind)) } /// Add an entry, respecting [`Self::from`] and [`Self::until`]. Does not /// overwrite existing entries if a root date is specified. fn add(&mut self, entry: Entry) { if let Some(dates) = entry.dates { let root = dates.root(); if let Some(from) = self.from { if root < from { return; } } if let Some(until) = self.until { if until < root { return; } } self.dated.entry(root).or_insert(entry); } else { self.undated.push(entry); } } /// Add an entry, ignoring [`Self::from`] and [`Self::until`]. Always /// overwrites existing entries if a root date is specified. fn add_forced(&mut self, entry: Entry) { if let Some(dates) = entry.dates { self.dated.insert(dates.root(), entry); } else { self.undated.push(entry); } } // Actual evaluation fn has_date_stmt(statements: &[Statement]) -> bool { statements .iter() .any(|s| matches!(s, Statement::Date(_) | Statement::BDate(_))) } fn eval_task(&mut self, task: &Task) -> Result<()> { if Self::has_date_stmt(&task.statements) { for statement in &task.statements { self.eval_statement(statement)?; } } else if task.done.is_empty() { self.add(self.entry_with_remind(self.kind(), None)?); } for done in &task.done { self.eval_done(done)?; } Ok(()) } fn eval_note(&mut self, note: &Note) -> Result<()> { if Self::has_date_stmt(¬e.statements) { for statement in ¬e.statements { self.eval_statement(statement)?; } } else { self.add(self.entry_with_remind(self.kind(), None)?); } Ok(()) } fn eval_statement(&mut self, statement: &Statement) -> Result<()> { match statement { Statement::Date(spec) => self.eval_date(spec)?, Statement::BDate(spec) => self.eval_bdate(spec)?, Statement::From(date) => self.from = *date, Statement::Until(date) => self.until = *date, Statement::Except(date) => self.eval_except(*date), Statement::Move { span, from, to } => self.eval_move(*span, *from, *to)?, Statement::Remind(delta) => self.eval_remind(delta), } Ok(()) } fn eval_date(&mut self, spec: &Spec) -> Result<()> { match spec { Spec::Date(spec) => self.eval_date_spec(spec.into()), Spec::Weekday(spec) => self.eval_formula_spec(spec.into()), Spec::Formula(spec) => self.eval_formula_spec(spec.into()), } } fn eval_bdate(&mut self, spec: &BirthdaySpec) -> Result<()> { self.eval_birthday_spec(spec) } fn eval_except(&mut self, date: NaiveDate) { self.dated.remove(&date); } fn eval_move(&mut self, span: Span, from: NaiveDate, to: NaiveDate) -> Result<()> { if let Some(mut entry) = self.dated.remove(&from) { if let Some(dates) = entry.dates { let delta = to - from; entry.dates = Some(dates.move_by(delta)); } self.dated.insert(to, entry); Ok(()) } else { Err(Error::MoveWithoutSource { index: self.command.source.file(), span, }) } } fn eval_remind(&mut self, delta: &Option>) { if let Some(delta) = delta { self.remind = Some(Spanned::new(delta.span, (&delta.value).into())); } else { self.remind = None; } } fn eval_done(&mut self, done: &Done) -> Result<()> { self.add_forced(self.entry_with_remind( EntryKind::TaskDone(done.done_at), done.date.map(|date| date.into()), )?); Ok(()) } }