use crate::reader::ShapefileFeature;
use oxigdal_core::vector::FieldValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldFilterOp {
Eq,
Ne,
Gt,
Lt,
Gte,
Lte,
Contains,
StartsWith,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FilterValue {
String(String),
Integer(i64),
Float(f64),
Bool(bool),
}
impl FilterValue {
fn as_f64(&self) -> Option<f64> {
match self {
Self::Integer(i) => Some(*i as f64),
Self::Float(f) => Some(*f),
Self::String(_) | Self::Bool(_) => None,
}
}
}
#[derive(Debug, Clone)]
pub struct FieldFilter {
pub field: String,
pub op: FieldFilterOp,
pub value: FilterValue,
}
impl FieldFilter {
pub fn matches(&self, feature: &ShapefileFeature) -> bool {
let Some(attr) = feature.attributes.get(&self.field) else {
return false;
};
match self.op {
FieldFilterOp::Eq => self.eq_match(attr),
FieldFilterOp::Ne => !self.eq_match(attr),
FieldFilterOp::Gt => self.numeric_cmp(attr).is_some_and(|o| o > 0),
FieldFilterOp::Lt => self.numeric_cmp(attr).is_some_and(|o| o < 0),
FieldFilterOp::Gte => self.numeric_cmp(attr).is_some_and(|o| o >= 0),
FieldFilterOp::Lte => self.numeric_cmp(attr).is_some_and(|o| o <= 0),
FieldFilterOp::Contains => self.string_contains(attr),
FieldFilterOp::StartsWith => self.string_starts_with(attr),
}
}
fn eq_match(&self, attr: &FieldValue) -> bool {
match (attr, &self.value) {
(FieldValue::String(a), FilterValue::String(v)) => a == v,
(FieldValue::Integer(a), FilterValue::Integer(v)) => a == v,
(FieldValue::Float(a), FilterValue::Float(v)) => a == v,
(FieldValue::Bool(a), FilterValue::Bool(v)) => a == v,
(FieldValue::Integer(a), FilterValue::Float(v)) => (*a as f64) == *v,
(FieldValue::Float(a), FilterValue::Integer(v)) => *a == (*v as f64),
_ => false,
}
}
fn numeric_cmp(&self, attr: &FieldValue) -> Option<i8> {
let lhs: f64 = match attr {
FieldValue::Integer(i) => *i as f64,
FieldValue::Float(f) => *f,
_ => return None,
};
let rhs = self.value.as_f64()?;
if lhs < rhs {
Some(-1)
} else if lhs > rhs {
Some(1)
} else {
Some(0)
}
}
fn string_contains(&self, attr: &FieldValue) -> bool {
let FilterValue::String(needle) = &self.value else {
return false;
};
match attr {
FieldValue::String(haystack) => haystack.contains(needle.as_str()),
_ => false,
}
}
fn string_starts_with(&self, attr: &FieldValue) -> bool {
let FilterValue::String(prefix) = &self.value else {
return false;
};
match attr {
FieldValue::String(haystack) => haystack.starts_with(prefix.as_str()),
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reader::ShapefileFeature;
use std::collections::HashMap;
fn make_feature(attrs: Vec<(&str, FieldValue)>) -> ShapefileFeature {
let mut attributes = HashMap::new();
for (k, v) in attrs {
attributes.insert(k.to_string(), v);
}
ShapefileFeature::new(1, None, attributes)
}
#[test]
fn test_string_eq() {
let filter = FieldFilter {
field: "NAME".to_string(),
op: FieldFilterOp::Eq,
value: FilterValue::String("Paris".to_string()),
};
let yes = make_feature(vec![("NAME", FieldValue::String("Paris".to_string()))]);
let no = make_feature(vec![("NAME", FieldValue::String("London".to_string()))]);
assert!(filter.matches(&yes));
assert!(!filter.matches(&no));
}
#[test]
fn test_integer_ne() {
let filter = FieldFilter {
field: "VAL".to_string(),
op: FieldFilterOp::Ne,
value: FilterValue::Integer(0),
};
let yes = make_feature(vec![("VAL", FieldValue::Integer(5))]);
let no = make_feature(vec![("VAL", FieldValue::Integer(0))]);
assert!(filter.matches(&yes));
assert!(!filter.matches(&no));
}
#[test]
fn test_float_gt() {
let filter = FieldFilter {
field: "SCORE".to_string(),
op: FieldFilterOp::Gt,
value: FilterValue::Float(5.0),
};
let yes = make_feature(vec![("SCORE", FieldValue::Float(6.0))]);
let no = make_feature(vec![("SCORE", FieldValue::Float(4.0))]);
let equal = make_feature(vec![("SCORE", FieldValue::Float(5.0))]);
assert!(filter.matches(&yes));
assert!(!filter.matches(&no));
assert!(!filter.matches(&equal));
}
#[test]
fn test_contains() {
let filter = FieldFilter {
field: "NAME".to_string(),
op: FieldFilterOp::Contains,
value: FilterValue::String("oint".to_string()),
};
let yes = make_feature(vec![("NAME", FieldValue::String("Point A".to_string()))]);
let no = make_feature(vec![("NAME", FieldValue::String("Region B".to_string()))]);
assert!(filter.matches(&yes));
assert!(!filter.matches(&no));
}
#[test]
fn test_missing_field_returns_false() {
let filter = FieldFilter {
field: "NONEXISTENT".to_string(),
op: FieldFilterOp::Eq,
value: FilterValue::String("x".to_string()),
};
let feature = make_feature(vec![("NAME", FieldValue::String("y".to_string()))]);
assert!(!filter.matches(&feature));
}
}