Skip to content

Align formula error codes with Google Sheets conventions#104

Merged
hackerwins merged 2 commits into
mainfrom
refine-error-code
Apr 19, 2026
Merged

Align formula error codes with Google Sheets conventions#104
hackerwins merged 2 commits into
mainfrom
refine-error-code

Conversation

@ggyuchive

@ggyuchive ggyuchive commented Apr 2, 2026

Copy link
Copy Markdown
Member

Summary

Changes:

  • ErrNode type: expand v union from 5 to 8 error codes, ordered by ERROR.TYPE code (1–8): #NULL! #DIV/0! #VALUE! #REF! #NAME? #NUM! #N/A #ERROR!
  • SQRT, LN, LOG (num ≤ 0): #VALUE! → #NUM!
  • LOG (base = 1): #VALUE! → #DIV/0! (log denominator is zero)
  • LOG (base ≤ 0): #VALUE! → #NUM!
  • SQRTPI (negative): #VALUE! → #NUM!
  • MOD, QUOTIENT (divisor = 0): #VALUE! → #DIV/0!
  • POWER (non-finite result): #VALUE! → #NUM!
  • COMBIN/COMBINA (negative or k>n): #VALUE! → #NUM!
  • FACT/FACTDOUBLE (negative): #VALUE! → #NUM!
  • BASE/DECIMAL (base out of 2–36): #VALUE! → #NUM!
  • ASIN/ACOS (outside −1..1): #VALUE! → #NUM!
  • ACOSH (< 1), ATANH (outside −1..1), ACOTH (|x| ≤ 1): #VALUE! → #NUM!
  • CSCH/COTH (x = 0): #VALUE! → #DIV/0!
  • MULTINOMIAL (negative arg): #VALUE! → #NUM!
  • QUARTILE (quart outside 0–4): #VALUE! → #NUM!
  • PERCENTILE (k outside 0–1): #VALUE! → #NUM!
  • PERCENTRANK (sig = 0): #VALUE! → raw rank (no rounding); sig < 0 → #NUM!
  • EFFECT/NOMINAL (invalid params): #VALUE! → #NUM!
  • CHAR: upper bound 65535 → 1114111 (full Unicode); String.fromCharCode → String.fromCodePoint; out-of-range → #NUM! (was #VALUE!)
  • docs/design/formula.md: sync ErrNode docs and Error Types table

Test coverage added for every corrected error path.

Why

Previously the engine returned #VALUE! for numeric domain violations (e.g. SQRT of a negative, LOG of zero, FACT of a negative integer) and #VALUE! for division-by-zero in MOD/QUOTIENT, where Google Sheets returns #NUM! and #DIV/0! respectively. This caused ERROR.TYPE to always return #VALUE! (code 3) for inputs that Google Sheets maps to codes 2 or 6.

Linked Issues

Fixes #

Verification

CI automatically posts a verification summary comment on this PR with
per-lane results for both verify:self and verify:integration.

  • verify:self — CI comment shows ✅
  • verify:integration — CI comment shows ✅ (or explicit skip reason below)

Skip reason (if applicable):

Risk Assessment

  • User-facing risk:
  • Data/security risk:
  • Rollback plan:

Notes for Reviewers

  • UI changes (screenshots/gifs if applicable):
  • Follow-up work (if any):

