Implement formula evaluation for DATE statement
This commit is contained in:
parent
c391d76690
commit
cb7c87b8b2
7 changed files with 237 additions and 30 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -101,6 +101,12 @@ dependencies = [
|
||||||
"vec_map",
|
"vec_map",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "computus"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9eaa21df489b1e50af464db43c1fc69f60492060ac7baae3996fe8a5f476790"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
|
@ -370,6 +376,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"computus",
|
||||||
"pest",
|
"pest",
|
||||||
"pest_derive",
|
"pest_derive",
|
||||||
"structopt",
|
"structopt",
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ edition = "2018"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.45"
|
anyhow = "1.0.45"
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
|
computus = "1.0.0"
|
||||||
pest = "2.1.3"
|
pest = "2.1.3"
|
||||||
pest_derive = "2.1.0"
|
pest_derive = "2.1.0"
|
||||||
structopt = "0.3.25"
|
structopt = "0.3.25"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,134 @@
|
||||||
use crate::files::commands::{self, Expr, Var};
|
use chrono::{Datelike, NaiveDate};
|
||||||
use crate::files::primitives::{Spanned, Time};
|
|
||||||
|
use crate::files::commands::{self, Command, Expr, Var};
|
||||||
|
use crate::files::primitives::{Spanned, Time, Weekday};
|
||||||
|
|
||||||
use super::super::command::CommandState;
|
use super::super::command::CommandState;
|
||||||
|
use super::super::date::Dates;
|
||||||
use super::super::delta::{Delta, DeltaStep};
|
use super::super::delta::{Delta, DeltaStep};
|
||||||
use super::super::Result;
|
use super::super::{util, DateRange, Error, Result};
|
||||||
|
|
||||||
|
fn b2i(b: bool) -> i64 {
|
||||||
|
if b {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn i2b(i: i64) -> bool {
|
||||||
|
i != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Var {
|
||||||
|
fn eval(self, date: NaiveDate) -> Result<i64> {
|
||||||
|
Ok(match self {
|
||||||
|
Var::True => 1,
|
||||||
|
Var::False => 0,
|
||||||
|
Var::Monday => 1,
|
||||||
|
Var::Tuesday => 2,
|
||||||
|
Var::Wednesday => 3,
|
||||||
|
Var::Thursday => 4,
|
||||||
|
Var::Friday => 5,
|
||||||
|
Var::Saturday => 6,
|
||||||
|
Var::Sunday => 7,
|
||||||
|
Var::JulianDay => date.num_days_from_ce().into(),
|
||||||
|
Var::Year => date.year().into(),
|
||||||
|
Var::YearLength => util::year_length(date.year()).into(),
|
||||||
|
Var::YearDay => date.ordinal().into(),
|
||||||
|
Var::YearDayReverse => (util::year_length(date.year()) - date.ordinal0()).into(),
|
||||||
|
Var::YearWeek => (date.ordinal0().div_euclid(7) + 1).into(),
|
||||||
|
Var::YearWeekReverse => {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let yD = util::year_length(date.year()) - date.ordinal();
|
||||||
|
(yD.div_euclid(7) + 1).into()
|
||||||
|
}
|
||||||
|
Var::Month => date.month().into(),
|
||||||
|
Var::MonthLength => util::month_length(date.year(), date.month()).into(),
|
||||||
|
Var::MonthWeek => (date.month0().div_euclid(7) + 1).into(),
|
||||||
|
Var::MonthWeekReverse => {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let mD = util::month_length(date.year(), date.month()) - date.month0();
|
||||||
|
(mD.div_euclid(7) + 1).into()
|
||||||
|
}
|
||||||
|
Var::Day => date.day().into(),
|
||||||
|
Var::DayReverse => {
|
||||||
|
let ml = util::month_length(date.year(), date.month());
|
||||||
|
(ml - date.month0()).into()
|
||||||
|
}
|
||||||
|
Var::IsoYear => date.iso_week().year().into(),
|
||||||
|
Var::IsoYearLength => util::iso_year_length(date.iso_week().year()).into(),
|
||||||
|
Var::IsoWeek => date.iso_week().week().into(),
|
||||||
|
Var::Weekday => {
|
||||||
|
let wd: Weekday = date.weekday().into();
|
||||||
|
wd.num().into()
|
||||||
|
}
|
||||||
|
Var::Easter => {
|
||||||
|
let e = computus::gregorian(date.year()).map_err(|e| Error::Easter {
|
||||||
|
span: todo!(),
|
||||||
|
date,
|
||||||
|
msg: e,
|
||||||
|
})?;
|
||||||
|
NaiveDate::from_ymd(e.year, e.month, e.day).ordinal().into()
|
||||||
|
}
|
||||||
|
Var::IsWeekday => {
|
||||||
|
let wd: Weekday = date.weekday().into();
|
||||||
|
b2i(!wd.is_weekend())
|
||||||
|
}
|
||||||
|
Var::IsWeekend => {
|
||||||
|
let wd: Weekday = date.weekday().into();
|
||||||
|
b2i(wd.is_weekend())
|
||||||
|
}
|
||||||
|
Var::IsLeapYear => b2i(util::is_leap_year(date.year())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Expr {
|
||||||
|
fn eval(&self, date: NaiveDate) -> Result<i64> {
|
||||||
|
Ok(match self {
|
||||||
|
Expr::Lit(l) => *l,
|
||||||
|
Expr::Var(v) => v.eval(date)?,
|
||||||
|
Expr::Paren(e) => e.eval(date)?,
|
||||||
|
Expr::Neg(e) => -e.eval(date)?,
|
||||||
|
Expr::Add(a, b) => a.eval(date)? + b.eval(date)?,
|
||||||
|
Expr::Sub(a, b) => a.eval(date)? - b.eval(date)?,
|
||||||
|
Expr::Mul(a, b) => a.eval(date)? * b.eval(date)?,
|
||||||
|
Expr::Div(a, b) => {
|
||||||
|
let b = b.eval(date)?;
|
||||||
|
if b == 0 {
|
||||||
|
return Err(Error::DivByZero {
|
||||||
|
span: todo!(),
|
||||||
|
date,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
a.eval(date)?.div_euclid(b)
|
||||||
|
}
|
||||||
|
Expr::Mod(a, b) => {
|
||||||
|
let b = b.eval(date)?;
|
||||||
|
if b == 0 {
|
||||||
|
return Err(Error::ModByZero {
|
||||||
|
span: todo!(),
|
||||||
|
date,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
a.eval(date)?.rem_euclid(b)
|
||||||
|
}
|
||||||
|
Expr::Eq(a, b) => b2i(a.eval(date)? == b.eval(date)?),
|
||||||
|
Expr::Neq(a, b) => b2i(a.eval(date)? != b.eval(date)?),
|
||||||
|
Expr::Lt(a, b) => b2i(a.eval(date)? < b.eval(date)?),
|
||||||
|
Expr::Lte(a, b) => b2i(a.eval(date)? <= b.eval(date)?),
|
||||||
|
Expr::Gt(a, b) => b2i(a.eval(date)? > b.eval(date)?),
|
||||||
|
Expr::Gte(a, b) => b2i(a.eval(date)? >= b.eval(date)?),
|
||||||
|
Expr::Not(e) => b2i(!i2b(e.eval(date)?)),
|
||||||
|
Expr::And(a, b) => b2i(i2b(a.eval(date)?) && i2b(b.eval(date)?)),
|
||||||
|
Expr::Or(a, b) => b2i(i2b(a.eval(date)?) || i2b(b.eval(date)?)),
|
||||||
|
Expr::Xor(a, b) => b2i(i2b(a.eval(date)?) ^ i2b(b.eval(date)?)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct FormulaSpec {
|
pub struct FormulaSpec {
|
||||||
// TODO Implement more efficient exprs and expr evaluation
|
|
||||||
pub start: Expr,
|
pub start: Expr,
|
||||||
pub start_delta: Delta,
|
pub start_delta: Delta,
|
||||||
pub start_time: Option<Time>,
|
pub start_time: Option<Time>,
|
||||||
|
|
@ -78,8 +200,48 @@ impl From<&commands::WeekdaySpec> for FormulaSpec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CommandState<'a> {
|
impl FormulaSpec {
|
||||||
pub fn eval_formula_spec(&mut self, spec: FormulaSpec) -> Result<()> {
|
fn range(&self, s: &CommandState<'_>) -> Option<DateRange> {
|
||||||
todo!()
|
let mut range = s
|
||||||
|
.range
|
||||||
|
.expand_by(&self.end_delta)
|
||||||
|
.move_by(&self.start_delta);
|
||||||
|
|
||||||
|
if let Command::Task(_) = s.command.command {
|
||||||
|
if let Some(last_done) = s.last_done() {
|
||||||
|
range = range.with_from(last_done.succ())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.limit_from_until(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dates(&self, start: NaiveDate) -> Result<Dates> {
|
||||||
|
let root = self.start_delta.apply_date(start)?;
|
||||||
|
Ok(if let Some(root_time) = self.start_time {
|
||||||
|
let (other, other_time) = self.end_delta.apply_date_time(root, root_time)?;
|
||||||
|
Dates::new_with_time(root, root_time, other, other_time)
|
||||||
|
} else {
|
||||||
|
let other = self.end_delta.apply_date(root)?;
|
||||||
|
Dates::new(root, other)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval(&self, date: NaiveDate) -> Result<bool> {
|
||||||
|
Ok(i2b(self.start.eval(date)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> CommandState<'a> {
|
||||||
|
pub fn eval_formula_spec(&mut self, spec: FormulaSpec) -> Result<()> {
|
||||||
|
if let Some(range) = spec.range(self) {
|
||||||
|
for day in range.days() {
|
||||||
|
if spec.eval(day)? {
|
||||||
|
let dates = spec.dates(day)?;
|
||||||
|
self.add(self.kind(), Some(dates));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use crate::files::primitives::{Span, Time};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
/// A delta step resulted in an invalid date.
|
||||||
DeltaInvalidStep {
|
DeltaInvalidStep {
|
||||||
span: Span,
|
span: Span,
|
||||||
start: NaiveDate,
|
start: NaiveDate,
|
||||||
|
|
@ -13,6 +14,7 @@ pub enum Error {
|
||||||
prev: NaiveDate,
|
prev: NaiveDate,
|
||||||
prev_time: Option<Time>,
|
prev_time: Option<Time>,
|
||||||
},
|
},
|
||||||
|
/// A time-based delta step was applied to a date without time.
|
||||||
DeltaNoTime {
|
DeltaNoTime {
|
||||||
span: Span,
|
span: Span,
|
||||||
start: NaiveDate,
|
start: NaiveDate,
|
||||||
|
|
@ -29,6 +31,16 @@ pub enum Error {
|
||||||
/// A `MOVE a TO b` statement was executed, but there was no entry at the
|
/// A `MOVE a TO b` statement was executed, but there was no entry at the
|
||||||
/// date `a`.
|
/// date `a`.
|
||||||
MoveWithoutSource { span: Span },
|
MoveWithoutSource { span: Span },
|
||||||
|
/// A division by zero has occurred.
|
||||||
|
DivByZero { span: Span, date: NaiveDate },
|
||||||
|
/// A modulo operation by zero has occurred.
|
||||||
|
ModByZero { span: Span, date: NaiveDate },
|
||||||
|
/// Easter calculation failed.
|
||||||
|
Easter {
|
||||||
|
span: Span,
|
||||||
|
date: NaiveDate,
|
||||||
|
msg: &'static str,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = result::Result<T, Error>;
|
pub type Result<T> = result::Result<T, Error>;
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ impl DateRange {
|
||||||
self.until
|
self.until
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn days(&self) -> impl Iterator<Item = NaiveDate> {
|
||||||
|
(self.from.num_days_from_ce()..=self.until.num_days_from_ce())
|
||||||
|
.map(|n| NaiveDate::from_num_days_from_ce(n))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn years(&self) -> RangeInclusive<i32> {
|
pub fn years(&self) -> RangeInclusive<i32> {
|
||||||
self.from.year()..=self.until.year()
|
self.from.year()..=self.until.year()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
use chrono::{Datelike, NaiveDate};
|
use chrono::{Datelike, NaiveDate, Weekday};
|
||||||
|
|
||||||
pub fn is_leap_year(year: i32) -> bool {
|
pub fn is_leap_year(year: i32) -> bool {
|
||||||
NaiveDate::from_ymd_opt(year, 2, 29).is_some()
|
NaiveDate::from_ymd_opt(year, 2, 29).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_months(year: i32, month: u32, delta: i32) -> (i32, u32) {
|
pub fn is_iso_leap_year(year: i32) -> bool {
|
||||||
let month0 = (month as i32) - 1 + delta;
|
NaiveDate::from_isoywd_opt(year, 53, Weekday::Sun).is_some()
|
||||||
let year = year + month0.div_euclid(12);
|
}
|
||||||
let month = month0.rem_euclid(12) as u32 + 1;
|
|
||||||
(year, month)
|
pub fn year_length(year: i32) -> u32 {
|
||||||
|
NaiveDate::from_ymd(year, 12, 31).ordinal()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn month_length(year: i32, month: u32) -> u32 {
|
pub fn month_length(year: i32, month: u32) -> u32 {
|
||||||
|
|
@ -17,3 +18,18 @@ pub fn month_length(year: i32, month: u32) -> u32 {
|
||||||
.pred()
|
.pred()
|
||||||
.day()
|
.day()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn iso_year_length(year: i32) -> u32 {
|
||||||
|
if is_iso_leap_year(year) {
|
||||||
|
53 * 7
|
||||||
|
} else {
|
||||||
|
52 * 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_months(year: i32, month: u32, delta: i32) -> (i32, u32) {
|
||||||
|
let month0 = (month as i32) - 1 + delta;
|
||||||
|
let year = year + month0.div_euclid(12);
|
||||||
|
let month = month0.rem_euclid(12) as u32 + 1;
|
||||||
|
(year, month)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,32 +129,36 @@ impl From<chrono::Weekday> for Weekday {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Weekday {
|
impl Weekday {
|
||||||
pub fn name(&self) -> &'static str {
|
pub fn name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Weekday::Monday => "mon",
|
Self::Monday => "mon",
|
||||||
Weekday::Tuesday => "tue",
|
Self::Tuesday => "tue",
|
||||||
Weekday::Wednesday => "wed",
|
Self::Wednesday => "wed",
|
||||||
Weekday::Thursday => "thu",
|
Self::Thursday => "thu",
|
||||||
Weekday::Friday => "fri",
|
Self::Friday => "fri",
|
||||||
Weekday::Saturday => "sat",
|
Self::Saturday => "sat",
|
||||||
Weekday::Sunday => "sun",
|
Self::Sunday => "sun",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn num(&self) -> u8 {
|
pub fn num(self) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
Weekday::Monday => 1,
|
Self::Monday => 1,
|
||||||
Weekday::Tuesday => 2,
|
Self::Tuesday => 2,
|
||||||
Weekday::Wednesday => 3,
|
Self::Wednesday => 3,
|
||||||
Weekday::Thursday => 4,
|
Self::Thursday => 4,
|
||||||
Weekday::Friday => 5,
|
Self::Friday => 5,
|
||||||
Weekday::Saturday => 6,
|
Self::Saturday => 6,
|
||||||
Weekday::Sunday => 7,
|
Self::Sunday => 7,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_weekend(self) -> bool {
|
||||||
|
matches!(self, Self::Saturday | Self::Sunday)
|
||||||
|
}
|
||||||
|
|
||||||
/// How many days from now until the other weekday.
|
/// How many days from now until the other weekday.
|
||||||
pub fn until(&self, other: Self) -> u8 {
|
pub fn until(self, other: Self) -> u8 {
|
||||||
let num_self = self.num();
|
let num_self = self.num();
|
||||||
let num_other = other.num();
|
let num_other = other.num();
|
||||||
if num_self <= num_other {
|
if num_self <= num_other {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue