poreader 1.1.2

Read translation catalogs in PO format.
Documentation
mod node;

use crate::error::Error;
use lalrpop_util::{lalrpop_mod, ParseError};

lalrpop_mod!(formula, "/plural/formula/formula.rs");

#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct Formula {
    expr: node::Node,
}

impl Formula {
    pub(super) fn parse(input: &str) -> Result<Self, Error> {
        let input = input.trim();

        if input.is_empty() {
            return Ok(Formula { expr: node::Node::Var });
        }

        let parser = formula::FormulaParser::new();
        let res: Result<node::Node, ParseError<_, _, _>> = parser.parse(input);

        match res {
            Ok(expr) => Ok(Formula { expr }),
            Err(err) => Err(Error::PluralForms(err.to_string())),
        }
    }

    pub(super) fn execute(&self, count: usize) -> Option<usize> {
        let res = self.expr.execute(count as i64);

        if res < 0 {
            None
        } else {
            Some(res as usize)
        }
    }
}

// no-coverage:start
#[cfg(test)]
mod tests {
    use super::{
        node::{BinOp, Node, UnOp},
        Formula,
    };
    use std::collections::HashMap;

    struct TestCase {
        test_name: &'static str,
        source: &'static str,
        has_error: bool,
        node: Node,
        count_tests: HashMap<usize, Option<usize>>,
    }

