Commit Graph
124 Commits
Author SHA1 Message Date
3b65b9af58 Sonar maintainability: explicit imports, method paths, smaller functions (#2412)
* Sonar: explicit imports, fn paths for closures, extract helpers for cognitive complexity

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Fix imports

* Fix serde imports

* Fix engine imports

* Fix clippy gate and a type error from the explicit-import change

Remove the unused imports the review flagged (cf.rs, pivot.rs, tables.rs, cmd/sheet.rs) so clippy -D warnings passes, and point the backstage parent-folder lookup at Path::to_str, which the fn-path rewrite had broken.

Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Apply rustfmt to the explicit-import cleanup so the fmt gate passes

* Share the per-directory font path expansion in theme.rs

The candidate font lists repeated the same loop over the Linux font
directories in every role. A single helper expands them in the same order,
so the lookup order is unchanged and the duplicated blocks go away.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Read the Windows fonts folder in one place in theme.rs

Every script-font list repeated the same WINDIR lookup. A single helper
returns the same path, so the duplicated lines go away without changing
which font files are tried.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Build the light and dark UI palettes from one table in theme.rs

Tokens::light() and Tokens::dark() listed the same 40 colour fields in the
same order, so every change had to be made twice. A single palette
constructor takes the mode and picks each colour's value, so each field is
written once and both modes keep their exact colours.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Share the Noto CJK font list between the CJK script lists in theme.rs

The Japanese, Simplified and Korean lists each repeated the same loop over
the Linux CJK directories with a different locale file name. One helper
builds that loop, and each list keeps its own face index, so the lookup
order is unchanged.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Share Linux Noto directory list across Thai and Hebrew font lookups

Removes the duplicated directory arrays flagged by SonarCloud new-code duplication.

* Reuse in_dirs for Thai and Hebrew Noto font candidates

Removes the remaining duplicated per-directory loops flagged by SonarCloud.

* Share one font-list builder for the Thai and Hebrew faces

The two per-script functions repeated the same Windows, macOS, Linux and
variable-font setup. A single script_fonts helper now builds both lists,
keeping the candidate order unchanged, which clears the Sonar duplication gate.

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
2026-10-25 12:32:02 -04:00
Brandon Thomas 6208f15d29 Merge pull request #2463 from Silvertree2010:fix/h2062-yearfrac-basis-0-counts-the-last 2026-10-25 09:36:28 -04:00
Brandon Thomas 25d6a3a44f Merge pull request #2457 from Silvertree2010:fix/h1979-imsqrt-of-a-negative-real-return 2026-10-25 09:36:27 -04:00
Brandon Thomas 1871541948 Merge pull request #2443 from Silvertree2010:fix/h2018-isoweeknum-weeknum-21-return-wee 2026-10-25 09:36:26 -04:00
Brandon Thomas 990aa1315a Merge pull request #2428 from Silvertree2010:fix/h1934-minverse-returns-0-for-inverse-e 2026-10-25 09:36:25 -04:00
NetterSilverandClaude Haiku 5.5 b4487663aa Fix YEARFRAC basis 0 for February-end start and 31st end
Found while testing crates/functions/src/date.rs:443-457 (yearfrac_core, basis 0)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 17:10:13 +02:00
NetterSilverandClaude Haiku 5.5 92d2b9894f Fix IMSQRT leaving a stray real part for negative reals
Found while testing crates/functions/src/engineering.rs:713 (imsqrt, via Cx::powf at :562)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 14:24:48 +00:00
NetterSilverandClaude Haiku 5.5 86064077f1 Fix ISOWEEKNUM/WEEKNUM(…,21) for the first days of the calendar
Found while testing crates/functions/src/date.rs:123-129 (isoweek)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 15:04:35 +02:00
NetterSilverandClaude Haiku 5.5 6cb3220164 Scale MINVERSE rounding cutoff to the input matrix
Found while testing crates/functions/src/math.rs:923

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 11:09:26 +00:00
NetterSilverandClaude Haiku 5.5 14415f410d Fix PERCENTRANK returning #NUM! for very large significance
Found while testing crates/functions/src/stat.rs:257-260 (trunc_digits, used by percentrank_impl)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 11:16:03 +02:00
Dylan M. TaylorandCodebuff f284f6a825 M2.9: budget lambda work and bound wildcard substring search (#2415)
Sweep findings from the ac8da7d4 security scan (storytold-gridcraft-agent-
findings.json), remaining on main at 72c1d57d:

- finding 14 (crates/calc/src/eval.rs:988): every lambda invocation now
  spends one charge against a per-cell work budget (16M — one per cell of
  the largest array a reference may expand to), so a hostile, deeply
  nested LAMBDA/LET formula stops at the bound a legitimately wide one
  could reach instead of growing exponentially with nesting. Exhaustion
  is #CALC!, like the existing depth cap. Metered at `call_lambda_values`
  so every path into a body is counted (direct application, MAP/REDUCE/
  SCAN/BYROW/BYCOL/MAKEARRAY, D-function criteria); the criteria
  sub-evaluator hands its spend back to the parent.
- finding 16 (crates/functions/src/criteria.rs:313): `match_from` takes a
  step budget (1<<26: an honest match against Excel's 32,767-char cell
  ceiling costs a few hundred thousand steps), and `wildcard_find` drops
  the leading star run (a no-op for a prefix match) so the first literal
  anchor skips straight to its occurrences; all suffix attempts share one
  budget. Star churn degrades to no-match instead of quadratic work.

Unit tests: mock-host exhaustion/under-budget for lambda applications
(eval::fuel_tests), nested REDUCE under budget in full recalc, wildcard
behaviour unchanged.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>
2026-10-24 05:07:08 -04:00
Brandon ThomasandClaude Opus 5.5 72c1d57d1e Review fixes for #2393, #2394, #2397 and #2399
- PMT: take rate / m first so (pv + fv) / m can't overflow for a tiny rate (PMT(1e-300,10,1e12)).
- Keytips probe: an empty params object keys like no params.
- Number Stored as Text check reads the same accounting parentheses as typed entry.
- Lookups: a NaN number falls back to the general comparison instead of comparing equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 02:12:07 -04:00
Brandon Thomas 2c6e529b4d Merge PR #2400 2026-10-24 02:10:42 -04:00
Brandon Thomas d743f21992 Merge PR #2399
# Conflicts:
#	ROADMAP.md
#	docs/function-parity.md
2026-10-24 02:10:42 -04:00
Brandon Thomas f9af0d88a5 Merge PR #2397
# Conflicts:
#	ROADMAP.md
#	docs/function-parity.md
2026-10-24 02:10:42 -04:00
Brandon Thomas 14d6dabcaf Merge PR #2396 2026-10-24 02:10:36 -04:00
NetterSilverandClaude Haiku 5.5 b47c83f6aa Reject unmatched '.' and ',' in NUMBERVALUE text
Found while testing crates/functions/src/text.rs:326-340

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 01:48:12 +00:00
Harrison Booth 35067fa9ca Read accounting negatives like $(1,234) in number text
Typed entry, VALUE and arithmetic now read $(1,234), $ (5) and (5)% as
negative numbers, as Excel 365 does. Parentheses no longer nest: (-5),
((5)), $5% and dates in parentheses stay text. This changes main's
((100)) from 100 to text; the no-recursion guard from #2180 is kept and
its test now asserts the new result.
2026-10-23 16:57:24 -05:00
Harrison Booth 86f4114514 Merge remote-tracking branch 'origin/main' into gc-lookup-exact-number-compare
# Conflicts:
#	ROADMAP.md
#	docs/function-parity.md
2026-10-23 16:55:41 -05:00
Harrison Booth e883c84e7b Merge origin/main into the Analysis ToolPak value-argument branch
Resolves conflicts in ROADMAP.md, docs/function-parity.md and
crates/calc/src/tests.rs: both sides' progress-log rows and tests are kept.
2026-10-23 16:55:37 -05:00
Harrison Booth 1ce4781d3e Accuracy: PMT to Excel's last bits via ln_1p/exp_m1
PMT computed (1 + rate)^nper with powf, which rounds 1 + rate first and
drops the low bits of a small rate. Over 400 synthetic probes that left
PMT up to about 1000 ULPs from Excel (59 bit-exact); the ln_1p/exp_m1 form
is within 4 ULPs (244 bit-exact). A loan schedule guarded by
IF(p - SUM(principal) > 0, ...) turns on those bits: in 3000 synthetic
schedules main disagreed with Excel on the number of live periods in 1382,
the fix in 823.

Also follows Excel at the edges: a rate of -1 or less is #NUM!, a huge
nper gives -pv*rate instead of #NUM!, a tiny rate no longer collapses, and
a small negative rate keeps its payment. IPMT, PPMT, CUMIPMT and CUMPRINC
pick this up through pmt_calc. FV and PV are unchanged: Excel's own FV/PV
sit further from the exact value than the current powf form does.

The tests allow the one-ULP libm difference between platforms.
2026-10-23 16:24:18 -05:00
Brandon ThomasandClaude Opus 5.5 7cf44d0fbb Integration and review fixes for #2367, #2369 and #2387
- #2369: its test joins the single ui-egui integration-test binary (tests/it), as main now builds them.
- #2367: a cell set to empty text is a blank entry and doesn't grow the AutoFilter (CodeRabbit).
- #2387: MOD's range check also covers the sign-corrected remainder (CodeRabbit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:05:43 -04:00
NetterSilverandClaude Haiku 5.5 c746eea876 Fix MOD returning wrong sign and magnitude for tiny divisors
Found while testing crates/functions/src/math.rs:305-319

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 21:43:22 +02:00
Brandon Thomas d1399ff2ca Merge PR #2279 2026-10-23 10:11:12 -04:00
NetterSilverandClaude Opus 5.5 8307d3cb05 Treat empty instance_num in TEXTBEFORE/TEXTAFTER as 1
Found while testing crates/functions/src/text.rs:553

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:25:54 +00:00
Tyrel HavemanandClaude Opus 5.5 742a6e8535 functions: REGEXREPLACE bounds each replacement as it expands it (#2162)
From the local CodeRabbit review: the result was bounded per match, but
one match's expansion was built whole first, so a replacement repeating
$0 10,000 times over a 32,767-character match allocated about 330 MB
before the check. The replacement is now split into pieces as regex's
Captures::expand reads it (template_expands_like_regex checks they agree)
and appended piece by piece within the bound: 327 MB peak became 7 MB
(regexreplace_bounds_its_result).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Nnsfu3QGqjD1aFZbBgUWP
2026-10-21 23:45:50 -07:00
Tyrel HavemanandClaude Opus 5.5 e177bf1c44 calc: hardening: overlong digits, NaN indexes, REGEXREPLACE output (#2163, #2162)
- Text coercion: digits too many for a finite number are not a number,
  and a fraction must come out finite, so "1 <400 digits>/<400 digits>"
  (inf/inf) no longer coerces to NaN (test overlong_digits_stay_text).
- VLOOKUP/HLOOKUP and the D-functions' field index refuse NaN, which
  passed their "< 1" checks and became index 0, then underflowed
  (test nan_index_is_a_value_error; hostile_inputs_never_panic now also
  passes NaN and infinity to every function).
- REGEXREPLACE replaces match by match and gives up once the result can
  only be too long, instead of building every replacement first: the
  32,767 x 32,767 worst case went from 1.05 GB peak to 7 MB
  (test regexreplace_bounds_its_result).

#2165 (MAP's size check) is already fixed on main by the broadcast cap
(836b2698, #2182), whose test covers MAP.

Also adds my contributors/people.toml entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Nnsfu3QGqjD1aFZbBgUWP
2026-10-21 23:41:28 -07:00
aiongg a680ac59f0 Integration: track t07
# Conflicts:
#	crates/calc/src/tests.rs
#	docs/function-parity.md
2026-10-22 03:03:23 +00:00
aiongg 15dfbef548 Merge PR #2195: calc: cap LINEST-family regressor matrices (#2177) 2026-10-22 02:35:10 +00:00
aiongg 0cc4798ca9 Merge PR #1650: fix(ets): avoid repeated scans while interpolating gaps
docs/function-parity.md: status line takes this PR's change text with the newer review date; both history rows kept, newest first.
2026-10-22 02:34:55 +00:00
aiongg 2a3ea02e69 Merge PR #1526: functions: NETWORKDAYS.INTL accepts the all-weekend string "1111111" 2026-10-22 02:34:46 +00:00
aiongg 806a439efa Merge PR #1507: Read securities function dates in the workbook's date system 2026-10-22 02:34:46 +00:00
aiongg 011ae7266d Merge PR #1469: Let GROUPBY and PIVOTBY take a LAMBDA as their function 2026-10-22 02:34:45 +00:00
aiongg 941da33355 Merge PR #1432: Stop BINOM.INV/CRITBINOM hanging when trials exceed 2^53 2026-10-22 02:34:45 +00:00
aiongg a3baf552a1 Merge PR #1281: N() converts a computed array element by element, so it works inside SUMPRODUCT
crates/calc/src/tests.rs: kept both tests (union).
2026-10-22 02:34:42 +00:00
aiongg f55cbcd061 Merge PR #1237: BAHTTEXT: a final one after the millions reads เอ็ด 2026-10-22 02:34:35 +00:00
aiongg 91be133965 Merge PR #1114: Read date text arguments in the workbook date system (#1038) 2026-10-22 02:34:35 +00:00
aiongg a0a4b6a7d0 Merge PR #1105: Fix row keys merging distinct text rows in UNIQUE and grouping (#1012) 2026-10-22 02:34:35 +00:00
Higgs32584andClaude Opus 5.5 e1b8aad143 calc: cap LINEST-family regressor matrices (#2177)
LINEST, LOGEST, TREND and GROWTH built k x k normal-equation matrices
(plus two copies in sym_inverse, O(k^3)) with k taken straight from the
known_xs shape, so one formula could allocate gigabytes and abort on open.
More than 255 predictors is now #VALUE!, and more than 1e9 rows x k^2
multiply-adds is #NUM!, both checked before any allocation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:07:30 -04:00
Eduardo GarciaandClaude Sonnet 5.5 0b4cc5618f fix #1458: one Excel sheet-name quoting rule for formulas, pivot sources and ADDRESS
Sheet names starting with '.' or reading as R1C1 (R, C, RC, R1C1...) were saved
unquoted, so Excel could not open the file; PivotTable sources and ADDRESS left
cell-like names (A1) bare. The quoting rule, unquoting and sheet splitting now
live once in gridcraft_core::addr and replace the copies in the formula printer,
pivot, ADDRESS, chart, xlsx, Solver, Go To and the Name Box (which now reads ''
as ').

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 17:36:40 -03:00
NetterSilverandClaude Haiku 5.5 e9ffc71c6a Search from the end for negative TEXTBEFORE/TEXTAFTER instances
Found while testing crates/functions/src/text.rs:511-552

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 12:27:46 +00:00
Mex c30fa3f1cf fix(functions): avoid overflow in median midpoints (#1579) (#1585) 2026-10-17 21:13:03 -06:00
harrisonoconnorhover 1131de97ff fix(ets): avoid repeated interpolation scans (#1042) 2026-10-16 08:12:47 -04:00
midasmatterhorn 3f9b73b927 functions: NETWORKDAYS.INTL accepts the all-weekend string "1111111"
weekend_mask rejected the all-weekend string outright for both
NETWORKDAYS.INTL and WORKDAY.INTL, but Microsoft documents them as
differing: NETWORKDAYS.INTL "Using 1111111 will always return 0",
WORKDAY.INTL "1111111 is an invalid string". The crate already
implements that split — workday_core's per_week == 0 guard — which was
unreachable dead code while the extra clause stood. Drop the clause:
NETWORKDAYS.INTL counts zero workdays as documented and WORKDAY.INTL
errors through the guard it already has.

The existing test asserted the old behaviour and is updated.
Regression: working_days — on main the NETWORKDAYS.INTL assertion fails
(#VALUE! where Excel returns 0); with the clause dropped it passes and
WORKDAY.INTL is asserted to error.
2026-10-15 17:12:41 +03:00
NetterSilverandClaude Haiku 5.5 e1902111d8 Read securities function dates in the workbook's date system
Fixes #1457

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:29:44 +00:00
NetterSilverandClaude Haiku 5.5 cdc8177fae Let GROUPBY and PIVOTBY take a LAMBDA as their function
Fixes #903

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:09:18 +02:00
NetterSilverandClaude Haiku 5.5 9b155c48f7 Stop BINOM.INV/CRITBINOM hanging when trials exceed 2^53
Found while testing crates/functions/src/stat.rs:1244

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:43:57 +00:00
Harrison BoothandClaude Opus 5.5 ea190adad6 functions: Analysis ToolPak value arguments reject multi-cell references
EOMONTH, EDATE, WORKDAY(.INTL), NETWORKDAYS(.INTL), YEARFRAC, WEEKNUM, the
coupon, bond and T-bill functions, DISC, BIN2DEC, IM* and the other Analysis
ToolPak functions give #VALUE! in Excel 365 for a reference to more than one
cell in a value argument, in dynamic-array and legacy formulas alike, where
GridCraft lifted the range and spilled. Array constants and computed arrays
still lift, and range arguments (holidays, cash flows, schedules) and the
.INTL weekend still take ranges.

FnSpec gains a value_args mask, filled at registration from one table in
lift.rs; call_lifted turns a multi-cell reference at a value position into a
scalar #VALUE! before lifting, so EOMONTH(A1:A3,{1;2}) is a 2x1 array of
#VALUE! as in Excel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:55:46 -05:00
Harrison BoothandClaude Opus 5.5 5c20f16434 core: read accounting negatives like $(1,234) in number text
Excel 365 reads a dollar sign before accounting parentheses ($(1,234),
$ (5)) and a percent sign after them ((5)%) as negative numbers when the
text is typed, coerced by arithmetic or passed to VALUE. GridCraft gave
#VALUE! for these.

The parentheses branch no longer recurses: Excel reads only an unsigned
plain number or fraction inside, so (-5), ((5)), ($-5), and dates or
times in parentheses are now #VALUE! as in Excel (they used to give 5
or a negative serial). $5% is #VALUE! too: Excel never combines the
currency and percent signs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:46 -05:00
Harrison BoothandClaude Opus 5.5 ab4d58ca6f Lookups compare numbers exactly, like Excel
MATCH, XMATCH, VLOOKUP, HLOOKUP, XLOOKUP and LOOKUP compared numbers at
15 significant digits (through compare / compare_numbers), so a computed
key such as =0.1+0.2 (0.30000000000000004) found 0.3. Excel 365 compares
lookup keys exactly in exact, approximate and binary search modes, while
=, SWITCH and the criteria functions (COUNTIF(S), SUMIF(S), AVERAGEIF,
MAXIFS) keep the 15-digit comparison. Observed in Excel via COM with
synthetic values.

lookup_equal now compares numbers with ==, and the lookup searches order
values with lookup_order (exact for numbers, compare otherwise).
compare_numbers and the criteria path are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:26:06 -05:00