Home > Software design >  Filtering an Array to return grouped totals
Filtering an Array to return grouped totals

Time:09-28

Is there an efficient way to take a large Array of Objects and return the totals on certain properties, whilst still retaining some of the orginal data?

In our example we have an Array similar to the following:

[
  {
    Name: 'Bob',
    Month: January 2021,
    WorkCode: 'Fee1'
    Hours: 20,
    Salary: 400.00,
    ...
  },
  {
    Name: Bob',
    Month: January 2021,
    WorkCode: 'Fee2'
    Hours: 10,
    Salary: 200.00,
    ...
  },
  {
    Name: 'Bob',
    Month: January 2021,
    WorkCode: 'Fee3'
    Hours: 5,
    Salary: 100.00,
    ... 
  },
  {
    Name: 'Bob',
    Month: February 2021,
    WorkCode: 'Fee1'
    Hours: 15,
    Salary: 300.00,
    ...
  },
  {
    Name: 'Alice',
    Month: January 2021,
    WorkCode: 'Fee1'
    Hours: 10,
    Salary: 300.00,
    ...
  }
  ...
]

I want to be able to return each person in the array with the sum of their hours and salary up to a selected date (assume that the Months in the example are Date objects).

At the moment I am using Array.filter() and Array.some() to give me a subset of the original data that contains all the raw data that I need.

let weekRows = data.filter((currentRow) => {
  return currentRow.Month <= maxDate
})
if (this.workCodeFilter.length !== 0) {
  weekRows = weekRows.filter((currentRow) => {
    return this.workTypeFilter.some((f) => {
      return f.WorkCode === currentRow.WorkCode
    })
  })
}
if (this.peopleFilter.length !== 0) {
  weekRows = weekRows.filter((currentRow) => {
    return this.peopleFilter.some((f) => {
      return f.psRef === currentRow.ps_ref
      })
    })
}

I then pass the weekRows data to a for loop which uses findIndexByKeyValue to lookup the Person and the WorkCode in a result array, and either add a new entry if its the first instance, or increment the totals for Hours and Salary if they already have an entry. This last stage is very slow

The end result should be totals for Salary and Hours, per person, up to the required Date. It should also retain the additional data represented by the ...

Filters should be possible for Date (always), and optionally WorkCode and/or Person. Both WorkCode and Person can have multiple entries, hence Array.some()

Am I right that Array.reduce() should be able to do what I am looking for, whilst also replacing the Array.filter() and Array.some() calls

CodePudding user response:

If you seek for Array.prototype.reduce(), you may leverage that together with Map:

  • build up Map, having Name as a key and corresponding group total object as value (with date criteria as a bail out condition);
  • extract array of group totals with Map.prototype.values()

E.g.

const srcData = [{Name:"Bob",Month:"January 2021",WorkCode:"Fee1",Hours:20,Salary:400},{Name:"Bob",Month:"January 2021",WorkCode:"Fee2",Hours:10,Salary:200},{Name:"Bob",Month:"January 2021",WorkCode:"Fee3",Hours:5,Salary:100},{Name:"Bob",Month:"February 2021",WorkCode:"Fee1",Hours:15,Salary:300},{Name:"Alice",Month:"January 2021",WorkCode:"Fee1",Hours:10,Salary:300}],
      strToDate = str => {
        const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'November', 'December'],
              [month, year] = str.split(' ')
              return new Date(
                year,
                months.indexOf(month),
                1
              )
      }



      groupTotalTillDate = (src, maxDateStr) => [
        ...src
          .reduce((acc,{Name, Hours, Salary, Month}) => {
            if(strToDate(Month)>=strToDate(maxDateStr)) return acc
            const group = acc.get(Name)
            if(group){
              group.Hours  = Hours
              group.Salary  = Salary
            } else {
              acc.set(Name, {Name, Hours, Salary})
            }

           return acc
          }, new Map)
          .values()
      ]

console.log(groupTotalTillDate(srcData, 'February 2021'))
.as-console-wrapper{min-height:100%}

CodePudding user response:

Hope this is what you are looking for.

const data = [
  {
    Name: "Bob",
    Month: new Date("01 January 2020"),
    WorkCode: "Fee1",
    Hours: 20,
    Salary: 400.0,
  },
  {
    Name: "Bob",
    Month: new Date("01 January 2021"),
    WorkCode: "Fee2",
    Hours: 10,
    Salary: 200.0,
  },
  {
    Name: "Bob",
    Month: new Date("01 February 2021"),
    WorkCode: "Fee3",
    Hours: 5,
    Salary: 100.0,
  },
  {
    Name: "Bob",
    Month: new Date("01 March 2021"),
    WorkCode: "Fee1",
    Hours: 15,
    Salary: 300.0,
  },
  {
    Name: "Alice",
    Month: new Date("01 January 2021"),
    WorkCode: "Fee1",
    Hours: 10,
    Salary: 300.0,
  },
];

const getTotals = (data, maxDate) => {
  const isEmployeeAddedInAcc = (acc, item) =>
    acc.find((emp) => emp.Name === item.Name);

  const updateDetails = (item) => (employee) =>
    employee.Name === item.Name
      ? {
          ...employee,
          Salary: employee.Salary   item.Salary,
          Hours: employee.Hours   item.Hours,
        }
      : item;

  const salaryReducer = (acc, item) =>
    isEmployeeAddedInAcc(acc,item)
      ? acc.map(updateDetails(item))
      : [...acc, { Name: item.Name, Hours: item.Hours, Salary: item.Salary }];

  return data.filter((item) => item.Month < maxDate).reduce(salaryReducer, []);
};

console.log(getTotals(data, new Date("27 February 2021")));

CodePudding user response:

You could filter first and then take the accumulation of wanted values.

const
    data = [{ Name: 'Bob', Month: '2021-01', WorkCode: 'Fee1', Hours: 20, Salary: 400 }, { Name: 'Bob', Month: '2021-01', WorkCode: 'Fee2', Hours: 10, Salary: 200 }, { Name: 'Bob', Month: '2021-01', WorkCode: 'Fee3', Hours: 5, Salary: 100 }, { Name: 'Bob', Month: '2021-02', WorkCode: 'Fee1', Hours: 15, Salary: 300 }, { Name: 'Alice', Month: '2021-01', WorkCode: 'Fee1', Hours: 10, Salary: 300 }],
    keys = ['Hours', 'Salary'],
    zeroProps = Object.fromEntries(keys.map(k => [k, 0])),
    result = data
        .filter(({ Month }) => Month >= '2021-01')
        .reduce((hash => (r, { Name, ...o }) => {
            if (!hash[Name]) r.push(hash[Name] = { Name, ...zeroProps });
            keys.forEach(k => hash[Name][k]  = o[k]);
            return r;
        })({}), []);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

CodePudding user response:

Use the Array#reduce() method to eliminate elements that have a month greater than the specified one; I would use the format YYYY-MM. Something like:

let fhours = hours.filter(h => h.Month < Month);

Then in the following demo you would replace the variable hours for fhours to get the summary of the filtered hours.

let hours = [{
        Name: 'Bob',
        Month: '2021-01',
        WorkCode: 'Fee1',
        Hours: 20,
        Salary: 400.00
    },
    {
        Name: 'Bob',
        Month: '2021-01',
        WorkCode: 'Fee2',
        Hours: 10,
        Salary: 200.00
    },
    {
        Name: 'Bob',
        Month: '2021-01',
        WorkCode: 'Fee3',
        Hours: 5,
        Salary: 100.00
    },
    {
        Name: 'Bob',
        Month: '2021-02',
        WorkCode: 'Fee1',
        Hours: 15,
        Salary: 300.00
    },
    {
        Name: 'Alice',
        Month: '2021-01',
        WorkCode: 'Fee1',
        Hours: 10,
        Salary: 300.00
    }
];
console.log(hours.length);
console.log(hours.filter(h => h.Month < '2021-02').length);

let summary = hours.reduce((ac, cu) => ({
    ...ac,
    [cu.Name]: {
        Hours: ac[cu.Name].Hours   cu.Hours,
        Salary: ac[cu.Name].Salary   cu.Salary
    }
}), [...new Set(hours.map(h => h.Name))].reduce((a, c) => ({
    ...a,
    [c]: {
        Hours: 0,
        Salary: 0
    }
}), {}));

console.log(summary);

  • Related