Find any value in any level of an array of nested objects? - javascript

I've got an array of nested objects
var arr = [{
tires: 2,
exterior: {
color: 'white',
length: 2,
width: 1
}
},{
tires: 4,
exterior: {
color: 'blue',
length: 5,
width: 3
}
},{
tires: 4,
exterior: {
color: 'white',
length: 2,
width: 3
}
}];
I want to create a function such that:
var findItems = function(arr, value){
// return array of found items
};
Some examples:
findItems(arr, 'white'); // [ arr[0], arr[2] ]
findItems(arr, 2); // [ arr[0], arr[2] ]
findItems(arr, {tires: 2}); // [ arr[0] ]
findItems(arr, {tires: 2, color: 'white'}); // [ ]
findItems(arr, {width: 1, color: 'white'}); // [ arr[0] ]
It's easy enough to find values for arrays with non-nested objects or if you know the exact level that you want to search in. But I'm not sure how to go about just finding "any value, anywhere" in an array. I quickly get into looping hell.
I can use Underscore if that helps.

I think it would be something like this:
function isObject(val) {
return Object(val) === val;
}
function search(val, ctx) {
var valIsObject = isObject(val);
return (function search(context) {
if(!isObject(context)) return val === context;
if(valIsObject && Object.keys(val).every(function(key) {
return context[key] === val[key];
})) return true;
return Object.keys(context).some(function(key) {
return search(context[key]);
});
})(ctx);
}
function findItems(arr, value){
return arr.filter(function(item) {
return search(value, item);
});
}
It should work reasonably well, except in some edge cases like circular references (infinite recursion) or descriptor properties (the code may call the getter on an incompatible object).

Related

How to work with identical, deeply nested data?

For example, has the following data:
let example = {
content: [
...
{ // index = 3
id: "b3bbb2a0-3345-47a6-b4f9-51f22b875f22",
data: {
value: "hello",
content: [
...
{ // index = 0
id: "65b1e224-4eae-4a6d-8d00-c1caa9c7ed2a",
data: {
value: "world",
content: [
...
{ // index = 1
id: "263a4961-efa7-4639-8a57-b20b23a7cc9d",
data: {
value: "test",
content: [
// Nesting unknown.
]
}
}
]
}
}
]
}
}
]
}
And for example an array with indexes leading to the required element(but can be any other):
const ids = [3, 0, 1]
How can you work with an element having this data?
For example, need to change "value" in the element at the specified path in "ids".
You could take an array of indices and get the item of the property content by calling the function again for each missing index.
const
getElement = ({ content }, [index, ...indices]) => indices.length
? getElement(content[index], indices)
: content[index];
If needed, you could add a guard for a missing index and exit early.
You can just recursively loop over your element and change the value, if you got to the last element.
I have written a small example, all you would have to do, is to extend the method for your own data structure (el.data.content):
const el = [[1,2], [3,4], [5,6]];
const changeEl = (el, indexArr, val) => {
if(indexArr.length == 1) {
el[indexArr[0]] = val;
} else {
changeEl(el[indexArr.shift()], indexArr, val);
}
}
changeEl(el, [1, 0], 99);
console.log(el);

Javascript: Split array of objects into seperate arrays with dynamic names depending on property value

I have an array containing objects. Now I want to slice the array to new arrays containing only those objects matching a certain property value.
Ideally the new array names should be created dynamically.
The original array looks like this:
specificSlotButtonArray = [
{slotStarttime:"06:00:00", slotTimespan:1},
{slotStarttime:"09:00:00", slotTimespan:1},
{slotStarttime:"12:00:00", slotTimespan:2},
{slotStarttime:"15:00:00", slotTimespan:2},
{slotStarttime:"18:00:00", slotTimespan:3}
];
The new arrays should look like this:
timespan1 = [
{slotStarttime:"06:00:00", slotTimespan:1},
{slotStarttime:"09:00:00", slotTimespan:1}
]
timespan2 = [
{slotStarttime:"12:00:00", slotTimespan:2},
{slotStarttime:"15:00:00", slotTimespan:2}
]
timespan3 = [
{slotStarttime:"18:00:00", slotTimespan:3}
]
If possible, I want to avoid javascript syntax / functions, which are not supported by IE and some other older browsers.
I already tried to work with reduce() and slice(), but did not find a solution.
You can simply achieve your desired outcome using reduce, as you can produce an object using reduce, here's an example of how you could do it.
As you can see, it'll check that the relevant property on the object isn't null, if it is, then it's set to an empty array, after this check, it's safe to simply push the relevant values onto the array, like so.
var array = [{
slotStarttime: "06:00:00",
slotTimespan: 1
},
{
slotStarttime: "09:00:00",
slotTimespan: 1
},
{
slotStarttime: "12:00:00",
slotTimespan: 2
},
{
slotStarttime: "15:00:00",
slotTimespan: 2
},
{
slotStarttime: "18:00:00",
slotTimespan: 3
}
];
var newObject = array.reduce(function(obj, value) {
var key = `timespan${value.slotTimespan}`;
if (obj[key] == null) obj[key] = [];
obj[key].push(value);
return obj;
}, {});
console.log(newObject);
Use a generic group by key reducer. I will take it from a previous answer of mine. It is an elegant and simple way to generate a function that group your data by a particular key that comes as an argument.
const groupBy = key => (result,current) => {
const item = Object.assign({},current);
if (typeof result[current[key]] == 'undefined'){
result[current[key]] = [item];
}else{
result[current[key]].push(item);
}
return result;
};
const specificSlotButtonArray = [
{slotStarttime:"06:00:00", slotTimespan:1},
{slotStarttime:"09:00:00", slotTimespan:1},
{slotStarttime:"12:00:00", slotTimespan:2},
{slotStarttime:"15:00:00", slotTimespan:2},
{slotStarttime:"18:00:00", slotTimespan:3}
];
const timespan = specificSlotButtonArray.reduce(groupBy('slotTimespan'),{});
console.log(timespan);
You could reduce the array by taking an object for the part arrays.
var specificSlotButtonArray = [{ slotStarttime: "06:00:00", slotTimespan: 1 }, { slotStarttime: "09:00:00", slotTimespan: 1 }, { slotStarttime: "12:00:00", slotTimespan: 2 }, { slotStarttime: "15:00:00", slotTimespan: 2 }, { slotStarttime: "18:00:00", slotTimespan: 3 }],
timespan1 = [],
timespan2 = [],
timespan3 = [];
specificSlotButtonArray.reduce(function (r, o) {
r[o.slotTimespan].push(o);
return r;
}, { 1: timespan1, 2: timespan2, 3: timespan3 });
console.log(timespan1);
console.log(timespan2);
console.log(timespan3);
.as-console-wrapper { max-height: 100% !important; top: 0; }
The following soluce iterates once on specificSlotButtonArray using Array.reduce. This soluce will adapt to any number of slotTimespan.
const specificSlotButtonArray = [{
slotStarttime: '06:00:00',
slotTimespan: 1,
},
{
slotStarttime: '09:00:00',
slotTimespan: 1,
},
{
slotStarttime: '12:00:00',
slotTimespan: 2,
},
{
slotStarttime: '15:00:00',
slotTimespan: 2,
},
{
slotStarttime: '18:00:00',
slotTimespan: 3,
},
];
// Loop through the array
const splitted = specificSlotButtonArray.reduce((tmp, x) => {
// Look if we got already an array having the slotTimespan of the current
// item to treat
const match = tmp.find(y => y.some(z => z.slotTimespan === x.slotTimespan));
// If we have such array, push the data into it
if (match) {
match.push(x);
} else {
// If we don't create a new array
tmp.push([x]);
}
return tmp;
}, []);
console.log(splitted);
If you want to deals with the array straight after the Array.reduce you can use destructuration :
const [
timespan1,
timespan2,
timespan3
] = specificSlotButtonArray.reduce((tmp, x) => {
You can use this function to create separate arrays grouped by slotTimespan,
specificSlotButtonArray = [
{slotStarttime:"06:00:00", slotTimespan:1},
{slotStarttime:"09:00:00", slotTimespan:1},
{slotStarttime:"12:00:00", slotTimespan:2},
{slotStarttime:"15:00:00", slotTimespan:2},
{slotStarttime:"18:00:00", slotTimespan:3}
];
function groupBy(arr, property) {
return arr.reduce(function(memo, x) {
if (!memo[x[property]]) { memo[x[property]] = []; }
memo[x[property]].push(x);
return memo;
}, {});
}
console.log(groupBy(specificSlotButtonArray, "slotTimespan"));

Looping through (possibly infinitely) repeating object structure

I have a problem I can't get my head around. If I am looking for an object with a certain ID in a possibly infinite data structure, how can I loop through it until I find the object I need and return that object?
If this is what my data looks like, how can I get the object with id === 3 ?
{
id: 0,
categories: [
{
id: 1,
categories: [
{
id: 2,
categories: [ ... ]
},
{
id: 3,
categories: [ ... ]
},
{
id: 4,
categories: [ ... ]
},
]
}
]
}
I tried the following:
findCategory = (categoryID, notesCategory) => {
if (notesCategory.id === categoryID) {
return notesCategory;
}
for (let i = 0; i < notesCategory.categories.length; i += 1) {
return findCategory(categoryID, notesCategory.categories[i]);
}
return null;
};
But that doesn't get ever get to id === 3. It checks the object with id: 2 and then returns null. It never gets to the object with id: 3.
Here is a JSbin: https://jsbin.com/roloqedeya/1/edit?js,console
Here is the case. when you go in to the first iteration of 'for' loop, because of the return call, the execution is go out from the function. you can check it by using an console.log to print the current object in the begin of your function.
try this
function find(obj, id) {
if(obj.id === id) {
console.log(obj) // just for testing. you can remove this line
return obj
} else {
for(var i = 0; i < obj.categories.length; i++) {
var res = find(obj.categories[i], id);
if(res) return res;
}
}
}
hope this will help you. thanks
You need to store the intermediate result and return only of the object is found.
function findCategory(object, id) {
var temp;
if (object.id === id) {
return object;
}
object.categories.some(o => temp = findCategory(o, id));
return temp;
}
var data = { id: 0, categories: [{ id: 1, categories: [{ id: 2, categories: [] }, { id: 3, categories: [] }, { id: 4, categories: [] }] }] }
result = findCategory(data, 3);
console.log(result);

How to turn the following recursive function into a pure function?

The following function appends an object into a nested array (by searching for it recursively):
function appendDeep (arr, obj, newObj) {
if (arr.indexOf(obj) !== -1) {
arr.splice(arr.indexOf(obj) + 1, 0, newObj)
} else {
arr.map(item => {
if (item.children) spliceDeep(item.children, obj)
})
}
}
Example:
const colors = {
children: [
{
name: 'white',
},
{
name: 'yellow',
children: [
{
name: 'black'
}
]
}
]
}
const color = {
name: 'black'
}
const newColor = {
name: 'brown'
}
appendDeep(colors.children, color, newColor)
Result:
children: [
[
{
name: 'white',
},
{
name: 'yellow',
children: [
{
name: 'black'
},
{
name: 'brown'
}
]
}
]
]
As you can see appendDeep returns a side-effect; it modifies arr. So I decided to return the array instead (so the function would become pure):
function findDeep (arr, obj) {
if (arr.indexOf(obj) !== -1) {
console.log(arr)
return arr
} else {
arr.map(item => {
if (item.children) findDeep(item.children, obj)
})
}
}
And use the new function like this:
const newArr = findDeep(colors.children, color)
newArr.splice(newArr.indexOf(color) + 1, 0, newColor)
But I get this error:
bundle.js:19893 Uncaught TypeError: Cannot read property 'splice' of undefined
What I'm a doing wrong?
(Note: Here's the CodePen.)
(Note 2: console.log(arr) does return the nested children. But for some reason they become undefined outside of the function.)
You are not returning you recursive findDeep method within the map. Return that for the recursion to work because your conditional branch is not returning anything from within map. As a result you are getting the result as undefined. JSBin
First, a find method that will return the array in which the requested item is located (as a direct child).
function findDeep(arr, obj) {
return arr.map((item) => {
if (item.name === obj.name) {
return arr;
} else if (item.children) {
return findDeep(item.children, obj);
} else {
return undefined;
}
}).reduce((prev, cur) => {
return prev ? prev : cur;
});
}
You could use that to append items to the list, but that will still modify the original array:
function appendDeep(arr, color, newColor) {
let found = findDeep(arr, color);
if (found) {
found.splice(found.indexOf(color) + 1, 0, newColor);
}
return arr;
}
If you don't want to modify the original array, things get more complex. That's because the standard array functions such as push and splice will modify the original array. There's no quick fix, at least not that I know of, because preferably you would not want to clone any more items than you really have to.
You don't need to clone black, but you do need to clone the array that contains it (It can simply reuse the existing object for black.) That means the object for yellow also needs to be cloned (to use the cloned array) and the array in which yellow is located needs to be cloned. But white, which is in the same array, is not modified and does not need to be cloned. I've not figured out how to do that properly.
This is a proposal which uses thisArgs of Array#some.
function appendDeep(object, search, insert) {
function iter(a) {
if (a.name === search.name) {
this.children.push(insert);
return true;
}
return Array.isArray(a.children) && a.children.some(iter, a);
}
object.children.some(iter, object);
}
var colors = { children: [{ name: 'white', }, { name: 'yellow', children: [{ name: 'black' }] }] },
color = { name: 'black' },
newColor = { name: 'brown' };
appendDeep(colors, color, newColor);
document.write('<pre>' + JSON.stringify(colors, 0, 4) + '</pre>');

sort array of objects by object field

I build an array like this
result.push({ id: id, reference: sometext });
Now I want to sort this array by reference, which has some text.
I tried this:
result.sort(function(a,b) {
return result[a]-result[b];
});
This
[ { id: 1, reference: 'banana' },
{ id: 2, reference: 'apple' } ]
should get
[ { id: 2, reference: 'apple' },
{ id: 1, reference: 'banana' } ]
Try this.
result.sort(function(a,b) {
// Compare reference
if(a.reference < b.reference) {
// a's reference is lesser than the one in b
return -1;
} else if (a.reference == b.reference) {
// Both reference params are equal
return 0;
} else {
// a's reference is greater than that of b
return 1
}
});
This will return a sorted version of the results array.
Do it like this instead:
result.sort(function(a,b) {
return a.reference < b.reference ? -1 : 1;
});

Categories