xtyy
xtyy

Reputation: 47

reducing nested object key value inside an an array

i have the following arrays

array1 = [
    {a:{key:1 , value: 10} , b:{key:1 , value:12} , c:{key:1 , value: 5} , d:{key:1 , value:2}},
    {a:{key:2 , value: 10} , b:{key:2 , value:12} , c:{key:2 , value: 5} , d:{key:2 , value:2}},
    {a:{key:3 , value: 10} , b:{key:3 , value:12} , c:{key:3 , value: 5} , d:{key:3 , value:2}},
]
array2 = [
    {a:{key:1 , value: 10} , b:{key:1 , value:12} , c:{key:1 , value: 5} , d:{key:1 , value:2}},
    {a:{key:2 , value: 10} , b:{key:2 , value:12} , c:{key:2 , value: 5} , d:{key:2 , value:2}},
    {a:{key:4 , value: 10} , b:{key:4 , value:12} , c:{key:4 , value: 5} , d:{key:4 , value:2}},
]

reduced array based on key should look like this:

combinedArray= [
    {a:{key:1 , value: 20} , b:{key:1 , value:24} , c:{key:1 , value: 10} , d:{key:1 , value:4}},
    {a:{key:2 , value: 20} , b:{key:2 , value:24} , c:{key:2 , value: 10} , d:{key:2 , value:4}},
    {a:{key:3 , value: 10} , b:{key:3 , value:12} , c:{key:3 , value: 5} , d:{key:3 , value:2}},
    {a:{key:4 , value: 10} , b:{key:4 , value:12} , c:{key:4 , value: 5} , d:{key:4 , value:2}},
]

first i tried to merge the two arrays using const mergedArray = [...array1, ...array2]

now i want to check for key duplicates. for example, if there is key1 in both array1 and array2, remove the duplicates then combine the values of that key.

this is what i have tried but it is only iterating through a.key only:

function kdeAdder(param) {
    const array = [param.a]
    let tempHistory = [];
    for(let x=0;x<array.length;x++){
        array[x].forEach((item)=>{
            let noMatch = true; 
            if(tempHistory.length > 0) {
                tempHistory.forEach((tempItem, i)=>{
                    if(item.key === tempItem.key) {
                        tempHistory[i].value += item.value;
                        noMatch = !noMatch; 
                    }
                });
            }
            return (noMatch) ? tempHistory.push(item) : null;
        });

    }
    return tempHistory;
}
kdeAdder(mergedArray);

Upvotes: 3

Views: 266

Answers (4)

Nina Scholz
Nina Scholz

Reputation: 386560

You could group with the key property and the outer property.

const
    array1 = [{ a: { key: 1, value: 10 }, b: { key: 1, value: 12 }, c: { key: 1, value: 5 }, d: { key: 1, value: 2 } }, { a: { key: 2, value: 10 }, b: { key: 2, value: 12 }, c: { key: 2, value: 5 }, d: { key: 2, value: 2 } }, { a: { key: 3, value: 10 }, b: { key: 3, value: 12 }, c: { key: 3, value: 5 }, d: { key: 3, value: 2 } }],
    array2 = [{ a: { key: 1, value: 10 }, b: { key: 1, value: 12 }, c: { key: 1, value: 5 }, d: { key: 1, value: 2 } }, { a: { key: 2, value: 10 }, b: { key: 2, value: 12 }, c: { key: 2, value: 5 }, d: { key: 2, value: 2 } }, { a: { key: 4, value: 10 }, b: { key: 4, value: 12 }, c: { key: 4, value: 5 }, d: { key: 4, value: 2 } }],
    result = Object.values([...array1, ...array2].reduce((r, o) => {
        Object.entries(o).forEach(([k, { key, value }]) => {
            r[key] ??= {};
            r[key][k] ??= { key, value: 0 };
            r[key][k].value += value;
        });
        return r;
    }, {}));

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Upvotes: 0

Peter Seliger
Peter Seliger

Reputation: 13376

The following provided code implements an approach which tries to find a balance in between 1st being agnostic to any array item's current and future structure except for both property names, key and value, of any array item's second level structure and 2nd how to handle the merger of other unknown second level data.

Therefore the general approach creates an object based lookup from the shorter sourceList where each item gets referred to via the value of its second level key property, whereas the longer targetList will be reduced in order to create the final result of merged items from both arrays.

