How could I solve a case where I have to merge trees with repeated values? For example something like that:
const firstTree = {
id: 1,
name: 'name1',
otherProperty: 'property1',
children: [
{
id: '1a',
name: 'name1a',
otherProperty: 'property1a',
children: []
},
{
id: '1b',
name: 'name1b',
otherProperty: 'property1b',
children: [
{
id: '1ba',
name: 'name1ba',
otherProperty: 'property1ba',
children: []
},
{
id: '1bb',
name: 'name1bb',
otherProperty: 'property1bb',
children: []
}
]
}
]
};
const secondTree = {
id: 1,
name: 'name1',
otherProperty: 'property1',
children: [
{
id: '1b',
name: 'name1b',
otherProperty: 'property1b',
children: [
{
id: '1ba',
name: 'name1ba',
otherProperty: 'property1ba',
children: []
},
{
id: '2ba',
name: 'name2ba',
otherProperty: 'property2ba',
children: []
}
]
}
]
};
const thirdTree = {
id: '3',
name: 'name3',
otherProperty: 'property3',
children: [
{
id: '3a',
name: 'name3a',
otherProperty: 'property3a',
children: []
},
{
id: '3b',
name: 'name3b',
otherProperty: 'property3b',
children: []
}
]
};
const entryArray = [firstTree, secondTree, thirdTree];
And I want to have as a result merged the first tree with the second and additionaly the third tree where there were no common elements :
const mergedFirstAndSecond = {
id: 1,
name: 'name1',
otherProperty: 'property1',
children: [
{
id: '1a',
name: 'name1a',
otherProperty: 'property1a',
children: []
},
{
id: '1b',
name: 'name1b',
otherProperty: 'property1b',
children: [
{
id: '1ba',
name: 'name1ba',
otherProperty: 'property1ba',
children: []
},
{
id: '1bb',
name: 'name1bb',
otherProperty: 'property1bb',
children: []
},
{
id: '2ba',
name: 'name2ba',
otherProperty: 'property2ba',
children: []
}
]
}
]
};
const result = [mergedFirstAndSecond, thirdTree];
I mean a solution that would also work if the duplicate elements also occurred in three different trees, not just two. I will be very grateful for any suggestions.
You could first create the tree as a nested map structure (where each children property is a Map instance), and merge all trees in that structure. This will allow optimised lookup to see where an entry needs to be merged into.
Once you have that, replace all these children properties back to arrays.
function mergeTrees(...trees) {
function fillMap(src, map) {
let dst = map.get(src.id);
if (!dst) map.set(src.id, dst = { ...src, children: new Map });
for (let child of (src.children ?? [])) fillMap(child, dst.children);
}
// Merge into nested Map structure:
let mergedTree = new Map;
for (let tree of trees) fillMap(tree, mergedTree);
// Convert each map to array:
const toArrays = map => Array.from(map.values(), node =>
Object.assign(node, { children: toArrays(node.children) })
);
return toArrays(mergedTree);
}
// Demo
const firstTree = {id: 1,name: 'name1',otherProperty: 'property1',children: [{id: '1a',name: 'name1a',otherProperty: 'property1a',children: []}, {id: '1b',name: 'name1b',otherProperty: 'property1b',children: [{id: '1ba',name: 'name1ba',otherProperty: 'property1ba',children: []}, {id: '1bb',name: 'name1bb',otherProperty: 'property1bb',children: []}]}]};
const secondTree = {id: 1,name: 'name1',otherProperty: 'property1',children: [{id: '1b',name: 'name1b',otherProperty: 'property1b',children: [{id: '1ba',name: 'name1ba',otherProperty: 'property1ba',children: []}, {id: '2ba',name: 'name2ba',otherProperty: 'property2ba',children: []}]}]};
const thirdTree = {id: '3',name: 'name3',otherProperty: 'property3',children: [{id: '3a',name: 'name3a',otherProperty: 'property3a',children: []}, {id: '3b',name: 'name3b',otherProperty: 'property3b',children: []}]};
const entryArray = mergeTrees(firstTree, secondTree, thirdTree);
console.log(entryArray);
You can use recursion to merge the trees on their ids:
var firstTree = {'id': 1, 'name': 'name1', 'otherProperty': 'property1', 'children': [{'id': '1a', 'name': 'name1a', 'otherProperty': 'property1a', 'children': []}, {'id': '1b', 'name': 'name1b', 'otherProperty': 'property1b', 'children': [{'id': '1ba', 'name': 'name1ba', 'otherProperty': 'property1ba', 'children': []}, {'id': '1bb', 'name': 'name1bb', 'otherProperty': 'property1bb', 'children': []}]}]};
var secondTree = {'id': 1, 'name': 'name1', 'otherProperty': 'property1', 'children': [{'id': '1b', 'name': 'name1b', 'otherProperty': 'property1b', 'children': [{'id': '1ba', 'name': 'name1ba', 'otherProperty': 'property1ba', 'children': []}, {'id': '2ba', 'name': 'name2ba', 'otherProperty': 'property2ba', 'children': []}]}]};
var thirdTree = {'id': '3', 'name': 'name3', 'otherProperty': 'property3', 'children': [{'id': '3a', 'name': 'name3a', 'otherProperty': 'property3a', 'children': []}, {'id': '3b', 'name': 'name3b', 'otherProperty': 'property3b', 'children': []}]};
var entryArray = [firstTree, secondTree, thirdTree];
function merge_trees(trees){
var merger = {};
//find matches based on id
for (var t of trees){
for (var i of t){
if (!(i['id'] in merger)){
merger[i['id']] = Object.fromEntries(Object.keys(i).map(x => [x, (new Set([i[x]]))]));
}
for (var k of Object.keys(i)){
merger[i['id']][k] = new Set([...(k in merger[i['id']] ? merger[i['id']][k] : []), i[k]]);
}
}
}
var new_result = [];
//iterate over merges
for (var i of Object.keys(merger)){
var result = {}
for (var k of Object.keys(merger[i])){
//choose whether or not to merge again based on the size of the merged children
if (k === 'children'){
result[k] = merger[i][k].size > 1 ? merge_trees(merger[i][k]) : [...merger[i][k]].filter(x => x.length > 1)
}
else{
result[k] = merger[i][k].size === 1 ? [...merger[i][k]][0] : [...merger[i][k]]
}
}
new_result.push(result)
}
return new_result;
}
console.log(merge_trees(entryArray.map(x => [x])))
Related
Consider the following two arrays:
[
{
id: jhz,
name: 'John',
eyes: 'Green',
description: 'Cool guy',
},
{
id: mbe,
name: 'Mary',
brand: 'M&B',
text: 'Something',
}
]
[
{
id: jhz,
name: 'John',
eyes: '',
},
{
id: mbe,
name: 'Mary',
},
{
id: 'beh',
name: 'Bernard',
}
]
First array may have any kind of key value pairs, but it will always have the key id and name. I want to merge the two arrays by taking id and name into account and preserving them, while merging everything else and replacing them with data from the first array if any keys duplicate.
Also tricky part - the merged array needs to follow the order of the second array.
So in this example the result I'm looking for is:
[
{
id: jhz,
name: 'John',
eyes: 'Green',
description: 'Cool guy',
},
{
id: mbe,
name: 'Mary',
brand: 'M&B',
text: 'Something',
},
{
id: 'beh',
name: 'Bernard',
}
]
you can do something like this using Array.map
const data1 = [{
id: 'jhz',
name: 'John',
eyes: 'Green',
description: 'Cool guy',
},
{
id: 'mbe',
name: 'Mary',
brand: 'M&B',
text: 'Something',
}
]
const data2 = [{
id: 'jhz',
name: 'John',
eyes: '',
},
{
id: 'mbe',
name: 'Mary',
},
{
id: 'beh',
name: 'Bernard',
}
]
const result = data2.map(d => ({...d, ...(data1.find(d1 => d1.id === d.id && d1.name === d.name) || {})}))
console.log(result)
I am struggling to find out a solution to my problem, but at the moment I cannot come up with the right one.
When I have my two arrays of objects I want to filter based on category IDs and extract the data from the second one into a new array for example :
const array1 = [
{id: 1, name: 'Tropical'},
{id: 2, name: 'Common'}
]
const array2 = [
{id:1, name: 'Banana', category_id: 1},
{id:2, name: 'Mango', category_id: 1},
{id:3, name: 'Apple', category_id: 2},
]
And when click happens I detect the first ID and render the new array only with data that matches the ID.
Click Tropical
New array :
[
{id:1, name: 'Banana', category_id: 1},
{id:2, name: 'Mango', category_id: 1},
]
I would be happy if someone give me a hint on how can I tackle this problem. Thanks !
Correct me if I am wrong, So you need a function that received a categoryId and you need to filter out array2 based on that category_id
You can try this
const array1 = [{
id: 1,
name: 'Tropical'
},
{
id: 2,
name: 'Common'
}
]
const array2 = [{
id: 1,
name: 'Banana',
category_id: 1
},
{
id: 2,
name: 'Mango',
category_id: 1
},
{
id: 3,
name: 'Apple',
category_id: 2
},
]
function categoryFruits(categoryId) {
return array2.filter(obj => obj.id === categoryId)
}
console.log(categoryFruits(3));
Use reduce to map over each item in array1 and filter to grab the items of that category_id
const array1 = [{
id: 1,
name: 'Tropical'
},
{
id: 2,
name: 'Common'
}
]
const array2 = [{
id: 1,
name: 'Banana',
category_id: 1
},
{
id: 2,
name: 'Mango',
category_id: 1
},
{
id: 3,
name: 'Apple',
category_id: 2
},
]
const obj = array1.reduce((acc, cur) => {
acc[cur.name] = array2.filter(v => v.category_id === cur.id)
return acc
}, {})
console.log(obj)
You could do something like filtering array2 and taking all the elements that have Tropical as name in array1.
const array1 = [
{id: 1, name: 'Tropical'},
{id: 2, name: 'Common'}
]
const array2 = [
{id:1, name: 'Banana', category_id: 1},
{id:2, name: 'Mango', category_id: 1},
{id:3, name: 'Apple', category_id: 2},
]
// take tropical friuts
let tropicalFriuts = array2.filter(x => x.category_id === array1.filter(y => y.name === 'Tropical')[0].id);
console.log(tropicalFriuts);
If I understood your problem you want before find the id, based on the name of the category, and later filter array2 data based on this id.
const array1 = [{
id: 1,
name: 'Tropical'
},
{
id: 2,
name: 'Common'
}
]
const array2 = [{
id: 1,
name: 'Banana',
category_id: 1
},
{
id: 2,
name: 'Mango',
category_id: 1
},
{
id: 3,
name: 'Apple',
category_id: 2
},
]
const id_key = array1.find(item=>item.name === 'Tropical').id;
const result = array2.filter(item=>item.category_id === id_key);
console.log(result);
I have been looking around for a JavaScript method to return the index of a value but I can't seem to find one that works.
I have the following code:
let topics = [
{
id: 1,
name: 'Topic 1',
children: [
{
id: 2,
name: 'Subtopic 1.1' <---- Searching for this value
}
]
}
];
Is there a method to use on the topics variable to search through the entire object array at once for the value of Subtopic 1.1 and then return the parent index, which in this case would be 0.
There isn't a single function, but you can nest an Array.prototype.find function inside an Array.prototype.findIndex without issue to achieve what you want (findIndex to search through the parents, find to search through the children):
let topics = [{
id: 1,
name: 'Topic 1',
children: [{
id: 2,
name: 'Subtopic 1.1' // <---- Searching for this value
}]
},
{
id: 2,
name: 'Topic 6',
children: [{
id: 5,
name: 'Subtopic 1.7'
}]
},
{
id: 3,
name: 'Topic 9',
children: [{
id: 4,
name: 'Subtopic 1.192'
},
{
id: 28,
name: 'Subtopic 999'
}],
},
];
function findParentIndex(name) {
return topics.findIndex(topic => topic.children.find(child => child.name === name));
}
console.log(findParentId("Subtopic 1.192")); // 3
console.log(findParentId("Subtopic 1.1")); // 1
console.log(findParentId("Not in the list")); // -1
You can use array.findIndex()
let topics = [{
id: 1,
name: 'Topic 1',
children: [{
id: 1,name: 'Subtopic 1.2'
}, {
id: 4,name: 'Subtopic 1.4'
}, {
id: 2, name: 'Subtopic 1.1'
}]
}];
const findIndexOf = val => {
return topics[0].children.findIndex(e => e.name.trim() === val.trim())
}
console.log(findIndexOf('Subtopic 1.1'))
No.
You would iterate through children, then have a nested loop iterating through each index value. If you find a match, the incrementing variable from the parent loop is the index you want.
edit: code example
let topics = [
{
id: 1,
name: 'Topic 1',
children: [
{
id: 2,
name: 'Subtopic 1.1'
},
{
id: 2,
name: 'Subtopic 3.1'
},
{
id: 2,
name: 'Subtopic 1.1'
},
{
id: 2,
name: 'Subtopic 2.1'
},
{
id: 2,
name: 'Subtopic 1.1'
}
]
}
];
for (let i in topics[0]["children"]) {
if (topics[0]["children"][i]["name"] == "Subtopic 1.1") {
console.log(i)
}
}
i neeed to merge two arrays: Categories and Products. Each product has a category object. I need to organize by category, include the category object and keep the empty categories. GroupBy function include only one parameter.
const Categories= [
{id: 1, 'name': 'category1'}
{id: 2, 'name': 'category2'},
{id: 3, 'name': 'category3'},
{id: 4, 'name': 'category4'},
]
const Products= [
{id: 1, 'name': 'product1', category: {id: 1, name: 'category1'}},
{id: 2, 'name': 'product2', category: {id: 1, name: 'category1'}},
{id: 3, 'name': 'product3', category: {id: 2, name: 'category2'}},
{id: 4, 'name': 'product4', category: {id: 2, name: 'category2'}},
]
expected result
const result = [
{
category: {id: 1, name: 'category1'},
products:[{id:1, name: 'produt1'}, {id: 2, name: 'produto1'} ]
},
{
category: {id: 2, name: 'category2'},
products:[{id:3, name: 'produt3'}, {id: 4, name: 'produto4'} ]
},
{
category: {id: 3, name: 'category3'},
products:[]
},
{
category: {id: 4, name: 'category4'},
products:[]
},
]
attempts:
for (i = 0; i < categoriesJson.length; i++) {
categoriesJson[i] = _.assign({}, categoriesJson[i], { products: [] })
for (j = 0; j < productsJson.length; j++) {
if(productsJson[j].categoryId.objectId === categoriesJson[i].objectId){
categoriesJson[i].products.push(productsJson[j])
}
}
}
Concat the Categories (formatted by to a Product format) to the Products, group by the category.id, and then map each group - category is taken from the 1st item, while products are the the items in groups, without the category, and empty items are rejected:
const Products = [{"id":1,"name":"product1","category":{"id":1,"name":"category1"}},{"id":2,"name":"product2","category":{"id":1,"name":"category1"}},{"id":3,"name":"product3","category":{"id":2,"name":"category2"}},{"id":4,"name":"product4","category":{"id":2,"name":"category2"}}]
const Categories = [{"id":1,"name":"category1"},{"id":2,"name":"category2"},{"id":3,"name":"category3"},{"id":4,"name":"category4"}]
const result = _(Products)
.concat(Categories.map(category => ({ category })))
.groupBy('category.id')
.map(group => ({
category: _.head(group).category,
products: _(group)
.map(o => _.omit(o, 'category'))
.reject(_.isEmpty)
.value()
}))
.value()
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.js"></script>
And the same idea with lodash/fp. Wrap the _.flow() with the _.useWith() function, and preformat the Categories (2nd param) to fit the Categories. The rest is similar to the lodash chain.
const { useWith, identity, flow, concat, groupBy, map, head, omit, reject, isEmpty } = _
const formatProducts = flow(map(omit('category')), reject(isEmpty))
const fn = useWith(flow(
concat,
groupBy('category.id'),
map(group => ({
category: head(group).category,
products: formatProducts(group)
}))
), [identity, map(category => ({ category }))])
const Products = [{"id":1,"name":"product1","category":{"id":1,"name":"category1"}},{"id":2,"name":"product2","category":{"id":1,"name":"category1"}},{"id":3,"name":"product3","category":{"id":2,"name":"category2"}},{"id":4,"name":"product4","category":{"id":2,"name":"category2"}}]
const Categories = [{"id":1,"name":"category1"},{"id":2,"name":"category2"},{"id":3,"name":"category3"},{"id":4,"name":"category4"}]
const result = fn(Products, Categories)
console.log(result)
<script src='https://cdn.jsdelivr.net/g/lodash#4(lodash.min.js+lodash.fp.min.js)'></script>
If lodash is not a requirement in the solution, this is how I did it with plain javascript;
const Categories= [
{id: 1, 'name': 'category1'},
{id: 2, 'name': 'category2'},
{id: 3, 'name': 'category3'},
{id: 4, 'name': 'category4'}
];
const Products= [
{id: 1, 'name': 'product1', category: {id: 1, name: 'category1'}},
{id: 2, 'name': 'product2', category: {id: 1, name: 'category1'}},
{id: 3, 'name': 'product3', category: {id: 2, name: 'category2'}},
{id: 4, 'name': 'product4', category: {id: 2, name: 'category2'}},
];
const result = [];
for (let index in Categories) {
let category_id = Categories[index].id;
result.push({
category: Categories[index],
products: GetProductsWithCategoryId(category_id)
});
}
function GetProductsWithCategoryId(category_id) {
let products = [];
for (let index in Products) {
if (Products[index].category.id == category_id) {
products.push({
id: Products[index].id,
name: Products[index].name
});
}
}
return products;
}
console.log("result:", result);
Using reduce, create a mappedProducts object which groups the Products based on the category.id. Like this:
{
"1": [{ id: 1, name: "product1" }, { id: 2, name: "product2" }],
"2": [{ id: 3, name: "product3" }, { id: 4, name: "product4" }]
}
Then, map the Categories array and get the output for each category
const Categories=[{id:1,name:"category1"},{id:2,name:"category2"},{id:3,name:"category3"},{id:4,name:"category4"},],
Products=[{id:1,name:"product1",category:{id:1,name:"category1"}},{id:2,name:"product2",category:{id:1,name:"category1"}},{id:3,name:"product3",category:{id:2,name:"category2"}},{id:4,name:"product4",category:{id:2,name:"category2"}}];
const mappedProducts = Products.reduce((acc, { category, ...rest }) => {
acc[category.id] = acc[category.id] || [];
acc[category.id].push(rest)
return acc;
}, {})
const output = Categories.map(category => ({
category,
products: mappedProducts[category.id] || []
}))
console.log(output)
In a single function. Lodash is not necessary:
const Categories = [
{ id: 1, name: "category1" },
{ id: 2, name: "category2" },
{ id: 3, name: "category3" },
{ id: 4, name: "category4" }
];
const Products = [
{ id: 1, name: "product1", category: { id: 1, name: "category1" } },
{ id: 2, name: "product2", category: { id: 1, name: "category1" } },
{ id: 3, name: "product3", category: { id: 2, name: "category2" } },
{ id: 4, name: "product4", category: { id: 2, name: "category2" } }
];
function combine(categories, products) {
return categories.reduce((list, category) => {
const nextItem = {
category,
products: [
products.filter(p => p.category.id === category.id).map(
({ id, name }) => ({
id,
name
})
)
]
};
list.push(nextItem);
return list;
}, []);
}
const result = combine(Categories, Products)
Now for your information, if you had a huge list of categories and/or products, this wouldn't be the ideal solution as there is a lot of looping involved. Instead, you would cache products in such a way that you only ever need to look at a given product once (rather than looking at every product for every category). With a small data set, this optimization isn't necessary.
I'm trying to manipulate this sample array of objects.
var data = [
{ id: 'A', name: 'Test1', parentId: null },
{ id: 'B', name: 'Test2', parentId: 'A'},
{ id: 'C', name: 'Test3', parentId: 'A'},
{ id: 'D', name: 'Test4', parentId: null },
{ id: 'E', name: 'Test5', parentId: 'D'},
{ id: 'F', name: 'Test6', parentId: 'D'},
];
What I need to do is to map array to array like that
var data = [
{
id: 'A',
name: 'Test1',
parentId: null,
children:[
{ id: 'B', name: 'Test2', parentId: 'A'},
{ id: 'C', name: 'Test3', parentId: 'A'}
],
},
{
id: 'D',
name: 'Test4',
parentId: null,
children:[
{ id: 'E', name: 'Test5', parentId: 'D'},
{ id: 'F', name: 'Test6', parentId: 'D'}
],
},
];
What is the simplest way to do that, using lodash? Please help me.
_.groupBy is more effective in such scenarios. You need to group the elements by parentId, so that you can easily assign their children later.
var data = [
{ id: 'A', name: 'Test1', parentId: null },
{ id: 'B', name: 'Test2', parentId: 'A'},
{ id: 'C', name: 'Test3', parentId: 'A'},
{ id: 'D', name: 'Test4', parentId: null },
{ id: 'E', name: 'Test5', parentId: 'D'},
{ id: 'F', name: 'Test6', parentId: 'D'},
];
var map = _.groupBy(data, 'parentId');
map[null].forEach(item => item.children = map[item.id]);
console.log(map[null]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
You can do this using reduce method and create nested structure for any depth level.
var data = [{ id: 'A', name: 'Test1', parentId: null },{ id: 'B', name: 'Test2', parentId: 'A'},{ id: 'C', name: 'Test3', parentId: 'A'},{ id: 'D', name: 'Test4', parentId: null },{ id: 'E', name: 'Test5', parentId: 'D'},{ id: 'F', name: 'Test6', parentId: 'D'}];
function tree(data, parentId = null) {
return _.reduce(data, (r, e) => {
if (parentId == e.parentId) {
const o = _.clone(e);
const children = tree(data, e.id);
if (children.length) o.children = children;
r.push(o)
}
return r;
}, [])
}
const result = tree(data);
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.js"></script>
The usage of _.groupBy is the closest to your needs, just select parentId as previously said. If you need it to be recursive, you could try something like this:
var data = [
{ id: 'A', name: 'Test1', parentId: null },
{ id: 'B', name: 'Test2', parentId: 'A'},
{ id: 'C', name: 'Test3', parentId: 'A'},
{ id: 'D', name: 'Test4', parentId: null },
{ id: 'E', name: 'Test5', parentId: 'B'},
{ id: 'F', name: 'Test6', parentId: 'D'},
];
function assignChildrens(grouped, parentId) {
if (!grouped[parentId]) return [];
return _.map(grouped[parentId], v => {
v.childrens = assignChildrens(grouped, v.id);
return v;
});
}
var finalData = assignChildrens(_.groupBy(data, 'parentId'), null);
That way, you could have nested elements and still works.
You could take a single loop approach which takes the relation between children and parent and parents and children into a hash table and resturns only the children of the given root nodes.
var data = [{ id: 'A', name: 'Test1', parentId: null }, { id: 'B', name: 'Test2', parentId: 'A'}, { id: 'C', name: 'Test3', parentId: 'A'}, { id: 'D', name: 'Test4', parentId: null }, { id: 'E', name: 'Test5', parentId: 'D'}, { id: 'F', name: 'Test6', parentId: 'D'}],
tree = function(data, root) {
var t = {};
data.forEach(o => {
Object.assign(t[o.id] = t[o.id] || {}, o);
t[o.parentId] = t[o.parentId] || { id: o.parentId };
t[o.parentId].children = t[o.parentId].children || [];
t[o.parentId].children.push(t[o.id]);
});
return t[null].children;
}(data, null);
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }