0

I have an array of objects like this:

const dataset = 
  [ { date: '2018-01', color: 'red',  value1: null, value2: null, value3: 2,    value4: 6    } 
  , { date: '2018-01', color: 'red',  value1: 0,    value2: 4,    value3: null, value4: null } 
  , { date: '2018-02', color: 'red',  value1: null, value2: null, value3: 2,    value4: 10   } 
  , { date: '2018-02', color: 'red',  value1: -9,   value2: 0,    value3: null, value4: null } 
  , { date: '2019-01', color: 'blue', value1: null, value2: null, value3: 10,   value4: 3    } 
  , { date: '2019-01', color: 'blue', value1: -2,   value2: 8,    value3: null, value4: null } 
  , { date: '2019-02', color: 'blue', value1: null, value2: null, value3: 20,   value4: 2    } 
  , { date: '2019-02', color: 'blue', value1: 9,    value2: 7,    value3: null, value4: null } 
  , ...
  ]

and I want:

const result = 
  [ { date: '2018-01', color: 'red',  value1: 0,  value2: 4, value3: 2,  value4: 6  } 
  , { date: '2018-02', color: 'red',  value1: -9, value2: 0, value3: 2,  value4: 10 } 
  , { date: '2019-01', color: 'blue', value1: -2, value2: 8, value3: 10, value4: 3  } 
  , { date: '2019-02', color: 'blue', value1: 9,  value2: 7, value3: 20, value4: 2  } 
  , ...
  ] 

result contains the same information of dataset.
records with same date and color are merged togheter and null values are replaced by available data.

How can I do something like this? I have no idea

Mister Jojo
  • 20,093
  • 6
  • 21
  • 40
whitecircle
  • 237
  • 9
  • 34
  • 2
    You could use `reduce()`, you could loop over the array backwards merging like objects and removing the duplicates; there are various ways. What have you tried? – Taplar Sep 24 '20 at 17:11
  • 2
    Merging string values may be problematic - what if color keys differ? – kind user Sep 24 '20 at 17:12
  • refer this link https://stackoverflow.com/questions/14446511/most-efficient-method-to-groupby-on-an-array-of-objects?page=1&tab=votes#tab-top – pushpak Sep 24 '20 at 17:35

4 Answers4

3

You could destructure date and color and iterate the entries of the rest.

const
    dataset = [{ date: '2018-01', color: 'red', value1: null, value2: null, value3: 2, value4: 6 }, { date: '2018-01', color: 'red', value1: 0, value2: 4, value3: null, value4: null }, { date: '2018-02', color: 'red', value1: null, value2: null, value3: 2, value4: 10 }, { date: '2018-02', color: 'red', value1: -9, value2: 0, value3: null, value4: null }, { date: '2019-01', color: 'blue', value1: null, value2: null, value3: 10, value4: 3 }, { date: '2019-01', color: 'blue', value1: -2, value2: 8, value3: null, value4: null }, { date: '2019-02', color: 'blue', value1: null, value2: null, value3: 20, value4: 2}, { date: '2019-02', color: 'blue', value1: 9, value2: 7, value3: null, value4: null }],
    result = Object.values(dataset.reduce((r, { date, color, ...rest }) => {
        const key = [date, color].join('|');
        if (!r[key]) r[key] = { date, color };
        Object.entries(rest).forEach(([k, v]) => {
            if ([undefined, null].includes(r[key][k])) r[key][k] = v;
        });
        return r;
    }, {}));

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
1

You can make use of Array.reduce and Object.values for achieving the expected result.

const dataset = [{date:'2018-01',color:'red',value1:null,value2:null,value3:2,value4:6},{date:'2018-01',color:'red',value1:0,value2:4,value3:null,value4:null},{date:'2018-02',color:'red',value1:null,value2:null,value3:2,value4:10},{date:'2018-02',color:'red',value1:-9,value2:0,value3:null,value4:null},{date:'2019-01',color:'blue',value1:null,value2:null,value3:10,value4:3},{date:'2019-01',color:'blue',value1:-2,value2:8,value3:null,value4:null},{date:'2019-02',color:'blue',value1:null,value2:null,value3:20,value4:2},{date:'2019-02',color:'blue',value1:9,value2:7,value3:null,value4:null},]

const mergeData = (data) => {
  const finalRes = data.reduce((res, obj)=>{
    const {date, color} = obj;
    const key = `${date}_${color}`;
    res[key] = {
      ...res[key],
      ...obj,
      value1: res[key]?.value1 || obj.value1,
      value2: res[key]?.value2 || obj.value2,
      value3: res[key]?.value3 || obj.value3,
      value4: res[key]?.value4 || obj.value4
    }
    return res;
  }, {});
  return Object.values(finalRes);
}

console.log(mergeData(dataset))
.as-console-wrapper {
  max-height: 100% !important;
}

Below is a generic way instead of limiting the solution to value1 to value4. Also using Optional Chaining & Nullish coalescing to make sure that the null will not overwrite 0

