Search implementation for Multilevel array - javascript

i have JSON like below.
const testData = [
{
menu: 'Test',
submenu: [
{
menu: 'Test1',
submenu: [
{
menu: 'Test1.1',
},
{
menu: 'Test1.2',
},
{
secondLevel: [
{
menu: 'Test1.3',
submenu: [
{
menu: 'Test1.4',
},
{
menu: 'Test1.5',
},
],
},
],
},
],
},
i have used reduce function to traverse to search the expected word like Test1.1 i am getting proper value, whereas while search the Test1.4 is not coming up properly as it has secondLevel as parent object.
The code i used is below which is suggested in stackoverflow.
function search(data, value) {
return data.reduce((r, e) => {
const object = { ...e }
const result = search(e.submenu || [], value)
if (result.length) object.submenu = result
if (e.menu == value || result.length) r.push(object)
return r;
}, [])
}
Please suggest best way to search the element in secondLevel object as well. Thanks in advance

Related

How to loop over nested JSON array and filter out search Query in Angular?

I have a complex nested JSON Array and I want to filter it(name property) through based on what user enters in input tag and show it as an autocomplete. A basic of it I have created here on stackblitz click here for the code. I have two entries of name "Tom" in two different objects so when user types Tom it should appear twice in the autocomplete as of now it shows only once. So if I press letter "T" it should show me all the names starting with "T". Here in this case "Tom" twice if I press "To" and if I press just "T" then Tiffany and Tom 2 times. Could you please help me here in the code ?
Any help is appreciated. Thanks much!
You can check the below code also, I have updated code you check in stackbliz https://stackblitz.com/edit/angular-ivy-k14se7
matches = [];
ngOnInit() {}
searchKeyup(ev) {
var term: any = ev.target as HTMLElement;
console.log("Value", term.value);
this.matches = [];
let content = [
{
name: 'Robert',
children: [],
},
{
name: 'Doug',
children: [
{
name: 'James',
children: [
{
name: 'John',
children: [
{
name: 'Tom',
children: [],
},
],
},
],
},
],
},
{
name: 'Susan',
children: [
{
name: 'Tiffany',
children: [
{
name: 'Merry',
children: [
{
name: 'Sasha',
children: [],
},
{
name: 'Tommy',
children: [],
},
],
},
],
},
],
},
];
if(term.value.length > 0){
this.filter(content, term.value);
} else {
document.getElementById('list').innerHTML = '';
}
if (this.matches.length > 0) {
document.getElementById('list').innerHTML = this.matches.map(match => match.name).join(",");
} else{
document.getElementById('list').innerHTML = "";
}
}
filter(arr, term) {
arr.forEach((i) => {
if (i.name.includes(term)) {
this.matches.push(i);
}
if (i.children.length > 0) {
this.filter(i.children, term);
}
});
console.log(this.matches);
}
You were on a good path. The only missing thing in this recursive walk is keeping state of matches like this:
filter(arr, term, matches) {
if (term.length == 0) {
matches = [];
document.getElementById('list').innerHTML = '';
}
arr.forEach((i) => {
if (i.name.includes(term)) {
matches.push(i);
}
if (i.children.length > 0) {
this.filter(i.children, term, matches);
}
});
console.log(matches);
if (matches.length > 0) {
document.getElementById('list').innerHTML = matches[0].name;
}
}

Filter array of objects by value

I want to filter an array of objects, by a specific value within the objects.
In the example i've provided I want to filter the array 'pets' by a value in the array 'characteristics'. For example, where I have called the function with the param 'loyal', i'd only expect the object for the dog value to be returned, as only the dog has that characteristic.
At the moment when I call the function both objects are returned even though only the object for dog has that value in its characteristics array.
const pets = [
{
name: 'dog',
characteristics: [
{
value: 'loyal'
},
{
value: 'big'
}
]
},
{
name: 'cat',
characteristics: [
{
value: 'fluffy'
},
{
value: 'small'
}
]
},
]
function filterPets(pets, characteristic) {
return pets.filter(function(pet) {
return pet.characteristics.filter(o => o.value.includes(characteristic));
})
}
console.log(filterPets(pets, 'loyal'));
That's because for the characteristics check you're using filter, which always returns an array (even if a blank one), and even a blank array is a truthy value, so the outer filter keeps every pet you check. For that inner check, you want some, not filter, so you get a flag for whether any entries matched:
function filterPets(pets, characteristic) {
return pets.filter(function(pet) {
return pet.characteristics.some(o => o.value.includes(characteristic));
// −−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^
});
}
const pets = [
{
name: 'dog',
characteristics: [
{
value: 'loyal'
},
{
value: 'big'
}
]
},
{
name: 'cat',
characteristics: [
{
value: 'fluffy'
},
{
value: 'small'
}
]
},
];
function filterPets(pets, characteristic) {
return pets.filter(function(pet) {
return pet.characteristics.some(o => o.value.includes(characteristic));
});
}
console.log(filterPets(pets, 'loyal'));
Just for what it's worth, I assume characteristics are unique (you can't have "loyal" twice), so you might prefer to keep those in a Set so you can check for them more easily than .some(o => o.includes(characteristic)). For instance:
const pets = [
{
name: "dog",
characteristics: new Set(["loyal", "big"]),
},
{
name: "cat",
characteristics: new Set(["fluffy", "small"]),
},
];
function filterPets(pets, characteristic) {
return pets.filter(function(pet) {
return pet.characteristics.has(characteristic);
});
}
Live Example:
const pets = [
{
name: "dog",
characteristics: new Set(["loyal", "big"]),
},
{
name: "cat",
characteristics: new Set(["fluffy", "small"]),
},
];
function filterPets(pets, characteristic) {
return pets.filter(function(pet) {
return pet.characteristics.has(characteristic);
});
}
console.log(filterPets(pets, "loyal"));
console.log("Don't worry about the {} for characteristics, the Stack Snippets console doesn't know how to display Set objects. Look in the real console if you want to double-check the set.");
function filterPets(list, charValue) {
const filteredPets = []
list.map(function(pet,petIndex,array) {
pet.characteristics.map(function(charac){
if(charac.value === charValue){
return filteredPets.push(array[petIndex])
}
})
})
return filteredPets
}
filterPets(pets,'loyal');

Search Inside Dropdown with data in tree structure React JS

I have developed a custom component which renders dropdown with a tree like structure inside it and allows the user to search for values inside the dropdown. Somehow the search works only after two levels of the tree structure.
We would be able to search only on the inside of NextJS label. The previous levels do not render results.
My function looks like this:
const searchFunction = (menu: treeData[], searchText: string) => {
debugger; //eslint-disable-line no-debugger
for (let i = 0; i < menu.length; i++) {
if (menu[i].name.includes(searchText)) {
setFound(true);
return menu[i].name;
} else if (!menu[i].name.includes(searchText)) {
if (menu[i].children !== undefined) {
return searchFunction(menu[i].children, searchText);
}
} else {
return 'Not Found';
}
}
};
And My data is like this:
import { treeData } from './DdrTreeDropdown.types';
export const menu: treeData[] = [
{
name: 'Web Project',
children: [
{
name: 'NextJS',
children: [
{
name: 'MongoDB',
},
{
name: 'Backend',
children: [
{
name: 'NodeJS',
},
],
},
],
},
{
name: 'ReactJS',
children: [
{
name: 'Express',
},
{
name: 'mysql',
children: [
{
name: 'jwt',
},
],
},
],
},
],
},
{
name: 'lorem project',
children: [
{
name: 'Vue Js',
children: [
{
name: 'Oracle Db',
},
{
name: 'JDBC',
children: [
{
name: 'Java',
},
],
},
],
},
{
name: 'ReactJS',
children: [
{
name: 'Express',
},
{
name: 'mysql',
children: [
{
name: 'jwt',
},
],
},
],
},
],
},
];
The sandbox link of the component is here:
https://codesandbox.io/s/upbeat-feynman-89ozi?file=/src/styles.ts
I haven't looked at the context that this is used in, so apologies if I'm missing something about how this is supposed to work. I've assumed that you can call setFound after running this function based on whether it finds anything or not and that it only needs to return one value. But hopefully this helps.
const menu = [{"name":"Web Project","children":[{"name":"NextJS","children":[{"name":"MongoDB"},{"name":"Backend","children":[{"name":"NodeJS"}]}]},{"name":"ReactJS","children":[{"name":"Express"},{"name":"mysql","children":[{"name":"jwt"}]}]}]},{"name":"lorem project","children":[{"name":"Vue Js","children":[{"name":"Oracle Db"},{"name":"JDBC","children":[{"name":"Java"}]}]},{"name":"ReactJS","children":[{"name":"Express"},{"name":"mysql","children":[{"name":"jwt"}]}]}]}];
const searchFunction = (menu, searchText) => {
let result;
for(let i = 0; i < menu.length; i++) {
if(menu[i].name.includes(searchText)) {
return menu[i].name;
} else if(menu[i].children !== undefined) {
result = searchFunction(menu[i].children, searchText);
if(result) return result;
}
}
return null;
};
console.log(searchFunction(menu, 'NextJS'));
console.log(searchFunction(menu, 'jwt'));
console.log(searchFunction(menu, 'foo'));
Looking at why the current version doesn't work, I think it goes something like this:
Let's take 'jwt' as the searchText.
We start in the 'Web Project' object, the name does not match, so we go to the else if block (BTW, we can never reach the else block as the else if condition is the opposite of the if condition).
The 'Web Project' object does have children so we will return from the new call to searchFunction; notice that 'lorem project' can never be reached as we will (regardless of the result) return the value of searchFunction and skip the rest of the loop.
Inside of our new and subsequent calls to searchFunction the same is going to happen until we find either a matching item or an item without children.
If we get to an item without children the the loop will successfully carry on to the siblings of the item.
If it doesn't find a match or an item with children it will exit the for loop and return undefined up the chain to the caller of the initial searchFunction.

How to extract paths to 'enabled' objects in nested object arrays

I'm a novice to recursion and I have a JSON structure with arrays of nested objects. Some of these objects have a boolean enabled: true. I'm trying to figure out how to extract the paths to all enabled objects and their children.
I tried both cleaning up the original object by removing unused paths but I got lost in accessing the parents. I also tried building a separate array of paths using dot-notation, as I can probably build a new nested object from that. My latest attempt at the dot-notation extract:
const sourceData = {
title: "Work",
tags: [
{
title: "Cleaning",
tags: [
{
title: "Floors"
},
{ title: "Windows", enabled: true },
{ title: "Ceilings", enabled: true }
]
},
{
title: "Maintenance",
tags: [
{
title: "Walls",
enabled: true,
tags: [
{
title: "Brickwall"
},
{
title: "Wooden wall"
}
]
},
{
title: "Roof"
}
]
},
{
title: "Gardening"
}
]
};
function getEnabledPaths(level, acc) {
for (const tag of level.tags) {
if (tag.enabled) {
return tag.title;
} else if (tag.hasOwnProperty("tags")) {
var path = this.getEnabledPaths(tag);
if (path) acc.push(tag.title + "." + path);
}
}
return acc;
}
console.log(getEnabledPaths(sourceData, []));
I only get:
[
"Cleaning.Windows",
"Maintenance.Walls"
]
I would ideally end up with something like this:
[
'Work.Cleaning.Windows',
'Work.Cleaning.Ceilings',
'Work.Maintenance.Walls.Brickwall',
'Work.Maintenance.Walls.Wooden Wall'
]
In a perfect world (but I tried for days and went back to getting the dot notation results):
{
title: "Work",
tags: [
{
title: "Cleaning",
tags: [
{
title: "Windows",
enabled: true
},
{
title: "Ceilings",
enabled: true
}
]
},
{
title: "Maintenance",
tags: [
{
title: "Walls",
enabled: true,
tags: [
{
title: "Brickwall"
},
{
title: "Wooden wall"
}
]
}
]
}
]
};
The key to the recursion function is to both a) deal with children and b) the item itself.
Here's my take, which seems to work:
const sourceData = {title:"Work",tags:[{title:"Cleaning",tags:[{title:"Floors"},{title:"Windows",enabled:true},{title:"Ceilings",enabled:true}]},{title:"Maintenance",tags:[{title:"Walls",enabled:true,tags:[{title:"Brickwall"},{title:"Woodenwall"}]},{title:"Roof"}]},{title:"Gardening"}]};
function itemFilter(item) {
// enabled? done with this item
if (item.enabled) return item;
// not enabled and no tags? set to null
if (!item.tags) return null;
// filter all children, remove null children
item.tags = item.tags.map(child => itemFilter(child)).filter(child => child);
return item;
}
console.log(itemFilter(sourceData));
.as-console-wrapper {
max-height: 100vh !important;
}
You could pass enabled parameter down to lower levels of recursion if true value is found on some of the upper levels and based on that add path to the results or not.
const data ={"title":"Work","tags":[{"title":"Cleaning","tags":[{"title":"Floors"},{"title":"Windows","enabled":true},{"title":"Ceilings","enabled":true}]},{"title":"Maintenance","tags":[{"title":"Walls","enabled":true,"tags":[{"title":"Brickwall"},{"title":"Wooden wall"}]},{"title":"Roof"}]},{"title":"Gardening"}]}
function paths(data, prev = '', enabled = false) {
const result = [];
prev += (prev ? "." : '') + data.title;
if (!enabled && data.enabled) enabled = true;
if (!data.tags) {
if (enabled) {
result.push(prev);
}
} else {
data.tags.forEach(el => result.push(...paths(el, prev, enabled)))
}
return result;
}
const result = paths(data)
console.log(result)

Creating an array of custom objects from nested data structure

I'm working on a project that requires me to massage some API data (shown in the snippet below as 'apiData'). The data structure I ultimately need for the charting library I'm using (Recharts) is this:
[
{ date: '2018-04-24', TSLA: 283.37, AAPL: 250.01 },
{ date: '2018-04-25', AAPL: 320.34 }
]
I've put together the function below and it works well enough, but I'm having trouble getting all the data to show up, even if there's no match between dates. In the below example, you'll notice that the object for date "2018-04-23" in the apiData is excluded. Ideally, the final ds would look like this:
[
{ date: '2018-04-23', TSLA: 285.12 }
{ date: '2018-04-24', TSLA: 283.37, AAPL: 250.01 },
{ date: '2018-04-25', AAPL: 320.34 }
]
Also, there's probably a more performant way to do this, but I've been hacking away for a while and not seeing a better solution at the moment. E.g. the forEach isn't ideal as the data set grows, which it will when I need to plot long time periods of data.
So my questions are: 1) How can I make sure objects that match in date are combined while objects that don't are still included and 2) what's a more performant way to do this operation?
If anyone has any input or critique of my approach and how I can improve it, I'd greatly appreciate it.
Here's a link to a repl if it's more convenient then the code snippet below.
formatChartData = (data) => {
const chartData = data
.reduce((arr, stock) => {
const stockArr = stock.chart.map((item) => {
let chartObj = {};
chartObj.date = item.date;
chartObj[stock.quote.symbol] = item.close;
if (arr.length > 0) {
arr.forEach((arrItem) => {
if (arrItem.date === item.date) {
chartObj = { ...arrItem, ...chartObj };
}
});
}
return chartObj;
});
return stockArr;
}, []);
console.log(chartData)
}
const apiData = [
{
chart: [
{
date: "2018-04-23",
open: 291.29,
close: 285.12,
},
{
date: "2018-04-24",
open: 291.29,
close: 283.37,
},
],
news: [],
quote: {
symbol: "TSLA"
},
},
{
chart: [
{
date: "2018-04-24",
open: 200.29,
close: 250.01,
},
{
date: "2018-04-25",
open: 290.20,
close: 320.34,
},
],
news: [],
quote: {
symbol: "AAPL"
},
}
]
formatChartData(apiData)
EDIT: I ended up using charlietfl's solution with an inner forEach as I found this easier to read than using two reduce methods. The final function looks like this:
const chartData = data
.reduce((map, stock) => {
stock.chart.forEach((chart) => {
const chartObj = map.get(chart.date) || { date: chart.date };
chartObj[stock.quote.symbol] = chart.close;
map.set(chart.date, chartObj);
});
return map;
}, new Map());`
A cleaner way than having to loop through the accumulated new array each time to look for a date is to use one master object with dates as keys
Following I use reduce() to return a Map (could be an object literal also) using dates as keys and then convert the Map values iterable to array to get the final results
const dateMap = apiData.reduce((map,stock)=>{
return stock.chart.reduce((_, chartItem)=>{
// get stored object for this date, or create new object
const dayObj = map.get(chartItem.date) || {date: chartItem.date};
dayObj[stock.quote.symbol] = chartItem.close;
// store the object in map again using date as key
return map.set(chartItem.date, dayObj);
},map);
}, new Map)
const res = [...dateMap.values()];
console.log(res)
.as-console-wrapper {max-height: 100%!important;}
<script>
const apiData = [
{
chart: [
{
date: "2018-04-23",
open: 291.29,
close: 285.12,
},
{
date: "2018-04-24",
open: 291.29,
close: 283.37,
},
],
news: [],
quote: {
symbol: "TSLA"
},
},
{
chart: [
{
date: "2018-04-24",
open: 200.29,
close: 250.01,
},
{
date: "2018-04-25",
open: 290.20,
close: 320.34,
},
],
news: [],
quote: {
symbol: "AAPL"
},
}
]
</script>
Just correcting your code only, else reduce should be used instead of map, for charts also.
formatChartData = (data) => {
const chartData = data
.reduce((arr, stock) => {
const stockArr = stock.chart.map((item) => {
let chartObj = {};
chartObj.date = item.date;
chartObj[stock.quote.symbol] = item.close;
if (arr.length > 0) {
arr.forEach((arrItem, i) => {
if (arrItem.date === item.date) {
chartObj = { ...arrItem, ...chartObj };
delete(arr[i]);
}
});
}
return chartObj;
});
return [...arr, ...stockArr].filter(e=>!!e); //to remove undefined elements left by delete above.
}, []);
console.log(chartData)
}
const apiData = [
{
chart: [
{
date: "2018-04-23",
open: 291.29,
close: 285.12,
},
{
date: "2018-04-24",
open: 291.29,
close: 283.37,
},
],
news: [],
quote: {
symbol: "TSLA"
},
},
{
chart: [
{
date: "2018-04-24",
open: 200.29,
close: 250.01,
},
{
date: "2018-04-25",
open: 290.20,
close: 320.34,
},
],
news: [],
quote: {
symbol: "AAPL"
},
}
]
formatChartData(apiData)
The problem is that your reduce function is running for each item in the data array. When it runs on the first item in the data array it returns:
[
{ date: '2018-04-23', TSLA: 285.12 },
{ date: '2018-04-24', TSLA: 283.37 }
]
When it runs on the second item in the array it returns this:
[
{ date: '2018-04-24', TSLA: 283.37, AAPL: 250.01 },
{ date: '2018-04-25', AAPL: 320.34 }
]
This is because when the reduce runs on the last array item it is returning the result from that item. You are only merging items from the accumulator variable "arr" if their date is in the current array item as well. Since 2018-04-23 is in the first but not the second it is not being added. I have added two things to your code. First if the current date being looped on is in the accumulator variable "arr" I delete it from "arr" after it is merged in. The second change is after each .reduce loop there is still some dates left in "arr" that aren't in the current "stockArr". To deal with this I merge both "arr" and "stockArr" which will give you what you are looking for.
formatChartData = (data) => {
const chartData = data
.reduce((arr, stock) => {
const stockArr = stock.chart.map((item) => {
let chartObj = {};
chartObj.date = item.date;
chartObj[stock.quote.symbol] = item.close;
if (arr.length > 0) {
arr.forEach((arrItem, index) => {
if (arrItem.date === item.date) {
chartObj = { ...arrItem, ...chartObj };
arr.splice(index, 1);
}
});
}
return chartObj;
});
return [...arr, ...stockArr];
}, []);
console.log(chartData)
}
const data = [
{
chart: [
{
date: "2018-04-23",
open: 291.29,
close: 285.12,
},
{
date: "2018-04-24",
open: 291.29,
close: 283.37,
},
],
news: [],
quote: {
symbol: "TSLA"
},
},
{
chart: [
{
date: "2018-04-24",
open: 200.29,
close: 250.01,
},
{
date: "2018-04-25",
open: 290.20,
close: 320.34,
},
],
news: [],
quote: {
symbol: "AAPL"
},
}
]
formatChartData(data)

Categories

Resources