Selective deep clone [duplicate] - javascript

I have an array of deep JSON objects that look like similarly to this:
var hierarchy = [
{
"title": "category 1",
"children": [
{"title": "subcategory 1",
"children": [
{"id": 1, "title": "name 1"},
{"id": 2, "title": "name 2"},
{"id": 3, "title": "name 3"}
]
},
{"title": "subcategory 2",
"children": [
{"id": 1, "title": "name 4"}
]
}
]
},
{
"title": "category 2",
"children": [etc. - shortened for brevity]
}
];
So basically it is a hierarchy - there are categories which can have subcategories which contain objects with some IDs and names. I also have an array of IDs that are related to the deepest hierarchy level (objects with no children) and I need to filter this set of objects in such a way that only (sub)categories that contain defined objects remain.
So for example if I had an array containing two IDs:
var IDs = [2, 3];
the result would be:
var hierarchy = [
{
"title": "category 1",
"children": [
{"title": "subcategory 1",
"children": [
{"id": 2, "title": "name 2"},
{"id": 3, "title": "name 3"}
]
}
]
}
];
i.e. the whole, the whole 'category 2' object removed, the whole 'subcategory 2' removed, object with ID '1' removed.
The problem is that the depth of those objects is variable and unknown - some objects have no children, some have children that also have children etc., any subcategory can can itself have a subcategory and I basically need to find object with no children that have defined IDs and keep the whole path to each of them.
Thank you.

Basically, perform a depth first traversal of your tree invoking a callback function on each node. If that node is a leaf node and it's ID appears in your list then clone the branch that leads to that leaf, but don't re-clone any part of the branch that was already cloned.
Once you have constructed the partial and filtered copy of your tree you need to cleanup the original. I mutated the original tree in the process for book-keeping purposes - tracking which branches had already been cloned.
Edit: modified code to filter list of trees instead of just a single tree
var currentPath = [];
function depthFirstTraversal(o, fn) {
currentPath.push(o);
if(o.children) {
for(var i = 0, len = o.children.length; i < len; i++) {
depthFirstTraversal(o.children[i], fn);
}
}
fn.call(null, o, currentPath);
currentPath.pop();
}
function shallowCopy(o) {
var result = {};
for(var k in o) {
if(o.hasOwnProperty(k)) {
result[k] = o[k];
}
}
return result;
}
function copyNode(node) {
var n = shallowCopy(node);
if(n.children) { n.children = []; }
return n;
}
function filterTree(root, ids) {
root.copied = copyNode(root); // create a copy of root
var filteredResult = root.copied;
depthFirstTraversal(root, function(node, branch) {
// if this is a leaf node _and_ we are looking for its ID
if( !node.children && ids.indexOf(node.id) !== -1 ) {
// use the path that the depthFirstTraversal hands us that
// leads to this leaf. copy any part of this branch that
// hasn't been copied, at minimum that will be this leaf
for(var i = 0, len = branch.length; i < len; i++) {
if(branch[i].copied) { continue; } // already copied
branch[i].copied = copyNode(branch[i]);
// now attach the copy to the new 'parellel' tree we are building
branch[i-1].copied.children.push(branch[i].copied);
}
}
});
depthFirstTraversal(root, function(node, branch) {
delete node.copied; // cleanup the mutation of the original tree
});
return filteredResult;
}
function filterTreeList(list, ids) {
var filteredList = [];
for(var i = 0, len = list.length; i < len; i++) {
filteredList.push( filterTree(list[i], ids) );
}
return filteredList;
}
var hierarchy = [ /* your data here */ ];
var ids = [1,3];
var filtered = filterTreeList(hierarchy, ids);

You can use filterDeep method from deepdash extension for lodash:
var obj = [{/* get Vijay Jagdale's source object as example */}];
var idList = [2, 3];
var found = _.filterDeep(
obj,
function(value) {
return _.indexOf(idList, value.id) !== -1;
},
{ tree: true }
);
filtrate object will be:
[ { title: 'category 1',
children:
[ { title: 'subcategory 11',
children:
[ { id: 2, title: 'name 2' },
{ id: 3, title: 'name 3' } ] } ] },
{ title: 'category 2',
children:
[ { title: 'subcategory 21',
children: [ { id: 3, title: 'name cat2sub1id3' } ] } ] } ]
Here is the full working test for your use case

