How to deep merge two collections by duplicate key in JavaScript/Lodash? - javascript

I would like to merge two collections by duplicate key in javascript, here is example collections:
let collection1 = [
{
title: 'Overview',
key: 'Test-overview',
isLeaf: true
},
{
title: 'Folder 1',
key: 'Test-Folder_1',
children: [
{
title: 'Folder 1 Content 1',
key: 'Test-Folder_1-Content_1',
isLeaf: true,
},
],
}
]
let collection2 = [
{
title: 'Folder 1',
key: 'Test-Folder_1',
children: [
{
title: 'Sub Folder 1 in Folder 1',
key: 'Test-Folder_1-Sub_Folder_1',
children: [
{
title: 'Sub Folder 1 Conetent',
key: 'Test-Folder_1-Sub_Folder_1-Content',
isLeaf: true,
},
],
},
],
}
]
and this is example output:
let exampleOutput = [
{
title: 'Overview',
key: 'Test-overview',
isLeaf: true
},
{
title: 'Folder 1',
key: 'Test-Folder_1',
children: [
{
title: 'Folder 1 Content 1',
key: 'Test-Folder_1-Content_1',
isLeaf: true,
},
{
title: 'Sub Folder 1 in Folder 1',
key: 'Test-Folder_1-Sub_Folder_1',
children: [
{
title: 'Sub Folder 1 Conetent',
key: 'Test-Folder_1-Sub_Folder_1-Content',
isLeaf: true,
},
],
},
],
}
]
How can I achieve this output with Javascript? I tried with Lodash _.merge and _.mergeWith but the output is not what I want.
I also tried this link: Merge JavaScript objects in array with same key answer by #BenG but it only able to merge the first layer of the collections, which mean if I have collection3 that contain another content in Test-Folder_1-Sub_Folder_1, it will be replaced by the first layer of the new collection.

You can use recursion:
let collection1 = [{title: 'Overview', key: 'Test-overview', isLeaf: true}, {title: 'Folder 1', key: 'Test-Folder_1', children: [{title: 'Folder 1 Content 1', key: 'Test-Folder_1-Content_1', isLeaf: true, },],}]
let collection2 = [{title: 'Folder 1', key: 'Test-Folder_1', children: [{title: 'Sub Folder 1 in Folder 1', key: 'Test-Folder_1-Sub_Folder_1', children: [{title: 'Sub Folder 1 Conetent', key: 'Test-Folder_1-Sub_Folder_1-Content', isLeaf: true,},],},],}]
function merge(collections){
var formed = {}
for (var i of collections){
key = JSON.stringify(Object.keys(i).filter(x => x != 'children').map(x => [x, i[x]]))
if (!(key in formed)){
formed[key] = []
}
formed[key] = [...formed[key], ...('children' in i ? i.children : [])]
}
return Object.keys(formed).map(x => ({...Object.fromEntries(JSON.parse(x)), ...(formed[x].length ? {'children':merge(formed[x])} : {})}))
}
console.log(merge([...collection1, ...collection2]))

Related

Push value to multiple arrays

const array = [{
id: 1,
name: 'test',
attrs: [{
name: 'Attribute 1',
description: 'Description 1',
}],
values: [{
name: 'value 1',
attrs: [{
name: 'Attribute 1',
type: 'Type 1',
},
{
name: 'Attribute 2',
type: 'Type 2',
},
],
}, ],
}, ];
const newArray =
array.map((item) => {
return {
...item,
isNegative: true
};
});
console.log(newArray);
I receive data as displayed in the const array. I need to push a value 'isNegative' to array[].attrs and to each values.attrs. I'm only able to do it to the array[].attrs. How can I do it to the others?
You're only mapping over the top-level array. If you need to map over the arrays within each object, that's another call to .map(). For example:
const array = [{
id: 1,
name: 'test',
attrs: [{
name: 'Attribute 1',
description: 'Description 1',
}],
values: [{
name: 'value 1',
attrs: [{
name: 'Attribute 1',
type: 'Type 1',
},
{
name: 'Attribute 2',
type: 'Type 2',
},
],
}, ],
}, ];
const newArray =
array.map((item) => {
return {
...item,
isNegative: true,
attrs: item.attrs.map((attr) => {
return {
...attr,
isNegative: true
}
})
};
});
console.log(newArray);
Same with the values property, any array within objects in values, etc. Any array that you want to map to a new structure, call .map() on it.