const dataset = [{date:'2018-01',color:'red',value1:null,value2:null,value3:2,value4:6},{date:'2018-01',color:'red',value1:0,value2:4,value3:null,value4:null},{date:'2018-02',color:'red',value1:null,value2:null,value3:2,value4:10},{date:'2018-02',color:'red',value1:-9,value2:0,value3:null,value4:null},{date:'2019-01',color:'blue',value1:null,value2:null,value3:10,value4:3},{date:'2019-01',color:'blue',value1:-2,value2:8,value3:null,value4:null},{date:'2019-02',color:'blue',value1:null,value2:null,value3:20,value4:2},{date:'2019-02',color:'blue',value1:9,value2:7,value3:null,value4:null},]

const mergeData = (data) => {
  const finalRes = data.reduce((res, {date, color, ...rest}) => {
    const key = `${date}_${color}`;
    let newObj = {}
    Object.keys(rest).forEach(valKey => {
      newObj = {
        ...newObj,
        [valKey]:  res[key]?.[valKey] ?? rest[valKey] 
//check if the corresponding key is present in `res` object and if it's
//null or undefined then replace it with the value from the current object
//in the loop
      }
    })
    res[key] = {
      date,
      color,
      ...newObj
    }
    return res;
  }, {});
  return Object.values(finalRes);
}

console.log(mergeData(dataset))
.as-console-wrapper {
  max-height: 100% !important;
}
Nithish
  • 5,393
  • 2
  • 9
  • 24
  • it's a very static solution. – Nina Scholz Sep 24 '20 at 18:26
  • Note that if the first two elements are swapped the result includes a `null` value, because `0 || null //=> null`. Use [`??`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator) to check for nullish values `0 ?? null //=> 0`. – 3limin4t0r Sep 24 '20 at 18:27
  • Thanks for your comments @NinaScholz, @3limin4t0r. I have updated my answer so that it'll be dynamic enough and also `null` will not overwrite `0`. Thanks for helping in improving the answer :) – Nithish Sep 25 '20 at 04:43
0

We use reduce on the dataset.
Creating an empty ({}) Object accumulator (acc).
We use keys date+color.
If that key doesn't exist, we simply create it with the current value (cv).
If it does, we add cv's values to it.

After that, we take the created Object's values and assign to result.

var dataset = [
  {date: '2018-01', color: 'red', value1: null, value2: null, value3: 2, value4: 6},
  {date: '2018-01', color: 'red', value1: 0, value2: 4, value3: null, value4: null},
  {date: '2018-02', color: 'red', value1: null, value2: null, value3: 2, value4: 10},
  {date: '2018-02', color: 'red', value1: -9, value2: 0, value3: null, value4: null},
  {date: '2019-01', color: 'blue', value1: null, value2: null, value3: 10, value4: 3},
  {date: '2019-01', color: 'blue', value1: -2, value2: 8, value3: null, value4: null},
  {date: '2019-02', color: 'blue', value1: null, value2: null, value3: 20, value4: 2},
  {date: '2019-02', color: 'blue', value1: 9, value2: 7, value3: null, value4: null}
];

var result=Object.values(dataset.reduce(
  (acc, cv) => {
    if(acc[cv.date+cv.color]==undefined) acc[cv.date+cv.color]=cv;
    else {
      acc[cv.date+cv.color].value1+=cv.value1;
      acc[cv.date+cv.color].value2+=cv.value2;
      acc[cv.date+cv.color].value3+=cv.value3;
      acc[cv.date+cv.color].value4+=cv.value4;
    }
    return acc;
  },{}
));

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
iAmOren
  • 2,760
  • 2
  • 11
  • 23
0

I would first group the objects in dataset by date and color. Then merge the objects in a single group together by overriding only nullish (null and undefined) values.

I've build an example below abstracting the groupBy and mergeWith functionality in their own functions.

const dataset = [
  {date: '2018-01', color: 'red', value1: null, value2: null, value3: 2, value4: 6},
  {date: '2018-01', color: 'red', value1: 0, value2: 4, value3: null, value4: null},
  {date: '2018-02', color: 'red', value1: null, value2: null, value3: 2, value4: 10},
  {date: '2018-02', color: 'red', value1: -9, value2: 0, value3: null, value4: null},
  {date: '2019-01', color: 'blue', value1: null, value2: null, value3: 10, value4: 3},
  {date: '2019-01', color: 'blue', value1: -2, value2: 8, value3: null, value4: null},
  {date: '2019-02', color: 'blue', value1: null, value2: null, value3: 20, value4: 2},
  {date: '2019-02', color: 'blue', value1: 9, value2: 7, value3: null, value4: null},
]

const result = Array
  .from(
    groupBy(({date, color}) => JSON.stringify([date, color]), dataset)
      .values()
  )
  .map(group => mergeWith((_, a, b) => a ?? b, ...group));

console.log(result);



function groupBy(fn, iterable) {
  const groups = new Map();
  for (const item of iterable) {
    const key = fn(item);
    if (!groups.has(key)) groups.set(key, []);
    groups.get(key).push(item);
  }
  return groups;
}

function mergeWith(fn, ...objects) {
  const merged = Object.create(null);
  for (const object of objects) {
    for (const key in object) {
      if (key in merged) {
        merged[key] = fn(key, merged[key], object[key]);
      } else {
        merged[key] = object[key];
      }
    }
  }
  return merged;
}
3limin4t0r
  • 19,353
  • 2
  • 31
  • 52