Search code examples
javascriptarraysobjectrecursion

Depth-object search based on a path


I need help with tree walking to find matches.

  1. Search the object recursively to find a TEXT_TO_MATCH and return it's PATH
  2. With that PATH walk though every array it finds, until it finds another item
  3. Search that item to see if it has a path the SAME LENGTH as TEXT_TO_MATCH path but just in a different position in some PARENT array

EG: TEXT_TO_MATCH exists at .a[0].b.c.[0].value,

It should return all values which have the path:

.a[ANY_INDEX].b.c[ANY_INDEX].value

const pathToMatch = findText(DATA, 'A') // returns .a[0].b.c[0].value

findObjectInTree(DATA, pathToMatch) // returns all data which appear at .a[ANY_INDEX].b.c[ANY_INDEX].value

For example, my input data looks like this:

{
  a:[
    { b:{ c:[{value:'A'}]}},
    { b:{ c:[{foo:1},{value:'B'}]}},
    { b:{ c:[{value:'C', 
        b:  { c:
                [ {value:'D'} ]
            } 
        }]
    }}
  ],
  a1:[{ b:{ c:[{value:'AA'}]}}]
}

The result would be: ['A','B','C']

Why?

  • A,B,C ARE returned as they are on the same object path .a[ANY_INDEX].b.c[ANY_INDEX].value

  • D are NOT returned because it is deeper in the path at .a[ANY_INDEX].b.c[ANY_INDEX].b.c[ANY_INDEX].value

  • AA are NOT returned because it is on a different object key a1 .a1[ANY_INDEX].b.c[ANY_INDEX].value

The actual data my be larger and more nested- but a text match will always exist on the SAME path but at a different index of a ARRAY.


Solution

  • First get the path to the found string, second collect values with this path:

    const obj = {
        a: [
            { b: { c: [{ value: 'A' }] } },
            { b: { c: [{ foo: 1 }, { value: 'B' }] } },
            {
                b: {
                    c: [{
                        value: 'C',
                        b: {
                            c:
                                [{ value: 'D' }]
                        }
                    }]
                }
            }
        ],
        a1: [
            { b: { c: [{ value: 'A1' }] } },
            { b: { c: [{ foo: 1 }, { value: 'B1' }] } },
            {
                b: {
                    c: [{
                        value: 'C1',
                        b: {
                            c:
                                [{ value: 'D1' }]
                        }
                    }]
                }
            }
        ],
    };
    
    const str = 'A';
    
    // first find the value and its path from the root
    const path = findPath(obj, str);
    
    if (!path) {
        console.error(new Error(`The string "${str}" not found`));
    } else {
    
        // walk in path and collect values
    
        const collected = [];
        collect(obj, path, collected);
        console.log(collected);
    
    }
    
    function collect(obj, path, collected) {
    
        const part = path.shift();
        if (!path.length) {
            part in obj && collected.push(obj[part]);
            return;
        }
        if (part === 0) { // it's an array, iterate
            for (const item of obj) {
                collect(item, path.slice(), collected);
            }
            return;
        }
        part in obj && collect(obj[part], path, collected);
    
    }
    
    
    function findPath(obj, str, path = [], isArray) {
    
        for (const [key, val] of Object.entries(obj)) {
    
            const out = path.concat(isArray ? 0 : key);
    
            const isChildArray = Array.isArray(val);
    
            if (isChildArray || val.__proto__.constructor.name === 'Object') {
                const found = findPath(val, str, out, isChildArray);
                if (found) {
                    return found;
                }
            } else if (typeof val === 'string' && val.includes(str)) {
                return out;
            }
        }
    }