use crate::Expr;
pub(crate) const OR: u8 = 1;
pub(crate) const AND: u8 = 2;
pub(crate) const NOT: u8 = 3;
pub(crate) const PREDICATE: u8 = 4;
pub(crate) const ADDITIVE: u8 = 5;
pub(crate) const MULTIPLICATIVE: u8 = 6;
pub(crate) const POWER: u8 = 7;
pub(crate) const ATOM: u8 = u8::MAX;
pub(crate) fn of_op(op: &str) -> u8 {
match op.to_lowercase().as_str() {
"or" => OR,
"and" => AND,
"not" => NOT,
"=" | "<>" | "!=" | "<" | ">" | "<=" | ">=" | "like" | "between" | "in" | "isnull" => {
PREDICATE
}
"+" | "-" => ADDITIVE,
"*" | "/" | "%" => MULTIPLICATIVE,
"^" => POWER,
_ => ATOM,
}
}
pub(crate) fn of_expr(expr: &Expr) -> u8 {
match expr {
Expr::Operation { op, .. } => of_op(op),
_ => ATOM,
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Operands {
pub(crate) first: u8,
pub(crate) rest: u8,
}
impl Operands {
pub(crate) fn needs_parens(&self, index: usize, operand: &Expr) -> bool {
let required = if index == 0 { self.first } else { self.rest };
of_expr(operand) < required
}
}
pub(crate) fn operands(op: &str) -> Operands {
let precedence = of_op(op);
match precedence {
ATOM => Operands { first: 0, rest: 0 },
AND | OR | NOT => Operands {
first: precedence,
rest: precedence,
},
ADDITIVE | MULTIPLICATIVE | POWER => Operands {
first: precedence,
rest: precedence + 1,
},
_ => Operands {
first: precedence + 1,
rest: precedence + 1,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn levels_are_strictly_increasing() {
let increasing = OR < AND
&& AND < NOT
&& NOT < PREDICATE
&& PREDICATE < ADDITIVE
&& ADDITIVE < MULTIPLICATIVE
&& MULTIPLICATIVE < POWER
&& POWER < ATOM;
assert!(increasing);
}
#[test]
fn parser_ordering_matches_this_table() {
const BY_LEVEL: &[(&str, &str)] = &[
("or", "and"),
("and", "="),
("=", "+"),
("+", "*"),
("*", "^"),
];
for (looser, tighter) in BY_LEVEL {
assert!(
of_op(looser) < of_op(tighter),
"{looser} should bind more loosely than {tighter}"
);
let source = format!("a {looser} b {tighter} c");
let Ok(parsed) = source.parse::<Expr>() else {
panic!("{source} should parse");
};
let Expr::Operation { op, .. } = &parsed else {
panic!("{source} should parse to an operation, got {parsed:?}");
};
assert_eq!(
op, looser,
"{source} should be rooted at {looser}, the looser operator"
);
}
}
#[test]
fn operator_names_are_case_insensitive() {
assert_eq!(of_op("AND"), of_op("and"));
assert_eq!(of_op("isNull"), of_op("isnull"));
assert_eq!(of_op("Like"), of_op("like"));
}
#[test]
fn unknown_operators_are_function_calls() {
assert_eq!(of_op("casei"), ATOM);
assert_eq!(of_op("s_intersects"), ATOM);
assert!(!operands("casei").needs_parens(0, &Expr::Bool(true)));
}
#[test]
fn div_is_rendered_as_a_call() {
assert_eq!(of_op("div"), ATOM);
assert_eq!(of_op("DIV"), ATOM);
let or: Expr = "a or b".parse().unwrap();
assert!(!operands("div").needs_parens(0, &or));
let quotient: Expr = "a div b".parse().unwrap();
assert!(!operands("*").needs_parens(1, "ient));
}
#[test]
fn associative_operators_stay_flat() {
let and: Expr = "a and b".parse().unwrap();
assert!(!operands("and").needs_parens(0, &and));
assert!(!operands("and").needs_parens(1, &and));
}
#[test]
fn left_associative_arithmetic_wraps_only_on_the_right() {
let sub: Expr = "b - c".parse().unwrap();
assert!(!operands("-").needs_parens(0, &sub));
assert!(operands("-").needs_parens(1, &sub));
}
#[test]
fn looser_children_are_wrapped() {
let or: Expr = "a or b".parse().unwrap();
assert!(operands("and").needs_parens(1, &or));
assert!(operands("=").needs_parens(0, &or));
assert!(operands("*").needs_parens(1, &or));
}
#[test]
fn tighter_children_are_not_wrapped() {
let and: Expr = "a and b".parse().unwrap();
let product: Expr = "b * c".parse().unwrap();
assert!(!operands("or").needs_parens(0, &and));
assert!(!operands("+").needs_parens(0, &product));
}
}