简体   繁体   中英

Group items in JS array of objects X items at a time, and sum up the values of each group

Assume the following array of objects, each comprising a label property (a string representing a date) and a spend property (that holds a number):

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

Now, the number of items is not fixed and can range from 1 to infinity. Regardless of the array size, say I want to cluster every X items into an object that holds, again:

  1. label property comprised of the original label of the first and the last group items
  2. spent property representing the total of all of the group's items spend properties

and to have these objects returned in an array, like so:

myGroupedSpend = [
  ....
  {label: 'date from - date to', spend: 'total spend for these items'},
  {label: 'date from - date to', spend: 'total spend for these items'}
  ...
]

I tried doing the following, but the output is obviously wrong due to:

  1. The summed amounts of spend vary and do not match the actual total of all items
  2. The resulted labels (meaning, 'date from - date to'), are not consistent

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

For example, should the function be invoked with 10 as the groupEvery number, the output would be:

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

Would appreciate a suggestion on the proper way to achieve this. Tnx.

One approach is as follows:

 // 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 } */

JS Fiddle demo .

References:

Here is an implementation that does not rely on array data being ascendingly sorted one day after another.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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