Most efficient method to groupby on an array of objects
在数组中对对象进行分组的最有效方法是什么?
例如,给定此对象数组:
1 2 3 4 5 6 7 8 9 10 | [ { 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
我正在寻找的是能够总计特定值(如果要求)。
所以如果我做了groupby
1 2 3 4 | [ { Phase:"Phase 1", Value: 50 }, { Phase:"Phase 2", Value: 130 } ] |
如果我做了分组
1 2 3 4 5 6 | [ { 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,然后循环结果对象自己做总计?
如果你想避免使用外部库,你可以简洁地实现一个
1 2 3 4 5 6 7 8 9 10 | var groupBy = function(xs, key) { return xs.reduce(function(rv, x) { (rv[x[key]] = rv[x[key]] || []).push(x); return rv; }, {}); }; console.log(groupBy(['one', 'two', 'three'], 'length')); // => {3: ["one","two"], 5: ["three"]} |
使用ES6 Map对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | function groupBy(list, keyGetter) { const map = new Map(); list.forEach((item) => { const key = keyGetter(item); const collection = map.get(key); if (!collection) { map.set(key, [item]); } else { collection.push(item); } }); return map; } // example usage const pets = [ {type:"Dog", name:"Spot"}, {type:"Cat", name:"Tiger"}, {type:"Dog", name:"Rover"}, {type:"Cat", name:"Leo"} ]; const grouped = groupBy(pets, pet => pet.type); console.log(grouped.get("Dog")); // -> [{type:"Dog", name:"Spot"}, {type:"Dog", name:"Rover"}] console.log(grouped.get("Cat")); // -> [{type:"Cat", name:"Tiger"}, {type:"Cat", name:"Leo"}] |
关于地图:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
与ES6:
1 2 3 4 5 6 7 8 9 10 | const groupBy = (items, key) => items.reduce( (result, item) => ({ ...result, [item[key]]: [ ...(result[item[key]] || []), item, ], }), {}, ); |
虽然linq的答案很有趣,但它的重量也很重。我的方法有些不同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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上看到它。
我没有在Underscore中看到任何
现在
1 2 3 4 | [ {Phase:"Phase 1", Value: 50}, {Phase:"Phase 2", Value: 130} ] |
并且
1 2 3 4 5 6 | [ {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} ] |
但
1 2 3 4 5 | 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)}); }); |
现在
1 2 3 4 5 6 | [ {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} ] |
或者如果你注册了这个:
1 2 3 4 5 | DataGrouper.register("tasks", function(item) { return _.extend({}, item.key, {Tasks: _.map(item.vals, function(item) { return item.Task +" (" + item.Value +")"; }).join(",")}); }); |
然后调用
1 2 3 4 5 6 | [ {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)"} ] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | [ { "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"} ] } ] |
那是很多解释。我希望代码相当简单!
使用
1 2 3 4 5 6 7 8 9 | 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(); |
结果:
1 2 3 4 | [ { Phase:"Phase 1", Value: 50 }, { Phase:"Phase 2", Value: 130 } ] |
或者,更简单地使用基于字符串的选择器(DEMO):
1 2 | linq.GroupBy("$.Phase","", "k,e => { Phase:k, Value:e.Sum('$.Value|0') }").ToArray(); |
我会检查lodash groupBy它似乎完全符合你的要求。它也非常轻巧,非常简单。
小提琴示例:https://jsfiddle.net/r7szvt5k/
如果你的数组名称是
1 2 3 4 5 6 7 8 | import groupBy from 'lodash/groupBy'; // if you still use require: // const groupBy = require('lodash/groupBy'); const a = groupBy(arr, function(n) { return n.Phase; }); // a is your array grouped by Phase attribute |
1 2 | _.groupBy([{tipo: 'A' },{tipo: 'A'}, {tipo: 'B'}], 'tipo'); >> Object {A: Array[2], B: Array[1]} |
来自:http://underscorejs.org/#groupBy
您可以从
1 2 3 4 | const groupedMap = initialArray.reduce( (entryMap, e) => entryMap.set(e.id, [...entryMap.get(e.id)||[], e]), new Map() ); |
与其他解决方案相比,这有一些优势:
-
它不需要任何库(不像例如
_.groupBy() ) -
你得到一个JavaScript
Map 而不是一个对象(例如由_.groupBy() 返回)。这有很多好处,包括:- 它会记住首次添加项目的顺序,
- 键可以是任何类型而不仅仅是字符串。
-
Map 是一个数组数组更有用的结果。但是如果你想要一个数组数组,那么你可以调用Array.from(groupedMap.entries()) (对于一个[key, group array] 对的数组)或Array.from(groupedMap.values()) (对于一个简单的数组数组)。 - 它非常灵活;通常,无论您计划在下一次使用此地图做什么,都可以直接作为缩减的一部分来完成。
作为最后一点的一个例子,假设我有一个对象数组,我想通过id进行(浅)合并,如下所示:
1 2 3 | const objsToMerge = [{id: 1, name:"Steve"}, {id: 2, name:"Alice"}, {id: 1, age: 20}]; // The following variable should be created automatically const mergedArray = [{id: 1, name:"Steve", age: 20}, {id: 2, name:"Alice"}] |
为此,我通常首先按id分组,然后合并每个结果数组。相反,您可以直接在
1 2 3 4 5 6 | const mergedArray = Array.from( objsToMerge.reduce( (entryMap, e) => entryMap.set(e.id, {...entryMap.get(e.id)||{}, ...e}), new Map() ).values() ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Array.prototype.groupBy = function(keyFunction) { var groups = {}; this.forEach(function(el) { var key = keyFunction(el); if (key in groups == false) { groups[key] = []; } groups[key].push(el); }); return Object.keys(groups).map(function(key) { return { key: key, values: groups[key] }; }); }; |
您可以使用Alasql JavaScript库来完成:
1 2 3 4 5 | var data = [ { Phase:"Phase 1", Step:"Step 1", Task:"Task 1", Value:"5" }, { Phase:"Phase 1", Step:"Step 1", Task:"Task 2", Value:"10" }]; var res = alasql('SELECT Phase, Step, SUM(CAST([Value] AS INT)) AS [Value] \ FROM ? GROUP BY Phase, Step',[data]); |
在jsFiddle上试试这个例子。
顺便说一句:在大型阵列上(100000条记录以上)Alasql更快到了Linq。请参阅jsPref上的测试。
评论:
- 这里我将Value放在方括号中,因为VALUE是SQL中的关键字
- 我必须使用CAST()函数将字符串值转换为数字类型。
MDN在其
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // Grouping objects by a property // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce#Grouping_objects_by_a_property#Grouping_objects_by_a_property var people = [ { name: 'Alice', age: 21 }, { name: 'Max', age: 20 }, { name: 'Jane', age: 20 } ]; function groupBy(objectArray, property) { return objectArray.reduce(function (acc, obj) { var key = obj[property]; if (!acc[key]) { acc[key] = []; } acc[key].push(obj); return acc; }, {}); } var groupedPeople = groupBy(people, 'age'); // groupedPeople is: // { // 20: [ // { name: 'Max', age: 20 }, // { name: 'Jane', age: 20 } // ], // 21: [{ name: 'Alice', age: 21 }] // } |
虽然问题有一些答案,答案看起来有点过于复杂,但我建议使用vanilla Javascript进行分组(如果需要)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | function groupBy(array, groups, valueKey) { var map = new Map; groups = [].concat(groups); return array.reduce((r, o) => { groups.reduce((m, k, i, { length }) => { var child; if (m.has(o[k])) return m.get(o[k]); if (i + 1 === length) { child = Object .assign(...groups.map(k => ({ [k]: o[k] })), { [valueKey]: 0 }); r.push(child); } else { child = new Map; } m.set(o[k], child); return child; }, map)[valueKey] += +o[valueKey]; return r; }, []) }; var data = [{ 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" }]; console.log(groupBy(data, 'Phase', 'Value')); console.log(groupBy(data, ['Phase', 'Step'], 'Value')); |
1 | .as-console-wrapper { max-height: 100% !important; top: 0; } |
我想建议我的方法。首先,单独分组和聚合。让我们宣布典型的"分组依据"功能。它需要另一个函数来为每个要分组的数组元素生成"哈希"字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | Array.prototype.groupBy = function(hash){ var _hash = hash ? hash : function(o){return o;}; var _map = {}; var put = function(map, key, value){ if (!map[_hash(key)]) { map[_hash(key)] = {}; map[_hash(key)].group = []; map[_hash(key)].key = key; } map[_hash(key)].group.push(value); } this.map(function(obj){ put(_map, obj, obj); }); return Object.keys(_map).map(function(key){ return {key: _map[key].key, group: _map[key].group}; }); } |
分组完成后,您可以根据需要汇总数据
1 2 3 4 5 6 7 8 9 10 11 12 | data.groupBy(function(o){return JSON.stringify({a: o.Phase, b: o.Step});}) /* aggreagating */ .map(function(el){ var sum = el.group.reduce( function(l,c){ return l + parseInt(c.Value); }, 0 ); el.key.Value = sum; return el.key; }); |
共同的是它有效。我已经在chrome控制台中测试了这段代码。随时改进并发现错误;)
没有突变:
1 2 3 4 5 6 | 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"]} |
这个解决方案采用任意函数(不是键),因此它比上面的解决方案更灵活,并且允许箭头函数,它类似于LINQ中使用的lambda表达式:
1 2 3 4 5 6 | Array.prototype.groupBy = function (funcProp) { return this.reduce(function (acc, val) { (acc[funcProp(val)] = acc[funcProp(val)] || []).push(val); return acc; }, {}); }; |
注意:是否要扩展
大多数浏览器支持的示例:
1 | [{a:1,b:"b"},{a:1,c:"c"},{a:2,d:"d"}].groupBy(function(c){return c.a;}) |
使用箭头功能的示例(ES6):
1 | [{a:1,b:"b"},{a:1,c:"c"},{a:2,d:"d"}].groupBy(c=>c.a) |
以上两个例子都返回:
1 2 3 4 | { "1": [{"a": 1,"b":"b"}, {"a": 1,"c":"c"}], "2": [{"a": 2,"d":"d"}] } |
根据以前的答案
1 2 3 | const groupBy = (prop) => (xs) => xs.reduce((rv, x) => Object.assign(rv, {[x[prop]]: [...(rv[x[prop]] || []), x]}), {}); |
如果您的环境支持,使用对象扩展语法来查看它会更好一些。
1 2 3 4 5 | const groupBy = (prop) => (xs) => xs.reduce((acc, x) => ({ ...acc, [ x[ prop ] ]: [...( acc[ x[ prop ] ] || []), x], }), {}); |
这里,我们的reducer采用部分形成的返回值(从空对象开始),并返回一个由前一个返回值的展开成员组成的对象,以及一个新成员,其成员的密钥是根据当前迭代的值来计算的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | groupByArray(xs, key) { return xs.reduce(function (rv, x) { let v = key instanceof Function ? key(x) : x[key]; let el = rv.find((r) => r && r.key === v); if (el) { el.values.push(x); } else { rv.push({ key: v, values: [x] }); } return rv; }, []); } |
这个输出数组。
让我们生成一个通用的
1 2 3 4 5 6 7 8 9 10 11 | Array.prototype.groupBy = function(cb){ return function iterate([x,...xs], i = 0, r = [[],[]]){ cb(x,i,[x,...xs]) ? (r[0].push(x), r) : (r[1].push(x), r); return xs.length ? iterate(xs, ++i, r) : r; }(this); }; var arr = [0,1,2,3,4,5,6,7,8,9], res = arr.groupBy(e => e < 5); console.log(res); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | Array.prototype.groupBy = function (groupingKeyFn) { if (typeof groupingKeyFn !== 'function') { throw new Error("groupBy take a function as only parameter"); } return this.reduce((result, item) => { let key = groupingKeyFn(item); if (!result[key]) result[key] = []; result[key].push(item); return result; }, {}); } var a = [ {type:"video", name:"a"}, {type:"image", name:"b"}, {type:"video", name:"c"}, {type:"blog", name:"d"}, {type:"video", name:"e"}, ] console.log(a.groupBy((item) => item.type)); |
1 | <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"> |
Ceasar的答案很好,但仅适用于数组内部元素的内部属性(长度为字符串)。
这个实现更像是这个链接
1 2 3 4 5 6 7 | const groupBy = function (arr, f) { return arr.reduce((out, val) => { let by = typeof f === 'function' ? '' + f(val) : val[f]; (out[by] = out[by] || []).push(val); return out; }, {}); }; |
希望这可以帮助...
基于ES6
如果未提供
1 2 3 4 5 6 7 8 9 10 | const data = [{id: 1, score: 2},{id: 1, score: 3},{id: 2, score: 2},{id: 2, score: 4}] const group = (arr, k) => arr.reduce((r, c) => (r[c[k]] = [...r[c[k]] || [], c], r), {}); const groupBy = (arr, k, fn = () => true) => arr.reduce((r, c) => (fn(c[k]) ? r[c[k]] = [...r[c[k]] || [], c] : null, r), {}); console.log(group(data, 'id')) // grouping via `reduce` console.log(groupBy(data, 'id')) // same result if `fn` is omitted console.log(groupBy(data, 'score', x => x > 2 )) // group with the iteratee |
在OP问题的背景下:
1 2 3 4 5 6 7 8 | const data = [ { 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" } ] const groupBy = (arr, k) => arr.reduce((r, c) => (r[c[k]] = [...r[c[k]] || [], c], r), {}); const groupWith = (arr, k, fn = () => true) => arr.reduce((r, c) => (fn(c[k]) ? r[c[k]] = [...r[c[k]] || [], c] : null, r), {}); console.log(groupBy(data, 'Phase')) console.log(groupWith(data, 'Value', x => x > 30 )) // group by `Value` > 30 |
另一个反转分组的ES6版本,使用
1 2 3 4 5 6 | const data = [{A:"1"}, {B:"10"}, {C:"10"}] const groupKeys = arr => arr.reduce((r,c) => (Object.keys(c).map(x => r[c[x]] = [...r[c[x]] || [], x]),r),{}); console.log(groupKeys(data)) |
注意:为简洁起见,函数以简短形式(一行)发布,并仅涉及该想法。您可以展开它们并添加其他错误检查等。
只是为了补充Scott Sauyet的回答,有些人在评论中询问如何使用他的函数来组合value1,value2等,而不是仅仅分组一个值。
只需要编辑他的求和函数:
1 2 3 4 5 6 7 8 | DataGrouper.register("sum", function(item) { return _.extend({}, item.key, {VALUE1: _.reduce(item.vals, function(memo, node) { return memo + Number(node.VALUE1);}, 0)}, {VALUE2: _.reduce(item.vals, function(memo, node) { return memo + Number(node.VALUE2);}, 0)} ); }); |
离开主要一个(DataGrouper)不变:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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; }()); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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')); |
有排序功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | export const groupBy = function groupByArray(xs, key, sortKey) { return xs.reduce(function(rv, x) { let v = key instanceof Function ? key(x) : x[key]; let el = rv.find(r => r && r.key === v); if (el) { el.values.push(x); el.values.sort(function(a, b) { return a[sortKey].toLowerCase().localeCompare(b[sortKey].toLowerCase()); }); } else { rv.push({ key: v, values: [x] }); } return rv; }, []); }; |
样品:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | var state = [ { name:"Arkansas", population:"2.978M", flag: "https://upload.wikimedia.org/wikipedia/commons/9/9d/Flag_of_Arkansas.svg", category:"city" },{ name:"Crkansas", population:"2.978M", flag: "https://upload.wikimedia.org/wikipedia/commons/9/9d/Flag_of_Arkansas.svg", category:"city" }, { name:"Balifornia", population:"39.14M", flag: "https://upload.wikimedia.org/wikipedia/commons/0/01/Flag_of_California.svg", category:"city" }, { name:"Florida", population:"20.27M", flag: "https://upload.wikimedia.org/wikipedia/commons/f/f7/Flag_of_Florida.svg", category:"airport" }, { name:"Texas", population:"27.47M", flag: "https://upload.wikimedia.org/wikipedia/commons/f/f7/Flag_of_Texas.svg", category:"landmark" } ]; console.log(JSON.stringify(groupBy(state,'category','name'))); |
来自@mortb,@ jmarceli的回答和这篇文章,
我利用
没有第三方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | function groupBy(list, keyGetter) { const map = new Map(); list.forEach((item) => { const key = keyGetter(item); if (!map.has(key)) { map.set(key, [item]); } else { map.get(key).push(item); } }); return map; } const pets = [ {type:"Dog", age: 3, name:"Spot"}, {type:"Cat", age: 3, name:"Tiger"}, {type:"Dog", age: 4, name:"Rover"}, {type:"Cat", age: 3, name:"Leo"} ]; const grouped = groupBy(pets, pet => JSON.stringify({ type: pet.type, age: pet.age })); console.log(grouped); |
与Lodash第三方合作
1 2 3 4 5 6 7 8 9 10 11 | const pets = [ {type:"Dog", age: 3, name:"Spot"}, {type:"Cat", age: 3, name:"Tiger"}, {type:"Dog", age: 4, name:"Rover"}, {type:"Cat", age: 3, name:"Leo"} ]; let rslt = _.groupBy(pets, pet => JSON.stringify( { type: pet.type, age: pet.age })); console.log(rslt); |
这是一个ES6版本,不会在null成员上中断
1 2 3 4 5 6 | function groupBy (arr, key) { return (arr || []).reduce((acc, x = {}) => ({ ...acc, [x[key]]: [...acc[x[key]] || [], x] }), {}) } |
检查答案 - 不是分组解决,但是直接的答案。
对于具有计算的键名称的某个字段的对象数组的REAL GROUP BY。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | const inputArray = [ { 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" } ]; var outObject = inputArray.reduce(function(a, e) { // GROUP BY estimated key (estKey), well, may be a just plain key // a -- Accumulator result object // e -- sequentally checked Element, the Element that is tested just at this itaration // new grouping name may be calculated, but must be based on real value of real field let estKey = (e['Phase']); (a[estKey] ? a[estKey] : (a[estKey] = null || [])).push(e); return a; }, {}); console.log(outObject); |
使用
您还可以递归地对数据进行分组。例如,最初按
自己验证,运行它。 Этотосамоеоно,чтолюдиназываютгруппировкой?
祝你成功。
Даздравствуютвысокиепоказателимастерствапрограммистоввоимяпроцветаниявсегочеловечества! Ура,товарищи!
我已经扩展了接受的答案,包括按多个属性分组,添加thenby并使其纯粹功能,没有变异。请访问https://stackblitz.com/edit/typescript-ezydzv查看演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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; }; |
通常我使用Lodash JavaScript实用程序库和预先构建的
下面的函数允许任意字段的groupBy(和sum值 - OP需要什么)。在解决方案中,我们定义
1 2 3 4 5 6 7 8 9 | 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; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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']) ); |
我从underscore.js fiddler那里借用了这个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | 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; })); |
您可以在数组上使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // @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 |
开玩笑测试可能就像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // @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') }) |
1 2 3 4 5 6 | 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; }, {}) |
你可以通过以下方式完成。我刚刚形成了新的数组并从groupBy函数返回它。通过.map函数循环计算的计数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | var arr = [ { 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" } ]; var groupBy = (arr, pahse, step='') => { var pahseArr = []; var resultArr = []; arr.map((item)=>{ var pushed = false; pahseArr.map((ele)=>{ if(ele===item.Phase){ pushed = true; } }) if(!pushed){ pahseArr.push(item.Phase); } }) pahseArr.map((item)=>{ var sum = 0; arr.map((ele)=>{ if(ele.Phase===item){ sum += parseFloat(ele.Value) } }) resultArr.push({ Phase: item, Value: sum }) }) if(step!=''){ var resultArr = []; pahseArr.map((item)=>{ var stepArr = []; arr.map((item2)=>{ var pushed = false; stepArr.map((ele)=>{ if(ele===item2.Step){ pushed = true; } }) if(!pushed){ stepArr.push(item2.Step); } }) stepArr.map((item1)=>{ var sum = 0; arr.map((ele)=>{ if(ele.Step===item1 && ele.Phase===item){ sum += parseFloat(ele.Value) } }) resultArr.push({ Phase: item, Step: item1, Value: sum }) }) }) return resultArr; } return resultArr; } console.log(groupBy(arr, 'Phase')); console.log(groupBy(arr, 'Phase', 'Step')); |
这是使用ES6的令人讨厌,难以阅读的解决方案:
1 2 3 4 5 6 | export default (array, key) => { return array.reduce( (r, v, _, __, k = v[key]) => ((r[k] || (r[k] = [])).push(v), r), {} ); }; |
基于@Ceasar Bautista的最初想法,我修改了代码并使用typescript创建了一个groupBy函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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; }, []); }; |
此函数接受一个回调(比较器),它比较行并找到dublicates和第二个回调聚合共享者的回调(onDublicate)。
用法示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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']; }; |
调用
1 | groupBy(data, demoComparator, demoOnDublicate) |
将执行一个计算值之和的组。
1 2 | {name:"a", value: 33} {name:"b", value: 21} |
我们可以根据项目的需要创建尽可能多的回调函数,并根据需要聚合值。例如,在一种情况下,我需要合并两个数组而不是对数据求和。