2

I have three objects I need to iterate to check for duplicates to end up with a single object. If there are duplicates, I'd like to put them into an array so the user can select the best value so they all ultimately end up as strings or integers (or objects with strings/integers). They might look like this:

const a = {
  first_name: 'Tom',
  height: {
    feet: 5,
    inches: 0
  }
}
const b = {
  first_name: 'Thomas',
  last_name: 'Walsh',
  email: 'tomwalsh@domain.com',
  height: {
    feet: 6,
    inches: 0
  }
}
const c = {
  email: 'tomwalsh@sample.edu'
}

The result would look like this:

const result = {
  first_name: ['Tom', 'Thomas'],
  last_name: 'Walsh',
  email: ['tomwalsh@domain.com', 'tomwalsh@sample.edu'],
  height: {
    feet: [5, 6],
    inches: 0
  }
}

I have no idea if a, b, or c would be the authority on keys so I have to assume an unknown set of key/value pairs, but it's definitely small (no more than 20 pairs, only 2-3 would be more than 3 levels deep, and each of those levels would be a maximum of 4-5 key/value pairs.

I can create a new object, iterate each of the three recursively, and dump the value or create an array of values, but that seems inefficient. Any suggestions?

I've got lodash in the project if needed. Thanks!

I'm Joe Too
  • 5,468
  • 1
  • 17
  • 29

6 Answers6

1

Since there are nesting keys that might be missing in some of the objects, you can merge them via lodash's _.mergeWith(), and collect duplicates into arrays:

const a = {"first_name":"Tom","height":{"feet":5,"inches":0}}
const b = {"first_name":"Thomas","last_name":"Walsh","email":"tomwalsh@domain.com","height":{"feet":6,"inches":0}}
const c = {"email":"tomwalsh@sample.edu"}

const shouldCollect = (s) => _.negate(_.overSome([
  _.isUndefined,
  _.isObject,
  _.partial(_.eq, s)
]))


const mergeDupes = (...args) => _.mergeWith({}, ...args, (o, s) => {
  if(_.isArray(o)) return _.uniq([...o, s]);
  if(shouldCollect(s)(o)) return [o, s];
})

const result = mergeDupes(a, b, c)

console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>

If you want to remove non duplicate properties, you can then clean the object via a recursive _.transform():

const a = {"first_name":"Tom","height":{"feet":5,"inches":0}}
const b = {"first_name":"Thomas","last_name":"Walsh","email":"tomwalsh@domain.com","height":{"feet":6,"inches":0}}
const c = {"email":"tomwalsh@sample.edu"}

const shouldCollect = (s) => _.negate(_.overSome([
  _.isUndefined,
  _.isObject,
  _.partial(_.eq, s)
]))

const omitNonDuplicates = obj =>
  _.transform(obj, (a, v, k) => {
    if (_.isArray(v)) a[k] = v;
    else if (_.isObject(v)) {
      const clean = omitNonDuplicates(v);
      if(!_.isEmpty(clean)) a[k] = clean;
      return;
    }
  });
  
const mergeDupes = (...args) => omitNonDuplicates(_.mergeWith({}, ...args, (o, s) => {
  if(_.isArray(o)) return _.uniq([...o, s]);
  if(shouldCollect(s)(o)) return [o, s];
}))

const result = mergeDupes(a, b, c)

console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
0

I think there is not function to do that, you have to iterate over their properties.

const a = { first_name: 'Tom', height: {feet: 5, inches: 0 } };
const b = { first_name: 'Thomas', last_name: 'Walsh', email:'tomwalsh@domain.com',  height: { feet: 6, inches: 0} };
const c = { email: 'tomwalsh@sample.edu' };

let newObj = {};

[a, b, c].forEach(obj => {
    Object.entries(obj).forEach(([key, value]) => {
        if (newObj.hasOwnProperty(key)) { // if the property exist
            if (Array.isArray(newObj[key])) {
                newObj[key].push(value);
            } else {
                let temporalVal = newObj[key];
                newObj[key] = [temporalVal, value];
            }
        } else { // if the property does not exist, create it.
            newObj[key] = value;
        }
    })
});

console.log(newObj)
eag845
  • 1,013
  • 1
  • 11
  • 16
0

You will need a recursive function that dives into the object properties in tandem:

function mergeObjects(objs) {
    objs = objs.filter(obj => obj !== undefined);
    // return value or array when at least one of the values is a primitive
    if (objs.some(obj => Object(obj) !== obj)) { 
        objs = Array.from(new Set(objs)); // Get unique values
        return objs.length === 1 ? objs[0] : objs;
    }
    // Get all the keys
    const keys = Array.from(new Set([].concat(...objs.map(obj => Object.keys(obj))))); 
    return Object.assign({}, ...keys.map(key => 
        ({ [key]: mergeObjects(objs.map(obj => obj[key])) })));
}

// Demo:
const a = {first_name: 'Tom', height: {feet: 5,inches: 0}};
const b = {first_name: 'Thomas',last_name: 'Walsh',email: 'tomwalsh@domain.com', height: {feet: 6,inches: 0}};
const c = {email: 'tomwalsh@sample.edu'};

const result = mergeObjects([a,b, c]);
console.log(result);
trincot
  • 317,000
  • 35
  • 244
  • 286
0

You could take a recursive approach by checking the values and take the nested object for inner values.

const
    merge = (a, b) => {
        Object.entries(b).forEach(([k, v]) => {
            if (v && typeof v === 'object') {
                return merge(a[k] = a[k] || {}, v);
            }
            if (!(k in a)) {
                return a[k] = v;
            }
            if ([].concat(a[k]).includes(v)) {
                return;
            }
            a[k] = [].concat(a[k], v); 
        });
        return a;
    },
    a = { first_name: 'Tom', height: { feet: 5, inches: 0 } },
    b = { first_name: 'Thomas', last_name: 'Walsh', email: 'tomwalsh@domain.com', height: { feet: 6, inches: 0 } },
    c = { email: 'tomwalsh@sample.edu', height: { feet: 15 } },
    result = [a, b, c].reduce(merge, {});

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

Here is a concise recursive solution via Array.reduce and Array.forEach:

const a = { first_name: 'Tom', height: { feet: 5, inches: 0 } },
      b = { first_name: 'Thomas', last_name: 'Walsh', email: 'tomwalsh@domain.com', height: { feet: 6, inches: 0 } },
      c = { email: 'tomwalsh@sample.edu'},
      d = { first_name: 'Tomy', height: { feet: 15, inches: 10 } }

const mergeKeys = (a,b) => {
  if(a != undefined) {
    if(a === b) return a
    if(typeof b == 'object') return merge([a,b])      
    else return [...(Array.isArray(a) ? a : [a]), b]
  } else return b
}

const merge = arr => arr.reduce((r,c) => 
  (Object.entries(c).forEach(([k,v]) => r[k] = mergeKeys(r[k],c[k])), r) ,{})

console.log(merge([a,b,c,d]))
Akrion
  • 18,117
  • 1
  • 34
  • 54
0

How about Lodash + Deepdash:

let merged = _.cloneDeep(objects.shift()); // clone to keep source untouched
objects.forEach((obj) => {
  _.eachDeep(obj, (value, key, parent, ctx) => {
    if (_.isObject(value)) return;
    let exists = _.get(merged, ctx.path);
    if (exists == undefined) {
      exists = value;
    } else {
      exists = _.uniq([].concat(exists, value));
      if (exists.length == 1) exists = exists[0];
    }
    _.set(merged, ctx.path, exists);
  });
});

full test for your case

Yuri Gor
  • 1,353
  • 12
  • 26