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

例如,给定此对象数组:

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


当前回答

function groupBy(array, groupBy){
        return array.reduce((acc,curr,index,array) => {
           var  idx = curr[groupBy]; 
              if(!acc[idx]){
                    acc[idx] = array.filter(item => item[groupBy] === idx)
              } 
            return  acc; 

        },{})
    }

// call
groupBy(items,'Step')

其他回答

使用ES6:

const groupBy = (items, key) => items.reduce(
  (result, item) => ({
    ...result,
    [item[key]]: [
      ...(result[item[key]] || []),
      item,
    ],
  }), 
  {},
);

这里有一个ES6版本,不会在空成员上中断

function groupBy (arr, key) {
  return (arr || []).reduce((acc, x = {}) => ({
    ...acc,
    [x[key]]: [...acc[x[key]] || [], x]
  }), {})
}

这里有一个使用ES6的讨厌的、难以阅读的解决方案:

export default (arr, key) => 
  arr.reduce(
    (r, v, _, __, k = v[key]) => ((r[k] || (r[k] = [])).push(v), r),
    {}
  );

对于那些询问这是如何工作的人,这里有一个解释:

在这两个=>中,您可以获得免费回报Array.prototype.reduce函数最多包含4个参数。这就是为什么要添加第五个参数,这样我们就可以使用默认值在参数声明级别为组(k)创建一个廉价的变量声明。(是的,这是巫术)如果我们的当前组在上一次迭代中不存在,我们将创建一个新的空数组((r[k]||(r[k]=[]))。这将计算到最左边的表达式,换句话说,一个现有数组或一个空数组,这就是为什么在该表达式之后会立即推送,因为无论哪种方式都会得到一个数组。当有一个返回时,逗号运算符将丢弃最左边的值,返回该场景中经过调整的前一组。

更容易理解的版本是:

export default (array, key) => 
  array.reduce((previous, currentItem) => {
    const group = currentItem[key];
    if (!previous[group]) previous[group] = [];
    previous[group].push(currentItem);
    return previous;
  }, {});

编辑:

TS版本:

const groupBy = <T, K extends keyof any>(list: T[], getKey: (item: T) => K) =>
  list.reduce((previous, currentItem) => {
    const group = getKey(currentItem);
    if (!previous[group]) previous[group] = [];
    previous[group].push(currentItem);
    return previous;
  }, {} as Record<K, T[]>);

您可以在数组上使用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')
})

使用ES6的简单解决方案:

该方法有一个返回模型,可以比较n个财产。

const compareKey = (item, key, compareItem) => {
    return item[key] === compareItem[key]
}

const handleCountingRelatedItems = (listItems, modelCallback, compareKeyCallback) => {
    return listItems.reduce((previousValue, currentValue) => {
        if (Array.isArray(previousValue)) {
        const foundIndex = previousValue.findIndex(item => compareKeyCallback(item, currentValue))

        if (foundIndex > -1) {
            const count = previousValue[foundIndex].count + 1

            previousValue[foundIndex] = modelCallback(currentValue, count)

            return previousValue
        }

        return [...previousValue, modelCallback(currentValue, 1)]
        }

        if (compareKeyCallback(previousValue, currentValue)) {
        return [modelCallback(currentValue, 2)]
        }

        return [modelCallback(previousValue, 1), modelCallback(currentValue, 1)]
    })
}

const itemList = [
    { type: 'production', human_readable: 'Production' },
    { type: 'test', human_readable: 'Testing' },
    { type: 'production', human_readable: 'Production' }
]

const model = (currentParam, count) => ({
    label: currentParam.human_readable,
    type: currentParam.type,
    count
})

const compareParameter = (item, compareValue) => {
    const isTypeEqual = compareKey(item, 'type', compareValue)
    return isTypeEqual
}

const result = handleCountingRelatedItems(itemList, model, compareParameter)

 console.log('Result: \n', result)
/** Result: 
    [
        { label: 'Production', type: 'production', count: 2 },
        { label: 'Testing', type: 'testing', count: 1 }
    ]
*/