truth-table 0.2.0

Generate a truth table from a formula
//! Evaluate the final truth value of an expression

use crate::{
    lexer::{Token, Tokens},
    parse::Expr,
};

pub struct Equation {
    root: Expr,
    pub idents: Vec<char>,
}

impl Equation {
    pub fn from_tokens(tokens: &[Token]) -> Self {
        let root = Expr::from_tokens(tokens);
        let mut idents = Vec::new();
        for token in tokens {
            if let Token::Ident(ident) = token {
                // Dont store the same identifiers mutliple times
                if !idents.contains(ident) {
                    idents.push(*ident);
                }
            }
        }
        idents.sort_unstable();
        Self { root, idents }
    }

    #[allow(dead_code)]
    pub fn from_text(text: &str) -> Self {
        let tokens = Tokens::from_text(text);
        Self::from_tokens(&tokens)
    }

    pub fn eval_all(&self) -> Vec<(Vec<(char, bool)>, bool)> {
        let combinations: u64 = 1 << self.idents.len();
        let mut result = Vec::with_capacity(combinations as usize);
        // Get all boolean combinations by counting in binary
        for bits in 0..combinations {
            let mut mapping: Vec<(char, bool)> =
                Vec::with_capacity(self.idents.len());
            for i in (0..(self.idents.len())).rev() {
                mapping.push((
                    self.idents[self.idents.len() - i - 1],
                    ((bits >> i) & 1) != 0,
                ));
            }
            let eval_val = eval_expr_with(&self.root, &mapping);
            result.push((mapping, eval_val));
        }
        result
    }
}

/// Evaluates the final boolean value given a mapping from an identifier to its boolean value
fn eval_expr_with(expr: &Expr, mapping: &[(char, bool)]) -> bool {
    match expr {
        Expr::Ident(ident) => mapping
            .iter()
            .find(|(id, _)| id == ident)
            .map(|(_, val)| *val)
            .unwrap(), // If unwrapping fails here the given mapping was incomplete
        Expr::And(val) => {
            eval_expr_with(&val.0, mapping) && eval_expr_with(&val.1, mapping)
        }
        Expr::Or(val) => {
            eval_expr_with(&val.0, mapping) || eval_expr_with(&val.1, mapping)
        }
        Expr::Equals(val) => {
            eval_expr_with(&val.0, mapping) == eval_expr_with(&val.1, mapping)
        }
        Expr::NotEquals(val) => {
            eval_expr_with(&val.0, mapping) != eval_expr_with(&val.1, mapping)
        }
        Expr::Not(val) => !eval_expr_with(val, mapping),
        Expr::Implication(val) => {
            if eval_expr_with(&val.0, mapping) {
                eval_expr_with(&val.1, mapping)
            } else {
                true
            }
        }
        Expr::True => true,
        Expr::False => false,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn constants() {
        let equation = Equation::from_text("1 or 0");
        assert_eq!(eval_expr_with(&equation.root, &[]), true);
    }

    #[test]
    fn find_idents() {
        let equation = Equation::from_text("a or b and c and not d");
        assert_eq!(equation.idents, vec!['a', 'b', 'c', 'd']);
    }

    #[test]
    fn eval_with() {
        let equation = Equation::from_text("a or b");
        // true or false = true
        assert_eq!(
            eval_expr_with(&equation.root, &vec![('a', true), ('b', false)]),
            true
        );
        // false or false = false
        assert_eq!(
            eval_expr_with(&equation.root, &vec![('a', false), ('b', false)]),
            false
        );
    }

    #[test]
    fn eval_all() {
        let equation = Equation::from_text("a or b");
        assert_eq!(
            equation.eval_all(),
            vec![
                (vec![('a', false), ('b', false)], false),
                (vec![('a', false), ('b', true)], true),
                (vec![('a', true), ('b', false)], true),
                (vec![('a', true), ('b', true)], true),
            ]
        )
    }

    #[test]
    fn implication() {
        let equation = Equation::from_text("a -> b");
        assert_eq!(
            equation.eval_all(),
            vec![
                (vec![('a', false), ('b', false)], true),
                (vec![('a', false), ('b', true)], true),
                (vec![('a', true), ('b', false)], false),
                (vec![('a', true), ('b', true)], true),
            ]
        )
    }

    #[test]
    fn equals() {
        let equation = Equation::from_text("a==b");
        assert_eq!(
            equation.eval_all(),
            vec![
                (vec![('a', false), ('b', false)], true),
                (vec![('a', false), ('b', true)], false),
                (vec![('a', true), ('b', false)], false),
                (vec![('a', true), ('b', true)], true),
            ]
        )
    }

    #[test]
    fn not_equals() {
        let equation = Equation::from_text("a!=b");
        assert_eq!(
            equation.eval_all(),
            vec![
                (vec![('a', false), ('b', false)], false),
                (vec![('a', false), ('b', true)], true),
                (vec![('a', true), ('b', false)], true),
                (vec![('a', true), ('b', true)], false),
            ]
        )
    }
}