What's the pattern of the following JavaScript reduce function? - javascript

I'm having trouble understanding the following reduce function:
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, curr) => {
console.log('prev: ', prev)
console.log('curr: ', curr)
return prev || curr
})
}
Applied to this object:
const mockApps = {
name: 'orange',
children: [{
name: 'white'
}, {
name: 'green',
children: [{
name: 'yellow',
children: [{
name: 'red'
}, {
name: 'white'
}]
}, {
name: 'green',
children: [{
name: 'purple'
}]
}]
}, {
name: 'gray'
}]
}
const activeApp = {
name: 'purple',
color: 'purple',
path: 'writer'
}
findDeep(mockApps.children, activeApp)
I thought the pattern would be like the example at MDN:
[0, 1, 2, 3, 4].reduce(function(previousValue, currentValue, currentIndex, array) {
return previousValue + currentValue;
});
But to my surprise what I theorized was different from the output:
I thought the previousValue would be the returnValue of the previous iteration, but as you can see in the console, the third prev is undefined even if the currentValue of the previous iteration is not.
What's the correct pattern of this reduce function?
Here's the CodePen.

If you follow the code through, the first value passed to map (level 0) is:
{name: 'white'}
It doesn't have a name of purple, or any children so the result array is now:
[undefined]
The next item is:
{name: 'green',
children: [{
name: 'yellow',
children: [{
name: 'red'
}, {
name: 'white'
}]
}, {
name: 'green',
children: [{
name: 'purple'
}]
}]
}
It has a children property so its value is passed to a recursive call to findDeep (level 1), which is:
[{
name: 'yellow',
children: [{
name: 'red'
}, {
name: 'white'
}]
}, {
name: 'green',
children: [{
name: 'purple'
}]
}]
The first item passed to map, and again findDeep is called recursively (level 2) with:
[{name: 'red'},
{name: 'white'}]
}, {
name: 'green',
children: [{
name: 'purple'
}]
the first item has no name of purple or children, so this level map array is now:
[undefined]
Same for the next item, so now it's:
[undefined, undefined]
The next has a name 'purple', so it's added to the array:
[undefined, undefined,{name:'purple'}]
That is run through reduce, which is called with no accumulator so the first two values are passed as prev and cur. Since prev is falsey, the value of curr is returned as the accumulator so on the next call the values are undefined and {name:'purple'}, so that's returned to the level 1 map and it's array is now:
[{name:'purple'}]
There are no more members in this level, so that is passed to reduced. Since it's the only member in the array and there's no accumulator passed in, it's simply returned, so the level 1 result is:
[{name:'purple'}]
The last member at level 0 also returns undefined, so the final level 0 array is:
[{name:'purple'}, undefined]
Which is passed to reduce, with the two values being prev and curr respectively. Since prev the object isn't falsey, it's returned and the final result is:
[{name:'purple'}]
Note that if you use JSON.stringify to look at objects and arrays, undefined is changed to "null".

It seems you are assuming the console output belongs to one run of reduce, but this is not true.
The function findDeep calls itself recursively, so you'll get output from distinct calls to reduce.
I suggest you modify the code as follows to also see in the console when findDeep is being called and exited:
function findDeep(arr, obj) {
console.log('Entering findDeep');
var res = arr.map(item => {
if (item.name === obj.name) {
return arr
} else if (item.children) {
return findDeep(item.children, obj)
} else {
return undefined
}
}).reduce((prev, curr) => {
console.log('prev: ' + JSON.stringify(prev));
console.log('curr: ' + JSON.stringify(curr));
console.log('return: ' + JSON.stringify(prev || curr));
return prev || curr;
});
console.log('Exiting from findDeep');
return res;
}
This should bring light to the issue. Here is a snippet that writes the log to the browser:
console = { log: function(msg) {
document.write(msg + '<br>');
}}
function findDeep(arr, obj) {
console.log('Entering findDeep');
var res = arr.map(item => {
if (item.name === obj.name) {
return arr
} else if (item.children) {
return findDeep(item.children, obj)
} else {
return undefined
}
}).reduce((prev, curr) => {
console.log('prev: ' + JSON.stringify(prev));
console.log('curr: ' + JSON.stringify(curr));
console.log('return: ' + JSON.stringify(prev || curr));
return prev || curr;
});
console.log('Exiting from findDeep');
return res;
}
const mockApps = {
name: 'orange',
children: [{
name: 'white'
}, {
name: 'green',
children: [{
name: 'yellow',
children: [{
name: 'red'
}, {
name: 'white'
}]
}, {
name: 'green',
children: [{
name: 'purple'
}]
}]
}, {
name: 'gray'
}]
}
const activeApp = {
name: 'purple',
color: 'purple',
path: 'writer'
}
findDeep(mockApps.children, activeApp)
As you can see, where previously it seemed the value of prev did not correspond to the return value of the previous iteration, it now becomes clear that these two iterations belong to a different call of reduce, so it acts just like you expect.

Related

Create a hierarchy from an array that has hierarchy information in an attribute that is string with separator ">"

let's say I have an array of objects as below, this array has a list of objects which has a key and document count, the key has hierarchical information as a string separated by ">", the start of the attribute is parent, and then its child and so on.
[
{
key: 'sport',
doc_count: 2
},
{
key: 'sport>competition',
doc_count: 2
},
{
key: 'sport>competition>cricket',
doc_count: 1
},
{
key: 'sport>competition>football',
doc_count: 1
},
{
key: 'movies',
doc_count: 1
}
{
key: 'movies>english',
doc_count: 1
}
]
Can any one suggest me the quickest method to convert this array to this hierarchical data(See Below) in javascript.
[
{
label: 'sport',
parent: null,
doc_count: 2,
children: [
{
label: 'competition',
parent: 'sport',
doc_count: 2,
children: [
{
label: 'cricket',
parent: 'competition',
doc_count: 1,
children: []
},
{
label: 'football',
parent: 'competition',
doc_count: 1,
children: []
}
]
}
]
},
{
label: 'movies',
parent: null,
doc_count: 1,
children: [
{
label: 'english',
parent: 'movies',
doc_count: 1,
children: []
}
]
}
]'
I'm not going to do it for you (not out of malice, just because I'm not 100% sure I understand the question correctly),
but I'll hopefully give you a big point in the right direction.
The problem has something to do with recursion. If you are not aware recursion look like this:
function walk(items){
items.forEach(items => {
if(item.hasChildrenOrSomething){
// notice the function calls itself:
walk(item.children);
}
// Do something with item
});
}
walk(tree);
Here is a way to classify them in a tree structure, later on you can modify the data in it. This is just a step towards your solution.
I'm reducing the data here and calling a recursive function resolveData by passing accumulator and splitted key.
You can understand it better from the code:
var data = [{ key: 'sport', doc_count: 2 }, { key: 'sport>competition', doc_count: 2 }, { key: 'sport>competition>cricket', doc_count: 1 }, { key: 'sport>competition>football', doc_count: 1 }, { key: 'movies', doc_count: 1 }, { key: 'movies>english', doc_count: 1 }];
let resolveData = (acc, keyArray) => {
if (keyArray.length > 1) {
getIndex = acc.findIndex(val => val.key == keyArray[0]);
acc[getIndex].children = acc[getIndex].children || [];
isPresent = acc[getIndex].children.find(val => val.key == keyArray[1]);
if (!isPresent) acc[getIndex].children = [...acc[getIndex].children, { key: keyArray[1], children: [] }];
keyArray.shift();
resolveData(acc[getIndex].children, keyArray); // here we are performing recursion
}
return acc;
};
var tree = data.reduce((acc, elem) => {
keys = elem.key.split('>');
if (keys.length == 1) {
acc.push(elem); // pushing parent element here
} else {
var result = resolveData(acc, keys);
getAccIndex = acc.findIndex((value) => value.key == result[0].key);
acc[getAccIndex] = result[0];
}
return acc;
}, []);
console.log(tree);
I hope this will lead you to further direction. Thanks!
I solved it using this method,
function buildHierarchy(data) {
const categories = data.map((value) => {
const categoryKeys = value.key.split('>');
return {
label: categoryKeys[categoryKeys.length - 1],
parent: (categoryKeys.length > 1) ? categoryKeys[categoryKeys.length - 2] : null,
doc_count: value.doc_count
};
});
// Create list for top-level node(s)
const list = [];
// Cache for found parent index
const categoryMap = {};
categories.forEach((topic: Topic) => {
// No parentId means top level
if (!topic.parent) {
return list.push(topic);
}
// Insert node as child of parent in flat array
let parent = categoryMap[topic.parent];
if (typeof parent !== 'string') {
parent = categories.findIndex((el: Topic) => el.label === topic.parent);
categoryMap[topic.parent] = parent;
}
if (!categories[parent].children) {
return categories[parent].children = [topic];
}
categories[parent].children.push(topic);
});
return list;
}```

Check key in deep nested object in array

I have array of nested object. I have to check the property of key object and return its value. I have done it using the for loop and checking with children property exist or not. But I think it is not optimal way to do it. what will be most optimal way to do it. Here is the code array of object data. I have to get text for the id 121.
var abc = [
{
id: 1,
text: 'One',
children: [
{id: 11, text: 'One One'},
{id: 12, text: 'One two',
children: [ {id: 121, text: 'one two one'} ]}
]
},
{
id: 2,
text: 'two'
}
];
My approach is very specific to this problem. Here it is
for(var val of abc){
if(val.id == 121){
console.log('in first loop',val.text);
break;
}
if(Array.isArray(val.children)){
for(var childVal of val.children) {
if(childVal.id == 121){
console.log('in first child', childVal.text);
break;
}
if(Array.isArray(childVal.children)){
for(var nextChild of childVal.children){
if(nextChild.id == 121){
console.log('in next child', nextChild.text);
break;
}
}
}
}
}
}
You could create recursive function using for...in loop that returns the matched object and then you can get its text property.
var abc = [{"id":1,"text":"One","children":[{"id":11,"text":"One One"},{"id":12,"text":"One two","children":[{"id":121,"text":"one two one"}]}]},{"id":2,"text":"two"}]
function getProp(data, key, value) {
let result = null;
for (let i in data) {
if (typeof data[i] == 'object' && !result) {
result = getProp(data[i], key, value)
}
if (i == key && data[i] == value) {
result = data
}
}
return result;
}
const result = getProp(abc, 'id', 121)
console.log(result)
You could take an approach with a short circuit and return a result from a wanted property.
const
getValue = (object, key, id) => {
const search = o => {
if (!o || typeof o !== 'object') return;
if (o.id === id) return { value: o[key] };
var value;
Object.values(o).some(p => value = search(p));
return value;
};
return search(object)?.value;
};
var array = [{ id: 1, text: 'One', children: [{ id: 11, text: 'One One' }, { id: 12, text: 'One two', children: [{ id: 121, text: 'one two one' }] }] }, { id: 2, text: 'two' }];
console.log(getValue(array, 'text', 121));
console.log(getValue(array, 'text', 3000));
Given your Nodes have nested Nodes in a children property, using a recursion and Array.prototype.find() to find a Node by ID:
const getNode = (a, id, c = 'children', r) => {
const rec = a => a.find(o => o.id==id && (r=o) || c in o && rec(o[c]));
return rec(a) && r;
};
const abc = [{id: 1, text: 'One', children: [{id: 11, text: 'One one'}, {id: 12, text: 'One two', children: [{id: 121, text: 'One two one'}]}]}, {id: 2, text: 'Two' }];
console.log( getNode(abc, 121)?.text ); // One two one

Creating a reduced set and nested array within an array of objects

I'm trying to wrap my head around transforming an array of "flat" objects, into a condensed but nested version:
const startingArray = [
{ name: 'one', id: 100, thing: 1 },
{ name: 'one', id: 100, thing: 2 },
{ name: 'one', id: 100, thing: 4 },
{ name: 'two', id: 200, thing: 5 }
];
/*
desiredResult = [
{name: 'one', id:100, things: [
{thing: 1}, {thing: 2}, {thing:4}
]},
{name: 'two', id:200, things: [
{thing: 5}
]}
]
*/
// THIS DOES NOT WORK
const result = startingArray.reduce((acc, curr) => {
if (acc.name) {
acc.things.push(curr.thing)
}
return { name: curr.name, id: curr.id, things: [{thing: curr.thing}] };
}, {});
What am I not understanding?!
In your reduce callback, acc is not "each element of the array" (that's what curr is) or "the matching element in the results" (you have to determine this yourself), it's the accumulated object being transformed with each call to the function.
That is to say, when you return { name: curr.name, id: curr.id, things: [{thing: curr.thing}] };, it sets acc to that object for the next iteration, discarding whatever data was contained in it before - acc.name will only ever hold the last name iterated over and the results will never accumulate into anything meaningful.
What you want to do is accumulate the results in an array (because it's your desired output), making sure to return that array each iteration:
const startingArray = [
{ name: 'one', id: 100, thing: 1 },
{ name: 'one', id: 100, thing: 2 },
{ name: 'one', id: 100, thing: 4 },
{ name: 'two', id: 200, thing: 5 }
];
const result = startingArray.reduce((acc, curr) => {
let existing = acc.find(o => o.id == curr.id);
if(!existing) acc.push(existing = { name: curr.name, id: curr.id, things: [] });
existing.things.push({thing: curr.thing});
return acc;
}, []);
console.log(result);
Because of your desired result format this involves quite a few acc.find() calls, which is expensive - you can get around this in a concise way with this trick, using the first element of the accumulator array as a mapping of ids to references (also featuring ES6 destructuring):
const startingArray = [
{ name: 'one', id: 100, thing: 1 },
{ name: 'one', id: 100, thing: 2 },
{ name: 'one', id: 100, thing: 4 },
{ name: 'two', id: 200, thing: 5 }
];
const result = startingArray.reduce((acc, {name, id, thing}) => {
if(!acc[0][id])
acc.push(acc[0][id] = { name, id, things: [] });
acc[0][id].things.push({thing});
return acc;
}, [{}]).slice(1); //slice off the mapping as it's no longer needed
console.log(result);
Ok, providing alternative way. Key point is that you accumulate an Object and later take only the values, so get an Array:
const startingArray = [
{ name: 'one', id: 100, thing: 1 },
{ name: 'one', id: 100, thing: 2 },
{ name: 'one', id: 100, thing: 4 },
{ name: 'two', id: 200, thing: 5 }
];
const res = startingArray.reduce((acc, curr) => {
if (acc[curr.name]) {
acc[curr.name].things.push({thing: curr.thing})
} else {
acc[curr.name] = {
name: curr.name,
id: curr.id,
things: [{thing: curr.thing}]
}
}
return acc
}, {})
console.log(Object.values(res))

Move item anywhere in a nested array

I've managed to copy the intended object into the intended location (my code is below), but how do I move it? So it will not exist in the original location any more.
So in my example, I want to take the object with id of 14 (very bottom of the object) and move it into the children of the object with id of 3 (towards the top).
I know I need to modify this line: item.children.push(itemToMove) in my moveItem function, but some reason I can't think of it.
Also sorry about the very big/nested object, I wanted to make sure to cover a deeply nested object.
const myObj = [
{
id: 1,
name: '1',
children: [
{
id: 2,
name: '2',
children: [
{
id: 3,
name: '3',
children: []
}
]
},
{
id: 4,
name: '4',
children: [
{
id: 5,
name: '5',
children: [
{
id: 6,
name: '6',
children: [
{
id: 7,
name: '7',
children: []
}
]
}
]
}
]
},
]
},
{
id: 8,
name: '8',
children: [
{
id: 9,
name: '9',
children: [
{
id: 10,
name: '10',
children: []
}
]
},
{
id: 11,
name: '11',
children: [
{
id: 12,
name: '12',
children: [
{
id: 13,
name: '13',
children: [
{
id: 14,
name: '14',
children: []
}
]
}
]
}
]
},
]
}
]
let itemToMove = {
id: 14,
name: '14',
children: []
}
// move item, return updated obj
function moveItem(itemToMove, obj, parentId) {
for (let i=0;i<obj.length;i++) {
const value = obj[i];
const item = search(obj[i], parentId);
if (item) {
item.children.push(itemToMove); // pushed into children, but need to move not duplicate in
break;
}
}
function search(obj, id) {
if (obj.id === id) {
return obj;
}
for (let i=0;i<obj.children.length;i++) {
const possibleResult = search(obj.children[i], id);
if (possibleResult) {
return possibleResult;
}
}
}
return obj;
};
console.log(moveItem(itemToMove, myObj, 3))
I would probably do something like this, taking into account that if the insert fails, you should have some kind of way to re-instate the data. I also used ES6 which is different to your code, but it gives you some kind of idea.
let parent
function removeItem (obj, itemToFind) {
// Loop the object
obj.find((e, index) => {
// If the id's match remove from the parent if it exists otherwise from the object as its at root level
if (e.id === itemToFind.id) {
if (parent) {
parent.children.splice(index, 1)
} else {
obj.splice(index, 1)
}
// break the loop once returned true. Change find to forEach to remove all instances with id if allowing multiples
return true
}
// recurse
else if (e.children && e.children.length > 0) {
parent = e
return removeItem(e.children, itemToFind)
}
})
}
// move item, return updated obj
function moveItem (itemToMove, obj, parentId) {
for (let i = 0; i < obj.length; i++) {
const value = obj[i]
const item = search(obj[i], parentId)
if (item) {
item.children.push(itemToMove) // pushed into children, but need to move not duplicate in
break
}
}
function search (obj, id) {
if (obj.id === id) {
return obj
}
for (let i = 0; i < obj.children.length; i++) {
const possibleResult = search(obj.children[i], id)
if (possibleResult) {
return possibleResult
}
}
}
return obj
};
removeItem(myObj, itemToMove)
moveItem(itemToMove, myObj, 3)

Search deep in array and delete

I got the following array:
var arr = [
{
1: {
id: 1,
title: 'test'
},
children: [
{
1: {
id: 2,
title: 'test2'
}
}
]
}
];
The objects directly in the array are the groups. The 1: is the first language, 2: is second etc. The id is stored in every language object (due to the database I'm using). The children array is built the same way as the 'arr' array.
Example of multiple children:
var arr = [
{
1: {
id: 1,
title: 'test'
},
children: [
{
1: {
id: 2,
title: 'test2'
},
children: [
{
1: {
id: 3,
title: 'test3',
},
children: []
}
]
}
]
}
];
Now I need to delete items from this array. You can have unlimited children (I mean, children can have children who can have children etc.). I have a function which needs an ID parameter sent. My idea is to get the right object where the ID of language 1 is the id parameter. I got this:
function deleteFromArray(id)
{
var recursiveFunction = function (array)
{
for (var i = 0; i < array.length; i++)
{
var item = array[i];
if (item && Number(item[1].ID) === id)
{
delete item;
}
else if (item && Number(item[1].ID) !== id)
{
recursiveFunction(item.children);
}
}
};
recursiveFunction(arr);
}
However, I'm deleting the local variable item except for the item in the array. I don't know how I would fix this problem. I've been looking all over the internet but haven't found anything.
This proposal features a function for recursive call and Array.prototype.some() for the iteration and short circuit if the id is found. Then the array is with Array.prototype.splice() spliced.
var arr = [{ 1: { id: 1, title: 'test' }, children: [{ 1: { id: 2, title: 'test2' }, children: [{ 1: { id: 3, title: 'test3', }, children: [] }] }] }];
function splice(array, id) {
return array.some(function (a, i) {
if (a['1'].id === id) {
array.splice(i, 1)
return true;
}
if (Array.isArray(a.children)) {
return splice(a.children, id);
}
});
}
splice(arr, 2);
document.write('<pre>' + JSON.stringify(arr, 0, 4) + '</pre>');
var arr = [{ 1: { id: 1, title: 'test' }, children: [{ 1: { id: 2, title: 'test2' }, children: [{ 1: { id: 3, title: 'test3', }, children: [] }] }] }];
function deleteFromArray(id) {
function recursiveFunction(arr) {
for (var i = 0; i < arr.length; i++) {
var item = arr[i];
if (item && Number(item[1].id) === id) {
arr.splice(i, 1);
} else if (item && Number(item[1].id) !== id) {
item.children && recursiveFunction(item.children);
}
}
};
recursiveFunction(arr);
};
deleteFromArray(2);
document.getElementById("output").innerHTML = JSON.stringify(arr, 0, 4);
<pre id="output"></pre>
jsfiddle: https://jsfiddle.net/x7mv5h4j/2/
deleteFromArray(2) will make children empty and deleteFromArray(1) will make arr empty itself.

Categories

Resources