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

例如,给定此对象数组:

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


当前回答

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

其他回答

我从underscore.js fiddler那里借用了这个方法

window.helpers=(function (){
    var lookupIterator = function(value) {
        if (value == null){
            return function(value) {
                return value;
            };
        }
        if (typeof value === 'function'){
                return value;
        }
        return function(obj) {
            return obj[value];
        };
    },
    each = function(obj, iterator, context) {
        var breaker = {};
        if (obj == null) return obj;
        if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) {
            obj.forEach(iterator, context);
        } else if (obj.length === +obj.length) {
            for (var i = 0, length = obj.length; i < length; i++) {
                if (iterator.call(context, obj[i], i, obj) === breaker) return;
            }
        } else {
            var keys = []
            for (var key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) keys.push(key)
            for (var i = 0, length = keys.length; i < length; i++) {
                if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
            }
        }
        return obj;
    },
    // An internal function used for aggregate "group by" operations.
    group = function(behavior) {
        return function(obj, iterator, context) {
            var result = {};
            iterator = lookupIterator(iterator);
            each(obj, function(value, index) {
                var key = iterator.call(context, value, index, obj);
                behavior(result, key, value);
            });
            return result;
        };
    };

    return {
      groupBy : group(function(result, key, value) {
        Object.prototype.hasOwnProperty.call(result, key) ? result[key].push(value) :              result[key] = [value];
        })
    };
})();

var arr=[{a:1,b:2},{a:1,b:3},{a:1,b:1},{a:1,b:2},{a:1,b:3}];
 console.dir(helpers.groupBy(arr,"b"));
 console.dir(helpers.groupBy(arr,function (el){
   return el.b>2;
 }));

ES6基于reduce的版本,支持iteratee函数。

如果未提供iteratee函数,则工作正常:

const data=〔{id:1,得分:2},{id:1,得分:3},{id:2,得分:2},{id:2,得分:4}〕常量组=(arr,k)=>arr.reduce((r,c)=>(r[c[k]]=[…r[c[k]]||[],c],r),{});常量组By=(arr,k,fn=()=>真)=>arr.reduce((r,c)=>(fn(c[k])?r[c[k]]=[…r[c[k]]| |[],c]:null,r),{});console.log(group(data,'id'))//通过`reduce分组`console.log(groupBy(data,'id'))//如果省略了“fn”,则结果相同console.log(groupBy(data,'score',x=>x>2))//使用iteratee分组

关于OP问题:

const data=〔{阶段:“阶段1”,步骤:“步骤1”,任务:“任务1”,值:“5”},{阶段“阶段1“,步骤:”步骤1“,任务:”任务2“,值:”10“},{阶段:”阶段1“、步骤:”阶段2“,任务1“,值“15”}、{阶段”阶段1”、步骤:“阶段2”、任务:”“任务2”、值:”20“}、{阶段“2”,步骤“步骤:”“步骤1“、任务:“1”、值“25”},{阶段:“阶段2”,步骤:“步骤1”,任务:“任务2”,值:“30”},{阶段:“阶段2”,步骤:“步骤2”,任务:“任务1”,值:”35“},{阶段:”阶段2“,步骤:”步骤2“,任务:”任务2“,值::”40“}]常量组By=(arr,k)=>arr.reduce((r,c)=>(r[c[k]]=[…r[c[k]]||[],c],r),{});常量组With=(arr,k,fn=()=>真)=>arr.reduce((r,c)=>(fn(c[k])?r[c[k]]=[…r[c[k]]| |[],c]:null,r),{});console.log(groupBy(数据,'Phase'))console.log(groupWith(data,'Value',x=>x>30))//按`Value`>30分组

另一个ES6版本,它反转分组,将值用作键,将键用作分组值:

常量数据=[{A:“1”},{B:“10”}、{C:“10”}]常量组键=arr=>arr.reduce((r,c)=>(Object.keys(c).map(x=>r[c[x]]=[…r[c[x]]||[],x]),r),{});console.log(groupKeys(数据))

注意:函数以简短的形式(一行)发布,目的是为了简洁,并仅表达想法。您可以展开它们并添加其他错误检查等。

在Joseph Nields的回答之后,有一个polyfill用于将对象分组https://github.com/padcom/array-prototype-functions#arrayprototypegroupbyfieldormapper.因此,您可能希望使用现有的内容,而不是一次又一次地编写这些内容。

let groupbyKeys = function(arr, ...keys) {
  let keysFieldName = keys.join();
  return arr.map(ele => {
    let keysField = {};
    keysField[keysFieldName] = keys.reduce((keyValue, key) => {
      return keyValue + ele[key]
    }, "");
    return Object.assign({}, ele, keysField);
  }).reduce((groups, ele) => {
    (groups[ele[keysFieldName]] = groups[ele[keysFieldName]] || [])
      .push([ele].map(e => {
        if (keys.length > 1) {
          delete e[keysFieldName];
        }
        return e;
    })[0]);
    return groups;
  }, {});
};

console.log(groupbyKeys(array, 'Phase'));
console.log(groupbyKeys(array, 'Phase', 'Step'));
console.log(groupbyKeys(array, 'Phase', 'Step', 'Task'));

我对公认的答案进行了扩展,包括按多个财产分组,然后再加上,使其完全起作用,没有变异。观看演示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;
};