按多个键分组并求和,保持类型安全。

huangapple go评论78阅读模式
英文:

Group by and sum on multiple keys maintaining type safety

问题

给定一个对象数组,我想要按任意数量的对象键进行分组,并且对第二组任意数量的键的值进行求和。

例如,给定:

const arr = [
   { shape: "square", color: "red", available: 1, ordered: 1 },
   { shape: "square", color: "red", available: 2, ordered: 1 },
   { shape: "circle", color: "blue", available: 0, ordered: 3 },
   { shape: "square", color: "blue", available: 4, ordered: 4 },
];

如果我按shapecolor两个键进行分组,并且想要求availableordered的值之和,结果应该是:

[
  { shape: "square", color: "red", available: 3, ordered: 2 },
  { shape: "circle", color: "blue", available: 0, ordered: 3 },
  { shape: "square", color: "blue", available: 4, ordered: 4 },
];

我已经仔细研究了许多类似的SO讨论帖子[1,其中上面的示例是基于此的,2345]。问题在于它们中没有一个:

  • 提供一个通用的、即用即得的函数(它们是基于自定义对象键的)
  • 基于TypeScript并提供类型安全的实现
  • 对对象中的附加键(例如,如果arr包含另一个与转换无关的属性size,它不应该包含虚假值)进行足够处理

我该如何构建一个通用的、类型安全的groupBySum函数,它接受多个分组和求和键?

英文:

Given an array of objects, I'd like to group it by an arbitrary number of object keys, and sum the values of a second arbitrary number of keys.

For example, given:

const arr = [
   { shape: "square", color: "red", available: 1, ordered: 1 },
   { shape: "square", color: "red", available: 2, ordered: 1 },
   { shape: "circle", color: "blue", available: 0, ordered: 3 },
   { shape: "square", color: "blue", available: 4, ordered: 4 },
 ];

If I group by both shape and color and want the sum of the values of available and ordered, the result should be:

[
  { shape: "square", color: "red", available: 3, ordered: 2 },
  { shape: "circle", color: "blue", available: 0, ordered: 3 },
  { shape: "square", color: "blue", available: 4, ordered: 4 },
];