How to convert flat array containing a child "path" array into a nested array of objects based on the path?

I have an array of objects, each containing a path property which holds the value of "paths" to which I'd like to map the array elements to.
let myData = [
{
path: ['Movies', 'Comedies', 'TopRanked'],
name: 'The Hangover',
id: '1',
},
{
path: ['Movies', 'Comedies', 'TopRanked'],
name: 'Eurotrip',
id: '2',
},
{
path: ['Movies', 'Action'],
name: 'Need for Speed',
id: '3',
},
{
path: ['Life'],
name: 'Not so bad',
id: '4',
},
{
path: ['Life', 'Financial', 'Income'],
name: 'Making Hundreds',
id: '5',
},
{
path: ['Life', 'Financial', 'Income'],
name: 'Making Thousands',
id: '6',
},
{
path: ['Life', 'MonthlySpent'],
name: 'Just a little bit',
id: '7',
},
{
path: ['Life', 'MonthlySpent'],
name: 'Living large',
id: '8',
},
];
console.log(myData);
Essentially, the result I am looking for is a breakdown of that array into as many as nested arrays as needed (relative to all possible available paths), with each retaining its "type" - either a parent or an item. So the desired output is like so:
let myTree = [
{
name: 'Movies',
type: 'parent',
children: [
{
name: 'Comedies',
type: 'parent',
children: [
{
name: 'TopRanked',
type: 'parent',
children: [
{
name: 'The Hangover',
type: 'item',
id: 1,
path: ['Movies', 'Comedies', 'TopRanked']
},
{
name: 'Eurotrip',
type: 'item',
id: 2,
path: ['Movies', 'Comedies', 'TopRanked'],
}
]
},
]
},
{
name: 'Action',
type: 'parent',
children: [
{
name: 'Need for Speed',
type: 'item',
id: 3,
path: ['Movies', 'Action'],
},
]
}
]
},
{
name: 'Life',
type: 'parent',
children: [
{
name: 'Not so bad',
type: 'item',
id: 4,
path: ['Life'],
},
{
name: 'Financial',
type: 'parent',
children: [
{
name: 'Income',
type: 'parent',
children: [
{
name: 'Making Hundreds',
type: 'item',
id: 5,
path: ['Life', 'Financial', 'Income'],
},
{
name: 'Making Thousands',
type: 'item',
id: 6,
path: ['Life', 'Financial', 'Income'],
}
]
}
]
},
{
name: 'MonthlySpent',
type: 'parent',
children: [
{
name: 'Just a little bit',
type: 'item',
id: 7,
path: ['Life', 'MonthlySpent'],
},
{
name: 'Living Large',
type: 'item',
id: 8,
path: ['Life', 'MonthlySpent'],
}
]
}
]
}
]
console.log(myTree);
I tried the following, and while the tree structure is created, the "item"-types are not placed as the array-value of the last nested "parent" type:
function treeData(data) {
var result = [],
hash = { _: { children: result } };
data.forEach(function (object) {
object.path.reduce(function (o, p) {
if (!o[p]) {
o[p] = { _: { name: p, children: [] } };
o._.children.push(o[p]._);
}
return o[p];
}, hash)._.name = object.name;
});
return result;
}
Would appreciate a working solution, as I am wracking my head and can't find one. Tnnx.
The approach below follows a similar pattern to your code i.e. loop every object, but instead of a reduce simply loops every item in path and creates a branch off the root. When there are no more 'branches' then add the original object. See the comments.
let myData = data();
let myTree = treeData(data);
console.log(myTree);
function treeData(data) {
let root = {"children": []} // create origin
for (obj of myData) { // loop items in the data
obj.type = "Item"; // add a property to suit your output
let tree = root; // start at root every object
for (path of obj.path) { // loop over items in path
let branch = tree.children.find(k => k.name == path); // look for branch
if (!branch) { // if no branch, create one
branch = {"name": path, "type": "parent", "children": []}
tree.children.push(branch); // push this into children of current level
}
tree = branch; // set tree to branch before processing next item in path
}
tree.children.push(obj); // add the item to the hierarchy after path is exhausted
}
return root.children; // return children of the root to suit your output
}
function data() {
return [
{
path: ['Movies', 'Comedies', 'TopRanked'],
name: 'The Hangover',
id: '1',
},
{
path: ['Movies', 'Comedies', 'TopRanked'],
name: 'Eurotrip',
id: '2',
},
{
path: ['Movies', 'Action'],
name: 'Need for Speed',
id: '3',
},
{
path: ['Life'],
name: 'Not so bad',
id: '4',
},
{
path: ['Life', 'Financial', 'Income'],
name: 'Making Hundreds',
id: '5',
},
{
path: ['Life', 'Financial', 'Income'],
name: 'Making Thousands',
id: '6',
},
{
path: ['Life', 'MonthlySpent'],
name: 'Just a little bit',
id: '7',
},
{
path: ['Life', 'MonthlySpent'],
name: 'Living large',
id: '8',
},
];
}
.as-console-wrapper { max-height: 100% !important; top: 0; }

