Skip to main content

ganit_core/eval/functions/math/
criterion.rs

1use crate::types::Value;
2
3/// A parsed criterion used by COUNTIF, SUMIF, AVERAGEIF.
4#[derive(Debug)]
5pub enum Criterion {
6    NumEq(f64),
7    NumNe(f64),
8    NumLt(f64),
9    NumGt(f64),
10    NumLe(f64),
11    NumGe(f64),
12    /// Case-insensitive exact text match.
13    TextEq(String),
14    /// Case-insensitive not-equal text match.
15    TextNe(String),
16    /// Case-insensitive wildcard pattern (`*` = any chars, `?` = any single char).
17    WildcardEq(Vec<char>),
18    BoolEq(bool),
19}
20
21/// Flatten a `Value` into a flat list of references.
22/// `Value::Array` is expanded one level; scalars become a single-element slice.
23pub 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
30/// Parse a `Value` criterion argument into a [`Criterion`].
31pub 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()), // fallback — matches nothing useful
37    }
38}
39
40/// Parse a string like `">2"`, `"apple"`, `"a*"`, `"<>3"` into a [`Criterion`].
41fn parse_criterion_str(s: &str) -> Criterion {
42    // Strip operator prefix — longest match first.
43    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 there is an operator, or the bare string parses as a number, try numeric.
60    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), // "=" or bare number string
69            };
70        }
71        // Non-numeric after operator.
72        if op == "<>" {
73            return Criterion::TextNe(rest.to_lowercase());
74        }
75        // Other operators with non-numeric text: degrade to TextEq of original.
76        return Criterion::TextEq(s.to_lowercase());
77    }
78
79    // No operator prefix: check for wildcards.
80    if rest.contains('*') || rest.contains('?') {
81        return Criterion::WildcardEq(rest.to_lowercase().chars().collect());
82    }
83
84    Criterion::TextEq(rest.to_lowercase())
85}
86
87/// Test whether a `Value` satisfies a `Criterion`.
88pub fn matches_criterion(value: &Value, crit: &Criterion) -> bool {
89    match crit {
90        Criterion::NumEq(n) => match value {
91            Value::Number(v) => (v - n).abs() < 1e-10,
92            _ => false,
93        },
94        Criterion::NumNe(n) => match value {
95            Value::Number(v) => (v - n).abs() >= 1e-10,
96            _ => true, // non-numbers are "not equal" to a number
97        },
98        Criterion::NumLt(n) => matches!(value, Value::Number(v) if v < n),
99        Criterion::NumGt(n) => matches!(value, Value::Number(v) if v > n),
100        Criterion::NumLe(n) => matches!(value, Value::Number(v) if v <= n),
101        Criterion::NumGe(n) => matches!(value, Value::Number(v) if v >= n),
102        Criterion::TextEq(pat) => match value {
103            Value::Text(s) => s.to_lowercase() == *pat,
104            Value::Bool(b) => {
105                let s = if *b { "true" } else { "false" };
106                s == pat.as_str()
107            }
108            _ => false,
109        },
110        Criterion::TextNe(pat) => match value {
111            Value::Text(s) => s.to_lowercase() != *pat,
112            _ => true,
113        },
114        Criterion::WildcardEq(pattern) => match value {
115            Value::Text(s) => {
116                let text: Vec<char> = s.to_lowercase().chars().collect();
117                wildcard_match(pattern, &text)
118            }
119            _ => false,
120        },
121        Criterion::BoolEq(b) => matches!(value, Value::Bool(v) if v == b),
122    }
123}
124
125/// Full wildcard match: `pattern` must match the entire `text`.
126/// `*` matches any sequence of characters (including empty); `?` matches any single character.
127fn wildcard_match(pattern: &[char], text: &[char]) -> bool {
128    match (pattern.first(), text.first()) {
129        (None, None) => true,
130        (None, _) => false,
131        (Some('*'), _) => {
132            // Try consuming 0, 1, 2, … characters from text.
133            for i in 0..=text.len() {
134                if wildcard_match(&pattern[1..], &text[i..]) {
135                    return true;
136                }
137            }
138            false
139        }
140        (Some(_), None) => false,
141        (Some(p), Some(t)) => {
142            if *p == '?' || *p == *t {
143                wildcard_match(&pattern[1..], &text[1..])
144            } else {
145                false
146            }
147        }
148    }
149}
150
151// ── Tests ─────────────────────────────────────────────────────────────────
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::types::Value;
157
158    fn num(n: f64) -> Value { Value::Number(n) }
159    fn text(s: &str) -> Value { Value::Text(s.to_string()) }
160
161    #[test]
162    fn numeric_eq() {
163        let c = parse_criterion(&num(3.0));
164        assert!(matches_criterion(&num(3.0), &c));
165        assert!(!matches_criterion(&num(4.0), &c));
166    }
167
168    #[test]
169    fn text_criterion_gt() {
170        let c = parse_criterion(&text(">2"));
171        assert!(matches_criterion(&num(3.0), &c));
172        assert!(!matches_criterion(&num(1.0), &c));
173    }
174
175    #[test]
176    fn text_criterion_ne_num() {
177        let c = parse_criterion(&text("<>2"));
178        assert!(matches_criterion(&num(3.0), &c));
179        assert!(!matches_criterion(&num(2.0), &c));
180    }
181
182    #[test]
183    fn text_criterion_exact() {
184        let c = parse_criterion(&text("apple"));
185        assert!(matches_criterion(&text("Apple"), &c)); // case-insensitive
186        assert!(!matches_criterion(&text("banana"), &c));
187    }
188
189    #[test]
190    fn text_criterion_wildcard_star() {
191        let c = parse_criterion(&text("a*"));
192        assert!(matches_criterion(&text("apple"), &c));
193        assert!(matches_criterion(&text("a"), &c));
194        assert!(!matches_criterion(&text("banana"), &c));
195    }
196
197    #[test]
198    fn text_criterion_wildcard_question() {
199        let c = parse_criterion(&text("ap?"));
200        assert!(matches_criterion(&text("apt"), &c));
201        assert!(matches_criterion(&text("ape"), &c));
202        assert!(!matches_criterion(&text("apple"), &c));
203    }
204
205    #[test]
206    fn bool_criterion() {
207        let c = parse_criterion(&Value::Bool(true));
208        assert!(matches_criterion(&Value::Bool(true), &c));
209        assert!(!matches_criterion(&Value::Bool(false), &c));
210    }
211
212    #[test]
213    fn flatten_array() {
214        let arr = Value::Array(vec![num(1.0), num(2.0), num(3.0)]);
215        let flat = flatten_to_vec(&arr);
216        assert_eq!(flat.len(), 3);
217    }
218
219    #[test]
220    fn flatten_scalar() {
221        let v = num(5.0);
222        let flat = flatten_to_vec(&v);
223        assert_eq!(flat.len(), 1);
224    }
225}