英文:
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 },
];
如果我按shape
和color
两个键进行分组,并且想要求available
和ordered
的值之和,结果应该是:
[
{ 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,其中上面的示例是基于此的,2,3,4,5]。问题在于它们中没有一个:
- 提供一个通用的、即用即得的函数(它们是基于自定义对象键的)
- 基于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 propertysize
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 }>;
最后,请注意,不参与转换的任何键都会被丢弃。上面的示例不包含color
或available
,因为它们不可能包含有意义的值。这已经在返回类型中构建,因此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 = <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(),
];
};
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: "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"]))
<!-- end snippet -->
The Typescript implementation is type-safe. For example, if we try to pass an invalid key...
groupBySum(arr, ["blah"], ["ordered"]);
... the compiler will complain:
Type '"blah"' is not assignable to type '"shape" | "ordered" | "color" | "available"'.ts(2322)
The returned object is also type-safe. For example, the type of ans
in...
const ans = groupBySum(arr, ["shape"], ["ordered"])
...is:
Array<{ shape: string; ordered: number }>;
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: '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)
<!-- end snippet -->
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论