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

例如,给定此对象数组:

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


当前回答

如果希望避免使用外部库,可以简洁地实现groupBy()的普通版本,如下所示:

var groupBy=函数(xs,key){返回xs.reduce(函数(rv,x){(rv[x[key]]=rv[x[键]]| |[]).push(x);返回rv;}, {});};console.log(groupBy(['one','two','three'],'length'));//=>{“3”:[“1”,“2”],“5”:[”3“]}

其他回答

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

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

使用ES6 Map对象:

/***@描述*采用Array<V>和分组函数,*并返回由分组函数分组的数组的Map。**@param list V类型的数组。*@param keyGetter一个函数,将数组类型V作为输入,并返回类型K的值。*K通常是V的属性键。**@返回由分组函数分组的数组的映射。*///导出函数组By<K,V>(列表:Array<V>,keyGetter:(输入:V)=>K):Map<K,Array<V>>{//const-map=new map<K,Array<V>>();函数groupBy(list,keyGetter){const-map=new map();list.forEach((项)=>{const key=keyGetter(项);constcollection=map.get(key);if(!集合){map.set(键,[项]);}其他{collection.push(项);}});回归图;}//示例用法常量宠物=[{type:“Dog”,name:“Spot”},{类型:“猫”,名称:“老虎”},{类型:“狗”,名称:“路虎”},{类型:“猫”,名称:“利奥”}];const grouped=groupBy(宠物,宠物=>宠物类型);console.log(分组.get(“Dog”));//->〔{类型:“狗”,名称:“斑点”},{类型“狗”、名称:“漫游者”}〕console.log(分组.get(“Cat”));//->〔{类型:“猫”,名称:“老虎”},{类型“猫”、名称:“狮子座”}〕const奇数=符号();const even=符号();常量=[1,2,3,4,5,6,7];const oddEven=groupBy(数字,x=>(x%2===1?奇数:偶数));console.log(oddEven.get(奇数));//->[1,3,5,7]console.log(oddEven.get(偶数));//->[2,4,6]

关于地图:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map

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

常量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;}

虽然linq的回答很有趣,但它也很重。我的方法有些不同:

var DataGrouper = (function() {
    var has = function(obj, target) {
        return _.any(obj, function(value) {
            return _.isEqual(value, target);
        });
    };

    var keys = function(data, names) {
        return _.reduce(data, function(memo, item) {
            var key = _.pick(item, names);
            if (!has(memo, key)) {
                memo.push(key);
            }
            return memo;
        }, []);
    };

    var group = function(data, names) {
        var stems = keys(data, names);
        return _.map(stems, function(stem) {
            return {
                key: stem,
                vals:_.map(_.where(data, stem), function(item) {
                    return _.omit(item, names);
                })
            };
        });
    };

    group.register = function(name, converter) {
        return group[name] = function(data, names) {
            return _.map(group(data, names), converter);
        };
    };

    return group;
}());

DataGrouper.register("sum", function(item) {
    return _.extend({}, item.key, {Value: _.reduce(item.vals, function(memo, node) {
        return memo + Number(node.Value);
    }, 0)});
});

您可以在JSBin上看到它的作用。

我在Undercore中没有看到任何东西可以做已经做的事情,尽管我可能会错过它。它与_.incontains非常相似,但使用_.isEqual而不是==进行比较。除此之外,其余部分都是针对具体问题的,尽管只是试图通用。

现在DataGrouper.sum(data,[“Phase”])返回

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

DataGrouper.sum(data,[“Phase”,“Step”])返回

[
    {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}
]

但和在这里只是一个势函数。您可以根据需要注册其他人:

DataGrouper.register("max", function(item) {
    return _.extend({}, item.key, {Max: _.reduce(item.vals, function(memo, node) {
        return Math.max(memo, Number(node.Value));
    }, Number.NEGATIVE_INFINITY)});
});

现在DataGrouper.max(data,[“Phase”,“Step”])将返回

[
    {Phase: "Phase 1", Step: "Step 1", Max: 10},
    {Phase: "Phase 1", Step: "Step 2", Max: 20},
    {Phase: "Phase 2", Step: "Step 1", Max: 30},
    {Phase: "Phase 2", Step: "Step 2", Max: 40}
]

或者如果您注册了:

DataGrouper.register("tasks", function(item) {
    return _.extend({}, item.key, {Tasks: _.map(item.vals, function(item) {
      return item.Task + " (" + item.Value + ")";
    }).join(", ")});
});

然后调用DataGrouper.tasks(data,[“Phase”,“Step”])将得到

[
    {Phase: "Phase 1", Step: "Step 1", Tasks: "Task 1 (5), Task 2 (10)"},
    {Phase: "Phase 1", Step: "Step 2", Tasks: "Task 1 (15), Task 2 (20)"},
    {Phase: "Phase 2", Step: "Step 1", Tasks: "Task 1 (25), Task 2 (30)"},
    {Phase: "Phase 2", Step: "Step 2", Tasks: "Task 1 (35), Task 2 (40)"}
]

DataGrouper本身是一个函数。可以用数据和要分组的财产列表来调用它。它返回一个数组,该数组的元素是具有两个财产的对象:key是分组财产的集合,vals是包含不在key中的其余财产的对象数组。例如,DataGrouper(data,[“Phase”,“Step”])将产生:

[
    {
        "key": {Phase: "Phase 1", Step: "Step 1"},
        "vals": [
            {Task: "Task 1", Value: "5"},
            {Task: "Task 2", Value: "10"}
        ]
    },
    {
        "key": {Phase: "Phase 1", Step: "Step 2"},
        "vals": [
            {Task: "Task 1", Value: "15"}, 
            {Task: "Task 2", Value: "20"}
        ]
    },
    {
        "key": {Phase: "Phase 2", Step: "Step 1"},
        "vals": [
            {Task: "Task 1", Value: "25"},
            {Task: "Task 2", Value: "30"}
        ]
    },
    {
        "key": {Phase: "Phase 2", Step: "Step 2"},
        "vals": [
            {Task: "Task 1", Value: "35"}, 
            {Task: "Task 2", Value: "40"}
        ]
    }
]

DataGrouper.register接受一个函数并创建一个新函数,该函数接受初始数据和分组依据的财产。然后,这个新函数采用上述输出格式,并依次对每个输出格式运行函数,返回一个新数组。生成的函数根据您提供的名称存储为DataGrouper的属性,如果您只需要本地引用,也会返回。

这是很多解释。我希望代码相当简单!

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