Zhurik
Zhurik

Reputation: 166

What is the best way to add data from one array to another based on values in arrays?

I have 2 arrays of objects and I need to add data to them based on matching ids. I can check every value in both arrays, check if their id matches and use them. One parent can have many children, but child can have only one parent. Code example:

const parents = [
  {id: 1, name: 'John', kids: []}, 
  {id: 2, name: 'Caren', kids: []}
];
const children = [
  {parent_id: 2, name: 'Huey'},
  {parent_id: 1, name: 'Dewey'},
  {parent_id: 1, name: 'Louie'}
];

for (let i = 0; i < children.length; i++) {
  for (let j = 0; j < parents.length; j++) {
    if (parents[j].id == children[i].parent_id) {
      parents[j].kids.push(children[i]);
      break;
    }
  }
}

console.log(parents);

How can I improve it in any way?

Upvotes: 0

Views: 176

Answers (5)

Peter Seliger
Peter Seliger

Reputation: 13431

Like with a lot of use cases, similar to the one provided by the OP, a reduce based approach combined with a generically implemented callback assures a versatile usage.

One wants to operate the child list, since this list most probably comes with more items than the parent list. Of course this is just an assumption (best guess).

In order to not iterate the parent list again and again (via e.g. find) for each iteration step of a child list item, one programmatically builds a look-up table called index which is a member of the collector object that is passed into each next iteration and also needs to be provided as the reduce method's start value.

In order to demonstrate the flexibility of the approach, its example code comes with two different use cases, a non mutating one that leaves the original parent array untouched, but creates a new one matching the desired result, and a second one which does mutate the parent array ...

function pushItemIntoRelatedParentChildList(collector, item) {
  const { index, list } = collector;
  const { parent_id } = item;

  // make use of `index` for a faster parent lookup.
  let parent = index[parent_id];
  if (!parent) {

    // programmatically build the parent `index`.
    parent = index[parent_id] = list.find(({ id }) => id === parent_id);
  }
  // be defensive in case of child parent relationship does not exist.
  if (parent && parent.kids) {

    parent.kids.push(item);
  }
  return collector;
}


const parents = [
  {id: 1, name: 'John', kids: []}, 
  {id: 2, name: 'Caren', kids: []}
];
const children = [
  {parent_id: 2, name: 'Huey'},
  {parent_id: 1, name: 'Dewey'},
  {parent_id: 1, name: 'Louie'}
];


// non `parents` mutating usage.
const newParentList = children.reduce(pushItemIntoRelatedParentChildList, {

  index: {},
  list: parents.map(({ kids, ...rest }) => ({ ...rest, kids: [] }))

}).list;

console.log('(parents === newParentList) ?', (parents === newParentList));
console.log('parents :', parents);
console.log('newParentList :', newParentList);


// `parents` mutating usage.
children.reduce(pushItemIntoRelatedParentChildList, {
  index: {},
  list: parents
});

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

Upvotes: 1

George
George

Reputation: 36794

If it works, great! Although for...of may be of some interest to you:

for (const child of children) {
  for (const parent of parents) {
    if (parent.id == child.parent_id) {
      parent.kids.push(child);
      break;
    }
  }
}

Other than that..

This is also achievable through some fairly simple mapping and filtering:

const parents = [{
  id: 1,
  name: 'John',
  kids: []
}, {
  id: 2,
  name: 'Caren',
  kids: []
}];

const children = [{
    parent_id: 2,
    name: 'Huey'
  },
  {
    parent_id: 1,
    name: 'Dewey'
  },
  {
    parent_id: 1,
    name: 'Louie'
  }
];

// Remap our parents to an array of new objects
const withChildren = parents.map(parent => ({
  // which includes the parent data
  ...parent,
  // but also provides the kids
  kids: children
    // filter the kids based on whether or not the parent_id matches the currently mapped parent
    .filter(({ parent_id }) => parent_id === parent.id)
}));

console.log(withChildren);

Upvotes: 1

Ado
Ado

Reputation: 817

You could use an Hashtable and save the parent_id as key and the children in an array as value.
So you need to loop only once through the parents and once through the children. But you need extra space for the object.

const parents = [{ id: 1, name: 'John', kids: [] }, { id: 2, name: 'Caren', kids: [] }]
const children = [
  { parent_id: 2, name: 'Huey' },
  { parent_id: 1, name: 'Dewey' },
  { parent_id: 1, name: 'Louie' }
]

const childrenMap = {}

children.forEach(child => {
  childrenMap[child.parent_id]
    ? childrenMap[child.parent_id].push(child)
    : childrenMap[child.parent_id] = [child]
})

parents.forEach(parent => childrenMap[parent.id] && parent.kids.push(...childrenMap[parent.id]))

console.log('Parents', JSON.stringify(parents, null, 2))

Upvotes: 0

John V
John V

Reputation: 875

Try this

const parents = [{id: 1, name: 'John', kids: []}, {id: 2, name: 'Caren', kids: []}];
const children = [
  {parent_id: 2, name: 'Huey'},
  {parent_id: 1, name: 'Dewey'},
  {parent_id: 1, name: 'Louie'}
];
    
let result = [];
parents.map(v=>result[v.id]=v);
children.map(v=>(result[v.parent_id] && result[v.parent_id].kids.push(v)));
let result = result.filter(v=>!!v);

Upvotes: 0

Keith
Keith

Reputation: 24211

Personally I like for loops, mainly because they work well with async code too, but you could replace with forEach & find.

const parents = [
  {id: 1, name: 'John', kids: []}, 
  {id: 2, name: 'Caren', kids: []}];
const children = [
  {parent_id: 2, name: 'Huey'},
  {parent_id: 1, name: 'Dewey'},
  {parent_id: 1, name: 'Louie'}
];

children.forEach(child => {
  const parent = parents.find(
     parent => parent.id == child.parent_id);
  if (parent) parent.kids.push(child);
});


console.log(parents);

Upvotes: 2

Related Questions