create a pruned copy of tree in javascript

I'm trying to create a pruned version of the tree below where I have the source data/tree:
const treeData = [{
title: '0-0',
key: '0-0',
children: [{
title: '0-0-0',
key: '0-0-0',
children: [
{ title: '0-0-0-0', key: '0-0-0-0', children: [] },
{ title: '0-0-0-1', key: '0-0-0-1', children: [] },
{ title: '0-0-0-2', key: '0-0-0-2', children: [] },
],
}, {
title: '0-0-1',
key: '0-0-1',
children: [
{ title: '0-0-1-0', key: '0-0-1-0', children: [] },
{ title: '0-0-1-1', key: '0-0-1-1', children: [] },
{ title: '0-0-1-2', key: '0-0-1-2', children: [] },
],
}, {
title: '0-0-2',
key: '0-0-2',
children: []
}],
}, {
title: '0-1',
key: '0-1',
children: [
{ title: '0-1-0-0', key: '0-1-0-0', children: [] },
{ title: '0-1-0-1', key: '0-1-0-1', children: [] },
{ title: '0-1-0-2', key: '0-1-0-2', children: [] },
],
}, {
title: '0-2',
key: '0-2',
children: []
}];
and an array of leaf nodes as inputs.
const leafNodes = ['0-0-1-2', '0-1-0-1', '0-1-0-2']
Given this input, I would want this pruned tree that uses the leaf nodes to build all paths from the root to each leaf:
const pruned [{
title: '0-0',
key: '0-0',
children: [{
title: '0-0-1',
key: '0-0-1',
children: [
{ title: '0-0-1-2',
key: '0-0-1-2',
children: []
}
]
}]
}, {
title: '0-1',
key: '0-1',
children: [{
title: '0-1-0-1',
key: '0-1-0-1',
children: []
}, {
title: '0-1-0-2',
key: '0-1-0-2',
children: []
}]
}]
I was thinking of building the copy node by node instead of copying the data source and then taking away the paths not buildable based on the array/list of leaf nodes as I figured that would be the easiest to grok for maintainability purposes but even then, I'm puzzled as to how to coordinate the process, especially when accounting for the middle nodes that have already been added to my copy tree in progress as would be the case for '0-1-0-1' and '0-1-0-2'. At any rate, I've been stumped for awhile and threw my hands up. The code referenced is javascript but I'd be open to answers in other languages similar enough to javascript.
You could build new array/objects by finding the target key and collect all objects to it by returning the arrays with the necessary nodes.
function getParts(array, leafes) {
var result = [];
array.forEach(o => {
var children;
if (leafes.includes(o.key)) {
result.push(o);
return;
}
children = getParts(o.children, leafes);
if (children.length) {
result.push(Object.assign({}, o, { children }));
}
});
return result;
}
const
treeData = [{ title: '0-0', key: '0-0', children: [{ title: '0-0-0', key: '0-0-0', children: [{ title: '0-0-0-0', key: '0-0-0-0', children: [] }, { title: '0-0-0-1', key: '0-0-0-1', children: [] }, { title: '0-0-0-2', key: '0-0-0-2', children: [] }] }, { title: '0-0-1', key: '0-0-1', children: [{ title: '0-0-1-0', key: '0-0-1-0', children: [] }, { title: '0-0-1-1', key: '0-0-1-1', children: [] }, { title: '0-0-1-2', key: '0-0-1-2', children: [] }] }, { title: '0-0-2', key: '0-0-2', children: [] }] }, { title: '0-1', key: '0-1', children: [{ title: '0-1-0-0', key: '0-1-0-0', children: [] }, { title: '0-1-0-1', key: '0-1-0-1', children: [] }, { title: '0-1-0-2', key: '0-1-0-2', children: [] }] }, { title: '0-2', key: '0-2', children: [] }],
leafNodes = ['0-0-1-2', '0-1-0-1', '0-1-0-2'],
pruned = getParts(treeData, leafNodes);
console.log(pruned);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Get single property array from an array of objects

I have this array of objects and I want to get all the controls from this to another array:
this.formModel = {
sections: [
{
title: 'Section 01',
controls: [
new FormControlInput({
key: 'name 01',
label: 'Name 01'
}),
new FormControlSelect({
key: 'abc',
label: 'Abc'
})
]
},
{
title: 'Section 02',
controls: [
new FormControlInput({
key: 'name 02',
label: 'Name 02'
})
]
}
]
};
I am using map for this but I am not getting single array, I am getting array of arrays:
this.formModel.sections.map(function (x) { return x.controls; })
Getting this:
[
{
[{
key: 'name 01',
label: 'Name 01'
},
{
key: 'abc',
label: 'Abc'
}]
},
{
[{
key: 'name 02',
label: 'Name 02'
}]
}
]
What I want is this:
[
{
key: 'name 01',
label: 'Name 01'
},
{
key: 'abc',
label: 'Abc'
},
{
key: 'name 02',
label: 'Name 02'
}
]
You just need to flatten your array after mapping:
var obj = {
sections: [{
title: 'Section 01',
controls: [
{ key: 'name 01', label: 'Name 01' },
{ key: 'abc', label: 'Abc' }
]
}, {
title: 'Section 02',
controls: [
{ key: 'name 02', label: 'Name 02' }
]
}
]
};
var mapped = obj.sections.map(function (x) { return x.controls; });
var flattened = [].concat.apply([], mapped);
console.log(flattened);
To simplify your example:
// This is your structure:
var sections= [{
controls: [{}, {}] // C1
}, {
controls: [{}] // C2
}
];
// With the map, grabbing each `controls` property, and using that as an entry in your array:
var mapped = sections.map(function (x) { return x.controls; });
console.log(mapped);
// [[{},{}],[{}]]
// ^ C1 ^ C2
// We need to remove that extra layer of arrays:
var flattened = [].concat.apply([], mapped);
console.log(flattened);
You can use reduce to flatten the hierarchy
formModel.sections
.map(x => x.controls)
.reduce((prev, current) => prev.concat(current), [])
Use reduce instead of map:
let formModel = {
sections: [
{
title: 'Section 01',
controls: [
{
key: 'name 01',
label: 'Name 01'
},
{
key: 'abc',
label: 'Abc'
}
]
},
{
title: 'Section 02',
controls: [
{
key: 'name 02',
label: 'Name 02'
}
]
}
]
};
let result = formModel.sections.reduce((res, section) => {
return res = res.concat(section.controls);
}, []);
console.log(result);

Merging Arrays of Objects by Key/Values

I have two separate arrays of objects that I need to merge based if a specific key value matches. Might make more sense after analyzing the data:
Array 1
let categories = [
{ id: 5, slug: 'category-5', items: [] },
{ id: 4, slug: 'category-4', items: [] },
{ id: 3, slug: 'category-3', items: [] },
]
Array 2
let items = [
{ id: 5, data: [{ title: 'item title', description: 'item description' }] },
{ id: 5, data: [{ title: 'item title 2', description: 'item description 2' }] },
{ id: 4, data: [{ title: 'item title 4', description: 'item description 4' }] },
]
Expected Output
let mergedOutput = [
{ id: 5, slug: 'category-5',
items: [
{ title: 'item title', description: 'item description' },
{ title: 'item title 2', description: 'item description 2' }
]
},
{ id: 4, slug: 'category-4',
items: [
{ title: 'item title 4', description: 'item description 4' },
]
},
{ id: 3, slug: 'category-3', items: [] },
]
So....I need to add Array 2 to Array 1 if their id's match.
Array 1 will stay the same, but if Array 2 matches, the items property of Array 1 (empty) will be replaced by the data property of Array 2
I know this is a pretty basic / and redundant question, but I can't find the resources for my use case / object structure.
I was able to easily group arrays with lodash -- so if there is a similar solution with that library -- that would good! Or just some direction would suffice.
Thanks in advance!
You can loop first array and then use filter to get objects with same id as current element and add that items to current object.
let categories = [
{ id: 5, slug: 'category-5', items: [] },
{ id: 4, slug: 'category-4', items: [] },
{ id: 3, slug: 'category-3', items: [] },
]
let items = [
{ id: 5, data: [{ title: 'item title', description: 'item description' }] },
{ id: 5, data: [{ title: 'item title 2', description: 'item description 2' }] },
{ id: 4, data: [{ title: 'item title 4', description: 'item description 4' }] },
]
categories.forEach(function(e) {
var i = items.filter(a => a.id == e.id).map(a => a.data);
e.items = i;
})
console.log(categories)
You could reduce the items into categories:
let res = items.reduce((a, b) => {
let it = a.find(e => e.id === b.id);
if (! it) return a;
it.items = it.items.concat(b.data);
return a;
}, categories);
let categories = [{
id: 5,
slug: 'category-5',
items: []
},
{
id: 4,
slug: 'category-4',
items: []
},
{
id: 3,
slug: 'category-3',
items: []
},
];
let items = [{
id: 5,
data: [{
title: 'item title',
description: 'item description'
}]
},
{
id: 5,
data: [{
title: 'item title 2',
description: 'item description 2'
}]
},
{
id: 4,
data: [{
title: 'item title 4',
description: 'item description 4'
}]
},
];
let res = items.reduce((a, b) => {
let it = a.find(e => e.id === b.id);
if (! it) return a;
it.items = it.items.concat(b.data);
return a;
}, categories);
console.log(res);
It might be faster to get the ids in an object first, so we don't have to use find on the same id many times:
function merge(result, toMerge, mergeInto) {
let i = 0, hm = {};
for (let {id} of categories) {
hm[id] = i;
i++;
}
return toMerge.reduce((a,b) => {
let it = a[hm[b.id]];
if (!it) return a;
it[mergeInto] = it[mergeInto].concat(b.data);
return a;
}, result);
}
let categories = [
{ id: 5, slug: 'category-5', items: [] },
{ id: 4, slug: 'category-4', items: [] },
{ id: 3, slug: 'category-3', items: [] },
];
let items = [
{ id: 5, data: [{ title: 'item title', description: 'item description' }] },
{ id: 5, data: [{ title: 'item title 2', description: 'item description 2' }] },
{ id: 4, data: [{ title: 'item title 4', description: 'item description 4' }] },
];
function merge(result, toMerge, mergeInto) {
let i = 0, hm = {};
for (let {id} of categories) {
hm[id] = i;
i++;
}
return toMerge.reduce((a,b) => {
let it = result[hm[b.id]];
if (!it) return a;
it[mergeInto] = it[mergeInto].concat(b.data);
return a;
}, result);
}
console.log(merge(categories, items, 'items'));
I would make the categories as hash map and the key would be the id and iterate over all the items only.
then you get O(N) solution.

Categories

Resources