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('nn');
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.

Answer

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:

Leave a Reply

Your email address will not be published. Required fields are marked *