Proposal for Array.prototype.count

I frequently find myself needing to know how many items in an array meet a specific condition. There are already prototype functions like every and some which can tell you if a member/all members of the array meet a condition, but not exactly how many members do.

To solve this problem, I would like to propose an addition to the Array prototype called count. Array.prototype.count would accept a callback function as its only argument. For each member of the array it would call the callback function, passing that member as an argument, and return the number of elements of the array for which the callback function returns truthy. In all cases, array.count(fn) would be equivalent to array.reduce((acc, curr) => acc + fn(curr) ? 1 : 0, 0).

I'm new to the proposal process, so any advice or next steps would be greatly appreciated!

1 Like

What about just doing array.filter(<your condition>).length?

Admittedly, that is a bit cleaner than my reduce, but it does come with the overhead of creating a new array.

To compare to some existing Array methods, array.some(<condition>) could also be expressed as array.filter(<condition>).length > 0 but I think people found value in wrapping that functionality into a single method, and I would make the same argument here.

One common first step is to demonstrate that the problem being solved is a common one. For example, showing the number of times this pattern appears across open source projects, or how many downloads an equivalent library has.

There is a difference here though. array.some(<condition>) will stop iterating over the array as soon as the something returns true. array.filter(<condition>).length > 0 will always iterate over the whole array. The browser can't optimize this behavior (and iterate less often), because it doesn't know if your <condition> is also performing side-effects.

On the other hand, it's possible that when you do array.filter(<your condition>).length, the browser could see that all you're doing is taking the length of the filtered array, and optimize away the intermediate array that's being generated. I don't know if they actually do this, but I am constantly surprised by the types of things they do optimize. And, instead of creating a new .count() function for the purpose of optimizations, we could request that javascript engines start optimizing this particular behavior (not as an official requirement, just as a "please do this" kind of thing).

That's an interesting point, but I think it's more useful to the developer to have a semantic guarantee of behavior instead of writing code and hoping that the browser implements it efficiently. There are plenty of ways that you can achieve the same result (we've each shown an alternative in a single line of code) but this is more about the convenience than the functionality.

There's also something to be said for the "obviousness" of a solution. When I want to take an array and condense it to a single value, my mind immediately jumps to reduce. In this case, filter is a great alternative (for small arrays) but it doesn't align as closely with the intent of the programmer so it's more likely to get overlooked.

3 Likes

Great idea! I know that this is a problem I run into frequently with my code, but I didn't know how to quantify how common it is for the community at large.

A quick Google search for "js count occurrences in array" yields a few StackOverflow questions, including this one with 214 upvotes (and 376 upvotes on an answer that basically mirrors this conversation), as well as a large number of tutorial sites like w3schools walking readers through this exact problem.

Searching for this pattern in open source code is a little more tricky since I won't know variable names, implementation details, etc. so I would love to know if you have any pointers on how to do that.

Notice that the OP doesn't mention optimization. My guess is that's because the purpose of this proposal is not engine optimization, but improving code readability.

Array.prototype.filter is often misused where .some was meant. It's also frequently used for counting elements, because counting with .reduce is too verbose. But if .count was available, counting with .filter would qualify as code obfuscation.

Here some examples:

node/deps/npm/node_modules/npmlog/log.js:
    if (trackerConstructors.filter(function (C) { return C === P }).length) return

node/deps/npm/node_modules/@npmcli/git/lib/is-clean.js:
      .map(l => l.trim()).filter(l => l).length)

node/deps/npm/node_modules/npm-audit-report/lib/reporters/detail.js:
    if (vuln.via.filter(v => typeof v !== 'string').length !== 0)

node/deps/v8/tools/clusterfuzz/js_fuzzer/source_helpers.js:
    return !!this.ast.program.directives.filter(isStrictDirective).length;

node/test/sequential/test-worker-prof.js:
    const ticks = lines.filter((line) => /^tick,/.test(line)).length;

node/tools/doc/node_modules/argparse/argparse.js:
        let from = Object.entries(descriptor).filter(([ k, v ]) => k[0] !== '*' && v !== no_default).length

node/tools/doc/node_modules/argparse/argparse.js:
        let to = Object.entries(descriptor).filter(([ k ]) => k[0] !== '*').length

