sheetkit_core/formula/functions/
logical.rs

1//! Logical formula functions: TRUE, FALSE, IFERROR, IFNA, IFS, SWITCH, XOR.
2
3use crate::cell::CellValue;
4use crate::error::{Error, Result};
5use crate::formula::ast::Expr;
6use crate::formula::eval::{coerce_to_bool, Evaluator};
7use crate::formula::functions::check_arg_count;
8
9/// TRUE() - returns boolean TRUE.
10pub fn fn_true(args: &[Expr], _ctx: &mut Evaluator) -> Result<CellValue> {
11    check_arg_count("TRUE", args, 0, 0)?;
12    Ok(CellValue::Bool(true))
13}
14
15/// FALSE() - returns boolean FALSE.
16pub fn fn_false(args: &[Expr], _ctx: &mut Evaluator) -> Result<CellValue> {
17    check_arg_count("FALSE", args, 0, 0)?;
18    Ok(CellValue::Bool(false))
19}
20
21/// IFERROR(value, value_if_error) - returns value_if_error if the first arg is an error.
22pub fn fn_iferror(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
23    check_arg_count("IFERROR", args, 2, 2)?;
24    match ctx.eval_expr(&args[0]) {
25        Ok(CellValue::Error(_)) | Err(_) => ctx.eval_expr(&args[1]),
26        Ok(v) => Ok(v),
27    }
28}
29
30/// IFNA(value, value_if_na) - returns value_if_na if the first arg is #N/A.
31pub fn fn_ifna(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
32    check_arg_count("IFNA", args, 2, 2)?;
33    match ctx.eval_expr(&args[0]) {
34        Ok(CellValue::Error(ref e)) if e == "#N/A" => ctx.eval_expr(&args[1]),
35        Ok(v) => Ok(v),
36        Err(e) => {
37            let msg = e.to_string();
38            if msg.contains("#N/A") {
39                ctx.eval_expr(&args[1])
40            } else {
41                Err(e)
42            }
43        }
44    }
45}
46
47/// IFS(condition1, value1, [condition2, value2], ...) - evaluates conditions in order.
48pub fn fn_ifs(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
49    if args.len() < 2 || !args.len().is_multiple_of(2) {
50        return Err(Error::WrongArgCount {
51            name: "IFS".to_string(),
52            expected: "even number >= 2".to_string(),
53            got: args.len(),
54        });
55    }
56    let mut i = 0;
57    while i < args.len() {
58        let cond = ctx.eval_expr(&args[i])?;
59        if coerce_to_bool(&cond)? {
60            return ctx.eval_expr(&args[i + 1]);
61        }
62        i += 2;
63    }
64    Ok(CellValue::Error("#N/A".to_string()))
65}
66
67/// SWITCH(expression, value1, result1, [value2, result2], ..., [default]) - matches a value.
68pub fn fn_switch(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
69    if args.len() < 3 {
70        return Err(Error::WrongArgCount {
71            name: "SWITCH".to_string(),
72            expected: "3 or more".to_string(),
73            got: args.len(),
74        });
75    }
76    let expr_val = ctx.eval_expr(&args[0])?;
77    let pairs_end = if args.len().is_multiple_of(2) {
78        // Even: has a default value at the end
79        args.len() - 1
80    } else {
81        args.len()
82    };
83    let mut i = 1;
84    while i + 1 < pairs_end {
85        let case_val = ctx.eval_expr(&args[i])?;
86        if values_equal(&expr_val, &case_val) {
87            return ctx.eval_expr(&args[i + 1]);
88        }
89        i += 2;
90    }
91    // Check for default value
92    if args.len().is_multiple_of(2) {
93        ctx.eval_expr(&args[args.len() - 1])
94    } else {
95        Ok(CellValue::Error("#N/A".to_string()))
96    }
97}
98
99fn values_equal(a: &CellValue, b: &CellValue) -> bool {
100    match (a, b) {
101        (CellValue::Number(x), CellValue::Number(y)) => (x - y).abs() < f64::EPSILON,
102        (CellValue::String(x), CellValue::String(y)) => x.eq_ignore_ascii_case(y),
103        (CellValue::Bool(x), CellValue::Bool(y)) => x == y,
104        (CellValue::Empty, CellValue::Empty) => true,
105        _ => false,
106    }
107}
108
109/// XOR(logical1, [logical2], ...) - returns TRUE if an odd number of arguments are TRUE.
110pub fn fn_xor(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
111    check_arg_count("XOR", args, 1, 255)?;
112    let values = ctx.flatten_args_to_values(args)?;
113    let mut true_count = 0u32;
114    for v in &values {
115        if matches!(v, CellValue::Empty) {
116            continue;
117        }
118        if coerce_to_bool(v)? {
119            true_count += 1;
120        }
121    }
122    Ok(CellValue::Bool(
123        !true_count.is_multiple_of(2) && true_count > 0,
124    ))
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::formula::eval::{evaluate, CellSnapshot, Evaluator};
131    use crate::formula::parser::parse_formula;
132
133    fn eval(formula: &str) -> CellValue {
134        let snap = CellSnapshot::new("Sheet1".to_string());
135        let expr = parse_formula(formula).unwrap();
136        evaluate(&expr, &snap).unwrap()
137    }
138
139    #[test]
140    fn test_true_function() {
141        let snap = CellSnapshot::new("Sheet1".to_string());
142        let mut evaluator = Evaluator::new(&snap);
143        let result = fn_true(&[], &mut evaluator).unwrap();
144        assert_eq!(result, CellValue::Bool(true));
145    }
146
147    #[test]
148    fn test_false_function() {
149        let snap = CellSnapshot::new("Sheet1".to_string());
150        let mut evaluator = Evaluator::new(&snap);
151        let result = fn_false(&[], &mut evaluator).unwrap();
152        assert_eq!(result, CellValue::Bool(false));
153    }
154
155    #[test]
156    fn test_iferror_no_error() {
157        assert_eq!(eval(r#"IFERROR(42,"error")"#), CellValue::Number(42.0));
158    }
159
160    #[test]
161    fn test_iferror_with_error() {
162        assert_eq!(
163            eval(r#"IFERROR(1/0,"error")"#),
164            CellValue::String("error".to_string())
165        );
166    }
167
168    #[test]
169    fn test_ifna_no_na() {
170        assert_eq!(eval(r#"IFNA(42,"not found")"#), CellValue::Number(42.0));
171    }
172
173    #[test]
174    fn test_ifs_first_true() {
175        assert_eq!(
176            eval(r#"IFS(TRUE,"first",TRUE,"second")"#),
177            CellValue::String("first".to_string())
178        );
179    }
180
181    #[test]
182    fn test_ifs_second_true() {
183        assert_eq!(
184            eval(r#"IFS(FALSE,"first",TRUE,"second")"#),
185            CellValue::String("second".to_string())
186        );
187    }
188
189    #[test]
190    fn test_ifs_none_true() {
191        assert_eq!(
192            eval(r#"IFS(FALSE,"first",FALSE,"second")"#),
193            CellValue::Error("#N/A".to_string())
194        );
195    }
196
197    #[test]
198    fn test_switch_match() {
199        assert_eq!(
200            eval(r#"SWITCH(2,1,"one",2,"two",3,"three")"#),
201            CellValue::String("two".to_string())
202        );
203    }
204
205    #[test]
206    fn test_switch_default() {
207        assert_eq!(
208            eval(r#"SWITCH(99,1,"one",2,"two","other")"#),
209            CellValue::String("other".to_string())
210        );
211    }
212
213    #[test]
214    fn test_switch_no_match_no_default() {
215        assert_eq!(
216            eval(r#"SWITCH(99,1,"one",2,"two",3,"three")"#),
217            CellValue::Error("#N/A".to_string())
218        );
219    }
220
221    #[test]
222    fn test_xor_odd_true() {
223        assert_eq!(eval("XOR(TRUE,FALSE,TRUE,TRUE)"), CellValue::Bool(true));
224    }
225
226    #[test]
227    fn test_xor_even_true() {
228        assert_eq!(eval("XOR(TRUE,TRUE)"), CellValue::Bool(false));
229    }
230}