Skip to main content

sheetkit_core/formula/functions/
mod.rs

1//! Built-in Excel function dispatch.
2//!
3//! Provides [`lookup_function`] to resolve a function name to its implementation,
4//! and helper utilities used by individual function implementations.
5
6pub mod date_time;
7pub mod engineering;
8pub mod financial;
9pub mod information;
10pub mod logical;
11pub mod lookup;
12pub mod math;
13pub mod statistical;
14pub mod text;
15
16use crate::cell::CellValue;
17use crate::error::{Error, Result};
18use crate::formula::ast::Expr;
19use crate::formula::eval::{coerce_to_number, coerce_to_string, Evaluator};
20
21/// Signature for a built-in function implementation.
22///
23/// Functions receive unevaluated argument expressions and a mutable evaluator,
24/// allowing short-circuit evaluation (e.g., IF) and range expansion (e.g., SUM).
25pub type FunctionFn = fn(&[Expr], &mut Evaluator) -> Result<CellValue>;
26
27/// Resolve a function name (case-insensitive) to its implementation.
28pub fn lookup_function(name: &str) -> Option<FunctionFn> {
29    match name.to_ascii_uppercase().as_str() {
30        "SUM" => Some(fn_sum),
31        "AVERAGE" => Some(fn_average),
32        "COUNT" => Some(fn_count),
33        "COUNTA" => Some(fn_counta),
34        "MIN" => Some(fn_min),
35        "MAX" => Some(fn_max),
36        "IF" => Some(fn_if),
37        "ABS" => Some(fn_abs),
38        "INT" => Some(fn_int),
39        "ROUND" => Some(fn_round),
40        "MOD" => Some(fn_mod),
41        "POWER" => Some(fn_power),
42        "SQRT" => Some(fn_sqrt),
43        "LEN" => Some(fn_len),
44        "LOWER" => Some(fn_lower),
45        "UPPER" => Some(fn_upper),
46        "TRIM" => Some(fn_trim),
47        "LEFT" => Some(fn_left),
48        "RIGHT" => Some(fn_right),
49        "MID" => Some(fn_mid),
50        "CONCATENATE" => Some(fn_concatenate),
51        "AND" => Some(fn_and),
52        "OR" => Some(fn_or),
53        "NOT" => Some(fn_not),
54        "ISNUMBER" => Some(fn_isnumber),
55        "ISTEXT" => Some(fn_istext),
56        "ISBLANK" => Some(fn_isblank),
57        "ISERROR" => Some(fn_iserror),
58        "VALUE" => Some(fn_value),
59        "TEXT" => Some(fn_text),
60        "SUMIF" => Some(math::fn_sumif),
61        "SUMIFS" => Some(math::fn_sumifs),
62        "ROUNDUP" => Some(math::fn_roundup),
63        "ROUNDDOWN" => Some(math::fn_rounddown),
64        "CEILING" => Some(math::fn_ceiling),
65        "FLOOR" => Some(math::fn_floor),
66        "SIGN" => Some(math::fn_sign),
67        "RAND" => Some(math::fn_rand),
68        "RANDBETWEEN" => Some(math::fn_randbetween),
69        "PI" => Some(math::fn_pi),
70        "LOG" => Some(math::fn_log),
71        "LOG10" => Some(math::fn_log10),
72        "LN" => Some(math::fn_ln),
73        "EXP" => Some(math::fn_exp),
74        "PRODUCT" => Some(math::fn_product),
75        "QUOTIENT" => Some(math::fn_quotient),
76        "FACT" => Some(math::fn_fact),
77        "AVERAGEIF" => Some(statistical::fn_averageif),
78        "AVERAGEIFS" => Some(statistical::fn_averageifs),
79        "COUNTBLANK" => Some(statistical::fn_countblank),
80        "COUNTIF" => Some(statistical::fn_countif),
81        "COUNTIFS" => Some(statistical::fn_countifs),
82        "MEDIAN" => Some(statistical::fn_median),
83        "MODE" => Some(statistical::fn_mode),
84        "LARGE" => Some(statistical::fn_large),
85        "SMALL" => Some(statistical::fn_small),
86        "RANK" => Some(statistical::fn_rank),
87        "ISERR" => Some(information::fn_iserr),
88        "ISNA" => Some(information::fn_isna),
89        "ISLOGICAL" => Some(information::fn_islogical),
90        "ISEVEN" => Some(information::fn_iseven),
91        "ISODD" => Some(information::fn_isodd),
92        "TYPE" => Some(information::fn_type),
93        "N" => Some(information::fn_n),
94        "NA" => Some(information::fn_na),
95        "ERROR.TYPE" => Some(information::fn_error_type),
96        "CONCAT" => Some(text::fn_concat),
97        "FIND" => Some(text::fn_find),
98        "SEARCH" => Some(text::fn_search),
99        "SUBSTITUTE" => Some(text::fn_substitute),
100        "REPLACE" => Some(text::fn_replace),
101        "REPT" => Some(text::fn_rept),
102        "EXACT" => Some(text::fn_exact),
103        "T" => Some(text::fn_t),
104        "PROPER" => Some(text::fn_proper),
105        "TRUE" => Some(logical::fn_true),
106        "FALSE" => Some(logical::fn_false),
107        "IFERROR" => Some(logical::fn_iferror),
108        "IFNA" => Some(logical::fn_ifna),
109        "IFS" => Some(logical::fn_ifs),
110        "SWITCH" => Some(logical::fn_switch),
111        "XOR" => Some(logical::fn_xor),
112        "DATE" => Some(date_time::fn_date),
113        "TODAY" => Some(date_time::fn_today),
114        "NOW" => Some(date_time::fn_now),
115        "YEAR" => Some(date_time::fn_year),
116        "MONTH" => Some(date_time::fn_month),
117        "DAY" => Some(date_time::fn_day),
118        "HOUR" => Some(date_time::fn_hour),
119        "MINUTE" => Some(date_time::fn_minute),
120        "SECOND" => Some(date_time::fn_second),
121        "DATEDIF" => Some(date_time::fn_datedif),
122        "EDATE" => Some(date_time::fn_edate),
123        "EOMONTH" => Some(date_time::fn_eomonth),
124        "DATEVALUE" => Some(date_time::fn_datevalue),
125        "WEEKDAY" => Some(date_time::fn_weekday),
126        "WEEKNUM" => Some(date_time::fn_weeknum),
127        "NETWORKDAYS" => Some(date_time::fn_networkdays),
128        "WORKDAY" => Some(date_time::fn_workday),
129        "VLOOKUP" => Some(lookup::fn_vlookup),
130        "HLOOKUP" => Some(lookup::fn_hlookup),
131        "INDEX" => Some(lookup::fn_index),
132        "MATCH" => Some(lookup::fn_match),
133        "LOOKUP" => Some(lookup::fn_lookup),
134        "ROW" => Some(lookup::fn_row),
135        "COLUMN" => Some(lookup::fn_column),
136        "ROWS" => Some(lookup::fn_rows),
137        "COLUMNS" => Some(lookup::fn_columns),
138        "CHOOSE" => Some(lookup::fn_choose),
139        "ADDRESS" => Some(lookup::fn_address),
140        "FV" => Some(financial::fn_fv),
141        "PV" => Some(financial::fn_pv),
142        "NPV" => Some(financial::fn_npv),
143        "IRR" => Some(financial::fn_irr),
144        "PMT" => Some(financial::fn_pmt),
145        "IPMT" => Some(financial::fn_ipmt),
146        "PPMT" => Some(financial::fn_ppmt),
147        "RATE" => Some(financial::fn_rate),
148        "NPER" => Some(financial::fn_nper),
149        "DB" => Some(financial::fn_db),
150        "DDB" => Some(financial::fn_ddb),
151        "SLN" => Some(financial::fn_sln),
152        "SYD" => Some(financial::fn_syd),
153        "EFFECT" => Some(financial::fn_effect),
154        "NOMINAL" => Some(financial::fn_nominal),
155        "DOLLARDE" => Some(financial::fn_dollarde),
156        "DOLLARFR" => Some(financial::fn_dollarfr),
157        "CUMIPMT" => Some(financial::fn_cumipmt),
158        "CUMPRINC" => Some(financial::fn_cumprinc),
159        "XNPV" => Some(financial::fn_xnpv),
160        "XIRR" => Some(financial::fn_xirr),
161        "BIN2DEC" => Some(engineering::fn_bin2dec),
162        "BIN2HEX" => Some(engineering::fn_bin2hex),
163        "BIN2OCT" => Some(engineering::fn_bin2oct),
164        "DEC2BIN" => Some(engineering::fn_dec2bin),
165        "DEC2HEX" => Some(engineering::fn_dec2hex),
166        "DEC2OCT" => Some(engineering::fn_dec2oct),
167        "HEX2BIN" => Some(engineering::fn_hex2bin),
168        "HEX2DEC" => Some(engineering::fn_hex2dec),
169        "HEX2OCT" => Some(engineering::fn_hex2oct),
170        "OCT2BIN" => Some(engineering::fn_oct2bin),
171        "OCT2DEC" => Some(engineering::fn_oct2dec),
172        "OCT2HEX" => Some(engineering::fn_oct2hex),
173        "DELTA" => Some(engineering::fn_delta),
174        "GESTEP" => Some(engineering::fn_gestep),
175        "ERF" => Some(engineering::fn_erf),
176        "ERFC" => Some(engineering::fn_erfc),
177        "COMPLEX" => Some(engineering::fn_complex),
178        "IMREAL" => Some(engineering::fn_imreal),
179        "IMAGINARY" => Some(engineering::fn_imaginary),
180        "IMABS" => Some(engineering::fn_imabs),
181        "IMARGUMENT" => Some(engineering::fn_imargument),
182        "IMCONJUGATE" => Some(engineering::fn_imconjugate),
183        "IMSUM" => Some(engineering::fn_imsum),
184        "IMSUB" => Some(engineering::fn_imsub),
185        "IMPRODUCT" => Some(engineering::fn_improduct),
186        "IMDIV" => Some(engineering::fn_imdiv),
187        "IMPOWER" => Some(engineering::fn_impower),
188        "IMSQRT" => Some(engineering::fn_imsqrt),
189        "CONVERT" => Some(engineering::fn_convert),
190        "BESSELI" => Some(engineering::fn_besseli),
191        "BESSELJ" => Some(engineering::fn_besselj),
192        "BESSELK" => Some(engineering::fn_besselk),
193        "BESSELY" => Some(engineering::fn_bessely),
194        _ => None,
195    }
196}
197
198/// Verify that `args` has between `min` and `max` entries (inclusive).
199pub fn check_arg_count(name: &str, args: &[Expr], min: usize, max: usize) -> Result<()> {
200    if args.len() < min || args.len() > max {
201        let expected = if min == max {
202            format!("{min}")
203        } else {
204            format!("{min}..{max}")
205        };
206        return Err(Error::WrongArgCount {
207            name: name.to_string(),
208            expected,
209            got: args.len(),
210        });
211    }
212    Ok(())
213}
214
215/// Check whether a cell value matches a criteria string.
216///
217/// Criteria format: ">5", "<=10", "<>text", "=exact", plain "text".
218/// Numeric comparisons are used when both sides parse as numbers;
219/// otherwise strings are compared case-insensitively. Wildcard `*` and
220/// `?` at the start/end of plain text are supported in a simplified form.
221pub fn matches_criteria(cell_value: &CellValue, criteria: &str) -> bool {
222    if criteria.is_empty() {
223        return matches!(cell_value, CellValue::Empty);
224    }
225
226    let (op, val_str) = if let Some(rest) = criteria.strip_prefix("<=") {
227        ("<=", rest)
228    } else if let Some(rest) = criteria.strip_prefix(">=") {
229        (">=", rest)
230    } else if let Some(rest) = criteria.strip_prefix("<>") {
231        ("<>", rest)
232    } else if let Some(rest) = criteria.strip_prefix('<') {
233        ("<", rest)
234    } else if let Some(rest) = criteria.strip_prefix('>') {
235        (">", rest)
236    } else if let Some(rest) = criteria.strip_prefix('=') {
237        ("=", rest)
238    } else {
239        ("=", criteria)
240    };
241
242    let cell_num = coerce_to_number(cell_value).ok();
243    let crit_num: Option<f64> = val_str.parse().ok();
244
245    if let (Some(cn), Some(crn)) = (cell_num, crit_num) {
246        return match op {
247            "<=" => cn <= crn,
248            ">=" => cn >= crn,
249            "<>" => (cn - crn).abs() > f64::EPSILON,
250            "<" => cn < crn,
251            ">" => cn > crn,
252            "=" => (cn - crn).abs() < f64::EPSILON,
253            _ => false,
254        };
255    }
256
257    let cell_str = coerce_to_string(cell_value).to_ascii_lowercase();
258    let crit_lower = val_str.to_ascii_lowercase();
259
260    match op {
261        "=" => {
262            if crit_lower.contains('*') || crit_lower.contains('?') {
263                wildcard_match(&cell_str, &crit_lower)
264            } else {
265                cell_str == crit_lower
266            }
267        }
268        "<>" => {
269            if crit_lower.contains('*') || crit_lower.contains('?') {
270                !wildcard_match(&cell_str, &crit_lower)
271            } else {
272                cell_str != crit_lower
273            }
274        }
275        "<" => cell_str < crit_lower,
276        ">" => cell_str > crit_lower,
277        "<=" => cell_str <= crit_lower,
278        ">=" => cell_str >= crit_lower,
279        _ => false,
280    }
281}
282
283fn wildcard_match(text: &str, pattern: &str) -> bool {
284    let t: Vec<char> = text.chars().collect();
285    let p: Vec<char> = pattern.chars().collect();
286    let (tlen, plen) = (t.len(), p.len());
287    let mut dp = vec![vec![false; plen + 1]; tlen + 1];
288    dp[0][0] = true;
289    for j in 1..=plen {
290        if p[j - 1] == '*' {
291            dp[0][j] = dp[0][j - 1];
292        }
293    }
294    for i in 1..=tlen {
295        for j in 1..=plen {
296            if p[j - 1] == '*' {
297                dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
298            } else if p[j - 1] == '?' || p[j - 1] == t[i - 1] {
299                dp[i][j] = dp[i - 1][j - 1];
300            }
301        }
302    }
303    dp[tlen][plen]
304}
305
306/// Expand a single argument expression into a flat list of CellValues.
307pub fn collect_criteria_range_values(arg: &Expr, ctx: &mut Evaluator) -> Result<Vec<CellValue>> {
308    match arg {
309        Expr::Range { start, end } => ctx.expand_range(start, end),
310        _ => {
311            let v = ctx.eval_expr(arg)?;
312            Ok(vec![v])
313        }
314    }
315}
316
317// -- Aggregate functions --
318
319fn fn_sum(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
320    check_arg_count("SUM", args, 1, 255)?;
321    let nums = ctx.collect_numbers(args)?;
322    Ok(CellValue::Number(nums.iter().sum()))
323}
324
325fn fn_average(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
326    check_arg_count("AVERAGE", args, 1, 255)?;
327    let nums = ctx.collect_numbers(args)?;
328    if nums.is_empty() {
329        return Ok(CellValue::Error("#DIV/0!".to_string()));
330    }
331    let sum: f64 = nums.iter().sum();
332    Ok(CellValue::Number(sum / nums.len() as f64))
333}
334
335fn fn_count(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
336    check_arg_count("COUNT", args, 1, 255)?;
337    let values = ctx.flatten_args_to_values(args)?;
338    let count = values
339        .iter()
340        .filter(|v| matches!(v, CellValue::Number(_) | CellValue::Date(_)))
341        .count();
342    Ok(CellValue::Number(count as f64))
343}
344
345fn fn_counta(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
346    check_arg_count("COUNTA", args, 1, 255)?;
347    let values = ctx.flatten_args_to_values(args)?;
348    let count = values
349        .iter()
350        .filter(|v| !matches!(v, CellValue::Empty))
351        .count();
352    Ok(CellValue::Number(count as f64))
353}
354
355fn fn_min(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
356    check_arg_count("MIN", args, 1, 255)?;
357    let nums = ctx.collect_numbers(args)?;
358    if nums.is_empty() {
359        return Ok(CellValue::Number(0.0));
360    }
361    let min = nums.iter().copied().fold(f64::INFINITY, f64::min);
362    Ok(CellValue::Number(min))
363}
364
365fn fn_max(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
366    check_arg_count("MAX", args, 1, 255)?;
367    let nums = ctx.collect_numbers(args)?;
368    if nums.is_empty() {
369        return Ok(CellValue::Number(0.0));
370    }
371    let max = nums.iter().copied().fold(f64::NEG_INFINITY, f64::max);
372    Ok(CellValue::Number(max))
373}
374
375// -- Logical functions --
376
377fn fn_if(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
378    check_arg_count("IF", args, 1, 3)?;
379    let cond = ctx.eval_expr(&args[0])?;
380    let truth = crate::formula::eval::coerce_to_bool(&cond)?;
381    if truth {
382        if args.len() > 1 {
383            ctx.eval_expr(&args[1])
384        } else {
385            Ok(CellValue::Bool(true))
386        }
387    } else if args.len() > 2 {
388        ctx.eval_expr(&args[2])
389    } else {
390        Ok(CellValue::Bool(false))
391    }
392}
393
394fn fn_and(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
395    check_arg_count("AND", args, 1, 255)?;
396    let values = ctx.flatten_args_to_values(args)?;
397    for v in &values {
398        if matches!(v, CellValue::Empty) {
399            continue;
400        }
401        if !crate::formula::eval::coerce_to_bool(v)? {
402            return Ok(CellValue::Bool(false));
403        }
404    }
405    Ok(CellValue::Bool(true))
406}
407
408fn fn_or(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
409    check_arg_count("OR", args, 1, 255)?;
410    let values = ctx.flatten_args_to_values(args)?;
411    for v in &values {
412        if matches!(v, CellValue::Empty) {
413            continue;
414        }
415        if crate::formula::eval::coerce_to_bool(v)? {
416            return Ok(CellValue::Bool(true));
417        }
418    }
419    Ok(CellValue::Bool(false))
420}
421
422fn fn_not(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
423    check_arg_count("NOT", args, 1, 1)?;
424    let v = ctx.eval_expr(&args[0])?;
425    let b = crate::formula::eval::coerce_to_bool(&v)?;
426    Ok(CellValue::Bool(!b))
427}
428
429// -- Math functions --
430
431fn fn_abs(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
432    check_arg_count("ABS", args, 1, 1)?;
433    let v = ctx.eval_expr(&args[0])?;
434    let n = crate::formula::eval::coerce_to_number(&v)?;
435    Ok(CellValue::Number(n.abs()))
436}
437
438fn fn_int(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
439    check_arg_count("INT", args, 1, 1)?;
440    let v = ctx.eval_expr(&args[0])?;
441    let n = crate::formula::eval::coerce_to_number(&v)?;
442    Ok(CellValue::Number(n.floor()))
443}
444
445fn fn_round(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
446    check_arg_count("ROUND", args, 2, 2)?;
447    let v = ctx.eval_expr(&args[0])?;
448    let d = ctx.eval_expr(&args[1])?;
449    let n = crate::formula::eval::coerce_to_number(&v)?;
450    let digits = crate::formula::eval::coerce_to_number(&d)? as i32;
451    let factor = 10f64.powi(digits);
452    Ok(CellValue::Number((n * factor).round() / factor))
453}
454
455fn fn_mod(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
456    check_arg_count("MOD", args, 2, 2)?;
457    let a = crate::formula::eval::coerce_to_number(&ctx.eval_expr(&args[0])?)?;
458    let b = crate::formula::eval::coerce_to_number(&ctx.eval_expr(&args[1])?)?;
459    if b == 0.0 {
460        return Ok(CellValue::Error("#DIV/0!".to_string()));
461    }
462    // Excel MOD: result has the sign of the divisor.
463    let result = a - (a / b).floor() * b;
464    Ok(CellValue::Number(result))
465}
466
467fn fn_power(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
468    check_arg_count("POWER", args, 2, 2)?;
469    let base = crate::formula::eval::coerce_to_number(&ctx.eval_expr(&args[0])?)?;
470    let exp = crate::formula::eval::coerce_to_number(&ctx.eval_expr(&args[1])?)?;
471    Ok(CellValue::Number(base.powf(exp)))
472}
473
474fn fn_sqrt(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
475    check_arg_count("SQRT", args, 1, 1)?;
476    let n = crate::formula::eval::coerce_to_number(&ctx.eval_expr(&args[0])?)?;
477    if n < 0.0 {
478        return Ok(CellValue::Error("#NUM!".to_string()));
479    }
480    Ok(CellValue::Number(n.sqrt()))
481}
482
483// -- Text functions --
484
485fn fn_len(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
486    check_arg_count("LEN", args, 1, 1)?;
487    let v = ctx.eval_expr(&args[0])?;
488    let s = crate::formula::eval::coerce_to_string(&v);
489    Ok(CellValue::Number(s.len() as f64))
490}
491
492fn fn_lower(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
493    check_arg_count("LOWER", args, 1, 1)?;
494    let v = ctx.eval_expr(&args[0])?;
495    let s = crate::formula::eval::coerce_to_string(&v);
496    Ok(CellValue::String(s.to_lowercase()))
497}
498
499fn fn_upper(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
500    check_arg_count("UPPER", args, 1, 1)?;
501    let v = ctx.eval_expr(&args[0])?;
502    let s = crate::formula::eval::coerce_to_string(&v);
503    Ok(CellValue::String(s.to_uppercase()))
504}
505
506fn fn_trim(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
507    check_arg_count("TRIM", args, 1, 1)?;
508    let v = ctx.eval_expr(&args[0])?;
509    let s = crate::formula::eval::coerce_to_string(&v);
510    // Excel TRIM removes leading/trailing spaces and collapses internal runs.
511    let trimmed: String = s.split_whitespace().collect::<Vec<_>>().join(" ");
512    Ok(CellValue::String(trimmed))
513}
514
515fn fn_left(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
516    check_arg_count("LEFT", args, 1, 2)?;
517    let v = ctx.eval_expr(&args[0])?;
518    let s = crate::formula::eval::coerce_to_string(&v);
519    let n = if args.len() > 1 {
520        crate::formula::eval::coerce_to_number(&ctx.eval_expr(&args[1])?)? as usize
521    } else {
522        1
523    };
524    let result: String = s.chars().take(n).collect();
525    Ok(CellValue::String(result))
526}
527
528fn fn_right(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
529    check_arg_count("RIGHT", args, 1, 2)?;
530    let v = ctx.eval_expr(&args[0])?;
531    let s = crate::formula::eval::coerce_to_string(&v);
532    let n = if args.len() > 1 {
533        crate::formula::eval::coerce_to_number(&ctx.eval_expr(&args[1])?)? as usize
534    } else {
535        1
536    };
537    let chars: Vec<char> = s.chars().collect();
538    let start = chars.len().saturating_sub(n);
539    let result: String = chars[start..].iter().collect();
540    Ok(CellValue::String(result))
541}
542
543fn fn_mid(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
544    check_arg_count("MID", args, 3, 3)?;
545    let v = ctx.eval_expr(&args[0])?;
546    let s = crate::formula::eval::coerce_to_string(&v);
547    let start = crate::formula::eval::coerce_to_number(&ctx.eval_expr(&args[1])?)? as usize;
548    let count = crate::formula::eval::coerce_to_number(&ctx.eval_expr(&args[2])?)? as usize;
549    if start < 1 {
550        return Ok(CellValue::Error("#VALUE!".to_string()));
551    }
552    let result: String = s.chars().skip(start - 1).take(count).collect();
553    Ok(CellValue::String(result))
554}
555
556fn fn_concatenate(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
557    check_arg_count("CONCATENATE", args, 1, 255)?;
558    let mut result = String::new();
559    for arg in args {
560        let v = ctx.eval_expr(arg)?;
561        result.push_str(&crate::formula::eval::coerce_to_string(&v));
562    }
563    Ok(CellValue::String(result))
564}
565
566// -- Information functions --
567
568fn fn_isnumber(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
569    check_arg_count("ISNUMBER", args, 1, 1)?;
570    let v = ctx.eval_expr(&args[0])?;
571    Ok(CellValue::Bool(matches!(
572        v,
573        CellValue::Number(_) | CellValue::Date(_)
574    )))
575}
576
577fn fn_istext(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
578    check_arg_count("ISTEXT", args, 1, 1)?;
579    let v = ctx.eval_expr(&args[0])?;
580    Ok(CellValue::Bool(matches!(v, CellValue::String(_))))
581}
582
583fn fn_isblank(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
584    check_arg_count("ISBLANK", args, 1, 1)?;
585    let v = ctx.eval_expr(&args[0])?;
586    Ok(CellValue::Bool(matches!(v, CellValue::Empty)))
587}
588
589fn fn_iserror(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
590    check_arg_count("ISERROR", args, 1, 1)?;
591    let v = ctx.eval_expr(&args[0])?;
592    Ok(CellValue::Bool(matches!(v, CellValue::Error(_))))
593}
594
595// -- Conversion functions --
596
597fn fn_value(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
598    check_arg_count("VALUE", args, 1, 1)?;
599    let v = ctx.eval_expr(&args[0])?;
600    match crate::formula::eval::coerce_to_number(&v) {
601        Ok(n) => Ok(CellValue::Number(n)),
602        Err(_) => Ok(CellValue::Error("#VALUE!".to_string())),
603    }
604}
605
606fn fn_text(args: &[Expr], ctx: &mut Evaluator) -> Result<CellValue> {
607    check_arg_count("TEXT", args, 2, 2)?;
608    let v = ctx.eval_expr(&args[0])?;
609    let _fmt = ctx.eval_expr(&args[1])?;
610    // Simplified: just convert to string representation (full format codes not implemented).
611    Ok(CellValue::String(crate::formula::eval::coerce_to_string(
612        &v,
613    )))
614}