Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Find a full object path to a given value with JavaScript

Tags:

javascript

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.

like image 412
Sergey Kritskiy Avatar asked Sep 03 '25 13:09

Sergey Kritskiy


2 Answers

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].

like image 122
ibrahim mahrir Avatar answered Sep 05 '25 05:09

ibrahim mahrir


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"]
like image 44
nikos1351 Avatar answered Sep 05 '25 05:09

nikos1351