use crate::excel_error::ExcelError;
use super::eval_value::EvalValue;
use super::rounding::{excel_ceiling, excel_round, excel_roundup};
use super::value::CellValue;
pub fn apply(name: &str, args: &[EvalValue]) -> CellValue {
let result = match name {
"IF" => f_if(args),
"SUM" => f_sum(args),
"SUMIF" => f_sumif(args),
"VLOOKUP" => f_vlookup(args),
"INDEX" => f_index(args),
"MATCH" => f_match(args),
"ROUND" => f_round(args),
"ROUNDUP" => f_roundup(args),
"CEILING" => f_ceiling(args),
"IFERROR" => f_iferror(args),
"ISNUMBER" => f_isnumber(args),
"SEARCH" => f_search(args),
"TEXT" => f_text(args),
_ => Err(ExcelError::Name),
};
result.unwrap_or_else(CellValue::Error)
}
pub(crate) fn to_number(cv: &CellValue) -> Result<f64, ExcelError> {
match cv {
CellValue::Number(n) => Ok(*n),
CellValue::Empty => Ok(0.0),
CellValue::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
CellValue::Text(s) => s.trim().parse::<f64>().map_err(|_| ExcelError::Value),
CellValue::Error(e) => Err(*e),
}
}
fn is_truthy(cv: &CellValue) -> Result<bool, ExcelError> {
match cv {
CellValue::Bool(b) => Ok(*b),
CellValue::Number(n) => Ok(*n != 0.0),
CellValue::Empty => Ok(false),
CellValue::Text(s) => Ok(!s.is_empty()),
CellValue::Error(e) => Err(*e),
}
}
fn arg_scalar(args: &[EvalValue], i: usize) -> Result<&CellValue, ExcelError> {
match args.get(i) {
Some(EvalValue::Scalar(cv)) => Ok(cv),
Some(EvalValue::Range(_)) => Err(ExcelError::Value),
None => Err(ExcelError::Na), }
}
fn arg_range(args: &[EvalValue], i: usize) -> Result<&Vec<Vec<CellValue>>, ExcelError> {
match args.get(i) {
Some(EvalValue::Range(rows)) => Ok(rows),
Some(EvalValue::Scalar(_)) => Err(ExcelError::Value),
None => Err(ExcelError::Na),
}
}
fn flatten(rows: &[Vec<CellValue>]) -> impl Iterator<Item = &CellValue> {
rows.iter().flat_map(|r| r.iter())
}
fn values_equal(a: &CellValue, b: &CellValue) -> bool {
match (a, b) {
(CellValue::Number(x), CellValue::Number(y)) => x == y,
(CellValue::Text(x), CellValue::Text(y)) => x.eq_ignore_ascii_case(y),
(CellValue::Bool(x), CellValue::Bool(y)) => x == y,
(CellValue::Empty, CellValue::Empty) => true,
(CellValue::Number(x), CellValue::Text(t)) | (CellValue::Text(t), CellValue::Number(x)) => {
t.trim().parse::<f64>().map(|v| v == *x).unwrap_or(false)
},
_ => false,
}
}
fn f_if(args: &[EvalValue]) -> Result<CellValue, ExcelError> {
let cond = arg_scalar(args, 0)?;
Ok(if is_truthy(cond)? {
branch(args, 1)
} else {
branch(args, 2)
})
}
fn branch(args: &[EvalValue], i: usize) -> CellValue {
match args.get(i) {
Some(EvalValue::Scalar(cv)) => cv.clone(),
Some(EvalValue::Range(_)) => CellValue::Error(ExcelError::Value),
None => CellValue::Empty,
}
}
fn f_sum(args: &[EvalValue]) -> Result<CellValue, ExcelError> {
let mut total = 0.0;
for a in args {
match a {
EvalValue::Scalar(cv) => total += to_number(cv)?,
EvalValue::Range(rows) => {
for cv in flatten(rows) {
match cv {
CellValue::Error(e) => return Err(*e),
CellValue::Number(n) => total += n,
CellValue::Empty | CellValue::Text(_) | CellValue::Bool(_) => {},
}
}
},
}
}
Ok(CellValue::Number(total))
}
fn f_sumif(args: &[EvalValue]) -> Result<CellValue, ExcelError> {
let crit_range = arg_range(args, 0)?;
let criteria = arg_scalar(args, 1)?;
let sum_range = match args.get(2) {
Some(EvalValue::Range(r)) => r,
Some(EvalValue::Scalar(_)) => return Err(ExcelError::Value),
None => crit_range,
};
let mut total = 0.0;
for (c, target) in flatten(crit_range).zip(flatten(sum_range)) {
if values_equal(c, criteria) {
total += to_number(target)?;
}
}
Ok(CellValue::Number(total))
}
fn f_vlookup(args: &[EvalValue]) -> Result<CellValue, ExcelError> {
let lookup = arg_scalar(args, 0)?;
let table = arg_range(args, 1)?;
let col = to_number(arg_scalar(args, 2)?)?;
if col < 1.0 || col.fract() != 0.0 {
return Err(ExcelError::Value);
}
let col_idx = (col as usize) - 1;
for row in table {
if let Some(key) = row.first() {
if values_equal(key, lookup) {
return Ok(match row.get(col_idx) {
Some(cv) => cv.clone(),
None => CellValue::Error(ExcelError::Ref), });
}
}
}
Ok(CellValue::Error(ExcelError::Na))
}
fn f_index(args: &[EvalValue]) -> Result<CellValue, ExcelError> {
let range = arg_range(args, 0)?;
let n = one_based_index(to_number(arg_scalar(args, 1)?)?)?;
if let Some(EvalValue::Scalar(_)) = args.get(2) {
let col = one_based_index(to_number(arg_scalar(args, 2)?)?)?;
return Ok(match range.get(n - 1).and_then(|r| r.get(col - 1)) {
Some(cv) => cv.clone(),
None => CellValue::Error(ExcelError::Ref),
});
}
Ok(match flatten(range).nth(n - 1) {
Some(cv) => cv.clone(),
None => CellValue::Error(ExcelError::Ref),
})
}
fn one_based_index(n: f64) -> Result<usize, ExcelError> {
if n < 1.0 || n.fract() != 0.0 {
return Err(ExcelError::Value);
}
Ok(n as usize)
}
fn f_match(args: &[EvalValue]) -> Result<CellValue, ExcelError> {
let lookup = arg_scalar(args, 0)?;
let range = arg_range(args, 1)?;
for (i, cv) in flatten(range).enumerate() {
if values_equal(cv, lookup) {
return Ok(CellValue::Number((i + 1) as f64)); }
}
Ok(CellValue::Error(ExcelError::Na))
}
fn f_round(args: &[EvalValue]) -> Result<CellValue, ExcelError> {
round_like(args, excel_round)
}
fn f_roundup(args: &[EvalValue]) -> Result<CellValue, ExcelError> {
round_like(args, excel_roundup)
}
const MAX_ROUND_DIGITS: f64 = 307.0;
fn round_like(args: &[EvalValue], f: fn(f64, i32) -> f64) -> Result<CellValue, ExcelError> {
let x = to_number(arg_scalar(args, 0)?)?;
let digits = to_number(arg_scalar(args, 1)?)?;
if digits.fract() != 0.0 {
return Err(ExcelError::Value);
}
if digits.abs() > MAX_ROUND_DIGITS {
return Err(ExcelError::Num);
}
let result = f(x, digits as i32);
if result.is_finite() {
Ok(CellValue::Number(result))
} else {
Ok(CellValue::Error(ExcelError::Num))
}
}
fn f_ceiling(args: &[EvalValue]) -> Result<CellValue, ExcelError> {
let number = to_number(arg_scalar(args, 0)?)?;
let significance = to_number(arg_scalar(args, 1)?)?;
let result = excel_ceiling(number, significance);
if result.is_finite() {
Ok(CellValue::Number(result))
} else {
Ok(CellValue::Error(ExcelError::Num))
}
}
fn f_iferror(args: &[EvalValue]) -> Result<CellValue, ExcelError> {
Ok(match args.first() {
Some(EvalValue::Scalar(CellValue::Error(_))) => branch(args, 1),
Some(EvalValue::Scalar(cv)) => cv.clone(),
Some(EvalValue::Range(_)) => CellValue::Error(ExcelError::Value),
None => CellValue::Error(ExcelError::Na),
})
}
fn f_isnumber(args: &[EvalValue]) -> Result<CellValue, ExcelError> {
Ok(match args.first() {
Some(EvalValue::Scalar(cv)) => CellValue::Bool(matches!(cv, CellValue::Number(_))),
Some(EvalValue::Range(_)) => CellValue::Bool(false),
None => CellValue::Error(ExcelError::Na),
})
}
fn f_search(args: &[EvalValue]) -> Result<CellValue, ExcelError> {
let needle = text_of(arg_scalar(args, 0)?)?;
let haystack = text_of(arg_scalar(args, 1)?)?;
let start = search_start_position(args)?;
let hay_chars: Vec<char> = haystack.to_lowercase().chars().collect();
let needle_chars: Vec<char> = needle.to_lowercase().chars().collect();
if start > hay_chars.len() {
return Err(ExcelError::Value);
}
if needle_chars.is_empty() {
return Ok(CellValue::Number((start + 1) as f64));
}
search_char_position(&hay_chars, &needle_chars, start)
}
fn search_start_position(args: &[EvalValue]) -> Result<usize, ExcelError> {
match args.get(2) {
Some(_) => one_based_index(to_number(arg_scalar(args, 2)?)?).map(|i| i - 1),
None => Ok(0),
}
}
fn search_char_position(
hay_chars: &[char],
needle_chars: &[char],
start: usize,
) -> Result<CellValue, ExcelError> {
if needle_chars.len() > hay_chars.len() {
return Err(ExcelError::Value);
}
for i in start..=hay_chars.len() - needle_chars.len() {
if hay_chars[i..i + needle_chars.len()] == needle_chars[..] {
return Ok(CellValue::Number((i + 1) as f64)); }
}
Err(ExcelError::Value)
}
fn text_of(cv: &CellValue) -> Result<String, ExcelError> {
match cv {
CellValue::Text(s) => Ok(s.clone()),
CellValue::Number(n) => Ok(format_plain_number(*n)),
CellValue::Bool(b) => Ok(if *b { "TRUE".into() } else { "FALSE".into() }),
CellValue::Empty => Ok(String::new()),
CellValue::Error(e) => Err(*e),
}
}
fn f_text(args: &[EvalValue]) -> Result<CellValue, ExcelError> {
let value = to_number(arg_scalar(args, 0)?)?;
let format = match arg_scalar(args, 1)? {
CellValue::Text(s) => s.clone(),
CellValue::Empty => String::new(),
_ => return Err(ExcelError::Value),
};
Ok(CellValue::Text(format_number(value, &format)))
}
fn format_number(value: f64, format: &str) -> String {
let prefix: String = format
.chars()
.take_while(|c| !matches!(c, '#' | '0' | ',' | '.'))
.collect();
let body = &format[prefix.len()..];
let thousands = body.contains(',');
let decimals = body
.split_once('.')
.map(|(_, frac)| frac.chars().filter(|c| *c == '0' || *c == '#').count())
.unwrap_or(0) as i32;
let rounded = excel_round(value, decimals);
let negative = rounded < 0.0;
let abs = rounded.abs();
let int_part = abs.trunc() as i64;
let int_str = if thousands {
group_thousands(int_part)
} else {
int_part.to_string()
};
let mut out = String::new();
if negative {
out.push('-');
}
out.push_str(&prefix);
out.push_str(&int_str);
if decimals > 0 {
let scale = 10f64.powi(decimals);
let frac = ((abs - abs.trunc()) * scale).round() as i64;
out.push('.');
out.push_str(&format!("{:0width$}", frac, width = decimals as usize));
}
out
}
fn group_thousands(n: i64) -> String {
let digits = n.abs().to_string();
let bytes = digits.as_bytes();
let mut out = String::new();
let len = bytes.len();
for (i, b) in bytes.iter().enumerate() {
if i > 0 && (len - i).is_multiple_of(3) {
out.push(',');
}
out.push(*b as char);
}
out
}
fn format_plain_number(n: f64) -> String {
if n.fract() == 0.0 {
format!("{}", n as i64)
} else {
format!("{n}")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scalar(cv: CellValue) -> EvalValue {
EvalValue::Scalar(cv)
}
fn num(n: f64) -> EvalValue {
EvalValue::Scalar(CellValue::Number(n))
}
fn text(s: &str) -> EvalValue {
EvalValue::Scalar(CellValue::Text(s.to_string()))
}
fn col(values: &[CellValue]) -> EvalValue {
EvalValue::Range(values.iter().map(|v| vec![v.clone()]).collect())
}
#[test]
fn if_branches_on_truthiness() {
assert_eq!(
apply("IF", &[scalar(CellValue::Bool(true)), num(1.0), num(2.0)]),
CellValue::Number(1.0)
);
assert_eq!(
apply("IF", &[scalar(CellValue::Bool(false)), num(1.0), num(2.0)]),
CellValue::Number(2.0)
);
assert_eq!(
apply(
"IF",
&[
scalar(CellValue::Error(ExcelError::Ref)),
num(1.0),
num(2.0)
]
),
CellValue::Error(ExcelError::Ref)
);
}
#[test]
fn sum_sums_a_range_with_empty_as_zero() {
let r = col(&[
CellValue::Number(10.0),
CellValue::Empty,
CellValue::Number(5.0),
]);
assert_eq!(apply("SUM", &[r]), CellValue::Number(15.0));
}
#[test]
fn sum_propagates_an_error_member() {
let r = col(&[
CellValue::Number(1.0),
CellValue::Error(ExcelError::DivZero),
]);
assert_eq!(apply("SUM", &[r]), CellValue::Error(ExcelError::DivZero));
}
#[test]
fn sumif_sums_where_criteria_matches() {
let crit = col(&[
CellValue::Text("A".into()),
CellValue::Text("B".into()),
CellValue::Text("A".into()),
]);
let sums = col(&[
CellValue::Number(10.0),
CellValue::Number(20.0),
CellValue::Number(5.0),
]);
let out = apply("SUMIF", &[crit, text("A"), sums]);
assert_eq!(out, CellValue::Number(15.0));
}
#[test]
fn vlookup_exact_match_and_na_miss() {
let table = EvalValue::Range(vec![
vec![CellValue::Text("X".into()), CellValue::Number(100.0)],
vec![CellValue::Text("Y".into()), CellValue::Number(200.0)],
]);
assert_eq!(
apply(
"VLOOKUP",
&[
text("Y"),
table.clone(),
num(2.0),
scalar(CellValue::Bool(false))
]
),
CellValue::Number(200.0)
);
assert_eq!(
apply(
"VLOOKUP",
&[text("Z"), table, num(2.0), scalar(CellValue::Bool(false))]
),
CellValue::Error(ExcelError::Na)
);
}
#[test]
fn index_and_match_are_one_based() {
let r = col(&[
CellValue::Text("a".into()),
CellValue::Text("b".into()),
CellValue::Text("c".into()),
]);
assert_eq!(
apply("MATCH", &[text("b"), r.clone(), num(0.0)]),
CellValue::Number(2.0)
);
assert_eq!(
apply("INDEX", &[r.clone(), num(1.0)]),
CellValue::Text("a".into())
);
assert_eq!(
apply("INDEX", &[r, num(9.0)]),
CellValue::Error(ExcelError::Ref)
);
}
#[test]
fn match_miss_is_na() {
let r = col(&[CellValue::Number(1.0), CellValue::Number(2.0)]);
assert_eq!(
apply("MATCH", &[num(9.0), r, num(0.0)]),
CellValue::Error(ExcelError::Na)
);
}
#[test]
fn round_and_roundup_delegate_to_rounding() {
assert_eq!(
apply("ROUND", &[num(1594.925), num(2.0)]),
CellValue::Number(1594.93)
);
assert_eq!(
apply("ROUNDUP", &[num(3.001), num(2.0)]),
CellValue::Number(3.01)
);
}
#[test]
fn round_with_out_of_range_digits_is_num_not_non_finite() {
assert_eq!(
apply("ROUND", &[num(1.5), num(1e20)]),
CellValue::Error(ExcelError::Num)
);
assert_eq!(
apply("ROUNDUP", &[num(1.5), num(-1e20)]),
CellValue::Error(ExcelError::Num)
);
assert!(matches!(
apply("ROUND", &[num(1.5), num(307.0)]),
CellValue::Number(_)
));
}
#[test]
fn ceiling_coil_band_rounds_up_to_next_50() {
let req = 666.0_f64;
assert_eq!(
apply("CEILING", &[num(req * 1.05), num(50.0)]),
CellValue::Number(700.0)
);
}
#[test]
fn iferror_inspects_cellvalue_directly() {
assert_eq!(
apply(
"IFERROR",
&[scalar(CellValue::Error(ExcelError::Na)), num(7.0)]
),
CellValue::Number(7.0)
);
assert_eq!(
apply("IFERROR", &[num(3.0), num(7.0)]),
CellValue::Number(3.0)
);
}
#[test]
fn isnumber_on_each_variant() {
assert_eq!(apply("ISNUMBER", &[num(1.0)]), CellValue::Bool(true));
assert_eq!(apply("ISNUMBER", &[text("x")]), CellValue::Bool(false));
assert_eq!(
apply("ISNUMBER", &[scalar(CellValue::Bool(true))]),
CellValue::Bool(false)
);
assert_eq!(
apply("ISNUMBER", &[scalar(CellValue::Error(ExcelError::Na))]),
CellValue::Bool(false)
);
assert_eq!(
apply("ISNUMBER", &[scalar(CellValue::Empty)]),
CellValue::Bool(false)
);
}
#[test]
fn search_hit_and_miss_case_insensitive() {
assert_eq!(
apply("SEARCH", &[text("world"), text("Hello World")]),
CellValue::Number(7.0)
);
assert_eq!(
apply("SEARCH", &[text("xyz"), text("Hello World")]),
CellValue::Error(ExcelError::Value)
);
assert_eq!(
apply("SEARCH", &[text("abc"), text("ab")]),
CellValue::Error(ExcelError::Value)
);
}
#[test]
fn text_formats_the_lighthouse_currency_pattern() {
assert_eq!(
apply("TEXT", &[num(1594.93), text("£#,##0.00")]),
CellValue::Text("£1,594.93".into())
);
}
#[test]
fn unknown_name_is_name_error() {
assert_eq!(
apply("OFFSET", &[num(1.0)]),
CellValue::Error(ExcelError::Name)
);
}
}