use crate::data::dataframe::DataFrame;
use crate::data::expression::{Expr, Op, Value};
use crate::types::ColumnType;
use std::collections::HashMap;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PredOp {
Eq,
Ne,
Gt,
Ge,
Lt,
Le,
In,
NotIn,
Contains,
Between,
IsEmpty,
NotEmpty,
}
impl PredOp {
pub fn parse(s: &str) -> Result<Self, String> {
Ok(match s {
"eq" => Self::Eq,
"ne" => Self::Ne,
"gt" => Self::Gt,
"ge" => Self::Ge,
"lt" => Self::Lt,
"le" => Self::Le,
"in" => Self::In,
"not_in" => Self::NotIn,
"contains" => Self::Contains,
"between" => Self::Between,
"is_empty" => Self::IsEmpty,
"not_empty" => Self::NotEmpty,
other => {
return Err(format!(
"Unknown filter operator '{}'. Available: eq, ne, gt, ge, lt, le, \
in, not_in, contains, between, is_empty, not_empty",
other
))
}
})
}
}
#[derive(Clone, Debug)]
pub enum Operand {
Literal(Value),
Column(String),
List(Vec<Value>),
}
#[derive(Clone, Debug)]
pub struct Predicate {
pub col: String,
pub op: PredOp,
pub value: Operand,
}
#[derive(Clone, Debug)]
pub enum Clause {
One(Predicate),
AnyOf(Vec<Predicate>),
}
fn binop(op: Op, left: Expr, right: Expr) -> Expr {
Expr::BinOp {
op,
left: Box::new(left),
right: Box::new(right),
}
}
fn is_empty_expr(col: &str) -> Expr {
let column = || Expr::ColumnRef(col.to_string());
binop(
Op::Or,
Expr::IsNull(Box::new(column())),
binop(
Op::Eq,
Expr::FunctionCall {
name: "text".to_string(),
args: vec![column()],
},
Expr::Literal(Value::String(String::new())),
),
)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Shape {
AsIs,
Numeric,
IsoText,
}
fn shape_for(df: &DataFrame, col: &str) -> Result<Shape, String> {
Ok(match df.columns[df.column_index(col)?].col_type {
ColumnType::Integer | ColumnType::Float | ColumnType::Percentage | ColumnType::Currency => {
Shape::Numeric
}
ColumnType::Date | ColumnType::Datetime => Shape::IsoText,
_ => Shape::AsIs,
})
}
fn fit(value: &Value, shape: Shape) -> Result<Value, String> {
Ok(match (shape, value) {
(Shape::Numeric, Value::String(s)) => Value::Number(
s.trim()
.parse::<f64>()
.map_err(|_| format!("'{}' is not a number, and the column holds numbers", s))?,
),
(Shape::IsoText, other) => Value::String(other.to_string()),
_ => value.clone(),
})
}
fn as_text(col: &str) -> Expr {
Expr::FunctionCall {
name: "text".to_string(),
args: vec![Expr::ColumnRef(col.to_string())],
}
}
impl Predicate {
pub fn to_expr(&self, df: &DataFrame) -> Result<Expr, String> {
let shape = shape_for(df, &self.col)?;
let column = if shape == Shape::IsoText {
as_text(&self.col)
} else {
Expr::ColumnRef(self.col.clone())
};
let operand = |value: &Operand| -> Result<Expr, String> {
match value {
Operand::Literal(v) => Ok(Expr::Literal(fit(v, shape)?)),
Operand::Column(name) => Ok(if shape == Shape::IsoText {
as_text(name)
} else {
Expr::ColumnRef(name.clone())
}),
Operand::List(_) => {
Err("this operator takes a single value, not a list".to_string())
}
}
};
let simple = |op: Op| -> Result<Expr, String> {
Ok(binop(op, column.clone(), operand(&self.value)?))
};
match self.op {
PredOp::Eq => simple(Op::Eq),
PredOp::Ne => simple(Op::NotEq),
PredOp::Gt => simple(Op::Gt),
PredOp::Ge => simple(Op::Geq),
PredOp::Lt => simple(Op::Lt),
PredOp::Le => simple(Op::Leq),
PredOp::IsEmpty => Ok(is_empty_expr(&self.col)),
PredOp::NotEmpty => Ok(Expr::Not(Box::new(is_empty_expr(&self.col)))),
PredOp::Contains => {
let pattern = match &self.value {
Operand::Literal(Value::String(s)) => s.clone(),
Operand::Literal(other) => other.to_string(),
_ => return Err("'contains' takes a regex string".to_string()),
};
Ok(Expr::FunctionCall {
name: "contains".to_string(),
args: vec![column, Expr::Literal(Value::String(pattern))],
})
}
PredOp::Between => match &self.value {
Operand::List(bounds) if bounds.len() == 2 => Ok(binop(
Op::And,
binop(
Op::Geq,
column.clone(),
Expr::Literal(fit(&bounds[0], shape)?),
),
binop(Op::Leq, column, Expr::Literal(fit(&bounds[1], shape)?)),
)),
_ => Err("'between' takes a two-element [low, high] array".to_string()),
},
PredOp::In | PredOp::NotIn => match &self.value {
Operand::List(items) => {
let list = Expr::InList {
left: Box::new(column),
list: items
.iter()
.map(|v| fit(v, shape).map(Expr::Literal))
.collect::<Result<_, _>>()?,
};
Ok(if self.op == PredOp::In {
list
} else {
Expr::Not(Box::new(list))
})
}
_ => Err("'in' takes an array of values".to_string()),
},
}
}
}
pub fn clauses_to_expr(df: &DataFrame, clauses: &[Clause]) -> Result<Option<Expr>, String> {
let mut combined: Option<Expr> = None;
for clause in clauses {
let expr = match clause {
Clause::One(p) => p.to_expr(df)?,
Clause::AnyOf(predicates) => {
let mut any: Option<Expr> = None;
for p in predicates {
let e = p.to_expr(df)?;
any = Some(match any {
Some(acc) => binop(Op::Or, acc, e),
None => e,
});
}
any.unwrap_or(Expr::Literal(Value::Boolean(false)))
}
};
combined = Some(match combined {
Some(acc) => binop(Op::And, acc, expr),
None => expr,
});
}
Ok(combined)
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Fallback {
Allowed,
Forbidden,
}
pub fn select_rows(df: &DataFrame, expr: &Expr, fallback: Fallback) -> Result<Vec<usize>, String> {
use polars::prelude::*;
let visible = df.get_visible_df()?;
let height = visible.height();
let refusal = match expr.to_polars_expr() {
Ok(polars_expr) => match visible
.clone()
.lazy()
.select([polars_expr.alias("__match")])
.collect()
{
Ok(mask_df) => {
let mask = mask_df
.column("__match")
.map_err(|e| e.to_string())?
.as_materialized_series()
.bool()
.map_err(|_| {
"this filter produced values rather than yes-or-no answers".to_string()
})?
.clone();
if mask.len() == 1 {
return Ok(if mask.get(0).unwrap_or(false) {
(0..height).collect()
} else {
Vec::new()
});
}
if mask.len() != height {
return Err(format!(
"this filter produced {} verdicts for {} rows",
mask.len(),
height
));
}
return Ok(mask
.into_iter()
.enumerate()
.filter(|(_, hit)| hit.unwrap_or(false))
.map(|(i, _)| i)
.collect());
}
Err(e) => e.to_string(),
},
Err(e) => e,
};
if fallback == Fallback::Forbidden {
return Err(refusal);
}
let lookup: HashMap<&str, usize> = df
.columns
.iter()
.enumerate()
.map(|(i, c)| (c.name.as_str(), i))
.collect();
Ok((0..df.visible_row_count())
.filter(|i| {
expr.eval(df.row_order[*i], &lookup, df)
.as_bool()
.unwrap_or(false)
})
.collect())
}
pub fn matching_rows(df: &DataFrame, clauses: &[Clause]) -> Result<Vec<usize>, String> {
for clause in clauses {
let predicates: &[Predicate] = match clause {
Clause::One(p) => std::slice::from_ref(p),
Clause::AnyOf(list) => list,
};
for p in predicates {
df.column_index(&p.col)?;
if let Operand::Column(other) = &p.value {
df.column_index(other)?;
}
}
}
let expr = match clauses_to_expr(df, clauses)? {
Some(e) => e,
None => return Ok((*df.row_order).clone()),
};
let display = select_rows(df, &expr, Fallback::Forbidden)?;
Ok(display.into_iter().map(|i| df.row_order[i]).collect())
}