Skip to content

fix: skip non-numeric values in median aggregation - #6523

Merged
KevinVandy merged 2 commits into
TanStack:mainfrom
tarann26:fix/median-aggregation-skip-non-numeric
Aug 7, 2026
Merged

fix: skip non-numeric values in median aggregation#6523
KevinVandy merged 2 commits into
TanStack:mainfrom
tarann26:fix/median-aggregation-skip-non-numeric

Conversation

@tarann26

@tarann26 tarann26 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Changes

aggregationFn_median bailed out and returned undefined for the whole group the moment it hit a single non-numeric value (e.g. null). That's inconsistent with every other built-in aggregation function — sum, min, max, and mean all skip non-numeric/nullish values and aggregate over whatever numeric values remain instead of discarding the group.

This brings median in line with its siblings: it now filters out non-numeric values first, computes the median from what's left, and only returns undefined when no numeric values remain at all.

Fixes #5008

Note on behavior change

This does change the previously-asserted result for groups that contain non-numeric values. The existing test had:

expect(aggregationFn_median.aggregate(context([3, '2', 1, 2]))).toBeUndefined()

That assertion documented the buggy early-return behavior, not an intentional design choice — there's no rationale for it in the aggregation-overhaul PRs, and it directly contradicts how sum/min/max/mean already handle the same situation. I updated it to reflect the fixed behavior (median of the numeric values [3, 1, 2] -> 2) and added a dedicated regression test covering: a null inside a group, a mix of null/undefined/non-numeric-string, an even-length numeric result, and the all-non-numeric -> undefined fallback case.

Testing

  • pnpm --filter @tanstack/table-core test:lib — 62 files / 1272 tests passing
  • pnpm --filter @tanstack/table-core test:eslint — clean
  • pnpm --filter @tanstack/table-core test:types — clean

Summary by CodeRabbit

  • Bug Fixes

    • Median aggregation now ignores null, missing, and non-numeric values.
    • Median results are calculated from the remaining numeric values, including fractional results for even-sized datasets.
    • Returns no result only when there are no numeric values to aggregate.
  • Tests

    • Added coverage for mixed, invalid, and empty aggregation inputs.

aggregationFn_median bailed out and returned undefined for an entire
group as soon as it hit a single non-numeric (e.g. null) value, unlike
sum/min/max/mean which all skip non-numeric values and aggregate over
whatever numeric values remain. Bring median in line with the rest of
the built-in aggregation functions: filter out non-numeric values and
compute the median from what's left, only returning undefined when no
numeric values remain.

Fixes TanStack#5008
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The median aggregation now ignores nullish and non-numeric values. It returns undefined when no numeric values remain. Tests cover mixed inputs, numeric strings, fractional medians, and empty results.

Changes

Median aggregation behavior

Layer / File(s) Summary
Filter values and validate median results
packages/table-core/src/features/row-aggregation/aggregationFns.ts, packages/table-core/tests/unit/fns/aggregationFns.test.ts
The median aggregation filters non-numeric values before sorting and calculation. It returns undefined when the filtered input is empty. Tests cover nullish values, non-numeric values, numeric strings, mixed inputs, even-length results, and empty results.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: kevinvandy

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: skipping non-numeric values in median aggregation.
Description check ✅ Passed The description explains the change, motivation, issue link, behavior change, regression coverage, and test results; template checklist sections are omitted.
Linked Issues check ✅ Passed The implementation filters non-numeric values, calculates the median from remaining numbers, and returns undefined when none remain, satisfying issue #5008.
Out of Scope Changes check ✅ Passed The code and test changes are limited to the median aggregation behavior described in issue #5008.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/table-core/tests/unit/fns/aggregationFns.test.ts (1)

72-78: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Make the string-filtering assertions distinguish skipping from coercion.

[3, '2', 1, 2] and [null, undefined, 4, '5', 6] produce the same median whether the string is ignored or coerced. Add a case such as [1, '100', 3] with an expected median of 2.

Proposed test
+    expect(aggregationFn_median.aggregate(context([1, '100', 3]))).toBe(2)

