I am trying to write a filter function which takes in an object as a parameter and the query string as its second parameter. The function should return the list of all the values from the object that match the query string.
For example
var data = [{
label: 'Cars',
children: [{
label: 'Volkswagan',
children: [{
label: 'Passat'
}]
}, {
label: 'Toyota'
}]
}, {
label: 'Fruits',
children: [{
label: 'Grapes'
}, {
label: 'Oranges'
}]
}];
function filter(data, query){}
filter(data,'ra'); //['Grapes', 'Oranges']
My question is how to tackle the nested 'children' property for each indexed object?
You want to use recursion for this.
function filter(data, query){
var ret = [];
data.forEach(function(e){
// See if this element matches
if(e.label.indexOf(query) > -1){
ret.push(e.label);
}
// If there are children, then call filter() again
// to see if any children match
if(e.children){
ret = ret.concat(filter(e.children, query));
}
});
return ret;
}
Try using recursive calls based on the data type of each property. For example, in the case of a nested property that's an array, you will want to call filter on each element of that array. Similar logic in the case that the nested element is an object, you want to look at each property and call filter. I wrote this off the cuff, so I haven't tested all of the corner cases, but it works for your test example:
var results = [];
filter(data,'ra'); //['Grapes', 'Oranges']
console.log(results);
function filter(data,query){
for(var prop in data){
//array
if(Array.isArray(data[prop])){
for(var i = 0; i < data[prop].length; i++){
filter(data[prop][i],query);
}
} else if (typeof data[prop] === "object"){
filter(data[prop],query);
} else if(typeof data[prop] === "string"){
if(data[prop].indexOf(query) > -1){
results.push(data[prop]);
}
}
}
}
Related
expect result: ["human", "head", "eye"]
ex.
const data = {
name: "human",
children: [
{
name: "head",
children: [
{
name: "eye"
}
]
},
{
name: "body",
children: [
{
name: "arm"
}
]
}
]
}
const keyword = "eye"
Using the above data and using ffunction to obtain result
expect_result = f(data)
What kind of function should I write?
Thanks.
You could use an iterative and recursive approach by checking the property or the nested children for the wanted value. If found unshift the name to the result set.
function getNames(object, value) {
var names = [];
[object].some(function iter(o) {
if (o.name === value || (o.children || []).some(iter)) {
names.unshift(o.name);
return true;
}
});
return names;
}
var data = { name: "human", children: [{ name: "head", children: [{ name: "eye" }] }, { name: "body", children: [{ name: "arm" }] }] };
console.log(getNames(data, 'eye'));
console.log(getNames(data, 'arm'));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Try a recursive function
const getData = items => {
let fields = [];
for (const item of items) {
if (item.name) {
fields.push(item.name);
}
if (Array.isArray(item.children)) {
fields.push(...getData(item.children));
}
}
return fields;
}
var result = [] // This is outside the function since it get updated
function f(data) {
result.push(data.name); // Add the only name to the Array
if (data.children) { // just checking to see if object contains key children (staying safe)
for (var i = 0; i < data.children.length; i++) { // we are only looping through the children
f(data.children[i]); // Call this particular function to do the same thing again
}
}
}
Basically this function set the name. Then loop through the no of children and then calls a function to set the name of that child and then loop through its children too. Which happen to be a repercussive function till it finishes in order, all of them
I have an object that can be deeply nested with objects, arrays, arrays of objects, and so on.
Every nested object has a sys property which in turn has an id property.
I have a separate list of id values that correspond to objects that I want to remove from the original object. How can I go about recursively looping through the entire object and modify it to no longer include these?
For example, say I have the following data:
let data = {
sys: {
id: '1'
},
items: [
{
sys: {
id: '2'
},
fields: {
title: 'Item title',
sponsor: {
sys: {
id: '3'
},
fields: {
title: 'Sponsor Title'
}
},
actions: [
{
sys: {
id: '4'
},
fields: {
title: 'Google',
url: 'google.com'
}
},
{
sys: {
id: '5'
},
fields: {
title: 'Yahoo',
url: 'yahoo.com'
}
}
]
}
}
]
}
Then I have an array of id's to remove:
const invalidIds = ['3', '5'];
After I run the function, the resulting object should have the property with sys.id of '3' set to null, and the object with sys.id of '5' should simply be removed from its containing array:
// Desired Output:
{
sys: {
id: '1'
},
items: [
{
sys: {
id: '2'
},
fields: {
title: 'Item title',
sponsor: null,
actions: [
{
sys: {
id: '4'
},
fields: {
title: 'Google',
url: 'google.com'
}
}
]
}
}
]
}
With help from this solution, I'm able to recursively search through the object and its various nested arrays:
const process = (key, value) => {
if (typeof value === 'object' && value.sys && value.sys.id && invalidIds.includes(value.sys.id)) {
console.log('found one', value.sys.id);
}
};
const traverse = (obj, func) => {
for (let key in obj) {
func.apply(this, [key, obj[key]]);
if (obj[key] !== null) {
if (typeof obj[key] === 'object') {
traverse(obj[key], func);
} else if (obj[key].constructor === Array) {
obj[key].map(item => {
if (typeof item === 'object') {
traverse(item, func);
}
});
}
}
}
};
traverse(data, process);
However I can't figure out how to properly modify the array. In addition, I'd prefer to create an entirely new object rather than modify the existing one in order to keep things immutable.
Here are the observations that led to my solution:
To create a new object, you need to use return somewhere in your function.
To remove items from array, you need to filter out valid items first, then recursively call traverse on them.
typeof obj[key] === 'object' will return true even for Array, so
next else if block won't ever be hit.
As for implementation, my first step was to create a helper good function to detect invalid objects.
good = (obj) =>{
try{return !(invalidIds.includes(obj.sys.id));}
catch(err){return true;}
}
Now the main traverse -
traverse = (obj) => {
//I assumed when an object doesn't have 'sys' but have 'id', it must be sys obj.
if (obj==null) return null;
if(obj.constructor === Array) return obj.filter(good).map(traverse);
if(obj.sys==undefined) { //this checks if it's sys object.
if(obj.id!=undefined) return obj;
}
for (let key in obj) {
if (key!=0) {
if (good(obj[key])) {obj[key] = traverse(obj[key]);}
else {obj[key] = null;}
}
}
return obj;
};
In case of Array objects, as per point 2, I filtered out valid objects first, then mapped them to traverse. In case of Objects, = operator was used to catch valid sub-objects, returned by recursive call to traverse, instead of simply modifying them.
Note: I hardly know javascript, but took a go at it anyway because this recursive problem is fairly common. So watch out for JS specific issues. Specifically, as outlined in comment, I'm not content with how I checked for 'sys' objects.
Currently, I am using a recursive function within an existing angular controller to generate an array of indices based upon a tree structure, which is creating a "branch" to the specified target value. The intention of the function is to recursively check the values of a deeply nested object and loop through arrays as they may appear. The function works until it finds the target node and matches it. On the return call, which based upon what I know, should loop back up into the for loop. Instead, it exits the function and returns undefined. I did notice that the error thrown is generated by Angular, and is as follows.
angular.js:13236 ReferenceError: result is not defined
at questionsController.self.findIndex (questions-controller.js:724)
at questionsController.self.findIndex (questions-controller.js:734)
at questionsController.self.findIndex (questions-controller.js:724)
at questionsController.self.addQuestions (questions-controller.js:711)
at fn (eval at compile (angular.js:14086), <anonymous>:4:262)
at expensiveCheckFn (angular.js:15076)
at callback (angular.js:24546)
at Scope.$eval (angular.js:16820)
at Scope.$apply (angular.js:16920)
at HTMLButtonElement.<anonymous> (angular.js:24551)
The function is as follows:
self.findIndex = function (map, target, arr, index) {
if (map instanceof Array) {
for (var x = 0; x < map.length; x++) {
var newArr = arr.slice();
newArr.push(x);
result = self.findIndex(map[x], target, newArr, x);
if (result.length > 0) {
return result;
}
}
} else if (map instanceof Object) {
if (map.id == target.id) {
return arr;
}
else if (map.questions && map.questions.length > 0) {
return result = self.findIndex(map.questions, target, arr, index);
}
}
return [];
};
The data model is as follows:
[
{
id: 1,
name: 'foo',
questions: [
{
id: 2,
name: 'bar',
questions: []
}
]
},
{
id: 3,
name: 'foobar',
questions: [
{
id: 4,
name: 'barfoo',
questions: [
{
id: 5,
name: 'foobarfoo',
questions: []
},
{
id: 6,
name: 'barfoobar',
questions: []
}
]
}
]
}
]
Please let me know if any additional info regarding the question might help provide clarity.
I have a JSON object that was returned from an XML to js function. This xml converter creates arrays for every entry even when they should be strings. I cannot modify this original function so therefore I would like to take my final json object, iterate through it, detect if a value is an array of length 1 and, if so, change that array to a string.
Original object:
var json = {
user: {
name: ["bob"],
email: ["bob#example.org"]
},
items: [{
name: ["Object 1"]
},{
name: ["Object 2"]
}]
}
Should become:
var json = {
user: {
name: "bob",
email: "bob#example.org"
},
items: [{
name: "Object 1"
},{
name: "Object 2"
}]
}
I have considered the reviver function but a) I would like to avoid going back to a string and b) I am not sure if that would even work as it will probably just feed me each array element individually.
This recursive function seems to work for this problem:
function simplify(obj) {
for (var k in obj) {
if (Object.prototype.toString.call(obj[k]) == '[object Array]' && obj[k].length == 1) {
obj[k] = obj[k][0];
}
else if (typeof obj[k] == 'object') {
obj[k] = simplify(obj[k]);
}
}
return obj;
}
simplify(json);
Demo: http://jsfiddle.net/xkz4W/
Here's a recursive way to do it:
function flattenArrays(data) {
function processItem(item) {
if (Array.isArray(item)) {
if (item.length === 1 && typeof item[0] === "string") {
data[prop] = item[0];
} else {
for (var i = 0; i < item.length; i++) {
processItem(item[i]);
}
}
} else if (typeof item === "object") {
flattenArrays(item);
}
}
for (var prop in data) {
processItem(data[prop]);
}
}
Working demo: http://jsfiddle.net/jfriend00/L5WKs/
I have an array with several category objects, each of which has an items property containing an array of item objects. I want to map each item in each category to an object[] with objects that have the properties value and label. For some reason, I can't perform the concatenation.
var categories = [{
name: "category1",
items: [{
itemId: 1,
name: "Item1"
}, {
itemId: 2,
name: "Item2"
}]
}, {
name: "category2",
items: [{
itemId: 3,
name: "Item3"
}, {
itemId: 4,
name: "Item4"
}]
}];
var items = [];
for(var i = 0; i < categories.length; i++){
items.concat($.map(categories[i].items,function(elem){
return {value:elem.itemId, label:elem.name};
}));
}
console.log(items); //prints []
Expected Result
[{
label: "Item1",
value: "1"
},
{
label: "Item2",
value: "2"
},{
label: "Item3",
value: "3"
},{
label: "Item4",
value: "4"
}
I feel as if I am missing something very basic. I logged the result of the $.map function and it appears to be returning an []. Can anyone figure out the issue?
JSFiddle: http://jsfiddle.net/vymJv/
Another method using straight Javascript:
var x = categories.map(function(val) {
return val.items;
}).reduce(function(pre, cur) {
return pre.concat(cur);
}).map(function(e,i) {
return {label:e.name,value:e.itemId};
});
Output: x = [{label: "Item1", value: 1}, {label: "Item2", value: 2}, …]
The concat() method is used to join two or more arrays.
This method does not change the existing arrays, but returns a new
array, containing the values of the joined arrays.
http://jsfiddle.net/vymJv/1/
for(var i = 0; i < categories.length; i++){
items = items.concat($.map(categories[i].items, function(elem) {
return {value: elem.itemId, label: elem.name};
}));
}
updated with flatMap(not compatible with IE)
categories.flatMap((categories) => categories.items)
flatMap() method returns a new array formed by applying a given callback function to each element of the array, and then flattening the result by one level.
const items = categories
.map(category => category.items)
.reduce((prev, current) => [...prev, ...current])
.map((e, i) => ({ label: e.name, value: e.itemId }));
We could extend the array prototype by creating concatAll which will concatenate all of your arrays by iterating over each subarray, dumping each value into a new array.
Array.prototype.concatAll = function() {
var results = [];
this.forEach(function(subArray) {
subArray.forEach(function(subArrayValue) {
results.push(subArrayValue);
});
});
return results;
};
Then, we can get the desired result with the following:
let items = categories.map(function(category) {
return category.items.map(function(item) {
return {label: item.name, value: item.itemId};
});
}).concatAll();
We get the items by translating each category into an array of items. Because we have two categories, we will end up with two arrays of items. By applying concatAll on the final result, we flatten those two arrays of items and get the desired output.
This piece of code solves your task using a functional programming approach:
var items = [].concat.apply([], categories.map(cat =>
cat.items.map(elem => ({ value:elem.itemId, label:elem.name })))
)
Explanation: Function.prototype.apply() has the syntax fun.apply(thisArg, [argsArray]) and lets us provide parameters to a function in an array. Array.prototype.concat() combines an arbitrary amount of parameters into one array. If we now write Array.prototype.concat.apply([], [category1Items, ..., categoryNItems]), it actually equals [].concat(category1Items, ..., categoryNItems), which concatenates all parameters together. You can also replace Array.prototype.concat by [].concat to keep it shorter. Otherwise we just use standard mapping to get the job done.
You could also split the code apart a bit more for clarity:
function getItem(elem){
return {value:elem.itemId, label:elem.name};
}
function getCategoryItems(cat) {
return cat.items.map(getItem);
}
function flatten(arr) {
return Array.prototype.concat.apply([], arr);
}
var items = flatten(categories.map(getCategoryItems));