1use crate::reader::ShapefileFeature;
14use oxigdal_core::vector::FieldValue;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum FieldFilterOp {
21 Eq,
23 Ne,
25 Gt,
27 Lt,
29 Gte,
31 Lte,
33 Contains,
35 StartsWith,
37}
38
39#[derive(Debug, Clone, PartialEq)]
43pub enum FilterValue {
44 String(String),
46 Integer(i64),
48 Float(f64),
50 Bool(bool),
52}
53
54impl FilterValue {
55 fn as_f64(&self) -> Option<f64> {
59 match self {
60 Self::Integer(i) => Some(*i as f64),
61 Self::Float(f) => Some(*f),
62 Self::String(_) | Self::Bool(_) => None,
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
85pub struct FieldFilter {
86 pub field: String,
88 pub op: FieldFilterOp,
90 pub value: FilterValue,
92}
93
94impl FieldFilter {
95 pub fn matches(&self, feature: &ShapefileFeature) -> bool {
108 let Some(attr) = feature.attributes.get(&self.field) else {
109 return false;
110 };
111
112 match self.op {
113 FieldFilterOp::Eq => self.eq_match(attr),
114 FieldFilterOp::Ne => !self.eq_match(attr),
115 FieldFilterOp::Gt => self.numeric_cmp(attr).is_some_and(|o| o > 0),
116 FieldFilterOp::Lt => self.numeric_cmp(attr).is_some_and(|o| o < 0),
117 FieldFilterOp::Gte => self.numeric_cmp(attr).is_some_and(|o| o >= 0),
118 FieldFilterOp::Lte => self.numeric_cmp(attr).is_some_and(|o| o <= 0),
119 FieldFilterOp::Contains => self.string_contains(attr),
120 FieldFilterOp::StartsWith => self.string_starts_with(attr),
121 }
122 }
123
124 fn eq_match(&self, attr: &FieldValue) -> bool {
128 match (attr, &self.value) {
129 (FieldValue::String(a), FilterValue::String(v)) => a == v,
130 (FieldValue::Integer(a), FilterValue::Integer(v)) => a == v,
131 (FieldValue::Float(a), FilterValue::Float(v)) => a == v,
132 (FieldValue::Bool(a), FilterValue::Bool(v)) => a == v,
133 (FieldValue::Integer(a), FilterValue::Float(v)) => (*a as f64) == *v,
135 (FieldValue::Float(a), FilterValue::Integer(v)) => *a == (*v as f64),
136 _ => false,
137 }
138 }
139
140 fn numeric_cmp(&self, attr: &FieldValue) -> Option<i8> {
145 let lhs: f64 = match attr {
146 FieldValue::Integer(i) => *i as f64,
147 FieldValue::Float(f) => *f,
148 _ => return None,
149 };
150 let rhs = self.value.as_f64()?;
151 if lhs < rhs {
152 Some(-1)
153 } else if lhs > rhs {
154 Some(1)
155 } else {
156 Some(0)
157 }
158 }
159
160 fn string_contains(&self, attr: &FieldValue) -> bool {
163 let FilterValue::String(needle) = &self.value else {
164 return false;
165 };
166 match attr {
167 FieldValue::String(haystack) => haystack.contains(needle.as_str()),
168 _ => false,
169 }
170 }
171
172 fn string_starts_with(&self, attr: &FieldValue) -> bool {
175 let FilterValue::String(prefix) = &self.value else {
176 return false;
177 };
178 match attr {
179 FieldValue::String(haystack) => haystack.starts_with(prefix.as_str()),
180 _ => false,
181 }
182 }
183}
184
185#[cfg(test)]
188mod tests {
189 use super::*;
190 use crate::reader::ShapefileFeature;
191 use std::collections::HashMap;
192
193 fn make_feature(attrs: Vec<(&str, FieldValue)>) -> ShapefileFeature {
194 let mut attributes = HashMap::new();
195 for (k, v) in attrs {
196 attributes.insert(k.to_string(), v);
197 }
198 ShapefileFeature::new(1, None, attributes)
199 }
200
201 #[test]
202 fn test_string_eq() {
203 let filter = FieldFilter {
204 field: "NAME".to_string(),
205 op: FieldFilterOp::Eq,
206 value: FilterValue::String("Paris".to_string()),
207 };
208 let yes = make_feature(vec![("NAME", FieldValue::String("Paris".to_string()))]);
209 let no = make_feature(vec![("NAME", FieldValue::String("London".to_string()))]);
210 assert!(filter.matches(&yes));
211 assert!(!filter.matches(&no));
212 }
213
214 #[test]
215 fn test_integer_ne() {
216 let filter = FieldFilter {
217 field: "VAL".to_string(),
218 op: FieldFilterOp::Ne,
219 value: FilterValue::Integer(0),
220 };
221 let yes = make_feature(vec![("VAL", FieldValue::Integer(5))]);
222 let no = make_feature(vec![("VAL", FieldValue::Integer(0))]);
223 assert!(filter.matches(&yes));
224 assert!(!filter.matches(&no));
225 }
226
227 #[test]
228 fn test_float_gt() {
229 let filter = FieldFilter {
230 field: "SCORE".to_string(),
231 op: FieldFilterOp::Gt,
232 value: FilterValue::Float(5.0),
233 };
234 let yes = make_feature(vec![("SCORE", FieldValue::Float(6.0))]);
235 let no = make_feature(vec![("SCORE", FieldValue::Float(4.0))]);
236 let equal = make_feature(vec![("SCORE", FieldValue::Float(5.0))]);
237 assert!(filter.matches(&yes));
238 assert!(!filter.matches(&no));
239 assert!(!filter.matches(&equal));
240 }
241
242 #[test]
243 fn test_contains() {
244 let filter = FieldFilter {
245 field: "NAME".to_string(),
246 op: FieldFilterOp::Contains,
247 value: FilterValue::String("oint".to_string()),
248 };
249 let yes = make_feature(vec![("NAME", FieldValue::String("Point A".to_string()))]);
250 let no = make_feature(vec![("NAME", FieldValue::String("Region B".to_string()))]);
251 assert!(filter.matches(&yes));
252 assert!(!filter.matches(&no));
253 }
254
255 #[test]
256 fn test_missing_field_returns_false() {
257 let filter = FieldFilter {
258 field: "NONEXISTENT".to_string(),
259 op: FieldFilterOp::Eq,
260 value: FilterValue::String("x".to_string()),
261 };
262 let feature = make_feature(vec![("NAME", FieldValue::String("y".to_string()))]);
263 assert!(!filter.matches(&feature));
264 }
265}