Skip to content

fix(compat/meanBy): skip undefined values when averaging to match lodash - #1978

Open
kdelay wants to merge 1 commit into
toss:mainfrom
kdelay:fix/compat-meanby-undefined
Open

fix(compat/meanBy): skip undefined values when averaging to match lodash#1978
kdelay wants to merge 1 commit into
toss:mainfrom
kdelay:fix/compat-meanby-undefined

Conversation

@kdelay

@kdelay kdelay commented Jul 30, 2026

Copy link
Copy Markdown

Summary

es-toolkit/compat's meanBy returns NaN whenever the iteratee produces undefined for any element, while lodash skips those values and still divides by the full length.

import { meanBy } from 'es-toolkit/compat';
import lodashMeanBy from 'lodash/meanBy';

meanBy([{ a: 1 }, {}], 'a'); // NaN (before this PR)
lodashMeanBy([{ a: 1 }, {}], 'a'); // 0.5

More cases measured against lodash@4.18.1 on main (81af489):

call compat (before) lodash
meanBy([{ a: 1 }, {}], 'a') NaN 0.5
meanBy([{ a: 1 }, { a: undefined }, { a: 3 }], 'a') NaN 1.333...
meanBy([1, undefined, 2]) NaN 1
meanBy({ 0: { a: 1 }, 1: {}, length: 2 }, 'a') NaN 0.5
meanBy([{ a: { b: 1 } }, {}], 'a.b') NaN 0.5

This is also inconsistent inside compat itself: mean([1, undefined, 2]) already returns 1 like lodash, because compat/mean builds on compat/sum, which skips undefined.

Cause

src/compat/math/meanBy.ts delegated to the main library's meanBy, which sums with the main library's sumBy. That one adds every value as-is, so one undefined turns the whole sum into NaN. lodash's baseMean divides baseSum — which ignores undefined — by array.length.

Changes

  • Compute the sum with compat's own sumBy, which already implements lodash's undefined-skipping behavior, and divide by the length of items. When every value is undefined the result stays NaN, matching lodash.
  • Drop the intermediate Array.from copy, since compat/sumBy accepts array-likes directly.
  • Add regression tests for missing keys, explicit undefined, deep paths, array-likes, and the all-undefined case.
  • Document the behavior in the compat reference docs for all four languages.

Only es-toolkit/compat is changed; es-toolkit's own meanBy keeps its stricter getValue: (element: T) => number contract.

Test results

  • yarn vitest run src/compat/math src/math — 35 files, 288 tests pass
  • yarn vitest run — 659 files pass (tests/check-dist.spec.ts fails to build locally on Node 24.10 with an unrelated tsdown/PnP loader error; it also fails on unmodified main here)
  • yarn typecheck, yarn workspace type-tests test — no errors
  • yarn eslint on the changed files — no errors
  • Differential run of the table above against lodash@4.18.1 in the benchmarks workspace: all cases match after the change

@kdelay
kdelay requested review from dayongkr and raon0211 as code owners July 30, 2026 12:32
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
es-toolkit Ready Ready Preview Aug 8, 2026 1:05pm

Request Review

`es-toolkit/compat`'s `meanBy` delegated to the main library's `meanBy`,
which sums every value as-is. A single `undefined` from the iteratee
therefore poisoned the whole sum and the result became `NaN`.

lodash skips `undefined` values while summing and still divides by the
full length of the input, so `meanBy([{ a: 1 }, {}], 'a')` returns `0.5`
in lodash but returned `NaN` here. `compat`'s own `mean` already matches
lodash because it builds on `compat`'s `sum`, so the two were also
inconsistent with each other.

Compute the sum with `compat`'s `sumBy`, which already implements
lodash's undefined-skipping behavior, and divide by the input length.
This also avoids the intermediate `Array.from` copy for array-likes.
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.

1 participant