Also applies to: 89-91

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/table-core/tests/unit/fns/aggregationFns.test.ts` around lines 72 -
78, Update the median assertions in the test “preserves mean coercion and median
numeric-only behavior” to include a case such as [1, '100', 3] expecting 2,
clearly verifying that numeric strings are skipped rather than coerced. Apply
the same distinction to the additional median assertion around the referenced
range while preserving the existing mean coercion coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/table-core/src/features/row-aggregation/aggregationFns.ts`:
- Around line 273-275: Update the median aggregation input filter in the
relevant aggregation function to push values only when they are numbers and not
NaN, so all-NaN inputs return undefined and mixed inputs ignore NaN. Add
regression cases covering [NaN] and [NaN, 1].

---

Nitpick comments:
In `@packages/table-core/tests/unit/fns/aggregationFns.test.ts`:
- Around line 72-78: Update the median assertions in the test “preserves mean
coercion and median numeric-only behavior” to include a case such as [1, '100',
3] expecting 2, clearly verifying that numeric strings are skipped rather than
coerced. Apply the same distinction to the additional median assertion around
the referenced range while preserving the existing mean coercion coverage.
🪄 Autofix

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 Plus

Run ID: 293bdc5d-c1a4-4ee9-a072-40018dd624b5

📥 Commits

Reviewing files that changed from the base of the PR and between df470c5 and 728f0b9.

📒 Files selected for processing (2)
  • packages/table-core/src/features/row-aggregation/aggregationFns.ts
  • packages/table-core/tests/unit/fns/aggregationFns.test.ts

Comment on lines 273 to 275
const value = context.getValue(rows[i]!)
if (typeof value !== 'number') return undefined
values[i] = value
if (typeof value === 'number') values.push(value)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate file and relevant symbols:"
git ls-files | rg 'packages/table-core/src/features/row-aggregation/aggregationFns\.ts|aggregationFns\.ts' || true

echo
echo "Relevant lines:"
if [ -f packages/table-core/src/features/row-aggregation/aggregationFns.ts ]; then
  nl -ba packages/table-core/src/features/row-aggregation/aggregationFns.ts | sed -n '230,305p'
fi

echo
echo "Search for median/mean implementations:"
rg -n "aggregationFn_median|aggregationFn_mean|Number\.isNaN|typeof value === 'number'" packages/table-core/src/features/row-aggregation/aggregationFns.ts packages/table-core/src || true

echo
echo "Behavioral probe for median-like filter:"
node - <<'JS'
function currentMedianValues(values) {
  const out = []
  for (const value of values) {
    if (typeof value === 'number') out.push(value)
  }
  return out
}
for (const input of [[NaN], [NaN, 1], [1, NaN], [NaN, 2, 3]]) {
  console.log(JSON.stringify(input), '=>', JSON.stringify(currentMedianValues(input)))
}
console.log('typeof NaN === "number":', typeof NaN === 'number')
console.log('Number.isNaN(NaN):', Number.isNaN(NaN))
console.log('median-like value:', Number.isNaN(input => {
  const filtered = input.filter(x => typeof x === 'number' && !Number.isNaN(x))
  filtered.sort((a,b) => a-b)
  const midIdx = Math.floor(filtered.length / 2)
  return filtered.length % 2 === 0 && filtered.length > 0
    ? (filtered[midIdx - 1] + filtered[midIdx]) / 2
    : filtered[midIdx]
})([NaN, 1]) ? 'not computed' : undefined)
JS

Repository: TanStack/table

Length of output: 475


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Relevant lines:"
awk '{printf "%6d\t%s\n", NR, $0}' packages/table-core/src/features/row-aggregation/aggregationFns.ts | sed -n '230,305p'

echo
echo "Search for median/mean implementations:"
rg -n "aggregationFn_median|aggregationFn_mean|Number\.isNaN|typeof value === 'number'|" packages/table-core/src/features/row-aggregation/aggregationFns.ts packages/table-core/src || true

echo
echo "Behavioral probe:"
node - <<'JS'
function currentValues(values) {
  const out = []
  for (const value of values) {
    if (typeof value === 'number') out.push(value)
  }
  return out
}
for (const input of [[NaN], [NaN, 1], [1, NaN], [NaN, 2, 3]]) {
  console.log(JSON.stringify(input), '=>', JSON.stringify(currentValues(input)))
}
console.log('typeof NaN === "number":', typeof NaN === 'number')
console.log('Number.isNaN(NaN):', Number.isNaN(NaN))

function medianLike(values) {
  const filtered = currentValues(values)
  const n = filtered.length
  if (!n) return NaN
  const sorted = [...filtered].sort((a,b) => a-b)
  const mid = Math.floor(n / 2)
  return n % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]
}
console.log('current median([NaN]):', medianLike([NaN]))
console.log('current median([NaN, 1]):', medianLike([NaN, 1]))
JS

