3

I have a collection like this:

const data = [
{index: 1, number: 's1', uniqId: '123', city: 'LA'},
{index: 2, number: 's2', uniqId: '321', city: 'NY'},
{index: 3, number: 's3', uniqId: '123', city: 'LA'},
{index: 4, number: 's4', uniqId: '111', city: 'TX'},
{index: 5, number: 's5', uniqId: '321', city: 'NY'}
]

I'd like to group it to have below result:

const data = [
{index: 1, numbers: ['s1', 's3'], uniqId: '123', city: 'LA'},
{index: 2, numbers: ['s2', 's5'], uniqId: '321', city: 'NY'},
{index: 3, number: 's4', uniqId: '111', city: 'TX'},
]

I've got a solution but I believe that it can be achieved in a more elegant way. I can use ramda only but preferred is vanilla solution. Here's my solution:

 return Object.values(
    data.reduce((r, e) => {
      const key = `${e.uniqId}|${e.city}`;
      if (!r[key]) {
        r[key] = e;
        if (r[key].numbers && !isEmpty(r[key].numbers)) {
          r[key].numbers.push(e.number);
        } else {
          r[key].numbers = [];
          r[key].numbers.push(e.number);
        }
      } else if (r[key].numbers && !isEmpty(r[key].numbers)) {
        r[key].numbers.push(e.number);
      } else {
        r[key].numbers = [];
        r[key].numbers.push(e.number);
      }
      return r;
    }, {}),
  ).map((item, index) => ({ ...item, index: index }));

6 Answers6

2

You're doing way more work than you have to in your reducer

const data = [
{index: 1, number: 's1', uniqId: '123', city: 'LA'},
{index: 2, number: 's2', uniqId: '321', city: 'NY'},
{index: 3, number: 's3', uniqId: '123', city: 'LA'},
{index: 4, number: 's4', uniqId: '111', city: 'TX'},
{index: 5, number: 's5', uniqId: '321', city: 'NY'}
]

const reduced = data.reduce((acc, e) => {
  const key = `${e.uniqId}|${e.city}`;
  if (! (key in acc)) {
    acc[key] = Object.assign({}, e);
    delete acc[key]['number'];
    acc[key]['numbers'] = [];
  }
  acc[key]['numbers'].push(e.number);
  return acc;
}, {});

console.log(Object.values(reduced));
TKoL
  • 10,782
  • 1
  • 26
  • 50
0

An alternate reducer function

data.reduce((acc, curr) => {
  const existingIdRow = acc.find(existingElement => existingElement.uniqId === curr.uniqId);
  if(existingIdRow && existingIdRow.numbers)
    existingIdRow.numbers.push(curr.number);
  else {
    const { uniqId, city } = curr;
    acc.push({index: acc.length + 1, numbers: [curr.number], uniqId, city});
  }
  return acc
}, [])
Marc Sloth Eastman
  • 575
  • 1
  • 6
  • 18
0

well yes, you can do it using just one reducer and a condition to loop, so this will do it faster than getting the keys of the objects and then loopping through it.

const data = [
  {index: 1, number: 's1', uniqId: '123', city: 'LA'},
  {index: 2, number: 's2', uniqId: '321', city: 'NY'},
  {index: 3, number: 's3', uniqId: '123', city: 'LA'},
  {index: 4, number: 's4', uniqId: '111', city: 'TX'},
  {index: 5, number: 's5', uniqId: '321', city: 'NY'}
]


const reducer = (accum, cv, currentIndex, source) => {
  const hasValue = accum.some(entry => entry.uniqId === cv.uniqId);
  // we already proccessed it.
  if (hasValue) return accum;

  // we create an object with the desired structure.
  const {
    index,
    uniqId,
    city,
    number
  } = cv;
  let newObj = {
    index,
    uniqId,
    city,
    numbers: [number]
  };

  //now lets fill the numbers :)
  source.forEach((v, index) => {
    //index !== currentIndex &&
    if (index !== currentIndex && v.uniqId === uniqId) {
      newObj['numbers'].push(v.number);
    }
  })

  return [...accum, newObj];

}

const result = data.reduce(reducer, []);

console.log(result)
Prince Hernandez
  • 2,942
  • 1
  • 6
  • 17
0

Here's a really concise take on the problem using the first object in the array as a map and object destructuring:

const data = [
{index: 1, number: 's1', uniqId: '123', city: 'LA'},
{index: 2, number: 's2', uniqId: '321', city: 'NY'},
{index: 3, number: 's3', uniqId: '123', city: 'LA'},
{index: 4, number: 's4', uniqId: '111', city: 'TX'},
{index: 5, number: 's5', uniqId: '321', city: 'NY'}
]

const result = data.reduce((acc, {number,uniqId,city}) => {
  if (!acc[0][uniqId]) {
    acc.push(acc[0][uniqId] = {index: acc.length, numbers: [], uniqId, city});
  }
  acc[0][uniqId].numbers.push(number);
  return acc;
}, [{}]).slice(1);

console.log(result);
Klaycon
  • 8,967
  • 6
  • 26
0

Typically you don't want to have two different properties listing similar data, so the first thing I did was create a map() function to change all number props to numbers and made them single item arrays. Then I used the reduce() function to group the objs with a uniqId together. I would have left it at that, but since you wanted a result with either number or numbers depending on the object, I wrote a simple map() func at the end to convert back to this format.

const data = [
{index: 1, number: 's1', uniqId: '123', city: 'LA'},
{index: 2, number: 's2', uniqId: '321', city: 'NY'},
{index: 3, number: 's3', uniqId: '123', city: 'LA'},
{index: 4, number: 's4', uniqId: '111', city: 'TX'},
{index: 5, number: 's5', uniqId: '321', city: 'NY'}
]


let res = data.map((el) => {
   el.numbers = [el.number]
   delete el.number
   return el
}).reduce((acc,cur) => {
   let ids = acc.map(obj => obj.uniqId)
   let io = ids.indexOf(cur.uniqId)
   if(io > -1){
      acc[io].numbers.push(cur.numbers[0])
   }else{
      acc.push(cur)
   }
   
   return acc
},[])

console.log(res)

res = res.map(el => {
   if(el.numbers.length <= 1){
      el.number = el.numbers[0]
      delete el.numbers
   }
   return el
})

console.log(res)
symlink
  • 10,400
  • 6
  • 25
  • 47
0

Here's a simple way to do this with a Map, and conforms to your desired output logically - alas, the numbers property is out of position compared to your desired output.

If that is important, I leave that for you to sort out ;)

const data = [
  { index: 1, number: 's1', uniqId: '123', city: 'LA' },
  { index: 2, number: 's2', uniqId: '321', city: 'NY' },
  { index: 3, number: 's3', uniqId: '123', city: 'LA' },
  { index: 4, number: 's4', uniqId: '111', city: 'TX' },
  { index: 5, number: 's5', uniqId: '321', city: 'NY' }
];

const reducer = (acc, e, idx, arr) => {
      const key = `${e.uniqId}|${e.city}`;
      let value = acc.get(key);
      if (value === undefined) {  
       value = Object.assign({},e);    
        acc.set(key, value);
        value.index = acc.size;
      } else {
        if('number' in value) {
         value.numbers = [value.number]
          delete value.number;
     }
        value.numbers.push(e.number);
      }
      if (++idx === arr.length) {
        return Array.from(acc.values());
      }
      return acc;
    };
    
const result = data.reduce(reducer, new Map());
document.getElementById('result').innerText = JSON.stringify(result);
<code id="result"></code>
RamblinRose
  • 3,551
  • 2
  • 13
  • 24