在一个对象数组中进行分组的最有效方法

915

什么是在数组中对对象进行groupby的最有效方法?

例如,给定以下对象数组:

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

我正在使用表格展示这些信息。我想按不同的方法进行分组,但是我想对值进行求和。

我正在使用Underscore.js的groupby函数,这很有帮助,但并不能完全满足我的需求,因为我不想让它们“分开”,而是更像SQL中的group by 方法将它们“合并”起来。

我想要的是能够对特定值进行汇总(如果被请求的话)。

所以如果我按Phase 进行分组,我希望收到:

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

如果我将 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 }
]

是否有适用于此的有用脚本,或者我应该坚持使用Underscore.js,然后循环遍历结果对象自己进行总计?


虽然 _.groupBy 本身不能完成工作,但它可以与其他 Underscore 函数结合使用来完成所需的操作,无需手动循环。请参考此回答: https://dev59.com/uGYq5IYBdhLWcg3wfgnd#66112210。 - Julian
更易读的答案版本:function groupBy(data, key){ return data.reduce( (acc, cur) => { acc[cur[key]] = acc[cur[key]] || []; // 如果键是新的,则将其值初始化为数组,否则保留其自己的数组值 acc[cur[key]].push(cur); return acc; } , []) } - aderchox
62个回答

0

如果你需要进行多重分组:


    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"
            }
        ]
    }
};


0
data = [{id:1, name:'BMW'}, {id:2, name:'AN'}, {id:3, name:'BMW'}, {id:1, name:'NNN'}]
key = 'id'//try by id or name
data.reduce((previous, current)=>{
    previous[current[key]] && previous[current[key]].length != 0 ? previous[current[key]].push(current) : previous[current[key]] = new Array(current)
    return previous;
}, {})

0

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

0
var newArr = data.reduce((acc, cur) => {
    const existType = acc.find(a => a.Phase === cur.Phase);
    if (existType) {
        existType.Value += +cur.Value;
        return acc;
    }

    acc.push({
        Phase: cur.Phase,
        Value: +cur.Value
    });
    return acc;
}, []);

你的解决方案是第一个展示了隐含要求的(a)分组阶段和(b)按值求和的聚合阶段。你能否解释一下使用的“语言概念”(reduce,lambdas等)以分享一个知识丰富的方法,而不仅仅是一个可复制的片段? - hc_dev

0
let f_a_a_o__grouped_by_s_prop = function(
    a_o,
    s_prop
){
    let a_v = [];
    return a_o.map(
        function(o){
            let v = o[s_prop];
            if(!a_v.includes(v)){
                a_v.push(v)
                return a_o.filter(
                    o=>o[s_prop] == v
                )
            }
            return false
        }
    ).filter(v=>v);
    //test f_a_a_o__grouped_by_s_prop([{n:2},{n:2},{n:2},{n:3},{n:5},{n:5}], 'n').length == 3
}

0

你可以在数组上使用 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

一个 Jest 测试可以像这样

// @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')
})

0

使用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 }
    ]
*/

0
以下函数允许按任意字段进行groupBy(并求和值-OP所需)。在解决方案中,我们定义了cmp函数,根据分组的fields比较两个对象。在let w=...中,我们创建了子集对象x字段的副本。在y[sumBy]=+y[sumBy]+(+x[sumBy])中,我们使用'+'将字符串转换为数字。
function groupBy(data, fields, sumBy='Value') {
  let r=[], cmp= (x,y) => fields.reduce((a,b)=> a && x[b]==y[b], true);
  data.forEach(x=> {
    let y=r.find(z=>cmp(x,z));
    let w= [...fields,sumBy].reduce((a,b) => (a[b]=x[b],a), {})
    y ? y[sumBy]=+y[sumBy]+(+x[sumBy]) : r.push(w);
  });
  return r;
}

const d = [ 
    { 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" }
];



function groupBy(data, fields, sumBy='Value') {
  let r=[], cmp= (x,y) => fields.reduce((a,b)=> a && x[b]==y[b], true);
  data.forEach(x=> {
    let y=r.find(z=>cmp(x,z));
    let w= [...fields,sumBy].reduce((a,b) => (a[b]=x[b],a), {})
    y ? y[sumBy]=+y[sumBy]+(+x[sumBy]) : r.push(w);
  });
  return r;
}


// TEST
let p=(t,o) => console.log(t, JSON.stringify(o));
console.log('GROUP BY:');

p('Phase', groupBy(d,['Phase']) );
p('Step', groupBy(d,['Step']) );
p('Phase-Step', groupBy(d,['Phase', 'Step']) );
p('Phase-Task', groupBy(d,['Phase', 'Task']) );
p('Step-Task', groupBy(d,['Step', 'Task']) );
p('Phase-Step-Task', groupBy(d,['Phase','Step', 'Task']) );


0

如果你使用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)
    }
  })

0
var data = [{"name":1},{"name":1},{"name":2},{"name":2},{"name":3}]

groupBy(data, 'name','age','gender')


function groupBy(arr, ...keys) {
    const result = {};
    arr.forEach(obj => {
      let current = result;
      keys.forEach(key => {
        const value = obj[key];
        current[value] = current[value] || {};
        current = current[value];
      });
      current.values = current.values || [];
      current.values.push(obj);
    });
    return result;
  }

//result: 1,2,3

这段代码定义了一个名为"data"的对象数组,其中包含一些属性。然后,它定义了一个名为"groupBy"的函数,该函数使用剩余参数语法,接受一个数组"arr"和动态数量的字符串参数"keys"。

在函数内部,定义了一个新的空对象"result",用于存储groupBy函数的最终结果。然后,函数遍历"arr"数组中的每个对象,并创建一个名为"current"的新变量,并将其设置为"result"对象。

对于"keys"数组中的每个键,函数从当前对象中提取相应属性的值,并将其设置为"current"对象的新属性。如果该属性不存在,则创建一个空对象。

最后,函数将当前对象添加到"current"对象的"values"数组中。

在函数结束时,返回包含分组数据的"result"对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接