    impl TestCase {
        fn make_tests() -> Vec<Self> {
            vec![
                TestCase {
                    test_name: "Empty string",
                    source: "",
                    has_error: false,
                    node: Node::Var,
                    count_tests: vec![(100, Some(100))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Variable",
                    source: "n",
                    has_error: false,
                    node: Node::Var,
                    count_tests: vec![(100, Some(100))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Constant",
                    source: "100",
                    has_error: false,
                    node: Node::new_num(100),
                    count_tests: vec![(0, Some(100)), (5, Some(100)), (100, Some(100))]
                        .into_iter()
                        .collect(),
                },
                TestCase {
                    test_name: "Zero",
                    source: "0",
                    has_error: false,
                    node: Node::new_num(0),
                    count_tests: vec![(0, Some(0)), (5, Some(0)), (100, Some(0))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Unrecognized",
                    source: "azerty",
                    has_error: true,
                    node: Node::Var,
                    count_tests: HashMap::new(),
                },
                TestCase {
                    test_name: "Operator `+`",
                    source: "n + 10",
                    has_error: false,
                    node: Node::new_binop(BinOp::Add, Node::Var, Node::new_num(10)),
                    count_tests: vec![(100, Some(110))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Operator `+` (with error)",
                    source: "n + ",
                    has_error: true,
                    node: Node::Var,
                    count_tests: HashMap::new(),
                },
                TestCase {
                    test_name: "Operator `-`",
                    source: "n - 10",
                    has_error: false,
                    node: Node::new_binop(BinOp::Sub, Node::Var, Node::new_num(10)),
                    count_tests: vec![(5, None), (10, Some(0)), (100, Some(90))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Operator `*`",
                    source: "n * 10",
                    has_error: false,
                    node: Node::new_binop(BinOp::Mul, Node::Var, Node::new_num(10)),
                    count_tests: vec![(0, Some(0)), (5, Some(50))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Operator `/`",
                    source: "n / 10",
                    has_error: false,
                    node: Node::new_binop(BinOp::Div, Node::Var, Node::new_num(10)),
                    count_tests: vec![(0, Some(0)), (20, Some(2)), (35, Some(3))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Operator `%`",
                    source: "n % 10",
                    has_error: false,
                    node: Node::new_binop(BinOp::Mod, Node::Var, Node::new_num(10)),
                    count_tests: vec![(0, Some(0)), (23, Some(3)), (35, Some(5))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Operator `==`",
                    source: "n == 10",
                    has_error: false,
                    node: Node::new_binop(BinOp::Eq, Node::Var, Node::new_num(10)),
                    count_tests: vec![(100, Some(0)), (10, Some(1))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Operator `!=`",
                    source: "n != 10",
                    has_error: false,
                    node: Node::new_binop(BinOp::Ne, Node::Var, Node::new_num(10)),
                    count_tests: vec![(2, Some(1)), (100, Some(1)), (10, Some(0))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Operator `<`",
                    source: "n < 10",
                    has_error: false,
                    node: Node::new_binop(BinOp::Lt, Node::Var, Node::new_num(10)),
                    count_tests: vec![(2, Some(1)), (100, Some(0)), (10, Some(0))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Operator `<=`",
                    source: "n <= 10",
                    has_error: false,
                    node: Node::new_binop(BinOp::Lte, Node::Var, Node::new_num(10)),
                    count_tests: vec![(2, Some(1)), (100, Some(0)), (10, Some(1))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Operator `>`",
                    source: "n > 10",
                    has_error: false,
                    node: Node::new_binop(BinOp::Gt, Node::Var, Node::new_num(10)),
                    count_tests: vec![(2, Some(0)), (100, Some(1)), (10, Some(0))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Operator `>=`",
                    source: "n >= 10",
                    has_error: false,
                    node: Node::new_binop(BinOp::Gte, Node::Var, Node::new_num(10)),
                    count_tests: vec![(2, Some(0)), (100, Some(1)), (10, Some(1))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Operator `!` (not)",
                    source: "!n",
                    has_error: false,
                    node: Node::new_unop(UnOp::Not, Node::Var),
                    count_tests: vec![(100, Some(0)), (0, Some(1))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Operator 'neg'",
                    source: "-n",
                    has_error: false,
                    node: Node::new_unop(UnOp::Neg, Node::Var),
                    count_tests: vec![(100, None), (0, Some(0))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Operator `&&`",
                    source: "(5 < n) && n <= 25",
                    has_error: false,
                    node: Node::new_binop(
                        BinOp::And,
                        Node::new_binop(BinOp::Lt, Node::new_num(5), Node::Var),
                        Node::new_binop(BinOp::Lte, Node::Var, Node::new_num(25)),
                    ),
                    count_tests: vec![(100, Some(0)), (0, Some(0)), (5, Some(0)), (10, Some(1)), (25, Some(1))]
                        .into_iter()
                        .collect(),
                },
                TestCase {
                    test_name: "Operator `||`",
                    source: "(5 >= n) || n > 25",
                    has_error: false,
                    node: Node::new_binop(
                        BinOp::Or,
                        Node::new_binop(BinOp::Gte, Node::new_num(5), Node::Var),
                        Node::new_binop(BinOp::Gt, Node::Var, Node::new_num(25)),
                    ),
                    count_tests: vec![(100, Some(1)), (0, Some(1)), (5, Some(1)), (10, Some(0)), (25, Some(0))]
                        .into_iter()
                        .collect(),
                },
                TestCase {
                    test_name: "Expression with `?`",
                    source: "n < 10 ? 1 : 2",
                    has_error: false,
                    node: Node::new_cond(
                        Node::new_binop(BinOp::Lt, Node::Var, Node::new_num(10)),
                        Node::new_num(1),
                        Node::new_num(2),
                    ),
                    count_tests: vec![(100, Some(2)), (0, Some(1)), (10, Some(2))].into_iter().collect(),
                },
                TestCase {
                    test_name: "Big expression",
                    source: "n > 10 ? (n % 10) == 3 ? 10 : n < 100 ? 20 : (!(n > 200) ? -n + 1000 : 1234) : n - 10",
                    has_error: false,
                    node: Node::new_cond(
                        Node::new_binop(BinOp::Gt, Node::Var, Node::new_num(10)),
                        Node::new_cond(
                            Node::new_binop(
                                BinOp::Eq,
                                Node::new_binop(BinOp::Mod, Node::Var, Node::new_num(10)),
                                Node::new_num(3),
                            ),
                            Node::new_num(10),
                            Node::new_cond(
                                Node::new_binop(BinOp::Lt, Node::Var, Node::new_num(100)),
                                Node::new_num(20),
                                Node::new_cond(
                                    Node::new_unop(
                                        UnOp::Not,
                                        Node::new_binop(BinOp::Gt, Node::Var, Node::new_num(200)),
                                    ),
                                    Node::new_binop(
                                        BinOp::Add,
                                        Node::new_unop(UnOp::Neg, Node::Var),
                                        Node::new_num(1000),
                                    ),
                                    Node::new_num(1234),
                                ),
                            ),
                        ),
                        Node::new_binop(BinOp::Sub, Node::Var, Node::new_num(10)),
                    ),
                    count_tests: vec![
                        (0, None),
                        (10, Some(0)),
                        (43, Some(10)),
                        (53, Some(10)),
                        (55, Some(20)),
                        (44, Some(20)),
                        (441, Some(1234)),
                        (404, Some(1234)),
                        (150, Some(850)),
                        (156, Some(844)),
                        (200, Some(800)),
                    ]
                    .into_iter()
                    .collect(),
                },
                TestCase {
                    test_name: "Big expression (with error)",
                    source: "n > 10 ? (n % 10) == 3 ? 10 : (n < 100 ? 20 : (!(n > 200) ? -n + 1000 : 1234) : n - 10",
                    has_error: true,
                    node: Node::Var,
                    count_tests: HashMap::new(),
                },
            ]
        }

        fn run(&self) {
            let prefix = format!("For test {}", self.test_name);
            let input = self.source;
            let formula = match Formula::parse(input) {
                Err(err) => {
                    assert!(
                        self.has_error,
                        "{}, error found while parsig formula: `{}`: {:?}",
                        prefix, input, err
                    );

                    return;
                }
                Ok(formula) => {
                    assert!(
                        !self.has_error,
                        "{}, parser should return an error for source: `{}`",
                        prefix, input
                    );

                    formula
                }
            };

            assert_eq!(formula.expr, self.node, "{}", prefix);

            for (count, index) in self.count_tests.iter() {
                let res = formula.execute(*count);

                assert_eq!(
                    res.as_ref(),
                    index.as_ref(),
                    "{}, bad index for count {}",
                    prefix,
                    count
                )
            }
        }
    }

    #[test]
    fn formula() {
        TestCase::make_tests().into_iter().for_each(|t| t.run());
    }

    #[test]
    fn test_struct_formula() {
        let formula = Formula { expr: Node::Var };
        let copy = formula.clone();

        assert_eq!(copy.expr, formula.expr);
        assert_eq!(copy, formula);
        assert_eq!(format!("{:?}", formula), String::from("Formula { expr: Var }"));
    }

    impl Formula {
        pub(in super::super) fn for_tests_empty() -> Formula {
            Formula { expr: Node::new_num(0) }
        }

        pub(in super::super) fn for_tests_shift() -> Formula {
            Formula {
                expr: Node::new_binop(BinOp::Sub, Node::Var, Node::new_num(100)),
            }
        }
    }
}
// no-coverage:stop