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> {
24 match v {
25 Value::Array(arr) => arr.iter().collect(),
26 other => vec![other],
27 }
28}
29
30pub fn parse_criterion(v: &Value) -> Criterion {
32 match v {
33 Value::Number(n) => Criterion::NumEq(*n),
34 Value::Bool(b) => Criterion::BoolEq(*b),
35 Value::Text(s) => parse_criterion_str(s),
36 _ => Criterion::TextEq(String::new()), }
38}
39
40fn parse_criterion_str(s: &str) -> Criterion {
42 let (op, rest) = if let Some(r) = s.strip_prefix("<>") {
44 ("<>", r)
45 } else 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 {
56 ("", s)
57 };
58
59 if !op.is_empty() || rest.parse::<f64>().is_ok() {
61 if let Ok(n) = rest.parse::<f64>() {
62 return match op {
63 "<>" => Criterion::NumNe(n),
64 ">=" => Criterion::NumGe(n),
65 "<=" => Criterion::NumLe(n),
66 ">" => Criterion::NumGt(n),
67 "<" => Criterion::NumLt(n),
68 _ => Criterion::NumEq(n), };
70 }
71 if op == "<>" {
73 return Criterion::TextNe(rest.to_lowercase());
74 }
75 return Criterion::TextEq(s.to_lowercase());
77 }
78
79 let has_wildcard_or_tilde = {
83 let chars: Vec<char> = rest.chars().collect();
84 let mut found = false;
85 let mut i = 0;
86 while i < chars.len() {
87 if chars[i] == '~' {
88 found = true; break;
90 } else if chars[i] == '*' || chars[i] == '?' {
91 found = true;
92 break;
93 } else {
94 i += 1;
95 }
96 }
97 found
98 };
99 if has_wildcard_or_tilde {
100 return Criterion::WildcardEq(rest.to_lowercase().chars().collect());
101 }
102
103 Criterion::TextEq(rest.to_lowercase())
104}
105
106pub fn matches_criterion(value: &Value, crit: &Criterion) -> bool {
108 match crit {
109 Criterion::NumEq(n) => match value {
110 Value::Number(v) => (v - n).abs() < 1e-10,
111 Value::Text(s) => s.trim().parse::<f64>().is_ok_and(|v| (v - n).abs() < 1e-10),
116 _ => false,
117 },
118 Criterion::NumNe(n) => match value {
119 Value::Number(v) => (v - n).abs() >= 1e-10,
120 Value::Text(s) => !s.trim().parse::<f64>().is_ok_and(|v| (v - n).abs() < 1e-10),
121 _ => true, },
123 Criterion::NumLt(n) => matches!(value, Value::Number(v) if v < n),
124 Criterion::NumGt(n) => matches!(value, Value::Number(v) if v > n),
125 Criterion::NumLe(n) => matches!(value, Value::Number(v) if v <= n),
126 Criterion::NumGe(n) => matches!(value, Value::Number(v) if v >= n),
127 Criterion::TextEq(pat) => match value {
128 Value::Text(s) => s.to_lowercase() == *pat,
129 Value::Bool(b) => {
130 let s = if *b { "true" } else { "false" };
131 s == pat.as_str()
132 }
133 _ => false,
134 },
135 Criterion::TextNe(pat) => match value {
136 Value::Text(s) => s.to_lowercase() != *pat,
137 _ => true,
138 },
139 Criterion::WildcardEq(pattern) => match value {
140 Value::Text(s) => {
141 let text: Vec<char> = s.to_lowercase().chars().collect();
142 wildcard_match(pattern, &text)
143 }
144 _ => false,
145 },
146 Criterion::BoolEq(b) => matches!(value, Value::Bool(v) if v == b),
147 }
148}
149
150fn wildcard_match(pattern: &[char], text: &[char]) -> bool {
153 match (pattern.first(), text.first()) {
154 (None, None) => true,
155 (None, _) => false,
156 (Some('*'), _) => {
157 for i in 0..=text.len() {
159 if wildcard_match(&pattern[1..], &text[i..]) {
160 return true;
161 }
162 }
163 false
164 }
165 (Some(_), None) => false,
166 (Some('~'), _) => {
167 if pattern.len() < 2 {
169 return false;
170 }
171 match text.first() {
172 Some(t) if *t == pattern[1] => wildcard_match(&pattern[2..], &text[1..]),
173 _ => false,
174 }
175 }
176 (Some(p), Some(t)) => {
177 if *p == '?' || *p == *t {
178 wildcard_match(&pattern[1..], &text[1..])
179 } else {
180 false
181 }
182 }
183 }
184}
185
186#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::types::Value;
192
193 fn num(n: f64) -> Value { Value::Number(n) }
194 fn text(s: &str) -> Value { Value::Text(s.to_string()) }
195
196 #[test]
197 fn numeric_eq() {
198 let c = parse_criterion(&num(3.0));
199 assert!(matches_criterion(&num(3.0), &c));
200 assert!(!matches_criterion(&num(4.0), &c));
201 }
202
203 #[test]
204 fn text_criterion_gt() {
205 let c = parse_criterion(&text(">2"));
206 assert!(matches_criterion(&num(3.0), &c));
207 assert!(!matches_criterion(&num(1.0), &c));
208 }
209
210 #[test]
211 fn text_criterion_ne_num() {
212 let c = parse_criterion(&text("<>2"));
213 assert!(matches_criterion(&num(3.0), &c));
214 assert!(!matches_criterion(&num(2.0), &c));
215 }
216
217 #[test]
218 fn text_criterion_exact() {
219 let c = parse_criterion(&text("apple"));
220 assert!(matches_criterion(&text("Apple"), &c)); assert!(!matches_criterion(&text("banana"), &c));
222 }
223
224 #[test]
225 fn text_criterion_wildcard_star() {
226 let c = parse_criterion(&text("a*"));
227 assert!(matches_criterion(&text("apple"), &c));
228 assert!(matches_criterion(&text("a"), &c));
229 assert!(!matches_criterion(&text("banana"), &c));
230 }
231
232 #[test]
233 fn text_criterion_wildcard_question() {
234 let c = parse_criterion(&text("ap?"));
235 assert!(matches_criterion(&text("apt"), &c));
236 assert!(matches_criterion(&text("ape"), &c));
237 assert!(!matches_criterion(&text("apple"), &c));
238 }
239
240 #[test]
241 fn bool_criterion() {
242 let c = parse_criterion(&Value::Bool(true));
243 assert!(matches_criterion(&Value::Bool(true), &c));
244 assert!(!matches_criterion(&Value::Bool(false), &c));
245 }
246
247 #[test]
248 fn flatten_array() {
249 let arr = Value::Array(vec![num(1.0), num(2.0), num(3.0)]);
250 let flat = flatten_to_vec(&arr);
251 assert_eq!(flat.len(), 3);
252 }
253
254 #[test]
255 fn flatten_scalar() {
256 let v = num(5.0);
257 let flat = flatten_to_vec(&v);
258 assert_eq!(flat.len(), 1);
259 }
260}