Creating nested json object based on given condition - javascript

I have a flat list (array of objects), like next one:
var myList = [
{id:1, name:"ABC", type:"level_1"},
{id:2, name:"XYZ", type:"level_1"},
{id:1, name:"ABC_level 2", type:"level_2", level_one_id:1},
{id:2, name:"XYZ_level 2", type:"level_2", level_one_id:2},
{id:1, name:"ABC_level 3", type:"level_3", level_two_id:1},
{id:2, name:"XYZ_level 3", type:"level_3", level_two_id:2},
];
Then, I have to group them in such a way that I can create a hierarchy of levels (which I tried to do in the below lines of code):
var myList = [
{id:1, name:"ABC", type:"level_1"},
{id:2, name:"XYZ", type:"level_1"},
{id:1, name:"ABC_level 2", type:"level_2", level_one_id:1},
{id:2, name:"XYZ_level 2", type:"level_2", level_one_id:2},
{id:1, name:"ABC_level 3", type:"level_3", level_two_id:1},
{id:2, name:"XYZ_level 3", type:"level_3", level_two_id:2},
];
var myNestedList = {
levels: []
};
//-----------pushing level1----------
myList.forEach((res => {
if (res.type == "level_1") {
myNestedList.levels.push(res);
}
}));
//-----------pushing level 2---------
myNestedList.levels.forEach((res) => {
myList.forEach((val) => {
if (val.type == "level_2" && val.level_one_id == res.id) {
res["level_2"] = [] || res["level_2"];
res["level_2"].push(val);
}
})
})
//-----------pushing level 3---------
myNestedList.levels.forEach((res) => {
res["level_2"].forEach((val) => {
myList.forEach((lastlevel) => {
if (lastlevel.type == "level_3" && lastlevel.level_two_id == val.id) {
val["level_3"] = [] || val["level_3"];
val["level_3"].push(lastlevel);
}
})
})
})
console.log(myNestedList);
Although I'm able to achieve the result, I'm sure this code can be more precise and meaningful. Can we make use of lodash here and get this code shorter?
Any help would be much appreciated. Thanks!

You could take a virtual unique id for the object and for referencing the parents and collect the items in a tree.
This approach works with unsorted data as well.
var data = [{ id: 1, name: "ABC", type: "level_1" }, { id: 2, name: "XYZ", type: "level_1" }, { id: 1, name: "ABC_level 2", type: "level_2", level_one_id: 1 }, { id: 2, name: "XYZ_level 2", type: "level_2", level_one_id: 2 }, { id: 1, name: "ABC_level 3", type: "level_3", level_two_id: 1 }, { id: 2, name: "XYZ_level 3", type: "level_3", level_two_id: 2 }],
tree = function (data) {
var t = {};
data.forEach(o => {
var level = o.type.match(/\d+$/)[0],
parent = o[Object.keys(o).filter(k => k.startsWith('level_'))[0]] || 0,
parentId = `${level - 1}.${parent}`,
id = `${level}.${o.id}`,
children = `level_${level}`;
Object.assign(t[id] = t[id] || {}, o);
t[parentId] = t[parentId] || {};
t[parentId][children] = t[parentId][children] || [];
t[parentId][children].push(t[id]);
});
return t['0.0'].level_1;
}(data);
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }

I can't make any sense of this data representing a real tree. But I can see it turning into something like a list of lists, one for each base id, something like this:
[
[
{id: 1, name: "ABC", type: "level_1"},
{id: 1, name: "ABC_level 2", type: "level_2", level_one_id: 1},
{id: 1, name: "ABC_level 3", type: "level_3", level_two_id: 1}
],
[
{id: 2, name: "XYZ", type: "level_1"},
{id: 2, name: "XYZ_level 2", type: "level_2", level_one_id: 2},
{id: 2, name: "XYZ_level 3", type: "level_3", level_two_id: 2}
]
]
If that format is useful, then this code could help you get there:
// utility function
const group = (fn) => (xs) =>
Object .values (xs .reduce ((a, x) => ({...a, [fn (x)]: (a [fn (x)] || []) .concat (x)}), {}))
// helper function
const numericSuffix = str => Number (str .type .match (/(\d+)$/) [1])
// main function -- I don't have a sense of what a good name for this would be
const foo = (xs) =>
group (o => o.id) (xs)
.map (x => x .sort ((a, b) => numericSuffix(a) - numericSuffix(b)))
// data
const myList = [{id: 1, name: "ABC", type: "level_1"}, {id: 2, name: "XYZ", type: "level_1"}, {id: 1, name: "ABC_level 2", type: "level_2", level_one_id: 1}, {id: 2, name: "XYZ_level 2", type: "level_2", level_one_id: 2}, {id: 1, name: "ABC_level 3", type: "level_3", level_two_id: 1}, {id: 2, name: "XYZ_level 3", type: "level_3", level_two_id: 2}]
// demo
console .log (foo (myList))
.as-console-wrapper {max-height: 100% !important; top: 0}
We use a custom group function as well as one that extracts the numeric end of a string (to be used in sorting so that level_10 comes after level_9 and not before level_2) group could be replaced by Underscore, lodash or Ramda groupBy functions, but you'd probably then have to call Object.values() on the results.
The main function groups the data on their ids, then sorts the group by that numeric suffix.
Note that this technique only makes sense if there is only one element for a given id at any particular level. If there could be more, and you really need a tree, I don't see how your input structure could determine future nesting.

Related

Recursively create a parent and child string for every child and it's child

I'm attempting to create an array of objects as the final output which includes the parent name and it's children. The id would be the last child in the name
Sample output I'm trying to achieve :
[
{id:1,name:"1"},
{id:2,name:"1-2"},
{id:3,name:"1-2-3"},
{id:4,name:"1-2-4"}
]
Code:
let item = {
id: 1,
name: "1",
children: [{
id: 2,
name: "2",
children: [{
id: 3,
name: "3",
children: []
},
{
id: 4,
name: "4",
children: []
}
]
}]
}
const createList = (item) => {
let name = item.children.map(e => {
return createList(e)
})
return item.name + "-" + name
}
console.log(createList(item))
A simple recursion will do the trick:
let item = {
id: 1,
name: "1",
children: [{
id: 2,
name: "2",
children: [{
id: 3,
name: "3",
children: []
},
{
id: 4,
name: "4",
children: []
}
]
}]
}
var result = []
const createList = (items, acc) => {
items.forEach(item => {
const newAcc = {
id: item.id,
name: acc ? `${acc.name}-${item.name}` : `${item.name}`
}
result.push(newAcc)
createList(item.children, newAcc)
})
}
createList([item], null)
console.log(result)
You're on the right track with recursion, but the map function is probably not the best choice for this. Map function doesn't work the way you might expect when there is only one element in the array, as it is not meant to be used to process lists in such a way, but only to "map" data.
Try switching to a for loop or foreach and building the string yourself.
Example:
let sample = {
id: 1,
name: "1",
children: [{
id: 2,
name: "2",
children: [{
id: 3,
name: "3",
children: []
},
{
id: 4,
name: "4",
children: []
}
]
}]
};
function buildArray(input, parentName) {
let output = [];
for (let item of input) {
let name = parentName ? `${parentName}-${item.name}` : item.name;
output.push({ id: item.id, name: name });
if (item.children) {
output = output.concat(buildArray(item.children, name));
}
}
return output;
}
let result = buildArray(sample.children, sample.name);
let output = document.getElementById('myresult');
output.innerText = JSON.stringify(result);
<p id="myresult"></p>
I think we can do this more simply. I'd prefer code like this:
const convert = ({id, name, children}, prefix = '') => [
{id, name: prefix + name},
...children .flatMap (c => convert (c, prefix + name + '-'))
]
const sample = {id: 1, name: "1", children: [{id: 2, name: "2", children: [{id: 3, name: "3", children: []}, {id: 4, name: "4", children: []}]}]}
console .log (convert (sample))
.as-console-wrapper {max-height: 100% !important; top: 0}
Note that our recursion bottoms without an explicit case for it because mapping over an empty array returns an empty array, without making further calls. And that result is just folded into our accumulation by flatMap.
If you prefer not to include default parameters (and there are some good reasons for that preference), you could simply wrap an internal version of this function into a public function, supplying the initial prefix value to that internal one. This would also allow us to simplify the function reference passed to flatMap:
const _convert = (prefix = '') => ({id, name, children}) => [
{id, name: prefix + name},
...children .flatMap (_convert (prefix + name + '-'))
]
const convert = _convert ('')
const sample = {id: 1, name: "1", children: [{id: 2, name: "2", children: [{id: 3, name: "3", children: []}, {id: 4, name: "4", children: []}]}]}
console .log (convert (sample))
.as-console-wrapper {max-height: 100% !important; top: 0}

