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

例如,给定此对象数组:

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


当前回答

使用lodash库很简单

let temp = []
  _.map(yourCollectionData, (row) => {
    let index = _.findIndex(temp, { 'Phase': row.Phase })
    if (index > -1) {
      temp[index].Value += row.Value 
    } else {
      temp.push(row)
    }
  })

其他回答

基于@Ceasar Bautista的原始想法,我修改了代码并使用typescript创建了一个groupBy函数。

static groupBy(data: any[], comparator: (v1: any, v2: any) => boolean, onDublicate: (uniqueRow: any, dublicateRow: any) => void) {
    return data.reduce(function (reducedRows, currentlyReducedRow) {
      let processedRow = reducedRows.find(searchedRow => comparator(searchedRow, currentlyReducedRow));

      if (processedRow) {
        // currentlyReducedRow is a dublicateRow when processedRow is not null.
        onDublicate(processedRow, currentlyReducedRow)
      } else {
        // currentlyReducedRow is unique and must be pushed in the reducedRows collection.
        reducedRows.push(currentlyReducedRow);
      }

      return reducedRows;
    }, []);
  };

此函数接受一个回调(比较器)和一个第二个回调(onDuplicate),该回调比较行并查找副本。

用法示例:

data = [
    { name: 'a', value: 10 },
    { name: 'a', value: 11 },
    { name: 'a', value: 12 },
    { name: 'b', value: 20 },
    { name: 'b', value: 1 }
  ]

  private static demoComparator = (v1: any, v2: any) => {
    return v1['name'] === v2['name'];
  }

  private static demoOnDublicate = (uniqueRow, dublicateRow) => {
    uniqueRow['value'] += dublicateRow['value'];    
  };

使命感

groupBy(data, demoComparator, demoOnDublicate) 

将执行计算值和的分组。

{name: "a", value: 33}
{name: "b", value: 21}

我们可以根据项目的需要创建任意多个回调函数,并根据需要聚合这些值。在一个例子中,我需要合并两个数组,而不是求和数据。

我对公认的答案进行了扩展,包括按多个财产分组,然后再加上,使其完全起作用,没有变异。观看演示https://stackblitz.com/edit/typescript-ezydzv

export interface Group {
  key: any;
  items: any[];
}

export interface GroupBy {
  keys: string[];
  thenby?: GroupBy;
}

export const groupBy = (array: any[], grouping: GroupBy): Group[] => {
  const keys = grouping.keys;
  const groups = array.reduce((groups, item) => {
    const group = groups.find(g => keys.every(key => item[key] === g.key[key]));
    const data = Object.getOwnPropertyNames(item)
      .filter(prop => !keys.find(key => key === prop))
      .reduce((o, key) => ({ ...o, [key]: item[key] }), {});
    return group
      ? groups.map(g => (g === group ? { ...g, items: [...g.items, data] } : g))
      : [
          ...groups,
          {
            key: keys.reduce((o, key) => ({ ...o, [key]: item[key] }), {}),
            items: [data]
          }
        ];
  }, []);
  return grouping.thenby ? groups.map(g => ({ ...g, items: groupBy(g.items, grouping.thenby) })) : groups;
};

这里有一个使用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[]>);

无突变:

const groupBy = (xs, key) => xs.reduce((acc, x) => Object.assign({}, acc, {
  [x[key]]: (acc[x[key]] || []).concat(x)
}), {})

console.log(groupBy(['one', 'two', 'three'], 'length'));
// => {3: ["one", "two"], 5: ["three"]}

一种新的方法,有一个用于分组的对象和另外两个用于创建键的函数,并获得一个包含所需分组项的对象和另一个用于添加值的键。

常量groupBy=(数组,组,valueKey)=>{常量getKey=o=>groups.map(k=>o[k]).join('|'),getObject=o=>Object.fromEntries([…groups.map(k=>[k,o[k]]),[valueKey,0]);groups=[].contat(组);return Object.values(array.reduce((r,o)=>{(r[getKey(o)]??=getObject(o))[valueKey]+=+o[valueKey];返回r;}, {}));},data=[{阶段:“阶段1”,步骤:“步骤1”,任务:“任务1”,值:“5”},{阶段“阶段1“,步骤:”步骤1“,任务:”任务2“,值:”10“},{阶段:”阶段1“、步骤:”第2“、任务:”第1“、值:”15“}、{阶段”阶段1”、步骤:“第2”、任务:“2”、值:“20”}、{阶段“第2阶段”,步骤“步骤:”第一步“,任务“任务:”,值“25”}阶段:“阶段2”,步骤:“步骤1”,任务:“任务2”,值:“30”},{阶段:“阶段2”,步骤:“步骤2”,任务:“任务1”,值:”35“},{阶段:”阶段2“,步骤:”步骤2“,任务:”任务2“,值::”40“}];console.log(groupBy(data,'Phase','Value'));console.log(groupBy(数据,['Phase','Step'],'Value'));.作为控制台包装{最大高度:100%!重要;顶部:0;}

旧方法:

虽然这个问题有一些答案,而且答案看起来有点过于复杂,但我建议使用香草Javascript进行分组,并使用嵌套的(如果需要)Map。

函数groupBy(数组,组,valueKey){var map=新地图;groups=[].contat(组);返回数组.reduce((r,o)=>{groups.reduce((m,k,i,{length})=>{var子;如果(m.has(o[k]))返回m.get(o[k]);如果(i+1==长度){child=对象.assign(…groups.map(k=>({[k]:o[k]})),{[valueKey]:0});r.push(儿童);}其他{child=新地图;}m.set(o[k],子);回归儿童;},map)[valueKey]+=+o[valueKey];返回r;}, [])};var data=[{阶段:“阶段1”,步骤:“步骤1”,任务:“任务1”,值:“5”},{阶段“阶段1“,步骤:”步骤1“,任务:”任务2“,值:”10“},{阶段:”阶段1“、步骤:”阶段2“,任务1“,值“15”}、{阶段”阶段1”、步骤:“阶段2”,任务2“、值:”20“}、{阶段2“、步骤1”、任务1“、任务1”、值:“25”},{阶段:“阶段2”,步骤:“步骤1”,任务:“任务2”,值:“30”},{阶段:“阶段2”,步骤:“步骤2”,任务:“任务1”,值:”35“},{阶段:”阶段2“,步骤:”步骤2“,任务:”任务2“,值::”40“}];console.log(groupBy(data,'Phase','Value'));console.log(groupBy(数据,['Phase','Step'],'Value'));.作为控制台包装{最大高度:100%!重要;顶部:0;}