Since approach and implementation are unaware of an items first level structure, one has to reduce again all of a currently processed item's entries. For each of a target item's unknown entry one can rely on such an entry's 2nd level properties, key and value. From all the available data, either known or unknown, one can aggregate the common merger of both the source- and the target-item; their values will be totaled and both of their unknown rest data will be merged by spread syntax, where the latter is the approach's trade off/compromise.

function aggregateFirstValueKeyBasedLookup(lookup, item) {
  lookup[Object.values(item)[0]?.key ?? ''] = item;
  return lookup;  
}
function createKeyBasedValueMergerFromSourceLookup(
  { lookup = {}, result = [] }, targetItem, idx, arr,
) {
  let currentLookupKey;

  result.push(Object
    .entries(targetItem)
    .reduce((merger, [
      targetEntryKey, {
        key, value: targetEntryValue = 0, ...targetEntryRest
      }
    ]) => {
      currentLookupKey = key;

      const sourceItem = lookup[key] ?? {};
      const {
        value: sourceEntryValue = 0, ...sourceEntryRest
      } = sourceItem[targetEntryKey] ?? {};

      return Object.assign(merger, {
        [ targetEntryKey ]: {
          key,
          value: (targetEntryValue + sourceEntryValue),
          ...targetEntryRest,
          ...sourceEntryRest,
        },            
      });

    }, {})
  );
  // delete already processed source-items from lookup.
  Reflect.deleteProperty(lookup, currentLookupKey);

  if (idx >= arr.length - 1) {
    // finalize the result by ...
    result.push(
      // ...pushing all of the lookup's
      //    unprocessed source-items.
      ...[...Object.values(lookup)]
    );
  }  
  return { lookup, result };
}


const array1 = [{
  a: { key: 1, value: 10 }, b: { key: 1, value: 12 }, c: { key: 1, value: 5 }, d: { key: 1, value: 2 }
}, {
  a: { key: 2, value: 10 }, b: { key: 2, value: 12 }, c: { key: 2, value: 5 }, d: { key: 2, value: 2 }
}, {
  a: { key: 3, value: 10 }, b: { key: 3, value: 12 }, c: { key: 3, value: 5 }, d: { key: 3, value: 2 }
}];
const array2 = [{
  a: { key: 1, value: 10 }, b: { key: 1, value: 12 }, c: { key: 1, value: 5 }, d: { key: 1, value: 2 }
}, {
  a: { key: 2, value: 10 }, b: { key: 2, value: 12 }, c: { key: 2, value: 5 }, d: { key: 2, value: 2 }
}, {
  a: { key: 4, value: 10 }, b: { key: 4, value: 12 }, c: { key: 4, value: 5 }, d: { key: 4, value: 2 }
}];

const [ targetList, sourceList ]
  = [array1, array2].sort((a, b) => b.length - a.length);

const sourceLookup = sourceList
  .reduce(aggregateFirstValueKeyBasedLookup, Object.create(null));

console.log({ sourceLookup });

const { result: mergedItemList } = targetList
  .reduce(createKeyBasedValueMergerFromSourceLookup, {
    lookup: sourceLookup, result: [],
  });

console.log({ mergedItemList });


// - changed item structure which keeps
//   just the most necessary pattern.

const newItemStructureList1 = [{
  quick: { key: 'foo', value: 33, biz: 'biz' },
  brown: { key: 'foo', value: 22, baz: 'baz' },
  fox: { key: 'foo', value: 11, buzz: 'buzz' },
}, {
  quick: { key: 'bar', value: 11, baz: 'baz' },
  brown: { key: 'bar', value: 33, biz: 'biz' },
  fox: { key: 'bar', value: 22, booz: 'booz' },
}, {
  quick: { key: 'baz', value: 22, baz: 'baz' },
  brown: { key: 'baz', value: 11, biz: 'biz' },
  fox: { key: 'baz', value: 33, booz: 'booz' },
}];
const newItemStructureList2 = [{
  brown: { key: 'foo', value: 11, baz: 'baz' },
  fox: { key: 'foo', value: 33, booz: 'booz' },
  quick: { key: 'foo', value: 22, baz: 'baz' },
}, {
  fox: { key: 'baz', value: 33, buzz: 'buzz' },
  quick: { key: 'baz', value: 11, biz: 'biz' },
  brown: { key: 'baz', value: 33, baz: 'baz' },
}];

const [ target, source ]
  = [newItemStructureList1, newItemStructureList2].sort((a, b) => b.length - a.length);

const lookup = source
  .reduce(aggregateFirstValueKeyBasedLookup, Object.create(null));