Filter for array of objects that contain all key value pairs of one object in another array of objects

Given I have this array of objects:
let array1 = [
{id: 1, name: "Test Item 1", price: 100, type: 'item'},
{id: 1, name: "Test Fee 1", price: 200, type: 'fee'},
{id: 3, name: "Test 3", price: 300, type: 'item'},
]
Now i want to filter so I only have the objects where every key value of the following array match:
let array2 = [
{id: 1, type: "item"},
{id: 3, type: "item"},
]
This should return array1[0] and array1[2].
My current solution has been this, but it wont work if array2 has multiple object keys to compare to:
array1.filter(row => {
return array2.find(r => {
return Object.entries(row).some(([key, value]) => r[key] === value);
})
});
array1.filter((row) =>
array2.some((template) =>
Object.keys(template).every((key) => template[key] === row[key])
)
);
I think the answer in comment above is cleaner than mine.
In array1.filter() you can iterate all objects in array2 and compare keys using Object.keys()
let array1 = [
{id: 1, name: "Test Item 1", price: 100, type: 'item'},
{id: 1, name: "Test Fee 1", price: 200, type: 'fee'},
{id: 3, name: "Test 3", price: 300, type: 'item'},
]
let array2 = [
{id: 1, type: "item"},
{id: 3, type: "item"},
]
const res = array1.filter(row => {
for (const obj of array2) {
if (Object.keys(obj).every(k => obj[k] === row[k])) {
return row
}
}
})
console.log(res)
Lodash if you don't mind:
const array1 = [{id: 1, name: "Test Item 1", price: 100, type: 'item'},{id: 1, name: "Test Fee 1", price: 200, type: 'fee'},{id: 3, name: "Test 3", price: 300, type: 'item'}];
const array2 = [{id: 1, type: "item"},{id: 3, type: "item"}];
const fn = _.overSome(array2.map(_.matches));
const result = _.filter(array1, fn);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0 }
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.21/lodash.min.js"></script>

Search/Filter nested Array Javascript/Lodash

