Merge pull request #752 from marcoarnulfo:fix/function-excel-diffs-738
This commit is contained in:
commit
80d282e7a5
|
|
@ -1167,16 +1167,18 @@ impl<'h> Evaluator<'h> {
|
|||
v.scalar().to_bool().unwrap_or(true)
|
||||
}
|
||||
};
|
||||
let text = if a1 {
|
||||
text
|
||||
// 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,
|
||||
Ok(None) => text,
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -443,6 +443,22 @@ fn references_functions() {
|
|||
assert_eq!(t.num("B3"), 50.0);
|
||||
t.set("B4", "=INDIRECT(\"A\"&4)");
|
||||
assert_eq!(t.num("B4"), 40.0);
|
||||
// With a1 = FALSE the text must be R1C1: A1-style text is #REF!, not read as A1.
|
||||
t.set("C1", "=INDIRECT(\"R3C1\",FALSE)");
|
||||
assert_eq!(t.num("C1"), 30.0);
|
||||
t.set("C2", "=INDIRECT(\"A3\",FALSE)");
|
||||
assert_eq!(t.get("C2"), Value::Error(CellError::Ref));
|
||||
// Defined names don't depend on the reference style.
|
||||
t.wb.names.push(gridcraft_model::DefinedName {
|
||||
name: "Third".into(),
|
||||
scope: None,
|
||||
formula: "Sheet1!$A$3".into(),
|
||||
comment: String::new(),
|
||||
hidden: false,
|
||||
});
|
||||
t.calc.rebuild(&t.wb);
|
||||
t.set("C3", "=INDIRECT(\"Third\",FALSE)");
|
||||
assert_eq!(t.num("C3"), 30.0);
|
||||
t.set("B5", "=SUM(A1:INDEX(A1:A4,3))");
|
||||
assert_eq!(t.num("B5"), 60.0);
|
||||
t.set("B6", "=SUM(A:A)");
|
||||
|
|
|
|||
|
|
@ -138,9 +138,10 @@ impl Criterion {
|
|||
match self.op {
|
||||
Op::Eq => blank,
|
||||
Op::Ne => !blank,
|
||||
// "<" / ">" with nothing: Excel compares against "" for text cells.
|
||||
// "<" / ">" with nothing: Excel compares against "" for text cells, but an
|
||||
// empty string never matches ("<=" and ">=" count it in neither).
|
||||
_ => match &v {
|
||||
Value::Text(t) => cmp_op(self.op, compare_text(t, "")),
|
||||
Value::Text(t) if !t.is_empty() => cmp_op(self.op, compare_text(t, "")),
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
|
|
@ -361,6 +362,9 @@ mod tests {
|
|||
assert!(m("<>", Value::from("a")));
|
||||
assert!(!m("<>", Value::Empty));
|
||||
assert!(m("TRUE", Value::Bool(true)));
|
||||
// A bare "<=" or ">=" never matches an empty string (Excel 365: COUNTIF(="", "<=") is 0).
|
||||
assert!(!m("<=", Value::from("")));
|
||||
assert!(!m(">=", Value::from("")));
|
||||
assert!(m("<b", Value::from("apple")));
|
||||
assert!(!m("<b", Value::Number(1.0)));
|
||||
assert!(m("#N/A", Value::Error(CellError::NA)));
|
||||
|
|
|
|||
|
|
@ -321,7 +321,8 @@ fn mirr(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||
neg += x / (1.0 + fr).powi(i as i32);
|
||||
}
|
||||
}
|
||||
if pos == 0.0 || neg == 0.0 {
|
||||
// No positive flow is not an error in Excel: the formula below gives -1.
|
||||
if neg == 0.0 {
|
||||
return Err(CellError::Div0);
|
||||
}
|
||||
let r = (-pos * (1.0 + rr).powf(n - 1.0) / neg).powf(1.0 / (n - 1.0)) - 1.0;
|
||||
|
|
@ -1813,6 +1814,8 @@ mod tests {
|
|||
close_tol(ev("MIRR", vec![rf(m), n(0.1), n(0.12)]), 0.1260943, 1e-6);
|
||||
close_tol(ev("MIRR", vec![rf(col(&[-120000.0, 39000.0, 30000.0, 21000.0])), n(0.1), n(0.12)]), -0.048044655, 1e-7);
|
||||
is_err(ev("MIRR", vec![rf(col(&[1.0, 2.0])), n(0.1), n(0.1)]), CellError::Div0);
|
||||
// Excel 365 gives -1 when every flow is negative: the positive flows are worth nothing.
|
||||
close_tol(ev("MIRR", vec![rf(col(&[-10000.0, -3400.0, -6500.0, -1000.0])), n(0.07), n(0.01)]), -1.0, 1e-12);
|
||||
close(ev("FVSCHEDULE", vec![n(1.0), av(row(&[0.09, 0.11, 0.1]))]), 1.33089);
|
||||
close(ev("EFFECT", vec![n(0.0525), n(4.0)]), 0.0535426673707584);
|
||||
is_err(ev("EFFECT", vec![n(-0.1), n(4.0)]), CellError::Num);
|
||||
|
|
|
|||
|
|
@ -845,6 +845,7 @@ pub(crate) fn specs() -> Vec<FnSpec> {
|
|||
fixed
|
||||
),
|
||||
f!("DOLLAR", 1, 2, Text, S, "DOLLAR(number, [decimals])", "Formats a number as currency text.", dollar),
|
||||
f!("USDOLLAR", 1, 2, Compatibility, S, "USDOLLAR(number, [decimals])", "Formats a number as currency text, like DOLLAR.", dollar),
|
||||
f!("BAHTTEXT", 1, 1, Text, S, "BAHTTEXT(number)", "Spells out a number in Thai words with Baht.", bahttext),
|
||||
f!("T", 1, 1, Text, S, "T(value)", "Returns the value if it is text, otherwise empty text.", t_fn),
|
||||
f!(
|
||||
|
|
@ -1104,6 +1105,8 @@ mod tests {
|
|||
is_text(ev("DOLLAR", vec![n(1234.567)]), "$1,234.57");
|
||||
is_text(ev("DOLLAR", vec![n(-1234.567), n(1.0)]), "($1,234.6)");
|
||||
is_text(ev("DOLLAR", vec![n(0.5), n(0.0)]), "$1");
|
||||
is_text(ev("USDOLLAR", vec![n(1.0)]), "$1.00");
|
||||
is_text(ev("USDOLLAR", vec![n(-1234.567), n(1.0)]), "($1,234.6)");
|
||||
is_text(ev("T", vec![t("x")]), "x");
|
||||
is_text(ev("T", vec![n(1.0)]), "");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Function parity: worksheet functions and formulas
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (TEXT and number formats: fraction literals, `?` thousands padding, booleans through the text section) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (USDOLLAR added as a compatibility function; MIRR, COUNTIF and INDIRECT edge cases match Excel 365) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
|
||||
Excel's worksheet functions, category by category, against GridCraft's function library. Part of
|
||||
[target-app-parity.md](target-app-parity.md); gaps feed [gaps.md](gaps.md).
|
||||
|
|
@ -20,6 +20,8 @@ Excel's worksheet functions, category by category, against GridCraft's function
|
|||
evaluator's special forms in `crates/calc/src/eval.rs` (`SPECIAL_FUNCTIONS`: IF, IFS, CHOOSE,
|
||||
OFFSET, INDIRECT, INDEX, SUBTOTAL, AGGREGATE, TEXT, LET, LAMBDA, MAP, REDUCE, SCAN, BYROW, BYCOL,
|
||||
MAKEARRAY…), excluding the test-only `ADD2`. Every one is an Excel name.
|
||||
- **Not on Microsoft's list:** USDOLLAR, a legacy function Excel still evaluates (like DOLLAR),
|
||||
is in our library as a Compatibility function for files that use it. It is not counted above.
|
||||
- **Working** excludes stubs that return an error by design: the 7 CUBE functions (`#N/A`, no
|
||||
cube connections) and WEBSERVICE and FILTERXML (`#VALUE!`). Excel for Mac also lacks
|
||||
WEBSERVICE and FILTERXML; Excel for Windows has them.
|
||||
|
|
@ -106,6 +108,7 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | USDOLLAR added as a Compatibility function (not on Microsoft's list, so the counts don't change); MIRR with no positive flow is -1, COUNTIF's bare `"<="` / `">="` skip empty strings, INDIRECT with `a1` = FALSE gives `#REF!` for A1-style text (#738) |
|
||||
| 2026-10-11 | minor | TEXT / number formats fixed against Excel-cached results: fraction literals and zero fractions, `0` denominators zero padded, `?` pads the thousands separator, booleans use the text section (226 mismatches → 0 in 352 TEXT cells) |
|
||||
| 2026-10-11 | minor | Iterative calculation: circular references iterate up to Maximum Iterations / Maximum Change when enabled |
|
||||
| 2026-10-11 | minor | Trim-reference operators (`:.`, `.:`, `.:.`) and unbracketed `[[#This Row],Col]` done; dropped the stale "Scope of what's missing" rows for TRIMRANGE, PERCENTOF, BAHTTEXT and the trim references |
|
||||
|
|
|
|||
Loading…
Reference in New Issue