1

For a simple object:

const simpleObject = {
    "a": "aaa" ,
    "b": "bbb" ,
};

I know how to get the key of a value:

Object.keys(simpleObject).find(key => simpleObject[key] === "aaa");

However, if I have a complex object like this:

const complexObject = {
    "A": [  "a1", {"a2": ["a21", "a22" ]}, "a3"],   //compact representation
    "B": [                                          //expanded representation
            "b1", 
            "b2",
            {"b3": [
                    "b31", 
                    "b32"
                ]
            }
         ],
};

How to return the key of a value as deep as possible? For example:

Input Output
A A
a1 A
a2 A
a21 A.a2

I think the first step is to get the value list of all the keys:

valueList = Object.values(map1);

then using some kind of loop. But I don't know how to work it out.

In best scenario the pattern of the object can be go on, but if that's too hard then stopping the recursion at level 2 is fine.

I need a solution that works on plain JS (I'm making an automation script for Fibery). But if there is a framework I'm happy to know.

Ooker
  • 1,969
  • 4
  • 28
  • 58
  • What should be the output when you have "A" as input? – trincot Jul 05 '23 at 06:10
  • a simple "A" is fine – Ooker Jul 05 '23 at 06:12
  • 1
    But that would not be consistent, because then I would expect input "a2" to give "A.a2" and not "A". Can you clarify? – trincot Jul 05 '23 at 06:13
  • good catch. When users input "a2", then they expect it to be "A". I think let's make "A" an exception. In general I don't expect them to input "A". – Ooker Jul 05 '23 at 06:16
  • 1
    OK, well in my answer, the input "A" will output "." to indicate it was found, but there is no ancestor key to it. – trincot Jul 05 '23 at 06:17
  • What if the complex object is at its top level an array, and the value to be found is directly in the top-level array? – trincot Jul 05 '23 at 06:58
  • you mean `const complexObject = {["a", "b"]}`? Well the object is written manually so there should be no worry. What I concern about the model is whether it's optimal or not. Like, instead of a single object of arrays and I use multiple arrays. For example `const array = ["A", "B", "C"]; const A = ["a1", "a2", "a3"]; const a2 = ["a21", "a22"]`. That makes it much simpler IMO. But this question is about the first model – Ooker Jul 05 '23 at 07:17
  • So `complexObject` will never be like `["1", "2", {"A": "3"}, "4"]` (top level array)? – trincot Jul 05 '23 at 08:03
  • @trincot that's right – Ooker Jul 05 '23 at 08:15
  • The OP's schema is not consequently applied ... if **in** `A` and **out** `A` as well as **in** `a1` and **out** `A` then already an input of `a2` should output `A.a2` because both `A` and `a2` are object keys on the same outer level of each of their belonging structure. – Peter Seliger Jul 05 '23 at 13:10

3 Answers3

3

You could build the path when unwinding from recursion. The base case is when either the given object is the value itself, or the (non-array) object has the value as its key.

function findValue(obj, value) {
    if (Object(obj) !== obj) return obj === value ? "." : "";
    if (!Array.isArray(obj) && Object.hasOwn(obj, value)) return ".";
    for (const [key, child] of Object.entries(obj)) {
        const path = findValue(child, value);
        if (path) return Array.isArray(obj) ? path : (key + "." + path).replace(/\.+$/, "");
    }
}

// The example from the question:
const complexObject = {A:["a1",{a2:["a21","a22"]},"a3"],B:["b1","b2",{b3:["b31","b32"]}]};
console.log(findValue(complexObject, "a1")); // A
console.log(findValue(complexObject, "a2")); // A
console.log(findValue(complexObject, "a21")); // A.a2

Note that to be in line with your expected output, this output has no indication about the index of the array where the value was found; it just lists the keys of plain objects.

Also, if the value happens to be a key of the top-level object, then there is no path to output. In that case this solution outputs "." so to still have a clue that the value was found.

trincot
  • 317,000
  • 35
  • 244
  • 286
2

This can be solved by recursively searching for the value in an object.

Below is an example for it. (assuming non-null values)

const complexObject={A:["a1",{a2:["a21","a22"]},"a3"],B:["b1","b2",{b3:["b31","b32"]}]};

function findValue(obj, value, path = "") {

    /** Traverse through the object or array. */
    for (const key in obj) {
        const val = obj[key];
        
        /** If key itself the value, then stop it. */
        if (key === value) {
          return (path !== "") ? path + "." + key : key;
        }
        
        /** If it's a string, then it's a dead end. */
        if ((typeof val === "string")) {
            if (val === value) {
                return (path !== "") ? path + "." + key : key;
            }
        } else {

            /** If it's not, then we need to traverse again. */
            const newPath = (path !== "") ? path + "." + key : key;
            const val = findValue(obj[key], value, newPath);
            if (val !== null) {
                return val;
            }
        }
    }

    return null;
}

const query = prompt("What value you want to find?", "a21");
const answer = findValue(complexObject, query);

document.write(answer);
Shri Hari L
  • 4,551
  • 2
  • 6
  • 18
1

By keeping at hand a number of convenience functions, we can often write such code quite simply. Based on an existing deepFindPath, I can write this function as

const findValue = (o, s) => 
  deepFindPath(v => v === s)(o).filter(k => String(k) === k).join('.')

My deepFindPath accepts a predicate and returns a function which takes an object and returns an array of the nested keys from that object for the first-found path where the value matches the predicate. For your complex object and key 'a21', it would return ['A', 0, 'a2', 0]. Since in your case, we don't want the array indices, we remove all non-string keys from the result. And then we join the result on '.'. It looks like this:

function * traverse (o = {}, path = []) {
  if (Object(o) === o) {
    for (let [k, v] of Object.entries(o)) {
      const newPath = [...path, Array.isArray(o) ? Number(k): k]
      yield [newPath, v]
      yield * traverse(v, newPath)
    }
  }
}


const deepFindPath = (pred) => (o) => {
  for (let [p, v] of traverse(o)) {
    if (pred (v)) {return p} 
  }
  return [] // empty path if not found
}

const findValue = (o, s) => 
  deepFindPath(v => v === s)(o).filter(k => String(k) === k).join('.')

const complexObject = {A: ["a1", {a2: ["a21", "a22" ]}, "a3"], B: ["b1", "b2", {b3: ["b31", "b32"]}]}

const tests = ['a1', 'a22', 'b2', 'b32', 'xyz']

tests.forEach(s => console.log(`${s}: "${findValue(complexObject, s)}"`))
.as-console-wrapper {max-height: 100% !important; top: 0}

Note that deepFindPath calls traverse to list the [path, value] combinations, and stops when it finds one where the value matches the predicate. traverse is a simple generator function.

Both traverse and deepFindPath are generic, reusable utility functions. Such things make writing custom code like findValue fairly simple.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103