babel/node_modules/eslint/lib/rules/array-element-newline.js:
            }).filter(isBreak => isBreak === true).length;

babel/node_modules/eslint/lib/rules/indent-legacy.js:
            const spaces = indentChars.filter(char => char === " ").length;

babel/node_modules/eslint/lib/rules/indent-legacy.js:
            const tabs = indentChars.filter(char => char === "\t").length;

babel/node_modules/eslint/lib/rules/indent.js:
            const numSpaces = actualIndent.filter(char => char === " ").length;

babel/node_modules/eslint/lib/rules/indent.js:
            const numTabs = actualIndent.filter(char => char === "\t").length;

babel/node_modules/eslint/lib/rules/multiline-comment-style.js:
                .filter(line => line.trim().length)

babel/node_modules/eslint/lib/rules/multiline-comment-style.js:
                .filter(line => line.trim().length)

babel/node_modules/eslint/lib/rules/no-multi-spaces.js:
        const hasExceptions = Object.keys(exceptions).filter(key => exceptions[key]).length > 0;

babel/node_modules/eslint/lib/init/config-initializer.js:
    const enabledRules = finalRuleIds.filter(ruleId => (newConfig.rules[ruleId] !== 0)).length;

babel/node_modules/webpack/node_modules/commander/index.js:
        this._args.filter(function(a) { return a.required; }).length === 0) {

babel/node_modules/webpack/lib/performance/SizeLimitsPlugin.js:
	compilation.chunks.filter(chunk => !chunk.canBeInitial()).length >

babel/node_modules/prettier/index.js:
  const crlf = newlines.filter(newline => newline === '\r\n').length;

babel/node_modules/prettier/index.js:
  const containsTag = node.children.filter(isJsxNode$4).length > 0;

babel/node_modules/prettier/index.js:
  const containsMultipleExpressions = node.children.filter(child => child.type === "JSXExpressionContainer").length > 1;

babel/node_modules/prettier/index.js:
  return ["superClass", "extends", "mixins", "implements"].filter(key => Boolean(node[key])).length > 1;

babel/node_modules/rollup/dist/shared/rollup.js:
            .filter(outputFile => Object.keys(outputFile).length > 0).sort((outputFileA, outputFileB) => {

babel/node_modules/rollup/dist/es/shared/rollup.js:
            .filter(outputFile => Object.keys(outputFile).length > 0).sort((outputFileA, outputFileB) => {

babel/node_modules/typescript/lib/tsserver.js:
        ts.filter(packageJson._requiredBy, function (r) { return r[0] === "#" || r === "/"; }).length === 0) {

babel/node_modules/typescript/lib/tsserverlibrary.js:
        ts.filter(packageJson._requiredBy, function (r) { return r[0] === "#" || r === "/"; }).length === 0) {

babel/node_modules/typescript/lib/typescript.js:
        ts.filter(packageJson._requiredBy, function (r) { return r[0] === "#" || r === "/"; }).length === 0) {

babel/node_modules/typescript/lib/typescriptServices.js:
        ts.filter(packageJson._requiredBy, function (r) { return r[0] === "#" || r === "/"; }).length === 0) {

babel/node_modules/typescript/lib/typingsInstaller.js:
        ts.filter(packageJson._requiredBy, function (r) { return r[0] === "#" || r === "/"; }).length === 0) {

elasticsearch-js/test/integration/reporter.js:
          suite['@failures'] = testcaseList.filter(t => t.failure).length

elasticsearch-js/test/integration/reporter.js:
          suite['@skipped'] = testcaseList.filter(t => t.skipped).length

elasticsearch-js/test/integration/test-runner.js:
    shouldSkip = !!action.features.filter(f => !~supportedFeatures.indexOf(f)).length

elasticsearch-js/test/unit/helpers/bulk.test.js:
          t.strictEqual(params.body.split('\n').filter(Boolean).length, 6)

1 Like

SourceGraph has a powerful 'structural search' mode which helps with this.

sourceGraph search for lang:javascript .filter(...).length

2 Likes

That's pretty neat. The search you posted easily returns thousands of instances (though some of them should really be replaced by some rather than count). Searching for reduce uses is a bit tougher since the structure relies on the implementation of the inner function. I didn't find as many reduce instances, but it's possible that my search criteria were bad.