truth-table 0.2.0

Generate a truth table from a formula
//! Convert raw text to a linear token stream

use std::{
    fmt::{self, Display, Write},
    ops::Deref,
};

use logos::Logos;

#[allow(missing_docs)]
#[derive(Debug, PartialEq, Logos)]
pub enum Token {
    #[regex("[a-zA-Z]+", |lex| lex.slice().chars().next().unwrap())]
    Ident(char),
    #[token("(")]
    OpenParen,
    #[token(")")]
    CloseParen,
    #[token("not")]
    #[token("!")]
    Not,
    #[token("->")]
    Implication,
    #[token("and")]
    #[token("&")]
    #[token("&&")]
    And,
    #[token("or")]
    #[token("|")]
    #[token("||")]
    Or,
    #[token("=")]
    #[token("==")]
    Equals,
    #[token("!=")]
    NotEquals,
    #[token("0")]
    False,
    #[token("1")]
    True,
    #[error]
    #[regex(r"[ \t\n]", logos::skip)]
    Error,
}

#[derive(Debug)]
pub struct Tokens {
    tokens: Vec<Token>,
}

impl Tokens {
    pub fn from_text(text: &str) -> Self {
        Self {
            tokens: Token::lexer(text).collect(),
        }
    }
}

impl Deref for Tokens {
    type Target = [Token];
    fn deref(&self) -> &Self::Target {
        &self.tokens
    }
}

impl Display for Tokens {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for token in self.tokens.iter() {
            match token {
                Token::Ident(c) => f.write_char(*c),
                Token::OpenParen => f.write_char('('),
                Token::CloseParen => f.write_char(')'),
                Token::Not => f.write_char('¬'),
                Token::Implication => f.write_str(""),
                Token::And => f.write_str(""),
                Token::Or => f.write_str(""),
                Token::Equals => f.write_str(" = "),
                Token::NotEquals => f.write_str(""),
                Token::False => f.write_char('0'),
                Token::True => f.write_char('1'),
                Token::Error => continue,
            }?
        }
        Ok(())
    }
}

pub fn rposition_if_not_in_paren(
    tokens: &[Token],
    needle: Token,
) -> Option<usize> {
    debug_assert!(!matches!(needle, Token::OpenParen | Token::CloseParen));
    let mut open_parens = 0;
    for (i, token) in tokens.iter().enumerate().rev() {
        match token {
            Token::OpenParen => open_parens += 1,
            Token::CloseParen => open_parens -= 1,
            _ if open_parens == 0 && token == &needle => return Some(i),
            _ => (),
        }
    }
    assert_eq!(open_parens, 0, "Non matching parentheses");
    None
}

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

    #[test]
    fn implication() {
        let s = "a -> b";
        assert_eq!(
            Tokens::from_text(s).tokens,
            vec![Token::Ident('a'), Token::Implication, Token::Ident('b')]
        );
    }

    #[test]
    fn equals() {
        let s = "a == b";
        assert_eq!(
            Tokens::from_text(s).tokens,
            vec![Token::Ident('a'), Token::Equals, Token::Ident('b')]
        );
    }

    #[test]
    fn not_equals() {
        let s = "a != b";
        assert_eq!(
            Tokens::from_text(s).tokens,
            vec![Token::Ident('a'), Token::NotEquals, Token::Ident('b')]
        );
    }
}