For my React.js project I would like to create a search-filter of a nested Array. Users will search with an input-field.
var dataExample = [
{
type: "human", details: [
{id: 1, name: "Peter", description: "friendly, black-hair"},
{id: 5, name: "Susan", description: "blond"}
]
},
{
type: "animal", details: [
{id: 2, name: "Will", description: "lazy, cute"},
{id: 3, name: "Bonny", description: "beautiful"}
]
}
];
In my search-input-field I want to look for "name" or something in "description". The data structure of the array should remain the same.
The output when I'm searching for "friendly" or "Peter" should be:
[
{
type: "human", details: [
{id: 1, name: "Peter", description: "friendly, black-hair"}
]
}
];
Now I tried something like this:
let myfilter = dataExample.filter((data) => {
data.details.filter((items) => {
return (items.type.indexOf("human") !== -1 || //input of user
items.description.indexOf("friendly"))
})
})
Unfortunately, this is not how it works. Can anybody help me? Lodash would be no problem, too. Thank you so much.
You can use array#reduce with array#filter and to check for your word you can use string#incldues.
const dataExample = [ { type: "human", details: [ {id: 1, name: "Peter", description: "friendly, black-hair"}, {id: 5, name: "Susan", description: "blond"} ] }, { type: "animal",details: [ {id: 2, name: "Will", description: "lazy, cute"}, {id: 3, name: "Bonny", description: "beautiful"} ] } ],
term = 'Peter',
result = dataExample.reduce((r, {type,details}) => {
let o = details.filter(({name,description}) => name.includes(term) || description.includes(term));
if(o && o.length)
r.push({type, details : [...o]});
return r;
},[]);
console.log(result);
Here are some examples without lodash.
var dataAll = [
{
type: "human",
details: [
{id: 1, name: "Peter", description: "friendly, black-hair"},
{id: 5, name: "Susan", description: "blond"}
]
},
{
type: "animal",
details: [
{id: 2, name: "Will", description: "lazy, cute"},
{id: 3, name: "Bonny", description: "beautiful"}
]
}
];
var entryTypeFilter = data => data.type.indexOf("hum") !== -1;
var entryDetailDescFilter = data => data.description.indexOf("friend") !== -1;
var entryDetailsMapper = data => {
return {
type: data.type,
details: data.details.filter(entryDetailDescFilter)
};
};
var entryNoDetailsFilter = data => data.details && data.details.length !== 0;
var dataFilteredByType = dataAll.filter(entryTypeFilter);
var dataFilteredByDesc = dataAll.map(entryDetailsMapper);
var dataFilteredByTypeAndDesc = dataAll.filter(entryTypeFilter).map(entryDetailsMapper);
var dataFilteredByDescTrimmingEmptyDetailEntries = dataAll.map(entryDetailsMapper).filter(entryNoDetailsFilter);
In modern javascript you might want to search on how to use the ... keyword for the mapping callback functions.

Categorize Similar items into separate objects from Array lists

I have an array of items that I get from API as a response body.
data = [{id: 1, category: "kitchen", name: "noodles"},
{id: 2, category: "general", name: "Wi-Fi"},
{id: 3, category: "sports", name: "Football"},]
I want to iterate over the arrays, and get the data like :
var categorized = {
kitchen: [{id: 1, category: "kitchen", name: "noodles"}],
general : [{id: 2, category: "general", name: "Wi-Fi"}],
sports : [{id: 3, category: "sports", name: "Football"}]
};
Is there any lodash methods, or any ES6 shortcuts for this ?
In answer to your question 'is there a lodash method?' Yes: https://lodash.com/docs/4.17.4#groupBy. For your specific example:
const categorized = _.groupBy(data, 'category');
Edit: You could roll your own groupBy type function with ES6 as in another example. But if you are using lodash anyway this is a whole lot cleaner.
I used array.reduce to get the structure
var data = [{
id: 1,
category: "kitchen",
name: "noodles"
}, {
id: 2,
category: "general",
name: "Wi-Fi"
}, {
id: 3,
category: "sports",
name: "Football"
}]
var newData = data.reduce(function(obj, v, i) {
obj[v.category] = obj[v.category] || [];
obj[v.category].push(v);
return obj;
}, {});
console.log(newData);
In ES6 you could so using:
var newData = data.reduce((obj, v, i)=> {
obj[v.category] = obj[v.category] || [];
obj[v.category].push(v);
return obj;
}, {});
console.log(newData);

Group by Object ID's in Javascript

I have an array of ID's and organizations like so:
var ids = ['1','2', '3'];
var orgs =
[
{ name: "Org 1", id: 1 },
{ name: "Org 2", id: 2 },
{ name: "Org 3", id: 2 }
]
I want to loop through these to output something like this:
{
1: [
{name: "Org 1", id: 1}
],
2: [
{name: "Org 2", id: 2},
{name: "Org 3", id: 2}
]
}
I tried this without success:
var results = orgs.forEach(function (org) {
if (results[org.id]) {
results.push(org)
} else {
results[org.id] = [org]
};
});
If you don't want to use a library like Underscore, Ramda, or Lo-Dash, then it's simple enough to write this using reduce:
var results = orgs.reduce(function(results, org) {
(results[org.id] = results[org.id] || []).push(org);
return results;
}, {})
you should use underscore and just return your id
http://underscorejs.org/#groupBy
_.groupBy([1.3, 2.1, 2.4], function(num){ return Math.floor(num); });
// => {1: [1.3], 2: [2.1, 2.4]}
you might also want to take a look at lo-dash

Categories