I've extensively gone through many similar SO threads [1, from which the example above is based on, 2, 3, 4, 5]. The issue is that none of them:

  • Provide a generic, ready-to-use function (they're based on custom object keys)
  • Are based on Typescript and provide a type-safe implementation
  • Deal adequately with additional keys in the object (e.g., if arr contained another property size not involved in the transformation it shouldn't contain bogus values)

How can I build a generic, type-safe groupBySum function that accepts multiple grouping and summing keys?

答案1

得分: 2

以下是翻译好的部分:

下面的TypeScript函数满足所有所需的条件:

/**
 * 求和对象值在对象数组中,按任意对象键分组。
 *
 * @remarks
 * 此方法接受并返回一个对象数组。
 * 结果对象数组包含原始数组中对象键的子集。
 *
 * @param arr - 要按键分组和求和的对象数组。
 * @param groupByKeys - 用于分组的键数组。
 * @param sumKeys - 用于求和的键数组。键必须引用数值。
 * @returns 一个对象数组,按groupByKeys分组,sumKeys中的键值相加。
 */
const groupBySum = <T, K extends keyof T, S extends keyof T>(
  arr: T[],
  groupByKeys: K[],
  sumKeys: S[]
): Pick<T, K | S>[] => {
  return [
    ...arr
      .reduce((accu, curr) => {
        const keyArr = groupByKeys.map((key) => curr[key]);
        const key = keyArr.join("-");
        const groupedSum =
          accu.get(key) ||
          Object.assign(
            {},
            Object.fromEntries(groupByKeys.map((key) => [key, curr[key]])),
            Object.fromEntries(sumKeys.map((key) => [key, 0]))
          );
        for (let key of sumKeys) {
          groupedSum[key] += curr[key];
        }
        return accu.set(key, groupedSum);
      }, new Map())
      .values(),
  ];
};

以下的代码片段使用JavaScript的等效部分来展示基于你的arr的一些示例:

const arr = [
   { shape: "square", color: "red", available: 1, ordered: 1 },
   { shape: "square", color: "red", available: 2, ordered: 1 },
   { shape: "circle", color: "blue", available: 0, ordered: 3 },
   { shape: "square", color: "blue", available: 4, ordered: 4 },
 ];

const groupBySum = (arr, groupByKeys, sumKeys) => {
  return [
    ...arr
      .reduce((accu, curr) => {
        const keyArr = groupByKeys.map((key) => curr[key]);
        const key = keyArr.join("-");
        const groupedSum =
          accu.get(key) ||
          Object.assign(
            {},
            Object.fromEntries(groupByKeys.map((key) => [key, curr[key]])),
            Object.fromEntries(sumKeys.map((key) => [key, 0]))
          );
        for (let key of sumKeys) {
          groupedSum[key] += curr[key];
        }
        return accu.set(key, groupedSum);
      }, new Map())
      .values(),
  ];
};

console.log('groupBySum(arr, ["shape"], ["available"])')
console.log(groupBySum(arr, ["shape"], ["available"]))
console.log('\n\ngroupBySum(arr, ["color"], ["ordered"])')
console.log(groupBySum(arr, ["color"], ["ordered"]))
console.log('\n\ngroupBySum(arr, ["shape", "color"], ["available", "ordered"])')
console.log(groupBySum(arr, ["shape", "color"], ["available", "ordered"]))

TypeScript的实现是类型安全的。例如,如果我们尝试传递一个无效的键...

groupBySum(arr, ["blah"], ["ordered"]);

...编译器会报错:

Type '"blah"' is not assignable to type '"shape" | "ordered" | "color" | "available"'.ts(2322)

返回的对象也是类型安全的。例如,...

const ans = groupBySum(arr, ["shape"], ["ordered"])

...的ans的类型是:

Array<{ shape: string; ordered: number }>;

最后,请注意,不参与转换的任何键都会被丢弃。上面的示例不包含coloravailable,因为它们不可能包含有意义的值。这已经在返回类型中构建,因此TypeScript知道不要期望它们。

英文:

The following TypeScript function meets all the desired criteria:

/**
 * Sums object value(s) in an array of objects, grouping by arbitrary object keys.
 *
 * @remarks
 * This method takes and returns an array of objects.
 * The resulting array of object contains a subset of the object keys in the
 * original array.
 *
 * @param arr - The array of objects to group by and sum.
 * @param groupByKeys - An array with the keys to group by.
 * @param sumKeys - An array with the keys to sum. The keys must refer
 *    to numeric values.
 * @returns An array of objects, grouped by groupByKeys and with the values
 *    of keys in sumKeys summed up.
 */
const groupBySum = &lt;T, K extends keyof T, S extends keyof T&gt;(
  arr: T[],
  groupByKeys: K[],
  sumKeys: S[]
): Pick&lt;T, K | S&gt;[] =&gt; {
  return [
    ...arr
      .reduce((accu, curr) =&gt; {
        const keyArr = groupByKeys.map((key) =&gt; curr[key]);
        const key = keyArr.join(&quot;-&quot;);
        const groupedSum =
          accu.get(key) ||
          Object.assign(
            {},
            Object.fromEntries(groupByKeys.map((key) =&gt; [key, curr[key]])),
            Object.fromEntries(sumKeys.map((key) =&gt; [key, 0]))
          );
        for (let key of sumKeys) {
          groupedSum[key] += curr[key];
        }
        return accu.set(key, groupedSum);
      }, new Map())
      .values(),
  ];
};

The code snippet below uses the JavaScript equivalent to showcase a few examples based on your arr:

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

const arr = [
{ shape: &quot;square&quot;, color: &quot;red&quot;, available: 1, ordered: 1 },
{ shape: &quot;square&quot;, color: &quot;red&quot;, available: 2, ordered: 1 },
{ shape: &quot;circle&quot;, color: &quot;blue&quot;, available: 0, ordered: 3 },
{ shape: &quot;square&quot;, color: &quot;blue&quot;, available: 4, ordered: 4 },
];
const groupBySum = (arr, groupByKeys, sumKeys) =&gt; {
return [
...arr
.reduce((accu, curr) =&gt; {
const keyArr = groupByKeys.map((key) =&gt; curr[key]);
const key = keyArr.join(&quot;-&quot;);
const groupedSum =
accu.get(key) ||
Object.assign(
{},
Object.fromEntries(groupByKeys.map((key) =&gt; [key, curr[key]])),
Object.fromEntries(sumKeys.map((key) =&gt; [key, 0]))
);
for (let key of sumKeys) {
groupedSum[key] += curr[key];
}
return accu.set(key, groupedSum);
}, new Map())
.values(),
];
};
console.log(&#39;groupBySum(arr, [&quot;shape&quot;], [&quot;available&quot;])&#39;)
console.log(groupBySum(arr, [&quot;shape&quot;], [&quot;available&quot;]))
console.log(&#39;\n\ngroupBySum(arr, [&quot;color&quot;], [&quot;ordered&quot;])&#39;)
console.log(groupBySum(arr, [&quot;color&quot;], [&quot;ordered&quot;]))
console.log(&#39;\n\ngroupBySum(arr, [&quot;shape&quot;, &quot;color&quot;], [&quot;available&quot;, &quot;ordered&quot;])&#39;)
console.log(groupBySum(arr, [&quot;shape&quot;, &quot;color&quot;], [&quot;available&quot;, &quot;ordered&quot;]))

<!-- end snippet -->

The Typescript implementation is type-safe. For example, if we try to pass an invalid key...

groupBySum(arr, [&quot;blah&quot;], [&quot;ordered&quot;]);

... the compiler will complain:

Type &#39;&quot;blah&quot;&#39; is not assignable to type &#39;&quot;shape&quot; | &quot;ordered&quot; | &quot;color&quot; | &quot;available&quot;&#39;.ts(2322)

The returned object is also type-safe. For example, the type of ans in...

const ans = groupBySum(arr, [&quot;shape&quot;], [&quot;ordered&quot;])

...is:

Array&lt;{ shape: string; ordered: number }&gt;;

Finally, note that any keys not involved in the transformation are dropped. The example above doesn't contain color or available, which couldn't possibly contain meaningful values. This is built in the return type, so TypeScript knows not to expect them.

答案2

得分: 0

以下是您要翻译的内容:

const arr = [
  { shape: 'square', color: 'red', available: 1, ordered: 1 },
  { shape: 'square', color: 'red', available: 2, ordered: 1 },
  { shape: 'circle', color: 'blue', available: 0, ordered: 3 },
  { shape: 'square', color: 'blue', available: 4, ordered: 4 },
]

function groupAndSum(arr, groupKeys, sumKeys) {
  const groupedData = arr.reduce((acc, obj) => {
    const groupValues = groupKeys.map((key) => obj[key]).join('-')

    if (!acc.has(groupValues)) {
      acc.set(groupValues, { ...obj })
    } else {
      const existingObj = acc.get(groupValues)
      sumKeys.forEach((key) => (existingObj[key] += obj[key]))
    }

    return acc
  }, new Map())

  return Array.from(groupedData.values())
}

const groupKeys = ['shape', 'color']
const sumKeys = ['available', 'ordered']

const result = groupAndSum(arr, groupKeys, sumKeys)
console.log(result)
英文:

You can do:

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

const arr = [
{ shape: &#39;square&#39;, color: &#39;red&#39;, available: 1, ordered: 1 },
{ shape: &#39;square&#39;, color: &#39;red&#39;, available: 2, ordered: 1 },
{ shape: &#39;circle&#39;, color: &#39;blue&#39;, available: 0, ordered: 3 },
{ shape: &#39;square&#39;, color: &#39;blue&#39;, available: 4, ordered: 4 },
]
function groupAndSum(arr, groupKeys, sumKeys) {
const groupedData = arr.reduce((acc, obj) =&gt; {
const groupValues = groupKeys.map((key) =&gt; obj[key]).join(&#39;-&#39;)
if (!acc.has(groupValues)) {
acc.set(groupValues, { ...obj })
} else {
const existingObj = acc.get(groupValues)
sumKeys.forEach((key) =&gt; (existingObj[key] += obj[key]))
}
return acc
}, new Map())
return Array.from(groupedData.values())
}
const groupKeys = [&#39;shape&#39;, &#39;color&#39;]
const sumKeys = [&#39;available&#39;, &#39;ordered&#39;]
const result = groupAndSum(arr, groupKeys, sumKeys)
console.log(result)

<!-- end snippet -->

huangapple
  • 本文由 发表于 2023年5月17日 20:48:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/76272301.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定