Skip to content

Add Operator functions and move UNIQUE/CONCAT to Operator#149

Merged
hackerwins merged 1 commit into
mainfrom
add-operator-func
Apr 22, 2026
Merged

Add Operator functions and move UNIQUE/CONCAT to Operator#149
hackerwins merged 1 commit into
mainfrom
add-operator-func

Conversation

@ggyuchive

@ggyuchive ggyuchive commented Apr 22, 2026

Copy link
Copy Markdown
Member

Summary

  • Move UNIQUE in lookup and CONCAT in text to operator
  • Add ADDRESS, CHOOSE, COLUMN, COLUMNS, FORMULATEXT, GETPIVOTDATA, HLOOKUP, INDEX, INDIRECT, LOOKUP, MATCH, OFFSET, ROW, ROWS, SHEET, VLOOKUP, XLOOKUP functions to Operator

Why

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

    • Operator formula functions fully implemented with 100% coverage, including arithmetic operations (ADD, MINUS, MULTIPLY, DIVIDE, POW), unary operators (UMINUS, UPLUS, UNARY_PERCENT), and comparison functions (EQ, NE, LT, LTE, GT, GTE).
    • Added ISBETWEEN function for range-based conditional evaluation.
  • Documentation

    • Updated formula coverage documentation to reflect complete Operator category implementation (17 functions).

- Move UNIQUE in lookup and CONCAT in text to operator
- Add ADDRESS, CHOOSE, COLUMN, COLUMNS, FORMULATEXT, GETPIVOTDATA,
HLOOKUP, INDEX, INDIRECT, LOOKUP, MATCH, OFFSET, ROW, ROWS,
SHEET, VLOOKUP, XLOOKUP funtions to Operator
@github-actions

github-actions Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 118.3s

Lane Status Duration
sheets:build ✅ pass 12.8s
docs:build ✅ pass 8.2s
verify:fast ✅ pass 60.3s
frontend:build ✅ pass 15.5s
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

@codecov

codecov Bot commented Apr 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.06536% with 61 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/sheets/src/formula/functions-operator.ts 79.77% 50 Missing and 4 partials ⚠️
packages/sheets/src/formula/functions-helpers.ts 30.00% 7 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Created a new functions-operator.ts module consolidating operator-backed spreadsheet functions including arithmetic operators (ADD, MINUS, MULTIPLY, DIVIDE, POW), unary operators (UMINUS, UPLUS, UNARY_PERCENT), comparison operators (EQ, NE, LT, LTE, GT, GTE), and special functions (ISBETWEEN, CONCAT, UNIQUE). Moved CONCAT and UNIQUE implementations from text and lookup modules respectively, updated function catalog metadata with a new Operator category, and added comprehensive tests.

Changes

Cohort / File(s) Summary
Documentation
docs/design/sheets/formula-coverage.md
Updated Operator row to show full coverage (17 operators, 100%) and removed related notes.
Function Catalog & Metadata
packages/sheets/src/formula/function-catalog.ts
Added operator function metadata entries and created new Operator category containing 17 operator/comparison functions plus CONCAT and UNIQUE.
New Operator Functions Module
packages/sheets/src/formula/functions-operator.ts
New file implementing all arithmetic operators, unary operators, comparison operators, ISBETWEEN with inclusive/exclusive bounds, CONCAT string concatenation, and UNIQUE single-cell extraction.
Shared Helpers
packages/sheets/src/formula/functions-helpers.ts
Added exported firstCellValue() helper to resolve first cell from a reference node for reuse across modules.
Function Registry Updates
packages/sheets/src/formula/functions.ts
Imported and registered operatorEntries in the main FunctionMap.
Refactored Function Modules
packages/sheets/src/formula/functions-lookup.ts, packages/sheets/src/formula/functions-text.ts
Removed uniqueFunc from lookup module and concatFunc from text module; now use shared implementations from operators module via firstCellValue helper.
Test Coverage
packages/sheets/test/formula/formula.test.ts, packages/sheets/test/formula/function-catalog.test.ts
Added comprehensive operator function tests (arithmetic, unary, comparison, ISBETWEEN) and updated category expectations to include new Operator category.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Operators hop into place, neat and tidy,
Arithmetic, comparison—each one's handy,
CONCAT and UNIQUE join the brigade,
A new category that's perfectly made!
From chaos to order, the formulas dance,
Spreadsheets rejoice at this organized stance!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: moving UNIQUE and CONCAT to a new Operator module and adding operator functions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 add-operator-func

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.

@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

