I need to merge the objects together. The resource
property is what determines if the objects can be merged. To determine where the hours
property valu
One technique is to loop over each member, find the first one with that resource, update the totals in it, and then filter so as to retain only the first occurrences.
members.filter(member => {
const first = members.find(m => m.resource === member.resource);
if (member.billable) first.totalBillableHours += member.hours;
else first.totalNonBillableHours += member.hours;
first.totalHours += member.hours.
return first === member;
});
A more orthodox approach would be to group the objects by resource, creating an array of objects for each resource, and then transform that into your desired output, as in
totals(groupBy(members, 'resource'))
groupBy
would be defined as producing something of the form:
{
resource1: [obj, obj],
resource2: [obj, obj]
}
To take totals
first, that would be
function totals(groups) {
const hours = m => m.hours;
const billable = m => m.billable;
const not = f => x => !f(x);
return Object.keys(groups).map(resource => {
const members = groups[resource];
const totalHours = sum(members.map(hours));
const billableHours = sum(members.filter(billable).map(hours));
const nonBillableHours = sum(members.filter(not(billable)).map(hours));
return {resource, totalHours, billableHours, nonBillableHours};
});
}
sum
can be written as
const sum = arr => arr.reduce((a, b) => a + b, 0);
There are many implementations of groupBy
out there, including ones provided by libraries such as underscore. Here's a real simple version:
function groupBy(arr, prop) {
return arr.reduce((result, obj) {
const key = obj[prop];
if (!result[key]) result[key] = [];
result[key].push(obj);
return result;
}, {});
}
The problem is you're relying on the adjacent record of each record. Instead, you could keep a tally of each member
by their resource
. Since resource
is unique, you can use it as a property of an object that keeps the tally -- think of it like a key. Then you can add the number of hours in each record to the appropriate object.
Here's my attempt: http://codepen.io/anon/pen/bBYyPa
var members = [
{billable: true, hours: 15, name: "Joe Smith", resource: "00530000003mgYGAAY", totalBillableHours: 0, totalHours: 0, totalNonBillableHours: 0},
{billable: true, hours: 5, name: "Joe Smith", resource: "00530000003mgYGAAY", totalBillableHours: 0, totalHours: 0, totalNonBillableHours: 0},
{billable: false, hours: 5, name: "Joe Smith", resource: "00530000003mgYGAAY", totalBillableHours: 0, totalHours: 0, totalNonBillableHours: 0},
{billable: false, hours: 5, name: "Jan Smith", resource: "00530000003mgYTAAY", totalBillableHours: 0, totalHours: 0, totalNonBillableHours: 0},
{billable: true, hours: 12, name: "Jan Smith", resource: "00530000003mgYTAAY", totalBillableHours: 0, totalHours: 0, totalNonBillableHours: 0},
{billable: true, hours: 2, name: "Jam Smith", resource: "00530000003mgYTAAY", totalBillableHours: 0, totalHours: 0, totalNonBillableHours: 0}
];
var membersObj = {};
for (i = 0; i < members.length; i++) {
var member = members[i];
if (!membersObj[member.resource]){
membersObj[member.resource] = members[i];
}
if(member.billable){
membersObj[member.resource].totalBillableHours += member.hours;
} else {
membersObj[member.resource].totalNonBillableHours += member.hours;
}
membersObj[member.resource].totalHours += member.hours;
}
console.log(membersObj);
Of course, this gives you back an object instead of an array, but that can be converted if necessary.
Here is the output:
{
'00530000003mgYGAAY':
{ billable: true,
hours: 15,
name: 'Joe Smith',
resource: '00530000003mgYGAAY',
totalBillableHours: 20,
totalHours: 25,
totalNonBillableHours: 5 },
'00530000003mgYTAAY':
{ billable: false,
hours: 5,
name: 'Jan Smith',
resource: '00530000003mgYTAAY',
totalBillableHours: 14,
totalHours: 19,
totalNonBillableHours: 5 }
}
It gets pretty tricky when you remove items from an array while iterating over it.
I've rewritten your solution in a more functional way here: http://codepen.io/tinacious/pen/gLXJow?editors=1011
var members = [
{billable: true, hours: 15, name: "Joe Smith", resource: "00530000003mgYGAAY", totalBillableHours: 0, totalHours: 0, totalNonBillableHours: 0},
{billable: true, hours: 5, name: "Joe Smith", resource: "00530000003mgYGAAY", totalBillableHours: 0, totalHours: 0, totalNonBillableHours: 0},
{billable: false, hours: 5, name: "Joe Smith", resource: "00530000003mgYGAAY", totalBillableHours: 0, totalHours: 0, totalNonBillableHours: 0},
{billable: false, hours: 5, name: "Jan Smith", resource: "00530000003mgYTAAY", totalBillableHours: 0, totalHours: 0, totalNonBillableHours: 0},
{billable: true, hours: 12, name: "Jan Smith", resource: "00530000003mgYTAAY", totalBillableHours: 0, totalHours: 0, totalNonBillableHours: 0},
{billable: true, hours: 2, name: "Jam Smith", resource: "00530000003mgYTAAY", totalBillableHours: 0, totalHours: 0, totalNonBillableHours: 0}
];
function combineMembers(members) {
var combinedMembers = {};
members.forEach(function (member) {
var resourceId = member.resource;
var typeOfHour = member.billable ? 'totalBillableHours' : 'totalNonBillableHours';
if (!combinedMembers[resourceId]) {
combinedMembers[resourceId] = Object.assign({}, member);
}
combinedMembers[resourceId][typeOfHour] += member.hours;
combinedMembers[resourceId].totalHours += member.hours;
});
return Object.keys(combinedMembers).map(function (resourceId) {
return combinedMembers[resourceId];
});
}
console.log(combineMembers(members));
The resulting output is what you are looking for:
Array[2]
0 : Object
billable : true
hours : 15
name : "Joe Smith"
resource : "00530000003mgYGAAY"
totalBillableHours : 20
totalHours : 25
totalNonBillableHours : 5
__proto__ : Object
1 : Object
billable : false
hours : 5
name : "Jan Smith"
resource : "00530000003mgYTAAY"
totalBillableHours : 14
totalHours : 19
totalNonBillableHours : 5
__proto__ : Object
length : 2
__proto__ : Array[0]
Here's a version using the Ramda library (disclaimer: I'm one of the authors):
const process = pipe(
groupBy(prop('resource')),
values,
map(group => reduce((totals, member) => ({
name: member.name,
resource: member.resource,
totalHours: totals.totalHours + member.hours,
totalBillableHours: totals.totalBillableHours +
(member.billable ? member.hours : 0),
totalNonBillableHours: totals.totalNonBillableHours +
(member.billable ? 0 : member.hours)
}), head(group), group))
);
With this,
process(members)
yields
[
{
name: "Joe Smith",
resource: "00530000003mgYGAAY",
totalBillableHours: 20,
totalHours: 25,
totalNonBillableHours: 5
},
{
name: "Jam Smith",
resource: "00530000003mgYTAAY",
totalBillableHours: 14,
totalHours: 19,
totalNonBillableHours: 5
}
]
This works in two stages. First it collects the like values (using groupBy
) and extracts the results as an array (using values
).
Then it maps over the resulting list of groups, reducing each one to a single value by combining the fields as appropriate.
This might not be much of a help to you, as pulling in a library like Ramda for just one task is probably a ridiculous idea. But you might take inspiration in the breakdown of the problem.
Most of the Ramda functions used here are easy to create on your own and are quite useful for many purposes.
You can see this in action on the Ramda REPL.