如何计算数组中出现的次数并将其转换为对象数组?
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 步:map
ping 所有条目的结果
我们现在可以将单个条目转换成它的最终形式,但我们必须用一个充满此类条目的数组来完成此操作。我们知道如何使用 Array.prototype.map
来做到这一点。但是直接使用有两个问题。一个有点晦涩: Array.prototype.map
passes additional parameters (索引和整个数组)除了我们的初始值。有时会导致问题。第二个问题很简单:对于这里提倡的风格,纯函数比对象方法要好得多。
所以我们写了一个简单的函数map
,它简单地调用Array.prototype.map
4:
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 包括 pipe
和 compose
,它们的工作方式非常相似,但以相反的顺序列出它们的参数。
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 最终,我们可能希望扩展此功能以也适用于对象,或许还有其他类型。但现在这已经足够了。
我正在尝试输入一组名称(有重复项)并输出一组对象。
输入:单个数组
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 步:map
ping 所有条目的结果
我们现在可以将单个条目转换成它的最终形式,但我们必须用一个充满此类条目的数组来完成此操作。我们知道如何使用 Array.prototype.map
来做到这一点。但是直接使用有两个问题。一个有点晦涩: Array.prototype.map
passes additional parameters (索引和整个数组)除了我们的初始值。有时会导致问题。第二个问题很简单:对于这里提倡的风格,纯函数比对象方法要好得多。
所以我们写了一个简单的函数map
,它简单地调用Array.prototype.map
4:
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 包括 pipe
和 compose
,它们的工作方式非常相似,但以相反的顺序列出它们的参数。
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 最终,我们可能希望扩展此功能以也适用于对象,或许还有其他类型。但现在这已经足够了。