hypertask_engine 0.2.0

The core logic engine used to run hypertask
Documentation
use crate::error::*;
use crate::task::Task;
use chrono::prelude::*;
use std::collections::HashMap;
use std::rc::Rc;

#[derive(Clone)]
pub enum RPNSymbol {
    Add,
    Branch,
    Divide,
    Duplicate,
    Equal,
    GetEnvironment,
    GetProp,
    GetTag,
    GreaterThan,
    LessThan,
    Log,
    Multiply,
    Number(f64),
    Pow,
    Rem,
    Sqrt,
    Subtract,
    Swap,
    Symbol(String),
}

impl RPNSymbol {
    pub fn parse_programs(ss: &[String]) -> Vec<Self> {
        let mut acc = vec![];

        for s in ss {
            for symbol in s.split_whitespace() {
                acc.push(Self::parse(symbol));
            }
        }

        acc
    }

    pub fn parse_program(s: &str) -> Vec<Self> {
        s.split_whitespace().map(Self::parse).collect::<Vec<Self>>()
    }

    pub fn parse(s: &str) -> Self {
        use RPNSymbol::*;
        match s {
            "+" => Add,
            "?" => Branch,
            "/" => Divide,
            "&" => Duplicate,
            "=" => Equal,
            "$" => GetEnvironment,
            ":" => GetProp,
            "#" => GetTag,
            ">" => GreaterThan,
            "<" => LessThan,
            "~" => Log,
            "*" => Multiply,
            "^" => Pow,
            "%" => Rem,
            "_" => Sqrt,
            "@" => Swap,
            "-" => Subtract,
            x => match x.parse::<f64>() {
                Ok(n) => Number(n),
                Err(_) => Symbol(x.to_string()),
            },
        }
    }
}

fn sanitize_date_time(dt: &Option<DateTime<Utc>>) -> f64 {
    dt.map(|date_time| date_time.timestamp()).unwrap_or(0) as f64
}

macro_rules! stack_machine_binary_method {
    ($name:ident, $a: ident, $b: ident, $op:expr, $msg:expr) => {
        fn $name(&mut self) -> HyperTaskResult<()> {
            let $a = self.pop_number().map_err( |e| { HyperTaskError::new( HyperTaskErrorDomain::ScoreCalculator, HyperTaskErrorAction::Run).msg($msg).from(e) }) ?;
            let $b = self.pop_number().map_err( |e| { HyperTaskError::new( HyperTaskErrorDomain::ScoreCalculator, HyperTaskErrorAction::Run).msg($msg).from(e) }) ?;
            self.push_number($op)
        }
    };
}

macro_rules! stack_machine_unary_method {
    ($name:ident, $a: ident, $op:expr, $msg:expr) => {
        fn $name(&mut self) -> HyperTaskResult<()> {
            let $a = self.pop_number().map_err( |e| { HyperTaskError::new( HyperTaskErrorDomain::ScoreCalculator, HyperTaskErrorAction::Run).msg($msg).from(e) }) ?;
            self.push_number($op)
        }
    };
}

pub struct StackMachine {
    stack: Vec<RPNSymbol>,
    instructions: Rc<Vec<RPNSymbol>>,
    environment: HashMap<&'static str, f64>,
}

impl StackMachine {
    pub fn new(instructions: Vec<RPNSymbol>, environment: HashMap<&'static str, f64>) -> Self {
        Self {
            stack: Vec::with_capacity((instructions.len() as f64).sqrt() as usize),
            instructions: Rc::new(instructions),
            environment,
        }
    }

    fn pop(&mut self) -> HyperTaskResult<RPNSymbol> {
        self.stack.pop().ok_or_else(|| {
            HyperTaskError::new(
                HyperTaskErrorDomain::ScoreCalculator,
                HyperTaskErrorAction::Run,
            )
            .msg("tried to pop an empty stack")
        })
    }

    fn pop_number(&mut self) -> HyperTaskResult<f64> {
        let value = self.pop()?;

        if let RPNSymbol::Number(n) = value {
            Ok(n)
        } else {
            Err(HyperTaskError::new(
                HyperTaskErrorDomain::ScoreCalculator,
                HyperTaskErrorAction::Run,
            )
            .msg("popped value is not a number"))
        }
    }

    fn pop_symbol(&mut self) -> HyperTaskResult<String> {
        let value = self.pop()?;

        if let RPNSymbol::Symbol(s) = value {
            Ok(s)
        } else {
            Err(HyperTaskError::new(
                HyperTaskErrorDomain::ScoreCalculator,
                HyperTaskErrorAction::Run,
            )
            .msg("popped value is not a symbol"))
        }
    }

    fn push_number(&mut self, number: f64) -> HyperTaskResult<()> {
        self.stack.push(RPNSymbol::Number(number));
        Ok(())
    }

