简体   繁体   English

按键名称对对象数组进行分组和聚合

[英]Group and aggregate array of objects by key names

I would like to write a function in JS that takes lists of names as arguments and is able to group by and aggregate by the specified column names. 我想在JS中编写一个函数,该函数将名称列表作为参数,并且能够按指定的列名称进行分组和汇总。 For example, my data might look like: 例如,我的数据可能如下所示:

const SALES = [
  { lead: 'Mgr 1', revenue: 49.99, repName: 'Rep 1', forecast: 81.00 },
  { lead: 'Mgr 1', revenue: 9.99, repName: 'Rep 1', forecast: 91.00 },
  { lead: 'Mgr 1', revenue: 9.99, repName: 'Rep 13', forecast: 82.00 },
  { lead: 'Mgr 2', revenue: 99.99, repName: 'Rep 3', forecast: 101.00 },
  { lead: 'Mgr 2', revenue: 9.99, repName: 'Rep 5', forecast: 89.00 },
  { lead: 'Mgr 3', revenue: 199.99, repName: 'Rep 6', forecast: 77.00 }
];

I can group and aggregate this data like this: 我可以像这样对数据进行分组和汇总:

let grouped = {};
SALES.forEach(({lead, repName, revenue}) => {
  grouped[[lead, repName]] = grouped[[lead, repName]] || { lead, repName, revenue: 0 };
  grouped[[lead, repName]].revenue = +grouped[[lead, repName]].revenue + (+revenue);
});
grouped = Object.values(grouped);

console.warn('Look at this:\n', grouped);

However, I would like this to be more dynamic so that I don't have to write an if-else statement for all the possible combinations of groupings and aggregations. 但是,我希望它更具动态性,这样我就不必为分组和聚合的所有可能组合编写if-else语句。 The following code shows something that I would like to get working, but currently does not. 以下代码显示了一些我想开始使用的东西,但目前还没有。

function groupByTotal(arr, groupByCols, aggregateCols) {
  let grouped = {};
  arr.forEach(({ groupByCols, aggregateCols }) => {
    grouped[groupByCols] = grouped[groupByCols] || { groupByCols, aggregateCols: 0 };
    grouped[groupByCols].aggregateCols = +grouped[groupByCols].aggregateCols + (+aggregateCols);
  });
  grouped = Object.values(grouped);
  return grouped;
}

groupByTotal(SALES,['lead','repName'],'revenue')

Expected output might look like this: 预期的输出可能如下所示:

[
  { lead: "Mgr 1", repName: "Rep 1", revenue: 59.98 },
  { lead: "Mgr 1", repName: "Rep 13", revenue: 9.99 },
  { lead: "Mgr 2", repName: "Rep 3", revenue: 99.99 },
  { lead: "Mgr 2", repName: "Rep 5", revenue: 9.99 },
  { lead: "Mgr 3", repName: "Rep 6", revenue: 199.99 }
]

Ideally, I would like to be able to pass in any number of column names to group by or to be aggregated. 理想情况下,我希望能够传入任意数量的列名以进行分组或聚合。 Any help would be greatly appreciated. 任何帮助将不胜感激。

Currently you are creating a key based on the stringified value of [lead, repName] . 当前,您正在基于[lead, repName]的字符串化值创建密钥。 You could get this dynamically based on groupByCols 您可以基于groupByCols动态groupByCols

// gets the values for "groupByCols" seperated by `|` to create a unique key
const values = groupByCols.map(k => o[k]).join("|");

You'd also need to get a subset of the object based on groupByCols 您还需要基于groupByCols获取对象的子集

const subSet = (o, keys) => keys.reduce((r, k) => (r[k] = o[k], r), {})

