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

例如,给定此对象数组:

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


当前回答

如果您需要通过以下方式进行多组:


    const populate = (entireObj, keys, item) => {
    let keysClone = [...keys],
        currentKey = keysClone.shift();

    if (keysClone.length > 0) {
        entireObj[item[currentKey]] = entireObj[item[currentKey]] || {}
        populate(entireObj[item[currentKey]], keysClone, item);
    } else {
        (entireObj[item[currentKey]] = entireObj[item[currentKey]] || []).push(item);
    }
}

export const groupBy = (list, key) => {
    return list.reduce(function (rv, x) {

        if (typeof key === 'string') (rv[x[key]] = rv[x[key]] || []).push(x);

        if (typeof key === 'object' && key.length) populate(rv, key, x);

        return rv;

    }, {});
}

const myPets = [
    {name: 'yaya', type: 'cat', color: 'gray'},
    {name: 'bingbang', type: 'cat', color: 'sliver'},
    {name: 'junior-bingbang', type: 'cat', color: 'sliver'},
    {name: 'jindou', type: 'cat', color: 'golden'},
    {name: 'dahuzi', type: 'dog', color: 'brown'},
];

// run 
groupBy(myPets, ['type', 'color']));

// you will get object like: 

const afterGroupBy = {
    "cat": {
        "gray": [
            {
                "name": "yaya",
                "type": "cat",
                "color": "gray"
            }
        ],
        "sliver": [
            {
                "name": "bingbang",
                "type": "cat",
                "color": "sliver"
            },
            {
                "name": "junior-bingbang",
                "type": "cat",
                "color": "sliver"
            }
        ],
        "golden": [
            {
                "name": "jindou",
                "type": "cat",
                "color": "golden"
            }
        ]
    },
    "dog": {
        "brown": [
            {
                "name": "dahuzi",
                "type": "dog",
                "color": "brown"
            }
        ]
    }
};

其他回答

发帖是因为即使这个问题已经7年了,我仍然没有看到一个符合原始标准的答案:

我不希望它们“拆分”,而是“合并”,更像SQL组方法

我最初发表这篇文章是因为我想找到一种方法来减少对象数组(例如,当您从csv中读取时创建的数据结构),并通过给定索引聚合以生成相同的数据结构。我正在寻找的返回值是另一个对象数组,而不是我在这里看到的嵌套对象或映射。

下面的函数获取一个数据集(对象数组)、一个索引列表(数组)和一个reducer函数,并将reducer功能应用于索引的结果作为一个对象数组返回。

function agg(data, indices, reducer) {

  // helper to create unique index as an array
  function getUniqueIndexHash(row, indices) {
    return indices.reduce((acc, curr) => acc + row[curr], "");
  }

  // reduce data to single object, whose values will be each of the new rows
  // structure is an object whose values are arrays
  // [{}] -> {{}}
  // no operation performed, simply grouping
  let groupedObj = data.reduce((acc, curr) => {
    let currIndex = getUniqueIndexHash(curr, indices);

    // if key does not exist, create array with current row
    if (!Object.keys(acc).includes(currIndex)) {
      acc = {...acc, [currIndex]: [curr]}
    // otherwise, extend the array at currIndex
    } else {
      acc = {...acc, [currIndex]: acc[currIndex].concat(curr)};
    }

    return acc;
  }, {})

  // reduce the array into a single object by applying the reducer
  let reduced = Object.values(groupedObj).map(arr => {
    // for each sub-array, reduce into single object using the reducer function
    let reduceValues = arr.reduce(reducer, {});

    // reducer returns simply the aggregates - add in the indices here
    // each of the objects in "arr" has the same indices, so we take the first
    let indexObj = indices.reduce((acc, curr) => {
      acc = {...acc, [curr]: arr[0][curr]};
      return acc;
    }, {});

    reduceValues = {...indexObj, ...reduceValues};


    return reduceValues;
  });


  return reduced;
}

我将创建一个返回count(*)和sum(Value)的reducer:

reducer = (acc, curr) => {
  acc.count = 1 + (acc.count || 0);
  acc.value = +curr.Value + (acc.value|| 0);
  return acc;
}

最后,使用我们的reducer将agg函数应用于原始数据集会生成一个应用了适当聚合的对象数组:

agg(tasks, ["Phase"], reducer);
// yields:
Array(2) [
  0: Object {Phase: "Phase 1", count: 4, value: 50}
  1: Object {Phase: "Phase 2", count: 4, value: 130}
]

