Functions: COUNTIF family returns #VALUE! for criteria over 255 characters
Excel rejects a text criterion longer than 255 characters in COUNTIF(S), SUMIF(S), AVERAGEIF(S), MAXIFS and MINIFS with #VALUE!; GridCraft matched it anyway. Found by the calc oracle on an Excel-saved report where a 268-character concatenated criterion caches #VALUE!. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fc45b7f6d8
commit
5524e2520a
|
|
@ -5,6 +5,9 @@ use std::cmp::Ordering;
|
|||
use gridcraft_core::parse::parse_number_text;
|
||||
use gridcraft_core::{CellError, Value, compare_numbers, compare_text};
|
||||
|
||||
/// Longest text criterion COUNTIF and its relatives accept; a longer one is `#VALUE!`.
|
||||
const MAX_CRITERION_CHARS: usize = 255;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Op {
|
||||
Eq,
|
||||
|
|
@ -48,6 +51,16 @@ impl Criterion {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parses the criteria argument of COUNTIF(S), SUMIF(S), AVERAGEIF(S), MAXIFS and MINIFS:
|
||||
/// like `parse`, but text longer than 255 characters is `#VALUE!`, as in Excel.
|
||||
pub(crate) fn parse_arg(v: &Value) -> Result<Criterion, CellError> {
|
||||
match v {
|
||||
Value::Array(a) => Criterion::parse_arg(a.data.first().unwrap_or(&Value::Empty)),
|
||||
Value::Text(t) if t.chars().nth(MAX_CRITERION_CHARS).is_some() => Err(CellError::Value),
|
||||
other => Ok(Criterion::parse(other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_text(t: &str) -> Criterion {
|
||||
let (op, rest) = if let Some(r) = t.strip_prefix(">=") {
|
||||
(Op::Ge, r)
|
||||
|
|
|
|||
|
|
@ -957,7 +957,7 @@ fn criteria_mask(a: &[Arg], first_pair: usize, rows: usize, cols: usize) -> R<Ve
|
|||
if r.rows != rows || r.cols != cols {
|
||||
return Err(CellError::Value);
|
||||
}
|
||||
let c = Criterion::parse(&crit.value.scalar());
|
||||
let c = Criterion::parse_arg(&crit.value.scalar())?;
|
||||
for (m, v) in mask.iter_mut().zip(r.iter()) {
|
||||
if *m && !c.matches(v) {
|
||||
*m = false;
|
||||
|
|
@ -1017,7 +1017,7 @@ fn minifs(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||
|
||||
fn countif(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
let range = as_array(&crate::util::arg(a, 0)?.value);
|
||||
let c = Criterion::parse(&crate::util::arg(a, 1)?.value.scalar());
|
||||
let c = Criterion::parse_arg(&crate::util::arg(a, 1)?.value.scalar())?;
|
||||
num_val(range.iter().filter(|v| c.matches(v)).count() as f64)
|
||||
}
|
||||
|
||||
|
|
@ -1026,7 +1026,7 @@ fn countif(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||
/// the evaluator does that resizing (it has the references), so cells line up one to one here.
|
||||
fn if_numbers(a: &[Arg]) -> R<Vec<f64>> {
|
||||
let range = as_array(&crate::util::arg(a, 0)?.value);
|
||||
let c = Criterion::parse(&crate::util::arg(a, 1)?.value.scalar());
|
||||
let c = Criterion::parse_arg(&crate::util::arg(a, 1)?.value.scalar())?;
|
||||
let sum_range = if has(a, 2) { Some(as_array(&crate::util::arg(a, 2)?.value)) } else { None };
|
||||
let mut out = Vec::new();
|
||||
for r in 0..range.rows {
|
||||
|
|
@ -1643,4 +1643,16 @@ mod tests {
|
|||
is_err(ev("COUNTIFS", vec![rf(qty.clone()), t(">1"), rf(qty)]), CellError::Value);
|
||||
is_err(ev("SUMIF", vec![rf(col(&[1.0, 2.0])), t(">0"), rf(arr(vec![vec![Value::Error(NA)], vec![nv(1.0)]]))]), NA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn criteria_longer_than_255_chars_are_value_errors() {
|
||||
let ok = "x".repeat(255);
|
||||
let long = "x".repeat(256);
|
||||
let cells = arr(vec![vec![tv(&ok)], vec![tv(&long)]]);
|
||||
let qty = col(&[1.0, 2.0]);
|
||||
close(ev("COUNTIF", vec![rf(cells.clone()), t(&ok)]), 1.0);
|
||||
is_err(ev("COUNTIF", vec![rf(cells.clone()), t(&long)]), CellError::Value);
|
||||
close(ev("SUMIFS", vec![rf(qty.clone()), rf(cells.clone()), t(&ok)]), 1.0);
|
||||
is_err(ev("SUMIFS", vec![rf(qty), rf(cells), t(&long)]), CellError::Value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue