如何计算数组中出现的次数并将其转换为对象数组?

How to count the number of occurances in an array and translate it into an array of object?

我正在尝试输入一组名称(有重复项)并输出一组对象。

输入:单个数组

const arr = ['M', 'M', 'M', 'S', 'S', 'S', 'R', 'C', 'S', 'S', 'S']

输出:对象数组

[
  {
    name: 'M'
    duplicates: 3
  },
  {
    name: 'S'
    duplicates: 6
  }...
]

我已经尝试过这个解决方案,但它为我提供了键名的每个数组项的名称。那不是我想要的。

这是我当前的代码:

const result = arr.reduce((acc, curr) => {
    acc[curr] ??
        (acc[curr] = {
            [curr]: 0,
        })
    acc[curr][curr]++
    return acc
}, [])
console.log(result)

/*output:
[
  C: {C: 1}
  M: {M: 3}
  R: {R: 1}
  S: {S: 6}
]
*/

通过元素索引累加器,增加匹配键的计数,在末尾去除索引(Object.values)...

const arr = ['M', 'M', 'M', 'S', 'S', 'S', 'R', 'C', 'S', 'S', 'S'];

let result = Object.values(arr.reduce((acc, el) => {
  if (!acc.hasOwnProperty(el)) acc[el] = { name: el, duplicates: 0 };
  acc[el].duplicates++;
  return acc;
}, {}));


console.log(result)

我想讨论一个更难的方法来做到这一点。

对,没错,更难!但这只有在您从头开始构建时才会如此。如果你已经有了一个实用函数库,它可能会简单得多。

我们最终将编写一个如下所示的转换函数:

const transform = pipe (
  countBy (identity),
  Object .entries,
  map (zipObj (['name', 'duplicates']))
)

我们的想法是,通过一些小的实用函数,我们可以以更简单的声明方式编写这样的函数,将其描述为一组转换。

下面我们开始构建我们自己的实用函数列表。这些是根据 Ramda 中的函数建模的(免责声明:我是 Ramda 作者),但我们在这里编写了自己的版本。我们的功能将比 Ramda 中的功能更简单,而且功能也不那么强大。但它们将涵盖大量用例并且易于扩展。

第 1 步:count

我们要做的第一件事是按键统计元素。

我们要拍

['M', 'M', 'M', 'S', 'S', 'S', 'R', 'C', 'S', 'S', 'S']

并将其转换为

{M: 3, S: 6, R: 1, C: 1}

我们可以将这个函数写成对我们值的简单归约,像这样1:

const count = (xs) => 
  xs .reduce ((a, x) => ((a [x] = (a [x] || 0) + 1), a), Object .create (null))

我们可以就此打住,但很容易看到一些有用的扩展。也许我们不关心大小写,想把所有小写的m和大写的M一起统计。或者我们有一个人的生日列表,我们想按出生年份来计算他们。或者许多场景中的任何一个。如果我们将该函数扩展为也采用转换函数,我们可以一次覆盖所有这些。它可能看起来像这样2:

const countBy = (fn) => (xs) =>
  xs .reduce ((a, x) => ((a [fn (x)] = (a [fn (x)] || 0) + 1), a), Object .create (null))

我们可能会这样使用它:

const decade = ({dob}) => `${dob .slice (0, 3)}0-${dob .slice (2, 3)}9`
const people = [{name: 'sue', dob: '1950-10-31'}, {name: 'bob', dob: '1967-04-28'},
                {name: 'jan', dob: '1972-02-26'}, {name: 'ron', dob: '1966-01-05'},
                {name: 'lil', dob: '1961-04-17'}, {name: 'tim', dob: '1958-09-12'}]

countBy (decade) (people) //=> {"1950-59": 2, "1960-69": 3, "1970-79": 1}

现在我们可以通过将 identity 函数传递给 countBy 来重写 count 来使用它。 identity 是微不足道的 (x) => x,结果却出人意料地有用:

const count = countBy (identity)

第 2 步:分离属性

所以现在我们有 {M: 3, S: 6, R: 1, C: 1},但我们需要将其转换为一个对象数组,每个对象对应一个 属性。为此有一个内置的 JS 函数,Object.entries。 (Ramda 有自己的版本 toPairs,早于 Object.entries,但现在没有理由不使用内置版本。)

那么我们可以做

Object .entries ({M: 3, S: 6, R: 1, C: 1})

回来

[['M', 3], ['S', 6], ['R', 1], ['C', 1]]

第 3 步:转换为最终形式,使用 zipObj

现在我们要转换,比如说['M', 3]{name: 'M', duplicates: 3}

一个不错的可能性是编写一个 zip* 函数,将两个等长列表压缩到一个新结构中。有许多可能的变体,但让我们写一个简单地采用键列表和相同大小的值列表,并将它们配对作为对象的条目3:

const zipObj = (ks) => (vs) =>
  ks .reduce ((a, k, i) => ((a [k] = vs [i]), a), {})

有了这个,我们可以调用

zipObj (['name', 'duplicates']) (['M', 3])
//=> {name: 'M', duplicates: 3}

第 4 步:mapping 所有条目的结果

