按数组中的对象分组最有效的方法是什么?

例如,给定此对象数组:

[ 
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 1", Value: "15" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 2", Value: "20" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 1", Value: "25" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 2", Value: "30" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 1", Value: "35" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 2", Value: "40" }
]

我正在表格中显示这些信息。我想通过不同的方法进行分组,但我想对值求和。

我将Undercore.js用于其groupby函数,这很有用,但并不能完成全部任务,因为我不希望它们“拆分”,而是“合并”,更像SQL groupby方法。

我要找的是能够合计特定值(如果需要)。

因此,如果我按阶段分组,我希望收到:

[
    { Phase: "Phase 1", Value: 50 },
    { Phase: "Phase 2", Value: 130 }
]

如果我组了阶段/步骤,我会收到:

[
    { Phase: "Phase 1", Step: "Step 1", Value: 15 },
    { Phase: "Phase 1", Step: "Step 2", Value: 35 },
    { Phase: "Phase 2", Step: "Step 1", Value: 55 },
    { Phase: "Phase 2", Step: "Step 2", Value: 75 }
]

是否有一个有用的脚本,或者我应该坚持使用Undercore.js,然后遍历生成的对象,自己计算总数?


当前回答

让我们在重用已经编写的代码(即Undercore)的同时充分回答最初的问题。如果将Undercore的>100个功能组合在一起,您可以使用Undercore做得更多。下面的解决方案演示了这一点。

步骤1:按财产的任意组合对数组中的对象进行分组。这使用了一个事实,即_.groupBy接受一个返回对象组的函数。它还使用了_.schain、_.pick、_.values、_.jjoin和_.value。请注意,这里并不严格需要_.value,因为链接的值在用作属性名称时会自动展开。我加入它是为了防止在没有自动展开的情况下有人试图编写类似的代码时发生混淆。

// Given an object, return a string naming the group it belongs to.
function category(obj) {
    return _.chain(obj).pick(propertyNames).values().join(' ').value();
}

// Perform the grouping.
const intermediate = _.groupBy(arrayOfObjects, category);

给定原始问题中的arrayOfObjects并将propertyNames设置为['Phase','Step'],intermediate将获得以下值:

