1

I could really use some help Debugging a problem. Im trying to build a nested object (tree) from a very unusual array im getting from a backend (dont have access to the BE so i have to work with what i got). I’ve gotten 95 percent of it done but im running into a bug

I know there are many of these flat arrays to nested objects in StackOverFlow, but this array i'm given to work with is way different. It does not have ids or typical properties of a flatten array. The good thing is that I have most of it done.

Here is a basic example of The problem I'm having:

Data:



    let stackTest = {
        predicates: [
            {
                primaryTerm: "test1",
                level: 1,
                condition: 0,
            },
            {
                primaryTerm: "and",
                level: 1,
                condition: 2,
            },
            {
                primaryTerm: "test2",
                level: 1,
                condition: 0,
            },
            {
                primaryTerm: "and",
                level: 1,
                condition: 2,
            },
            {
                primaryTerm: "test3",
                level: 2,
                condition: 0,
            },
            {
                primaryTerm: "or",
                level: 2,
                condition: 1,
            },
            {
                primaryTerm: "test4",
                level: 2,
                condition: 0,
            },
            {
                primaryTerm: "or",
                level: 2,
                condition: 1,
            },
            {
                primaryTerm: "test5",
                level: 3,
                condition: 0,
            },
            {
                primaryTerm: "and",
                level: 3,
                condition: 2,
            },
            {
                primaryTerm: "test5",
                level: 3,
                condition: 0,
            },
        
        ]


Desired Function output:



    [
        {
            id: 1,
            children: [
                {
                    id: 2,
                    children: [],
                    parentId: 1,
                    level: 1,
                    primaryTerm: 'test1',
                    condition: 0
                },
                {
                    id: 3,
                    children: [],
                    parentId: 1,
                    primaryTerm: 'test2',
                    level: 1,
                    condition: 0
                },
                {
                    id: 4,
                    children: [
                        {
                            primaryTerm: 'test3',
                            level: 2,
                            condition: 0,
                            id: 5,
                            children: [],
                            parentId: 4
                        },
                        {
                            id: 6,
                            children: [],
                            parentId: 4,
                            primaryTerm: 'test4',
                            level: 2,
                            condition: 0
                        },
    
                        {
                            id: 8,
                            children: [
                                {
                                    primaryTerm: 'test5',
                                    level: 3,
                                    condition: 0,
                                    id: 9,
                                    children: [],
                                    parentId: 8
                                },
                                {
                                    id: 10,
                                    children: [],
                                    parentId: 8,
                                    primaryTerm: 'test5',
                                    level: 3,
                                    condition: 0
                                }
                            ],
                            parentId: 4,
                            level: 2,
                            primaryTerm: 'and',
                            condition: 2
                        }
    
                    ],
                    parentId: 0,
                    level: 1,
                    primaryTerm: 'or',
                    condition: 1
                }
            ],
            parentId: 0,
            level: 0,
            primaryTerm: 'and',
            condition: 2
        }
    ]


 

Desired Tree Representation:



                                           AND

                                    /       |       \

                               Test1      Test2        OR

                                                     /    |    \       

                                                   Test3  Test4   AND
                   
                                                                   |   \

                                                                  Test5   Test6

                                                                  Actual Function output:



    [
      {
        id: 1,
        children: [
          {
            id: 2,
            children: [],
            parentId: 1,
            level: 1,
            primaryTerm: 'test1',
            condition: 0
          },
          {
            id: 3,
            children: [],
            parentId: 1,
            primaryTerm: 'test2',
            level: 1,
            condition: 0
          },
          {
            id: 4,
            children: [
              {
                primaryTerm: 'test3',        
                level: 2,
                condition: 0,
                id: 5,
                children: [],
                parentId: 4
              },
              {
                id: 6,
                children: [],
                parentId: 4,
                primaryTerm: 'test4',
                level: 2,
                condition: 0
              },
              {
                id: 7,
                children: [
                  {
                    id: 8,
                    children: [
                      {
                        primaryTerm: 'test5',
                        level: 3,
                        condition: 0,
                        id: 9,
                        children: [],
                        parentId: 8
                      },
                      {
                        id: 10,
                        children: [],
                        parentId: 8,
                        primaryTerm: 'test5',
                        level: 3,
                        condition: 0
                      }
                    ],
                    parentId: 7,
                    level: 2,
                    primaryTerm: 'and',
                    condition: 2
                  }
                ],
                parentId: 1,
                level: 1
              }
            ],
            parentId: 0,
            level: 1,
            primaryTerm: 'or',
            condition: 1
          }
        ],
        parentId: 0,
        level: 0,
        primaryTerm: 'and',
        condition: 2
      }
    ]


Actual Tree Representation:


                                           AND

                                  /          |     \

                         Test1          Test2          OR

                                                  /     |    \       

                                              Test3   Test4    {}  --here is the issue

                                                                 \
                                                                                                 
                                                                    AND
                       
                                                                     |     \
                    
                                                                   Test5    Test6



The Problem: An extra empty object is being created, this object shouldn't be there at all. not sure why this extra node is being created. I suspect it may have to do with the buildEmptyFlatArr, it may not be properly appending the parentId since that too is messed up. This issue only happens when a nested branch is to the right of the Tree, if the nested branch was to the left of the tree it wouldn't happen.

What i have done so far:

rule: objects that have 'and' / 'or' are considered Logical operators only logical (AND/OR) objects can have children steps

  • 1: Initialize the nested obj(tree) to the level of the first predicate:

The functions buildEmptyFlatArr and listToTree handle this

  • 2: when we get to the (And/OR) object, give the properties of a logical container to the new nodes where the level of that empty node matches (logicalObject level -1 )

The updatedTreeNode function handles this

  • 3: if the following item in the array has the same level as the last logical object, then append the following object to the logical operator from step #2. if the levels differ, append an empty node until you reach the level of that currently looped object.

The updatedTreeNode function handles this

The Code:




    let id = 1;
    let lastLogical = {};
    let lastItemAdded = {};
    let initTree;
    
    
    const buildEmptyFlatArr = (num, branch = false, branchLastValue) => {
        let initArr = [];
    
        for (let i = 0; i  {
        let map = {},
            node,
            roots = [],
            i;
    
        for (i = 0; i  {
        tree.forEach((item, index) => {
            if (logical) {
    
                if (item.level === target.level - 1 
                     ) {
                    let objToMod = item;
    
                    if (item.condition === undefined) {
                        for (const [key, value] of Object.entries(target)) {
                            if (key === "level") {
                                objToMod["level"] = target.level - 1;
                            } else {
                                objToMod[key] = value;
                            }
                        }
                    }
    
                    lastLogical = item;
                    lastItemAdded = item;
    
                } else if (item.children.length > 0 ) {
                    lastLogical = item;
                    updateTreeNode(item.children, target, first, logical);
                }
            } else if (first) {
                if (item.level === target.level) {
                    let objToMod = item;
                    for (const [key, value] of Object.entries(target)) {
                        objToMod[key] = value;
                    }
                    lastItemAdded = item;
                    return;
                } else if (item.children !== null && item.children.length > 0) {
                    updateTreeNode(item.children, target, first, logical);
                }
            } else if (!first && !logical) {
                if (Math.abs(lastLogical.level - target.level) > 1) {
                    let buildArrayLength =  target.level - 1;
                    let arrToAppend = buildEmptyFlatArr(buildArrayLength, true, target);
                    let newBranchToAppend = listToTree(arrToAppend);
    
                    lastLogical.children = [...lastLogical.children, { ...newBranchToAppend[0] }];
                    lastItemAdded = item;
                    lastLogical = item;
    
                } else if (item.level === target.level - 1 && lastLogical === item) {
                    let objToMod = item;
                    objToMod.children = [
                        ...objToMod.children,
                        { id: id, children: [], parentId: item.id, ...target },
                    ];
                    id += 1;
                    lastItemAdded = item;
                    lastLogical = item
                } else {
                    updateTreeNode(item.children, target);
                }
            }
        });
    };
    
    const buildTree = (templateData) => {
        templateData.forEach((item, index) => {
            if (index === 0) {
                let list = buildEmptyFlatArr(item.level);
                initTree = listToTree(list);
                updateTreeNode(initTree, item, true, false);
            } else if (item.condition === 1 || item.condition === 2) {
                updateTreeNode(initTree, item, false, true);
            } else {
                updateTreeNode(initTree, item);
            }
        });
    };
    
    
    
    buildTree(templateObj8.predicates);
        console.dir(initTree, { depth: null });




EDIT: I did not do a good job asking this question (first question i think) additional example of how the tree is suppose to look like. The initial example i've given is a tree weighted to the right. but the tree should be able to handle a more complex tree like this

second set of Data



    let stackTest = {
        predicates: [
            {
                primaryTerm: "test1",
                level: 3,
                condition: 0,
            },
            {
                primaryTerm: "and",
                level: 3,
                condition: 2,
            },
            {                          
                primaryTerm: "test2",
                level: 3,
                condition: 0,
            },
            {
                primaryTerm: "and",
                level: 1,
                condition: 2,
            },
            {
                primaryTerm: "test3",
                level: 2,
                condition: 0,
            },
            {
                primaryTerm: "or",
                level: 2,
                condition: 1,
            },
            {
                primaryTerm: "test4",
                level: 2,
                condition: 0,
            },
            {
                primaryTerm: "or",
                level: 2,
                condition: 1,
            },
            {
                primaryTerm: "test5",
                level: 3,
                condition: 0,
            },
            {
                primaryTerm: "and",
                level: 3,
                condition: 2,
            },
            {
                primaryTerm: "test5",
                level: 3,
                condition: 0,
            },
            {
                primaryTerm: "and",
                level: 1,
                condition: 2,
            },
            {
                primaryTerm: "test6",
                level: 2,
                condition: 0,
            },
            {
                primaryTerm: "or",
                level: 2,
                condition: 1,
            },
            {
                primaryTerm: "test7",
                level: 2,
                condition: 0,
            },
        
        ]


current output of my code



    [
      {
        id: 1,
        children: [
          {
            id: 2,
            children: [
              {
                id: 3,
                children: [
                  {
                    id: 4,
                    children: [],
                    parentId: 3,
                    level: 3,
                    primaryTerm: 'test1',
                    condition: 0
                  },
                  {
                    id: 5,
                    children: [],
                    parentId: 3,
                    primaryTerm: 'test2',
                    level: 3,
                    condition: 0
                  }
                ],
                parentId: 2,
                level: 2,
                primaryTerm: 'and',
                condition: 2
              }
            ],
            parentId: 1,
            level: 1,
            primaryTerm: 'or',
            condition: 1
          },
          {
            id: 6,
            children: [
              {
                primaryTerm: 'test3',
                level: 2,
                condition: 0,
                id: 7,
                children: [],
                parentId: 6
              },
              {
                id: 8,
                children: [],
                parentId: 6,
                primaryTerm: 'test4',
                level: 2,
                condition: 0
              },
              {
                id: 9,
                children: [
                  {
                    id: 10,
                    children: [
                      {
                        primaryTerm: 'test5',
                        level: 3,
                        condition: 0,
                        id: 11,
                        children: [],
                        parentId: 10
                      },
                      {
                        id: 12,
                        children: [],
                        parentId: 10,
                        primaryTerm: 'test5',
                        level: 3,
                        condition: 0
                      }
                    ],
                    parentId: 9,
                    level: 2,
                    primaryTerm: 'and',
                    condition: 2
                  }
                ],
                parentId: 1,
                level: 1
              }
            ],
            parentId: 0,
            level: 1,
            primaryTerm: 'or',
            condition: 1
          },
          {
            id: 13,
            children: [
              {
                primaryTerm: 'test6',
                level: 2,
                condition: 0,
                id: 14,
                children: [],
                parentId: 13
              },
              {
                id: 15,
                children: [],
                parentId: 13,
                primaryTerm: 'test7',
                level: 2,
                condition: 0
              }
            ],
            parentId: 0,
            level: 1,
            primaryTerm: 'or',
            condition: 1
          }
        ],
        parentId: 0,
        level: 0,
        primaryTerm: 'and',
        condition: 2
      }
    ]


Here is the current output of my function, test6 and test 7 are properly inserted. where the empty object exists is where the extra node is being inserted. if this extra empty object wasnt there it would be perfect


                                       AND

                                  /     |     \

                             OR         OR          OR

                           /         /  |  \         |  \       

                     AND       test3 test4  {}      test6  test7                                             
                   /   \                     |  
               test1    test2               and                          
                                            / \                                                  
                                       test5  test5                    
                       
                                                                     
                    
                                                                  

as you can see object with id: #9 is an extra object that should not be there that is the issue im dealing with. thank you

LuisPM
  • 13
  • 5
  • do you have always the same operator for a level? – Nina Scholz Jul 21 '22 at 17:32
  • @NinaScholz the logicalOperator can be an And /Or the only thing that distinguishes them in the 'condition' property, condition: 1 means 'or', condition: '2' means 'and', condition: 0 means its a node , but the child nodes can be anything – LuisPM Jul 21 '22 at 17:41
  • why do you get the second result? can you make a tree of it? – Nina Scholz Jul 23 '22 at 17:56
  • @NinaScholz edited to post to include the tree representation of the latest output – LuisPM Jul 25 '22 at 14:40

2 Answers2

1

This is an approach with an array for the operator for every level.

The tree contains always sub trees with an operator as root and operands as leaves. Any operand can contains sub trees as well.

If a level is deeper than the level before, it creates a new sub tree with the operand at top and children for the operands.

const
    getTree = predicates => {
        const
            result = [],
            levels = [{ children: result }];

        let id = 1,
            lastLevel = -Infinity;

        predicates.forEach(o => {
            const level = o.level;

            if (level > lastLevel) {
                let l = level - 1;
                while (!levels[l]) l--;
                while (l < level) {
                    // generate new operator
                    // assign children to levels
                    levels[l].children.push(levels[l + 1] = { id, parentId: levels[l]?.id || 0, children: [] });
                    id++;
                    l++;
                }
            }

            if (o.condition) { // update operators
                if (!levels[level].condition) Object.assign(levels[level], o);
            } else { // assign levels
                levels[level].children.push({ id, parentId: levels[level].id, ...o, children: [] });
                id++;
            }

            lastLevel = level;
        });
        return result;    
    },    
    predicates1 = [{ primaryTerm: "test1", level: 1, condition: 0 }, { primaryTerm: "and", level: 1, condition: 2, }, { primaryTerm: "test2", level: 1, condition: 0 }, { primaryTerm: "and", level: 1, condition: 2 }, { primaryTerm: "test3", level: 2, condition: 0 }, { primaryTerm: "or", level: 2, condition: 1 }, { primaryTerm: "test4", level: 2, condition: 0 }, { primaryTerm: "or", level: 2, condition: 1 }, { primaryTerm: "test5", level: 3, condition: 0 }, { primaryTerm: "and", level: 3, condition: 2 }, { primaryTerm: "test5", level: 3, condition: 0 }],
    predicates2 = [{ primaryTerm: "test1", level: 3, condition: 0 }, { primaryTerm: "and", level: 3, condition: 2 }, { primaryTerm: "test2", level: 3, condition: 0 }, { primaryTerm: "and", level: 1, condition: 2 }, { primaryTerm: "test3", level: 2, condition: 0 }, { primaryTerm: "or", level: 2, condition: 1 }, { primaryTerm: "test4", level: 2,  condition: 0 }, { primaryTerm: "or", level: 2, condition: 1 }, { primaryTerm: "test5",level: 3,  condition: 0 }, { primaryTerm: "and",level: 3,  condition: 2 }, { primaryTerm: "test5", level: 3, condition: 0 }, { primaryTerm: "and", level: 1, condition: 2 }, { primaryTerm: "test6", level: 2, condition: 0 }, { primaryTerm: "or", level: 2, condition: 1 }, { primaryTerm: "test7", level: 2, condition: 0 }];


console.log(getTree(predicates1));
console.log(getTree(predicates2));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
0

This should get you started:

function parse(tokens, level) {
    let expr = []

    while (tokens.length) {
        let tok = tokens[0]
        if (tok.level < level) {
            break;
        }
        if (tok.level === level) {
            expr.push(tok.primaryTerm);
            tokens.shift();
            continue;
        }
        expr.push(parse(tokens, tok.level))
    }

    return {op: expr[1], args: expr.filter((_, n) => n % 2 === 0)}
}

For your input, parse(stackTest.predicates, 1) returns this AST:

{
    "op": "and",
    "args": [
        "test1",
        "test2",
        {
            "op": "or",
            "args": [
                "test3",
                "test4",
                {
                    "op": "and",
                    "args": [
                        "test5",
                        "test5"
                    ]
                }
            ]
        }
    ]
}
gog
  • 10,367
  • 2
  • 24
  • 38
  • appreciate the answer but this wont work, i already have code above that organizes the data, i need help debugging it since an extra object is being created. when the branch is to the right of the tree but not when it is to the left – LuisPM Jul 21 '22 at 18:04
  • @LuisPM: well, wouldn't it be easier to debug 20 lines that work rather than 100 lines that don't? ;) It's your call, of course. – gog Jul 21 '22 at 18:11
  • i can try building ontop of what you have provided, again i appreciate the help. right now Im not getting the same output object. the "op" and "args" are only showing for the first level and not the rest, they become nested arrays after the first level – LuisPM Jul 21 '22 at 18:27
  • NVM i renamed the function to parse2 since i copied the first iteration of your answer and forgot to update the name of the recursive call as well im now getting the props for all nested elements – LuisPM Jul 21 '22 at 18:33