-1

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 values goes, the billable property is used. I want the array to look like this

members = [{billable: true, hours: 15, name: "Joe Smith", resource: "00530000003mgYGAAY", totalBillableHours: 20, totalHours: 25, totalNonBillableHours: 5},
{billable: false, hours: 5, name: "Jan Smith", resource: "00530000003mgYTAAY", totalBillableHours: 14, totalHours: 19, totalNonBillableHours: 10}]

The state of billable and hours properties for the final result is irrelevant.

Here's the code. Can't seem to figure out why it wont work.

http://codepen.io/anon/pen/MbOdmV?editors=0002

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: "Jan Smith", resource: "00530000003mgYTAAY", totalBillableHours: 0, totalHours: 0, totalNonBillableHours: 0}  
];

for (i = 0; i < members.length; i++) {
  var member = members[i];

  if (member.resource == members[i + 1].resource) {

    if(member.billable == true) {
      member.totalBillableHours += members[i + 1].hours;                     
    }
    else {
      member.totalNonBillableHours += members[i + 1].hours;
    }
    member.totalHours += members[i + 1].hours;
    members.splice(i + 1, 1);
    --i;
    if (members[i + 2] == undefined) {
      break;
    }           

  }
}
console.log(members);
Tyler Zika
  • 1,144
  • 2
  • 14
  • 25
  • This is awfully awkward... are you just wondering how to go about adding up all the billable hours for each resource? What is the question here? – Brad Evans Dec 01 '16 at 02:58
  • @BradEvans It's more then that. I need to have two separate objects to display on my angular app. – Tyler Zika Dec 01 '16 at 02:59
  • Walk through your code in your head, or on a piece of paper, or on a blackboard, or find someone and talk through the code, or user your debugger to walk through it. You should find your problem(s) soon enough. –  Dec 01 '16 at 03:18
  • @torazaburo trust me, I wouldn't post a question unless I've been working on it for hours. I'm lost. – Tyler Zika Dec 01 '16 at 03:21
  • I agree that this solution is awkward. See any of the answers below. The common theme is to *not* try to do this in place in your array. That's what makes this so hard to comprehend. – Scott Sauyet Dec 01 '16 at 03:35

4 Answers4

2

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]
Tina
  • 1,186
  • 10
  • 11
  • I appreciate your attempt, but this isn't what I'm looking for. I need the property fields `totalBillableHours`, `totalHours`, and `totalNonBillableHours` updated. – Tyler Zika Dec 01 '16 at 03:09
  • @TylerZika I have updated the answer to consider the billable property and output the results you are looking for. – Tina Dec 01 '16 at 03:26
2

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 } 
}
fredrover
  • 2,997
  • 2
  • 17
  • 24
1

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;
  }, {});
}
  • This is a horrible abuse of `filter`, but I've got to admit it leads to very simple code. – Scott Sauyet Dec 01 '16 at 03:39
  • The update is less abusive, but sadly less elegant too! ;-) I would probably choose it, though, over the other answers here. (Well, if I was already using Ramda, I'd choose my answer below.) And depending on how far one wanted to go with reusable parts, `hours` might be written in terms of a more generic `prop`/`get`/`property` function. and `map(hours)` might be `pluck('hours')`, etc. ... and pretty soon you're building a library! – Scott Sauyet Dec 01 '16 at 12:48
1

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.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • Sorry to write you here... I found your profile as top users for ramdajs, so I would like to ask you if you could have the time to have a quick look at my question, I would really appreciate your feedback. Thanks in advance for your expertises http://stackoverflow.com/questions/40946787/converting-imperative-to-functional-style-paradigm – GibboK Dec 04 '16 at 12:18
  • @GibboK: I have played with it a bit. I'll respond there. – Scott Sauyet Dec 07 '16 at 12:43
  • Thanks Scott, I really appreciate your time and help. – GibboK Dec 07 '16 at 13:08