// OR if fromEntries() is supported
const subSet = (o, keys) => Object.fromEntries(keys.map(k => [k, o[k]))

Rest of the logic would be similar to what you're already doing. 其余逻辑将类似于您已经在做的事情。 Use the unique in grouped . grouped使用唯一性。 Get the subset of the object and add/update aggregateCols key based on whether the key already exists or not 获取对象的子集,并根据键是否已经存在来添加/更新aggregateCols

 const SALES = [ { lead: 'Mgr 1', revenue: 49.99, repName: 'Rep 1', forecast: 81.00 }, { lead: 'Mgr 1', revenue: 9.99, repName: 'Rep 1', forecast: 91.00 }, { lead: 'Mgr 1', revenue: 9.99, repName: 'Rep 13', forecast: 82.00 }, { lead: 'Mgr 2', revenue: 99.99, repName: 'Rep 3', forecast: 101.00 }, { lead: 'Mgr 2', revenue: 9.99, repName: 'Rep 5', forecast: 89.00 }, { lead: 'Mgr 3', revenue: 199.99, repName: 'Rep 6', forecast: 77.00 } ]; const subSet = (o, keys) => keys.reduce((r, k) => (r[k] = o[k], r), {}) function groupByTotal(arr, groupByCols, aggregateCols) { let grouped = {}; arr.forEach(o => { const values = groupByCols.map(k => o[k]).join("|"); if (grouped[values]) grouped[values][aggregateCols] += o[aggregateCols] else grouped[values] = { ...subSet(o, groupByCols), [aggregateCols]: o[aggregateCols] } }) return Object.values(grouped); } console.log("Sum revenue based on lead and repName") console.log(groupByTotal(SALES, ['lead', 'repName'], 'revenue')) console.log("Sum forecast based on lead: ") console.log(groupByTotal(SALES, ['lead'], 'forecast')) 

If you want to pass a array of columns to sum, you can loop through aggregateCols and sum each property in grouped : 如果您想传递一个列数组进行求和,则可以遍历aggregateCols并对grouped每个属性求和:

if (grouped[values]) {
    aggregateCols.forEach(col => grouped[values][col] += o[col])
    grouped[values].Count++
} else {
    grouped[values] = subSet(o, groupByCols);
    grouped[values].Count = 1
    aggregateCols.forEach(col => grouped[values][col] = o[col])
}

You can implement a similar algorithm using pure JavaScript. 您可以使用纯JavaScript实现类似的算法。

Just know how to create your key and aggregate the data as in the following scenario. 只知道如何创建密钥并聚合数据,如以下情况所示。

 const SALES = [ { lead: 'Mgr 1', revenue: 49.99, repName: 'Rep 1', forecast: 81.00 }, { lead: 'Mgr 1', revenue: 9.99, repName: 'Rep 1', forecast: 91.00 }, { lead: 'Mgr 1', revenue: 9.99, repName: 'Rep 13', forecast: 82.00 }, { lead: 'Mgr 2', revenue: 99.99, repName: 'Rep 3', forecast: 101.00 }, { lead: 'Mgr 2', revenue: 9.99, repName: 'Rep 5', forecast: 89.00 }, { lead: 'Mgr 3', revenue: 199.99, repName: 'Rep 6', forecast: 77.00 } ]; console.log(aggregate(SALES, ['lead', 'repName'], 'revenue')); function aggregate(data, keyFields, accumulator) { var createNewObj = (ref, fields) => { return fields.reduce((result, key) => { return Object.assign(result, { [key] : ref[key] }); }, {}); } return Object.values(data.reduce((result, object, index, ref) => { let key = keyFields.map(key => object[key]).join(''); let val = result[key] || createNewObj(object, keyFields); val[accumulator] = (val[accumulator] || 0) + object[accumulator]; return Object.assign(result, { [key] : val }); }, {})); } 
 .as-console-wrapper { top: 0; max-height: 100% !important; } 

Result 结果

[
  {
    "lead": "Mgr 1",
    "repName": "Rep 1",
    "revenue": 59.98
  },
  {
    "lead": "Mgr 1",
    "repName": "Rep 13",
    "revenue": 9.99
  },
  {
    "lead": "Mgr 2",
    "repName": "Rep 3",
    "revenue": 99.99
  },
  {
    "lead": "Mgr 2",
    "repName": "Rep 5",
    "revenue": 9.99
  },
  {
    "lead": "Mgr 3",
    "repName": "Rep 6",
    "revenue": 199.99
  }
]

Alternative with custom accumulator function 自定义累加器功能的替代方案

The following example uses an accumulator object that contains a reference field and a function that applies a mathematical expression. 下面的示例使用一个累加器对象,该对象包含一个参考字段和一个应用数学表达式的函数。

{
  key: 'revenue',
  fn : (total, value) => total + value
}

 const SALES = [ { lead: 'Mgr 1', revenue: 49.99, repName: 'Rep 1', forecast: 81.00 }, { lead: 'Mgr 1', revenue: 9.99, repName: 'Rep 1', forecast: 91.00 }, { lead: 'Mgr 1', revenue: 9.99, repName: 'Rep 13', forecast: 82.00 }, { lead: 'Mgr 2', revenue: 99.99, repName: 'Rep 3', forecast: 101.00 }, { lead: 'Mgr 2', revenue: 9.99, repName: 'Rep 5', forecast: 89.00 }, { lead: 'Mgr 3', revenue: 199.99, repName: 'Rep 6', forecast: 77.00 } ]; console.log(aggregate(SALES, ['lead', 'repName'], { key: 'revenue', fn : (total, value) => total + value })); function aggregate(data, keyFields, accumulator) { var createNewObj = (ref, fields) => { return fields.reduce((result, key) => { return Object.assign(result, { [key] : ref[key] }); }, {}); } return Object.values(data.reduce((result, object, index, ref) => { let key = keyFields.map(key => object[key]).join(''); let val = result[key] || createNewObj(object, keyFields); val[accumulator.key] = accumulator.fn(val[accumulator.key] || 0, object[accumulator.key]); return Object.assign(result, { [key] : val }); }, {})); } 
 .as-console-wrapper { top: 0; max-height: 100% !important; } 

Another functional approach 另一功能方法

If you want to accumulate multiple fields, you will need to drop the field to reference and just modify the entire object, but this is typically more dangerous. 如果要累积多个字段,则需要删除该字段以进行引用,而只是修改整个对象,但这通常更危险。

 const SALES = [ { lead: 'Mgr 1', revenue: 49.99, repName: 'Rep 1', forecast: 81.00 }, { lead: 'Mgr 1', revenue: 9.99, repName: 'Rep 1', forecast: 91.00 }, { lead: 'Mgr 1', revenue: 9.99, repName: 'Rep 13', forecast: 82.00 }, { lead: 'Mgr 2', revenue: 99.99, repName: 'Rep 3', forecast: 101.00 }, { lead: 'Mgr 2', revenue: 9.99, repName: 'Rep 5', forecast: 89.00 }, { lead: 'Mgr 3', revenue: 199.99, repName: 'Rep 6', forecast: 77.00 } ]; console.log(aggregate(SALES, ['lead', 'repName'], (prev, curr) => { return Object.assign(prev, { revenueTotal : (prev['revenueTotal'] || 0) + curr['revenue'], forecastMax : Math.max((prev['forecastMax'] || -Number.MAX_VALUE), curr['forecast']), forecastMin : Math.min((prev['forecastMin'] || +Number.MAX_VALUE), curr['forecast']) }); })); function aggregate(data, keyFields, accumulatorFn) { var createNewObj = (ref, fields) => { return fields.reduce((result, key) => { return Object.assign(result, { [key] : ref[key] }); }, {}); } return Object.values(data.reduce((result, object, index, ref) => { let key = keyFields.map(key => object[key]).join(''); let val = result[key] || createNewObj(object, keyFields); return Object.assign(result, { [key] : accumulatorFn(val, object) }); }, {})); } 
 .as-console-wrapper { top: 0; max-height: 100% !important; } 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM