1

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.

Saurav Kumar
  • 563
  • 1
  • 6
  • 13
  • 1
    `result = {}` is not declared thus an implicit global. So all the recursive calls share a single `result` variable. This is very likely the problem you have, if not at least one of the problems. – VLAZ May 09 '22 at 09:52
  • @VLAZ: Can you help me to properly declare the `result`. I mean I tried it declaring with `let result = {}` but it didn't do any difference. – Saurav Kumar May 09 '22 at 09:57
  • There is likely other problems. I notice that *lots of things* are undeclared. The `i` in the loops is likely a problem but also other variables. There is almost never a reason to have variables without declaration. Unlike Python, they default to global scope. Not sure if missing variable declarations are the only issue here, though. – VLAZ May 09 '22 at 09:59
  • [Adding the declarations makes a difference](https://jsbin.com/guwidedeso/edit?js,console) but it does make circular references. I'm not sure if that's intended or not. – VLAZ May 09 '22 at 10:02
  • @VLAZ: Indeed, it though changed the output but still didn't get the desired result. My output is also the same as you mentioned in the link. B2 has become a direct child of A which is incorrect. – Saurav Kumar May 09 '22 at 10:15
  • @SauravKumar ... regarding all the so far provided answers / approaches are the any questions left? – Peter Seliger May 11 '22 at 11:19
  • @SauravKumar ... from what the OP was presenting before (and the base problem / hierarchy does not change) the expected output of what the OP refers to as _"the actual input"_ does not match the basic building pattern. `'--Partner Programs'` and `'--Activation'` have to be the children of `'-SKU Life Cycle Management (SLM)'` and not of `'-Business Planning and Commercialization - BPC'`. If the OP wants the expected output, the input array needs to be changed to the two former mentioned child items become direct followers of the latter (parent) item. – Peter Seliger May 11 '22 at 20:09
  • @SauravKumar ... _"To me, it's quite difficult to understand all the answers, so could make them work for the new DS I've mentioned."_ ... That's exactly the reason I'm very often asking an OP if ... _from all the provided answers / approaches are there questions left?_ Just say what remained unclear and ask for further help and/or better explanation/s. – Peter Seliger May 11 '22 at 22:09
  • @PeterSeliger: Frankly speaking none of the logic I understood except the one I mentioned in the question. I guess I've to enroll in some programming classes and learn the evaluation of the programming logic. :') – Saurav Kumar May 12 '22 at 14:44
  • @SauravKumar ... 1/2 ... One just needs to have a look into [destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) and [`Array.prototype.reduce`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce). All of the answers' final solutions use an accumulating/collecting object and/or array as `initialValue` (the 2nd parameter) of `reduce`. – Peter Seliger May 12 '22 at 15:41
  • @SauravKumar ... 2/2 ... The array will be used as lookup/storage for the most recent reference of each detected level. The object is the root of the to be aggregated final result. With each iteration the accumulator/collector gets passed as the callback function's 1st parameter whereas the 2nd parameter is the currently to be processed item. From the latter one retrieves the necessary information about this item's level, it's final wording/text and the properties that will be used in the aggregation of a newly to be created and assigned node/branch. – Peter Seliger May 12 '22 at 15:41
  • @SauravKumar ... And a pointed remark I couldn't hold ... why, from all the solutions the OP didn't fully comprehend, did the OP decide to upvote/accept an answer with a rather obfuscating coding style? (And this is not questioning Nina's solution which I can fluently read and which I like. It's a serious question with the intention of trying to get to the bottom of the OP's behavior / decision making process.) – Peter Seliger May 12 '22 at 15:51

3 Answers3

4

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; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • Hi, I've updated my question to the real-time scenario. Could you please help me with how I can use your logic to get the desired output? – Saurav Kumar May 11 '22 at 12:56
3

There is no recursion needed. Since the only reliable information about the to be added (next) nesting level is carried by the dashes count, one can add the next level anyway just to the object reference of the most recently processed previous/lower level.

Thus a lookup in form of an array which refers by its index to each recently created level/branch is everything needed for a simple loop based solution.

The next provided implementation uses a reduce based approach.

function aggregateObjectHierarchyLevel(
  { recentLevels, result = {} },
  item,
  idx,
) {
  // handle/synchronize the initial `result` reference.
  if (idx === 0) {
    recentLevels = [result];
  }
  const [_, dashes = '', key = ''] = item.match(/^([-]*)(.*)/) ?? [];
  const level = dashes.length;

  recentLevels[level + 1] = recentLevels[level][key] = {};

  return { recentLevels, result };
};


console.log(
  ['A', '-B1', '--B2', '-C1', 'D']
    .reduce(aggregateObjectHierarchyLevel, {})
    .result
)
console.log(
  ['A', '-B1', '--B2', '---B3', '-C1', 'D']
    .reduce(aggregateObjectHierarchyLevel, {})
    .result
)
console.log(
  ['A', '-B1', '--B2', '---B3', '--C2', '---C3', '-D1', 'E']
    .reduce(aggregateObjectHierarchyLevel, {})
    .result
)
.as-console-wrapper { min-height: 100%!important; top: 0; }

Edit ... due to the OP's real world data changes

From my above comment ...

"@SauravKumar ... from what the OP was presenting before (and the base problem / hierarchy does not change) the expected output of what the OP refers to as "the actual input" does not match the basic building pattern. '--Partner Programs' and '--Activation' have to be the children of '-SKU Life Cycle Management (SLM)' and not of '-Business Planning and Commercialization - BPC'. If the OP wants the expected output, the input array needs to be changed to the two former mentioned child items become direct followers of the latter (parent) item."

The update/refactoring from the above code to the next provided one took just 3 minutes which proves the robustness of the first approach.

The only changes within the reducer function were due to the necessary object destructuring and object re/assembling. The main approach at no point needed to be changed.

function aggregateObjectHierarchyLevel(
  { recentLevels, result = {} },
  { shortHandName, componentName: name , ...itemEntriesRest },
  idx,
) {
  // handle/synchronize the initial `result` reference.
  if (idx === 0) {
    recentLevels = [result];
  }
  const [_, dashes = '', componentName = ''] = name.match(/^([-]*)(.*)/) ?? [];
  const level = dashes.length;

  recentLevels[level][shortHandName] = {
    shortHandName,
    componentName,
    ...itemEntriesRest,
    children: recentLevels[level + 1] = {},
  };
  return { recentLevels, result };
};

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" }
];