我们现在可以将单个条目转换成它的最终形式,但我们必须用一个充满此类条目的数组来完成此操作。我们知道如何使用 Array.prototype.map 来做到这一点。但是直接使用有两个问题。一个有点晦涩: Array.prototype.map passes additional parameters (索引和整个数组)除了我们的初始值。有时会导致问题。第二个问题很简单:对于这里提倡的风格,纯函数比对象方法要好得多。

所以我们写了一个简单的函数map,它简单地调用Array.prototype.map4:

const map = (fn) => (xs) =>
  xs .map ((x) => fn (x))

我们可以这样使用它:

map (zipObj (['name', 'duplicate'])) (
  [['M', 3], ['S', 6], ['R', 1], ['C', 1]]
)

产生最终结果。

在注意到 Array.prototype.map 中的缺陷后,我们将其用于 map 的实现中,这似乎很奇怪,但我相信这是有道理的。我们通过定义 map 来解决第二个问题。第一个问题只需通过管理 Array.prototype.map 即可解决,仅将值传递给它。 Ramda 从头开始​​重写这些,这样做可以勉强获得更多的性能,但随着时间的推移,性能优势被侵蚀,代码也更加繁琐。

第 5 步:pipe将这些函数组合在一起

为了使这成为一组漂亮的声明性步骤,我们还需要一个部分:一种将函数粘合在一起的方法,一个接一个 运行。我的图像是数据流经的管道,因此使用 pipe 来实现此功能。这与函数组合的数学概念非常相似,Ramda 包括 pipecompose,它们的工作方式非常相似,但以相反的顺序列出它们的参数。

pipe 是一个简单的函数:

const pipe = (...fns) => (x) =>
  fns .reduce ((a, fn) => fn (a), x)

我们简单地遍历函数,将上一次调用的结果传递给下一个函数,从传入的值开始。

有了这个,我们现在可以把它们放在一起了。

第 6 步:组合我们的函数

有了以上所有内容,我们现在可以写出原问题的解决方案了:

const countBy = (fn) => (xs) =>
  xs .reduce ((a, x) => ((a [fn (x)] = (a [fn (x)] || 0) + 1), a), Object .create (null))
const identity = (x) => 
  x
const count = countBy (identity)
const zipObj = (ks) => (vs) =>
  ks .reduce ((a, k, i) => ((a [k] = vs [i]), a), {})
const map = (fn) => (xs) =>
  xs .map ((x) => fn (x))
const pipe = (...fns) => (x) =>
  fns .reduce ((a, fn) => fn (a), x)

const transform = pipe (
  countBy (identity),
  Object .entries,
  map (zipObj (['name', 'duplicates']))
)

const arr = ['M', 'M', 'M', 'S', 'S', 'S', 'R', 'C', 'S', 'S', 'S']

console .log (transform (arr))
console .log (transform (['toString', 'toString', 'valueOf']))
.as-console-wrapper {max-height: 100% !important; top: 0}

请注意,此处唯一的自定义代码是 transform。其余的是我们可以为系统的其他部分和未来系统保留的实用函数。

课程

  • 小的辅助函数可以以强大的方式组合起来,使我们更容易编写解决更大问题的方法。

  • 坚持不懈地将代码分解成越来越小的部分是值得的,直到我们找到隐藏在问题中的辅助函数。

  • 一旦你拥有大量的辅助函数,解决这些问题就会变得非常快。实际上,我在不到两分钟的时间内就在 Ramda 中为此编写了 my solution。最终版本的不同之处仅在于添加了 count,而 Ramda 版本需要 countBy (identity) 以及我使用 Ramda 的 toPairs 而不是 Object.entries.

未决问题

  • 性能:这几乎肯定不如 danh 的答案那么高效。这个最终会多次循环数据。有时这是一个真正的问题。您将必须根据您的具体情况决定更简洁的代码是否值得一些低效率。没有通用的答案。但我倾向于使用尽可能简单的代码 运行 。如果我的系统速度不符合我的标准,我会分析以找到最重要的瓶颈并首先解决这些瓶颈。很少有人像这样更改代码。

  • API设计:我这里把要求的输出作为硬性要求。可能是;我不知道OP的需求。但我会发现 countBy{M: 3, S: 6, R: 1, C: 1} 的输出对于大多数处理来说是更有用的结构。我建议始终检查更简单的结构是否比定制的结构更能满足需求。



1 使用 Object .create (null) 而不是 {} 使我们能够在对另一个答案的评论中处理 .通过使用 null 原型创建,我们可以避免在累加器对象中对 'toString'.

等属性进行虚假匹配

2 我们可能应该注意到 countBy 中的低效率:它计算了 fn(x) 两次。解决这个问题很简单,但它让我们偏离了这里的要点。老实说,我也经常不打扰。

3 有一整套函数涉及压缩两个大小相等的数组。如果我们扩展它,最终我们可能会在更通用的 zipWith 之上重写 zipObj,就像我们在 countBy 之上重写 count 一样。这留作 reader.

的练习

4 最终,我们可能希望扩展此功能以也适用于对象,或许还有其他类型。但现在这已经足够了。