agg(tasks, ["Phase", "Step"], reducer);
// yields:
Array(4) [
  0: Object {Phase: "Phase 1", Step: "Step 1", count: 2, value: 15}
  1: Object {Phase: "Phase 1", Step: "Step 2", count: 2, value: 35}
  2: Object {Phase: "Phase 2", Step: "Step 1", count: 2, value: 55}
  3: Object {Phase: "Phase 2", Step: "Step 2", count: 2, value: 75}
]

正确答案——只是浅分组。理解减少是很好的。问题还提供了额外总计计算的问题。

这里是一个由一些字段组成的对象数组的REAL GROUP BY,该字段具有1)计算的键名称和2)通过提供所需键的列表来实现分组级联的完整解决方案并将其唯一值转换为根键,如SQL GROUP BY做

常量inputArray=[{阶段:“阶段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”}];var outObject=inputArray.reduce(函数(a,e){//GROUP BY估计密钥(estKey)可能只是一个普通密钥//a——累加器结果对象//e——依次检查的元素,即在此位置测试的元素//可以计算新的分组名称,但必须基于实字段的实际值设estKey=(e['Phase']);(a[estKey]?a[estKey]:(a[est Key]=null | |[])).push(e);返回a;}, {});console.log(outObject);

使用estKey--您可以按多个字段分组,添加其他聚合、计算或其他处理。

您还可以递归地分组数据。例如,最初按阶段分组,然后按步骤字段分组等等脂肪休息数据。

常量输入数组=[{阶段:“阶段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”}];/***获取obj WITH属性的SHALLOW副本的小助手*/const rmProp=(obj,prop)=>(({[prop]:_,…rest})=>rest)(obj))/***按关键字分组数组。结果数组的根键是value*指定密钥的。**@param{Array}src源数组*@param{String}key分组依据的by key*@return{Object}将分组对象作为值的对象*/const grpBy=(src,key)=>src.reduce((a,e)=>((a[e[key]]=a[e[键]]| |[]).push(rmProp(e,键)),a), {});/***如果对象数组仅由具有单个值的对象组成,则折叠该数组。*将其替换为剩余值。*/const blowObj=obj=>Array.isArray(obj)&&obj.length==1&&Object.values(obj[0]).length==1?对象.值(obj[0])[0]:obj;/***带有键列表的递归分组`keyList`可以是数组*或UNIQUE值将使用逗号分隔的键名列表*成为结果对象的关键点。*/const grpByReal=函数(src,keyList){const[key,…rest]=Array.isArray(keyList)?keyList:String(keyList).trim().split(/\s*,\s*/);常量res=键?grpBy(src,key):[…src];if(剩余长度){for(常量k,单位:res){res[k]=grpByReal(res[k],其余)}}其他{for(常量k,单位:res){res[k]=blowObj(res[k])}}回报率;}console.log(JSON.stringify(grpByReal(inputArray,'阶段,步骤,任务'),null,2));

我从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;
 }));

此解决方案采用任意函数(而不是键),因此比上述解决方案更灵活,并允许箭头函数,这与LINQ中使用的lambda表达式类似:

Array.prototype.groupBy = function (funcProp) {
    return this.reduce(function (acc, val) {
        (acc[funcProp(val)] = acc[funcProp(val)] || []).push(val);
        return acc;
    }, {});
};

注意:是否要扩展Array的原型取决于您。

大多数浏览器支持的示例:

[{a:1,b:"b"},{a:1,c:"c"},{a:2,d:"d"}].groupBy(function(c){return c.a;})

使用箭头函数(ES6)的示例:

[{a:1,b:"b"},{a:1,c:"c"},{a:2,d:"d"}].groupBy(c=>c.a)

以上两个示例都返回:

{
  "1": [{"a": 1, "b": "b"}, {"a": 1, "c": "c"}],
  "2": [{"a": 2, "d": "d"}]
}

使用linq.js可能更容易做到这一点,它是linq在JavaScript(DEMO)中的真正实现:

var linq = Enumerable.From(data);
var result =
    linq.GroupBy(function(x){ return x.Phase; })
        .Select(function(x){
          return {
            Phase: x.Key(),
            Value: x.Sum(function(y){ return y.Value|0; })
          };
        }).ToArray();

结果:

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

或者,更简单地使用基于字符串的选择器(DEMO):

linq.GroupBy("$.Phase", "",
    "k,e => { Phase:k, Value:e.Sum('$.Value|0') }").ToArray();