mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-28 04:36:51 -06:00
@@ -1266,3 +1266,31 @@ fn offset_extreme_arguments_return_ref() {
|
||||
assert_eq!(crate::evaluate(&wb, 0, c("D1"), formula), Value::Number(expected), "{formula}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn median_large_values_recalculate_dependents() {
|
||||
let mut t = T::new();
|
||||
t.set("A1", "1e308");
|
||||
t.set("A2", "1.6e308");
|
||||
t.set("B1", "=MEDIAN(A1:A2)");
|
||||
t.set("B2", "=AGGREGATE(12,0,A1:A2)");
|
||||
t.set("C1", "=B1/1e308");
|
||||
t.set("C2", "=B2/1e308");
|
||||
for cell in ["B1", "B2"] {
|
||||
assert!((t.num(cell) / 1e308 - 1.3).abs() < 1e-14);
|
||||
}
|
||||
for cell in ["C1", "C2"] {
|
||||
assert!((t.num(cell) - 1.3).abs() < 1e-14);
|
||||
}
|
||||
t.set("A2", "-1e308");
|
||||
for cell in ["B1", "B2", "C1", "C2"] {
|
||||
assert_eq!(t.num(cell), 0.0);
|
||||
}
|
||||
t.set("A1", "-1.6e308");
|
||||
for cell in ["B1", "B2"] {
|
||||
assert!((t.num(cell) / 1e308 + 1.3).abs() < 1e-14);
|
||||
}
|
||||
for cell in ["C1", "C2"] {
|
||||
assert!((t.num(cell) + 1.3).abs() < 1e-14);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ fn median(v: Vec<f64>) -> R<f64> {
|
||||
let s = sorted(v);
|
||||
let n = s.len();
|
||||
let hi = s.get(n / 2).copied().unwrap_or(0.0);
|
||||
if n % 2 == 1 { Ok(hi) } else { Ok((s.get(n / 2 - 1).copied().unwrap_or(0.0) + hi) / 2.0) }
|
||||
if n % 2 == 1 { Ok(hi) } else { Ok(s.get(n / 2 - 1).copied().unwrap_or(0.0).midpoint(hi)) }
|
||||
}
|
||||
|
||||
/// Values occurring at least twice, ordered by first occurrence, with their (equal, maximal) count.
|
||||
@@ -2255,6 +2255,46 @@ mod tests {
|
||||
av(col(xs))
|
||||
}
|
||||
|
||||
// The two middle values are finite and their midpoint is representable even when
|
||||
// adding them first would overflow. Exercise both callers of the shared helper.
|
||||
#[test]
|
||||
fn median_finite_midpoint_does_not_overflow() {
|
||||
for (values, expected) in [
|
||||
(vec![1e308, 1e308], 1e308),
|
||||
(vec![-1e308, -1e308], -1e308),
|
||||
(vec![1.6e308, 1e308], 1.3e308),
|
||||
(vec![-1e308, -1.6e308], -1.3e308),
|
||||
(vec![f64::MAX, f64::MAX], f64::MAX),
|
||||
(vec![-f64::MAX, -f64::MAX], -f64::MAX),
|
||||
(vec![-f64::MAX, f64::MAX], 0.0),
|
||||
(vec![1.7e308, -1.7e308, 1e308, 1.6e308], 1.3e308),
|
||||
] {
|
||||
close(ev("MEDIAN", values.iter().copied().map(n).collect()), expected);
|
||||
close(ev("MEDIAN", vec![rf(col(&values))]), expected);
|
||||
close(aggregate_values(12, &values.iter().copied().map(nv).collect::<Vec<_>>()), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn median_midpoint_keeps_small_values_and_argument_semantics() {
|
||||
// Halving each operand unconditionally can lose the smallest numbers. Use exact
|
||||
// equality here: the usual aggregate-test tolerance would accept zero.
|
||||
for value in [f64::MIN_POSITIVE, -f64::MIN_POSITIVE, f64::from_bits(1), -f64::from_bits(1)] {
|
||||
assert_eq!(ev("MEDIAN", vec![n(value), n(value)]), nv(value));
|
||||
assert_eq!(aggregate_values(12, &[nv(value), nv(value)]), nv(value));
|
||||
}
|
||||
close(ev("MEDIAN", vec![c(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0])]), 3.5);
|
||||
close(ev("MEDIAN", vec![c(&[1e308, -1e308, 3.0])]), 3.0);
|
||||
close(ev("MEDIAN", vec![n(1e308)]), 1e308);
|
||||
close(ev("MEDIAN", vec![t("4"), b(true)]), 2.5);
|
||||
close(ev("MEDIAN", vec![rf(arr(vec![vec![nv(1.0), tv("ignored"), Value::Empty, Value::Bool(true), nv(6.0)]]))]), 3.5);
|
||||
is_err(ev("MEDIAN", vec![t("invalid")]), CellError::Value);
|
||||
is_err(ev("MEDIAN", vec![e(CellError::NA), n(1e308)]), CellError::NA);
|
||||
is_err(ev("MEDIAN", vec![rf(arr(vec![vec![tv("ignored"), Value::Empty]]))]), CellError::Num);
|
||||
is_err(aggregate_values(12, &[]), CellError::Num);
|
||||
is_err(aggregate_values(12, &[nv(1e308), Value::Error(CellError::Div0)]), CellError::Div0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn averages_and_counts() {
|
||||
close(ev("AVERAGE", vec![n(1.0), n(2.0), n(6.0)]), 3.0);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Function parity: worksheet functions and formulas
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** minor (clearing large sparse ranges recalculates every changed cell's dependents, #1032) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (MEDIAN and AGGREGATE median avoid intermediate overflow) · **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).
|
||||
@@ -101,6 +101,11 @@ fraction sections with literal text (`#\:#=/=#`, `#_#/#`), zero fractions, zero-
|
||||
denominators, `?` placeholders padding the thousands separator, and booleans through the text
|
||||
section (`TEXT(TRUE,";;;\@@\@")` = `@TRUE@`).
|
||||
|
||||
`MEDIAN` and `AGGREGATE` function number 12 use an overflow-safe midpoint for even-sized
|
||||
inputs, so large finite middle values do not become `#NUM!` merely because their sum overflows (#1579).
|
||||
Sorting, argument coercion and error propagation are unchanged; this is a numerical correction
|
||||
under the documented median rule, not an extreme-value comparison against desktop Excel.
|
||||
|
||||
Work to close (16–26 h in total): the in-scope missing functions (10–16 h with the table above),
|
||||
an oracle corpus of Excel-computed results per function (owner runs Excel on generated
|
||||
workbooks; results checked in as data, not Excel files) and fixing what it finds (6–10 h).
|
||||
@@ -109,6 +114,7 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | minor | MEDIAN and AGGREGATE median use an overflow-safe midpoint (#1579); regressions cover finite extremes, tiny values and dependent recalculation |
|
||||
| 2026-10-13 | minor | Clear reports changed stored cells sparsely, including those beyond the first million coordinates; dependent formulas and removed spills update, while clearing empty merges still unblocks spills (#1032) |
|
||||
| 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) |
|
||||
|
||||
Reference in New Issue
Block a user