4

I have an array of objects with items (only have name property) and groups (with a children property, they may contain items or other groups) and I need to get a full path to needle value, so in this case it'd be myObj[2]["children"][0]["children"][1]["children"][0], plus I'm limited to quite old JS version ECMA 262 (I'm using it inside Photoshop)

var myObj = [
{
 "name": "group1",
 "children": [
 {
 "name": "group2",
 "children": [
 {
 "name": "item0"
 }]
 }]
},
{
 "name": "item1"
},
{
 "name": "needleGroup",
 "children": [
 {
 "name": "needleNestedGroup",
 "children": [
 {
 "name": "item3"
 },
 {
 "name": "needleNestedDeeperGroup",
 "children": [
 {
 "name": "needle"
 }]
 }]
 }]
}];

My first idea was to transform object to array or arrays so it'd be easier to process, so my object became

[
 [
 [
 "item0"
 ]
 ],
 "item1", 
 [
 [
 "item3", 
 [
 "needle"
 ]
 ]
 ]
];

But.. that's it. I can't figure out hot to track down only the indexes I need. Could you please point out a correct direction.

Felix Kling
820k181 gold badges1.1k silver badges1.2k bronze badges
asked Nov 29, 2018 at 16:17
5
  • What is "JavaScript EX4"? Commented Nov 29, 2018 at 16:22
  • sorry, I should've write ECMA 262 (version/edition 3). It's a javascript engine inside Photoshop, I'll update my question Commented Nov 29, 2018 at 16:48
  • Does this version of ES have recursion and Array#join? Commented Nov 29, 2018 at 16:51
  • yes, almost everything before let/const/arrow functions Commented Nov 29, 2018 at 16:52
  • 1
    This should be a good starting point: Fastest way to flatten / un-flatten nested JSON objects Commented Nov 29, 2018 at 18:30

4 Answers 4

9

Use a recursive function to look for the item you want. Once the function find it, it will return an array. Each step back of the recursion will unshift the object key of this step:

function find(obj, item) {
 for(var key in obj) { // for each key in obj (obj is either an object or an array)
 if(obj[key] && typeof obj[key] === "object") { // if the current property (value of obj[key]) is also an object/array
 var result = find(obj[key], item); // try finding item in that object
 if(result) { // if we find it
 result.unshift(key); // we shift the current key to the path array (result will be an array of keys)
 return result; // and we return it to our caller
 }
 } else if(obj[key] === item) { // otherwise (if obj[key] is not an object or array) we check if it is the item we're looking for
 return [key]; // if it is then we return the path array (this is where the path array get constructed)
 }
 }
}

The output of this function will be an array of keys leading to item. You can easily transform it to the format in the question:

function findFormatted(obj, item) {
 var path = find(obj, item); // first let's get the path array to item (if it exists)
 if(path == null) { // if it doesn't exist
 return ""; // return something to signal its inexistance
 }
 return 'myObj["' + path.join('"]["') + '"]'; // otherwise format the path array into a string and return it
}

Example:

function find(obj, item) {
 for(var key in obj) {
 if(obj[key] && typeof obj[key] === "object") {
 var result = find(obj[key], item);
 if(result) {
 result.unshift(key);
 return result;
 }
 } else if(obj[key] === item) {
 return [key];
 }
 }
}
function findFormatted(obj, item) {
 var path = find(obj, item);
 if(path == null) {
 return "";
 }
 return 'myObj["' + path.join('"]["') + '"]';
}
var myObj = [{"name":"group1","children":[{"name":"group2","children":[{"name":"item0"}]}]},{"name":"item1"},{"name":"needleGroup","children":[{"name":"needleNestedGroup","children":[{"name":"item3"},{"name":"needleNestedDeeperGroup","children":[{"name":"needle"}]}]}]}];
console.log("find(myObj, \"needle\"): " + JSON.stringify(find(myObj, "needle")));
console.log("findFormatted(myObj, \"needle\"): " + findFormatted(myObj, "needle"));

Note: The indexes for the arrays are also formatted as strings, but that won't be a problem as someArray["2"] is equivalent to someArray[2].

answered Nov 29, 2018 at 17:17
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you very much! I need to learn more about recursive functions, I don't think I understand how exactly your example works
@SergeyKritskiy You're welcome! Recursion is bit tricky to understand but you'll get it eventually. There is plenty of resources about online. Happy reading!
1

I've created something you might use. The code below returns an Array of paths to keys, values, objects you are looking for.

See snippet and example to see what you can do.

To make it work you have to pass key and/or value you want to find in element and element which is an Array or Object.

It's written in newer JS standard but it shouldn't be a problem to compile it to older standard.

function findKeyValuePairsPath(keyToFind, valueToFind, element) {
 if ((keyToFind === undefined || keyToFind === null) &&
 (valueToFind === undefined || valueToFind === null)) {
 console.error('You have to pass key and/or value to find in element!');
 return [];
 }
 const parsedElement = JSON.parse(JSON.stringify(element));
 const paths = [];
 if (this.isObject(parsedElement) || this.isArray(parsedElement)) {
 checkObjOrArr(parsedElement, keyToFind, valueToFind, 'baseElement', paths);
 } else {
 console.error('Element must be an Object or Array type!', parsedElement);
 }
 console.warn('Main element', parsedElement);
 return paths;
}
function isObject(elem) {
 return elem && typeof elem === 'object' && elem.constructor === Object;
}
function isArray(elem) {
 return Array.isArray(elem);
}
function checkObj(obj, keyToFind, valueToFind, path, paths) {
 Object.entries(obj).forEach(([key, value]) => {
 if (!keyToFind && valueToFind === value) {
 // we are looking for value only
 paths.push(`${path}.${key}`);
 } else if (!valueToFind && keyToFind === key) {
 // we are looking for key only
 paths.push(path);
 } else if (key === keyToFind && value === valueToFind) {
 // we ale looking for key: value pair
 paths.push(path);
 }
 checkObjOrArr(value, keyToFind, valueToFind, `${path}.${key}`, paths);
 });
}
function checkArr(array, keyToFind, valueToFind, path, paths) {
 array.forEach((elem, i) => {
 if (!keyToFind && valueToFind === elem) {
 // we are looking for value only
 paths.push(`${path}[${i}]`);
 }
 checkObjOrArr(elem, keyToFind, valueToFind, `${path}[${i}]`, paths);
 });
}
function checkObjOrArr(elem, keyToFind, valueToFind, path, paths) {
 if (this.isObject(elem)) {
 checkObj(elem, keyToFind, valueToFind, path, paths);
 } else if (this.isArray(elem)) {
 checkArr(elem, keyToFind, valueToFind, path, paths);
 }
}
const example = [
 {
 exampleArr: ['lol', 'test', 'rotfl', 'yolo'],
 key: 'lol',
 },
 {
 exampleArr: [],
 key: 'lol',
 },
 {
 anotherKey: {
 nestedKey: {
 anotherArr: ['yolo'],
 },
 anotherNestedKey: 'yolo',
 },
 emptyKey: null,
 key: 'yolo',
 },
];
console.log(findKeyValuePairsPath('key', 'lol', example)); // ["baseElement[0]", "baseElement[1]"]
console.log(findKeyValuePairsPath(null, 'yolo', example)); // ["baseElement[0].exampleArr[3]", "baseElement[2].anotherKey.nestedKey.anotherArr[0]", "baseElement[2].anotherKey.anotherNestedKey", "baseElement[2].key"]
console.log(findKeyValuePairsPath('anotherNestedKey', null, example)); //["baseElement[2].anotherKey"]

answered Nov 6, 2019 at 11:56

Comments

1

I came accross this issue and took the chance to create find-object-paths, which solves this problem: Finding paths in an object by either keys, values or key/value combinations.

NPM: find-object-paths

Github: getPaths

Example object:

{
 "company": {
 "name": "ACME INC",
 "address": "1st Street, Toontown, TT",
 "founded": "December 31st 1969",
 "hasStocks": true,
 "numberOfEmployees": 2,
 "numberOfActors": 3
 },
 "employees": [
 {
 "employeeNumber": 1,
 "name": "Hugo Boss",
 "age": 65,
 "isCEO": true
 },
 {
 "employeeNumber": 2,
 "name": "Herbert Assistant",
 "age": 32,
 "isCEO": false
 }
 ],
 "actors": [
 {
 "actorId": 1,
 "name": "Bugs Bunny",
 "retired": false,
 "playedIn": [
 {
 "movie": "Who Framed Roger Rabbit",
 "started": 1988
 },
 {
 "movie": "Space Jam",
 "started": 1996
 },
 {
 "movie": "Looney Tunes: Back in Action",
 "started": 2003
 }
 ]
 },
 {
 "actorId": 2,
 "name": "Pepé le Pew",
 "retired": true,
 "playedIn": [
 {
 "movie": "For Scent-imental Reasons",
 "started": 1949
 }
 ]
 },
 {
 "actorId": 3,
 "name": "Marvin the Martian",
 "retired": true,
 "playedIn": [
 {
 "movie": "Marvin the Martian in the Third Dimension",
 "started": 1996
 },
 {
 "movie": "Duck Dodgers and the Return of the 241⁄2th Century",
 "started": 1980
 },
 {
 "movie": "Hare-Way to the Stars",
 "started": 1958
 }
 ]
 },
 {
 "actorId": 4,
 "name": "Yosemite Sam",
 "retired": false,
 "playedIn": [
 {
 "movie": "Who Framed Roger Rabbit",
 "started": 1988
 },
 {
 "movie": "Space Jam",
 "started": 1996
 },
 {
 "movie": "Looney Tunes: Back in Action",
 "started": 2003
 }
 ]
 }
 ],
 "distributionChannels": [
 "Celluloyd",
 [
 "VHS",
 "Betamax",
 "DVD",
 "Blueray"
 ],
 [
 "channel",
 12,
 true
 ]
 ]
 }

So, the basic usage could be:

import { findObjectPaths } from 'findObjectPaths';
class TestMe {
 static async main() {
 let acmeInc = {};
 rawFileContent = fs.readFileSync(p.resolve(__dirname, 'acmeInc.json'), 'utf-8');
 acmeInc = JSON.parse(rawFileContent);
 let path = findObjectPaths(acmeInc, {key: 'founded'});
 // company.founded
 path = findObjectPaths(acmeInc, {value: 'December 31st 1969'});
 // company.founded
 const allPaths: string[] = findObjectPaths(acmeInc, {key: 'actorId'}) as string[];
 /* [ 'actors[0].actorId',
 'actors[1].actorId',
 'actors[2].actorId',
 'actors[3].actorId' ]
 */
 const ceoPath = findObjectPaths(acmeInc, {key: 'isCEO', value: true});
 // employees[0].isCEO
 }
}
TestMe.main();

See the full documentation here: https://github.com/maugenst/getPaths#readme

BR

Dharman
34k27 gold badges105 silver badges156 bronze badges
answered Jan 21, 2022 at 13:38

Comments

0

Assuming that you have a nested and repetitive pattern of objects in your data-set, the following solution would do the trick for you.

const nodePath = { value: [] };
function findTreeNodeAndPath(
 targetNodeKey,
 targetNodeValue,
 nodeChildrenKey,
 tree
) {
 if (tree[targetNodeKey] === targetNodeValue) {
 nodePath.value.push(tree);
 return;
 }
 if (tree[nodeChildrenKey].length > 0) {
 tree[nodeChildrenKey].forEach(children => {
 if (nodePath.value.length < 1) {
 findTreeNodeAndPath(
 targetNodeKey,
 targetNodeValue,
 nodeChildrenKey,
 children
 );
 }
 });
 } else if (tree[nodeChildrenKey].length === 0) {
 return;
 }
 if (nodePath.value.length > 0) {
 nodePath.value.push(tree);
 }
}
const exampleData = {
 name: "Root",
 children: [
 {
 name: "A2",
 children: [
 {
 name: "AC1",
 children: [
 {
 name: "ACE1",
 children: []
 }
 ]
 },
 {
 name: "AC2",
 children: [
 {
 name: "ACF1",
 children: []
 },
 {
 name: "ACF2",
 children: [
 {
 name: "ACFG1",
 children: []
 }
 ]
 },
 {
 name: "ACF3",
 children: [
 {
 name: "ACFH1",
 children: []
 }
 ]
 }
 ]
 }
 ]
 }
 ]
};
findTreeNodeAndPath("name", "ACFG1", "children", exampleData);
console.log(nodePath.value)

The recursive part of the code will iterate on the children of the current node. The existing strategy here is depending on the nodePath.value having at least one element, which indicates that it found the targetted node. Later on, it'll skip the remaining nodes and would break out of recursion.

The nodePath.value variable will give you the node-to-root path.

answered Aug 1, 2019 at 20:03

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.