diff --git a/crates/functions/src/stat/descriptive.rs b/crates/functions/src/stat/descriptive.rs index dcf80d276..f4e4b0160 100644 --- a/crates/functions/src/stat/descriptive.rs +++ b/crates/functions/src/stat/descriptive.rs @@ -336,6 +336,10 @@ pub(super) fn percentrank_impl(a: &[Arg], exc: bool) -> R { if sig < 1.0 || s.is_empty() { return Err(CellError::Num); } + // Excel: 10^significance overflows from 309 up. + if sig >= 309.0 { + return Err(CellError::Div0); + } let (first, last) = (s.first().copied().unwrap_or(0.0), s.last().copied().unwrap_or(0.0)); if x < first || x > last { return Err(CellError::NA); @@ -359,6 +363,10 @@ pub(super) fn percentrank_impl(a: &[Arg], exc: bool) -> R { let (rl, rh) = (rank_of(lo), rank_of(hi)); rl + (x - lo) / (hi - lo) * (rh - rl) }; + // From significance 15 up Excel returns 15 significant digits, rounded. + if sig >= 15.0 { + return num_val(format!("{r:.14e}").parse().unwrap_or(r)); + } num_val(trunc_digits(r, sig)) } diff --git a/crates/functions/src/stat/tests.rs b/crates/functions/src/stat/tests.rs index 6fc4059e7..ba59116cd 100644 --- a/crates/functions/src/stat/tests.rs +++ b/crates/functions/src/stat/tests.rs @@ -227,10 +227,19 @@ fn percentiles() { close(ev("PERCENTRANK.EXC", vec![e9.clone(), n(5.43)]), 0.381); close(ev("PERCENTRANK.EXC", vec![e9, n(5.43), n(1.0)]), 0.3); let t = c(&[1.0, 2.0, 3.0]); - close(ev("PERCENTRANK", vec![t.clone(), n(2.0), n(400.0)]), 0.5); - close(ev("PERCENTRANK.INC", vec![t.clone(), n(2.0), n(400.0)]), 0.5); - close(ev("PERCENTRANK.INC", vec![t.clone(), n(1.0), n(400.0)]), 0.0); - close(ev("PERCENTRANK.EXC", vec![t, n(2.0), n(400.0)]), 0.5); + // Excel 16: significance 309 and up is #DIV/0!, 308 still works. + for name in ["PERCENTRANK", "PERCENTRANK.INC", "PERCENTRANK.EXC"] { + is_err(ev(name, vec![t.clone(), n(2.0), n(309.0)]), CellError::Div0); + is_err(ev(name, vec![t.clone(), n(1.0), n(400.0)]), CellError::Div0); + close(ev(name, vec![t.clone(), n(2.0), n(308.0)]), 0.5); + } + // From significance 15 up the result has 15 significant digits. + let k = c(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]); + for sig in [15.0, 16.0, 308.0] { + close_tol(ev("PERCENTRANK.INC", vec![k.clone(), n(2.123_456_789), n(sig)]), 0.187_242_798_166_667, 1e-16); + } + close_tol(ev("PERCENTRANK.INC", vec![k.clone(), n(2.123_456_789), n(14.0)]), 0.187_242_798_166_66, 1e-15); + close(ev("PERCENTRANK.EXC", vec![k, n(2.123_456_789), n(16.0)]), 0.265_432_098_625); } #[test] diff --git a/docs/function-parity.md b/docs/function-parity.md index e4b81a8f7..369fffa55 100644 --- a/docs/function-parity.md +++ b/docs/function-parity.md @@ -1,6 +1,6 @@ # Function parity: worksheet functions and formulas -> **Last reviewed:** 2026-10-31 · **Last updated:** 2026-10-31 · **Change:** minor (edge cases matched to Excel: base conversions, BITLSHIFT/BITRSHIFT, UNICHAR noncharacters, UPPER/LOWER/PROPER casing, FIXED/DOLLAR output, F/T/GAMMA/WEIBULL/BETA distributions, BINOM trial cap, HARMEAN; earlier: function edge-case parity row; earlier: function edge-case parity row; earlier: ECMA.CEILING listed with USDOLLAR as not on Microsoft's list; earlier: IMSQRT reproduces the stray real part Excel gives for negative reals) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-31 · **Last updated:** 2026-10-31 · **Change:** minor (edge cases matched to Excel: base conversions, BITLSHIFT/BITRSHIFT, UNICHAR noncharacters, UPPER/LOWER/PROPER casing, FIXED/DOLLAR output, F/T/GAMMA/WEIBULL/BETA distributions, BINOM trial cap, HARMEAN; earlier: function edge-case parity row; earlier: function edge-case parity row; earlier: function edge-case parity row; earlier: ECMA.CEILING listed with USDOLLAR as not on Microsoft's list; earlier: IMSQRT reproduces the stray real part Excel gives for negative reals) · **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). @@ -93,6 +93,7 @@ This improves series preparation; it does not change the model-fitting or season | 15-significant-digit comparison semantics | done (#57): `=0.1+0.2=0.3` is TRUE, and MATCH and COUNTIF compare the same way. Arithmetic keeps full precision, as Excel does — `=(0.1+0.2)-0.3` is 5.55E-17 | | IMSQRT and the other IM* functions against Excel's text | IMSQRT follows Excel's polar route, so a negative real gives Excel's stray real part: `IMSQRT("-4")` is `1.22514845490862E-16+2i` (checked in Excel 16.0 on 79 inputs). Differences kept: IMSQRT of a number with a part above about 1E154 or below about 1E-155 gives the exact root (Excel gives `#NUM!` or 0); IMPOWER, IMEXP, IMSIN, IMCOS, IMSINH and IMCOSH keep exact results where Excel shows the same kind of noise (`IMPOWER("-4",2)` is `16-3.92047505570758E-15i`) or different trailing digits near multiples of pi/2; IMCSC and IMCSCH differ by one unit in the 15th digit; IMARGUMENT returns 16–17 digits where Excel returns 15; the text of IM* and COMPLEX results differs for some magnitudes (Excel writes `COMPLEX(1E-10,1)` as `0.0000000001+i` but `COMPLEX(0.000000123456789012345,1)` as `1.23456789012345E-07+i`, and an integer past 15 digits rounded and zero-padded) | | MOD quotient limit | done: Excel 16.0 returns `#NUM!` when `ABS(number/divisor) >= 1.1259E12`; lower quotients retain the remainder | +| PERCENTRANK significance and precision | done: Excel 16.0 returns `#DIV/0!` from significance 309, and rounds to 15 significant digits for significance 15–308 | | External workbook references (`[Book.xlsx]Sheet1!A1`) | parsed; links dropped on open, no update | | Localized function names and `;` separators | **missing** (#45); en-US only | | Long flat operator chains (`=A1+A2+…`, `-`, `*`, `/`, `^`, `&`, comparisons) | done: no operand limit below Excel's 8,192-character formula limit (Excel, observed: `=1+1+…` with 4,096 operands is exactly 8,192 characters with the `=` and calculates; one more character is refused). Evaluated (#484), printed (#628), stored, copied, adjusted, saved and reopened without recursing once per operand, on a 1 MiB stack (#916); written to XLSX as plain formulas, not dynamic arrays; Spanish entry accepts them |