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

7

我希望编写一个JS函数,它以名称列表为参数,并能按指定的列名称进行分组和聚合。例如,我的数据可能如下所示:

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

我可以按照以下方式对这些数据进行分组和聚合:
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);

然而,我希望它能更具动态性,这样我就不必为所有可能的分组和聚合组合编写if-else语句。以下代码显示了我想要使其工作的内容,但目前并没有实现。

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')

预期输出可能看起来像这样:
[
  { 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 }
]

理想情况下,我希望能够传入任意数量的列名称进行分组或聚合。非常感谢您的帮助。
2个回答

5

当前您正在根据字符串化值[lead,repName]创建关键字。 您可以基于groupByCols动态获取此关键字。

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

您还需要根据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]))

其他逻辑与您已经在做的类似。在 grouped 中使用 unique。获取对象子集并根据键是否已存在添加/更新 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'))

如果您想传递一组要求求和的列,您可以循环遍历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])
}

完美运行!谢谢您详细解释的理由。 - ErrorJordan
还有一个额外的要求。是否可以添加一个布尔参数,指定输出是否应包括一个附加列,其中包含分组在一起的记录数量的计数? - ErrorJordan
这个函数一次不能聚合多列数据,比如我想要每个潜在客户的总收入和预测值。这个能实现吗?@adiga - ErrorJordan
1
@AlwaysError 更新了aggregateCols属性的数组。计数会类似。在对象创建时添加默认的count = 1。当更新时,count++ - adiga

3
你可以使用纯JavaScript实现类似的算法。
只需了解如何创建密钥并按以下方案聚合数据即可。

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

结果

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

使用自定义累加器函数的替代方案

下面的示例使用一个包含引用字段和应用数学表达式的函数的累加器对象。

{
  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; }

另一种函数式方法

如果你想要累加多个字段,你需要放弃对字段的引用,只修改整个对象,但这通常更加危险。

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


1
喜欢你的.as-console-wrapper样式块! :) - KyleMit
看起来很棒!感谢您的帮助!最好的祝福! - ErrorJordan
@Mr.Polywhirl 嗯...这对我似乎不起作用。"acumulator.fn 不是一个函数" - ErrorJordan
@Mr.Polywhirl 谢谢,我会继续努力。此外,这个函数每次只允许我聚合一个列,比如说如果我想对每个线索汇总收入和预测值,那么是否可能实现呢? - ErrorJordan
@AlwaysError 你需要设置一个不同的函数,就像最后一个例子中所看到的那样。 - Mr. Polywhirl
显示剩余2条评论

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