moys
moys

Reputation: 8033

Reduce an arry of objects to an object with key value pairs with addition & comparision

I have an array of objects like this

const inputArray = [
  { name: "sam", date: "1 / 1 / 23", confirmed: "yes", spent: 0 },
  { name: "sam", date: "1 / 2 / 23", confirmed: "yes", spent: 4 },
  { name: "sam", date: "1 / 3 / 23", confirmed: "yes", spent: 4 },
  { name: "sam", date: "1 / 4 / 23", confirmed: "no", spent: 4 },
  { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 4 },
  { name: "bill", date: "1 / 6 / 23", confirmed: "yes", spent: 4 },
  { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 0 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "yes", spent: 0 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 },
];

I want an output object like this

const outputObj = {
  sam: { spent: 12, confirmedAndNotSpent: 1, notConfirmedAndSpent: 1 },
  bill: { spent: 8, confirmedAndNotSpent: 1, notConfirmedAndSpent: 0 },
  bill: { spent: 4, confirmedAndNotSpent: 1, notConfirmedAndSpent: 2 },
};

How do I achieve this.

I tired this first

let try1 = inputArray.reduce((accumulator, current) => {
  if (!accumulator[current.name]) accumulator[current.name] = 0;
  accumulator[current.name] += +current.spent;
  return accumulator;
}, {});

This gives the the output like so { sam: 12, bill: 8, annie: 4 }.

However, I am unable to convert even this into an object with the code below.

let try2 = inputArray.reduce((accumulator, current) => {
  if (!accumulator[current.name]) accumulator[current.name] = {};
  accumulator[current.name][current.spent] += +current.spent;
  return accumulator;
}, {})

The above code gives the out put below

{
  sam: { '0': NaN, '4': NaN },
  bill: { '0': NaN, '4': NaN },
  annie: { '0': NaN, '2': NaN }
}

Can anyone help to get the output I want? Thanks.

Update: Output in a different format Just one follow-up, how to get the output like below?

const outputObj2 = [
  { name: "sam", spent: 12, confirmedAndNotSpent: 1, notConfirmedAndSpent: 1 },
  { name: "bill", spent: 8, confirmedAndNotSpent: 1, notConfirmedAndSpent: 0 },
  { name: "annie", spent: 4, confirmedAndNotSpent: 1, notConfirmedAndSpent: 2 },
];

The code below gets me the output I want but is this best way?.

let try3 = inputArray.reduce((accumulator, current) => {
  if (!accumulator.some((accumulator) => accumulator.name === current.name)) {
    let obj = {
      name: current.name,
      spent: current.spent,
      confirmedAndNotSpent:
        current.confirmed === "yes" && current.spent === 0 ? 1 : 0,
      notConfirmedAndSpent: current.confirmed === "no" ? 1 : 0,
    };
    accumulator.push(obj);
  } else {
    //Find index of specific object using findIndex method.
    objIndex = accumulator.findIndex((obj) => obj.name == current.name);

    //update the specific object using index
    accumulator[objIndex].spent += current.spent;

    if (current.confirmed === "yes" && current.spent === 0) {
      accumulator[objIndex].confirmedAndNotSpent++;
    }

    if (current.confirmed === "no") {
      accumulator[objIndex].notConfirmedAndSpent++;
    }
  }

  return accumulator;
}, []);

The output I get is as below. Only the details of the last person is coming & it is not even an object. I think I am missing the portion of pushing details of each name as an object into the array but for the life of me, I am not able to figure it out.

[
  {
    name: 'sam',
    spent: 12,
    confirmedAndNotSpent: 1,
    notConfirmedAndSpent: 1
  },
  {
    name: 'bill',
    spent: 8,
    confirmedAndNotSpent: 1,
    notConfirmedAndSpent: 0
  },
  {
    name: 'annie',
    spent: 4,
    confirmedAndNotSpent: 1,
    notConfirmedAndSpent: 2
  }
]

Please add the answer to the existing portion of the answer.

Upvotes: 1

Views: 97

Answers (3)

robere2
robere2

Reputation: 1716

