mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-27 20:26:48 -06:00
refactor(calc): move eval.rs items to eval/
This commit is contained in:
+12
-1433
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
||||
use super::*;
|
||||
|
||||
/// The aggregates that can fold a reference without building an array.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum Agg {
|
||||
Sum,
|
||||
Count,
|
||||
Average,
|
||||
}
|
||||
|
||||
impl Agg {
|
||||
pub(super) fn of(name: &str) -> Option<Agg> {
|
||||
match name {
|
||||
"SUM" => Some(Agg::Sum),
|
||||
"COUNT" => Some(Agg::Count),
|
||||
"AVERAGE" => Some(Agg::Average),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a folded aggregate carries from one value to the next.
|
||||
#[derive(Default)]
|
||||
pub(super) struct Acc {
|
||||
pub(super) sum: f64,
|
||||
pub(super) count: u64,
|
||||
}
|
||||
|
||||
impl Acc {
|
||||
/// Takes a value found inside a reference: only numbers count. `Err` is an error value that
|
||||
/// ends `SUM` and `AVERAGE`; `COUNT` skips errors.
|
||||
pub(super) fn add_cell(&mut self, kind: Agg, v: &Value) -> Result<(), CellError> {
|
||||
match v {
|
||||
Value::Number(n) => {
|
||||
self.sum += n;
|
||||
self.count = self.count.saturating_add(1);
|
||||
Ok(())
|
||||
}
|
||||
Value::Error(e) if kind != Agg::Count => Err(*e),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn finish(&self, kind: Agg) -> Value {
|
||||
match kind {
|
||||
Agg::Sum => Value::number(self.sum),
|
||||
Agg::Count => Value::number(self.count as f64),
|
||||
Agg::Average if self.count == 0 => Value::Error(CellError::Div0),
|
||||
Agg::Average => Value::number(self.sum / self.count as f64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- operators
|
||||
|
||||
pub(super) fn map_unary(v: &Value, f: &dyn Fn(f64) -> Value, plus: bool, sys: gridcraft_core::DateSystem) -> Value {
|
||||
match v {
|
||||
Value::Array(a) => Value::from(Array { rows: a.rows, cols: a.cols, data: a.data.iter().map(|x| map_unary(x, f, plus, sys)).collect() }),
|
||||
Value::Error(e) => Value::Error(*e),
|
||||
Value::Text(_) if plus => v.clone(),
|
||||
other => match other.to_number_in(sys) {
|
||||
Ok(n) => f(n),
|
||||
Err(e) => Value::Error(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Element-wise combination with Excel's broadcasting.
|
||||
pub fn zip2(a: &Value, b: &Value, f: impl Fn(&Value, &Value) -> Value) -> Value {
|
||||
match (a, b) {
|
||||
(Value::Array(x), Value::Array(y)) => {
|
||||
let rows = broadcast_dim(x.rows, y.rows);
|
||||
let cols = broadcast_dim(x.cols, y.cols);
|
||||
let Some(n) = broadcast_len(rows, cols) else { return Value::Error(CellError::Num) };
|
||||
let mut data = Vec::with_capacity(n);
|
||||
for r in 0..rows {
|
||||
for c in 0..cols {
|
||||
data.push(f(&x.get_broadcast(r, c), &y.get_broadcast(r, c)));
|
||||
}
|
||||
}
|
||||
Value::from(Array { rows, cols, data })
|
||||
}
|
||||
(Value::Array(x), s) => Value::from(Array { rows: x.rows, cols: x.cols, data: x.data.iter().map(|v| f(v, s)).collect() }),
|
||||
(s, Value::Array(y)) => Value::from(Array { rows: y.rows, cols: y.cols, data: y.data.iter().map(|v| f(s, v)).collect() }),
|
||||
(x, y) => f(x, y),
|
||||
}
|
||||
}
|
||||
|
||||
/// Cell count of a `rows`×`cols` broadcast result, or `None` when it would exceed [`MAX_CELLS`].
|
||||
/// Each operand passes its own size cap, but their broadcast product can be far larger
|
||||
/// (`SEQUENCE(1048576)*SEQUENCE(1,16384)` is 17 billion cells), so it is checked before allocating.
|
||||
pub(super) fn broadcast_len(rows: usize, cols: usize) -> Option<usize> {
|
||||
rows.checked_mul(cols).filter(|&n| n as u64 <= MAX_CELLS)
|
||||
}
|
||||
|
||||
fn broadcast_dim(a: usize, b: usize) -> usize {
|
||||
if a == 1 {
|
||||
b
|
||||
} else if b == 1 {
|
||||
a
|
||||
} else {
|
||||
a.max(b)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn zip3(c: &Array, t: &Value, f: &Value, g: impl Fn(&Value, &Value, &Value) -> Value) -> Value {
|
||||
let dims = |v: &Value| if let Value::Array(a) = v { (a.rows, a.cols) } else { (1, 1) };
|
||||
let (tr, tc) = dims(t);
|
||||
let (fr, fc) = dims(f);
|
||||
let rows = broadcast_dim(broadcast_dim(c.rows, tr), fr);
|
||||
let cols = broadcast_dim(broadcast_dim(c.cols, tc), fc);
|
||||
let Some(n) = broadcast_len(rows, cols) else { return Value::Error(CellError::Num) };
|
||||
let get = |v: &Value, r: usize, col: usize| if let Value::Array(a) = v { a.get_broadcast(r, col) } else { v.clone() };
|
||||
let mut data = Vec::with_capacity(n);
|
||||
for r in 0..rows {
|
||||
for col in 0..cols {
|
||||
data.push(g(&c.get_broadcast(r, col), &get(t, r, col), &get(f, r, col)));
|
||||
}
|
||||
}
|
||||
Value::from(Array { rows, cols, data })
|
||||
}
|
||||
|
||||
/// One cell of a LAMBDA helper's result, or `None` for an array of more than one value: arrays
|
||||
/// can't nest, so the whole helper gives `#CALC!`, as in Excel.
|
||||
pub(super) fn single(v: Value) -> Option<Value> {
|
||||
match v {
|
||||
Value::Array(a) if a.rows * a.cols > 1 => None,
|
||||
v => Some(v.scalar()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binary_values(op: BinOp, x: &Value, y: &Value, sys: gridcraft_core::DateSystem) -> Value {
|
||||
zip2(x, y, |a, b| binary_scalar(op, a, b, sys))
|
||||
}
|
||||
|
||||
fn binary_scalar(op: BinOp, a: &Value, b: &Value, sys: gridcraft_core::DateSystem) -> Value {
|
||||
if let Value::Error(e) = a {
|
||||
return Value::Error(*e);
|
||||
}
|
||||
if let Value::Error(e) = b {
|
||||
return Value::Error(*e);
|
||||
}
|
||||
let num = |f: fn(f64, f64) -> Value| match (a.to_number_in(sys), b.to_number_in(sys)) {
|
||||
(Ok(x), Ok(y)) => f(x, y),
|
||||
(Err(e), _) | (_, Err(e)) => Value::Error(e),
|
||||
};
|
||||
use std::cmp::Ordering::*;
|
||||
match op {
|
||||
BinOp::Add => num(|x, y| Value::number(x + y)),
|
||||
BinOp::Sub => num(|x, y| Value::number(x - y)),
|
||||
BinOp::Mul => num(|x, y| Value::number(x * y)),
|
||||
BinOp::Div => num(|x, y| if y == 0.0 { Value::Error(CellError::Div0) } else { Value::number(x / y) }),
|
||||
BinOp::Pow => num(|x, y| {
|
||||
if x == 0.0 && y == 0.0 {
|
||||
Value::Error(CellError::Num)
|
||||
} else if x == 0.0 && y < 0.0 {
|
||||
Value::Error(CellError::Div0)
|
||||
} else {
|
||||
Value::number(x.powf(y))
|
||||
}
|
||||
}),
|
||||
BinOp::Concat => match (a.to_text(), b.to_text()) {
|
||||
(Ok(x), Ok(y)) => {
|
||||
if x.len() + y.len() > 32767 * 4 {
|
||||
Value::Error(CellError::Value)
|
||||
} else {
|
||||
Value::text(x + &y)
|
||||
}
|
||||
}
|
||||
(Err(e), _) | (_, Err(e)) => Value::Error(e),
|
||||
},
|
||||
BinOp::Eq => Value::Bool(compare(a, b) == Equal),
|
||||
BinOp::Ne => Value::Bool(compare(a, b) != Equal),
|
||||
BinOp::Lt => Value::Bool(compare(a, b) == Less),
|
||||
BinOp::Gt => Value::Bool(compare(a, b) == Greater),
|
||||
BinOp::Le => Value::Bool(compare(a, b) != Greater),
|
||||
BinOp::Ge => Value::Bool(compare(a, b) != Less),
|
||||
BinOp::Range | BinOp::Union | BinOp::Intersect => Value::Error(CellError::Value),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
use super::array::{Acc, Agg, zip2, zip3};
|
||||
use super::format::same_kind;
|
||||
use super::special::{ev_shape, resized_sum_range, resizes_sum_range};
|
||||
use super::*;
|
||||
|
||||
impl<'h> Evaluator<'h> {
|
||||
pub(super) fn call(&mut self, name: &str, args: &[Expr]) -> Ev {
|
||||
match name {
|
||||
"IF" => {
|
||||
if args.is_empty() || args.len() > 3 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let c = self.arg_val(args, 0).unwrap_or_default();
|
||||
if let Value::Array(arr) = &c {
|
||||
// Array condition: element-wise.
|
||||
let t = self.arg_val(args, 1).unwrap_or(Value::Bool(true));
|
||||
let f = if args.len() > 2 { self.arg_val(args, 2).unwrap_or(Value::Bool(false)) } else { Value::Bool(false) };
|
||||
return Ev::V(zip3(arr, &t, &f, |c, t, f| match c.to_bool() {
|
||||
Ok(true) => t.clone(),
|
||||
Ok(false) => f.clone(),
|
||||
Err(e) => Value::Error(e),
|
||||
}));
|
||||
}
|
||||
match c.to_bool() {
|
||||
Ok(true) => match args.get(1) {
|
||||
Some(Expr::Missing) => Ev::V(Value::Number(0.0)),
|
||||
Some(e) => self.eval(e),
|
||||
None => Ev::V(Value::Bool(true)),
|
||||
},
|
||||
Ok(false) => match args.get(2) {
|
||||
Some(Expr::Missing) => Ev::V(Value::Number(0.0)),
|
||||
Some(e) => self.eval(e),
|
||||
None => Ev::V(Value::Bool(false)),
|
||||
},
|
||||
Err(e) => err(e),
|
||||
}
|
||||
}
|
||||
"IFS" => {
|
||||
if args.len() < 2 || !args.len().is_multiple_of(2) {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
for pair in args.chunks(2) {
|
||||
let [c, v] = pair else { break };
|
||||
let cv = self.value(c);
|
||||
match cv.scalar().to_bool() {
|
||||
Ok(true) => return self.eval(v),
|
||||
Ok(false) => {}
|
||||
Err(e) => return err(e),
|
||||
}
|
||||
}
|
||||
err(CellError::NA)
|
||||
}
|
||||
"IFERROR" | "IFNA" => {
|
||||
if args.len() != 2 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let v = self.arg_ev(args, 0).unwrap_or(Ev::V(Value::Empty));
|
||||
let na_only = name == "IFNA";
|
||||
let catch = |e: CellError| if na_only { e == CellError::NA } else { true };
|
||||
match &v {
|
||||
Ev::V(Value::Error(e)) if catch(*e) => self.eval(&args[1]),
|
||||
Ev::V(Value::Array(a)) if a.data.iter().any(|x| x.as_error().is_some_and(catch)) => {
|
||||
let alt = self.arg_val(args, 1).unwrap_or_default();
|
||||
let data = a.data.iter().map(|x| if x.as_error().is_some_and(catch) { alt.scalar() } else { x.clone() }).collect();
|
||||
Ev::V(Array::new(a.rows, a.cols, data).map(Value::from).unwrap_or(Value::Error(CellError::Value)))
|
||||
}
|
||||
Ev::R(_) => {
|
||||
let val = self.deref(v.clone());
|
||||
match val {
|
||||
Value::Error(e) if catch(e) => self.eval(&args[1]),
|
||||
Value::Array(ref a) if a.data.iter().any(|x| x.as_error().is_some_and(catch)) => {
|
||||
let alt = self.arg_val(args, 1).unwrap_or_default();
|
||||
let data = a.data.iter().map(|x| if x.as_error().is_some_and(catch) { alt.scalar() } else { x.clone() }).collect();
|
||||
Ev::V(Array::new(a.rows, a.cols, data).map(Value::from).unwrap_or(Value::Error(CellError::Value)))
|
||||
}
|
||||
_ => v,
|
||||
}
|
||||
}
|
||||
_ => v,
|
||||
}
|
||||
}
|
||||
"CHOOSE" => {
|
||||
if args.len() < 2 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let index = self.value(&args[0]);
|
||||
if let Value::Array(_) = index {
|
||||
// An array of indexes picks element by element: `CHOOSE({1,2},A1:A3,B1:B3)`
|
||||
// is a two-column array. The library CHOOSE lifts over its arguments.
|
||||
let Some(spec) = gridcraft_functions::lookup(name) else { return err(CellError::Name) };
|
||||
let mut evaluated = vec![Arg { value: index, from_ref: false }];
|
||||
for a in &args[1..] {
|
||||
let value = self.value(a);
|
||||
evaluated.push(Arg { value, from_ref: false });
|
||||
}
|
||||
let mut ctx = FnCtx { ev: self, criteria: None, lambda: None };
|
||||
return gridcraft_functions::call(spec, &evaluated, &mut ctx).pipe(Ev::V);
|
||||
}
|
||||
let i = match index.to_number() {
|
||||
Ok(n) => n.trunc() as i64,
|
||||
Err(e) => return err(e),
|
||||
};
|
||||
if i < 1 || i as usize >= args.len() {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
self.eval(&args[i as usize])
|
||||
}
|
||||
"SWITCH" => {
|
||||
if args.len() < 3 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let v = self.arg_val(args, 0).unwrap_or_default().scalar();
|
||||
if let Value::Error(e) = v {
|
||||
return err(e);
|
||||
}
|
||||
let rest = &args[1..];
|
||||
let mut i = 0;
|
||||
while i + 1 < rest.len() {
|
||||
let cand = self.value(&rest[i]).scalar();
|
||||
if !cand.is_error() && compare(&v, &cand) == std::cmp::Ordering::Equal && same_kind(&v, &cand) {
|
||||
return self.eval(&rest[i + 1]);
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
if rest.len() % 2 == 1 { rest.last().map_or(err(CellError::NA), |d| self.eval(d)) } else { err(CellError::NA) }
|
||||
}
|
||||
"ROW" | "COLUMN" => {
|
||||
let is_row = name == "ROW";
|
||||
if args.is_empty() || matches!(args.first(), Some(Expr::Missing)) {
|
||||
return Ev::V(Value::number(if is_row { self.at.row + 1 } else { self.at.col + 1 } as f64));
|
||||
}
|
||||
match self.eval(&args[0]) {
|
||||
Ev::R(a) => {
|
||||
let Some(a) = a.first() else { return err(CellError::Ref) };
|
||||
let r = self.trim_lines(a);
|
||||
if is_row {
|
||||
let rows: Vec<Value> = (r.start.row..=r.end.row).map(|x| Value::number(x as f64 + 1.0)).collect();
|
||||
if rows.len() == 1 { Ev::V(rows.into_iter().next().unwrap_or_default()) } else { Ev::V(Array::column(rows).into()) }
|
||||
} else {
|
||||
let cols: Vec<Value> = (r.start.col..=r.end.col).map(|x| Value::number(x as f64 + 1.0)).collect();
|
||||
if cols.len() == 1 { Ev::V(cols.into_iter().next().unwrap_or_default()) } else { Ev::V(Array::row(cols).into()) }
|
||||
}
|
||||
}
|
||||
Ev::V(Value::Error(e)) => err(e),
|
||||
_ => err(CellError::Value),
|
||||
}
|
||||
}
|
||||
"ROWS" | "COLUMNS" => {
|
||||
if args.len() != 1 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let is_rows = name == "ROWS";
|
||||
match self.eval(&args[0]) {
|
||||
Ev::R(a) => match a.as_slice() {
|
||||
[a] => Ev::V(Value::number(if is_rows { a.range.height() } else { a.range.width() } as f64)),
|
||||
_ => err(CellError::Ref),
|
||||
},
|
||||
Ev::V(Value::Array(arr)) => Ev::V(Value::number(if is_rows { arr.rows } else { arr.cols } as f64)),
|
||||
Ev::V(Value::Error(e)) => err(e),
|
||||
Ev::V(_) => Ev::V(Value::Number(1.0)),
|
||||
Ev::L(_) => err(CellError::Value),
|
||||
}
|
||||
}
|
||||
"AREAS" => match args.first().map(|a| self.eval(a)) {
|
||||
Some(Ev::R(a)) => Ev::V(Value::number(a.len() as f64)),
|
||||
Some(Ev::V(Value::Error(e))) => err(e),
|
||||
_ => err(CellError::Value),
|
||||
},
|
||||
"ISREF" => Ev::V(Value::Bool(matches!(args.first().map(|a| self.eval(a)), Some(Ev::R(_))))),
|
||||
"ISFORMULA" | "FORMULATEXT" => {
|
||||
let Some(Ev::R(a)) = args.first().map(|a| self.eval(a)) else { return err(CellError::NA) };
|
||||
let Some(a) = a.first() else { return err(CellError::NA) };
|
||||
let f = self.host.workbook().sheet(a.sheet).and_then(|s| s.cell(a.range.start)).and_then(|c| c.formula.clone());
|
||||
if name == "ISFORMULA" {
|
||||
Ev::V(Value::Bool(f.is_some()))
|
||||
} else {
|
||||
match f {
|
||||
Some(f) => Ev::V(Value::text(format!("={}", f.text))),
|
||||
None => err(CellError::NA),
|
||||
}
|
||||
}
|
||||
}
|
||||
"SHEET" | "SHEETS" => {
|
||||
let wb = self.host.workbook();
|
||||
if args.is_empty() {
|
||||
return Ev::V(Value::number(if name == "SHEET" { self.sheet + 1 } else { wb.sheets.len() } as f64));
|
||||
}
|
||||
match self.eval(&args[0]) {
|
||||
Ev::R(a) => {
|
||||
if name == "SHEET" {
|
||||
a.first().map_or(err(CellError::Ref), |a| Ev::V(Value::number(a.sheet as f64 + 1.0)))
|
||||
} else {
|
||||
let mut s: Vec<usize> = a.iter().map(|x| x.sheet).collect();
|
||||
s.dedup();
|
||||
Ev::V(Value::number(s.len() as f64))
|
||||
}
|
||||
}
|
||||
Ev::V(Value::Text(t)) if name == "SHEET" => {
|
||||
let wb = self.host.workbook();
|
||||
wb.sheet_index(&t).map_or(err(CellError::NA), |i| Ev::V(Value::number(i as f64 + 1.0)))
|
||||
}
|
||||
_ => err(CellError::NA),
|
||||
}
|
||||
}
|
||||
"OFFSET" => self.offset(args),
|
||||
"INDIRECT" => self.indirect(args),
|
||||
"INDEX" => self.index(args),
|
||||
"XLOOKUP" => self.xlookup(args),
|
||||
"CELL" => self.cell_info(args),
|
||||
"TRIMRANGE" => self.trimrange(args),
|
||||
"SUBTOTAL" | "AGGREGATE" => self.subtotal(name, args),
|
||||
"TEXT" => {
|
||||
if args.len() != 2 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let v = self.arg_val(args, 0).unwrap_or_default();
|
||||
let f = self.arg_val(args, 1).unwrap_or_default();
|
||||
let sys = self.host.workbook().date_system;
|
||||
let one = |v: &Value, f: &Value| -> Value {
|
||||
if let Value::Error(e) = v {
|
||||
return Value::Error(*e);
|
||||
}
|
||||
let code = match f.to_text() {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Value::Error(e),
|
||||
};
|
||||
match gridcraft_numfmt::text_function(v, &code, sys) {
|
||||
Ok(s) => Value::text(s),
|
||||
Err(e) => Value::Error(e),
|
||||
}
|
||||
};
|
||||
Ev::V(zip2(&v, &f, one))
|
||||
}
|
||||
"LET" => self.let_(args),
|
||||
"LAMBDA" => {
|
||||
if args.is_empty() {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let mut params = Vec::new();
|
||||
for p in &args[..args.len() - 1] {
|
||||
match p {
|
||||
Expr::Name(n) => params.push(n.trim_start_matches('[').trim_end_matches(']').to_string()),
|
||||
_ => return err(CellError::Value),
|
||||
}
|
||||
}
|
||||
let Some(body) = args.last() else { return err(CellError::Value) };
|
||||
Ev::L(Arc::new(Lambda { params, body: body.clone(), env: self.env.clone() }))
|
||||
}
|
||||
"ISOMITTED" => {
|
||||
let r = matches!(args.first(), Some(Expr::Name(n)) if self.env.iter().rev().find(|(k, _)| gridcraft_formula::same_name(k, n)).is_some_and(|(_, v)| matches!(v, Ev::V(Value::Empty))));
|
||||
Ev::V(Value::Bool(r))
|
||||
}
|
||||
"MAP" | "REDUCE" | "SCAN" | "BYROW" | "BYCOL" | "MAKEARRAY" => self.higher_order(name, args),
|
||||
"GROUPBY" | "PIVOTBY" => {
|
||||
// The aggregation may be a function given by name (`SUM`): pass it as its name.
|
||||
let i = if name == "GROUPBY" { 2 } else { 3 };
|
||||
match args.get(i).and_then(|a| self.function_by_name(a)) {
|
||||
Some(f) => {
|
||||
let mut args = args.to_vec();
|
||||
if let Some(a) = args.get_mut(i) {
|
||||
*a = Expr::Text(f.into());
|
||||
}
|
||||
self.builtin(name, &args)
|
||||
}
|
||||
None => self.builtin(name, args),
|
||||
}
|
||||
}
|
||||
"NOW" | "TODAY" | "RAND" | "RANDBETWEEN" | "RANDARRAY" => {
|
||||
self.host.note_volatile();
|
||||
self.builtin(name, args)
|
||||
}
|
||||
_ => self.builtin(name, args),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn builtin(&mut self, name: &str, args: &[Expr]) -> Ev {
|
||||
let Some(spec) = gridcraft_functions::lookup(name) else {
|
||||
// A defined name holding a LAMBDA, or a LET-bound lambda.
|
||||
return match self.name(name) {
|
||||
Ev::L(l) => self.apply_lambda(&l, args),
|
||||
Ev::V(Value::Error(x)) => err(x),
|
||||
_ => err(CellError::Name),
|
||||
};
|
||||
};
|
||||
if args.len() < spec.min_args || args.len() > spec.max_args {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
if let Some(kind) = Agg::of(spec.name) {
|
||||
return self.aggregate(kind, args);
|
||||
}
|
||||
let mut evaluated = Vec::with_capacity(args.len());
|
||||
// SUMIF/AVERAGEIF: the size of the criteria range (argument 1), which the sum range takes.
|
||||
let mut criteria_shape = None;
|
||||
// Database functions: the criteria range (argument 2), whose formulas are computed criteria.
|
||||
let mut criteria = None;
|
||||
// GROUPBY/PIVOTBY: the LAMBDA given as their function.
|
||||
let mut lambda = None;
|
||||
let lambda_at = match name {
|
||||
"GROUPBY" => Some(2),
|
||||
"PIVOTBY" => Some(3),
|
||||
_ => None,
|
||||
};
|
||||
for (i, a) in args.iter().enumerate() {
|
||||
let mut ev = self.eval(a);
|
||||
if lambda_at == Some(i)
|
||||
&& let Ev::L(l) = &ev
|
||||
{
|
||||
lambda = Some(l.clone());
|
||||
ev = Ev::V(Value::Empty);
|
||||
}
|
||||
if spec.category == gridcraft_functions::Category::Database
|
||||
&& i == 2
|
||||
&& let Ev::R(areas) = &ev
|
||||
&& let [area] = areas.as_slice()
|
||||
{
|
||||
criteria = Some(*area);
|
||||
}
|
||||
if resizes_sum_range(name, args.len()) {
|
||||
if i == 0 {
|
||||
criteria_shape = ev_shape(&ev);
|
||||
} else if i == 2
|
||||
&& let (Some((h, w)), Ev::R(areas)) = (criteria_shape, &ev)
|
||||
&& let [area] = areas.as_slice()
|
||||
{
|
||||
ev = Ev::R(vec![Area { sheet: area.sheet, range: resized_sum_range(area.range, h, w) }]);
|
||||
}
|
||||
}
|
||||
let from_ref = matches!(ev, Ev::R(_));
|
||||
let value = match ev {
|
||||
Ev::R(areas) if areas.len() > 1 => {
|
||||
// Multi-area references (unions): flatten into one row for aggregates.
|
||||
let mut vals = Vec::new();
|
||||
for ar in &areas {
|
||||
match self.area_value(ar) {
|
||||
Value::Array(a) => vals.extend(a.data.iter().cloned()),
|
||||
v => vals.push(v),
|
||||
}
|
||||
}
|
||||
Value::from(Array::row(vals))
|
||||
}
|
||||
ev => self.deref(ev),
|
||||
};
|
||||
evaluated.push(Arg { value, from_ref });
|
||||
}
|
||||
let mut ctx = FnCtx { ev: self, criteria, lambda };
|
||||
gridcraft_functions::call(spec, &evaluated, &mut ctx).pipe(Ev::V)
|
||||
}
|
||||
|
||||
/// Performs aggregation operations without copying its ranges: blocks are folded over the stored
|
||||
/// cells, so no array is built and there is no `MAX_CELLS` limit. Arguments are evaluated and read one
|
||||
/// after another, as the array path does, so volatile formulas (like `RAND`) see the same order.
|
||||
/// Only `SUM`, `COUNT` and `AVG` are supported currently.
|
||||
fn aggregate(&mut self, kind: Agg, args: &[Expr]) -> Ev {
|
||||
let mut acc = Acc::default();
|
||||
let mut first_error = None;
|
||||
for a in args {
|
||||
let ev = self.eval(a);
|
||||
let steps = match ev {
|
||||
Ev::R(areas) => areas.iter().map(|area| self.aggregate_area(kind, area, &mut acc)).collect(),
|
||||
ev => {
|
||||
let value = self.deref(ev);
|
||||
vec![self.aggregate_value(kind, value, false, &mut acc)]
|
||||
}
|
||||
};
|
||||
if first_error.is_none() {
|
||||
first_error = steps.into_iter().find_map(Result::err);
|
||||
}
|
||||
}
|
||||
first_error.unwrap_or_else(|| Ev::V(acc.finish(kind)))
|
||||
}
|
||||
|
||||
/// Adds one area to `acc`; `Err` is the result the function ends with.
|
||||
fn aggregate_area(&mut self, kind: Agg, a: &Area, acc: &mut Acc) -> Result<(), Ev> {
|
||||
if !a.range.is_single() {
|
||||
let mut error = None;
|
||||
let walked = self.host.visit_range(a.sheet, a.range, &mut |v| match acc.add_cell(kind, v) {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
error = Some(e);
|
||||
false
|
||||
}
|
||||
});
|
||||
if walked {
|
||||
return error.map_or(Ok(()), |e| Err(err(e)));
|
||||
}
|
||||
}
|
||||
let value = self.area_value(a);
|
||||
self.aggregate_value(kind, value, true, acc)
|
||||
}
|
||||
|
||||
/// Adds one argument's value by calling `SUM` and `COUNT` on it, so coercion and error rules
|
||||
/// stay in one place (`AVERAGE` of an argument is its `SUM` over its `COUNT`).
|
||||
fn aggregate_value(&mut self, kind: Agg, value: Value, from_ref: bool, acc: &mut Acc) -> Result<(), Ev> {
|
||||
let (Some(sum), Some(count)) = (gridcraft_functions::lookup("SUM"), gridcraft_functions::lookup("COUNT")) else {
|
||||
return Err(err(CellError::Name));
|
||||
};
|
||||
let args = [Arg { value, from_ref }];
|
||||
if kind != Agg::Count {
|
||||
match gridcraft_functions::call(sum, &args, &mut FnCtx { ev: self, criteria: None, lambda: None }) {
|
||||
Value::Number(n) => acc.sum += n,
|
||||
other => return Err(Ev::V(other)),
|
||||
}
|
||||
}
|
||||
if kind != Agg::Sum {
|
||||
match gridcraft_functions::call(count, &args, &mut FnCtx { ev: self, criteria: None, lambda: None }) {
|
||||
Value::Number(n) => acc.count = acc.count.saturating_add(n as u64),
|
||||
other => return Err(Ev::V(other)),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A worksheet function given by name where a function is expected (`BYROW(A1:C3,SUM)`,
|
||||
/// `GROUPBY(A2:A9,B2:B9,SUM)`): its name, unless a LET/LAMBDA variable or a defined name has
|
||||
/// that name.
|
||||
pub(super) fn function_by_name(&self, e: &Expr) -> Option<String> {
|
||||
let Expr::Name(n) = e else { return None };
|
||||
if self.env.iter().any(|(k, _)| gridcraft_formula::same_name(k, n)) || self.host.workbook().name(n, self.sheet).is_some() {
|
||||
return None;
|
||||
}
|
||||
let u = n.to_ascii_uppercase();
|
||||
gridcraft_functions::lookup(&u).map(|_| u)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use super::*;
|
||||
|
||||
/// CELL("format") code for a number format.
|
||||
pub(super) fn cell_format_code(code: &str) -> String {
|
||||
let c = code.to_ascii_lowercase();
|
||||
if c == "general" {
|
||||
return "G".into();
|
||||
}
|
||||
if c.contains('%') {
|
||||
return format!("P{}", decimals(&c));
|
||||
}
|
||||
if c.contains('e') && c.contains('0') {
|
||||
return format!("S{}", decimals(&c));
|
||||
}
|
||||
if c.contains('$') {
|
||||
return format!("C{}", decimals(&c));
|
||||
}
|
||||
if c.contains('y') || c.contains('d') {
|
||||
return "D1".into();
|
||||
}
|
||||
if c.contains('h') {
|
||||
return "D9".into();
|
||||
}
|
||||
if c.contains(',') {
|
||||
return format!(",{}", decimals(&c));
|
||||
}
|
||||
format!("F{}", decimals(&c))
|
||||
}
|
||||
|
||||
fn decimals(c: &str) -> usize {
|
||||
c.split_once('.').map_or(0, |(_, d)| d.chars().take_while(|ch| *ch == '0').count())
|
||||
}
|
||||
|
||||
pub(super) fn same_kind(a: &Value, b: &Value) -> bool {
|
||||
std::mem::discriminant(a) == std::mem::discriminant(b) || (a.is_empty() || b.is_empty())
|
||||
}
|
||||
|
||||
/// Converts an R1C1 reference text to A1 (for INDIRECT(…, FALSE)). `Ok(None)` means the text is
|
||||
/// not R1C1; `Err` means it is R1C1 but points past the sheet.
|
||||
pub fn r1c1_to_a1(s: &str, at: CellRef) -> Result<Option<String>, CellError> {
|
||||
let (sheet, body) = match s.rsplit_once('!') {
|
||||
Some((sh, b)) => (format!("{sh}!"), b),
|
||||
None => (String::new(), s),
|
||||
};
|
||||
let conv = |part: &str| -> Result<Option<String>, CellError> {
|
||||
let u = part.to_ascii_uppercase();
|
||||
let Some((rpart, cpart)) = u.strip_prefix('R').and_then(|rest| rest.split_once('C')) else { return Ok(None) };
|
||||
// `None`: not R1C1. `Some(None)`: R1C1, but past the sheet.
|
||||
let num = |p: &str, base: u32, max: u32| -> Option<Option<(u32, bool)>> {
|
||||
let (v, absolute) = if p.is_empty() {
|
||||
(Some(i64::from(base)), false)
|
||||
} else if let Some(inner) = p.strip_prefix('[').and_then(|x| x.strip_suffix(']')) {
|
||||
(i64::from(base).checked_add(inner.parse().ok()?), false)
|
||||
} else {
|
||||
(p.parse::<u32>().ok()?.checked_sub(1).map(i64::from), true)
|
||||
};
|
||||
Some(v.filter(|v| (0..i64::from(max)).contains(v)).map(|v| (v as u32, absolute)))
|
||||
};
|
||||
let (Some(r), Some(c)) = (num(rpart, at.row, MAX_ROWS), num(cpart, at.col, MAX_COLS)) else { return Ok(None) };
|
||||
let (Some((r, ra)), Some((c, ca))) = (r, c) else { return Err(CellError::Ref) };
|
||||
Ok(Some(format!("{}{}{}{}", if ca { "$" } else { "" }, gridcraft_core::col_to_letters(c), if ra { "$" } else { "" }, r + 1)))
|
||||
};
|
||||
let out = match body.split_once(':') {
|
||||
Some((a, b)) => match (conv(a)?, conv(b)?) {
|
||||
(Some(a), Some(b)) => format!("{a}:{b}"),
|
||||
_ => return Ok(None),
|
||||
},
|
||||
None => match conv(body)? {
|
||||
Some(x) => x,
|
||||
None => return Ok(None),
|
||||
},
|
||||
};
|
||||
Ok(Some(format!("{sheet}{out}")))
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
use super::array::{broadcast_len, single};
|
||||
use super::*;
|
||||
|
||||
impl<'h> Evaluator<'h> {
|
||||
pub fn apply_lambda(&mut self, l: &Lambda, args: &[Expr]) -> Ev {
|
||||
// Too many or too few argument positions is #VALUE!. An explicitly empty position, as in
|
||||
// `(1,)`, is `Expr::Missing` and still counts as a position (it binds as omitted). `f()`
|
||||
// parses to no arguments, which for a one-parameter lambda can also be read as one empty
|
||||
// position, so that call still binds the parameter as omitted.
|
||||
let positions_ok = args.len() == l.params.len() || (args.is_empty() && l.params.len() == 1);
|
||||
if !positions_ok {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let mut bound = Vec::with_capacity(l.params.len());
|
||||
for (i, p) in l.params.iter().enumerate() {
|
||||
let v = match args.get(i) {
|
||||
Some(Expr::Missing) | None => Ev::V(Value::Empty),
|
||||
Some(e) => self.eval(e),
|
||||
};
|
||||
bound.push((p.clone(), v));
|
||||
}
|
||||
self.call_lambda_values(l, bound)
|
||||
}
|
||||
|
||||
pub(super) fn call_lambda_values(&mut self, l: &Lambda, bound: Vec<(String, Ev)>) -> Ev {
|
||||
if self.calls >= MAX_LAMBDA_CALLS {
|
||||
return err(CellError::Num);
|
||||
}
|
||||
// #2382: one charge per invocation, so a cell's total work stays bounded however the
|
||||
// lambdas nest. Foregrounding the charge here means every path into a body (direct
|
||||
// `(2,3)` application, `MAP`/`REDUCE`/`BYROW` helpers, D-function criteria) meters.
|
||||
self.fuel = match self.fuel.checked_sub(1) {
|
||||
Some(f) => f,
|
||||
None => return err(CellError::Calc),
|
||||
};
|
||||
self.calls += 1;
|
||||
let saved = std::mem::replace(&mut self.env, l.env.clone());
|
||||
self.env.extend(bound);
|
||||
let r = self.eval(&l.body);
|
||||
self.env = saved;
|
||||
self.calls -= 1;
|
||||
r
|
||||
}
|
||||
|
||||
pub(super) fn let_(&mut self, args: &[Expr]) -> Ev {
|
||||
if args.len() < 3 || args.len().is_multiple_of(2) {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let base = self.env.len();
|
||||
for pair in args[..args.len() - 1].chunks(2) {
|
||||
let [Expr::Name(n), v] = pair else {
|
||||
self.env.truncate(base);
|
||||
return err(CellError::Value);
|
||||
};
|
||||
let val = self.eval(v);
|
||||
self.env.push((n.clone(), val));
|
||||
}
|
||||
let r = match args.last() {
|
||||
Some(body) => self.eval(body),
|
||||
None => err(CellError::Value),
|
||||
};
|
||||
self.env.truncate(base);
|
||||
r
|
||||
}
|
||||
|
||||
/// The function argument of a LAMBDA helper; a function given by name takes `arity`
|
||||
/// arguments.
|
||||
fn lambda_arg(&mut self, e: Option<&Expr>, arity: usize) -> Option<Arc<Lambda>> {
|
||||
if let Some(f) = e.and_then(|x| self.function_by_name(x)) {
|
||||
let params: Vec<String> = (0..arity).map(|i| format!("_xleta.{i}")).collect();
|
||||
let body = Expr::Call(f, params.iter().map(|p| Expr::Name(p.clone())).collect());
|
||||
return Some(Arc::new(Lambda { params, body, env: Vec::new() }));
|
||||
}
|
||||
match e.map(|x| self.eval(x)) {
|
||||
Some(Ev::L(l)) => Some(l),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn higher_order(&mut self, name: &str, args: &[Expr]) -> Ev {
|
||||
let as_array = |v: Value| -> Arc<Array> {
|
||||
match v {
|
||||
Value::Array(a) => a,
|
||||
other => Arc::new(Array::scalar(other)),
|
||||
}
|
||||
};
|
||||
match name {
|
||||
"MAP" => {
|
||||
if args.len() < 2 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let Some(l) = self.lambda_arg(args.last(), args.len() - 1) else { return err(CellError::Value) };
|
||||
let arrays: Vec<Arc<Array>> = args[..args.len() - 1].iter().map(|a| as_array(self.value(a))).collect();
|
||||
let rows = arrays.iter().map(|a| a.rows).max().unwrap_or(1);
|
||||
let cols = arrays.iter().map(|a| a.cols).max().unwrap_or(1);
|
||||
let Some(n) = broadcast_len(rows, cols) else { return err(CellError::Num) };
|
||||
// The LAMBDA takes one parameter per array.
|
||||
if l.params.len() != arrays.len() {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for r in 0..rows {
|
||||
for c in 0..cols {
|
||||
let bound = l.params.iter().zip(arrays.iter()).map(|(p, a)| (p.clone(), Ev::V(a.get_broadcast(r, c)))).collect();
|
||||
let v = self.call_lambda_values(&l, bound);
|
||||
let Some(one) = single(self.deref(v)) else { return err(CellError::Calc) };
|
||||
out.push(one);
|
||||
}
|
||||
}
|
||||
Ev::V(Array::new(rows, cols, out).map(Value::from).unwrap_or(Value::Error(CellError::Calc)))
|
||||
}
|
||||
"REDUCE" | "SCAN" => {
|
||||
if args.len() != 3 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let mut acc = self.value(&args[0]);
|
||||
let arr = as_array(self.value(&args[1]));
|
||||
let Some(l) = self.lambda_arg(args.get(2), 2) else { return err(CellError::Value) };
|
||||
if l.params.len() != 2 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let mut scan = Vec::with_capacity(arr.data.len());
|
||||
for v in arr.data.iter() {
|
||||
let bound = vec![(l.params[0].clone(), Ev::V(acc.clone())), (l.params[1].clone(), Ev::V(v.clone()))];
|
||||
let r = self.call_lambda_values(&l, bound);
|
||||
acc = self.deref(r);
|
||||
if name == "SCAN" {
|
||||
let Some(one) = single(acc.clone()) else { return err(CellError::Calc) };
|
||||
scan.push(one);
|
||||
}
|
||||
}
|
||||
if name == "SCAN" {
|
||||
Ev::V(Array::new(arr.rows, arr.cols, scan).map(Value::from).unwrap_or(Value::Error(CellError::Calc)))
|
||||
} else {
|
||||
Ev::V(acc)
|
||||
}
|
||||
}
|
||||
"BYROW" | "BYCOL" => {
|
||||
if args.len() != 2 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let arr = as_array(self.value(&args[0]));
|
||||
let Some(l) = self.lambda_arg(args.get(1), 1) else { return err(CellError::Value) };
|
||||
let Some(p) = l.params.first().cloned() else { return err(CellError::Value) };
|
||||
if l.params.len() != 1 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let by_row = name == "BYROW";
|
||||
let n = if by_row { arr.rows } else { arr.cols };
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let slice: Vec<Value> = if by_row {
|
||||
(0..arr.cols).map(|c| arr.get(i, c).cloned().unwrap_or_default()).collect()
|
||||
} else {
|
||||
(0..arr.rows).map(|r| arr.get(r, i).cloned().unwrap_or_default()).collect()
|
||||
};
|
||||
let a = if by_row { Array::row(slice) } else { Array::column(slice) };
|
||||
let r = self.call_lambda_values(&l, vec![(p.clone(), Ev::V(a.into()))]);
|
||||
let Some(one) = single(self.deref(r)) else { return err(CellError::Calc) };
|
||||
out.push(one);
|
||||
}
|
||||
let a = if by_row { Array::column(out) } else { Array::row(out) };
|
||||
Ev::V(a.into())
|
||||
}
|
||||
"MAKEARRAY" => {
|
||||
if args.len() != 3 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let (rows, cols) = match (self.arg_num(args, 0, 0.0), self.arg_num(args, 1, 0.0)) {
|
||||
(Ok(r), Ok(c)) => (r.trunc(), c.trunc()),
|
||||
(Err(e), _) | (_, Err(e)) => return err(e),
|
||||
};
|
||||
if rows < 1.0 || cols < 1.0 || rows * cols > MAX_CELLS as f64 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let Some(l) = self.lambda_arg(args.get(2), 2) else { return err(CellError::Value) };
|
||||
if l.params.len() != 2 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let (rows, cols) = (rows as usize, cols as usize);
|
||||
let mut out = Vec::with_capacity(rows * cols);
|
||||
for r in 0..rows {
|
||||
for c in 0..cols {
|
||||
let bound = vec![
|
||||
(l.params[0].clone(), Ev::V(Value::number(r as f64 + 1.0))),
|
||||
(l.params[1].clone(), Ev::V(Value::number(c as f64 + 1.0))),
|
||||
];
|
||||
let v = self.call_lambda_values(&l, bound);
|
||||
let Some(one) = single(self.deref(v)) else { return err(CellError::Calc) };
|
||||
out.push(one);
|
||||
}
|
||||
}
|
||||
Ev::V(Array::new(rows, cols, out).map(Value::from).unwrap_or(Value::Error(CellError::Calc)))
|
||||
}
|
||||
_ => err(CellError::Name),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
use super::format::{cell_format_code, r1c1_to_a1};
|
||||
use super::*;
|
||||
|
||||
impl<'h> Evaluator<'h> {
|
||||
/// Whole rows/columns trimmed for ROW()/COLUMN() arrays.
|
||||
pub(super) fn trim_lines(&mut self, a: &Area) -> RangeRef {
|
||||
if a.range.is_full_cols() || a.range.is_full_rows() { self.trim(a) } else { a.range }
|
||||
}
|
||||
|
||||
pub(super) fn offset(&mut self, args: &[Expr]) -> Ev {
|
||||
self.host.note_volatile();
|
||||
if args.len() < 3 || args.len() > 5 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let base = match self.eval(&args[0]) {
|
||||
Ev::R(a) if a.len() == 1 => a[0],
|
||||
Ev::V(Value::Error(e)) => return err(e),
|
||||
_ => return err(CellError::Value),
|
||||
};
|
||||
let rows = match self.arg_num(args, 1, 0.0) {
|
||||
Ok(n) => n.trunc() as i64,
|
||||
Err(e) => return err(e),
|
||||
};
|
||||
let cols = match self.arg_num(args, 2, 0.0) {
|
||||
Ok(n) => n.trunc() as i64,
|
||||
Err(e) => return err(e),
|
||||
};
|
||||
let h = match self.arg_num(args, 3, base.range.height() as f64) {
|
||||
Ok(n) => n.trunc() as i64,
|
||||
Err(e) => return err(e),
|
||||
};
|
||||
let w = match self.arg_num(args, 4, base.range.width() as f64) {
|
||||
Ok(n) => n.trunc() as i64,
|
||||
Err(e) => return err(e),
|
||||
};
|
||||
if h == 0 || w == 0 {
|
||||
return err(CellError::Ref);
|
||||
}
|
||||
let r0 = (base.range.start.row as i64).saturating_add(rows);
|
||||
let c0 = (base.range.start.col as i64).saturating_add(cols);
|
||||
let (r0, r1) = if h > 0 { (r0, r0.saturating_add(h - 1)) } else { (r0.saturating_add(h + 1), r0) };
|
||||
let (c0, c1) = if w > 0 { (c0, c0.saturating_add(w - 1)) } else { (c0.saturating_add(w + 1), c0) };
|
||||
if r0 < 0 || c0 < 0 || r1 >= MAX_ROWS as i64 || c1 >= MAX_COLS as i64 {
|
||||
return err(CellError::Ref);
|
||||
}
|
||||
Ev::R(vec![Area { sheet: base.sheet, range: RangeRef::new(CellRef::new(r0 as u32, c0 as u32), CellRef::new(r1 as u32, c1 as u32)) }])
|
||||
}
|
||||
|
||||
pub(super) fn indirect(&mut self, args: &[Expr]) -> Ev {
|
||||
self.host.note_volatile();
|
||||
if args.is_empty() || args.len() > 2 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let text = match self.arg_val(args, 0).map(|v| v.scalar().to_text()) {
|
||||
Some(Ok(t)) => t,
|
||||
Some(Err(e)) => return err(e),
|
||||
None => return err(CellError::Value),
|
||||
};
|
||||
let a1 = match args.get(1) {
|
||||
None | Some(Expr::Missing) => true,
|
||||
Some(e) => {
|
||||
let v = self.value(e);
|
||||
v.scalar().to_bool().unwrap_or(true)
|
||||
}
|
||||
};
|
||||
// With a1 = FALSE, text that isn't R1C1 can still be a name, but never an A1 reference.
|
||||
let (text, a1_ok) = if a1 {
|
||||
(text, true)
|
||||
} else {
|
||||
match r1c1_to_a1(&text, self.at) {
|
||||
Ok(Some(a1_text)) => (a1_text, true),
|
||||
Ok(None) => (text, false),
|
||||
Err(e) => return err(e),
|
||||
}
|
||||
};
|
||||
match gridcraft_formula::parse(&text) {
|
||||
Ok(Expr::Ref(_)) if !a1_ok => err(CellError::Ref),
|
||||
Ok(e @ (Expr::Ref(_) | Expr::Name(_) | Expr::Struct(_))) => match self.eval(&e) {
|
||||
r @ Ev::R(_) => r,
|
||||
_ => err(CellError::Ref),
|
||||
},
|
||||
_ => err(CellError::Ref),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn index(&mut self, args: &[Expr]) -> Ev {
|
||||
if args.is_empty() || args.len() > 4 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let src = self.eval(&args[0]);
|
||||
let mut arg = |i: usize| match args.get(i) {
|
||||
None | Some(Expr::Missing) => None,
|
||||
Some(e) => Some(self.value(e)),
|
||||
};
|
||||
let (rows, cols) = (arg(1), arg(2));
|
||||
let area_n = match self.arg_num(args, 3, 1.0) {
|
||||
Ok(n) => n.trunc() as i64,
|
||||
Err(e) => return err(e),
|
||||
};
|
||||
let two = args.len() == 2;
|
||||
let num = |v: &Option<Value>| v.as_ref().map_or(Ok(0.0), |v| v.to_number()).map(|n| n.trunc() as i64);
|
||||
// Arrays of positions pick a value for each: INDEX(A1:C10,{1;2},{1,3}) is 2×2.
|
||||
if let (Some(Value::Array(_)), _) | (_, Some(Value::Array(_))) = (&rows, &cols) {
|
||||
let as_array = |v: &Option<Value>| match v {
|
||||
Some(Value::Array(a)) => Arc::clone(a),
|
||||
Some(v) => Arc::new(Array::scalar(v.clone())),
|
||||
None => Arc::new(Array::scalar(Value::Number(0.0))),
|
||||
};
|
||||
let (ra, ca) = (as_array(&rows), as_array(&cols));
|
||||
let (h, w) = (ra.rows.max(ca.rows), ra.cols.max(ca.cols));
|
||||
if (h as u64).saturating_mul(w as u64) > MAX_CELLS {
|
||||
return err(CellError::Num);
|
||||
}
|
||||
let mut out = Vec::with_capacity(h * w);
|
||||
for r in 0..h {
|
||||
for c in 0..w {
|
||||
let pos = (num(&Some(ra.get_broadcast(r, c))), num(&Some(ca.get_broadcast(r, c))));
|
||||
let v = match pos {
|
||||
(Ok(row), Ok(col)) => {
|
||||
let one = self.index_one(src.clone(), row, col, area_n, two);
|
||||
match self.deref(one) {
|
||||
Value::Array(a) if a.rows * a.cols > 1 => Value::Error(CellError::Calc),
|
||||
v => v.scalar(),
|
||||
}
|
||||
}
|
||||
(Err(e), _) | (_, Err(e)) => Value::Error(e),
|
||||
};
|
||||
out.push(v);
|
||||
}
|
||||
}
|
||||
return Ev::V(Array::new(h, w, out).map(Value::from).unwrap_or(Value::Error(CellError::Value)));
|
||||
}
|
||||
let (row, col) = match (num(&rows), num(&cols)) {
|
||||
(Ok(r), Ok(c)) => (r, c),
|
||||
(Err(e), _) | (_, Err(e)) => return err(e),
|
||||
};
|
||||
self.index_one(src, row, col, area_n, two)
|
||||
}
|
||||
|
||||
/// INDEX for one row and column position (0 = the whole column or row).
|
||||
fn index_one(&mut self, src: Ev, row: i64, col: i64, area_n: i64, two_args: bool) -> Ev {
|
||||
if row < 0 || col < 0 {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
match src {
|
||||
Ev::R(areas) => {
|
||||
let area = usize::try_from((area_n as u64).saturating_sub(1)).ok();
|
||||
let Some(a) = area.and_then(|i| areas.get(i)).copied() else { return err(CellError::Ref) };
|
||||
let r = a.range;
|
||||
// A single row or column with one index picks along it. An area with several rows
|
||||
// and columns needs both: `INDEX(A1:C3,2)` is #REF! in Excel (an empty third
|
||||
// argument, `INDEX(A1:C3,2,)`, still means the whole row). Arrays differ:
|
||||
// `INDEX({1,2;3,4},2)` is the row.
|
||||
if two_args && r.height() > 1 && r.width() > 1 {
|
||||
return err(CellError::Ref);
|
||||
}
|
||||
let (row, col) = if two_args && r.height() == 1 { (1, row) } else { (row, col) };
|
||||
if row as u64 > r.height() as u64 || col as u64 > r.width() as u64 {
|
||||
return err(CellError::Ref);
|
||||
}
|
||||
let (r0, r1) = if row == 0 { (r.start.row, r.end.row) } else { (r.start.row + row as u32 - 1, r.start.row + row as u32 - 1) };
|
||||
let (c0, c1) = if col == 0 { (r.start.col, r.end.col) } else { (r.start.col + col as u32 - 1, r.start.col + col as u32 - 1) };
|
||||
Ev::R(vec![Area { sheet: a.sheet, range: RangeRef::new(CellRef::new(r0, c0), CellRef::new(r1, c1)) }])
|
||||
}
|
||||
Ev::V(Value::Array(arr)) => {
|
||||
let (row, col) = if two_args && arr.rows == 1 { (1, row) } else { (row, col) };
|
||||
if row as usize > arr.rows || col as usize > arr.cols {
|
||||
return err(CellError::Ref);
|
||||
}
|
||||
match (row, col) {
|
||||
(0, 0) => Ev::V(Value::Array(arr)),
|
||||
(0, c) => Ev::V(Array::column((0..arr.rows).map(|r| arr.get(r, c as usize - 1).cloned().unwrap_or_default()).collect()).into()),
|
||||
(r, 0) => Ev::V(Array::row((0..arr.cols).map(|c| arr.get(r as usize - 1, c).cloned().unwrap_or_default()).collect()).into()),
|
||||
(r, c) => Ev::V(arr.get(r as usize - 1, c as usize - 1).cloned().unwrap_or(Value::Error(CellError::Ref))),
|
||||
}
|
||||
}
|
||||
Ev::V(Value::Error(e)) => err(e),
|
||||
Ev::V(v) => {
|
||||
if row <= 1 && col <= 1 {
|
||||
Ev::V(v)
|
||||
} else {
|
||||
err(CellError::Ref)
|
||||
}
|
||||
}
|
||||
Ev::L(_) => err(CellError::Value),
|
||||
}
|
||||
}
|
||||
|
||||
/// XLOOKUP whose result range is a reference returns a reference, as in Excel, so that it can
|
||||
/// end a range (`SUM(XLOOKUP(a,…):XLOOKUP(b,…))`) or feed CELL. Everything else (arrays of
|
||||
/// lookup values, array arguments, errors) goes to the library function.
|
||||
pub(super) fn xlookup(&mut self, args: &[Expr]) -> Ev {
|
||||
if (3..=6).contains(&args.len())
|
||||
&& let (Ev::R(look), Ev::R(ret)) = (self.eval(&args[1]), self.eval(&args[2]))
|
||||
&& let ([l], [r]) = (look.as_slice(), ret.as_slice())
|
||||
&& let Some(spec) = gridcraft_functions::lookup("XMATCH")
|
||||
{
|
||||
let key = self.value(&args[0]);
|
||||
let (l, r) = (*l, *r);
|
||||
let vertical = l.range.width() == 1 && l.range.height() > 1;
|
||||
let horizontal = l.range.height() == 1 && l.range.width() > 1;
|
||||
let same_len = if vertical { r.range.height() == l.range.height() } else { r.range.width() == l.range.width() };
|
||||
if !matches!(key, Value::Array(_)) && (vertical || horizontal) && same_len {
|
||||
let mut xargs = vec![Arg { value: key, from_ref: false }, Arg { value: self.area_value(&l), from_ref: true }];
|
||||
// An empty match_mode before a given search_mode is the default (0), not the end.
|
||||
let given = |i: usize| args.get(i).is_some_and(|e| !matches!(e, Expr::Missing));
|
||||
let last = if given(5) {
|
||||
5
|
||||
} else if given(4) {
|
||||
4
|
||||
} else {
|
||||
3
|
||||
};
|
||||
for i in 4..=last {
|
||||
match args.get(i) {
|
||||
Some(Expr::Missing) | None => xargs.push(Arg { value: Value::Number(0.0), from_ref: false }),
|
||||
Some(e) => {
|
||||
let value = self.value(e);
|
||||
xargs.push(Arg { value, from_ref: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut ctx = FnCtx { ev: self, criteria: None, lambda: None };
|
||||
return match gridcraft_functions::call(spec, &xargs, &mut ctx) {
|
||||
Value::Number(p) if p >= 1.0 => {
|
||||
let k = p as u32 - 1;
|
||||
let range = if vertical {
|
||||
RangeRef::new(
|
||||
CellRef::new(r.range.start.row + k, r.range.start.col),
|
||||
CellRef::new(r.range.start.row + k, r.range.end.col),
|
||||
)
|
||||
} else {
|
||||
RangeRef::new(
|
||||
CellRef::new(r.range.start.row, r.range.start.col + k),
|
||||
CellRef::new(r.range.end.row, r.range.start.col + k),
|
||||
)
|
||||
};
|
||||
Ev::R(vec![Area { sheet: r.sheet, range }])
|
||||
}
|
||||
Value::Error(CellError::NA) if args.get(3).is_some_and(|e| !matches!(e, Expr::Missing)) => self.eval(&args[3]),
|
||||
Value::Error(e) => err(e),
|
||||
_ => err(CellError::Value),
|
||||
};
|
||||
}
|
||||
}
|
||||
self.builtin("XLOOKUP", args)
|
||||
}
|
||||
|
||||
pub(super) fn cell_info(&mut self, args: &[Expr]) -> Ev {
|
||||
let kind = match self.arg_val(args, 0).map(|v| v.scalar().to_text()) {
|
||||
Some(Ok(t)) => t.to_ascii_lowercase(),
|
||||
Some(Err(e)) => return err(e),
|
||||
None => return err(CellError::Value),
|
||||
};
|
||||
let area = match args.get(1).map(|a| self.eval(a)) {
|
||||
Some(Ev::R(a)) => a.first().copied(),
|
||||
None => Some(Area { sheet: self.sheet, range: RangeRef::cell(self.at) }),
|
||||
_ => return err(CellError::Value),
|
||||
};
|
||||
let Some(a) = area else { return err(CellError::Ref) };
|
||||
let c = a.range.start;
|
||||
let wb = self.host.workbook();
|
||||
let sheet = wb.sheet(a.sheet);
|
||||
match kind.as_str() {
|
||||
"address" => Ev::V(Value::text(format!("${}${}", gridcraft_core::col_to_letters(c.col), c.row + 1))),
|
||||
"row" => Ev::V(Value::number(c.row as f64 + 1.0)),
|
||||
"col" => Ev::V(Value::number(c.col as f64 + 1.0)),
|
||||
"contents" => Ev::V(self.host.cell_value(a.sheet, c)),
|
||||
"type" => {
|
||||
let v = self.host.cell_value(a.sheet, c);
|
||||
Ev::V(Value::text(match v {
|
||||
Value::Empty => "b",
|
||||
Value::Text(_) => "l",
|
||||
_ => "v",
|
||||
}))
|
||||
}
|
||||
"width" => Ev::V(Value::number(sheet.map_or(8.43, |s| gridcraft_model::points_to_chars(s.col_width(c.col) as f64)).round())),
|
||||
// Excel's `<folder>/[<file>]<sheet>`, which the classic sheet-name formula splits on
|
||||
// the `]`. Empty until the workbook has a file of its own, as in Excel (#1489).
|
||||
"filename" => Ev::V(Value::text(match &wb.source_path {
|
||||
Some(file) => {
|
||||
let name = file.file_name().unwrap_or_default().to_string_lossy();
|
||||
let on = sheet.map_or("", |s| s.name.as_str());
|
||||
file.parent().unwrap_or(std::path::Path::new("")).join(format!("[{name}]{on}")).to_string_lossy().into_owned()
|
||||
}
|
||||
None => String::new(),
|
||||
})),
|
||||
"sheetname" => Ev::V(Value::text(sheet.map(|s| s.name.clone()).unwrap_or_default())),
|
||||
"protect" => Ev::V(Value::number(if sheet.is_some_and(|s| wb.styles.get(s.style_id(c)).protection.locked) { 1.0 } else { 0.0 })),
|
||||
"format" => {
|
||||
let code = sheet.map(|s| wb.styles.get(s.style_id(c)).num_fmt.as_str().to_string()).unwrap_or_default();
|
||||
Ev::V(Value::text(cell_format_code(&code)))
|
||||
}
|
||||
"color" | "parentheses" => Ev::V(Value::Number(0.0)),
|
||||
"prefix" => Ev::V(Value::text("")),
|
||||
_ => err(CellError::Value),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn subtotal(&mut self, name: &str, args: &[Expr]) -> Ev {
|
||||
// SUBTOTAL needs function_num and ref1; AGGREGATE needs function_num, options and ref1.
|
||||
let min_args = if name == "AGGREGATE" { 3 } else { 2 };
|
||||
if args.len() < min_args {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let code = match self.arg_num(args, 0, 0.0) {
|
||||
Ok(n) => n.trunc() as u32,
|
||||
Err(e) => return err(e),
|
||||
};
|
||||
let (func, options, rest) = if name == "AGGREGATE" {
|
||||
let opt = match self.arg_num(args, 1, 0.0) {
|
||||
Ok(n) => n.trunc() as u32,
|
||||
Err(e) => return err(e),
|
||||
};
|
||||
(code, opt, &args[2..])
|
||||
} else {
|
||||
let (f, ignore_hidden) = if code > 100 { (code - 100, true) } else { (code, false) };
|
||||
(f, if ignore_hidden { 5 } else { 0 }, &args[1..])
|
||||
};
|
||||
if !(1..=19).contains(&func) || (name == "SUBTOTAL" && func > 11) {
|
||||
return err(CellError::Value);
|
||||
}
|
||||
let skip_hidden = matches!(options, 1 | 3 | 5 | 7);
|
||||
// SUBTOTAL leaves out rows a filter hides whatever the function number (Microsoft's
|
||||
// SUBTOTAL documentation); 101-111 also leave out rows hidden by hand.
|
||||
let skip_filtered = name == "SUBTOTAL";
|
||||
let skip_errors = matches!(options, 2 | 3 | 6 | 7);
|
||||
// SUBTOTAL always leaves out nested SUBTOTAL/AGGREGATE cells (101-111 too); AGGREGATE does so for options 0-3.
|
||||
let skip_nested = name == "SUBTOTAL" || options <= 3;
|
||||
let mut values = Vec::new();
|
||||
let mut k = None;
|
||||
let refs = if name == "AGGREGATE" && func >= 14 { &rest[..1.min(rest.len())] } else { rest };
|
||||
if name == "AGGREGATE" && func >= 14 {
|
||||
k = rest.get(1).map(|e| self.value(e));
|
||||
}
|
||||
// Every cell visited (even one skipped as hidden, nested or an error) and every value
|
||||
// counts against one budget for the whole call. All arguments are evaluated (references
|
||||
// stay lazy) and charged before any cell is read: a value stored in XFD1048576 makes
|
||||
// `A:XFD` trim to the whole sheet, ~17 billion cells (#2175).
|
||||
enum Input {
|
||||
Cells(usize, RangeRef),
|
||||
Values(Value),
|
||||
}
|
||||
let mut inputs = Vec::new();
|
||||
let mut budget = MAX_CELLS;
|
||||
for e in refs {
|
||||
match self.eval(e) {
|
||||
Ev::R(areas) => {
|
||||
for a in areas {
|
||||
let range = self.trim(&a);
|
||||
budget = match budget.checked_sub(range.count()) {
|
||||
Some(left) => left,
|
||||
None => return err(CellError::Num),
|
||||
};
|
||||
inputs.push(Input::Cells(a.sheet, range));
|
||||
}
|
||||
}
|
||||
Ev::V(v) => {
|
||||
let n = match &v {
|
||||
Value::Array(a) => u64::try_from(a.data.len()).unwrap_or(u64::MAX),
|
||||
_ => 1,
|
||||
};
|
||||
budget = match budget.checked_sub(n) {
|
||||
Some(left) => left,
|
||||
None => return err(CellError::Num),
|
||||
};
|
||||
inputs.push(Input::Values(v));
|
||||
}
|
||||
Ev::L(_) => return err(CellError::Value),
|
||||
}
|
||||
}
|
||||
for input in inputs {
|
||||
match input {
|
||||
Input::Cells(sheet, range) => {
|
||||
for c in range.iter() {
|
||||
let (hidden, nested) = {
|
||||
let wb = self.host.workbook();
|
||||
let s = wb.sheet(sheet);
|
||||
let hidden =
|
||||
s.is_some_and(|s| if skip_hidden { s.is_row_hidden(c.row) } else { skip_filtered && s.is_row_filtered(c.row) });
|
||||
let nested = skip_nested
|
||||
&& s.and_then(|s| s.cell(c)).and_then(|cell| cell.formula.as_ref()).is_some_and(|f| {
|
||||
let u = f.text.to_ascii_uppercase();
|
||||
u.contains("SUBTOTAL(") || u.contains("AGGREGATE(")
|
||||
});
|
||||
(hidden, nested)
|
||||
};
|
||||
if hidden || nested {
|
||||
continue;
|
||||
}
|
||||
let v = self.host.cell_value(sheet, c);
|
||||
if skip_errors && v.is_error() {
|
||||
continue;
|
||||
}
|
||||
values.push(v);
|
||||
}
|
||||
}
|
||||
Input::Values(Value::Array(a)) => values.extend(a.data.iter().filter(|v| !(skip_errors && v.is_error())).cloned()),
|
||||
Input::Values(v) => values.push(v),
|
||||
}
|
||||
}
|
||||
if let Some(k) = k {
|
||||
return Ev::V(gridcraft_functions::aggregate_values_k(func, &values, &k));
|
||||
}
|
||||
Ev::V(gridcraft_functions::aggregate_values(func, &values))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
use super::*;
|
||||
|
||||
/// SUMIF and AVERAGEIF with a sum range: Excel sizes the sum range like the criteria range.
|
||||
pub(super) fn resizes_sum_range(name: &str, args: usize) -> bool {
|
||||
args == 3 && matches!(name, "SUMIF" | "AVERAGEIF")
|
||||
}
|
||||
|
||||
/// Height and width of a single-area reference or an array argument.
|
||||
pub(super) fn ev_shape(ev: &Ev) -> Option<(u32, u32)> {
|
||||
match ev {
|
||||
Ev::R(areas) => match areas.as_slice() {
|
||||
[a] => Some((a.range.height(), a.range.width())),
|
||||
_ => None,
|
||||
},
|
||||
Ev::V(Value::Array(a)) => Some((u32::try_from(a.rows).unwrap_or(u32::MAX), u32::try_from(a.cols).unwrap_or(u32::MAX))),
|
||||
Ev::V(_) => Some((1, 1)),
|
||||
Ev::L(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The range SUMIF/AVERAGEIF add up: from the sum range's top-left cell, as many rows and columns
|
||||
/// as the criteria range (`height` × `width`), cut off at the sheet's edge.
|
||||
pub fn resized_sum_range(sum: RangeRef, height: u32, width: u32) -> RangeRef {
|
||||
let row = sum.start.row.saturating_add(height.saturating_sub(1)).min(MAX_ROWS - 1);
|
||||
let col = sum.start.col.saturating_add(width.saturating_sub(1)).min(MAX_COLS - 1);
|
||||
RangeRef::new(sum.start, CellRef::new(row, col))
|
||||
}
|
||||
|
||||
/// Collects the static references of an expression with their sheets resolved; `dynamic` is
|
||||
/// set when the formula uses INDIRECT/OFFSET/names/tables whose targets can change.
|
||||
pub fn precedents(wb: &Workbook, sheet: usize, e: &Expr) -> (Vec<Area>, bool) {
|
||||
let mut out = Vec::new();
|
||||
let mut dynamic = false;
|
||||
let push = |out: &mut Vec<Area>, r: &Reference, range: RangeRef| match &r.sheet {
|
||||
SheetSel::Current => out.push(Area { sheet, range }),
|
||||
SheetSel::Named(n) => {
|
||||
if let Some(i) = wb.sheet_index(n) {
|
||||
out.push(Area { sheet: i, range });
|
||||
}
|
||||
}
|
||||
SheetSel::Span(a, b) => {
|
||||
if let (Some(i), Some(j)) = (wb.sheet_index(a), wb.sheet_index(b)) {
|
||||
for s in i.min(j)..=i.max(j) {
|
||||
out.push(Area { sheet: s, range });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
e.walk(&mut |x| match x {
|
||||
Expr::Ref(r) => push(&mut out, r, r.range()),
|
||||
// The cells SUMIF/AVERAGEIF really read from the sum range (see `resized_sum_range`); when
|
||||
// the criteria range's size isn't known statically, recalculate the formula every time.
|
||||
Expr::Call(n, args) if resizes_sum_range(n, args.len()) => match (args.first(), args.get(2)) {
|
||||
(Some(Expr::Ref(c)), Some(Expr::Ref(s))) => {
|
||||
let (crit, sum) = (c.range(), s.range());
|
||||
push(&mut out, s, resized_sum_range(sum, crit.height(), crit.width()));
|
||||
}
|
||||
(_, Some(Expr::Number(_) | Expr::Text(_) | Expr::Bool(_) | Expr::Error(_) | Expr::Array(_) | Expr::Missing)) => {}
|
||||
_ => dynamic = true,
|
||||
},
|
||||
Expr::Call(n, _)
|
||||
if matches!(n.as_str(), "INDIRECT" | "OFFSET" | "NOW" | "TODAY" | "RAND" | "RANDBETWEEN" | "RANDARRAY" | "CELL" | "INFO") =>
|
||||
{
|
||||
dynamic = true
|
||||
}
|
||||
Expr::Name(_) | Expr::Struct(_) => dynamic = true,
|
||||
Expr::Call(n, _) if gridcraft_functions::lookup(n).is_none() && !is_special(n) => dynamic = true,
|
||||
// A range ending in a function's result (`A1:INDEX(B1:B9,n)`) covers the cells between
|
||||
// the references inside it, not only those references.
|
||||
Expr::Binary(BinOp::Range, a, b) if !matches!((&**a, &**b), (Expr::Ref(_), Expr::Ref(_))) => {
|
||||
let mut inner = precedents(wb, sheet, a).0;
|
||||
inner.extend(precedents(wb, sheet, b).0);
|
||||
let mut boxes: Vec<Area> = Vec::new();
|
||||
for area in inner {
|
||||
match boxes.iter_mut().find(|x| x.sheet == area.sheet) {
|
||||
Some(x) => x.range = x.range.union(&area.range),
|
||||
None => boxes.push(area),
|
||||
}
|
||||
}
|
||||
out.extend(boxes);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
(out, dynamic)
|
||||
}
|
||||
|
||||
fn is_special(n: &str) -> bool {
|
||||
matches!(
|
||||
n,
|
||||
"IF" | "IFS"
|
||||
| "IFERROR"
|
||||
| "IFNA"
|
||||
| "CHOOSE"
|
||||
| "SWITCH"
|
||||
| "ROW"
|
||||
| "COLUMN"
|
||||
| "ROWS"
|
||||
| "COLUMNS"
|
||||
| "AREAS"
|
||||
| "ISREF"
|
||||
| "ISFORMULA"
|
||||
| "FORMULATEXT"
|
||||
| "SHEET"
|
||||
| "SHEETS"
|
||||
| "OFFSET"
|
||||
| "INDIRECT"
|
||||
| "INDEX"
|
||||
| "CELL"
|
||||
| "TRIMRANGE"
|
||||
| "SUBTOTAL"
|
||||
| "AGGREGATE"
|
||||
| "TEXT"
|
||||
| "LET"
|
||||
| "LAMBDA"
|
||||
| "ISOMITTED"
|
||||
| "MAP"
|
||||
| "REDUCE"
|
||||
| "SCAN"
|
||||
| "BYROW"
|
||||
| "BYCOL"
|
||||
| "MAKEARRAY"
|
||||
)
|
||||
}
|
||||
|
||||
/// Names of functions the evaluator implements itself (merged into the function list for UI).
|
||||
pub const SPECIAL_FUNCTIONS: &[&str] = &[
|
||||
"IF",
|
||||
"IFS",
|
||||
"IFERROR",
|
||||
"IFNA",
|
||||
"CHOOSE",
|
||||
"SWITCH",
|
||||
"ROW",
|
||||
"COLUMN",
|
||||
"ROWS",
|
||||
"COLUMNS",
|
||||
"AREAS",
|
||||
"ISREF",
|
||||
"ISFORMULA",
|
||||
"FORMULATEXT",
|
||||
"SHEET",
|
||||
"SHEETS",
|
||||
"OFFSET",
|
||||
"INDIRECT",
|
||||
"INDEX",
|
||||
"CELL",
|
||||
"TRIMRANGE",
|
||||
"SUBTOTAL",
|
||||
"AGGREGATE",
|
||||
"TEXT",
|
||||
"LET",
|
||||
"LAMBDA",
|
||||
"ISOMITTED",
|
||||
"MAP",
|
||||
"REDUCE",
|
||||
"SCAN",
|
||||
"BYROW",
|
||||
"BYCOL",
|
||||
"MAKEARRAY",
|
||||
];
|
||||
|
||||
pub fn is_known_function(n: &str) -> bool {
|
||||
is_special(&n.to_ascii_uppercase()) || gridcraft_functions::lookup(n).is_some()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn unused(_: RefKind) {}
|
||||
Reference in New Issue
Block a user