{
    "Phase 1 Step 1": [
        { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
        { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" }
    ],
    "Phase 1 Step 2": [
        { Phase: "Phase 1", Step: "Step 2", Task: "Task 1", Value: "15" },
        { Phase: "Phase 1", Step: "Step 2", Task: "Task 2", Value: "20" }
    ],
    "Phase 2 Step 1": [
        { Phase: "Phase 2", Step: "Step 1", Task: "Task 1", Value: "25" },
        { Phase: "Phase 2", Step: "Step 1", Task: "Task 2", Value: "30" }
    ],
    "Phase 2 Step 2": [
        { Phase: "Phase 2", Step: "Step 2", Task: "Task 1", Value: "35" },
        { Phase: "Phase 2", Step: "Step 2", Task: "Task 2", Value: "40" }
    ]
}

步骤2:将每个组缩减为单个平面对象,并将结果返回到数组中。除了我们之前看到的函数之外,下面的代码使用了_.pulse、..first、..pick、..extend、..reduce和..map。.first保证在这种情况下返回一个对象,因为..groupBy不会产生空组。在这种情况下,值是必需的。

// Sum two numbers, even if they are contained in strings.
const addNumeric = (a, b) => +a + +b;

// Given a `group` of objects, return a flat object with their common
// properties and the sum of the property with name `aggregateProperty`.
function summarize(group) {
    const valuesToSum = _.pluck(group, aggregateProperty);
    return _.chain(group).first().pick(propertyNames).extend({
        [aggregateProperty]: _.reduce(valuesToSum, addNumeric)
    }).value();
}

// Get an array with all the computed aggregates.
const result = _.map(intermediate, summarize);

给定我们之前获得的中间值,并将aggregateProperty设置为Value,我们得到了询问者想要的结果:

[
    { Phase: "Phase 1", Step: "Step 1", Value: 15 },
    { Phase: "Phase 1", Step: "Step 2", Value: 35 },
    { Phase: "Phase 2", Step: "Step 1", Value: 55 },
    { Phase: "Phase 2", Step: "Step 2", Value: 75 }
]

我们可以将所有这些放在一个函数中,该函数将arrayOfObjects、propertyNames和aggregateProperty作为参数。请注意,arrayOfObjects实际上也可以是带有字符串键的普通对象,因为_.groupBy接受其中之一。为此,我将arrayOfObjects重命名为collection。

function aggregate(collection, propertyNames, aggregateProperty) {
    function category(obj) {
        return _.chain(obj).pick(propertyNames).values().join(' ');
    }
    const addNumeric = (a, b) => +a + +b;
    function summarize(group) {
        const valuesToSum = _.pluck(group, aggregateProperty);
        return _.chain(group).first().pick(propertyNames).extend({
            [aggregateProperty]: _.reduce(valuesToSum, addNumeric)
        }).value();
    }
    return _.chain(collection).groupBy(category).map(summarize).value();
}

aggregate(arrayOfObjects,['Phase','Step'],'Value')现在将再次为我们提供相同的结果。

我们可以更进一步,使调用者能够计算每个组中值的任何统计信息。我们可以这样做,也可以让调用者向每个组的摘要添加任意财产。我们可以在缩短代码的同时完成所有这些。我们用iteratee参数替换aggregateProperty参数,并将其直接传递给_.reduce:

function aggregate(collection, propertyNames, iteratee) {
    function category(obj) {
        return _.chain(obj).pick(propertyNames).values().join(' ');
    }
    function summarize(group) {
        return _.chain(group).first().pick(propertyNames)
            .extend(_.reduce(group, iteratee)).value();
    }
    return _.chain(collection).groupBy(category).map(summarize).value();
}

实际上,我们将部分责任转移给了来电者;她必须提供一个可以传递给.reduce的iterate,这样对.reduce的调用将生成一个包含她想要添加的聚合财产的对象。例如,我们使用以下表达式获得与之前相同的结果:

aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => ({
    Value: +memo.Value + +value.Value
}));

对于一个稍微复杂一点的迭代的示例,假设我们希望计算每个组的最大值而不是总和,并且我们希望添加一个列出组中出现的Task的所有值的Tasks属性。这里有一种方法可以做到这一点,使用上面最后一个版本的聚合(和_.union):

aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => ({
    Value: Math.max(memo.Value, value.Value),
    Tasks: _.union(memo.Tasks || [memo.Task], [value.Task])
}));

我们得到以下结果:

[
    { Phase: "Phase 1", Step: "Step 1", Value: 10, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 1", Step: "Step 2", Value: 20, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 2", Step: "Step 1", Value: 30, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 2", Step: "Step 2", Value: 40, Tasks: [ "Task 1", "Task 2" ] }
]

归功于@much2learn,他还发布了一个可以处理任意缩减函数的答案。我又写了几个SO答案,演示了如何通过组合多个Undercore函数来实现复杂的功能:

https://stackoverflow.com/a/64938636/1166087https://stackoverflow.com/a/64094738/1166087https://stackoverflow.com/a/63625129/1166087https://stackoverflow.com/a/63088916/1166087

其他回答

您可以使用Alasql JavaScript库来实现:

var data = [ { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
             { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" }];

var res = alasql('SELECT Phase, Step, SUM(CAST([Value] AS INT)) AS [Value] \
                  FROM ? GROUP BY Phase, Step',[data]);

在jsFiddle尝试这个示例。

BTW:在大型阵列(100000条记录及以上)上,Alasql比Linq更快。参见jsPref中的测试。

评论:

这里我将Value放在方括号中,因为Value是SQL中的关键字我必须使用CAST()函数将字符串值转换为数字类型。

让我们在重用已经编写的代码(即Undercore)的同时充分回答最初的问题。如果将Undercore的>100个功能组合在一起,您可以使用Undercore做得更多。下面的解决方案演示了这一点。

步骤1:按财产的任意组合对数组中的对象进行分组。这使用了一个事实,即_.groupBy接受一个返回对象组的函数。它还使用了_.schain、_.pick、_.values、_.jjoin和_.value。请注意,这里并不严格需要_.value,因为链接的值在用作属性名称时会自动展开。我加入它是为了防止在没有自动展开的情况下有人试图编写类似的代码时发生混淆。

// Given an object, return a string naming the group it belongs to.
function category(obj) {
    return _.chain(obj).pick(propertyNames).values().join(' ').value();
}

// Perform the grouping.
const intermediate = _.groupBy(arrayOfObjects, category);

给定原始问题中的arrayOfObjects并将propertyNames设置为['Phase','Step'],intermediate将获得以下值:

{
    "Phase 1 Step 1": [
        { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
        { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" }
    ],
    "Phase 1 Step 2": [
        { Phase: "Phase 1", Step: "Step 2", Task: "Task 1", Value: "15" },
        { Phase: "Phase 1", Step: "Step 2", Task: "Task 2", Value: "20" }
    ],
    "Phase 2 Step 1": [
        { Phase: "Phase 2", Step: "Step 1", Task: "Task 1", Value: "25" },
        { Phase: "Phase 2", Step: "Step 1", Task: "Task 2", Value: "30" }
    ],
    "Phase 2 Step 2": [
        { Phase: "Phase 2", Step: "Step 2", Task: "Task 1", Value: "35" },
        { Phase: "Phase 2", Step: "Step 2", Task: "Task 2", Value: "40" }
    ]
}

步骤2:将每个组缩减为单个平面对象,并将结果返回到数组中。除了我们之前看到的函数之外,下面的代码使用了_.pulse、..first、..pick、..extend、..reduce和..map。.first保证在这种情况下返回一个对象,因为..groupBy不会产生空组。在这种情况下,值是必需的。

// Sum two numbers, even if they are contained in strings.
const addNumeric = (a, b) => +a + +b;

// Given a `group` of objects, return a flat object with their common
// properties and the sum of the property with name `aggregateProperty`.
function summarize(group) {
    const valuesToSum = _.pluck(group, aggregateProperty);
    return _.chain(group).first().pick(propertyNames).extend({
        [aggregateProperty]: _.reduce(valuesToSum, addNumeric)
    }).value();
}

// Get an array with all the computed aggregates.
const result = _.map(intermediate, summarize);

给定我们之前获得的中间值,并将aggregateProperty设置为Value,我们得到了询问者想要的结果:

[
    { Phase: "Phase 1", Step: "Step 1", Value: 15 },
    { Phase: "Phase 1", Step: "Step 2", Value: 35 },
    { Phase: "Phase 2", Step: "Step 1", Value: 55 },
    { Phase: "Phase 2", Step: "Step 2", Value: 75 }
]

我们可以将所有这些放在一个函数中,该函数将arrayOfObjects、propertyNames和aggregateProperty作为参数。请注意,arrayOfObjects实际上也可以是带有字符串键的普通对象,因为_.groupBy接受其中之一。为此,我将arrayOfObjects重命名为collection。

function aggregate(collection, propertyNames, aggregateProperty) {
    function category(obj) {
        return _.chain(obj).pick(propertyNames).values().join(' ');
    }
    const addNumeric = (a, b) => +a + +b;
    function summarize(group) {
        const valuesToSum = _.pluck(group, aggregateProperty);
        return _.chain(group).first().pick(propertyNames).extend({
            [aggregateProperty]: _.reduce(valuesToSum, addNumeric)
        }).value();
    }
    return _.chain(collection).groupBy(category).map(summarize).value();
}

aggregate(arrayOfObjects,['Phase','Step'],'Value')现在将再次为我们提供相同的结果。

我们可以更进一步,使调用者能够计算每个组中值的任何统计信息。我们可以这样做,也可以让调用者向每个组的摘要添加任意财产。我们可以在缩短代码的同时完成所有这些。我们用iteratee参数替换aggregateProperty参数,并将其直接传递给_.reduce:

function aggregate(collection, propertyNames, iteratee) {
    function category(obj) {
        return _.chain(obj).pick(propertyNames).values().join(' ');
    }
    function summarize(group) {
        return _.chain(group).first().pick(propertyNames)
            .extend(_.reduce(group, iteratee)).value();
    }
    return _.chain(collection).groupBy(category).map(summarize).value();
}

实际上,我们将部分责任转移给了来电者;她必须提供一个可以传递给.reduce的iterate,这样对.reduce的调用将生成一个包含她想要添加的聚合财产的对象。例如,我们使用以下表达式获得与之前相同的结果:

aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => ({
    Value: +memo.Value + +value.Value
}));

对于一个稍微复杂一点的迭代的示例,假设我们希望计算每个组的最大值而不是总和,并且我们希望添加一个列出组中出现的Task的所有值的Tasks属性。这里有一种方法可以做到这一点,使用上面最后一个版本的聚合(和_.union):

aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => ({
    Value: Math.max(memo.Value, value.Value),
    Tasks: _.union(memo.Tasks || [memo.Task], [value.Task])
}));

我们得到以下结果:

[
    { Phase: "Phase 1", Step: "Step 1", Value: 10, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 1", Step: "Step 2", Value: 20, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 2", Step: "Step 1", Value: 30, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 2", Step: "Step 2", Value: 40, Tasks: [ "Task 1", "Task 2" ] }
]

归功于@much2learn,他还发布了一个可以处理任意缩减函数的答案。我又写了几个SO答案,演示了如何通过组合多个Undercore函数来实现复杂的功能:

https://stackoverflow.com/a/64938636/1166087https://stackoverflow.com/a/64094738/1166087https://stackoverflow.com/a/63625129/1166087https://stackoverflow.com/a/63088916/1166087

_.groupBy([{tipo: 'A' },{tipo: 'A'}, {tipo: 'B'}], 'tipo');
>> Object {A: Array[2], B: Array[1]}

发件人:http://underscorejs.org/#groupBy

使用ES6 Map对象:

/***@描述*采用Array<V>和分组函数,*并返回由分组函数分组的数组的Map。**@param list V类型的数组。*@param keyGetter一个函数,将数组类型V作为输入,并返回类型K的值。*K通常是V的属性键。**@返回由分组函数分组的数组的映射。*///导出函数组By<K,V>(列表:Array<V>,keyGetter:(输入:V)=>K):Map<K,Array<V>>{//const-map=new map<K,Array<V>>();函数groupBy(list,keyGetter){const-map=new map();list.forEach((项)=>{const key=keyGetter(项);constcollection=map.get(key);if(!集合){map.set(键,[项]);}其他{collection.push(项);}});回归图;}//示例用法常量宠物=[{type:“Dog”,name:“Spot”},{类型:“猫”,名称:“老虎”},{类型:“狗”,名称:“路虎”},{类型:“猫”,名称:“利奥”}];const grouped=groupBy(宠物,宠物=>宠物类型);console.log(分组.get(“Dog”));//->〔{类型:“狗”,名称:“斑点”},{类型“狗”、名称:“漫游者”}〕console.log(分组.get(“Cat”));//->〔{类型:“猫”,名称:“老虎”},{类型“猫”、名称:“狮子座”}〕const奇数=符号();const even=符号();常量=[1,2,3,4,5,6,7];const oddEven=groupBy(数字,x=>(x%2===1?奇数:偶数));console.log(oddEven.get(奇数));//->[1,3,5,7]console.log(oddEven.get(偶数));//->[2,4,6]

关于地图:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map

这是一个基于TS的功能,不是性能最好的,但很容易阅读和理解!

function groupBy<T>(array: T[], key: string): Record<string, T[]> {
const groupedObject = {}
for (const item of array) {
  const value = item[key]
    if (groupedObject[value] === undefined) {
  groupedObject[value] = []
  }
  groupedObject[value].push(item)
}
  return groupedObject
}

我们以->

const data = [
{ Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
{ Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" },
{ Phase: "Phase 1", Step: "Step 2", Task: "Task 1", Value: "15" },
{ Phase: "Phase 1", Step: "Step 2", Task: "Task 2", Value: "20" },
];
console.log(groupBy(data, 'Step'))
{
'Step 1': [
    {
      Phase: 'Phase 1',
      Step: 'Step 1',
      Task: 'Task 1',
      Value: '5'
    },
    {
      Phase: 'Phase 1',
      Step: 'Step 1',
      Task: 'Task 2',
      Value: '10'
    }
  ],
  'Step 2': [
    {
      Phase: 'Phase 1',
      Step: 'Step 2',
      Task: 'Task 1',
      Value: '15'
    },
    {
      Phase: 'Phase 1',
      Step: 'Step 2',
      Task: 'Task 2',
      Value: '20'
    }
  ]
}