I have arrays of objects that can also have arrays of their own. My main goal is to find an object with a given id in the whole tree and get readmap to that element by displaying the names of the objects names where it occurs.
For example I have data object like this:
{
id: '0',
name: "Boys"
children: [
{
name: "Soldiers",
children: [
{
name: "Bravo"
children: [
{name: "Tom"},
{name: "Andrew"}
]
}
]
},
{
name: "Runners",
children: [
{
name: "Team B"
children: [
{name: "Mark"},
{name: "David"}
]
}
]
}
]
}
I am currently finding an item by a function
function findByName (name, array) {
for (const node of array) {
if (node.name === name) return node;
if (node.children) {
const child = findByName(name, node.children);
if (child) return child;
}
}
}
But to achive my goal I need also roadmap to that value. For example.
When I want to find "Tom". Besides results of findByName I would like to get {name: "Tom", road: ["Boys", "Soldiers", "Bravo"]
You would need to pass down another property which handles the path. Start by defining path as an empty array. And since you only care about the name, you can push the name into this array everytime you find a node that has children.
Then you just keep passing the updated array to your recursive function. See my working example below:
(I updated your function to return an object which contains both the result and path)
function findByName(name, array, path = []) {
for (const node of array) {
if (node.name === name) return {result: node, path};
if (node.children) {
path.push(node.name) // We update the path with the current node name that has children
const child = findByName(name, node.children, path );
if (child) return { result: child, path};
}
}
}
Demo:
https://jsitor.com/VnktoLq49
You could add the path for every leve in the calling function without handing over the path.
const
findByName = (array, name) => {
for (const node of array) {
if (node.name === name) return { ...node, path: [] };
if (node.children) {
const child = findByName(node.children, name);
if (child) return { ...child, path: [node.name, ...child.path] };
}
}
},
data = [{ id: '0', name: "Boys", children: [{ name: "Soldiers", children: [{ name: "Bravo", children: [{ name: "Tom" }, { name: "Andrew" }] }] }, { name: "Runners", children: [{ name: "Team B", children: [{ name: "Mark" }, { name: "David" }] }] }] }];
console.log(findByName(data, 'Tom'));
.as-console-wrapper { max-height: 100% !important; top: 0; }
I like generators for this kind or problem because it allows you to select one, many, or all results. Additionally generators give control to the caller, allowing you to stop searching whenever you are satisfied with the result. This can be accomplished with a single function -
function* select(a = [], query = Boolean, path = [])
{ for (const t of a)
{ if (query(t)) yield { ...t, path }
yield *select(t.children, query, [...path, t.name])
}
}
const data =
[{ id: '0', name: "Boys", children: [{ name: "Soldiers", children: [{ name: "Bravo", children: [{ name: "Tom" }, { name: "Andrew" }] }] }, { name: "Runners", children: [{ name: "Team B", children: [{ name: "Mark" }, { name: "David" }] }] }] }]
// select "Tom" OR "Mark"
for (const r of select(data, v => v.name == 'Tom' || v.name == "Mark"))
console.log("found:", r)
found: {
"name": "Tom",
"path": [
"Boys",
"Soldiers",
"Bravo"
]
}
found: {
"name": "Mark",
"path": [
"Boys",
"Runners",
"Team B"
]
}
If you want only the first result, we can use return or break, and searching stops immediately, potentially saving many wasted computations -
function first (it)
{ for (const x of it)
return x // <- return and stop searching
}
first(select(data, v => v.name == "Andrew"))
{
"name": "Andrew",
"path": [
"Boys",
"Soldiers",
"Bravo"
]
}
If you want all of the results, we can use Array.from. Because select is flexible, it allows us to do all sorts of useful queries -
Array.from(select(data, v => !v.children), r => r.name)
[
"Tom",
"Andrew",
"Mark",
"David"
]
Related
I have an array that looks something like this:
const arrayObj = [
{
id: 1,
itemsList: [
{
name: "Paul",
},
{
name: "Newman",
},
],
},
{
id: 2,
itemsList: [
{
name: "Jack",
},
{
name: "Man",
},
],
},
]
What I want is to filter the objects whose itemsList contain an object with the name of a certain value. For example, I want to be able to filter out an array with objects whose inner objects with names that contain "ul" (in this case the name Paul contains "ul"), it should give me an output as such:
const outputArray = [
{
id: 1,
itemsList: [
{
name: "Paul",
},
{
name: "Newman",
},
]
}
]
So far, I've only been able to filter out a simple flat array of objects with this function:
function filterByName(array: any, string: any) {
return array.filter((obj: any) =>
["name"].some((key: any) =>
String(obj[key]).toLowerCase().includes(string.toLowerCase())
)
);
}
but I don't know how to apply it to my case.
Here you can use the some method combined with the includes method
const arrayObj = [{
id: 1,
itemsList: [{
name: "Paul",
},
{
name: "Newman",
},
],
},
{
id: 2,
itemsList: [{
name: "Jack",
},
{
name: "Man",
},
],
},
]
const getFilterArray = (name) => {
return arrayObj.filter(obj => obj.itemsList.some(x => x.name.toLowerCase().includes(name.toLowerCase())))
}
console.log(getFilterArray("ul"))
const result = arrayObj.filter(({ itemsList }) =>
itemsList.some(({ name }) => name.toLowerCase().includes('ul')));
Can you try this?
I have the following array of deeply nested objects:
const data = [
{
name: "foo",
children:[
{
count: 1,
name: "A"
},
{
count: 2,
name: "B"
}
]
},
{
name: "bar",
children: [
{
count: 3,
name: "C",
children: [
{
count: 4,
name: "D"
}
]
}
]
}
]
The way I'd like to transform this would be such as:
const expectedStructure = [
{
count: 1,
name: "A",
label: "foo = A"
},
{
count: 2,
name: "B",
label: "foo = B"
},
{
count: 3,
name: "C",
label: "bar = C"
},
{
count: 4,
name: "D",
label: "bar = D"
}
]
I created recursive function that transforms nested array into array of flat objects.
Here's my code:
function getChildren(array, result=[]) {
array.forEach(({children, ...rest}) => {
result.push(rest);
if(children) {
getChildren(children, result);
}
});
return result;
}
And here's output I get:
[ { name: 'foo' },
{ count: 1, name: 'A' },
{ count: 2, name: 'B' },
{ name: 'bar' },
{ count: 3, name: 'C' },
{ count: 4, name: 'D' } ]
The problem is that I need to add label field to every object in my output array, and I can't find a solution without iterating multiple times through the final array to make desired transformation. How to properly insert label field without hugely augmenting complexity of the function?
Check each iteration whether the current item is a "parent" item, and reassign label if it is.
const data = [{name:"foo",children:[{count:1,name:"A"},{count:2,name:"B"}]},{name:"bar",children:[{count:3,name:"C",children:[{count:4,name:"D"}]}]}];
function getChildren(array, result = [], label = "") {
array.forEach(({ children, name, count }) => {
if (!label || name[1]) {
label = `${name} = `;
}
if (count) {
result.push({ count, name, label: label + name });
}
if (children) {
getChildren(children, result, label);
}
});
return result;
}
const res = getChildren(data);
console.log(res);
You can use a different function for the nested levels, so you can pass the top-level name properties down through all those recursion levels.
function getTopChildren(array, result = []) {
array.forEach(({
name,
children
}) => {
if (children) {
getChildren(children, name, result);
}
});
return result;
}
function getChildren(array, name, result) {
array.forEach(({
children,
...rest
}) => {
rest.label = `${name} = ${rest.name}`;
result.push(rest);
if (children) {
getChildren(children, name, result);
}
});
}
const data = [{
name: "foo",
children: [{
count: 1,
name: "A"
},
{
count: 2,
name: "B"
}
]
},
{
name: "bar",
children: [{
count: 3,
name: "C",
children: [{
count: 4,
name: "D"
}]
}]
}
]
console.log(getTopChildren(data));
You can also do this recursively with flatMap based on whether or not a parent has been passed into the recursive call :
const data = [{
name: "foo",
children: [{
count: 1,
name: "A"
},
{
count: 2,
name: "B"
}
]
},
{
name: "bar",
children: [{
count: 3,
name: "C",
children: [{
count: 4,
name: "D"
}]
}]
}
];
function flatten(arr, parent = null) {
return parent
? arr.flatMap(({name, count, children}) => [
{name, count, label: `${parent} = ${name}`},
...flatten(children || [], parent)
])
: arr.flatMap(({name, children}) => flatten(children || [], name));
}
console.log(flatten(data));
Sometimes it's a little easier to reason about the code and write it clearly using generators. You can yield* from the recursive calls:
const data = [{name: "foo",children:[{count: 1,name: "A"},{ count: 2,name: "B"}]},{name: "bar",children: [{count: 3,name: "C",children: [{count: 4,name: "D"}]}]}]
function* flat(input, n){
if (!input) return
if (Array.isArray(input)) {
for (let item of input)
yield* flat(item, n)
}
let _name = n || input.name
if ('count' in input) {
yield { count:input.count, name:input.name, label:`${_name} = ${input.name}`}
}
yield* flat(input.children, _name)
}
let g = [...flat(data)]
console.log(g)
The function returns a generator, so you need to spread it into a list [...flat(data)] if you want a list or iterate over it if you don't need to store the list.
I'm trying to filter an array having nested level of 3.. I have to filter this array at the last level.
array = [{
children: [{
children: [{
children: [],
id: 2694,
name: "Some Random data"
}, {
children: [],
id: 2695,
name: "Another Random Data"
}],
id: 2574,
name: "Test data",
}],
id: 2530,
name: "Main Test data"
}, {
children: [{
children: [{
children: [],
id: 2696,
name: "Secondary test Data"
}, {
children: [],
id: -1,
name: "Random Text"
}],
id: 2575,
name: "Another random Text"
}],
id: 2531,
name: "New Data"
}]
I have tried this function
function(random){
let array3=[];
this.array.forEach(cap=>{
let tempparent={...cap};
let child1= tempparent.children.forEach(ch=>{
let tempfeat={...ch};
let tempchildren = tempfeat.children.filter(fe=>{
if(fe.id!=random.id){
return fe
}
});
// console.log(tempchildren)
tempfeat.children = tempchildren;
// console.log(tempfeat.children)
});
console.log(child1)
tempparent.children= child1;
console.log(tempparent.children)
nodes3.push(tempparent)
})
this.array= array3
console.log(this.array);
}
I want to filter it at third level using id value. when the id matches the matched object must be removed.
You could take a dynamic approach and take the children out of the object, check the id and if found, ignore the object.
Otherwise take the children and get a subset with a recursive call and rebuild a new object and push this one to the result set.
This approach does not mutate the original data, but returns all new objects and works for an arbitrary count of levels.
function remove(array, id) {
return array.reduce((r, { children, ...o }) => {
if (o.id === id) return r;
children = remove(children || [], id);
if (children.length) o.children = children;
r.push(o);
return r;
}, []);
}
var data = [{ children: [{ children: [{ children: [], id: 2694, name: "Some Random data" }, { children: [], id: 2695, name: "Another Random Data" }], id: 2574, name: "Test data", }], id: 2530, name: "Main Test data" }, { children: [{ children: [{ children: [], id: 2696, name: "Secondary test Data" }, { children: [], id: -1, name: "Random Text" }], id: 2575, name: "Another random Text" }], id: 2531, name: "New Data" }],
result = remove(data, 2574);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can use recursive function.
For example, like this
function removeById(_id,arr){
if(arr.id === _id){
return true;
}else{
arr.children.forEach(currentItem => {
if(getById(_id,currentItem)){
arr.children = arr.children.filter(x=>x.id !== _id);
}
});
}
}
and using this function
removeById(2694,array[0]);
removeById(2694,array[1]);
please examine example
let say i have a tree in javascript
a1
--b
----c1
a2
--b2
--b3
----c2
and if i wanted to find c2, it should return a2->b3->c2
Lets say my json looked like this?
treeFamily = {
name : "Parent",
children: [{
name : "Child1",
children: [{
name : "Grandchild1",
children: []
},{
name : "Grandchild2",
children: []
},{
name : "Grandchild3",
children: []
}]
}, {
name: "Child2",
children: []
}]
};
You could check if the nested children have the wanted key/value. Then take the name and hand over the result to the outer call.
function findPath(array, target) {
var path;
array.some(({ name, children }) => {
var temp;
if (name === target) {
path = [name];
return true;
}
if (temp = findPath(children, target)) {
path = [name, ...temp];
return true;
}
});
return path;
}
var treeFamily = { name: "Parent", children: [{ name: "Child1", children: [{ name: "Grandchild1", children: [] }, { name: "Grandchild2", children: [] }, { name: "Grandchild3", children: [] }] }, { name: "Child2", children: [] }] };
console.log(findPath([treeFamily], 'Grandchild2'));
console.log(findPath([treeFamily], 'foo'));
You can use for...of to search the children by calling the function recursively. If the target is found, the name is returned, and combined with the previous names. If not, the function will return undefined. Alternatively, you can return an empty array.
const findPath = (targetName, { name, children }) => {
if(name === targetName) return [name];
for(const child of children) {
const result = findPath(targetName, child);
if(result) return [name, ...result];
}
// if child not found implicitly return undefined or return [] to get an empty array
};
const treeFamily = { name: "Parent", children: [{ name: "Child1", children: [{ name: "Grandchild1", children: [] }, { name: "Grandchild2", children: [] }, { name: "Grandchild3", children: [] }] }, { name: "Child2", children: [] }] };
console.log(findPath('Child2', treeFamily));
console.log(findPath('Grandchild3', treeFamily));
console.log(findPath('Grandchild400', treeFamily));
I want to filter out a nested array of objects but stuck at the filter part.
How to remove one of the mark?
this.state = {
data: [
{
id: 1,
name: "Main",
subs: [
{
id: "jay",
name: "Jay",
mark: [
{
id: "5a5d84b94a074c49ef2d4553",
name: 100
},
{
id: "5a5d84b94a074119ef2d4553",
name: 70
}
]
}
]
}
]
};
https://codesandbox.io/s/p39momxzp7
I try to use es6 as it's more readable.
expected output
data: [
{
id: 1,
name: "Main",
subs: [
{
id: "jay",
name: "Jay",
mark: [
{
id: "5a5d84b94a074119ef2d4553",
name: 70
}
]
}
]
}
]
Since there are multiple nested arrays in your data structure, you need to use forEach those many times
data.forEach( s => //iterate data
s.subs.forEach( t => //iterate subs
( t.mark = t.mark.slice( 1, 2 ) ) ) ); //slice the second value out
Demo
var data = [{
id: 1,
name: "Main",
subs: [{
id: "jay",
name: "Jay",
mark: [{
id: "5a5d84b94a074c49ef2d4553",
name: 100
},
{
id: "5a5d84b94a074119ef2d4553",
name: 70
}
]
}]
}];
data.forEach(s => s.subs.forEach(t => (t.mark = t.mark.slice(1,2))));
console.log(JSON.stringify(data, 0, 4))
In case the last value should be picked?
data.forEach( s => //iterate data
s.subs.forEach( t => //iterate subs
( t.mark = t.mark.slice( -1 ) ) ) ); //slice the last value out
If you are trying to filter a relevant mark by a given id,
you can combine Array#map and Array#filter to achieve it:
Note that i'm also using the Object Rest/Spread Properties proposal (stage 4)
Running example
const state = {
data: [{
id: 1,
name: "Main",
subs: [{
id: "jay",
name: "Jay",
mark: [{
id: "5a5d84b94a074c49ef2d4553",
name: 100
}, {
id: "5a5d84b94a074119ef2d4553",
name: 70
}]
}]
}]
};
const mark_id = '5a5d84b94a074119ef2d4553';
const nextState = {
...state,
data: state.data.map(obj => {
const filteredSubs = obj.subs.map(sub => {
const markById = sub.mark.filter(m => m.id === mark_id);
return {
...sub,
mark: markById
}
});
return {
...obj,
subs: filteredSubs
}
})
};
console.log(nextState);
You can even use lodash which contains many methods that can be handled easily.
Check if this is what you are looking for. (there is a good scope to refactor it but before that would like to understand if thats what you are looking for)
Below is the code that has been used there.
let inputId = "5a5d84b94a074c49ef2d4553";
let filteredData =_.each(_.cloneDeep(data), function(value, key1) {
_.each(value.subs, function(valueSubs, key2) {
var finalSubMark = _.find(valueSubs.mark, function(eachMark) {
return eachMark.id == inputId;
});
_.set(valueSubs, "mark", finalSubMark);
});
});
https://codesandbox.io/s/v065w05rly