Search code examples
javascriptrecursiontreeiterationtail-recursion

Recursive approach for building a tree like object hierarchy does not work as expected


I'm trying to write functionality something like this:

Input:

['A', '-B1', '--B2', '-C1', 'D']

Output:

{'A': {'B1': {'B2': {}}, 'C1': {}}, 'D': {}}

As you can see that B1 and C1 are children of A and B2 is a child of B1. It can go any level nested based on -

I wrote a Javascript code for the same but it seems something wrong when children are created. Here is my code:

function fetch_levels(li, chr='-') {
    var res_list = []
    for (i=0; i<li.length; i++) {
        item = li[i]
        level = item.length - item.replace(RegExp(`^${chr}+`), '').length
        key = item.replace(RegExp(`^${chr}+`), '')
        value = {}
        res_list.push({
            level: level,
            name: key,
            value: value
        })
    }
    return res_list
}

function ttree_to_json(ttree, level=0) {
    result = {}
    for (i = 0; i < ttree.length; i++) {
        cn = ttree[i]
        nn = ttree[i+1] || {'level': -1}

        // Edge cases
        if (cn['level'] > level) {
            continue
        }
        if (cn['level'] < level) {
            return result
        }

        // Recursion
        if (nn['level'] == level) {
            result[cn['name']] = cn['value']
        } else if (nn['level'] > level) {
            rr = ttree_to_json(ttree.slice(i+1), level=nn['level'])
            result[cn['name']] = rr
        }
        else {
            result[cn['name']] = cn['value']
            return result
        }
    }
    return result
}

And this is how the functions can be invoked:

console.log(ttree_to_json(fetch_levels(['A', '-B1', '--B2', '-C', 'D'])))

And the output I got is something like:

Object { B2: {…}, C: {} }

Which is wrong.

Can any help me to figure out what is wrong with the JavaScript code?

The solution is inspired by the sample code mentioned here:

Edit (May 11, 2022)

My bad I didn't give the actual problem. To make the question shorter and precise I gave a different data structure. All the answers are given here work perfectly fine with the above DS. But I cannot make my actual DS to get the desired output.

Here is the actual input:

[
    {
        "componentId": "1067256",
        "componentName": "Readiness",
        "shortHandName": "GTM"
    },
    {
        "componentId": "1067343",
        "componentName": "-Business Planning and Commercialization - BPC",
        "shortHandName": "BPC"
    },
    {
        "componentId": "1068213",
        "componentName": "-SKU Life Cycle Management (SLM)",
        "shortHandName": "SLM"
    },
    {
        "componentId": "1068210",
        "componentName": "--Partner Programs",
        "shortHandName": "Partner"
    },
    {
        "componentId": "1067317",
        "componentName": "--Activation",
        "shortHandName": "Activation"
    },
    {
        "componentId": "1067346",
        "componentName": "Sales Compensation",
        "shortHandName": "Sales Comp"
    }
]

Expected output:

{
    "GTM": {
        "componentId": "1067256",
        "componentName": "Readiness",
        "shortHandName": "GTM",
        "children": {
            "BPC": {
                "componentId": "1067343",
                "componentName": "Business Planning and Commercialization - BPC",
                "shortHandName": "BPC",
                "children": {
                    "Partner": {
                        "componentId": "1068210",
                        "componentName": "Partner Programs",
                        "shortHandName": "Partner",
                        "children": {}
                    },
                    "Activation": {
                        "componentId": "1067317",
                        "componentName": "Activation",
                        "shortHandName": "Activation",
                        "children": {}
                    }
                }
            },
            "SLM": {
                "componentId": "1068213",
                "componentName": "SKU Life Cycle Management (SLM)",
                "shortHandName": "SLM",
                "children": {}
            }
        }
    },
    "Sales Comp": {
        "componentId": "1067346",
        "componentName": "Sales Compensation",
        "shortHandName": "Sales Comp",
        "children": {}
    }
}

Explanation:

  • Using the componentName the parent and child relationship (or level) is decided based on - which can go any nested level.
  • shortHandName is used as a key and the value which is an object should contain all properties including the newly added attribute children
  • children will then be having the same structure and the lowest level child will have {} as children.

To me, it's quite difficult to understand all the answers, so could make them work for the new DS I've mentioned.


Solution

  • You could simplify the code by taking an array for the level references of nested objects.

    Any property is assigned to the given level (count of dashes) and it takes object to the next level.

    const
        data = ['A', '-B1', '--B2', '-C1', 'D'],
        result = {},
        levels = [result];
        
    data.forEach(s => {
        let level = 0;
        while (s[level] === '-') level++;
        s = s.slice(level);
        levels[level][s] = levels[level + 1] = {};
    });
    
    console.log(result);
    .as-console-wrapper { max-height: 100% !important; top: 0; }

    Real world problem's solution

    const
        data = [{ componentId: "1067256", componentName: "Readiness", shortHandName: "GTM" }, { componentId: "1067343", componentName: "-Business Planning and Commercialization - BPC", shortHandName: "BPC" }, { componentId: "1068213", componentName: "-SKU Life Cycle Management (SLM)", shortHandName: "SLM" }, { componentId: "1068210", componentName: "--Partner Programs", shortHandName: "Partner" }, { componentId: "1067317", componentName: "--Activation", shortHandName: "Activation" }, { componentId: "1067346", componentName: "Sales Compensation", shortHandName: "Sales Comp" }],
        getLevelString = (string, level = 0) => string[0] === '-'
            ? getLevelString(string.slice(1), level + 1)
            : { level, string },
        result = data
            .reduce((levels, o) => {
                const { level, string: componentName } = getLevelString(o.componentName);
                levels[level][o.shortHandName] = { ...o, componentName, children: levels[level + 1] = {} };
                return levels;
            }, [{}])
            [0];
    
    console.log(result);
    .as-console-wrapper { max-height: 100% !important; top: 0; }