truecalc_core/eval/functions/math/
criterion.rs1use crate::types::Value;
2
3#[derive(Debug)]
5pub enum Criterion {
6 NumEq(f64),
7 NumNe(f64),
8 NumLt(f64),
9 NumGt(f64),
10 NumLe(f64),
11 NumGe(f64),
12 TextEq(String),
14 TextNe(String),
16 WildcardEq(Vec<char>),
18 BoolEq(bool),
19}
20
21pub fn flatten_to_vec(v: &Value) -> Vec<&Value> {
26 match v {
27 Value::Array(arr) => arr.iter().flat_map(flatten_to_vec).collect(),
28 other => vec![other],
29 }
30}
31
32pub fn parse_criterion(v: &Value) -> Criterion {
34 match v {
35 Value::Number(n) => Criterion::NumEq(*n),
36 Value::Bool(b) => Criterion::BoolEq(*b),
37 Value::Text(s) => parse_criterion_str(s),
38 _ => Criterion::TextEq(String::new()), }
40}
41
42fn parse_criterion_str(s: &str) -> Criterion {
44 let (op, rest) = if let Some(r) = s.strip_prefix("<>") {
46 ("<>", r)
47 } else if let Some(r) = s.strip_prefix(">=") {
48 (">=", r)
49 } else if let Some(r) = s.strip_prefix("<=") {
50 ("<=", r)
51 } else if let Some(r) = s.strip_prefix('>') {
52 (">", r)
53 } else if let Some(r) = s.strip_prefix('<') {
54 ("<", r)
55 } else if let Some(r) = s.strip_prefix('=') {
56 ("=", r)
57 } else {
58 ("", s)
59 };
60
61 if !op.is_empty() || rest.parse::<f64>().is_ok() {
63 if let Ok(n) = rest.parse::<f64>() {
64 return match op {
65 "<>" => Criterion::NumNe(n),
66 ">=" => Criterion::NumGe(n),
67 "<=" => Criterion::NumLe(n),
68 ">" => Criterion::NumGt(n),
69 "<" => Criterion::NumLt(n),
70 _ => Criterion::NumEq(n), };
72 }
73 if op == "<>" {
75 return Criterion::TextNe(rest.to_lowercase());
76 }
77 return Criterion::TextEq(s.to_lowercase());
79 }
80
81 let has_wildcard_or_tilde = {
85 let chars: Vec<char> = rest.chars().collect();
86 let mut found = false;
87 let mut i = 0;
88 while i < chars.len() {
89 if chars[i] == '~' {
90 found = true; break;
92 } else if chars[i] == '*' || chars[i] == '?' {
93 found = true;
94 break;
95 } else {
96 i += 1;
97 }
98 }
99 found
100 };
101 if has_wildcard_or_tilde {
102 return Criterion::WildcardEq(rest.to_lowercase().chars().collect());
103 }
104
105 Criterion::TextEq(rest.to_lowercase())
106}
107
108pub fn matches_criterion(value: &Value, crit: &Criterion) -> bool {
110 match crit {
111 Criterion::NumEq(n) => match value {
112 Value::Number(v) => (v - n).abs() < 1e-10,
113 Value::Text(s) => s.trim().parse::<f64>().is_ok_and(|v| (v - n).abs() < 1e-10),
118 _ => false,
119 },
120 Criterion::NumNe(n) => match value {
121 Value::Number(v) => (v - n).abs() >= 1e-10,
122 Value::Text(s) => !s.trim().parse::<f64>().is_ok_and(|v| (v - n).abs() < 1e-10),
123 _ => true, },
125 Criterion::NumLt(n) => matches!(value, Value::Number(v) if v < n),
126 Criterion::NumGt(n) => matches!(value, Value::Number(v) if v > n),
127 Criterion::NumLe(n) => matches!(value, Value::Number(v) if v <= n),
128 Criterion::NumGe(n) => matches!(value, Value::Number(v) if v >= n),
129 Criterion::TextEq(pat) => match value {
130 Value::Text(s) => s.to_lowercase() == *pat,
131 Value::Bool(b) => {
132 let s = if *b { "true" } else { "false" };
133 s == pat.as_str()
134 }
135 _ => false,
136 },
137 Criterion::TextNe(pat) => match value {
138 Value::Text(s) => s.to_lowercase() != *pat,
139 _ => true,
140 },
141 Criterion::WildcardEq(pattern) => match value {
142 Value::Text(s) => {
143 let text: Vec<char> = s.to_lowercase().chars().collect();
144 wildcard_match(pattern, &text)
145 }
146 _ => false,
147 },
148 Criterion::BoolEq(b) => matches!(value, Value::Bool(v) if v == b),
149 }
150}
151
152fn wildcard_match(pattern: &[char], text: &[char]) -> bool {
155 match (pattern.first(), text.first()) {
156 (None, None) => true,
157 (None, _) => false,
158 (Some('*'), _) => {
159 for i in 0..=text.len() {
161 if wildcard_match(&pattern[1..], &text[i..]) {
162 return true;
163 }
164 }
165 false
166 }
167 (Some(_), None) => false,
168 (Some('~'), _) => {
169 if pattern.len() < 2 {
171 return false;
172 }
173 match text.first() {
174 Some(t) if *t == pattern[1] => wildcard_match(&pattern[2..], &text[1..]),
175 _ => false,
176 }
177 }
178 (Some(p), Some(t)) => {
179 if *p == '?' || *p == *t {
180 wildcard_match(&pattern[1..], &text[1..])
181 } else {
182 false
183 }
184 }
185 }
186}
187
188#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::types::Value;
194
195 fn num(n: f64) -> Value { Value::Number(n) }
196 fn text(s: &str) -> Value { Value::Text(s.to_string()) }
197
198 #[test]
199 fn numeric_eq() {
200 let c = parse_criterion(&num(3.0));
201 assert!(matches_criterion(&num(3.0), &c));
202 assert!(!matches_criterion(&num(4.0), &c));
203 }
204
205 #[test]
206 fn text_criterion_gt() {
207 let c = parse_criterion(&text(">2"));
208 assert!(matches_criterion(&num(3.0), &c));
209 assert!(!matches_criterion(&num(1.0), &c));
210 }
211
212 #[test]
213 fn text_criterion_ne_num() {
214 let c = parse_criterion(&text("<>2"));
215 assert!(matches_criterion(&num(3.0), &c));
216 assert!(!matches_criterion(&num(2.0), &c));
217 }
218
219 #[test]
220 fn text_criterion_exact() {
221 let c = parse_criterion(&text("apple"));
222 assert!(matches_criterion(&text("Apple"), &c)); assert!(!matches_criterion(&text("banana"), &c));
224 }
225
226 #[test]
227 fn text_criterion_wildcard_star() {
228 let c = parse_criterion(&text("a*"));
229 assert!(matches_criterion(&text("apple"), &c));
230 assert!(matches_criterion(&text("a"), &c));
231 assert!(!matches_criterion(&text("banana"), &c));
232 }
233
234 #[test]
235 fn text_criterion_wildcard_question() {
236 let c = parse_criterion(&text("ap?"));
237 assert!(matches_criterion(&text("apt"), &c));
238 assert!(matches_criterion(&text("ape"), &c));
239 assert!(!matches_criterion(&text("apple"), &c));
240 }
241
242 #[test]
243 fn bool_criterion() {
244 let c = parse_criterion(&Value::Bool(true));
245 assert!(matches_criterion(&Value::Bool(true), &c));
246 assert!(!matches_criterion(&Value::Bool(false), &c));
247 }
248
249 #[test]
250 fn flatten_array() {
251 let arr = Value::Array(vec![num(1.0), num(2.0), num(3.0)]);
252 let flat = flatten_to_vec(&arr);
253 assert_eq!(flat.len(), 3);
254 }
255
256 #[test]
257 fn flatten_scalar() {
258 let v = num(5.0);
259 let flat = flatten_to_vec(&v);
260 assert_eq!(flat.len(), 1);
261 }
262}