    stack_machine_unary_method!(run_sqrt, lhs, lhs.sqrt(), "could not sqrt");
    stack_machine_binary_method!(run_add, lhs, rhs, lhs + rhs, "could not add");
    stack_machine_binary_method!(run_divide, lhs, rhs, lhs / rhs, "could not divide");
    stack_machine_binary_method!(run_multiply, lhs, rhs, lhs * rhs, "could not multiply");
    stack_machine_binary_method!(run_subtract, lhs, rhs, lhs - rhs, "could not subtract");
    stack_machine_binary_method!(run_pow, lhs, rhs, lhs.powf(rhs), "could not pow");
    stack_machine_binary_method!(run_log, lhs, rhs, lhs.log(rhs), "could not get log");
    stack_machine_binary_method!(
        run_rem,
        lhs,
        rhs,
        lhs.rem_euclid(rhs),
        "could not get remainder"
    );
    stack_machine_binary_method!(
        run_equal,
        lhs,
        rhs,
        if (lhs - rhs).abs() < std::f64::EPSILON {
            1.0
        } else {
            0.0
        },
        "could not compare for equality"
    );
    stack_machine_binary_method!(
        run_greater_than,
        lhs,
        rhs,
        if lhs > rhs { 1.0 } else { 0.0 },
        "could not compare for greater than"
    );
    stack_machine_binary_method!(
        run_less_than,
        lhs,
        rhs,
        if lhs < rhs { 1.0 } else { 0.0 },
        "could not compare for less than"
    );

    fn run_duplicate(&mut self) -> HyperTaskResult<()> {
        let x = self.pop()?;
        self.stack.push(x.clone());
        self.stack.push(x.clone());
        Ok(())
    }

    fn run_get_prop(&mut self, task: &Task) -> HyperTaskResult<()> {
        let prop_name = self.pop_symbol()?;

        let replace = match prop_name.as_str() {
            "created_at" => task.get_created_at().timestamp() as f64,
            "done" => sanitize_date_time(task.get_done()),
            "due" => sanitize_date_time(task.get_due()),
            "snooze" => sanitize_date_time(task.get_snooze()),
            "updated_at" => task.get_updated_at().timestamp() as f64,
            "wait" => sanitize_date_time(task.get_wait()),

            _ => {
                return Err(HyperTaskError::new(
                    HyperTaskErrorDomain::ScoreCalculator,
                    HyperTaskErrorAction::Run,
                )
                .with_msg(|| format!("`{}` is not a valid prop name", &prop_name)));
            }
        };

        self.push_number(replace)
    }

    fn run_get_tag(&mut self, task: &Task) -> HyperTaskResult<()> {
        let tag_name = self.pop_symbol()?;

        let replace = if task.get_tags().contains(&tag_name) {
            1.0
        } else {
            0.0
        };

        self.push_number(replace)
    }

    fn run_get_environment(&mut self) -> HyperTaskResult<()> {
        let environment_name = self.pop_symbol()?;

        let replace = *self
            .environment
            .get(environment_name.as_str())
            .ok_or_else(|| {
                HyperTaskError::new(
                    HyperTaskErrorDomain::ScoreCalculator,
                    HyperTaskErrorAction::Run,
                )
                .with_msg(|| format!("`{}` is not a valid environment name", &environment_name))
            })?;

        self.push_number(replace)
    }

    fn run_swap(&mut self) -> HyperTaskResult<()> {
        let one = self.pop()?;
        let two = self.pop()?;
        self.stack.push(one);
        self.stack.push(two);
        Ok(())
    }

    fn run_branch(&mut self) -> HyperTaskResult<()> {
        let query = self.pop_number()?;
        let if_true = self.pop()?;
        let if_false = self.pop()?;

        let to_push = if query == 0.0 || query.is_infinite() || query.is_nan() {
            if_false
        } else {
            if_true
        };

        self.stack.push(to_push);

        Ok(())
    }

    pub fn run_on(&mut self, task: &Task) -> HyperTaskResult<f64> {
        self.stack.clear();

        for instruction in &*(self.instructions.clone()) {
            match instruction {
                RPNSymbol::Add => self.run_add(),
                RPNSymbol::Branch => self.run_branch(),
                RPNSymbol::Divide => self.run_divide(),
                RPNSymbol::Duplicate => self.run_duplicate(),
                RPNSymbol::Equal => self.run_equal(),
                RPNSymbol::GetEnvironment => self.run_get_environment(),
                RPNSymbol::GetProp => self.run_get_prop(task),
                RPNSymbol::GetTag => self.run_get_tag(task),
                RPNSymbol::GreaterThan => self.run_greater_than(),
                RPNSymbol::LessThan => self.run_less_than(),
                RPNSymbol::Log => self.run_log(),
                RPNSymbol::Multiply => self.run_multiply(),
                RPNSymbol::Pow => self.run_pow(),
                RPNSymbol::Rem => self.run_rem(),
                RPNSymbol::Sqrt => self.run_sqrt(),
                RPNSymbol::Swap => self.run_swap(),
                RPNSymbol::Subtract => self.run_subtract(),

                RPNSymbol::Number(n) => self.push_number(*n),
                RPNSymbol::Symbol(s) => {
                    self.stack.push(RPNSymbol::Symbol(s.to_string()));
                    Ok(())
                }
            }?
        }

        Ok(self.pop_number()?)
    }
}