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

例如,给定此对象数组:

[ 
    { 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,然后遍历生成的对象,自己计算总数?


当前回答

您可以在数组上使用forEach并构造一组新的项。以下是如何使用FlowType注释实现这一点

// @flow

export class Group<T> {
  tag: number
  items: Array<T>

  constructor() {
    this.items = []
  }
}

const groupBy = (items: Array<T>, map: (T) => number) => {
  const groups = []

  let currentGroup = null

  items.forEach((item) => {
    const tag = map(item)

    if (currentGroup && currentGroup.tag === tag) {
      currentGroup.items.push(item)
    } else {
      const group = new Group<T>()
      group.tag = tag
      group.items.push(item)
      groups.push(group)

      currentGroup = group
    }
  })

  return groups
}

export default groupBy

玩笑测试可以是这样的

// @flow

import groupBy from './groupBy'

test('groupBy', () => {
  const items = [
    { name: 'January', month: 0 },
    { name: 'February', month: 1 },
    { name: 'February 2', month: 1 }
  ]

  const groups = groupBy(items, (item) => {
    return item.month
  })

  expect(groups.length).toBe(2)
  expect(groups[1].items[1].name).toBe('February 2')
})

其他回答

让我们在重用已经编写的代码(即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

我已经改进了答案。此函数获取组字段数组并返回分组对象,该对象的键也是组字段的对象。

function(xs, groupFields) {
        groupFields = [].concat(groupFields);
        return xs.reduce(function(rv, x) {
            let groupKey = groupFields.reduce((keyObject, field) => {
                keyObject[field] = x[field];
                return keyObject;
            }, {});
            (rv[JSON.stringify(groupKey)] = rv[JSON.stringify(groupKey)] || []).push(x);
            return rv;
        }, {});
    }



let x = [
{
    "id":1,
    "multimedia":false,
    "language":["tr"]
},
{
    "id":2,
    "multimedia":false,
    "language":["fr"]
},
{
    "id":3,
    "multimedia":true,
    "language":["tr"]
},
{
    "id":4,
    "multimedia":false,
    "language":[]
},
{
    "id":5,
    "multimedia":false,
    "language":["tr"]
},
{
    "id":6,
    "multimedia":false,
    "language":["tr"]
},
{
    "id":7,
    "multimedia":false,
    "language":["tr","fr"]
}
]

groupBy(x, ['multimedia','language'])

//{
//{"multimedia":false,"language":["tr"]}: Array(3), 
//{"multimedia":false,"language":["fr"]}: Array(1), 
//{"multimedia":true,"language":["tr"]}: Array(1), 
//{"multimedia":false,"language":[]}: Array(1), 
//{"multimedia":false,"language":["tr","fr"]}: Array(1)
//}

这是一个基于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'
    }
  ]
}

我想建议一下我的方法。首先,分开分组和聚合。让我们声明原型“groupby”函数。它需要另一个函数为要分组的每个数组元素生成“哈希”字符串。

Array.prototype.groupBy = function(hash){
  var _hash = hash ? hash : function(o){return o;};

  var _map = {};
  var put = function(map, key, value){
    if (!map[_hash(key)]) {
        map[_hash(key)] = {};
        map[_hash(key)].group = [];
        map[_hash(key)].key = key;

    }
    map[_hash(key)].group.push(value); 
  }

  this.map(function(obj){
    put(_map, obj, obj);
  });

  return Object.keys(_map).map(function(key){
    return {key: _map[key].key, group: _map[key].group};
  });
}

分组完成后,您可以根据需要聚合数据

data.groupBy(function(o){return JSON.stringify({a: o.Phase, b: o.Step});})
    /* aggreagating */
    .map(function(el){ 
         var sum = el.group.reduce(
           function(l,c){
             return l + parseInt(c.Value);
           },
           0
         );
         el.key.Value = sum; 
         return el.key;
    });

一般来说,它是有效的。我已经在chrome控制台中测试了这段代码。并随时改进和发现错误;)

让我们生成一个通用的Array.protocol.groupBy()工具。为了多样化,让我们在递归方法上使用ES6 fancyty扩展运算符进行Haskell式模式匹配。同样,让我们让Array.prototype.groupBy()接受一个回调,该回调将项(e)、索引(i)和应用的数组(a)作为参数。

Array.prototype.groupBy=函数(cb){返回函数迭代([x,…xs],i=0,r=[[],[]]){cb(x,i,[x,…xs])?(r[0].推(x),r):(r[1].推(x),r);是否返回xs.length?迭代(xs,++i,r):r;}(本);};var arr=[0,1,2,3,4,5,6,7,8,9],res=arr.groupBy(e=>e<5);console.log(res);