Two issues with your code:

  1. You made a typo. Where you declare try2, this line:
accumulator[current.name][current.spent] += +current.spent;

should be:

accumulator[current.name].spent += +current.spent;
  1. In JavaScript, undefined + 0 (or any number) is equal to NaN. To solve this. You have to first initialize your spent property to 0:
  if (!accumulator[current.name]) accumulator[current.name] = { spent: 0 }

For the other variables, these can all be accomplished in a similar manner, just with the necessary conditional checks.

const inputArray = [
  { name: "sam", date: "1 / 1 / 23", confirmed: "yes", spent: 0 },
  { name: "sam", date: "1 / 2 / 23", confirmed: "yes", spent: 4 },
  { name: "sam", date: "1 / 3 / 23", confirmed: "yes", spent: 4 },
  { name: "sam", date: "1 / 4 / 23", confirmed: "no", spent: 4 },
  { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 4 },
  { name: "bill", date: "1 / 6 / 23", confirmed: "yes", spent: 4 },
  { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 0 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "yes", spent: 0 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 },
];

let try2 = inputArray.reduce((accumulator, current) => {
  if (!accumulator[current.name]) accumulator[current.name] = {
    spent: 0,
    confirmedAndNotSpent: 0,
    notConfirmedAndSpent: 0
  };
  
  accumulator[current.name].spent += current.spent;
  
  if(current.confirmed === "yes" && current.spent === 0) {
    accumulator[current.name].confirmedAndNotSpent++;
  }
  
  if(current.confirmed === "no") {
    accumulator[current.name].notConfirmedAndSpent++;
  }
  return accumulator;
}, {})

document.getElementById("out").innerText = JSON.stringify(try2, null, 4)
<pre id="out"></p>

EDIT:

To get these in an array instead, the code can remain largely the same, however you do need to find the object within the accumulator array that corresponds to the current person.

const inputArray = [
  { name: "sam", date: "1 / 1 / 23", confirmed: "yes", spent: 0 },
  { name: "sam", date: "1 / 2 / 23", confirmed: "yes", spent: 4 },
  { name: "sam", date: "1 / 3 / 23", confirmed: "yes", spent: 4 },
  { name: "sam", date: "1 / 4 / 23", confirmed: "no", spent: 4 },
  { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 4 },
  { name: "bill", date: "1 / 6 / 23", confirmed: "yes", spent: 4 },
  { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 0 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "yes", spent: 0 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 },
];

let try2 = inputArray.reduce((accumulator, current) => {
  // Find the object within the array for the current person.
  let personObj = accumulator.find((v) => v.name === current.name);
  // If there is no object for the current person within the array, create it and push it.
  if(!personObj) {
    personObj = {
      name: current.name,
      spent: 0,
      confirmedAndNotSpent: 0,
      notConfirmedAndSpent: 0
    };
    accumulator.push(personObj);
  }
  
  // Same as above, just on the person object...
  personObj.spent += current.spent;
  
  if(current.confirmed === "yes" && current.spent === 0) {
   personObj.confirmedAndNotSpent++;
  }
  
  if(current.confirmed === "no") {
    personObj.notConfirmedAndSpent++;
  }
  return accumulator;
}, [])

document.getElementById("out").innerText = JSON.stringify(try2, null, 4)
<pre id="out"></p>

This implementation won't scale well (complexity O(n^2)), but can be improved by combining the two techniques to create your array after creating the map (O(n)).

const inputArray = [
  { name: "sam", date: "1 / 1 / 23", confirmed: "yes", spent: 0 },
  { name: "sam", date: "1 / 2 / 23", confirmed: "yes", spent: 4 },
  { name: "sam", date: "1 / 3 / 23", confirmed: "yes", spent: 4 },
  { name: "sam", date: "1 / 4 / 23", confirmed: "no", spent: 4 },
  { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 4 },
  { name: "bill", date: "1 / 6 / 23", confirmed: "yes", spent: 4 },
  { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 0 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "yes", spent: 0 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 },
];

