1

I'm merging two objects together to create a filter object. However I want to group the merged objects property values where the keys are the same.

So...

[{category: 'furniture'}, {category: 'mirrors'}, {availability: 'in_stock'}]

becomes

[{category: ['furniture', 'mirrors']}, {availability: 'in_stock'}]

any ideas?

1

7 Answers 7

3

With lodash you merge the entire array to a new object by spreading into _.mergeWith(). The customizer should use empty arrays as default values for the current values, and concat the values. Use _.map() to convert back to an array.

const data = [{category: 'furniture'}, {category: 'mirrors'}, {availability: 'in_stock'}];

const result = _.map(
  _.mergeWith({}, ...data, (a = [], b = [], key) => a.concat(b)),
  (val, key) => ({ [key]: val })
)

console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js" integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

Using vanilla JS, reduce the array to a Map using the objects' keys as the keys of the Map, with an empty array as the value, and push the objects' values into the arrays. Use Array.from() to convert the Map to an array.

const data = [{category: 'furniture'}, {category: 'mirrors'}, {availability: 'in_stock'}];

const result = Array.from(
  data.reduce((acc, obj) => {
    Object.entries(obj)
      .forEach(([key, val]) => {
        if(!acc.has(key)) acc.set(key, [])

        acc.get(key).push(val)
      })

    return acc
  }, new Map()),
  ([key, val]) => ({ [key]: val })
)

console.log(result)

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for this. I tried lodash but couldnt find a specific way to achieve the desired output. As I already have lodash in the project I'm gonna give this one a go.
Lodash solution working perfectly!
2

You can use reduce like this:

const data = [
  { category: 'furniture' }, 
  { category: 'mirrors' }, 
  { availability: 'in_stock' }
];

const result = data.reduce(
  (a, x) => {
    const key = Object.keys(x)[0]; // find the key of the current object
    if (!a.tmp[key]) { // if the current key doesn't exist in the lookup object (tmp) yet ...
      a.tmp[key] = []; // create an empty array in the lookup object for the current key
      a.result.push({ [key]: a.tmp[key] }); // push the current object to the result
    }
    a.tmp[key].push(x[key]); // push the current value to the array
    return a;
  }, 
  { result: [], tmp: {} },
).result;

console.log(result);

I'm sure there are easier ways to achieve this, but that's the best I can come up with right now.

7 Comments

Amazing. Thank you!
@DanEmery ... the above solution is neither amazing nor clever in any way. It is error prone at least twice for firstly reduce does not allow/implement/recognize a thisArg parameter, and secondly, even if thisArg was supported, an arrow function could not access the correct this since arrow functions do not have an own binding to this. Thus Đinh Carabus' solution directly writes to and reads from the global object (window for browsers) for the lookup task.
@PeterSeliger, You're right, I somehow got that mixed up with Array.prototype.forEach. I'll fix that, thanks!
@ĐinhCarabus ... I would implement the reducer function even more generic, though the OP and the example data do not ask for it. Right now the solution works property name agnostic, but assuming objects with a single key only. One could come up with an approach that supports multi-key objects. Also, it seems like the OP wants to keep the original structure for objects which have a sole unique property name, whereas the above current solution merges and collects { availability: ['in_stock'] } instead of leaving it untouched as with the OP's requirements ... { availability: 'in_stock' }.
@DanEmery ... The target data structure can not be left that ambiguous. One has to consequently push every same key value into an array, even though such a key (property name) was unique throughout all the other data. Keeping it ambiguous the problems occur as soon as a value is an array itself. A process has no chance (of cause with a lot of additional code it would work) figuring out if it was an initial value or the array the current item needs to be pushed to.
|
1

we can also achieve this by using forEach loop :

const input = [{category: 'furniture'}, {category: 'mirrors'}, {availability: 'in_stock'}];

const resultObj = {};
const resultArr = [];

input.forEach((obj) => {
  resultObj[Object.keys(obj)[0]] = [];
})

input.forEach((obj) => {
  resultObj[Object.keys(obj)[0]].push(obj[Object.keys(obj)[0]]);
  resultArr.push(resultObj);
})

console.log([...new Set(resultArr)]);

2 Comments

Nice solution, will give it a go!
@CreativeLearner this doesn't quite match the desired output format. You have merged the properties into one object instead of keeping them in seperate objects as requested by the OP.
1

Another one reduce solution

const arr = [{category: 'furniture', category2: 'furniture2'}, {category: 'mirrors'}, {availability: 'in_stock'}]


const result = Object.values(arr
  .flatMap((obj) => Object.entries(obj))
  .reduce((acc, [key, value]) => {
    acc[key] = acc[key] 
      ? {[key]: [...acc[key][key], value] } 
      : {[key]: [value] }
  
  return acc;
  }, {}));
  
console.log(result)
.as-console-wrapper{min-height: 100%!important; top: 0}

Comments

1

A generic implementation could achieve a merger of any kind of objects regardless of amount and kind of an(y) object's property names.

Since the result of such an implementation is an object, one needs additional treatment in order to cover the OP's requirement(s).

function mergeAndCollectItemEntries(result, item) {
  // return the programmatically aggregated merger/result.
  return Object
    // get an item's entry array.
    .entries(item)
    // for each key-value pair ...
    .reduce((merger, [key, value]) => {

      // ... access and/or create a `key` specific array ...
      // ... and push `value` into this array.
      (merger[key] ??= []).push(value);

      // return the programmatically aggregated merger/result.
      return merger;

    }, result);
}

const sampleData = [
  { category: 'furniture' },
  { category: 'mirrors' },
  { availability: 'in_stock' },
];
const mergedData = sampleData
  .reduce(mergeAndCollectItemEntries, {});

const mergedDataList = Object
  .entries(
    sampleData
      .reduce(mergeAndCollectItemEntries, {})
  ) 
  .map(entry => Object.fromEntries([entry]));
//.map(([key, value]) => ({ [key]: value }));

console.log({
  sampleData,
  mergedData,
  mergedDataList,
});
console.log(
  Object
    .entries([
        { category: 'furniture', foo: 'baz' },
        { category: 'mirrors', bar: 'bizz' },
        { availability: 'in_stock', bar: 'buzz' },
      ].reduce(
        mergeAndCollectItemEntries, {}
      )
    ).map(
      ([key, value]) => ({ [key]: value })
    //entry => Object.fromEntries([entry])
    )
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

Comments

0

Another approach here with building an tracking object to merge the values.

Handle the cases of single value keep as string and multiple values as array per the expected output.

const merge = (arr, output = {}) => {
  arr.forEach((item) => {
    const [[key, val]] = Object.entries(item);
    if (key in output) {
      output[key] = Array.isArray(output[key])
        ? output[key].concat(val)
        : [output[key]].concat(val);
    } else {
      output[key] = val;
    }
  });
  return Object.entries(output).map(([key, val]) => ({ [key]: val }));
};

const data = [
  { category: "furniture" },
  { category: "mirrors" },
  { availability: "in_stock" },
];

console.log(merge(data));

Comments

0

A solution that builds on Object.groupBy:

const arr = [{category:'furniture'},{category:'mirrors'},{availability:'in_stock'}];
const result = 
     Object.entries(Object.groupBy(arr.flatMap(Object.entries), ([k]) => k))
           .map(([k, v]) => ({[k]: (v.length > 1 ? v.map(([,v]) => v) : v[0][1])}));
console.log(result); 
// => [{category: ["furniture", "mirrors"]}, {availability: "in_stock"}]

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.