Repository: TanStack/table

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "aggregatedFn section:"
awk '{printf "%6d\t%s\n", NR, $0}' packages/table-core/src/features/row-aggregation/aggregationFns.ts | sed -n '250,290p'

echo
echo "Number.isNaN occurrences:"
rg -n "Number\.isNaN|type === 'number'|values\.sort|median|mean" packages/table-core/src/features/row-aggregation/aggregationFns.ts

echo
echo "Behavioral probe:"
node - <<'JS'
function currentValues(values) {
  const out = []
  for (const value of values) {
    if (typeof value === 'number') out.push(value)
  }
  return out
}
function sortedMedian(values) {
  const filtered = currentValues(values)
  const sorted = [...filtered].sort((a,b) => a-b)
  const n = sorted.length
  const mid = Math.floor(n / 2)
  return n % 2 === 0 && n > 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]
}
for (const input of [[NaN], [NaN, 1], [1, NaN], [NaN, 2, 3]]) {
  console.log(JSON.stringify(input), '=> values', JSON.stringify(currentValues(input)), 'median', sortedMedian(input))
}
console.log('typeof NaN === "number":', typeof NaN === 'number')
console.log('Number.isNaN(NaN):', Number.isNaN(NaN))
console.log('proposed median([NaN]):', null)
JS

Repository: TanStack/table

Length of output: 2302


Exclude NaN from median inputs.

typeof value === 'number' includes NaN, so [NaN] returns NaN instead of undefined. Add !Number.isNaN(value) before pushing numeric values, and add regression cases for [NaN] and [NaN, 1].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/table-core/src/features/row-aggregation/aggregationFns.ts` around
lines 273 - 275, Update the median aggregation input filter in the relevant
aggregation function to push values only when they are numbers and not NaN, so
all-NaN inputs return undefined and mixed inputs ignore NaN. Add regression
cases covering [NaN] and [NaN, 1].

Source: MCP tools

@nx-cloud

nx-cloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 4169e7e

Command Status Duration Result
nx affected --targets=test:eslint,test:sherif,t... ✅ Succeeded 13m 6s View ↗
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 59s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-07 12:52:38 UTC

@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown
More templates

@tanstack/alpine-table

npm i https://pkg.pr.new/@tanstack/alpine-table@6523

@tanstack/angular-table

npm i https://pkg.pr.new/@tanstack/angular-table@6523

@tanstack/angular-table-devtools

npm i https://pkg.pr.new/@tanstack/angular-table-devtools@6523

@tanstack/ember-table

npm i https://pkg.pr.new/@tanstack/ember-table@6523

@tanstack/lit-table

npm i https://pkg.pr.new/@tanstack/lit-table@6523

@tanstack/match-sorter-utils

npm i https://pkg.pr.new/@tanstack/match-sorter-utils@6523

@tanstack/octane-table

npm i https://pkg.pr.new/@tanstack/octane-table@6523

@tanstack/preact-table

npm i https://pkg.pr.new/@tanstack/preact-table@6523

@tanstack/preact-table-devtools

npm i https://pkg.pr.new/@tanstack/preact-table-devtools@6523

@tanstack/react-table

npm i https://pkg.pr.new/@tanstack/react-table@6523

@tanstack/react-table-devtools

npm i https://pkg.pr.new/@tanstack/react-table-devtools@6523

@tanstack/solid-table

npm i https://pkg.pr.new/@tanstack/solid-table@6523

@tanstack/solid-table-devtools

npm i https://pkg.pr.new/@tanstack/solid-table-devtools@6523

@tanstack/svelte-table

npm i https://pkg.pr.new/@tanstack/svelte-table@6523

@tanstack/table-core

npm i https://pkg.pr.new/@tanstack/table-core@6523

@tanstack/table-devtools

npm i https://pkg.pr.new/@tanstack/table-devtools@6523

@tanstack/vue-table

npm i https://pkg.pr.new/@tanstack/vue-table@6523

@tanstack/vue-table-devtools

npm i https://pkg.pr.new/@tanstack/vue-table-devtools@6523

commit: 4169e7e

@KevinVandy
KevinVandy merged commit 18aaf92 into TanStack:main Aug 7, 2026
10 checks passed
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.

Median aggregation fails if null values - inconsistent with other aggregation functions- linked to #4837

2 participants