// All same as original answer...
let try2 = inputArray.reduce((accumulator, current) => {
  if (!accumulator[current.name]) accumulator[current.name] = {
    name: current.name,
    spent: 0,
    confirmedAndNotSpent: 0,
    notConfirmedAndSpent: 0
  };
  
  accumulator[current.name].spent += current.spent;
  
  if(current.confirmed === "yes" && current.spent === 0) {
    accumulator[current.name].confirmedAndNotSpent++;
  }
  
  if(current.confirmed === "no") {
    accumulator[current.name].notConfirmedAndSpent++;
  }
  return accumulator;
}, {})

// Now convert the object into an array by pushing each key's value to an array.
const try2AsArr = [];
for(const key of Object.keys(try2)) {
  try2AsArr.push(try2[key]);
}

document.getElementById("out").innerText = JSON.stringify(try2AsArr, null, 4)
<pre id="out"></p>

Upvotes: 1

anqit
anqit

Reputation: 1090

const inputArray = [
  { name: "sam", date: "1 / 1 / 23", confirmed: "yes", spent: 0 },
  { name: "sam", date: "1 / 2 / 23", confirmed: "yes", spent: 4 },
  { name: "sam", date: "1 / 3 / 23", confirmed: "yes", spent: 4 },
  { name: "sam", date: "1 / 4 / 23", confirmed: "no", spent: 4 },
  { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 4 },
  { name: "bill", date: "1 / 6 / 23", confirmed: "yes", spent: 4 },
  { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 0 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "yes", spent: 0 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 },
];

const output = inputArray.reduce((acc, {name, confirmed, spent: currSpent}) => {
    let {spent, confirmedAndNotSpent, notConfirmedAndSpent} = acc[name] || {spent: 0, confirmedAndNotSpent: 0, notConfirmedAndSpent: 0}
    spent += currSpent
    confirmedAndNotSpent += confirmed === 'yes' && currSpent === 0
  notConfirmedAndSpent += confirmed === 'no'
  
  return {...acc, [name]: {spent, confirmedAndNotSpent, notConfirmedAndSpent, }}
}, {})

console.log(output)

produces:

{
 annie: {
   confirmedAndNotSpent: 1,
   notConfirmedAndSpent: 2,
   spent: 4
 },
 bill: {
   confirmedAndNotSpent: 1,
   notConfirmedAndSpent: 0,
   spent: 8
 },
 sam: {
   confirmedAndNotSpent: 1,
   notConfirmedAndSpent: 1,
   spent: 12
 }
}

The idea is to reduce the input array to the desired object as you started to do, but accumulate entire objects for each name, rather than just individual fields of the goal objects, and to create the per-name-default objects with the desired keys and appropriate initial values.

Upvotes: 1

CertainPerformance
CertainPerformance

Reputation: 370789

When iterating over an array item, perform all the checks you need in that one iteration - increment spent, increment confirmedAndNotSpent if appropriate, and increment notConfirmedAndSpent if appropriate. Make the accumulator (or outputObj) an object of objects - the value should be an object, not just a number, to hold all the information you want.

const inputArray = [
  { name: "sam", date: "1 / 1 / 23", confirmed: "yes", spent: 0 },
  { name: "sam", date: "1 / 2 / 23", confirmed: "yes", spent: 4 },
  { name: "sam", date: "1 / 3 / 23", confirmed: "yes", spent: 4 },
  { name: "sam", date: "1 / 4 / 23", confirmed: "no", spent: 4 },
  { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 4 },
  { name: "bill", date: "1 / 6 / 23", confirmed: "yes", spent: 4 },
  { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 0 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "yes", spent: 0 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 },
  { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 },
];
const output = {};
for (const { name, date, confirmed, spent } of inputArray) {
  output[name] ??= { spent: 0, confirmedAndNotSpent: 0, notConfirmedAndSpent: 0 };
  output[name].spent += spent;
  if (confirmed === 'yes' && spent === 0) {
    output[name].confirmedAndNotSpent++;
  }
  if (confirmed === 'no') {
    output[name].notConfirmedAndSpent++;
  }
}
console.log(output);

Upvotes: 1

Related Questions