🧹 Nitpick comments (6)
packages/sheets/test/formula/function-catalog.test.ts (1)

419-431: Add representative Operator catalog assertions.

Line 427 verifies that some Operator category exists, but it does not lock down the new catalog entries or the CONCAT/UNIQUE category move. Consider adding explicit assertions such as findFunction('ADD')!.category, findFunction('CONCAT')!.category, and findFunction('UNIQUE')!.category resolving to Operator. As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases.

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

In `@packages/sheets/test/formula/function-catalog.test.ts` around lines 419 -
431, Add explicit assertions to lock down the Operator category entries: call
findFunction('ADD'), findFunction('CONCAT') and findFunction('UNIQUE') and
assert their .category resolves to 'Operator' (i.e.,
expect(findFunction('ADD')!.category).toEqual('Operator'), etc.). Add these
assertions in the same test near the listFunctionCategories() check so the test
validates the catalog entries and the CONCAT/UNIQUE category assignment using
the existing findFunction helper.
packages/sheets/src/formula/functions-operator.ts (4)

31-52: compareScalars return type declares an unreachable bool variant.

The declared return type is number | { t: 'bool'; v: boolean } | ErrNode, but no path returns a bool node — only number or ErrNode. That then forces the awkward typeof cmp === 'object' && 't' in cmp check plus as ErrNode cast at the call sites (Line 213, and again at Lines 269–270). Tightening the return type to number | ErrNode removes the dead variant and lets the callers use a simple typeof cmp !== 'number' narrow without casts.

♻️ Proposed change
-function compareScalars(
-  left: EvalNode,
-  right: EvalNode,
-): number | { t: 'bool'; v: boolean } | ErrNode {
+function compareScalars(
+  left: EvalNode,
+  right: EvalNode,
+): number | ErrNode {

and at call sites:

-    if (typeof cmp === 'object' && 't' in cmp) return cmp as ErrNode;
+    if (typeof cmp !== 'number') return cmp;
-  if (typeof cmpLower === 'object') return cmpLower as ErrNode;
-  if (typeof cmpUpper === 'object') return cmpUpper as ErrNode;
+  if (typeof cmpLower !== 'number') return cmpLower;
+  if (typeof cmpUpper !== 'number') return cmpUpper;

Also applies to: 213-213

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

In `@packages/sheets/src/formula/functions-operator.ts` around lines 31 - 52, The
compareScalars function's declared return type includes an unused "{ t: 'bool';
v: boolean }" variant; change its signature to return number | ErrNode (remove
the bool node variant) and keep the implementation returning only numbers or
ErrNode; then update call sites that handle its result (the comparisons that
currently test for an object or cast to ErrNode) to simply use typeof cmp !==
'number' to detect errors and narrow types without casts (look for usages of
compareScalars and the existing object/'t' in cmp checks and replace them with a
numeric-type check).

9-23: DRY: resolveScalar duplicates resolveValue from formula.ts.

The JSDoc explicitly says this mirrors formula.ts's resolveValue. Having two copies of the empty/TRUE/FALSE/Number coercion will drift over time (e.g., one is updated to handle a new literal and the other isn't), which would silently change comparison semantics between =A1<B1 and =LT(A1,B1). Prefer exporting the existing helper from formula.ts and importing it here.

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

In `@packages/sheets/src/formula/functions-operator.ts` around lines 9 - 23, The
resolveScalar function duplicates coercion logic already implemented in
resolveValue (formula.ts); remove resolveScalar, export or make resolveValue
available from formula.ts, import resolveValue into this module, and replace all
uses of resolveScalar with resolveValue (ensuring EvalNode and Grid types are
imported or compatible). Verify signatures match (node: EvalNode, grid?: Grid)
and update any call sites in functions-operator.ts to pass the same args so
comparison semantics remain consistent.

200-223: Exhaustive switch lacks a return outside the cases.

comparisonFunc's arrow returns inside each case, but there is no trailing return/throw. Under noImplicitReturns/strict settings this can fail to type-check; even if it compiles today, a future addition to the op union would silently fall through to undefined. Add an exhaustive fallback.

🛡️ Proposed change
     switch (op) {
       case 'eq': return { t: 'bool', v: cmp === 0 };
       case 'ne': return { t: 'bool', v: cmp !== 0 };
       case 'lt': return { t: 'bool', v: cmp < 0 };
       case 'lte': return { t: 'bool', v: cmp <= 0 };
       case 'gt': return { t: 'bool', v: cmp > 0 };
       case 'gte': return { t: 'bool', v: cmp >= 0 };
     }
+    const _exhaustive: never = op;
+    return _exhaustive;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheets/src/formula/functions-operator.ts` around lines 200 - 223,
The function comparisonFunc currently switches on op
('eq'|'ne'|'lt'|'lte'|'gt'|'gte') and returns inside each case but has no
fallback return; add an exhaustive fallback after the switch in the returned
arrow function to avoid undefined returns — either throw a descriptive Error
mentioning the unexpected op or return an ErrNode/EvalNode indicating an
internal error. Update comparisonFunc (the inner returned function that calls
twoArgs, resolveScalar and compareScalars) so any unknown op value is handled
deterministically (use the same ErrNode shape used elsewhere) to satisfy
noImplicitReturns and prevent silent undefined.

315-333: Use a proper type signature instead of any[] for the exported operator registry.

operatorEntries uses [string, (...args: any[]) => EvalNode][], which loses type safety. Every operator function in this file already declares the correct signature with ctx: FunctionContext, visit: (tree: ParseTree) => EvalNode, grid?: Grid. Define a shared type alias and apply it to operatorEntries so misregistration is caught at compile time:

+type OperatorFn = (
+  ctx: FunctionContext,
+  visit: (tree: ParseTree) => EvalNode,
+  grid?: Grid,
+) => EvalNode;
+
-export const operatorEntries: [string, (...args: any[]) => EvalNode][] = [
+export const operatorEntries: [string, OperatorFn][] = [

All required imports (FunctionContext, ParseTree, Grid) are already present at the top of this file.

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

In `@packages/sheets/src/formula/functions-operator.ts` around lines 315 - 333,
Create a proper function type alias (e.g., OperatorFn) matching the real
signature used by all operator implementations: (ctx: FunctionContext, visit:
(tree: ParseTree) => EvalNode, grid?: Grid) => EvalNode, then change the
operator registry declaration for operatorEntries from [string, (...args: any[])
=> EvalNode][] to [string, OperatorFn][] so misregistered entries (e.g.,
addFunc, minusFunc, concatFunc, uniqueFunc, etc.) are type-checked at compile
time; keep the alias and the updated operatorEntries export in this file so
existing imports (FunctionContext, ParseTree, Grid) are reused.
packages/sheets/test/formula/formula.test.ts (1)

3700-3830: LGTM.

Good, focused coverage for the new operator-backed functions. One optional enhancement to consider: none of the ISBETWEEN cases exercise cell-reference inputs or error-propagation (e.g., ISBETWEEN(A1, 1, 10) with A1 unset, or with a #DIV/0! value/bound). Adding a case or two there would lock in the resolveScalar/error-propagation paths in isbetweenFunc.

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

In `@packages/sheets/test/formula/formula.test.ts` around lines 3700 - 3830, Add
tests to exercise ISBETWEEN with cell-reference inputs and error propagation:
add cases using evaluate('=ISBETWEEN(A1,1,10)', grid) where A1 is unset (should
behave like blank/zero per resolveScalar) and where A1 or bounds contain an
error token (e.g., '#DIV/0!') to verify isbetweenFunc forwards errors; target
the test block named "ISBETWEEN" and use the evaluate helper and isbetweenFunc
semantics to assert correct TRUE/FALSE or error string results so
resolveScalar/error-propagation paths are covered.
🤖 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/sheets/formula-coverage.md`:
- Line 45: Update the coverage table and summary to remove stale Operator rows
and reconcile totals: delete or update the outdated "ISBETWEEN" and the
"Operator functions (ADD, MINUS, MULTIPLY, etc.)" out-of-scope note so they no
longer contradict the Operator row that shows 100% coverage, then recalculate
and update the unique-function count and summary totals to reflect the newly
included Operator entries (ensure references to "Operator", "ISBETWEEN", and the
"Operator functions (ADD, MINUS, MULTIPLY, etc.)" strings are updated/removed
consistently across the document).

In `@packages/sheets/src/formula/functions-operator.ts`:
- Around line 280-297: The concatFunc implementation correctly enforces a
minimum of 2 arguments (returns ErrNode.NA when args.expr().length < 2) but
tests only cover 2 and 3 args; add a unit test that invokes concat with a single
literal argument and asserts the result is `#N/A` (ErrNode.NA) to prevent
regressions—follow the existing test style used for concat's 2- and 3-argument
cases and assert the visited evaluation yields an error node representing NA.

---

Nitpick comments:
In `@packages/sheets/src/formula/functions-operator.ts`:
- Around line 31-52: The compareScalars function's declared return type includes
an unused "{ t: 'bool'; v: boolean }" variant; change its signature to return
number | ErrNode (remove the bool node variant) and keep the implementation
returning only numbers or ErrNode; then update call sites that handle its result
(the comparisons that currently test for an object or cast to ErrNode) to simply
use typeof cmp !== 'number' to detect errors and narrow types without casts
(look for usages of compareScalars and the existing object/'t' in cmp checks and
replace them with a numeric-type check).
- Around line 9-23: The resolveScalar function duplicates coercion logic already
implemented in resolveValue (formula.ts); remove resolveScalar, export or make
resolveValue available from formula.ts, import resolveValue into this module,
and replace all uses of resolveScalar with resolveValue (ensuring EvalNode and
Grid types are imported or compatible). Verify signatures match (node: EvalNode,
grid?: Grid) and update any call sites in functions-operator.ts to pass the same
args so comparison semantics remain consistent.
- Around line 200-223: The function comparisonFunc currently switches on op
('eq'|'ne'|'lt'|'lte'|'gt'|'gte') and returns inside each case but has no
fallback return; add an exhaustive fallback after the switch in the returned
arrow function to avoid undefined returns — either throw a descriptive Error
mentioning the unexpected op or return an ErrNode/EvalNode indicating an
internal error. Update comparisonFunc (the inner returned function that calls
twoArgs, resolveScalar and compareScalars) so any unknown op value is handled
deterministically (use the same ErrNode shape used elsewhere) to satisfy
noImplicitReturns and prevent silent undefined.
- Around line 315-333: Create a proper function type alias (e.g., OperatorFn)
matching the real signature used by all operator implementations: (ctx:
FunctionContext, visit: (tree: ParseTree) => EvalNode, grid?: Grid) => EvalNode,
then change the operator registry declaration for operatorEntries from [string,
(...args: any[]) => EvalNode][] to [string, OperatorFn][] so misregistered
entries (e.g., addFunc, minusFunc, concatFunc, uniqueFunc, etc.) are
type-checked at compile time; keep the alias and the updated operatorEntries
export in this file so existing imports (FunctionContext, ParseTree, Grid) are
reused.

In `@packages/sheets/test/formula/formula.test.ts`:
- Around line 3700-3830: Add tests to exercise ISBETWEEN with cell-reference
inputs and error propagation: add cases using evaluate('=ISBETWEEN(A1,1,10)',
grid) where A1 is unset (should behave like blank/zero per resolveScalar) and
where A1 or bounds contain an error token (e.g., '#DIV/0!') to verify
isbetweenFunc forwards errors; target the test block named "ISBETWEEN" and use
the evaluate helper and isbetweenFunc semantics to assert correct TRUE/FALSE or
error string results so resolveScalar/error-propagation paths are covered.

In `@packages/sheets/test/formula/function-catalog.test.ts`:
- Around line 419-431: Add explicit assertions to lock down the Operator
category entries: call findFunction('ADD'), findFunction('CONCAT') and
findFunction('UNIQUE') and assert their .category resolves to 'Operator' (i.e.,
expect(findFunction('ADD')!.category).toEqual('Operator'), etc.). Add these
assertions in the same test near the listFunctionCategories() check so the test
validates the catalog entries and the CONCAT/UNIQUE category assignment using
the existing findFunction helper.
🪄 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: 2287b3b9-0f3e-435b-99f0-17614a9086dc

📥 Commits

Reviewing files that changed from the base of the PR and between 7910f8a and 31f76b2.

📒 Files selected for processing (9)
  • docs/design/sheets/formula-coverage.md
  • packages/sheets/src/formula/function-catalog.ts
  • packages/sheets/src/formula/functions-helpers.ts
  • packages/sheets/src/formula/functions-lookup.ts
  • packages/sheets/src/formula/functions-operator.ts
  • packages/sheets/src/formula/functions-text.ts
  • packages/sheets/src/formula/functions.ts
  • packages/sheets/test/formula/formula.test.ts
  • packages/sheets/test/formula/function-catalog.test.ts
💤 Files with no reviewable changes (1)
  • packages/sheets/src/formula/functions-text.ts

Comment thread docs/design/sheets/formula-coverage.md
Comment thread packages/sheets/src/formula/functions-operator.ts
@hackerwins
hackerwins merged commit c87f21f into main Apr 22, 2026
4 checks passed
@hackerwins
hackerwins deleted the add-operator-func branch April 22, 2026 16:34
@hackerwins hackerwins mentioned this pull request May 1, 2026
3 tasks
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