Summary by CodeRabbit

  • New Features

    • Added support for more spreadsheet error codes (#NULL!, #NAME?, #NUM!) for finer-grained error reporting.
  • Bug Fixes

    • Standardized and made error responses more consistent across many formula families (math, statistical, text, date, finance, lookup, engineering, logical, database).
    • Unresolved/unknown formulas now report #NAME?; many failures now return consistent error codes (e.g., #NUM!, #DIV/0!, #VALUE!, #N/A).
  • Tests

    • Updated formula tests to reflect revised error-code behavior and edge cases.
  • Documentation

    • Updated formula docs to describe the expanded canonical error set and mappings.

@coderabbitai

coderabbitai Bot commented Apr 2, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ddc1c401-4244-4d40-be98-184379dcc01c

📥 Commits

Reviewing files that changed from the base of the PR and between 29433de and c12916e.

📒 Files selected for processing (3)
  • packages/sheets/src/formula/functions-math.ts
  • packages/sheets/src/formula/functions-statistical.ts
  • packages/sheets/test/formula/formula.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/sheets/src/formula/functions-math.ts
  • packages/sheets/src/formula/functions-statistical.ts
  • packages/sheets/test/formula/formula.test.ts

📝 Walkthrough

Walkthrough

This PR centralizes and typesheet error handling by introducing an ordered ErrValues/ErrValue set and frozen ErrNode singletons, replacing hardcoded error literals across formula evaluators, functions, and worksheet model code; it expands supported error codes to include #NULL!, #NAME?, and #NUM!, and standardizes error-return behavior (e.g., unknown functions → #NAME?, generic failures → #ERROR!).

Changes

Cohort / File(s) Summary
Error infrastructure
packages/sheets/src/formula/formula.ts
Added ErrValues (8 codes), ErrValue type, ErrValue named accessors, errValueCode() mapper, and frozen ErrNode singletons; changed ErrNode.v to use ErrValue.
Math
packages/sheets/src/formula/functions-math.ts
Replaced inline error objects with ErrNode.*; refined domain/error semantics (MOD/QUOTIENT → DIV0, SQRT/POWER/LOG etc. → NUM), tightened DECIMAL parsing.
Financial
packages/sheets/src/formula/functions-financial.ts
Standardized argument/validation error returns to ErrNode.* (NA/NUM/VALUE/DIV0); changed nper divide-by-zero path to ErrNode.DIV0.
Statistical
packages/sheets/src/formula/functions-statistical.ts
Mapped many error paths to ErrNode.*; adjusted quartile/percentile/percentrank error behavior and added early-return in PERCENTRANK when significance is zero.
Text
packages/sheets/src/formula/functions-text.ts
Switched to ErrNode.NA/ErrNode.VALUE; expanded CHAR to use fromCodePoint and larger Unicode range, and return ErrNode.NUM for out-of-range.
Date/Time
packages/sheets/src/formula/functions-date.ts
Normalized parse/validation errors to ErrNode.NA/ErrNode.VALUE across date/time helpers and functions.
Engineering
packages/sheets/src/formula/functions-engineering.ts
Replaced inline error returns with ErrNode.*; adjusted parseComplexArg and related validation errors.
Helpers & Arguments
packages/sheets/src/formula/functions-helpers.ts, packages/sheets/src/formula/arguments.ts
Centralized conversion/ref helpers to return ErrNode.VALUE instead of ad-hoc error objects.
Info & Database
packages/sheets/src/formula/functions-info.ts, packages/sheets/src/formula/functions-database.ts
ERROR.TYPE expanded to resolve referenced errors; many functions now return ErrNode.*; imports/signature tweak for optional grid parameter.
Logical
packages/sheets/src/formula/functions-logical.ts
Replaced error literals with ErrNode.* and updated IFNA/ISNA checks to use ErrValue constants.
Lookup & Reference
packages/sheets/src/formula/functions-lookup.ts
Standardized error returns across lookup/array/reference APIs to use ErrNode singletons.
Worksheet model
packages/sheets/src/model/worksheet/calculator.ts, .../input.ts, .../shifting.ts
Switched cyclic/shift/relocate/align logic to use ErrValue.REF / ErrValues for detection and emitted error values.
Tests & Docs
packages/sheets/test/formula/formula.test.ts, docs/design/sheets/formula.md
Updated tests to expect expanded error codes (#NUM!, #NAME?, etc.) and doc table reworked to document the 8-code canonical set.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Evaluator
  participant FunctionModule
  participant ErrNode
  participant Worksheet
  Caller->>Evaluator: evaluate(formula)
  Evaluator->>FunctionModule: dispatch(functionName, args)
  FunctionModule-->>ErrNode: return ErrNode.NAME / ErrNode.NUM / ...
  FunctionModule-->>Evaluator: EvalNode (err singleton)
  Evaluator->>Worksheet: write cell value (uses ErrValue.* if needed)
  Worksheet-->>Caller: final cell value (err or normal)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hop through code with careful paws,

Replacing scattered error claws.
Eight small tokens, neat and bright,
Now every formula finds the light.
A rabbit cheers for ErrNode’s might!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: aligning formula error codes with Google Sheets conventions, which is the core objective across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 98.35% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refine-error-code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 118.6s

Lane Status Duration
sheets:build ✅ pass 13.6s
docs:build ✅ pass 8.4s
verify:fast ✅ pass 59.8s
frontend:build ✅ pass 15.3s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 4.8s
cli:build ✅ pass 1.8s
verify:entropy ✅ pass 14.5s

Verification: verify:integration

Result: ✅ PASS

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/design/formula.md`:
- Around line 181-188: Update the `#NULL!` entry in the error table to mark it
as reserved/unsupported until the evaluator can actually emit it: either remove
the active description or replace it with a brief note like "reserved — engine
does not currently emit this error"; also add a short sentence near the grammar
discussion clarifying that there is currently no range-intersection operator and
the evaluator cannot produce `#NULL!` (reference `#NULL!` and the
grammar/range-intersection mention to locate where to edit).

In `@packages/sheets/src/formula/formula.ts`:
- Around line 496-499: The new ErrNode union includes '#NAME?' but the evaluator
still throws for unknown function calls and returns '#ERROR!' for unresolved
identifiers; update the name-resolution failure paths so they emit an ErrNode
with v: '#NAME?' instead of throwing or returning '#ERROR!'. Specifically, in
the function-call evaluation path (the code that currently throws for unknown
functions) and in the identifier resolution path (the code that currently
returns '#ERROR!'), replace those outcomes with returning { t: 'err', v:
'#NAME?' } so ERROR.TYPE and other error-handling downstream see the correct
error kind.

In `@packages/sheets/src/formula/functions-math.ts`:
- Around line 1208-1209: The combinatorics functions must consistently emit
`#NUM`! for invalid n/k domain checks; update the error return in permutFunc and
permutationaFunc (and the other occurrence around the earlier check at the spot
referenced near 1239) so that when n<0 || k<0 || k>n they return { t: 'err', v:
'#NUM!' } instead of the current { t: 'err', v: '#VALUE!' }; locate and replace
those error-return sites inside permutFunc and permutationaFunc (and the similar
check noted at the 1239 location) so ERROR.TYPE behavior matches COMBIN/COMBINA.
- Around line 1388-1390: The DECIMAL implementation currently uses
parseInt(str.v, b) which accepts partial matches (e.g., "12Z" -> 12); update the
DECIMAL validation so the entire input string only contains valid digits for
base b before returning a number or, alternatively, compare the parsed value
back to a fully-parsed string to ensure no trailing invalid characters.
Specifically, in the DECIMAL logic around parseInt(str.v, b) (variables b and
str.v), check that str.v matches an allowed-character pattern for base b (or
re-serialize the parsed value and verify equality) and if any invalid character
is present return { t: 'err', v: '#VALUE!' } instead of accepting a partial
parse.

In `@packages/sheets/src/formula/functions-statistical.ts`:
- Around line 967-969: The exclusive variants are returning the wrong error type
for out-of-range quart/percent/significance inputs; update quartileexcFunc,
percentileexcFunc, and percentrankexcFunc so that the checks that currently
return '#VALUE!' (and the sig < 1 check in percentrankexcFunc) instead return
'#NUM!' to match the INC counterparts and ensure consistent ERROR.TYPE behavior
for invalid-range and significance values.

In `@packages/sheets/src/formula/functions-text.ts`:
- Around line 784-789: The CHAR implementation returns supplementary characters
via String.fromCodePoint but LEN, LEFT, RIGHT, MID, and CODE still operate on
UTF-16 code units, causing broken behavior for non-BMP characters; update these
functions to be code-point aware: change LEN to count code points (e.g., iterate
with for...of or use Array.from/ spread on the string), change LEFT/RIGHT/MID to
slice by code points rather than code units (build substrings by iterating code
points), and change CODE to return the full code point via
String.prototype.codePointAt(0) (and bounds-check appropriately). Locate and
modify the implementations of LEN, LEFT, RIGHT, MID, CODE and ensure they
interoperate with CHAR(String.fromCodePoint) so supplementary characters are
treated as single characters throughout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b589cdbe-94e6-4310-a15d-868049cb422d

📥 Commits

Reviewing files that changed from the base of the PR and between 85f7df9 and a0a6ef4.

📒 Files selected for processing (7)
  • docs/design/formula.md
  • packages/sheets/src/formula/formula.ts
  • packages/sheets/src/formula/functions-financial.ts
  • packages/sheets/src/formula/functions-math.ts
  • packages/sheets/src/formula/functions-statistical.ts
  • packages/sheets/src/formula/functions-text.ts
  • packages/sheets/test/formula/formula.test.ts

Comment thread docs/design/formula.md Outdated
Comment thread packages/sheets/src/formula/formula.ts
Comment thread packages/sheets/src/formula/functions-math.ts Outdated
Comment thread packages/sheets/src/formula/functions-math.ts Outdated
Comment thread packages/sheets/src/formula/functions-statistical.ts Outdated
Comment thread packages/sheets/src/formula/functions-text.ts
@ggyuchive
ggyuchive marked this pull request as draft April 2, 2026 17:43
Expand ErrNode from 5 to 8 codes (adds #NULL!, #NAME?, #NUM!), align
numeric domain violations with Google Sheets, introduce named ErrValue
accessors so ERROR.TYPE and every call site share a single source of
truth, and collapse repeated `{ t: 'err', v: ErrValue.X }` literals
into frozen `ErrNode.X` singletons.

Why:
  - Numeric domain violations previously returned #VALUE!, disagreeing
    with Google Sheets (#NUM! / #DIV/0!). Downstream code branching on
    ERROR.TYPE codes now matches Google's behavior.
  - The 1200+ raw '#…' literals across formula/* and model/worksheet/*
    meant ERROR.TYPE had to hand-maintain a duplicated 1–8 code table.
    Centralizing the ordering in ErrValues[] lets errValueCode() derive
    the code from the array index, and lets call sites reference
    ErrValue.NA / ErrValue.DIV0 / … instead of magic strings.
  - After centralizing ErrValue, every error return site still rebuilt
    a fresh `{ t: 'err', v: ErrValue.X }` wrapper — 1175 allocations
    that all carry the same shape. Hoisting them to frozen ErrNode.X
    singletons removes the allocation churn, shortens call sites, and
    gives one place to document when each code is appropriate.

Changes:
  - ErrNode.v: 5 → 8 codes ordered to match ERROR.TYPE (1=#NULL!, …,
    8=#ERROR!). Domain-violation functions updated: SQRT, LN, LOG,
    LOG base, SQRTPI, MOD, QUOTIENT, POWER, COMBIN/COMBINA,
    FACT/FACTDOUBLE, BASE/DECIMAL, ASIN/ACOS/ACOSH/ATANH/ACOTH,
    CSCH/COTH, MULTINOMIAL, QUARTILE, PERCENTILE, PERCENTRANK,
    EFFECT/NOMINAL, CHAR.
  - Add ErrValue named-accessor object, ErrValue type alias, and
    errValueCode(v) helper in formula.ts. Replace literal '#…' with
    ErrValue.* across 16 source files.
  - Add ErrNode frozen-singleton const (ErrNode.NULL, DIV0, VALUE,
    REF, NAME, NUM, NA, ERROR) in formula.ts with per-code JSDoc
    explaining when each code should be emitted — pick-the-right-code
    guidance lives with the singletons themselves. Replace 1175 call
    sites of `{ t: 'err', v: ErrValue.X }` with `ErrNode.X` across
    arguments.ts, functions-database/date/engineering/financial/
    helpers/info/logical/lookup/math/statistical/text.ts, and
    formula.ts itself. Drop unused ErrValue imports where the file
    now only emits errors (never compares against them).
  - ERROR.TYPE uses errValueCode() instead of a duplicated map and
    now dereferences single-cell refs, so ERROR.TYPE(A1) where A1
    holds an error string returns 1–8.
  - Google Sheets parity fixes from review feedback:
      • PERMUT / PERMUTATIONA domain errors now emit #NUM! (matching
        COMBIN) instead of #VALUE!.
      • QUARTILE.EXC / PERCENTILE.EXC / PERCENTRANK.EXC emit #NUM!
        for out-of-domain quartile, k, or significance arguments.
      • DECIMAL validates every character against the requested base
        before calling parseInt, so DECIMAL(\"12Z\", 10) returns
        #NUM! instead of silently truncating to 12; empty string
        returns #VALUE!.
  - defaultAlign in input.ts now checks membership in the shared
    ErrValues list directly, dropping the hand-maintained partial
    FormulaErrorValues set.
  - docs/design/sheets/formula.md: sync ErrNode docs, expand the
    Error Types table to all 8 codes, and note that #NULL! is
    reserved only to preserve the canonical ERROR.TYPE ordering —
    Wafflebase never emits it because the Excel space-intersection
    operator does not exist in Google Sheets / Wafflebase.
  - Tests: reference-based ERROR.TYPE cases (1–8 over error cells;
    #N/A for number/text/boolean/empty/range), PERMUT/PERMUTATIONA
    domain-error cases, DECIMAL invalid-character and empty-string
    cases, and EXC-variant domain-error cases.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.7 <[email protected]>
@ggyuchive
ggyuchive marked this pull request as ready for review April 19, 2026 15:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
packages/sheets/src/formula/functions-financial.ts (1)

1258-1780: ⚠️ Potential issue | 🟡 Minor

Financial functions should return #NUM! (not #VALUE!) for zero denominators and numeric-domain violations, consistent with Google Sheets behavior.

The PR standardizes numeric errors to #NUM!, but several financial functions still return #VALUE! for cases that should be #NUM!:

  • DISC (line 1258), YIELDDISC (line 1313), RECEIVED (line 1429), INTRATE (line 1457), PRICEMAT (line 1600), YIELDMAT (line 1633/1635): Return #VALUE! when divisors are zero; should return ErrNode.NUM.
  • RRI (line 1780): Returns #VALUE! when pv.v === 0 or nper.v === 0; should return ErrNode.NUM.
  • PDURATION (line 1758): Returns #VALUE! for negative/zero rates and values; should return ErrNode.NUM.
  • RATE (line 419), IRR (line 462/467), XIRR (line 793/798): Non-convergence paths return #VALUE!; should return ErrNode.NUM per Google Sheets convention.

Consider consolidating these into a follow-up PR for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheets/src/formula/functions-financial.ts` around lines 1258 - 1780,
Several financial functions return ErrNode.VALUE (i.e., `#VALUE`!) for zero
denominators or numeric-domain violations but per PR guidance and Google Sheets
behavior they should return ErrNode.NUM (`#NUM`!); update the error returns in
DISC (discountFunc), YIELDDISC (yielddiscFunc), RECEIVED (receivedFunc), INTRATE
(intrateFunc), PRICEMAT (pricematFunc), YIELDMAT (yieldmatFunc), PDURATION
(pdurationFunc) and RRI (rriFunc) to return ErrNode.NUM where the code currently
returns ErrNode.VALUE (or does numeric-domain checks), and change
non-convergence/error paths in RATE, IRR, and XIRR implementations (functions
named rateFunc, irrFunc, xirrFunc) to return ErrNode.NUM instead of
ErrNode.VALUE; ensure you only replace the specific error return values and keep
the existing guard checks and control flow intact.
packages/sheets/src/formula/functions-engineering.ts (1)

352-367: ⚠️ Potential issue | 🟠 Major

Return #NUM! for DEC2 numeric-domain failures.*

These branches still map out-of-range inputs and too-small places to ErrNode.VALUE, so ERROR.TYPE remains inconsistent with the PR’s numeric-domain convention.

🔧 Proposed error-code alignment
-  if (val < -549755813888 || val > 549755813887) return ErrNode.VALUE;
+  if (val < -549755813888 || val > 549755813887) return ErrNode.NUM;
@@
-    if (p < hex.length) return ErrNode.VALUE;
+    if (p < hex.length) return ErrNode.NUM;
@@
-  if (val < -512 || val > 511) return ErrNode.VALUE;
+  if (val < -512 || val > 511) return ErrNode.NUM;
@@
-    if (p < bin.length) return ErrNode.VALUE;
+    if (p < bin.length) return ErrNode.NUM;
@@
-  if (val < -536870912 || val > 536870911) return ErrNode.VALUE;
+  if (val < -536870912 || val > 536870911) return ErrNode.NUM;
@@
-    if (p < oct.length) return ErrNode.VALUE;
+    if (p < oct.length) return ErrNode.NUM;

Also applies to: 410-425, 468-483

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheets/src/formula/functions-engineering.ts` around lines 352 - 367,
The numeric-domain error handling in functions-engineering.ts must return
ErrNode.NUM (representing `#NUM`!) instead of ErrNode.VALUE for DEC2* conversions:
change the out-of-range check that currently returns ErrNode.VALUE when val <
-549755813888 || val > 549755813887 to return ErrNode.NUM, and change the
places-too-small branch (the check if p < hex.length) that returns ErrNode.VALUE
to return ErrNode.NUM; apply the same replacement in the other DEC2* blocks
referenced (the similar branches around the blocks handling ranges and places at
the locations corresponding to the other diffs, e.g. the blocks around lines
410-425 and 468-483) while leaving existing early error propagation (places.t
=== 'err') unchanged.
packages/sheets/src/formula/functions-statistical.ts (2)

1266-1305: ⚠️ Potential issue | 🟠 Major

Guard single-value PERCENTRANK arrays before dividing by n - 1.

With one numeric value and x equal to that value, rank = smaller / (n - 1) becomes 0 / 0, returning { t: 'num', v: NaN } instead of a spreadsheet error.

🛡️ Proposed guard
   const vals = collectNumericValues(exprs[0], visit, grid);
   if (!Array.isArray(vals)) return vals;
   if (vals.length === 0) return ErrNode.NA;
+  if (vals.length < 2) return ErrNode.DIV0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheets/src/formula/functions-statistical.ts` around lines 1266 -
1305, The PERCENTRANK implementation can divide by zero when vals has a single
numeric value (n === 1); add a guard after computing n (and before any division
by n - 1) to detect n === 1 and return a spreadsheet error (e.g. ErrNode.DIV0)
so we don't compute rank = smaller / (n - 1) or do interpolation; update the
block surrounding vars n, smaller, idx, and rank in functions-statistical.ts to
return ErrNode.DIV0 when n === 1.

3106-3124: ⚠️ Potential issue | 🟠 Major

Avoid NaN from Z.TEST when sigma is omitted for a single sample.

For one data point without an explicit sigma, the sample variance divides by dataResult.length - 1 (0), producing NaN and returning it as a numeric result. Also reject non-positive explicit sigma before using it as a divisor.

🛡️ Proposed guard
   const dataResult = collectNumericValues(exprs[0], visit, grid);
   if (!Array.isArray(dataResult)) return dataResult;
   if (dataResult.length === 0) return ErrNode.VALUE;
@@
   if (exprs.length >= 3) {
     const s = NumberArgs.map(visit(exprs[2]), grid);
     if (s.t === 'err') return s;
     sigma = s.v;
+    if (sigma <= 0) return ErrNode.VALUE;
   } else {
+    if (dataResult.length < 2) return ErrNode.DIV0;
     // Sample standard deviation
     const variance = dataResult.reduce((a, b) => a + (b - mean) ** 2, 0) / (dataResult.length - 1);
     sigma = Math.sqrt(variance);
+    if (sigma === 0) return ErrNode.DIV0;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheets/src/formula/functions-statistical.ts` around lines 3106 -
3124, The Z.TEST implementation can produce NaN when sigma is omitted for a
single sample and must reject non-positive explicit sigma; update the block in
the function using collectNumericValues, exprs, NumberArgs.map and sigma so that
if dataResult.length - 1 <= 0 (i.e. single data point and no explicit sigma) you
return ErrNode.DIV0 (or the project's appropriate divide-by-zero error) instead
of computing variance, and if an explicit sigma (from
NumberArgs.map(visit(exprs[2]))) is provided validate that s.v is finite and > 0
and return ErrNode.DIV0 (or ErrNode.NUM per project convention) when not, before
using sigma as a divisor; keep the rest of the z calculation and final return
using normCdf unchanged.
packages/sheets/src/formula/functions-math.ts (3)

2084-2120: ⚠️ Potential issue | 🟠 Major

AGGREGATE statistical error paths diverge from SUBTOTAL and Google Sheets.

SUBTOTAL now correctly returns ErrNode.DIV0 when there aren't enough values for STDEV/STDEVP/VAR/VARP (lines 2018, 2025, 2034, 2040), matching Google Sheets. The same cases inside AGGREGATE (func nums 7, 8, 10, 11) still return ErrNode.VALUE, so e.g. AGGREGATE(7, 0, A1) vs. SUBTOTAL(7, A1) on a single value will report different ERROR.TYPE codes for the same underlying cause.

Also, the default branch (line 2119) is hit for an unknown function_num; Google Sheets returns #VALUE! there, which is fine, but note that 12 (MEDIAN) is the only extra entry — if you intend to support 13–19 (MODE/LARGE/SMALL/PERCENTILE/QUARTILE/…) the fall-through is silently wrong.

🛠️ Proposed fix for the insufficient-sample cases
     case 7: { // STDEV.S
-      if (nums.length < 2) return ErrNode.VALUE;
+      if (nums.length < 2) return ErrNode.DIV0;
       const mean = nums.reduce((a, b) => a + b, 0) / nums.length;
       const variance = nums.reduce((a, b) => a + (b - mean) ** 2, 0) / (nums.length - 1);
       return { t: 'num', v: Math.sqrt(variance) };
     }
     case 8: { // STDEV.P
-      if (nums.length === 0) return ErrNode.VALUE;
+      if (nums.length === 0) return ErrNode.DIV0;
       const mean = nums.reduce((a, b) => a + b, 0) / nums.length;
       const variance = nums.reduce((a, b) => a + (b - mean) ** 2, 0) / nums.length;
       return { t: 'num', v: Math.sqrt(variance) };
     }
     case 9: return { t: 'num', v: nums.reduce((a, b) => a + b, 0) }; // SUM
     case 10: { // VAR.S
-      if (nums.length < 2) return ErrNode.VALUE;
+      if (nums.length < 2) return ErrNode.DIV0;
       const mean = nums.reduce((a, b) => a + b, 0) / nums.length;
       return { t: 'num', v: nums.reduce((a, b) => a + (b - mean) ** 2, 0) / (nums.length - 1) };
     }
     case 11: { // VAR.P
-      if (nums.length === 0) return ErrNode.VALUE;
+      if (nums.length === 0) return ErrNode.DIV0;
       const mean = nums.reduce((a, b) => a + b, 0) / nums.length;
       return { t: 'num', v: nums.reduce((a, b) => a + (b - mean) ** 2, 0) / nums.length };
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheets/src/formula/functions-math.ts` around lines 2084 - 2120, The
AGGREGATE switch (variable fn) currently returns ErrNode.VALUE for
insufficient-sample errors in cases 7 (STDEV.S), 8 (STDEV.P), 10 (VAR.S) and 11
(VAR.P); change those early-return branches to return ErrNode.DIV0 to match
SUBTOTAL/Google Sheets behavior, e.g. replace "if (nums.length < 2) return
ErrNode.VALUE" with "return ErrNode.DIV0" in case 7 and 10, and replace "if
(nums.length === 0) return ErrNode.VALUE" with "return ErrNode.DIV0" in case 8
and 11; keep the rest of the calculations unchanged. Also confirm whether you
intend to implement functions 13–19 (MODE/LARGE/SMALL/PERCENTILE/QUARTILE/etc.)
because MEDIAN (12) is currently the only extra implemented item and any missing
function_nums will fall through to the default ErrNode.VALUE.

2359-2372: ⚠️ Potential issue | 🟠 Major

LOG10 must return #NUM! for non-positive input, not #VALUE!.

LN and LOG both return ErrNode.NUM when their arguments are ≤ 0, but LOG10 incorrectly returns ErrNode.VALUE for the same condition. Google Sheets returns #NUM! for both LOG10(0) and LOG10(-1), making this an error-type mismatch that breaks consistency across the logarithm functions.

Fix
-  if (n.v <= 0) return ErrNode.VALUE;
+  if (n.v <= 0) return ErrNode.NUM;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheets/src/formula/functions-math.ts` around lines 2359 - 2372,
log10Func currently returns ErrNode.VALUE for non-positive inputs but should
return ErrNode.NUM to match LN/LOG and Google Sheets behavior; locate the
function log10Func and change the branch that checks n.v <= 0 to return
ErrNode.NUM instead of ErrNode.VALUE (keeping the rest of the flow: obtain args
via ctx.args(), map the argument using NumberArgs.map(visit(exprs[0]), grid),
and preserve existing error propagation for n.t === 'err').

2270-2300: ⚠️ Potential issue | 🟡 Minor

Change ErrNode.VALUE to ErrNode.NUM for singular matrix detection.

At line 2281, the singular matrix detection returns ErrNode.VALUE, but Google Sheets reports #NUM! for MINVERSE of a singular square matrix. This is a numeric-domain failure (analogous to other #NUM! mappings throughout the codebase), not a type/shape error. The non-matrix/non-square branches at lines 2251/2254 correctly use ErrNode.VALUE.

🛠️ Proposed fix
-    if (maxVal < 1e-12) return ErrNode.VALUE; // Singular
+    if (maxVal < 1e-12) return ErrNode.NUM; // Singular matrix → `#NUM`!
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheets/src/formula/functions-math.ts` around lines 2270 - 2300, In
the Gauss-Jordan elimination inside the MINVERSE implementation, the
singular-matrix check currently returns ErrNode.VALUE; change that to
ErrNode.NUM so a singular square matrix yields the numeric error used for
MINVERSE; specifically replace the return at the pivot-check (the spot using
maxVal < 1e-12 within the loop in the Gauss-Jordan code that references aug,
maxRow/maxVal and pivot) to return ErrNode.NUM instead of ErrNode.VALUE.
♻️ Duplicate comments (1)
packages/sheets/src/formula/functions-text.ts (1)

784-789: ⚠️ Potential issue | 🟠 Major

Keep CHAR’s expanded code-point range consistent with the text stack.

String.fromCodePoint can now return supplementary characters, but LEN, LEFT, RIGHT, MID, and CODE still operate on UTF-16 code units. That leaves formulas like LEN(CHAR(128512)), LEFT(CHAR(128512), 1), and CODE(CHAR(128512)) inconsistent with the new CHAR range.

🔎 Read-only verification
#!/bin/bash
set -euo pipefail

node - <<'NODE'
const s = String.fromCodePoint(0x1F600);
console.log({
  rendered: s,
  length: s.length,
  leftOneCodeUnit: JSON.stringify(s.slice(0, 1)),
  charCodeAt0: s.charCodeAt(0),
  codePointAt0: s.codePointAt(0),
});
NODE

rg -n -C2 'str\.v\.(length|slice|charCodeAt)\b|String\.fromCodePoint' -- 'packages/sheets/src/formula/functions-text.ts'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheets/src/formula/functions-text.ts` around lines 784 - 789, The
CHAR implementation currently allows code points up to 1114111 and uses
String.fromCodePoint, which creates supplementary characters incompatible with
the rest of the text stack; restrict the valid range to UTF-16 code units by
changing the upper bound check from 1114111 to 0xFFFF (65535) and replace
String.fromCodePoint(code) with String.fromCharCode(code) (keep the existing
check that returns ErrNode.NUM), referencing the local variable code, the
ErrNode.NUM return, and the current String.fromCodePoint usage so the CHAR
result is consistent with LEN/LEFT/RIGHT/MID/CODE behavior.
🧹 Nitpick comments (1)
packages/sheets/src/model/worksheet/input.ts (1)

347-350: Centralized error detection — behavior now auto-picks up #NULL!, #NAME?, #NUM!.

The cast-to-readonly string[] is the idiomatic workaround for Array.includes being typed with the element type. If this pattern shows up elsewhere, a tiny helper like isErrValue(v: string): v is ErrValue alongside ErrValues would let call sites drop the cast, but it's not essential here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheets/src/model/worksheet/input.ts` around lines 347 - 350, The
code in defaultAlign uses a cast "(ErrValues as readonly
string[]).includes(rawValue)" to detect error strings; replace this pattern with
a small type-guard helper and use it instead: add an isErrValue(v: string): v is
ErrValue function that checks membership against ErrValues (using includes
without casting), then call isErrValue(rawValue) inside defaultAlign to return
'center' for error values; update other call sites to use isErrValue as needed
so you can drop the cast everywhere (look for ErrValues and defaultAlign to
locate places to change).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/sheets/src/formula/functions-math.ts`:
- Around line 731-733: The ATAN2 implementation currently returns ErrNode.VALUE
when both inputs are zero; update the branch in the ATAN2 function that checks
"if (x.v === 0 && y.v === 0)" to return ErrNode.DIV0 instead of ErrNode.VALUE so
ATAN2(0, 0) yields `#DIV/0`! (matching Google Sheets and the other divisor-zero
cases like MOD/QUOTIENT/CSCH/COTH).

In `@packages/sheets/src/formula/functions-statistical.ts`:
- Around line 3488-3493: The code calls Math.log on knownY (used in
GROWTH/LOGEST) without validating values, so zero/negative entries produce
-Infinity/NaN; update the knownY handling in the GROWTH/LOGEST paths (symbols:
knownY, lnY, linearRegression, GROWTH, LOGEST) to first check for any
non-positive value (e.g., if (knownY.some(y => y <= 0)) ) and return the
spreadsheet domain error (ErrNode.NUM) before computing lnY or calling
linearRegression; apply the same check to the other identical block referenced
(around the 3572–3576 block).

---

Outside diff comments:
In `@packages/sheets/src/formula/functions-engineering.ts`:
- Around line 352-367: The numeric-domain error handling in
functions-engineering.ts must return ErrNode.NUM (representing `#NUM`!) instead of
ErrNode.VALUE for DEC2* conversions: change the out-of-range check that
currently returns ErrNode.VALUE when val < -549755813888 || val > 549755813887
to return ErrNode.NUM, and change the places-too-small branch (the check if p <
hex.length) that returns ErrNode.VALUE to return ErrNode.NUM; apply the same
replacement in the other DEC2* blocks referenced (the similar branches around
the blocks handling ranges and places at the locations corresponding to the
other diffs, e.g. the blocks around lines 410-425 and 468-483) while leaving
existing early error propagation (places.t === 'err') unchanged.

In `@packages/sheets/src/formula/functions-financial.ts`:
- Around line 1258-1780: Several financial functions return ErrNode.VALUE (i.e.,
`#VALUE`!) for zero denominators or numeric-domain violations but per PR guidance
and Google Sheets behavior they should return ErrNode.NUM (`#NUM`!); update the
error returns in DISC (discountFunc), YIELDDISC (yielddiscFunc), RECEIVED
(receivedFunc), INTRATE (intrateFunc), PRICEMAT (pricematFunc), YIELDMAT
(yieldmatFunc), PDURATION (pdurationFunc) and RRI (rriFunc) to return
ErrNode.NUM where the code currently returns ErrNode.VALUE (or does
numeric-domain checks), and change non-convergence/error paths in RATE, IRR, and
XIRR implementations (functions named rateFunc, irrFunc, xirrFunc) to return
ErrNode.NUM instead of ErrNode.VALUE; ensure you only replace the specific error
return values and keep the existing guard checks and control flow intact.

In `@packages/sheets/src/formula/functions-math.ts`:
- Around line 2084-2120: The AGGREGATE switch (variable fn) currently returns
ErrNode.VALUE for insufficient-sample errors in cases 7 (STDEV.S), 8 (STDEV.P),
10 (VAR.S) and 11 (VAR.P); change those early-return branches to return
ErrNode.DIV0 to match SUBTOTAL/Google Sheets behavior, e.g. replace "if
(nums.length < 2) return ErrNode.VALUE" with "return ErrNode.DIV0" in case 7 and
10, and replace "if (nums.length === 0) return ErrNode.VALUE" with "return
ErrNode.DIV0" in case 8 and 11; keep the rest of the calculations unchanged.
Also confirm whether you intend to implement functions 13–19
(MODE/LARGE/SMALL/PERCENTILE/QUARTILE/etc.) because MEDIAN (12) is currently the
only extra implemented item and any missing function_nums will fall through to
the default ErrNode.VALUE.
- Around line 2359-2372: log10Func currently returns ErrNode.VALUE for
non-positive inputs but should return ErrNode.NUM to match LN/LOG and Google
Sheets behavior; locate the function log10Func and change the branch that checks
n.v <= 0 to return ErrNode.NUM instead of ErrNode.VALUE (keeping the rest of the
flow: obtain args via ctx.args(), map the argument using
NumberArgs.map(visit(exprs[0]), grid), and preserve existing error propagation
for n.t === 'err').
- Around line 2270-2300: In the Gauss-Jordan elimination inside the MINVERSE
implementation, the singular-matrix check currently returns ErrNode.VALUE;
change that to ErrNode.NUM so a singular square matrix yields the numeric error
used for MINVERSE; specifically replace the return at the pivot-check (the spot
using maxVal < 1e-12 within the loop in the Gauss-Jordan code that references
aug, maxRow/maxVal and pivot) to return ErrNode.NUM instead of ErrNode.VALUE.

In `@packages/sheets/src/formula/functions-statistical.ts`:
- Around line 1266-1305: The PERCENTRANK implementation can divide by zero when
vals has a single numeric value (n === 1); add a guard after computing n (and
before any division by n - 1) to detect n === 1 and return a spreadsheet error
(e.g. ErrNode.DIV0) so we don't compute rank = smaller / (n - 1) or do
interpolation; update the block surrounding vars n, smaller, idx, and rank in
functions-statistical.ts to return ErrNode.DIV0 when n === 1.
- Around line 3106-3124: The Z.TEST implementation can produce NaN when sigma is
omitted for a single sample and must reject non-positive explicit sigma; update
the block in the function using collectNumericValues, exprs, NumberArgs.map and
sigma so that if dataResult.length - 1 <= 0 (i.e. single data point and no
explicit sigma) you return ErrNode.DIV0 (or the project's appropriate
divide-by-zero error) instead of computing variance, and if an explicit sigma
(from NumberArgs.map(visit(exprs[2]))) is provided validate that s.v is finite
and > 0 and return ErrNode.DIV0 (or ErrNode.NUM per project convention) when
not, before using sigma as a divisor; keep the rest of the z calculation and
final return using normCdf unchanged.

---

Duplicate comments:
In `@packages/sheets/src/formula/functions-text.ts`:
- Around line 784-789: The CHAR implementation currently allows code points up
to 1114111 and uses String.fromCodePoint, which creates supplementary characters
incompatible with the rest of the text stack; restrict the valid range to UTF-16
code units by changing the upper bound check from 1114111 to 0xFFFF (65535) and
replace String.fromCodePoint(code) with String.fromCharCode(code) (keep the
existing check that returns ErrNode.NUM), referencing the local variable code,
the ErrNode.NUM return, and the current String.fromCodePoint usage so the CHAR
result is consistent with LEN/LEFT/RIGHT/MID/CODE behavior.

---

Nitpick comments:
In `@packages/sheets/src/model/worksheet/input.ts`:
- Around line 347-350: The code in defaultAlign uses a cast "(ErrValues as
readonly string[]).includes(rawValue)" to detect error strings; replace this
pattern with a small type-guard helper and use it instead: add an isErrValue(v:
string): v is ErrValue function that checks membership against ErrValues (using
includes without casting), then call isErrValue(rawValue) inside defaultAlign to
return 'center' for error values; update other call sites to use isErrValue as
needed so you can drop the cast everywhere (look for ErrValues and defaultAlign
to locate places to change).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6d9f2b5e-e55b-4ef8-b4af-c2fc2e4c8441

📥 Commits

Reviewing files that changed from the base of the PR and between a0a6ef4 and 29433de.

📒 Files selected for processing (18)
  • docs/design/sheets/formula.md
  • packages/sheets/src/formula/arguments.ts
  • packages/sheets/src/formula/formula.ts
  • packages/sheets/src/formula/functions-database.ts
  • packages/sheets/src/formula/functions-date.ts
  • packages/sheets/src/formula/functions-engineering.ts
  • packages/sheets/src/formula/functions-financial.ts
  • packages/sheets/src/formula/functions-helpers.ts
  • packages/sheets/src/formula/functions-info.ts
  • packages/sheets/src/formula/functions-logical.ts
  • packages/sheets/src/formula/functions-lookup.ts
  • packages/sheets/src/formula/functions-math.ts
  • packages/sheets/src/formula/functions-statistical.ts
  • packages/sheets/src/formula/functions-text.ts
  • packages/sheets/src/model/worksheet/calculator.ts
  • packages/sheets/src/model/worksheet/input.ts
  • packages/sheets/src/model/worksheet/shifting.ts
  • packages/sheets/test/formula/formula.test.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/design/sheets/formula.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/sheets/test/formula/formula.test.ts

Comment thread packages/sheets/src/formula/functions-math.ts
Comment thread packages/sheets/src/formula/functions-statistical.ts
@hackerwins
hackerwins merged commit 073d6c4 into main Apr 19, 2026
4 checks passed
@hackerwins
hackerwins deleted the refine-error-code branch April 19, 2026 19:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants