I am trying to return a specific node in a JSON object structure which looks like this
{
"id":"0",
"children":[
{
"id":"1",
"children":[...]
},
{
"id":"2",
"children":[...]
}
]
}
So it's a tree-like child-parent relation. Every node has a unique ID.
I'm trying to find a specific node like this
function findNode(id, currentNode) {
if (id == currentNode.id) {
return currentNode;
} else {
currentNode.children.forEach(function (currentChild) {
findNode(id, currentChild);
});
}
}
I execute the search for example by findNode("10", rootNode). But even though the search finds a match the function always returns undefined. I have a bad feeling that the recursive function doesn't stop after finding the match and continues running an finally returns undefined because in the latter recursive executions it doesn't reach a return point, but I'm not sure how to fix this.
Please help!
When searching recursively, you have to pass the result back by returning it. You're not returning the result of findNode(id, currentChild), though.
function findNode(id, currentNode) {
var i,
currentChild,
result;
if (id == currentNode.id) {
return currentNode;
} else {
// Use a for loop instead of forEach to avoid nested functions
// Otherwise "return" will not work properly
for (i = 0; i < currentNode.children.length; i += 1) {
currentChild = currentNode.children[i];
// Search in the current child
result = findNode(id, currentChild);
// Return the result if the node has been found
if (result !== false) {
return result;
}
}
// The node has not been found and we have no more options
return false;
}
}
function findNode(id, currentNode) {
if (id == currentNode.id) {
return currentNode;
} else {
var result;
currentNode.children.forEach(function(node){
if(node.id == id){
result = node;
return;
}
});
return (result ? result : "No Node Found");
}
}
console.log(findNode("10", node));
This method will return the node if it present in the node list. But this will loop through all the child of a node since we can't successfully break the forEach flow. A better implementation would look like below.
function findNode(id, currentNode) {
if (id == currentNode.id) {
return currentNode;
} else {
for(var index in currentNode.children){
var node = currentNode.children[index];
if(node.id == id)
return node;
findNode(id, node);
}
return "No Node Present";
}
}
console.log(findNode("1", node));
I use the following
var searchObject = function (object, matchCallback, currentPath, result, searched) {
currentPath = currentPath || '';
result = result || [];
searched = searched || [];
if (searched.indexOf(object) !== -1 && object === Object(object)) {
return;
}
searched.push(object);
if (matchCallback(object)) {
result.push({path: currentPath, value: object});
}
try {
if (object === Object(object)) {
for (var property in object) {
if (property.indexOf("$") !== 0) {
//if (Object.prototype.hasOwnProperty.call(object, property)) {
searchObject(object[property], matchCallback, currentPath + "." + property, result, searched);
//}
}
}
}
}
catch (e) {
console.log(object);
throw e;
}
return result;
}
Then you can write
searchObject(rootNode, function (value) { return value != null && value != undefined && value.id == '10'; });
Now this works on circular references and you can match on any field or combination of fields you like by changing the matchCallback function.
Since this old question has been brought back up, here's a different approach. We can write a fairly generic searchTree function which we then use in a findId function. searchTree does the work of traversing the object; it accepts a callback as well as the tree; the callback determines if a node matches. As well as the node, the callback is supplied two functions, next and found, which we call with no parameters to signal, respectively, that we should proceed or that we've found our match. If no match is found, we return null.
It looks like this:
const searchTree = (fn) => (obj) =>
Array.isArray(obj)
? obj.length == 0
? null
: searchTree (fn) (obj [0]) || searchTree (fn) (obj .slice (1))
: fn (
obj,
() => searchTree (fn) (obj .children || []),
() => obj
)
const findId = (target, obj) => searchTree (
(node, next, found) => node.id == target ? found () : next(),
) (tree)
const tree = {id: 1, name: 'foo', children: [
{id: 2, name: 'bar', children: []},
{id: 3, name: 'baz', children: [
{id: 17, name: 'qux', children: []},
{id: 42, name: 'corge', children: []},
{id: 99, name: 'grault', children: []}
]}
]}
console .log (findId (42, tree))
console .log (findId (57, tree))
This code is specific to the structure where subnodes are found in an array under the property children. While we can make this more generic as necessary, I find this a common structure to support.
There is a good argument that this would be better written with mutual recursion. If we wanted, we could get the same API with this version:
const searchArray = (fn) => ([x, ...xs]) =>
x === undefined
? null
: searchTree (fn) (x) || searchArray (fn) (xs)
const searchTree = (fn) => (obj) =>
fn (
obj,
() => searchArray (fn) (obj .children || []),
(x) => x
)
This works the same way. But I find the code cleaner. Either should do the job, though.
We use object-scan for our data processing needs. It's conceptually very simple, but allows for a lot of cool stuff. Here is how you could solve your question
// const objectScan = require('object-scan');
const findNode = (id, input) => objectScan(['**'], {
abort: true,
rtn: 'value',
filterFn: ({ value }) => value.id === id
})(input);
const data = { id: '0', children: [{ id: '1', children: [ { id: '3', children: [] }, { id: '4', children: [] } ] }, { id: '2', children: [ { id: '5', children: [] }, { id: '6', children: [] } ] }] };
console.log(findNode('6', data));
// => { id: '6', children: [] }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
Similar questions were answered several times, but I just want to add a universal method that includes nested arrays
const cars = [{
id: 1,
name: 'toyota',
subs: [{
id: 43,
name: 'supra'
}, {
id: 44,
name: 'prius'
}]
}, {
id: 2,
name: 'Jeep',
subs: [{
id: 30,
name: 'wranger'
}, {
id: 31,
name: 'sahara'
}]
}]
function searchObjectArray(arr, key, value) {
let result = [];
arr.forEach((obj) => {
if (obj[key] === value) {
result.push(obj);
} else if (obj.subs) {
result = result.concat(searchObjectArray(obj.subs, key, value));
}
});
console.log(result)
return result;
}
searchObjectArray(cars, 'id', '31')
searchObjectArray(cars, 'name', 'Jeep')
I hope this helps someone
I really liked a tree search! A tree is an extremely common data structure for most of today's complex structured tasks. So I just had similar task for lunch too. I even did some deep research, but havent actually found anything new! So what I've got for you today, is "How I implemented that in modern JS syntax":
// helper
find_subid = (id, childArray) => {
for( child of childArray ) {
foundChild = find_id( i, child ); // not sub_id, but do a check (root/full search)!
if( foundChild ) // 200
return foundChild;
}
return null; // 404
}
// actual search method
find_id = (id, parent) => (id == parent.id) : parent : find_subid(id, parent.childArray);
Recursive structure search, modification, keys/values adjustments/replacement.
Usage Example:
const results = []; // to store the search results
mapNodesRecursively(obj, ({ v, key, obj, isCircular }) => {
// do something cool with "v" (or key, or obj)
// return nothing (undefined) to keep the original value
// if we search:
if (key === 'name' && v === 'Roman'){
results.push(obj);
}
// more example flow:
if (isCircular) {
delete obj[key]; // optionally - we decide to remove circular links
} else if (v === 'Russia') {
return 'RU';
} else if (key.toLocaleLowerCase() === 'foo') {
return 'BAR';
} else if (key === 'bad_key') {
delete obj[key];
obj['good_key'] = v;
} else {
return v; // or undefined, same effect
}
});
Tips and hints:
You can use it as a search callback, just return nothing (won't affect anything) and pick values you need to your Array/Set/Map.
Notice that callback is being run on every leaf/value/key (not just objects).
Or you can use the callback to adjust particular values and even change keys. Also it automatically detects circular loops and provides a flag for you to decide how to handle them.
The code
(uses ES6)
Function itself + some example demo data
function mapNodesRecursively(obj, mapCallback, { wereSet } = {}) {
if (!wereSet) {
wereSet = new Set();
}
if (obj && (obj === Object(obj) || Array.isArray(obj))) {
wereSet.add(obj);
for (let key in obj) {
if (!obj.hasOwnProperty(key)){
continue;
}
let v = obj[key];
const isCircular = wereSet.has(v);
const mapped = mapCallback({ v, key, obj, isCircular });
if (typeof (mapped) !== 'undefined') {
obj[key] = mapped;
v = mapped;
}
if (!isCircular) {
mapNodesRecursively(v, mapCallback, { wereSet });
}
}
}
return obj;
}
let obj = {
team: [
{
name: 'Roman',
country: 'Russia',
bad_key: 123,
},
{
name: 'Igor',
country: 'Ukraine',
FOO: 'what?',
},
{
someBool: true,
country: 'Russia',
},
123,
[
1,
{
country: 'Russia',
just: 'a nested thing',
a: [{
bad_key: [{
country: 'Russia',
foo: false,
}],
}],
},
],
],
};
// output the initial data
document.getElementById('jsInput').innerHTML = JSON.stringify(obj, null, 2);
// adding some circular link (to fix with our callback)
obj.team[1].loop = obj;
mapNodesRecursively(obj, ({ v, key, obj, isCircular }) => {
if (isCircular) {
delete obj[key]; // optionally - we decide to remove circular links
} else if (v === 'Russia') {
return 'RU';
} else if (key.toLocaleLowerCase() === 'foo') {
return 'BAR';
} else if (key === 'bad_key') {
delete obj[key];
obj['good_key'] = v;
} else {
return v;
}
});
// output the result - processed object
document.getElementById('jsOutput').innerHTML = JSON.stringify(obj, null, 2);
.col {
display: inline-block;
width: 40%;
}
<div>
<h3>Recursive structure modification, keys/values adjustments/replacement</h3>
<ol>
<li>
Replacing "Russia" values with "RU"
</li>
<li>
Setting the value "BAR" for keys "FOO"
</li>
<li>
Changing the key "bad_key" to "good_key"
</li>
</ol>
<div class="col">
<h4>BEFORE</h4>
<pre id="jsInput"></pre>
</div>
<div class="col">
<h4>AFTER</h4>
<pre id="jsOutput"></pre>
</div>
</div>
Related
I'm trying to get a tree segment based on an id. The id may be at the root, or anywhere in the children. My goal is to get that entire family tree line, and not other non-related data.
I have the entire data tree, and an id.
The idea is to do this recursively, since the number of children is unknown.
import "./styles.css";
export default function App() {
const folderTree = [
{
id: "1-1",
children: [
{
id: "1-2",
parentId: "1-1",
children: []
}
]
},
{
id: "2-1",
children: [
{
id: "2-2",
parentId: "2-1",
children: [
{
id: "2-4",
parentId: "2-2",
children: []
}
]
},
{
id: "2-3",
parentId: "2-1",
children: []
}
]
}
];
const getRelatedTreeFolders = (folders, selectedFolderId) => {
//** goes top to bottom
const recursiveChildCheck = (folder, id) => {
// THIS trial failed
// let foundNested = false;
// if (folder.id === id) {
// return true;
// }
// function recurse(folder) {
// if (!folder.hasOwnProperty("children") || folder.children.length === 0)
// return;
// for (var i = 0; i < folder.children.length; i++) {
// if (folder.children[i].id === id) {
// foundNested = true;
// break;
// } else {
// if (folder.children[i].children.length > 0) {
// recurse(folder.children[i].children);
// if (foundNested) {
// break;
// }
// }
// }
// }
// }
// recurse(folder);
// return foundNested;
const aChildHasIt =
folder.children.length > 0 && folder.children.some((f) => f.id === id);
if (aChildHasIt) return true;
let nestedChildHasIt = false;
/** The problem seems to be here */
folder.children.forEach((childFolder) => {
// Is using a forEach loop the correct way?
// ideally it seems there is a simple way to do a recursive .some on the dhildren...
childFolder.children.length>0 && recursiveChildCheck(childFolder, id)
});
if (nestedChildHasIt) return true;
folder.children && folder.children.forEach(recursiveChildCheck);
};
const treeSegment = folders.reduce((result = [], folder) => {
if (
folder.id === selectedFolderId ||
recursiveChildCheck(folder, selectedFolderId)
) {
result.push(folder);
}
return result;
}, []);
return treeSegment;
};
const selectedFolderId = "2-1";
const selectedFolderId1 = "2-2";
const selectedFolderId2 = "2-4";
const selectedFolderId3 = "2-3";
const selectedFolderId4 = "3-1";
const selectedFolderId5 = "1-1";
const selectedFolderId6 = "1-2";
console.log("parent");
console.log(getRelatedTreeFolders(folderTree, selectedFolderId));
console.log("child");
console.log(getRelatedTreeFolders(folderTree, selectedFolderId1));
console.log("grandchild"); // this fails
console.log(getRelatedTreeFolders(folderTree, selectedFolderId2));
console.log("sibling");
console.log(getRelatedTreeFolders(folderTree, selectedFolderId3));
console.log("not found");
console.log(getRelatedTreeFolders(folderTree, selectedFolderId4));
console.log("other parent");
console.log(getRelatedTreeFolders(folderTree, selectedFolderId5));
console.log("other child");
console.log(getRelatedTreeFolders(folderTree, selectedFolderId6));
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{/* <h2>{JSON.stringify(result)}</h2> */}
</div>
);
}
Some issues:
recursiveChildCheck is supposed to return a boolean, but in some cases nothing (undefined) is returned, because a return statement is missing in the following expression:
folder.children && folder.children.forEach(recursiveChildCheck);
Moreover, in the expression above, the second operand of the && operator will never be evaluated, because folder.children is an array, and arrays are always truthy, even empty arrays. To give the second operand a chance, the first operand should be folder.children.length > 0
But even with that correction, the second operand will always evaluate to undefined, as that is what .forEach returns by design. You should have a method call there that returns a boolean, like some.
nestedChildHasIt never gets any other value after its initialisation, and so the following return true will never happen:
if (nestedChildHasIt) return true;
You may have intended to set nestedChildHasIt to true in the preceding forEach loop, but it seems like you have an alternative way here to do the same as the other forEach loop you had at the end.
I think the issue you have been struggling with is that you need to both check a boolean condition (does the subtree have the id?) and you need to filter the children to the child for which this is true, creating a new node which has this unique child.
Corrected code:
function getForestSegment(nodes, id) {
function recur(nodes) {
for (const node of nodes) {
if (node.id === id) return [node];
const children = recur(node.children);
if (children.length) return [{ ...node, children}];
}
return [];
}
return recur(nodes);
}
// Example from question:
const forest = [{id: "1-1",children: [{id: "1-2",parentId: "1-1",children: []}]},{id: "2-1",children: [{id: "2-2",parentId: "2-1",children: [{id: "2-4",parentId: "2-2",children: []}]},{id: "2-3",parentId: "2-1",children: []}]}];
for (const id of ["2-1", "2-2", "2-4", "2-3", "3-1", "1-1", "1-2"]) {
console.log(id);
console.log(getForestSegment(forest, id));
}
I want to find the key of a value in a Javascript nested object with recursion.
Here is my attempt at the function. Are there more elegant ways to implement this?
const foo = { data: { data2: { data3: 'worked' }, data21: 'rand' }, data01: 'rand01' }
function findKey(obj, target) {
let result = null;
if (_.isEmpty(obj) || !_.isObject(obj)){
return null;
}
if (!_.isArray(obj) && Object.keys(obj).length > 0) {
for(let i=0; i < Object.keys(obj).length; i++){
let key = Object.keys(obj)[i];
let val = obj[key];
if (val === target) {
return key;
}else{
result = findKey(val, target);
}
if (result) {break}
}
}
return result;
}
console.log(findKey(foo, 'worked'))
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
For instance is there a way to avoid having to check the value of result to then break?
I feel like result should be able to bubble down the call stack until it returns at the very first function call without having to break.
This was recently brought back up, and one useful technique was not mentioned, generator functions. Often they simplify recursive traversals that need to stop early. Here we break the problem into two functions. One, the generator function nestedEntries gets all the (nested) key-value pairs in the object. The other calls that and returns the first one that matches a target value supplied.
function * nestedEntries (obj) {
for (let [k, v] of Object .entries (obj)) {
yield [k, v]
if (Object (v) === v) {yield * nestedEntries (v)}
}
}
const findKey = (obj, target) => {
for (let [k, v] of nestedEntries (obj)) {
if (v === target) return k
}
return null
}
const foo = {data01: 'rand01', data: {data21: 'rand', data2: { data3: 'worked' } }}
console .log (findKey (foo, 'worked'))
After the few questions made above, it looks like the function should:
Assume the input is always an object.
Assume it might encounter arrays in its way.
Assume it must stop after meeting one value (in case multiple value exists).
The provided input code given by the OP does not handle array cases.
Below code is sampled to work with these sample cases:
Plain nested object structure.
Object with nested arrays of objects or elements.
Below function accepts a second argument which is a callback to evaluate whether the element met is actually the one we're looking for. In this way, it's easier to handle more complex checks.
The recursive approach is kept and, once the key is met, the function simply return to avoid unnecessary searchs.
const foo = { data: { data2: { data3: 'worked' }, data21: 'rand' }, data01: 'rand01' };
const fooWithArrays = {
data: {
data2: {
data3: 'not here'
},
data4: [
{ data5: 'worked' },
{ data6: 'not me' }
]
}
};
const fooWithExpression = {
data: {
data2: {
data3: { id: 15, name: 'find me!' }
},
data21: {
data25: 'not me'
}
}
};
const findKeyByValue = (obj, equalsExpression) => {
// Loop key->value pairs of the input object.
for (var [key, v] of Object.entries(obj)) {
// if the value is an array..
if (Array.isArray(v)) {
// Loop the array.
for (let i = 0; i < v.length; i++) {
// check whether the recursive call returns a result for the nested element.
let res = findKeyByValue(v[i], equalsExpression);
// if so, the key was returned. Simply return.
if (res !== null && res !== undefined) return res;
}
}
// otherwise..
else {
// if the value is not null and not undefined.
if (v !== null && v !== undefined) {
// if the value is an object (typeof(null) would give object, hence the above if).
if (typeof(v) === 'object') {
// check whether the value searched is an object and the match is met.
if (equalsExpression(v)) return key;
// if not, recursively keep searching in the object.
let res = findKeyByValue(v, equalsExpression);
// if the key is found, return it.
if (res !== null && res !== undefined) return res;
}
else {
// finally, value must be a primitive or something similar. Compare.
let res = equalsExpression(v);
// if the condition is met, return the key.
if (res) return key;
// else.. continue.
}
}
else continue;
}
}
}
console.log( findKeyByValue(foo, (found) => found === 'worked') );
console.log( findKeyByValue(fooWithArrays, (found) => found === 'worked') );
console.log( findKeyByValue(fooWithExpression, (found) => found && found.id && found.id === 15) );
You can use Object.entries to iterate all the keys.
Also worth noting, Object.entries also works with Array's, so no
special handling required.
const foo = { data: { data2: { data3: 'worked' }, data21: 'rand' }, data01: 'rand01', arr: [{arrtest: "arr"},'xyz']}
function findKey(obj, target) {
const fnd = obj => {
for (const [k, v] of Object.entries(obj)) {
if (v === target) return k;
if (typeof v === 'object') {
const f = fnd(v);
if (f) return f;
}
}
}
return fnd(obj);
}
console.log(findKey(foo, 'worked'))
console.log(findKey(foo, 'arr'))
console.log(findKey(foo, 'xyz'))
If obj is exactly a plain object with subobjects without arrays, this does the trick.
function findKey(obj, target) {
for (let key in obj) {
const val = obj[key];
if (val === target) {
return key;
}
if (typeof val === "object" && !Array.isArray(val)) {
const ret = findKey(val, target);
if (ret) return ret;
}
}
}
const foo = {
data: { data2: { data3: "worked" }, data21: "rand" },
data01: "rand01",
};
console.log(findKey(foo, "worked"));
console.log(findKey(foo, "bloop"));
You can try regex, if data is just objects without arrays:
const foo = { data: { data2: { data3: 'worked' }, data21: 'rand' }, data01: 'rand01' }
const out = JSON.stringify(foo).match(/"([^{}]+)":"worked"/)[1];
console.log(out);
For simple data processing tasks like this we use object-scan. It's very powerful once you wrap your head around it and makes things a lot cleaner. Here is how you'd solve your questions
(took the liberty to take the input data from #briosheje answer)
// const objectScan = require('object-scan');
const findKeyByValue = (data, fn) => objectScan(['**'], {
abort: true,
rtn: 'property',
filterFn: ({ value }) => fn(value) === true
})(data);
const foo = { data: { data2: { data3: 'worked' }, data21: 'rand' }, data01: 'rand01' };
const fooWithArrays = { data: { data2: { data3: 'not here' }, data4: [{ data5: 'worked' }, { data6: 'not me' }] } };
const fooWithExpression = { data: { data2: { data3: { id: 15, name: 'find me!' } }, data21: { data25: 'not me' } } };
console.log(findKeyByValue(foo, (found) => found === 'worked'));
// => data3
console.log(findKeyByValue(fooWithArrays, (found) => found === 'worked'));
// => data5
console.log(findKeyByValue(fooWithExpression, (found) => found && found.id && found.id === 15));
// => data3
console.log(findKeyByValue(fooWithExpression, (found) => false));
// => undefined
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
I figured this must be a dup, but I can't find it on SO. Given an object like this:
let obj = { keyA: { keyB: 'hi', keyC: { keyD: null } }, keyE: 'hi' }
Is there a way I can find key paths to a given value, like this:
keyPaths(obj, 'hi') // -> [ 'keyA.keyB', 'keyE' ]
keyPaths(obj) // -> [ 'keyA.keyB.keyD' ]
I tried to adapt some of the answers that find deep values knowing the key, and I was almost able to adapt this one that finds deep nulls, but I can't figure out how to get the path back, instead of just the deepest key.
I would go with a depth first search like this :
let obj = { keyA: { keyB: 'hi', keyC: { keyD: null } }, keyE: 'hi' }
function keyPaths(parent, value = null, chain) {
let allResults = [];
for (const prop in parent) {
if (parent.hasOwnProperty(prop)) {
const element = parent[prop];
const newChain = chain ? chain + '.' + prop : prop;
if (element === value) {
allResults.push(newChain);
}
else if (Object.keys(prop).length > 1) {
allResults = [...allResults, ...keyPaths(element, value, newChain)];
}
}
}
return allResults;
}
console.log(keyPaths(obj, 'hi')) // -> [ 'keyA.keyB', 'keyE' ]
console.log(keyPaths(obj)) // -> [ 'keyA.keyB.keyC' ]
Basically, I check all the properties of the given element for a matching value. If a property doesn't match the value, but has child properties, I recursively call the function, and merge the results from the call iteration and the recursive call.
You do this pretty cleanly by using reduce inside a recursive function. The function will return an array, which you can than map() to whatever string values you want.
let obj = { keyA: { keyB: 'hi', keyC: { keyD: null } }, keyE: 'hi' }
function keyPaths(obj, val, path = [] ){
if (!obj) return
return Object.entries(obj).reduce((res, [k, v]) => {
let p = [...path, k]
if (v == val) res.push(p)
else if (v && typeof v == 'object') res.push(...keyPaths(v, val, p))
return res
}, [])
}
console.log(keyPaths(obj, 'hi').map(a => a.join('.')))
console.log(keyPaths(obj).map(a => a.join('|')))
If it's ok to use Lodash+Deepdash, then:
let paths = _(obj).filterDeep((v)=>v=='hi').paths().value();
Codepen is here
I have a HUGE collection and I am looking for a property by key someplace inside the collection. What is a reliable way to get a list of references or full paths to all objects containing that key/index? I use jQuery and lodash if it helps and you can forget about infinite pointer recursion, this is a pure JSON response.
fn({ 'a': 1, 'b': 2, 'c': {'d':{'e':7}}}, "d");
// [o.c]
fn({ 'a': 1, 'b': 2, 'c': {'d':{'e':7}}}, "e");
// [o.c.d]
fn({ 'aa': 1, 'bb': 2, 'cc': {'d':{'x':9}}, dd:{'d':{'y':9}}}, 'd');
// [o.cc,o.cc.dd]
fwiw lodash has a _.find function that will find nested objects that are two nests deep, but it seems to fail after that. (e.g. http://codepen.io/anon/pen/bnqyh)
This should do it:
function fn(obj, key) {
if (_.has(obj, key)) // or just (key in obj)
return [obj];
// elegant:
return _.flatten(_.map(obj, function(v) {
return typeof v == "object" ? fn(v, key) : [];
}), true);
// or efficient:
var res = [];
_.forEach(obj, function(v) {
if (typeof v == "object" && (v = fn(v, key)).length)
res.push.apply(res, v);
});
return res;
}
a pure JavaScript solution would look like the following:
function findNested(obj, key, memo) {
var i,
proto = Object.prototype,
ts = proto.toString,
hasOwn = proto.hasOwnProperty.bind(obj);
if ('[object Array]' !== ts.call(memo)) memo = [];
for (i in obj) {
if (hasOwn(i)) {
if (i === key) {
memo.push(obj[i]);
} else if ('[object Array]' === ts.call(obj[i]) || '[object Object]' === ts.call(obj[i])) {
findNested(obj[i], key, memo);
}
}
}
return memo;
}
here's how you'd use this function:
findNested({'aa': 1, 'bb': 2, 'cc': {'d':{'x':9}}, dd:{'d':{'y':9}}}, 'd');
and the result would be:
[{x: 9}, {y: 9}]
this will deep search an array of objects (hay) for a value (needle) then return an array with the results...
search = function(hay, needle, accumulator) {
var accumulator = accumulator || [];
if (typeof hay == 'object') {
for (var i in hay) {
search(hay[i], needle, accumulator) == true ? accumulator.push(hay) : 1;
}
}
return new RegExp(needle).test(hay) || accumulator;
}
If you can write a recursive function in plain JS (or with combination of lodash) that will be the best one (by performance), but if you want skip recursion from your side and want to go for a simple readable code (which may not be best as per performance) then you can use lodash#cloneDeepWith for any purposes where you have to traverse a object recursively.
let findValuesDeepByKey = (obj, key, res = []) => (
_.cloneDeepWith(obj, (v,k) => {k==key && res.push(v)}) && res
)
So, the callback you passes as the 2nd argument of _.cloneDeepWith will recursively traverse all the key/value pairs recursively and all you have to do is the operation you want to do with each. the above code is just a example of your case. Here is a working example:
var object = {
prop1: 'ABC1',
prop2: 'ABC2',
prop3: {
prop4: 'ABC3',
prop5Arr: [{
prop5: 'XYZ'
},
{
prop5: 'ABC4'
},
{
prop6: {
prop6NestedArr: [{
prop1: 'XYZ Nested Arr'
},
{
propFurtherNested: {key100: '100 Value'}
}
]
}
}
]
}
}
let findValuesDeepByKey = (obj, key, res = []) => (
_.cloneDeepWith(obj, (v,k) => {k==key && res.push(v)}) && res
)
console.log(findValuesDeepByKey(object, 'prop1'));
console.log(findValuesDeepByKey(object, 'prop5'));
console.log(findValuesDeepByKey(object, 'key100'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>
With Deepdash you can pickDeep and then get paths from it, or indexate (build path->value object)
var obj = { 'aa': 1, 'bb': 2, 'cc': {'d':{'x':9}}, dd:{'d':{'y':9}}}
var cherry = _.pickDeep(obj,"d");
console.log(JSON.stringify(cherry));
// {"cc":{"d":{}},"dd":{"d":{}}}
var paths = _.paths(cherry);
console.log(paths);
// ["cc.d", "dd.d"]
paths = _.paths(cherry,{pathFormat:'array'});
console.log(JSON.stringify(paths));
// [["cc","d"],["dd","d"]]
var index = _.indexate(cherry);
console.log(JSON.stringify(index));
// {"cc.d":{},"dd.d":{}}
Here is a Codepen demo
Something like this would work, converting it to an object and recursing down.
function find(jsonStr, searchkey) {
var jsObj = JSON.parse(jsonStr);
var set = [];
function fn(obj, key, path) {
for (var prop in obj) {
if (prop === key) {
set.push(path + "." + prop);
}
if (obj[prop]) {
fn(obj[prop], key, path + "." + prop);
}
}
return set;
}
fn(jsObj, searchkey, "o");
}
Fiddle: jsfiddle
In case you don't see the updated answer from #eugene, this tweak allows for passing a list of Keys to search for!
// Method that will find any "message" in the Apex errors that come back after insert attempts
// Could be a validation rule, or duplicate record, or pagemessage.. who knows!
// Use in your next error toast from a wire or imperative catch path!
// message: JSON.stringify(this.findNested(error, ['message', 'stackTrace'])),
// Testing multiple keys: this.findNested({thing: 0, list: [{message: 'm'}, {stackTrace: 'st'}], message: 'm2'}, ['message', 'stackTrace'])
findNested(obj, keys, memo) {
let i,
proto = Object.prototype,
ts = proto.toString,
hasOwn = proto.hasOwnProperty.bind(obj);
if ('[object Array]' !== ts.call(memo)) memo = [];
for (i in obj) {
if (hasOwn(i)) {
if (keys.includes(i)) {
memo.push(obj[i]);
} else if ('[object Array]' === ts.call(obj[i]) || '[object Object]' === ts.call(obj[i])) {
this.findNested(obj[i], keys, memo);
}
}
}
return memo.length == 0 ? null : memo;
}
Here's how I did it:
function _find( obj, field, results )
{
var tokens = field.split( '.' );
// if this is an array, recursively call for each row in the array
if( obj instanceof Array )
{
obj.forEach( function( row )
{
_find( row, field, results );
} );
}
else
{
// if obj contains the field
if( obj[ tokens[ 0 ] ] !== undefined )
{
// if we're at the end of the dot path
if( tokens.length === 1 )
{
results.push( obj[ tokens[ 0 ] ] );
}
else
{
// keep going down the dot path
_find( obj[ tokens[ 0 ] ], field.substr( field.indexOf( '.' ) + 1 ), results );
}
}
}
}
Testing it with:
var obj = {
document: {
payload: {
items:[
{field1: 123},
{field1: 456}
]
}
}
};
var results = [];
_find(obj.document,'payload.items.field1', results);
console.log(results);
Outputs
[ 123, 456 ]
We use object-scan for data processing tasks. It's pretty awesome once you've wrapped your head around how to use it.
// const objectScan = require('object-scan');
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' } } };
const r = objectScan(['a.*.*'], { joined: true, rtn: 'entry' })(haystack);
console.log(r);
// => [ [ 'a.e.f', 'g' ], [ 'a.b.c', 'd' ] ]
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
There are plenty more examples on the website.
The shortest and simplest solution:
Array.prototype.findpath = function(item,path) {
return this.find(function(f){return item==eval('f.'+path)});
}
I'm currently learning JavaScript and my teacher asked me to do an exercise that would return an array with all the names of this object:
{
name: 'grandma',
daughter: {
name: 'mother',
daughter: {
name: 'daughter',
daughter: {
name: 'granddaughter'
}
}
}
}
my question is similar to this one but the solution does not work for me because my object does not contain any arrays. The code I have so far:
function toArray(obj) {
const result = [];
for (const prop in obj) {
const value = obj[prop];
if (typeof value === 'object') {
result.push(toArray(value));
}
else {
result.push(value);
}
}
return result;
}
function nameMatrioska(target) {
return toArray(target);
}
which prints out this : [ 'grandma', [ 'mother', [ 'daughter', [Array] ] ] ]
but what my teacher wants is: ['grandma', 'mother', 'daughter', 'granddaughter']
codepen
Obviously you push an array to an array, where all nested children appears as an array.
To solve this problem, you could iterate the array and push only single items to the result set.
A different method is, to use some built-in techniques, which works with an array, and returns a single array without a nested array.
Some methods:
Array#concat, creates a new array. It works with older Javascript versions as well.
result = result.concat(toArray(value));
Array#push with an array and Function#apply for taking an array as parameter list. It works in situ and with older versions of JS.
Array.prototype.push.apply(result, toArray(value));
[].push.apply(result, toArray(value)); // needs extra empty array
Spread syntax ... for spreading an array as parameters. ES6
result.push(...toArray(value));
Spread syntax is a powerful replacement for apply with a greater use. Please the the examples as well.
Finally an example with spread syntax.
function toArray(obj) {
const result = [];
for (const prop in obj) {
const value = obj[prop];
if (value && typeof value === 'object') { // exclude null
result.push(...toArray(value));
// ^^^ spread the array
}
else {
result.push(value);
}
}
return result;
}
function nameMatrioska(target) {
return toArray(target);
}
var object = { name: 'grandma', daughter: { name: 'mother', daughter: { name: 'daughter', daughter: { name: 'granddaughter' } } } };
console.log(nameMatrioska(object));
You need .concat instead of .push. Push adds one item to an array; concat joins two arrays together.
['grandmother'].concat(['mother', 'daughter'])
-> ['grandmother', 'mother', 'daughter']
Unlike push, which modifies the array you call it on, concat creates a new array.
var a1 = [ 'grandmother' ];
a1.push( 'mother' );
console.log( a1 );
-> ['grandmother', 'mother']
var a2 = [ 'steve' ];
var result = a2.concat(['Jesus', 'Pedro']);
console.log( a1 );
-> ['steve']
console.log( result );
-> ['steve', 'Jesus', 'Pedro']
Try this
function toArray(obj) {
var result = "";
for (const prop in obj) {
const value = obj[prop];
if (typeof value === 'object') {
result = result.concat(" " + toArray(value));
}
else {
result = result.concat(value);
}
}
return result;
}
function nameMatrioska(target) {
return toArray(target).split(" ");
}
function toArray(obj) {
var result = [];
for (var prop in obj) {
var value = obj[prop];
if (typeof value === 'object') {
result = result.concat(toArray(value))
} else {
result.push(value);
}
}
return result;
}
function nameMatrioska(target) {
return toArray(target);
}
//USER
var names = {
name: 'grandma',
daughter: {
name: 'mother',
daughter: {
name: 'daughter',
daughter: {
name: 'granddaughter'
}
}
}
};
console.log(nameMatrioska(names));
//Output: ["grandma", "mother", "daughter", "granddaughter"]
You are really close.
You have to flatten your array in your last step.
Tip: In general be careful when checking for type object because e.g. null, undefined are also objects in JavaScript world!
function isObject(value) {
if(value === undefined) return "Undefined";
if(value === null) return "Null";
const string = Object.prototype.toString.call(value);
return string.slice(8, -1);
}
function collectPropertiesRec(object, propertyName) {
const result = [ ];
for(const currentPropertyName in object) {
const value = object[currentPropertyName];
if(isObject(value) === 'Object') {
result.push(collectPropertiesRec(value, propertyName));
}
else if(currentPropertyName === propertyName) {
result.push(value);
}
}
return result;
}
function flattenDeep(arr1) {
return arr1.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), [ ]);
}
//USER
const names = {
name: 'grandma',
daughter: {
name: 'mother',
daughter: {
name: 'daughter',
daughter: {
name: 'granddaughter'
}
}
}
};
var result = collectPropertiesRec(names, "name");
alert(flattenDeep(result).join(", "));