console.log({ lookup });

const { result: mergedItems } = target
  .reduce(createKeyBasedValueMergerFromSourceLookup, { lookup, result: [] });

console.log({ mergedItems });
.as-console-wrapper { min-height: 100%!important; top: 0; }

Upvotes: 0

trincot
trincot

Reputation: 350167

As you confirmed the key inner property is commonly shared by the four "a", "b", "c", "d" objects in an outer object, the a.key value can be used to identify which outer objects should merge.

You could group all objects (irrespective of whether they occur in array1 or array2) by that a.key, and then aggregate objects that occur in the same group. Both of these actions can be accomplished with a reduce call:

const aggregate = (objects) =>
    objects.reduce((x, y) => ({
        a: { key: x.a.key, value: x.a.value + y.a.value },
        b: { key: x.b.key, value: x.b.value + y.b.value },
        c: { key: x.c.key, value: x.c.value + y.c.value },
        d: { key: x.d.key, value: x.d.value + y.d.value },
    }));
                                
const merge = (array1, array2) =>
    Object.values(array1.concat(array2).reduce((acc, obj) => {
        (acc[obj.a.key] ??= []).push(obj);
        return acc;
    }, {})).map(aggregate);
    
const array1 = [
    {a:{key:1 , value: 10} , b:{key:1 , value:12} , c:{key:1 , value: 5} , d:{key:1 , value:2}},
    {a:{key:2 , value: 10} , b:{key:2 , value:12} , c:{key:2 , value: 5} , d:{key:2 , value:2}},
    {a:{key:3 , value: 10} , b:{key:3 , value:12} , c:{key:3 , value: 5} , d:{key:3 , value:2}},
];

const array2 = [
    {a:{key:1 , value: 10} , b:{key:1 , value:12} , c:{key:1 , value: 5} , d:{key:1 , value:2}},
    {a:{key:2 , value: 10} , b:{key:2 , value:12} , c:{key:2 , value: 5} , d:{key:2 , value:2}},
    {a:{key:4 , value: 10} , b:{key:4 , value:12} , c:{key:4 , value: 5} , d:{key:4 , value:2}},
]

console.log(merge(array1, array2));

Upvotes: 2

Amila Senadheera
Amila Senadheera

Reputation: 13235

You can first reduce the output to a single object since its a sort of accumulation of numbers, and then get the format you want as the second step.

const array1 = [ { a: { key: 1, value: 10 }, b: { key: 1, value: 12 }, c: { key: 1, value: 5 }, d: { key: 1, value: 2 }, }, { a: { key: 2, value: 10 }, b: { key: 2, value: 12 }, c: { key: 2, value: 5 }, d: { key: 2, value: 2 }, }, { a: { key: 3, value: 10 }, b: { key: 3, value: 12 }, c: { key: 3, value: 5 }, d: { key: 3, value: 2 }, }, ]; const array2 = [ { a: { key: 1, value: 10 }, b: { key: 1, value: 12 }, c: { key: 1, value: 5 }, d: { key: 1, value: 2 }, }, { a: { key: 2, value: 10 }, b: { key: 2, value: 12 }, c: { key: 2, value: 5 }, d: { key: 2, value: 2 }, }, { a: { key: 4, value: 10 }, b: { key: 4, value: 12 }, c: { key: 4, value: 5 }, d: { key: 4, value: 2 }, }, ];

const mergedArray = [...array1, ...array2];
const keys = []

const reducedOutput = mergedArray.reduce((prev, curr) => {
  Object.entries(curr).forEach(([mainKey, { key, value }]) => {
    // mainKey is a, b, c, d in your case
    if (!prev[mainKey]) {
      prev[mainKey] = {};
    }
    // key is 1, 2, 3, 4 in your case
    if (!keys.includes(key)) {
      keys.push(key)
    }
    prev[mainKey][key] = prev[mainKey][key]
      ? prev[mainKey][key] + value
      : value;
  });
  return prev;
}, {});

const output = keys.map(key => {
  const obj = {}
  Object.entries(reducedOutput).forEach(([k, v]) => {
    obj[k] = {key, value: v[key]}
  })
  return obj
})

console.log(output)

This will work with any other keys for a, b, c, d keys and 1, 2, 3, 4 keys you have used in two levels.

Using Object.entries(), Array.prototype.reduce(), Array.prototype.forEach(), and Array.prototype.map()

Upvotes: 0

Related Questions