Boolean operators parsing
This commit is contained in:
parent
b96cd2d781
commit
83b5431994
2 changed files with 189 additions and 60 deletions
|
@ -1,7 +1,7 @@
|
||||||
use chumsky::{
|
use chumsky::{
|
||||||
error::Simple,
|
error::Simple,
|
||||||
prelude::{choice, filter, just, none_of},
|
prelude::{choice, end, filter, just, none_of, recursive},
|
||||||
text::{int, keyword, TextParser},
|
text::{int, keyword, whitespace, TextParser},
|
||||||
Parser,
|
Parser,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -49,81 +49,104 @@ pub enum Literal {
|
||||||
Number(i32),
|
Number(i32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum BoolOp {
|
||||||
|
And,
|
||||||
|
Or,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub enum Expr {
|
pub enum Expr {
|
||||||
Fuzzy(Literal),
|
Fuzzy(Literal),
|
||||||
TextCmp(TextField, TextOp, String),
|
TextCmp(TextField, TextOp, String),
|
||||||
NumberCmp(NumberField, NumberOp, i32),
|
NumberCmp(NumberField, NumberOp, i32),
|
||||||
And(Box<Expr>, Box<Expr>),
|
Combined(Box<Expr>, BoolOp, Box<Expr>),
|
||||||
Or(Box<Expr>, Box<Expr>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_parser() -> impl Parser<char, Expr, Error = Simple<char>> {
|
pub fn make_parser() -> impl Parser<char, Expr, Error = Simple<char>> {
|
||||||
let quoted_str = just('"')
|
let combined = recursive(|expr| {
|
||||||
.ignore_then(none_of('"').repeated().collect::<String>())
|
let quoted_str = just('"')
|
||||||
.then_ignore(just('"'));
|
.ignore_then(none_of('"').repeated().collect::<String>())
|
||||||
|
.then_ignore(just('"'));
|
||||||
|
|
||||||
let raw_str = filter(|c: &char| !c.is_whitespace() && *c != '"')
|
let raw_str = filter(|c: &char| !c.is_whitespace() && *c != '"')
|
||||||
.repeated()
|
.repeated()
|
||||||
.at_least(1)
|
.at_least(1)
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
|
|
||||||
let str_ = choice((quoted_str, raw_str)).padded();
|
let str_ = choice((quoted_str, raw_str)).padded();
|
||||||
|
|
||||||
let number = int(10).map(|n: String| n.parse::<i32>().unwrap()).padded();
|
let number = int(10).from_str().unwrapped().padded();
|
||||||
|
|
||||||
let text_field = choice((
|
let text_field = choice((
|
||||||
keyword("album").to(TextField::Album),
|
keyword("album").to(TextField::Album),
|
||||||
keyword("albumartist").to(TextField::AlbumArtist),
|
keyword("albumartist").to(TextField::AlbumArtist),
|
||||||
keyword("artist").to(TextField::Artist),
|
keyword("artist").to(TextField::Artist),
|
||||||
keyword("composer").to(TextField::Composer),
|
keyword("composer").to(TextField::Composer),
|
||||||
keyword("genre").to(TextField::Genre),
|
keyword("genre").to(TextField::Genre),
|
||||||
keyword("label").to(TextField::Label),
|
keyword("label").to(TextField::Label),
|
||||||
keyword("lyricist").to(TextField::Lyricist),
|
keyword("lyricist").to(TextField::Lyricist),
|
||||||
keyword("path").to(TextField::Path),
|
keyword("path").to(TextField::Path),
|
||||||
keyword("title").to(TextField::Title),
|
keyword("title").to(TextField::Title),
|
||||||
))
|
))
|
||||||
.padded();
|
.padded();
|
||||||
|
|
||||||
let text_op = choice((
|
let text_op = choice((
|
||||||
just("=").to(TextOp::Eq),
|
just("=").to(TextOp::Eq),
|
||||||
just("!=").to(TextOp::NotEq),
|
just("!=").to(TextOp::NotEq),
|
||||||
just("%").to(TextOp::Like),
|
just("%").to(TextOp::Like),
|
||||||
just("!%").to(TextOp::NotLike),
|
just("!%").to(TextOp::NotLike),
|
||||||
))
|
))
|
||||||
.padded();
|
.padded();
|
||||||
|
|
||||||
let text_cmp = text_field
|
let text_cmp = text_field
|
||||||
.then(text_op)
|
.then(text_op)
|
||||||
.then(str_.clone())
|
.then(str_.clone())
|
||||||
.map(|((a, b), c)| Expr::TextCmp(a, b, c));
|
.map(|((a, b), c)| Expr::TextCmp(a, b, c));
|
||||||
|
|
||||||
let number_field = choice((
|
let number_field = choice((
|
||||||
keyword("discnumber").to(NumberField::DiscNumber),
|
keyword("discnumber").to(NumberField::DiscNumber),
|
||||||
keyword("tracknumber").to(NumberField::TrackNumber),
|
keyword("tracknumber").to(NumberField::TrackNumber),
|
||||||
keyword("year").to(NumberField::Year),
|
keyword("year").to(NumberField::Year),
|
||||||
))
|
))
|
||||||
.padded();
|
.padded();
|
||||||
|
|
||||||
let number_op = choice((
|
let number_op = choice((
|
||||||
just("=").to(NumberOp::Eq),
|
just("=").to(NumberOp::Eq),
|
||||||
just("!=").to(NumberOp::NotEq),
|
just("!=").to(NumberOp::NotEq),
|
||||||
just(">=").to(NumberOp::GreaterOrEq),
|
just(">=").to(NumberOp::GreaterOrEq),
|
||||||
just(">").to(NumberOp::Greater),
|
just(">").to(NumberOp::Greater),
|
||||||
just("<=").to(NumberOp::LessOrEq),
|
just("<=").to(NumberOp::LessOrEq),
|
||||||
just("<").to(NumberOp::Less),
|
just("<").to(NumberOp::Less),
|
||||||
))
|
))
|
||||||
.padded();
|
.padded();
|
||||||
|
|
||||||
let number_cmp = number_field
|
let number_cmp = number_field
|
||||||
.then(number_op)
|
.then(number_op)
|
||||||
.then(number)
|
.then(number)
|
||||||
.map(|((a, b), c)| Expr::NumberCmp(a, b, c));
|
.map(|((a, b), c)| Expr::NumberCmp(a, b, c));
|
||||||
|
|
||||||
let literal = number.map(Literal::Number).or(str_.map(Literal::Text));
|
let literal = number.map(Literal::Number).or(str_.map(Literal::Text));
|
||||||
let fuzzy = literal.map(Expr::Fuzzy);
|
let fuzzy = literal.map(Expr::Fuzzy);
|
||||||
|
|
||||||
text_cmp.or(number_cmp).or(fuzzy)
|
let filter = text_cmp.or(number_cmp).or(fuzzy);
|
||||||
|
let atom = filter;
|
||||||
|
|
||||||
|
let bool_op = choice((just("&&").to(BoolOp::And), just("||").to(BoolOp::Or))).padded();
|
||||||
|
|
||||||
|
let combined = atom
|
||||||
|
.clone()
|
||||||
|
.then(bool_op.then(atom).repeated())
|
||||||
|
.foldl(|a, (b, c)| Expr::Combined(Box::new(a), b, Box::new(c)));
|
||||||
|
|
||||||
|
combined
|
||||||
|
});
|
||||||
|
|
||||||
|
combined
|
||||||
|
.clone()
|
||||||
|
.then(whitespace().ignore_then(combined).repeated())
|
||||||
|
.foldl(|a: Expr, b: Expr| Expr::Combined(Box::new(a), BoolOp::And, Box::new(b)))
|
||||||
|
.then_ignore(end())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -250,3 +273,101 @@ fn can_parse_number_operators() {
|
||||||
Expr::NumberCmp(NumberField::DiscNumber, NumberOp::LessOrEq, 6),
|
Expr::NumberCmp(NumberField::DiscNumber, NumberOp::LessOrEq, 6),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_use_boolean_operators() {
|
||||||
|
let parser = make_parser();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parser.parse(r#"album % lands && title % "sword""#).unwrap(),
|
||||||
|
Expr::Combined(
|
||||||
|
Box::new(Expr::TextCmp(
|
||||||
|
TextField::Album,
|
||||||
|
TextOp::Like,
|
||||||
|
"lands".to_owned()
|
||||||
|
)),
|
||||||
|
BoolOp::And,
|
||||||
|
Box::new(Expr::TextCmp(
|
||||||
|
TextField::Title,
|
||||||
|
TextOp::Like,
|
||||||
|
"sword".to_owned()
|
||||||
|
))
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parser.parse(r#"album % lands || title % "sword""#).unwrap(),
|
||||||
|
Expr::Combined(
|
||||||
|
Box::new(Expr::TextCmp(
|
||||||
|
TextField::Album,
|
||||||
|
TextOp::Like,
|
||||||
|
"lands".to_owned()
|
||||||
|
)),
|
||||||
|
BoolOp::Or,
|
||||||
|
Box::new(Expr::TextCmp(
|
||||||
|
TextField::Title,
|
||||||
|
TextOp::Like,
|
||||||
|
"sword".to_owned()
|
||||||
|
))
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn boolean_operators_share_precedence() {
|
||||||
|
let parser = make_parser();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parser
|
||||||
|
.parse(r#"album % lands || album % tales && title % "sword""#)
|
||||||
|
.unwrap(),
|
||||||
|
Expr::Combined(
|
||||||
|
Box::new(Expr::Combined(
|
||||||
|
Box::new(Expr::TextCmp(
|
||||||
|
TextField::Album,
|
||||||
|
TextOp::Like,
|
||||||
|
"lands".to_owned()
|
||||||
|
)),
|
||||||
|
BoolOp::Or,
|
||||||
|
Box::new(Expr::TextCmp(
|
||||||
|
TextField::Album,
|
||||||
|
TextOp::Like,
|
||||||
|
"tales".to_owned()
|
||||||
|
))
|
||||||
|
)),
|
||||||
|
BoolOp::And,
|
||||||
|
Box::new(Expr::TextCmp(
|
||||||
|
TextField::Title,
|
||||||
|
TextOp::Like,
|
||||||
|
"sword".to_owned()
|
||||||
|
))
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parser
|
||||||
|
.parse(r#"album % lands && album % tales || title % "sword""#)
|
||||||
|
.unwrap(),
|
||||||
|
Expr::Combined(
|
||||||
|
Box::new(Expr::Combined(
|
||||||
|
Box::new(Expr::TextCmp(
|
||||||
|
TextField::Album,
|
||||||
|
TextOp::Like,
|
||||||
|
"lands".to_owned()
|
||||||
|
)),
|
||||||
|
BoolOp::And,
|
||||||
|
Box::new(Expr::TextCmp(
|
||||||
|
TextField::Album,
|
||||||
|
TextOp::Like,
|
||||||
|
"tales".to_owned()
|
||||||
|
))
|
||||||
|
)),
|
||||||
|
BoolOp::Or,
|
||||||
|
Box::new(Expr::TextCmp(
|
||||||
|
TextField::Title,
|
||||||
|
TextOp::Like,
|
||||||
|
"sword".to_owned()
|
||||||
|
))
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ use crate::app::index::{
|
||||||
storage::SongKey,
|
storage::SongKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::query::BoolOp;
|
||||||
|
|
||||||
struct SearchIndex {}
|
struct SearchIndex {}
|
||||||
|
|
||||||
impl SearchIndex {
|
impl SearchIndex {
|
||||||
|
@ -13,12 +15,18 @@ impl SearchIndex {
|
||||||
Expr::Fuzzy(s) => self.eval_fuzzy(s),
|
Expr::Fuzzy(s) => self.eval_fuzzy(s),
|
||||||
Expr::TextCmp(field, op, s) => self.eval_text_operator(*field, *op, &s),
|
Expr::TextCmp(field, op, s) => self.eval_text_operator(*field, *op, &s),
|
||||||
Expr::NumberCmp(field, op, n) => self.eval_number_operator(*field, *op, *n),
|
Expr::NumberCmp(field, op, n) => self.eval_number_operator(*field, *op, *n),
|
||||||
Expr::And(e, f) => self
|
Expr::Combined(e, op, f) => self.combine(e, *op, f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn combine(&self, e: &Box<Expr>, op: BoolOp, f: &Box<Expr>) -> HashSet<SongKey> {
|
||||||
|
match op {
|
||||||
|
BoolOp::And => self
|
||||||
.eval_expr(e)
|
.eval_expr(e)
|
||||||
.intersection(&self.eval_expr(f))
|
.intersection(&self.eval_expr(f))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect(),
|
.collect(),
|
||||||
Expr::Or(e, f) => self
|
BoolOp::Or => self
|
||||||
.eval_expr(e)
|
.eval_expr(e)
|
||||||
.union(&self.eval_expr(f))
|
.union(&self.eval_expr(f))
|
||||||
.cloned()
|
.cloned()
|
||||||
|
|
Loading…
Add table
Reference in a new issue