console.log(
  data
    .reduce(aggregateObjectHierarchyLevel, {})
    .result
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
3

Another version, quite similar in design to both Peter's and Nina's, but with a different coding style, looks like this:

const nest = (strings, result = {}) => strings .reduce (
  (levels, str, _, __, [___, hyphens, val] = str .match (/(\-*)(.+)/), lvl = hyphens .length) => (
    (levels [lvl] [val] = levels [lvl + 1] = {}), 
    levels
  ), [result]
) [0]

console .log (nest (['A', '-B1', '--B2', '-C1', 'D']))

We use the same nesting structure as both those answers, and use a similar regex as Peter to separate out the two parts of the string, and the same updating logic as in both of those answers. Here we do this in a .reduce call, with only the hidden accumulator being modified.

I wrote this independently of those, came back and saw that there were already two good answers, but thought this was enough different to include as well. (I also fixed it to steal Peter's destructuring of the regex match call. Much better than my original .slice (1, 3)-then-destructure approach!)

It's interesting to see three very different coding styles all approach the problem with the same fundamental design!

Update

This version handles the updated requirements in the question. In the future, with that much change, please simply start a second question and include a link back to the original for context. (You might still want to do this, as new questions get more attention.)

const nest = (input, result = {children: {}}) => input .reduce ((
  levels, 
  {componentId, componentName, shortHandName}, 
   _, __, [___, hyphens, val] = componentName .match (/(\-*)(.+)/), lvl = hyphens .length
) => (
  (levels [lvl] .children [shortHandName] = levels [lvl + 1] = {
    componentId, componentName: val, shortHandName, children: {}
  }), 
  levels
), [result]) [0] .children

const 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"}]

console .log (nest (input))
.as-console-wrapper {max-height: 100% !important; top: 0}

Note that this does not match your target output, nesting Partner and Activation inside SLM, not in BPC. But that seems to be correct given the input data. If you wanted to switch that, then SLM would have to come later in the input array.

There's not much more to say about the design. It's the exact same design as above, except with more fields, including a nested children one.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • _“ It's interesting to see three very different coding styles all approach the problem with the same fundamental design“_ … I like this aspect (distinct implementations) of such answers (same algorithm/approach) as well. It‘s like one can watch you guys thinking leaving mostly this personal (brain) signatur. – Peter Seliger May 10 '22 at 04:34
  • Updated to handle the new requirements. – Scott Sauyet May 11 '22 at 18:06