Although this is an old question I will add my 2 cents. The solution requires a straightforward iteration through the loops, subloops etc. and then compare IDs and build the resultant object. I have pure-javascript and jQuery solution. While the pure javascript works for the example above, I would recommend the jQuery solution, because it is more generic, and does a "deep copy" of the objects, in case you have large and complex objects you won't run into bugs.
function jsFilter(idList){
var rsltHierarchy=[];
for (var i=0;i<hierarchy.length;i++) {
var currCatg=hierarchy[i];
var filtCatg={"title":currCatg.title, "children":[]};
for (var j=0;j<currCatg.children.length;j++) {
var currSub=currCatg.children[j];
var filtSub={"title":currSub.title, "children":[]}
for(var k=0; k<currSub.children.length;k++){
if(idList.indexOf(currSub.children[k].id)!==-1)
filtSub.children.push({"id":currSub.children[k].id, "title":currSub.children[k].title});
}
if(filtSub.children.length>0)
filtCatg.children.push(filtSub);
}
if(filtCatg.children.length>0)
rsltHierarchy.push(filtCatg);
}
return rsltHierarchy;
}
function jqFilter(idList){
var rsltHierarchy=[];
$.each(hierarchy, function(index,currCatg){
var filtCatg=$.extend(true, {}, currCatg);
filtCatg.children=[];
$.each(currCatg.children, function(index,currSub){
var filtSub=$.extend(true, {}, currSub);
filtSub.children=[];
$.each(currSub.children, function(index,currSubChild){
if(idList.indexOf(currSubChild.id)!==-1)
filtSub.children.push($.extend(true, {}, currSubChild));
});
if(filtSub.children.length>0)
filtCatg.children.push(filtSub);
});
if(filtCatg.children.length>0)
rsltHierarchy.push(filtCatg);
});
return rsltHierarchy;
}
//Now test the functions...
var hierarchy = eval("("+document.getElementById("inp").value+")");
var IDs = eval("("+document.getElementById("txtBoxIds").value+")");
document.getElementById("oupJS").value=JSON.stringify(jsFilter(IDs));
$(function() {
$("#oupJQ").text(JSON.stringify(jqFilter(IDs)));
});
#inp,#oupJS,#oupJQ {width:400px;height:100px;display:block;clear:all}
#inp{height:200px}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
ID List: <Input id="txtBoxIds" type="text" value="[2, 3]">
<p>Input:
<textarea id="inp">[
{
"title": "category 1",
"children": [
{"title": "subcategory 11",
"children": [
{"id": 1, "title": "name 1"},
{"id": 2, "title": "name 2"},
{"id": 3, "title": "name 3"}
]
},
{"title": "subcategory 12",
"children": [
{"id": 1, "title": "name 4"}
]
}
]
},
{
"title": "category 2",
"children": [
{"title": "subcategory 21",
"children": [
{"id": 3, "title": "name cat2sub1id3"},
{"id": 5, "title": "name cat2sub1id5"}
]
},
{"title": "subcategory 22",
"children": [
{"id": 6, "title": "name cat2sub2id6"},
{"id": 7, "title": "name cat2sub2id7"}
]
}
]
}
]</textarea>
<p>Pure-Javascript solution results:
<textarea id="oupJS"></textarea>
<p>jQuery solution results:
<textarea id="oupJQ"></textarea>

I'd not reinvent the wheel. We use object-scan for most of our data processing now and it solves your question nicely. Here is how
// const objectScan = require('object-scan');
const filter = (input, ids) => {
objectScan(['**[*]'], {
filterFn: ({ value, parent, property }) => {
if (
('id' in value && !ids.includes(value.id))
|| ('children' in value && value.children.length === 0)
) {
parent.splice(property, 1);
}
}
})(input);
};
const hierarchy = [ { title: 'category 1', children: [ { title: 'subcategory 1', children: [ { id: 1, title: 'name 1' }, { id: 2, title: 'name 2' }, { id: 3, title: 'name 3' } ] }, { title: 'subcategory 2', children: [ { id: 1, title: 'name 4' } ] } ] }, { title: 'category 2', children: [] } ];
filter(hierarchy, [2, 3]);
console.log(hierarchy);
// => [ { title: 'category 1', children: [ { title: 'subcategory 1', children: [ { id: 2, title: 'name 2' }, { id: 3, title: 'name 3' } ] } ] } ]
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan

Related

Nested arrays and targeting array by keys

I'm really not sure how to word this issue but I will try my best.
I have a nested array:
const items = [
{
id: 1,
name: "Test name",
children: [
{
id: 5,
name: "Test name 5",
children: [
{
id: 6,
name: "Test name 6",
children: [],
},
],
},
],
},
{
id: 8,
name: "Test name 8",
children: [
{
id: 9,
name: "Test name 9",
children: [],
},
],
},
];
and I have an array of indexes where to target and update
const keys = [0,0,0]
The array of indexes should target Test name 6
How can I update Test name 6 to something else?
p.s. items and keys are dynamic. there might be dozens of nested items or dozens of indexes in keys
You could reduce the indices and check the children property.
After getting the final object, just assign the value to the wanted property.
const
getItem = (children, keys) => keys.reduce(
({ children = [] }, i) => children[i],
{ children }
),
items = [{ id: 1, name: "Test name", children: [{ id: 5, name: "Test name 5", children: [{ id: 6, name: "Test name 6", children: [] }] }] }, { id: 8, name: "Test name 8", children: [{ id: 9, name: "Test name 9", children: [] }] }],
keys = [0, 0, 0];
console.log(getItem(items, keys));
To update your nested array first you will first need to change it to a mutable variable, i.e. from a const to a let or var.
From there you can update the value with something like:
// Copy pasted data from your post
let items = [
{
id: 1,
name: "Test name",
children: [
{
id: 5,
name: "Test name 5",
children: [
{
id: 6,
name: "Test name 6",
children: [],
},
],
},
],
},
{
id: 8,
name: "Test name 8",
children: [
{
id: 9,
name: "Test name 9",
children: [],
},
],
},
];
const keys = [0,0,0]
// <----------------- CODE BELOW ---------------->
// var to store item each time you dive deeper into the nested array
// initalize with the first item
let item = items[keys[0]].children;
// loop through each key
for(let i = 1; i < keys.length; i++){
// if we are at the last key, set item equal to the item object
if(i == keys.length-1){
item = item[keys[i]];
}
// otherwise, set item equal to the item's children
else{
item = item[keys[i]].children
}
}
// once item has been reached - all keys have been looped through - data can be minupulated
item.name = "new value"
// "Test name 6" has been updated to "new value"
emphasized text
In this scenario, I usually use a Map referencing the items within the nested arrays. For example:
const items = [
{
id: 1,
name: "Test name",
children: [
{
id: 5,
name: "Test name 5",
children: [
{
id: 6,
name: "Test name 6",
children: [],
},
],
},
],
},
{
id: 8,
name: "Test name 8",
children: [
{
id: 9,
name: "Test name 9",
children: [],
},
],
},
];
const createItemsMap = items => {
const m = new Map();
(function _collect(items) {
for (const item of items) {
m.set(item.id, item);
if (item.children) {
_collect(item.children);
}
}
})(items);
return m;
}
const itemsMap = createItemsMap(items);
// modify item #6
itemsMap.get(6).name = "Modified name 6";
console.log("itemsMap.get(1) === items[0] is", itemsMap.get(1) === items[0]);
console.log(items);
With that map, modifying the items is simply a matter of doing this :
const keys = [1,5,6];
for (const key of keys)
const item = itemsMap.get(key);
// update item
item.name = `Updated name ${key}`;
item.newProp = 'New value!';
}
// ex:
console.log(items[0]);
// {
// id: 1,
// name: "Updated name 1",
// newProp: "New value!',
// children: [
// {
// id: 5,
// name: "Updated name 5",
// newProps: "New value!",
// children: [
// {
// id: 6,
// name: "Updated name 6",
// newProps: "New value!",
// children: [],
// },
// ],
// },
// ],
// },
And a few more freebies :
// get all existing keys
const keys = Array.from(itemsMap.keys());
// check if an item exists
const exists3 = itemsMap.has(3); // false
const exists5 = itemsMap.has(5); // true
// adding new children
const newChild = { id: 3, name: "Test name 3", children: [] };
// ... add as child of 5
itemsMap.get(5).children.push(newChild);
// ... add it to the map
itemsMap.set(newChild.id, newChild); // that's it!
// remove an item
const removed = itemsMap.get(3);
// ... find if there is a parent...
const parent = Array.from(itemsMap.values()).find(item =>
item.children.includes(removed)
);
if (parent) {
// ... if there is a parent, remove the child
parent.children = parent.children.filter(item =>
item !== removed
);
} else {
// ... otherwise it is a root item, so remove it
items = items.filter(item => item !== removed);
}
// ... remove from map
itemsMap.delete(removed.id);
Note: If items is modified directly (i.e. items are added or removed), then itemsMap needs to either be re-generated, or preferably updated. In any other case, both items and itemsMap reference to the same data.

Creating nested json object based on given condition

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.

Add child or remove item by path from tree like array of objects

I have nested array of objects like
let treeArr =
{
name: "Top Level", id:'a12',
children: [
{
name: "Level 2: A", id:'a',
children: [
{ name: "Daughter of A", id: 'a',
children: [
{ name: "Another Sub0 Issuer", id: '504' },
{ name: "Another Sub1 Issuer", id: '109' },
{ name: "Another Sub2 Issuer", id: '209' },
]
},
{ name: "Daughter of A", id: '165' },
]
},
{
name: "ABC Co LLC", id:'1234',
children: [
{ name: "Daughter of A", id: 'a' },
{ name: "Daughter of A", id: 'x' },
{ name: "Daughter of Y", id: 'a',
children:[
{ name: "Another Suba Issuer", id: '219' },
{ name: "Another Subb Issuer", id: '409',
children:[
{ name: "Another 4th Issuer", id: '200' },
{ name: "Another 4th Issuer", id: '300' },
{ name: "Another 4th Issuer", id: '400' },
]
},
{ name: "Another Suba Issuer", id: '479' },
]
}
]
}
]
}
function findIndexNested(data, id) {
if (data.id === id) return [];
let result;
const i = (data.children || []).findIndex(child => {
return result = findIndexNested(child, id)
});
if (result) return [i, ...result];
}
function findByPath(data, path) {
for (let i of path) data = data.children[i];
return data
}
I need to delete or add children to certain children items to/from treeArr,
my finder method returns index of searched item, for example: [0, 2, 0, 1]
first children of third children of first ...
so i need to generate this as code dynamically instead hardcoded
my current ugly solution is looks like below, path returns from findIndexNested(.,.)
treeArr.children[path[0]].children[path[1]].children[path[2]].children[path[3]]
how can I add child or remove found item from treeArr ?
function removebyPath(obj, path, i, len ) {
if(len===i+1){ delete obj.children[path[i]]; return;}
removebyPath(obj.children[path[i]],path, i+1, len );
}
let path = findIndexNested(obj, '219' );
removebyPath(obj, path, 0, path.length);
console.log('result:', obj );
removebyPath method removes item from passed path

Javascript Recurse JSON array with children objects

Is there a way to recurse the following JSON without for-looping the nested children?
My recursive function must be missing a case as it's not returning everything.
iterateTree(node, children) {
console.log(node.name)
if(node.children.length == 0) {
return;
}
for(var i = 0; i < children.length; i++) {
var child_node = children[i];
return this.iterateTree(child_node, child_node.children);
}
}
for(var i = 0; i < sample.length; i++) {
var node = sample[i];
this.iterateTree(node, node.children);
}
var sample = [
{
"name": "hello world",
"children": [
{
"name": "fruits",
"children": []
},
{
"name": "vegetables",
"children": []
},
{
"name": "meats",
"children": [
{
"name": "pork",
"children": []
},
{
"name": "beef",
"children": []
},
{
"name": "chicken",
"children": [
{
"name": "organic",
"children": []
},
{
"name": "farm raised",
"children": []
}
]
},
]
}
]
},
{
"name": "second folder",
"children": []
},
{
"name": "third folder",
"children": [
{
"name": "breads",
"children": []
},
{
"name": "coffee",
"children": [
{
"name": "latte",
"children": []
},
{
"name": "cappucino",
"children": []
},
{
"name": "mocha",
"children": []
},
]
},
]
}
]
Aiming to achieve the following output (similiar to file structure)
hello world
-fruits
-vegetables
-meats
--pork
--beef
--chicken
---organic
---farm raised
second folder
third folder
-breads
-coffee
--latte
--cappucino
--mocha
You could create recursive function using reduce method to iterate through your nested data structure, return array and then use join method on that array.
var sample = [{"name":"hello world","children":[{"name":"fruits","children":[]},{"name":"vegetables","children":[]},{"name":"meats","children":[{"name":"pork","children":[]},{"name":"beef","children":[]},{"name":"chicken","children":[{"name":"organic","children":[]},{"name":"farm raised","children":[]}]}]}]},{"name":"second folder","children":[]},{"name":"third folder","children":[{"name":"breads","children":[]},{"name":"coffee","children":[{"name":"latte","children":[]},{"name":"cappucino","children":[]},{"name":"mocha","children":[]}]}]}]
function tree(data, prev = '') {
return data.reduce((r, e) => {
r.push(prev + e.name)
if (e.children.length) r.push(...tree(e.children, prev + '-'));
return r;
}, [])
}
const result = tree(sample).join('\n')
console.log(result)
To create same structure in HTML you could use forEach method instead.
var sample = [{"name":"hello world","children":[{"name":"fruits","children":[]},{"name":"vegetables","children":[]},{"name":"meats","children":[{"name":"pork","children":[]},{"name":"beef","children":[]},{"name":"chicken","children":[{"name":"organic","children":[]},{"name":"farm raised","children":[]}]}]}]},{"name":"second folder","children":[]},{"name":"third folder","children":[{"name":"breads","children":[]},{"name":"coffee","children":[{"name":"latte","children":[]},{"name":"cappucino","children":[]},{"name":"mocha","children":[]}]}]}]
function tree(data, parent) {
const ul = document.createElement('ul');
data.forEach(el => {
const li = document.createElement('li');
li.textContent = el.name;
ul.appendChild(li);
if (el.children.length) {
tree(el.children, li)
}
})
parent.appendChild(ul)
}
const parent = document.getElementById('root')
tree(sample, parent)
<div id="root"></div>
var sample = [{"name":"hello world","children":[{"name":"fruits","children":[]},{"name":"vegetables","children":[]},{"name":"meats","children":[{"name":"pork","children":[]},{"name":"beef","children":[]},{"name":"chicken","children":[{"name":"organic","children":[]},{"name":"farm raised","children":[]}]}]}]},{"name":"second folder","children":[]},{"name":"third folder","children":[{"name":"breads","children":[]},{"name":"coffee","children":[{"name":"latte","children":[]},{"name":"cappucino","children":[]},{"name":"mocha","children":[]}]}]}]
level = 0;
var hyphens = '';
function recursive_loop(s) {
console.log(hyphens + s.name);
var c = s.children;
if (c.length) hyphens += '-';
var empty = false;
for (let i = 0; i < c.length; i++) {
if (c[i].children) {
recursive_loop(c[i]);
}
if (c[i].children.length)
empty = true;
}
if (empty) hyphens = '';
}
for (let i = 0; i < sample.length; i++) {
recursive_loop(sample[i]);
}
We use object-scan foe many data processing / traversal tasks. It's powerful once you wrap your head around it. Here is how you could solve your question
// const objectScan = require('object-scan');
const display = (input) => objectScan(['**'], {
reverse: false,
rtn: 'entry',
filterFn: ({ value }) => typeof value === 'string'
})(input)
.map(([k, v]) => `${'-'.repeat(k.length / 2 - 1)}${v}`);
const sample = [{ name: 'hello world', children: [{ name: 'fruits', children: [] }, { name: 'vegetables', children: [] }, { name: 'meats', children: [{ name: 'pork', children: [] }, { name: 'beef', children: [] }, { name: 'chicken', children: [{ name: 'organic', children: [] }, { name: 'farm raised', children: [] }] }] }] }, { name: 'second folder', children: [] }, { name: 'third folder', children: [{ name: 'breads', children: [] }, { name: 'coffee', children: [{ name: 'latte', children: [] }, { name: 'cappucino', children: [] }, { name: 'mocha', children: [] }] }] }];
const result = display(sample);
result.forEach((l) => console.log(l));
// => hello world
// => -fruits
// => -vegetables
// => -meats
// => --pork
// => --beef
// => --chicken
// => ---organic
// => ---farm raised
// => second folder
// => third folder
// => -breads
// => -coffee
// => --latte
// => --cappucino
// => --mocha
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan

JavaScript: how to filter deep JSON objects

I have an array of deep JSON objects that look like similarly to this:
var hierarchy = [
{
"title": "category 1",
"children": [
{"title": "subcategory 1",
"children": [
{"id": 1, "title": "name 1"},
{"id": 2, "title": "name 2"},
{"id": 3, "title": "name 3"}
]
},
{"title": "subcategory 2",
"children": [
{"id": 1, "title": "name 4"}
]
}
]
},
{
"title": "category 2",
"children": [etc. - shortened for brevity]
}
];
So basically it is a hierarchy - there are categories which can have subcategories which contain objects with some IDs and names. I also have an array of IDs that are related to the deepest hierarchy level (objects with no children) and I need to filter this set of objects in such a way that only (sub)categories that contain defined objects remain.
So for example if I had an array containing two IDs:
var IDs = [2, 3];
the result would be:
var hierarchy = [
{
"title": "category 1",
"children": [
{"title": "subcategory 1",
"children": [
{"id": 2, "title": "name 2"},
{"id": 3, "title": "name 3"}
]
}
]
}
];
i.e. the whole, the whole 'category 2' object removed, the whole 'subcategory 2' removed, object with ID '1' removed.
The problem is that the depth of those objects is variable and unknown - some objects have no children, some have children that also have children etc., any subcategory can can itself have a subcategory and I basically need to find object with no children that have defined IDs and keep the whole path to each of them.
Thank you.
Basically, perform a depth first traversal of your tree invoking a callback function on each node. If that node is a leaf node and it's ID appears in your list then clone the branch that leads to that leaf, but don't re-clone any part of the branch that was already cloned.
Once you have constructed the partial and filtered copy of your tree you need to cleanup the original. I mutated the original tree in the process for book-keeping purposes - tracking which branches had already been cloned.
Edit: modified code to filter list of trees instead of just a single tree
var currentPath = [];
function depthFirstTraversal(o, fn) {
currentPath.push(o);
if(o.children) {
for(var i = 0, len = o.children.length; i < len; i++) {
depthFirstTraversal(o.children[i], fn);
}
}
fn.call(null, o, currentPath);
currentPath.pop();
}
function shallowCopy(o) {
var result = {};
for(var k in o) {
if(o.hasOwnProperty(k)) {
result[k] = o[k];
}
}
return result;
}
function copyNode(node) {
var n = shallowCopy(node);
if(n.children) { n.children = []; }
return n;
}
function filterTree(root, ids) {
root.copied = copyNode(root); // create a copy of root
var filteredResult = root.copied;
depthFirstTraversal(root, function(node, branch) {
// if this is a leaf node _and_ we are looking for its ID
if( !node.children && ids.indexOf(node.id) !== -1 ) {
// use the path that the depthFirstTraversal hands us that
// leads to this leaf. copy any part of this branch that
// hasn't been copied, at minimum that will be this leaf
for(var i = 0, len = branch.length; i < len; i++) {
if(branch[i].copied) { continue; } // already copied
branch[i].copied = copyNode(branch[i]);
// now attach the copy to the new 'parellel' tree we are building
branch[i-1].copied.children.push(branch[i].copied);
}
}
});
depthFirstTraversal(root, function(node, branch) {
delete node.copied; // cleanup the mutation of the original tree
});
return filteredResult;
}
function filterTreeList(list, ids) {
var filteredList = [];
for(var i = 0, len = list.length; i < len; i++) {
filteredList.push( filterTree(list[i], ids) );
}
return filteredList;
}
var hierarchy = [ /* your data here */ ];
var ids = [1,3];
var filtered = filterTreeList(hierarchy, ids);
You can use filterDeep method from deepdash extension for lodash:
var obj = [{/* get Vijay Jagdale's source object as example */}];
var idList = [2, 3];
var found = _.filterDeep(
obj,
function(value) {
return _.indexOf(idList, value.id) !== -1;
},
{ tree: true }
);
filtrate object will be:
[ { title: 'category 1',
children:
[ { title: 'subcategory 11',
children:
[ { id: 2, title: 'name 2' },
{ id: 3, title: 'name 3' } ] } ] },
{ title: 'category 2',
children:
[ { title: 'subcategory 21',
children: [ { id: 3, title: 'name cat2sub1id3' } ] } ] } ]
Here is the full working test for your use case
Although this is an old question I will add my 2 cents. The solution requires a straightforward iteration through the loops, subloops etc. and then compare IDs and build the resultant object. I have pure-javascript and jQuery solution. While the pure javascript works for the example above, I would recommend the jQuery solution, because it is more generic, and does a "deep copy" of the objects, in case you have large and complex objects you won't run into bugs.
function jsFilter(idList){
var rsltHierarchy=[];
for (var i=0;i<hierarchy.length;i++) {
var currCatg=hierarchy[i];
var filtCatg={"title":currCatg.title, "children":[]};
for (var j=0;j<currCatg.children.length;j++) {
var currSub=currCatg.children[j];
var filtSub={"title":currSub.title, "children":[]}
for(var k=0; k<currSub.children.length;k++){
if(idList.indexOf(currSub.children[k].id)!==-1)
filtSub.children.push({"id":currSub.children[k].id, "title":currSub.children[k].title});
}
if(filtSub.children.length>0)
filtCatg.children.push(filtSub);
}
if(filtCatg.children.length>0)
rsltHierarchy.push(filtCatg);
}
return rsltHierarchy;
}
function jqFilter(idList){
var rsltHierarchy=[];
$.each(hierarchy, function(index,currCatg){
var filtCatg=$.extend(true, {}, currCatg);
filtCatg.children=[];
$.each(currCatg.children, function(index,currSub){
var filtSub=$.extend(true, {}, currSub);
filtSub.children=[];
$.each(currSub.children, function(index,currSubChild){
if(idList.indexOf(currSubChild.id)!==-1)
filtSub.children.push($.extend(true, {}, currSubChild));
});
if(filtSub.children.length>0)
filtCatg.children.push(filtSub);
});
if(filtCatg.children.length>0)
rsltHierarchy.push(filtCatg);
});
return rsltHierarchy;
}
//Now test the functions...
var hierarchy = eval("("+document.getElementById("inp").value+")");
var IDs = eval("("+document.getElementById("txtBoxIds").value+")");
document.getElementById("oupJS").value=JSON.stringify(jsFilter(IDs));
$(function() {
$("#oupJQ").text(JSON.stringify(jqFilter(IDs)));
});
#inp,#oupJS,#oupJQ {width:400px;height:100px;display:block;clear:all}
#inp{height:200px}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
ID List: <Input id="txtBoxIds" type="text" value="[2, 3]">
<p>Input:
<textarea id="inp">[
{
"title": "category 1",
"children": [
{"title": "subcategory 11",
"children": [
{"id": 1, "title": "name 1"},
{"id": 2, "title": "name 2"},
{"id": 3, "title": "name 3"}
]
},
{"title": "subcategory 12",
"children": [
{"id": 1, "title": "name 4"}
]
}
]
},
{
"title": "category 2",
"children": [
{"title": "subcategory 21",
"children": [
{"id": 3, "title": "name cat2sub1id3"},
{"id": 5, "title": "name cat2sub1id5"}
]
},
{"title": "subcategory 22",
"children": [
{"id": 6, "title": "name cat2sub2id6"},
{"id": 7, "title": "name cat2sub2id7"}
]
}
]
}
]</textarea>
<p>Pure-Javascript solution results:
<textarea id="oupJS"></textarea>
<p>jQuery solution results:
<textarea id="oupJQ"></textarea>
I'd not reinvent the wheel. We use object-scan for most of our data processing now and it solves your question nicely. Here is how
// const objectScan = require('object-scan');
const filter = (input, ids) => {
objectScan(['**[*]'], {
filterFn: ({ value, parent, property }) => {
if (
('id' in value && !ids.includes(value.id))
|| ('children' in value && value.children.length === 0)
) {
parent.splice(property, 1);
}
}
})(input);
};
const hierarchy = [ { title: 'category 1', children: [ { title: 'subcategory 1', children: [ { id: 1, title: 'name 1' }, { id: 2, title: 'name 2' }, { id: 3, title: 'name 3' } ] }, { title: 'subcategory 2', children: [ { id: 1, title: 'name 4' } ] } ] }, { title: 'category 2', children: [] } ];
filter(hierarchy, [2, 3]);
console.log(hierarchy);
// => [ { title: 'category 1', children: [ { title: 'subcategory 1', children: [ { id: 2, title: 'name 2' }, { id: 3, title: 'name 3' } ] } ] } ]
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan

Categories