2

I'm trying to find an efficient and short-as-possible way to collapse near-identical objects in an array into objects containing their own arrays with the nonidentical data. It sounds complicated when I try to explain it, let me rather show you what I mean:

I have an array of objects that looks like the following:

    [{ 
        id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd',
        first_name: 'SomeName',
        email: 'some@email.com',
        rName: 'User',                                // 0,1
        rAuthority: 'ROLE_USER',                      // 0,1
        pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e',
        pMobile: '012 345 6789',
        atId: '90db0c5d-3030-44aa-9dc0-40242af0d5c5', // 0,2
        atPlatform: 'web',
    },{ 
        id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd',
        first_name: 'SomeName',
        email: 'some@email.com',
        rName: 'User',                                // 0,1
        rAuthority: 'ROLE_USER',                      // 0,1
        pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e',
        pMobile: '012 345 6789',
        atId: 'e7d53cab-a9b9-40ae-9271-11d79c2f269c', // 1,3
        atPlatform: 'web',
    },{ 
        id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd',
        first_name: 'SomeName',
        email: 'some@email.com',
        rName: 'Admin',                               // 2,3
        rAuthority: 'ROLE_ADMIN',                     // 2,3
        pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e',
        pMobile: '012 345 6789',
        atId: '90db0c5d-3030-44aa-9dc0-40242af0d5c5', // 0,2
        atPlatform: 'web',
    },{ 
        id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd',
        first_name: 'SomeName',
        email: 'some@email.com',
        rName: 'Admin',                               // 2,3
        rAuthority: 'ROLE_ADMIN',                     // 2,3
        pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e',
        pMobile: '012 345 6789',
        atId: 'e7d53cab-a9b9-40ae-9271-11d79c2f269c', // 1,3
        atPlatform: 'web',
    }]

// I point out which of the properties are not identical by adding 
// quotes showing which indices of the array contains unique values of 
// said property. If the there's not a quote to the right of the 
// property it's identical across all indices.

I want to transform this array into an array where objects with duplicate ids are collapsed into one object with object arrays containing the nonidentical data. It looks like this:

[{
    id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd',
    first_name: 'SomeName',
    email: 'some@email.com',
    roles: [{
            name: 'User',
            authority: 'ROLE_USER'
    },{
            name: 'Admin',
            authority: 'ROLE_ADMIN'
    }],
    profiles: [{
            id: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e',
            mobile: '012 345 6789',
    }],
    tokens: [{
            id: '90db0c5d-3030-44aa-9dc0-40242af0d5c5',
            platform: 'web',
    },{
            id: 'e7d53cab-a9b9-40ae-9271-11d79c2f269c',
            platform: 'web',
    }]
}]

As you can see, all the properties from the previous array that had a prefix r now all have their own objects in an array in the roles property. p prefixed properties are in profiles and at prefixed properties are in tokens. An object is considered to be "identical" if it has the same id as another object.

Here's some code I've written that seems to successfully transform the first array into the second array:

...then(data => {

    let users = [];

    // gets the correct formatting
    data.forEach((d, i) => {
        let found = false;
        users.forEach((u, j) => {
            if(d.id === u.id) {
                u.roles.push({ name:d.rName, authority:d.rAuthority });
                u.tokens.push({ id:d.atId, platform:d.atPlatform });
                u.profiles.push({ id:d.pId, mobile:d.pMobile });
                found = true;
            }
        });
        if(!found) {
            users.push({
                id:d.id,
                first_name:d.first_name,
                email:d.email,
                roles: [{ name:d.rName, authority:d.rAuthority }],
                profiles: [{ id:d.pId, mobile:d.pMobile }],
                tokens: [{ id:d.atId, platform:d.atPlatform }]
            });
        }
    });

    // remove duplicates from sub-arrays
    users.forEach((user, i) => {
        user.roles = _.uniqBy(user.roles, 'name');
        user.profiles = _.uniqBy(user.profiles, 'id');
        user.tokens = _.uniqBy(user.tokens, 'id');
    });
});

I have two problems with this code. The first is that it's super long (I actually removed many properties from the first array for the sake of this question - in reality, each object has more than double the amount of properties you see here, making this code much longer), and the second is that I've got a strong suspicion that it's probably very inefficient.

Question:

Can someone please help me rewrite the code that I'm using to format my array into something shorter and more efficient. I do have lodash installed so I'd prefer answers that made use of it, but I'll gladly also accept vanilla.js answers.

Additional notes:

This question is a follow-up to another question I posted. Looking at this question will vaguely show you where the data I'm trying to transform in this question comes from. The short version is that it comes from the database, it's my idea of lazy loading using Knex.js. The idea is that that each User can have multiple Roles, Profiles and AuthTokens.

t.niese
  • 39,256
  • 9
  • 74
  • 101
SeriousLee
  • 1,301
  • 4
  • 20
  • 42

4 Answers4

1

You could use a reducer and build your new array of objects:

const input = [{
  id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd',
  first_name: 'SomeName',
  email: 'some@email.com',
  rName: 'User', // 0,1
  rAuthority: 'ROLE_USER', // 0,1
  pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e',
  pMobile: '012 345 6789',
  atId: '90db0c5d-3030-44aa-9dc0-40242af0d5c5', // 0,2
  atPlatform: 'web',
}, {
  id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd',
  first_name: 'SomeName',
  email: 'some@email.com',
  rName: 'User', // 0,1
  rAuthority: 'ROLE_USER', // 0,1
  pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e',
  pMobile: '012 345 6789',
  atId: 'e7d53cab-a9b9-40ae-9271-11d79c2f269c', // 1,3
  atPlatform: 'web',
}, {
  id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd',
  first_name: 'SomeName',
  email: 'some@email.com',
  rName: 'Admin', // 2,3
  rAuthority: 'ROLE_ADMIN', // 2,3
  pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e',
  pMobile: '012 345 6789',
  atId: '90db0c5d-3030-44aa-9dc0-40242af0d5c5', // 0,2
  atPlatform: 'web',
}, {
  id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd',
  first_name: 'SomeName',
  email: 'some@email.com',
  rName: 'Admin', // 2,3
  rAuthority: 'ROLE_ADMIN', // 2,3
  pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e',
  pMobile: '012 345 6789',
  atId: 'e7d53cab-a9b9-40ae-9271-11d79c2f269c', // 1,3
  atPlatform: 'web',
}];

console.log(input.reduce((acc, val, ind) => {
  if (!acc.find(el => el.id === val.id)) {
    acc.push({
      id: val.id,
      first_name: val.first_name,
      email: val.email,
      roles: [],
      profiles: [],
      tokens: []
    });
  }
  else {
    const el = acc.find(el => el.id === val.id);
    if (!el.roles.find(a => a.authority === val.rAuthority)) {
      el.roles.push({
        authority: val.rAuthority,
        name: val.rName
      });
    }
    if (!el.profiles.find(a => a.id === val.pId)) {
      el.profiles.push({
        id: val.pId,
        mobile: val.pMobile
      });
    }
    if (!el.tokens.find(a => a.id === val.atId)) {
      el.tokens.push({
        id: val.atId,
        platform: val.atPlatform
      });
    }
  }
  return acc;
}, []));
quirimmo
  • 9,800
  • 3
  • 30
  • 45
1

A slightly different approach from quirimmo's answer, using objects as intermediate values to map each item/subitem to its corresponding identifier, and then using Object.values to revert them back:

const input = [
  {id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd', first_name: 'SomeName', email: 'some@email.com', rName: 'User', rAuthority: 'ROLE_USER', pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e', pMobile: '012 345 6789', atId: '90db0c5d-3030-44aa-9dc0-40242af0d5c5', atPlatform: 'web'},
  {id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd', first_name: 'SomeName', email: 'some@email.com', rName: 'User', rAuthority: 'ROLE_USER', pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e', pMobile: '012 345 6789', atId: 'e7d53cab-a9b9-40ae-9271-11d79c2f269c', atPlatform: 'web'},
  {id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd', first_name: 'SomeName', email: 'some@email.com', rName: 'Admin', rAuthority: 'ROLE_ADMIN', pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e', pMobile: '012 345 6789', atId: '90db0c5d-3030-44aa-9dc0-40242af0d5c5', atPlatform: 'web'},
  {id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd', first_name: 'SomeName', email: 'some@email.com', rName: 'Admin', rAuthority: 'ROLE_ADMIN', pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e', pMobile: '012 345 6789', atId: 'e7d53cab-a9b9-40ae-9271-11d79c2f269c', atPlatform: 'web'}
];

const result = Object.values(input.reduce((result, item) => {
  if (!(item.id in result)) {
    result[item.id] = {id: item.id, first_name: item.first_name, email: item.email, roles: {}, profiles: {}, tokens: {}};
  }
  result[item.id].roles[item.rName] = {name: item.rName, authority: item.rAuthority};
  result[item.id].profiles[item.pId] = {id: item.pId, mobile: item.pMobile};
  result[item.id].tokens[item.atId] = {id: item.atId, platform: item.atPlatform};
  return result;
}, {})).map(item => {
  ['roles', 'profiles', 'tokens'].forEach(prop => item[prop] = Object.values(item[prop]));
  return item;
});

console.log(result);

Basically:

  • Array.reduce transforms the initial array into an object indexed by items' identifiers and containing roles/profiles/tokens objects also indexed by their corresponding identifiers,
  • Object.values converts the resulted object back to an array,
  • Array.map calls Object.values on each item's roles/profiles/tokens to transform them into the desired arrays.
Jeto
  • 14,596
  • 2
  • 32
  • 46
1

Using lodash, you can create a partially applied function that gets properties from an object using _.pickBy() and a regex with the string that the property should start with. Then you can create an attribute creator using the relevant string (at for tokens).

Now _.groupBy() the id of the items, and map each group to create the object using the attribute creators.

const data = [{"id":"6b6574cf-d77a-4ed8-852f-cb60a0d377cd","first_name":"SomeName","email":"some@email.com","rName":"User","rAuthority":"ROLE_USER","pId":"e7da65a9-ea2d-4c77-82f6-e1addc78fb6e","pMobile":"012 345 6789","atId":"90db0c5d-3030-44aa-9dc0-40242af0d5c5","atPlatform":"web"},{"id":"6b6574cf-d77a-4ed8-852f-cb60a0d377cd","first_name":"SomeName","email":"some@email.com","rName":"User","rAuthority":"ROLE_USER","pId":"e7da65a9-ea2d-4c77-82f6-e1addc78fb6e","pMobile":"012 345 6789","atId":"e7d53cab-a9b9-40ae-9271-11d79c2f269c","atPlatform":"web"},{"id":"6b6574cf-d77a-4ed8-852f-cb60a0d377cd","first_name":"SomeName","email":"some@email.com","rName":"Admin","rAuthority":"ROLE_ADMIN","pId":"e7da65a9-ea2d-4c77-82f6-e1addc78fb6e","pMobile":"012 345 6789","atId":"90db0c5d-3030-44aa-9dc0-40242af0d5c5","atPlatform":"web"},{"id":"6b6574cf-d77a-4ed8-852f-cb60a0d377cd","first_name":"SomeName","email":"some@email.com","rName":"Admin","rAuthority":"ROLE_ADMIN","pId":"e7da65a9-ea2d-4c77-82f6-e1addc78fb6e","pMobile":"012 345 6789","atId":"e7d53cab-a9b9-40ae-9271-11d79c2f269c","atPlatform":"web"}];
    
const createAttribute = startStr => {
  const pattern = new RegExp(`^${startStr}[A-Z]`);
  
  return arr => _.uniqWith(_.map(arr, _.flow([
    obj => _.pickBy(obj, (v, k) => pattern.test(k)),
    obj => _.mapKeys(obj, (v, k) => 
      k.replace(/(^[a-z]+)([A-Z].+$)/, '$2') // key name without the prefix
      .toLowerCase()
    )
  ])), _.isEqual);
};

const createRoles = createAttribute('r');
const createProfiles = createAttribute('p');
const createTokens = createAttribute('at');
    
const fn = _.flow([
  arr => _.groupBy(arr, 'id'),
  groups => _.map(groups, group => {
    const { id, first_name, email } = _.first(group);
    
    return {
      id,
      first_name,
      email,
      roles: createRoles(group),
      profiles: createProfiles(group),
      tokens: createTokens(group)
    };
  })
]);

const result = fn(data);

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

You could take some arrays for the wanted grouping with their keys and aliases.

var data = [{ id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd', first_name: 'SomeName', email: 'some@email.com', rName: 'User', rAuthority: 'ROLE_USER', pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e', pMobile: '012 345 6789', atId: '90db0c5d-3030-44aa-9dc0-40242af0d5c5', atPlatform: 'web' }, { id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd', first_name: 'SomeName', email: 'some@email.com', rName: 'User', rAuthority: 'ROLE_USER', pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e', pMobile: '012 345 6789', atId: 'e7d53cab-a9b9-40ae-9271-11d79c2f269c', atPlatform: 'web' }, { id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd', first_name: 'SomeName', email: 'some@email.com', rName: 'Admin', rAuthority: 'ROLE_ADMIN', pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e', pMobile: '012 345 6789', atId: '90db0c5d-3030-44aa-9dc0-40242af0d5c5', atPlatform: 'web' }, { id: '6b6574cf-d77a-4ed8-852f-cb60a0d377cd', first_name: 'SomeName', email: 'some@email.com', rName: 'Admin', rAuthority: 'ROLE_ADMIN', pId: 'e7da65a9-ea2d-4c77-82f6-e1addc78fb6e', pMobile: '012 345 6789', atId: 'e7d53cab-a9b9-40ae-9271-11d79c2f269c', atPlatform: 'web' }],
    head = [['id'], ['first_name'], ['email']],
    sub = [
        ['roles', [['name', 'rName'], ['authority', 'rAuthority']]],
        ['profiles', [['id', 'pId'], ['mobile', 'pMobile']]],
        ['tokens', [['id', 'atId'], ['platform', 'atPlatform']]]
    ],
    result = data.reduce((r, o) => {
        const mapData = ([key, alias = key]) => ({ [key]: o[alias] });
        var temp = r.find(q => q[head[0]] === o[head[0]]);
        if (!temp) {
            r.push(temp = Object.assign(...head.map(mapData)));
        }
        sub.forEach(([s, keys]) => {
            temp[s] = temp[s] || [];
            var inner = temp[s].find(q => q[keys[0][0]] === o[keys[0][1]]);
            if (!inner) {
                temp[s].push(Object.assign(...keys.map(mapData)));
            }
        });
        return r;
    }, []);

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