将 JS 对象数组中的项分组,一次 X 项,并对每组的值求和
Group items in JS array of objects X items at a time, and sum up the values of each group
假设以下对象数组,每个对象包含一个 label
属性(表示日期的字符串)和一个 spend
属性(包含一个数字) :
myMonthlySpend = [
{ label: "2021-02-03", spend: 4.95 },
{ label: "2021-02-04", spend: 15.96 },
{ label: "2021-02-05", spend: 11 },
{ label: "2021-02-06", spend: 10.07 },
{ label: "2021-02-07", spend: 6.83 },
{ label: "2021-02-08", spend: 4.85 }
];
现在,项目的数量是不固定的,可以从 1 到无穷大。无论数组大小如何,假设我想将每个 X
项聚类到一个包含的对象中,再次:
label
属性 由 first 的原始 label
和 last 组成组项目
spent
属性 表示该组所有项目的总和 spend
属性
并以数组形式返回这些对象,如下所示:
myGroupedSpend = [
....
{label: 'date from - date to', spend: 'total spend for these items'},
{label: 'date from - date to', spend: 'total spend for these items'}
...
]
我尝试执行以下操作,但由于以下原因,输出显然是错误的:
- 总支出金额不同,与所有项目的实际总金额不符
- 结果标签(意思是,'date from - date to')不一致
myMonthlySpend = [
{ label: "2021-02-03", spend: 4.95 },
{ label: "2021-02-04", spend: 15.96 },
{ label: "2021-02-05", spend: 11 },
{ label: "2021-02-06", spend: 10.07 },
{ label: "2021-02-07", spend: 6.83 },
{ label: "2021-02-08", spend: 4.85 },
{ label: "2021-02-09", spend: 5.01 },
{ label: "2021-02-10", spend: 5.09 },
{ label: "2021-02-11", spend: 9.1 },
{ label: "2021-02-12", spend: 10.18 },
{ label: "2021-02-13", spend: 10.17 },
{ label: "2021-02-14", spend: 10.16 },
{ label: "2021-02-15", spend: 10.07 },
{ label: "2021-02-16", spend: 9.94 },
{ label: "2021-02-17", spend: 9.76 },
{ label: "2021-02-18", spend: 10.09 },
{ label: "2021-02-19", spend: 10.05 },
{ label: "2021-02-20", spend: 9.93 },
{ label: "2021-02-21", spend: 9.8 },
{ label: "2021-02-22", spend: 10.26 },
{ label: "2021-02-23", spend: 10.03 },
{ label: "2021-02-24", spend: 10.09 },
{ label: "2021-02-25", spend: 10.09 },
{ label: "2021-02-26", spend: 9.95 },
{ label: "2021-02-27", spend: 9.78 },
{ label: "2021-02-28", spend: 9.77 },
{ label: "2021-03-01", spend: 10.11 },
{ label: "2021-03-02", spend: 10.04 },
{ label: "2021-03-03", spend: 10.01 },
{ label: "2021-03-04", spend: 5.06 },
{ label: "2021-03-05", spend: 4.72 },
{ label: "2021-03-06", spend: 5.36 },
{ label: "2021-03-07", spend: 4.98 },
{ label: "2021-03-08", spend: 1.51 }
];
// What is the actual total of all items.spend
let totalSpend = 0;
myMonthlySpend.forEach(v => totalSpend += v.spend);
// Grouping function
function groupItems(rawData, groupEvery) {
let allGroups = [];
let currentSpend = 0;
for (let i = 0, j = 0; i < rawData.length; i++) {
currentSpend += rawData[i].spend;
if (i >= groupEvery && i % groupEvery === 0) {
let currentLabel = rawData[i - groupEvery].label + ' - ' + rawData[i].label;
j++;
allGroups[j] = allGroups[j] || {};
allGroups[j] = {'label': currentLabel, 'spend': currentSpend};
currentSpend = 0;
} else if (i < groupEvery && i % groupEvery === 0) {
let currentLabel = rawData[0].label + ' - ' + rawData[groupEvery].label;
allGroups[j] = {'label': currentLabel, 'spend': currentSpend};
}
}
let checkTotal = 0;
allGroups.forEach(v => checkTotal += v.spend);
console.log('Total spend when grouped by', groupEvery, 'is', checkTotal,
'\nwhile the actual total should be', totalSpend);
return allGroups;
}
// Trying with grouping by 5 and by 9
console.log(groupItems(myMonthlySpend, 5))
console.log('\n\n');
console.log(groupItems(myMonthlySpend, 9))
.as-console-wrapper { max-height: 100% !important; top: 0; }
例如,如果以 10 作为 groupEvery 编号调用该函数,输出将是:
correctOutput = [
{ label: "2021-02-03 - 2021-02-12", spend: 83.03 },
{ label: "2021-02-13 - 2021-02-22", spend: 100.22 },
{ label: "2021-02-23 - 2021-03-04", spend: 94.92 },
{ label: "2021-03-05 - 2021-03-08", spend: 16.57 },
]
// Actual total is 294.77
// Output total is 294.74
希望就实现此目标的正确方法提出建议。 Tnx.
一种方法如下:
// your own original data, assigned as a variable using 'const' since I don't
// expect it to change during the course of the script:
const myMonthlySpend = [{
label: "2021-02-03",
spend: 4.95
},
{
label: "2021-02-04",
spend: 15.96
},
{
label: "2021-02-05",
spend: 11
},
{
label: "2021-02-06",
spend: 10.07
},
{
label: "2021-02-07",
spend: 6.83
},
{
label: "2021-02-08",
spend: 4.85
},
{
label: "2021-02-09",
spend: 5.01
},
{
label: "2021-02-10",
spend: 5.09
},
{
label: "2021-02-11",
spend: 9.1
},
{
label: "2021-02-12",
spend: 10.18
},
{
label: "2021-02-13",
spend: 10.17
},
{
label: "2021-02-14",
spend: 10.16
},
{
label: "2021-02-15",
spend: 10.07
},
{
label: "2021-02-16",
spend: 9.94
},
{
label: "2021-02-17",
spend: 9.76
},
{
label: "2021-02-18",
spend: 10.09
},
{
label: "2021-02-19",
spend: 10.05
},
{
label: "2021-02-20",
spend: 9.93
},
{
label: "2021-02-21",
spend: 9.8
},
{
label: "2021-02-22",
spend: 10.26
},
{
label: "2021-02-23",
spend: 10.03
},
{
label: "2021-02-24",
spend: 10.09
},
{
label: "2021-02-25",
spend: 10.09
},
{
label: "2021-02-26",
spend: 9.95
},
{
label: "2021-02-27",
spend: 9.78
},
{
label: "2021-02-28",
spend: 9.77
},
{
label: "2021-03-01",
spend: 10.11
},
{
label: "2021-03-02",
spend: 10.04
},
{
label: "2021-03-03",
spend: 10.01
},
{
label: "2021-03-04",
spend: 5.06
},
{
label: "2021-03-05",
spend: 4.72
},
{
label: "2021-03-06",
spend: 5.36
},
{
label: "2021-03-07",
spend: 4.98
},
{
label: "2021-03-08",
spend: 1.51
}
],
// a named function which takes two arguments:
// 1. expenses, an Array of Objects representing your expenditures, and
// 2. nSize, an Integer to define the size of the 'groups' you wish to
// sum.
// This function is defined using Arrow syntax since we have no specific
// need to use 'this' within the function:
expenseGroups = (expenses, nSize = 7) => {
// we use an Array literal with spread syntax to make a copy of
// the Array of Objects, in order to avoid operating upon the
// original Array:
let haystack = [...expenses],
// initialising an Array:
chunks = [];
// here, while they haystack has a non-zero length:
while (haystack.length) {
// we use Array.prototype.splice() to both remove the identified
// Array-elements from the Array (each time reducing the length
// of the Array), which returns the removed-elements to the calling-
// context; it's worth explaining that we take a 'slice' from the
// haystack Array, from index 0 (the first Array-element) up until
// but not including the index of nSize). Array.prototype.splice()
// modifies the original Array, which is why we had to use a copy
// and not the original itself. Once we have the 'slice' of the
// Array, that slice is then pushed into the 'chunks' array using
// Array.prototype.push():
chunks.push(haystack.splice(0, nSize));
}
// here use - and return the results of - Array.prototype.map(),
// which returns a new Array based on what we do with each
// Array-element as we iterate over that Array:
return chunks.map(
// 'chunk' is the first of three variabls available to
// Array.prototype.map(), and represents the current
// Array-element of the Array over which we're iterating:
(chunk) => {
// here we return an Object:
return {
// the property 'label' holds a value that is formed using a
// template-literal (delimited with back-ticks) in which
// JavaScript functions/expressions can be interpolated so
// long as they're within a sequence beginning with '${' and
// ending with '}'; here we take the read the label property-value
// from the zeroth (first) element in the chunk Array, and then
// we read the 'label' property from the last Array-element of
// the chunk Array:
'label': `${chunk[0].label} - ${chunk[chunk.length - 1].label}`,
// because we're representing a currency, I chose to use
// Intl.NumberFormat(), which should theoretically style
// the currency according to the locale in which it's used
// (as determined by the browser or OS):
'spend': new Intl.NumberFormat({
// using a currency style:
style: 'currency'
// applying the formatting to the number that results from:
}).format(
// here we use Array.prototype.reduce(), which iterates over
// an Array, and performs some function upon that Array;
// we use two of the arguments available to that function:
// 1. 'acc' which represents the 'accumulator' (or the current
// value that the function has produced,
// 2. 'curr' which represents the current array-element of the
// Array over which we're iterating, and upon which we're
// we're working:
chunk.reduce((acc, curr) => {
// here we take the accumulator, and add to it the value held
// in the current Array-element's 'spend' property-value:
return acc + curr.spend
// we initialise the default/starting value of the accumulator
// to 0:
}, 0)
)
}
});
};
console.log(expenseGroups(myMonthlySpend, 10));
/*
{
label: 2021-02-03 - 2021-02-12,
spend: 83.04
},
{
label: 2021-02-13 - 2021-02-22,
spend: 100.23
},
{
label: 2021-02-23 - 2021-03-04,
spend: 94.93
},
{
label: 2021-03-05 - 2021-03-08,
spend: 16.57
}
*/
参考文献:
这是一个不依赖数组数据一天又一天升序排序的实现。
function groupItems(rawData, groupEvery) {
// Get date boundaries
let firstDate = new Date(rawData[0].label);
let earliestTimestamp = rawData.reduce((a, b) => Math.min(a, new Date(b.label)), firstDate);
let earliestDate = new Date(earliestTimestamp);
let latestTimestamp = rawData.reduce((a, b) => Math.max(a, new Date(b.label)), firstDate);
let latestDate = new Date(latestTimestamp);
// Create an array of objects which contain start and end dates that represent the
// values by which we find the corresponding array index.
let groupItemsArray = [];
for (let currentStartDate = new Date(earliestDate);
currentStartDate <= latestDate;
currentStartDate.setDate(currentStartDate.getDate() + groupEvery)) {
let currentEndDate = new Date(currentStartDate);
currentEndDate.setDate(currentEndDate.getDate() + groupEvery - 1);
groupItemsArray.push({
startDate: new Date(currentStartDate),
endDate: currentEndDate > latestDate ? new Date(latestDate) : currentEndDate,
spend: 0
});
}
// For each date append value of spend to an existing item in groupItemsArray
// which obeys internal date range for the given date
for (let {label, spend} of rawData) {
let labelDate = new Date(label);
let index = groupItemsArray.findIndex(groupItem => groupItem.startDate <= labelDate
&& labelDate <= groupItem.endDate);
groupItemsArray[index].spend += spend;
}
// Map results to comply desired output format
return groupItemsArray.map(item => {
let startDateFormatted = item.startDate.toISOString().split('T')[0];
let endDateFormatted = item.endDate.toISOString().split('T')[0];
return {
label: startDateFormatted + " - " + endDateFormatted,
spend: Number(item.spend.toFixed(2))
}
});
}
假设以下对象数组,每个对象包含一个 label
属性(表示日期的字符串)和一个 spend
属性(包含一个数字) :
myMonthlySpend = [
{ label: "2021-02-03", spend: 4.95 },
{ label: "2021-02-04", spend: 15.96 },
{ label: "2021-02-05", spend: 11 },
{ label: "2021-02-06", spend: 10.07 },
{ label: "2021-02-07", spend: 6.83 },
{ label: "2021-02-08", spend: 4.85 }
];
现在,项目的数量是不固定的,可以从 1 到无穷大。无论数组大小如何,假设我想将每个 X
项聚类到一个包含的对象中,再次:
label
属性 由 first 的原始label
和 last 组成组项目spent
属性 表示该组所有项目的总和spend
属性
并以数组形式返回这些对象,如下所示:
myGroupedSpend = [
....
{label: 'date from - date to', spend: 'total spend for these items'},
{label: 'date from - date to', spend: 'total spend for these items'}
...
]
我尝试执行以下操作,但由于以下原因,输出显然是错误的:
- 总支出金额不同,与所有项目的实际总金额不符
- 结果标签(意思是,'date from - date to')不一致
myMonthlySpend = [
{ label: "2021-02-03", spend: 4.95 },
{ label: "2021-02-04", spend: 15.96 },
{ label: "2021-02-05", spend: 11 },
{ label: "2021-02-06", spend: 10.07 },
{ label: "2021-02-07", spend: 6.83 },
{ label: "2021-02-08", spend: 4.85 },
{ label: "2021-02-09", spend: 5.01 },
{ label: "2021-02-10", spend: 5.09 },
{ label: "2021-02-11", spend: 9.1 },
{ label: "2021-02-12", spend: 10.18 },
{ label: "2021-02-13", spend: 10.17 },
{ label: "2021-02-14", spend: 10.16 },
{ label: "2021-02-15", spend: 10.07 },
{ label: "2021-02-16", spend: 9.94 },
{ label: "2021-02-17", spend: 9.76 },
{ label: "2021-02-18", spend: 10.09 },
{ label: "2021-02-19", spend: 10.05 },
{ label: "2021-02-20", spend: 9.93 },
{ label: "2021-02-21", spend: 9.8 },
{ label: "2021-02-22", spend: 10.26 },
{ label: "2021-02-23", spend: 10.03 },
{ label: "2021-02-24", spend: 10.09 },
{ label: "2021-02-25", spend: 10.09 },
{ label: "2021-02-26", spend: 9.95 },
{ label: "2021-02-27", spend: 9.78 },
{ label: "2021-02-28", spend: 9.77 },
{ label: "2021-03-01", spend: 10.11 },
{ label: "2021-03-02", spend: 10.04 },
{ label: "2021-03-03", spend: 10.01 },
{ label: "2021-03-04", spend: 5.06 },
{ label: "2021-03-05", spend: 4.72 },
{ label: "2021-03-06", spend: 5.36 },
{ label: "2021-03-07", spend: 4.98 },
{ label: "2021-03-08", spend: 1.51 }
];
// What is the actual total of all items.spend
let totalSpend = 0;
myMonthlySpend.forEach(v => totalSpend += v.spend);
// Grouping function
function groupItems(rawData, groupEvery) {
let allGroups = [];
let currentSpend = 0;
for (let i = 0, j = 0; i < rawData.length; i++) {
currentSpend += rawData[i].spend;
if (i >= groupEvery && i % groupEvery === 0) {
let currentLabel = rawData[i - groupEvery].label + ' - ' + rawData[i].label;
j++;
allGroups[j] = allGroups[j] || {};
allGroups[j] = {'label': currentLabel, 'spend': currentSpend};
currentSpend = 0;
} else if (i < groupEvery && i % groupEvery === 0) {
let currentLabel = rawData[0].label + ' - ' + rawData[groupEvery].label;
allGroups[j] = {'label': currentLabel, 'spend': currentSpend};
}
}
let checkTotal = 0;
allGroups.forEach(v => checkTotal += v.spend);
console.log('Total spend when grouped by', groupEvery, 'is', checkTotal,
'\nwhile the actual total should be', totalSpend);
return allGroups;
}
// Trying with grouping by 5 and by 9
console.log(groupItems(myMonthlySpend, 5))
console.log('\n\n');
console.log(groupItems(myMonthlySpend, 9))
.as-console-wrapper { max-height: 100% !important; top: 0; }
例如,如果以 10 作为 groupEvery 编号调用该函数,输出将是:
correctOutput = [
{ label: "2021-02-03 - 2021-02-12", spend: 83.03 },
{ label: "2021-02-13 - 2021-02-22", spend: 100.22 },
{ label: "2021-02-23 - 2021-03-04", spend: 94.92 },
{ label: "2021-03-05 - 2021-03-08", spend: 16.57 },
]
// Actual total is 294.77
// Output total is 294.74
希望就实现此目标的正确方法提出建议。 Tnx.
一种方法如下:
// your own original data, assigned as a variable using 'const' since I don't
// expect it to change during the course of the script:
const myMonthlySpend = [{
label: "2021-02-03",
spend: 4.95
},
{
label: "2021-02-04",
spend: 15.96
},
{
label: "2021-02-05",
spend: 11
},
{
label: "2021-02-06",
spend: 10.07
},
{
label: "2021-02-07",
spend: 6.83
},
{
label: "2021-02-08",
spend: 4.85
},
{
label: "2021-02-09",
spend: 5.01
},
{
label: "2021-02-10",
spend: 5.09
},
{
label: "2021-02-11",
spend: 9.1
},
{
label: "2021-02-12",
spend: 10.18
},
{
label: "2021-02-13",
spend: 10.17
},
{
label: "2021-02-14",
spend: 10.16
},
{
label: "2021-02-15",
spend: 10.07
},
{
label: "2021-02-16",
spend: 9.94
},
{
label: "2021-02-17",
spend: 9.76
},
{
label: "2021-02-18",
spend: 10.09
},
{
label: "2021-02-19",
spend: 10.05
},
{
label: "2021-02-20",
spend: 9.93
},
{
label: "2021-02-21",
spend: 9.8
},
{
label: "2021-02-22",
spend: 10.26
},
{
label: "2021-02-23",
spend: 10.03
},
{
label: "2021-02-24",
spend: 10.09
},
{
label: "2021-02-25",
spend: 10.09
},
{
label: "2021-02-26",
spend: 9.95
},
{
label: "2021-02-27",
spend: 9.78
},
{
label: "2021-02-28",
spend: 9.77
},
{
label: "2021-03-01",
spend: 10.11
},
{
label: "2021-03-02",
spend: 10.04
},
{
label: "2021-03-03",
spend: 10.01
},
{
label: "2021-03-04",
spend: 5.06
},
{
label: "2021-03-05",
spend: 4.72
},
{
label: "2021-03-06",
spend: 5.36
},
{
label: "2021-03-07",
spend: 4.98
},
{
label: "2021-03-08",
spend: 1.51
}
],
// a named function which takes two arguments:
// 1. expenses, an Array of Objects representing your expenditures, and
// 2. nSize, an Integer to define the size of the 'groups' you wish to
// sum.
// This function is defined using Arrow syntax since we have no specific
// need to use 'this' within the function:
expenseGroups = (expenses, nSize = 7) => {
// we use an Array literal with spread syntax to make a copy of
// the Array of Objects, in order to avoid operating upon the
// original Array:
let haystack = [...expenses],
// initialising an Array:
chunks = [];
// here, while they haystack has a non-zero length:
while (haystack.length) {
// we use Array.prototype.splice() to both remove the identified
// Array-elements from the Array (each time reducing the length
// of the Array), which returns the removed-elements to the calling-
// context; it's worth explaining that we take a 'slice' from the
// haystack Array, from index 0 (the first Array-element) up until
// but not including the index of nSize). Array.prototype.splice()
// modifies the original Array, which is why we had to use a copy
// and not the original itself. Once we have the 'slice' of the
// Array, that slice is then pushed into the 'chunks' array using
// Array.prototype.push():
chunks.push(haystack.splice(0, nSize));
}
// here use - and return the results of - Array.prototype.map(),
// which returns a new Array based on what we do with each
// Array-element as we iterate over that Array:
return chunks.map(
// 'chunk' is the first of three variabls available to
// Array.prototype.map(), and represents the current
// Array-element of the Array over which we're iterating:
(chunk) => {
// here we return an Object:
return {
// the property 'label' holds a value that is formed using a
// template-literal (delimited with back-ticks) in which
// JavaScript functions/expressions can be interpolated so
// long as they're within a sequence beginning with '${' and
// ending with '}'; here we take the read the label property-value
// from the zeroth (first) element in the chunk Array, and then
// we read the 'label' property from the last Array-element of
// the chunk Array:
'label': `${chunk[0].label} - ${chunk[chunk.length - 1].label}`,
// because we're representing a currency, I chose to use
// Intl.NumberFormat(), which should theoretically style
// the currency according to the locale in which it's used
// (as determined by the browser or OS):
'spend': new Intl.NumberFormat({
// using a currency style:
style: 'currency'
// applying the formatting to the number that results from:
}).format(
// here we use Array.prototype.reduce(), which iterates over
// an Array, and performs some function upon that Array;
// we use two of the arguments available to that function:
// 1. 'acc' which represents the 'accumulator' (or the current
// value that the function has produced,
// 2. 'curr' which represents the current array-element of the
// Array over which we're iterating, and upon which we're
// we're working:
chunk.reduce((acc, curr) => {
// here we take the accumulator, and add to it the value held
// in the current Array-element's 'spend' property-value:
return acc + curr.spend
// we initialise the default/starting value of the accumulator
// to 0:
}, 0)
)
}
});
};
console.log(expenseGroups(myMonthlySpend, 10));
/*
{
label: 2021-02-03 - 2021-02-12,
spend: 83.04
},
{
label: 2021-02-13 - 2021-02-22,
spend: 100.23
},
{
label: 2021-02-23 - 2021-03-04,
spend: 94.93
},
{
label: 2021-03-05 - 2021-03-08,
spend: 16.57
}
*/
参考文献:
这是一个不依赖数组数据一天又一天升序排序的实现。
function groupItems(rawData, groupEvery) {
// Get date boundaries
let firstDate = new Date(rawData[0].label);
let earliestTimestamp = rawData.reduce((a, b) => Math.min(a, new Date(b.label)), firstDate);
let earliestDate = new Date(earliestTimestamp);
let latestTimestamp = rawData.reduce((a, b) => Math.max(a, new Date(b.label)), firstDate);
let latestDate = new Date(latestTimestamp);
// Create an array of objects which contain start and end dates that represent the
// values by which we find the corresponding array index.
let groupItemsArray = [];
for (let currentStartDate = new Date(earliestDate);
currentStartDate <= latestDate;
currentStartDate.setDate(currentStartDate.getDate() + groupEvery)) {
let currentEndDate = new Date(currentStartDate);
currentEndDate.setDate(currentEndDate.getDate() + groupEvery - 1);
groupItemsArray.push({
startDate: new Date(currentStartDate),
endDate: currentEndDate > latestDate ? new Date(latestDate) : currentEndDate,
spend: 0
});
}
// For each date append value of spend to an existing item in groupItemsArray
// which obeys internal date range for the given date
for (let {label, spend} of rawData) {
let labelDate = new Date(label);
let index = groupItemsArray.findIndex(groupItem => groupItem.startDate <= labelDate
&& labelDate <= groupItem.endDate);
groupItemsArray[index].spend += spend;
}
// Map results to comply desired output format
return groupItemsArray.map(item => {
let startDateFormatted = item.startDate.toISOString().split('T')[0];
let endDateFormatted = item.endDate.toISOString().split('T')[0];
return {
label: startDateFormatted + " - " + endDateFormatted,
spend: Number(item.spend.toFixed(2))
}
});
}