将 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 项聚类到一个包含的对象中,再次:

  1. label 属性 由 first 的原始 labellast 组成组项目
  2. 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'}
  ...
]

我尝试执行以下操作,但由于以下原因,输出显然是错误的:

  1. 总支出金额不同,与所有项目的实际总金额不符
  2. 结果标签(意思是,'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
}
*/

JS Fiddle demo.

参考文献:

这是一个不依赖数组数据一天又一天升序排序的实现。

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