0

I've got an array of comments with 4 properties: id, parent, content and comments. If a comment has a parent value of 0 it means that it is a top level comment and if it contains a number larger than 0 it will be the parents' id and I'd like for the comment to be placed in the comments' property of the parent. I've attempted to write a recursive TypeScript function to handle this but it duplicates certain comments as you can see in this screen shot:

Picture of the wrongly sorted comments

This is an example data-set and you can see my source code on thiscodesandbox.io:

[
  {
    id: 5819,
    parent: 0,
    content: "Waar vind ik meer?",
    comments: []
  },
  {
    id: 5820,
    parent: 5819,
    content: "Epic site!",
    comments: []
  },
  {
    id: 5821,
    parent: 5819,
    content: "Stoinks.",
    comments: []
  },
  {
    id: 5822,
    parent: 5821,
    content: "Can I get a what now?",
    comments: []
  },
  {
    id: 5823,
    parent: 0,
    content: "Yeeeee",
    comments: []
  }
];
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
Schotsl
  • 187
  • 1
  • 8
  • 29
  • @Yevgen: That edit looks to have really changed the data structure. Granted, an image is not the correct format here, but the nesting is missing. – Scott Sauyet Nov 11 '20 at 21:59
  • @Yevgen: rolling back. I think it's important here. – Scott Sauyet Nov 11 '20 at 22:12
  • First, an image of the results is not really helpful. Please take the time to format the current incorrect results in text. Second, please include the relevant code you've tried in the question. It's ok to also have a link to an external site. But that should not take the place of a thorough question. – Scott Sauyet Nov 11 '20 at 22:14

5 Answers5

1

IDK why your solution needs to be recursive. You'd end up with a slow algo.

If you index your comments by ID's you'd need to loop through them only 3 times. (Compared to theoretical n*n recursive approach.

1 Index your comments

const indexedComments = OneD.reduce((previous, current) => {
  // create a coppy of comment so we do not modify original comments
  // if you want to preserve, just use `= current` instead
  previous[current.id] = { ...current };
  return previous;
}, <{ [key: number]: IComment }>{});

2 Assign comments to their parent.

for (const comment of OneD) {
  if (comment.parent !== NO_PARENT) {
    indexedComments[comment.parent].comments.push(comment);
  }
}

3 The result is an array of comments with no parent.

// If you modified exiting comments in the first step, you can filter over OneD instead
const ThreeD = Object.values(indexedComments).filter(
  (comment) => comment.parent === NO_PARENT
);

console.log(ThreeD);

https://codesandbox.io/s/thirsty-shape-ws6y8?file=/src/index.ts:587-1283

kvetis
  • 6,682
  • 1
  • 28
  • 48
  • speaking of performance, you might want to check out [this one](https://jsbench.me/8xkhde6ohd/1) , I was lazy to source larger input, feel free to update – Yevhen Horbunkov Nov 11 '20 at 12:44
  • @YevgenGorbunkov Of course it is slower on 5 comments. [Try a hundred](https://jsbench.me/gfkhenw900/1) ;-) – kvetis Nov 12 '20 at 10:02
1

One fairly simple technique looks like this:

const nestComments = (root, xs) => 
  xs .filter (({parent}) => parent == root)
     .map (({id, parent, ...rest}) => ({id, ...rest, comments: nestComments (id, xs)}))

const comments = [{id: 5819, parent: 0, content: "Waar vind ik meer?"}, {id: 5820, parent: 5819, content: "Epic site!"}, {id: 5821, parent: 5819, content: "Stoinks."}, {id: 5822, parent: 5821, content: "Can I get a what now?"}, {id: 5823, parent: 0, content: "Yeeeee"}]

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

I didn't build this in isolation, though. I actually built it by using the technique described below and then inserting the function parameters back into the above.

I would actually in practice use the version below, or some variant of it over this custom function, as it lets me write this code in terms of simpler functions. But YMMV.


Not long ago, I wrote an answer offering a generic approach to these sorts of problems (see the "Update: Generalization" section). There's a lot more information about it there, but essentially this forest function takes a flat list, two functions, and a representation of the root node, and returns this sort of nested structure. The first function details how we build our current node given an original node and a function that will yield the children when we pass it a representation of the original node. The second function simply reports, when given a representation of the current node and a candidate child node, whether the second is in fact a child of the first.

// forest :: ([a], (a, (c -> [b])) -> b), ((c, a) -> Bool),  c) -> [b]

const forest = (xs, build, isChild, root) => 
  xs .filter (x => isChild (root, x))
     .map (node => build (node, root => forest (xs, build, isChild, root)))


const comments = [{id: 5819, parent: 0, content: "Waar vind ik meer?", comments: []}, {id: 5820, parent: 5819, content: "Epic site!", comments: []}, {id: 5821, parent: 5819, content: "Stoinks.", comments: []}, {id: 5822, parent: 5821, content: "Can I get a what now?", comments: []}, {id: 5823, parent: 0, content: "Yeeeee", comments: []}]

const nested = forest (
  comments,
  ({id, parent, ...rest}, handleKids) => ({id, ...rest, comments: handleKids (id)}), 
  (id, {parent}) => parent == id,
  0
)

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

The second function, isChild is fairly trivial here. We want to know if the second node is a child of the one represented by our id. We can just use (id, {parent}) => parent == id. Our root is represented by 0, and of course we're just going to pass a list of comments as xs. So the only thing to figure out is the build parameter, one which, if we have a node and a function, handleKids, will take its id and give us a array of children, how do we build the output we want? It's not that complicated. We simply copy over the important parts of our node, and add a comments node which is the result of calling the function with the current node's id. That looks like ({id, parent, ...rest}, handleKids) => ({id, ...rest, comments: handleKids (id)})

I chose to remove the now-unnecessary parent node. But if you wanted to keep it, it can simply be left out of the parameter destructuring, e.g. ({id, ...rest}, handleKids) => ({id, ...rest, comments: handleKids (id)})

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

A great opportunity to learn about reusable modules and mutual recursion. This solution in this answer solves your specific problem without any modification of the modules written in another answer -

// Main.js
import { tree } from './Tree'                      // <- use modules!

const input =
  [ ... ]

const result =
  tree                                             // <- make a tree
    ( input                                        // <- array of nodes
    , (node) => node.parent                        // <- foreign key
    , (node, comments) =>                          // <- node reconstructor
        ({ ...node, comments: comments(node.id) }) // <- primary key
    , 0                                            // <- select root
    )

console.log(JSON.stringify(result, null, 2))
[
  {
    "id": 5819,
    "parent": 0,
    "content": "Waar vind ik meer?",
    "comments": [
      {
        "id": 5820,
        "parent": 5819,
        "content": "Epic site!",
        "comments": []
      },
      {
        "id": 5821,
        "parent": 5819,
        "content": "Stoinks.",
        "comments": [
          {
            "id": 5822,
            "parent": 5821,
            "content": "Can I get a what now?",
            "comments": []
          }
        ]
      }
    ]
  },
  {
    "id": 5823,
    "parent": 0,
    "content": "Yeeeee",
    "comments": []
  }
]
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • @Schotsl: This is the technique that inspired my own similar answer. Mine is less efficient but slightly more general. Either is very useful. And Thankyou, once again it's great to see this put into practice. (BTW: My [current state of play](https://gist.github.com/CrossEye/92393ebd82fc774d482fc301b0a9fa7e) is fully curried with a slightly different parameter order.) – Scott Sauyet Nov 12 '20 at 14:17
  • Likewise Scott. Gotta say, I'm loving the [cleanliness of the call site](https://gist.github.com/CrossEye/92393ebd82fc774d482fc301b0a9fa7e#file-example-1-L9-L13) – Mulan Nov 12 '20 at 15:37
0

Saw your code , made changes , can try this.

interface IComment {
  id: number;
  parent: number;
  content: string;
  comments: IComment[];
}

const OneD: IComment[] = [
  {
    id: 5819,
    parent: 0,
    content: "Waar vind ik meer?",
    comments: []
  },
  {
    id: 5820,
    parent: 5819,
    content: "Epic site!",
    comments: []
  },
  {
    id: 5821,
    parent: 5819,
    content: "Stoinks.",
    comments: []
  },
  {
    id: 5822,
    parent: 5821,
    content: "Can I get a what now?",
    comments: []
  },
  {
    id: 5823,
    parent: 0,
    content: "Yeeeee",
    comments: []
  }
];


const ThreeD: IComment[] = [];
console.log(ThreeD);
function buildCommentsForASingleComment(parentComment:IComment)
{
    //Get all the comments that are child to this parent 
    OneD.forEach((singleComment)=>{
        if(singleComment.parent==parentComment.id && singleComment.id!=parentComment.id)
        {
            let finalSingleComment = buildCommentsForASingleComment(singleComment);
            parentComment.comments.push(finalSingleComment);
        }
    });
    return parentComment;
}
function generateCommentSection()
{
    OneD.forEach((singleComment)=>{
        if(singleComment.parent==0)
        {
            ThreeD.push(buildCommentsForASingleComment(singleComment));
        }
    })

}
generateCommentSection();
console.log(ThreeD)
Gopi krishna
  • 308
  • 1
  • 9
0

You could take a single loop with an object as reference to all id.

const
    data = [{ id: 5819, parent: 0, content: "Waar vind ik meer?", comments: [] }, { id: 5820, parent: 5819, content: "Epic site!", comments: [] }, { id: 5821, parent: 5819, content: "Stoinks.", comments: [] }, { id: 5822, parent: 5821, content: "Can I get a what now?", comments: [] }, { id: 5823, parent: 0, content: "Yeeeee", comments: [] }],
    tree = function (data, root) {
        var t = {};
        data.forEach(o => {
            Object.assign(t[o.id] = t[o.id] || {}, o);
            t[o.parent] = t[o.parent] || {};
            t[o.parent].comments = t[o.parent].comments || [];
            t[o.parent].comments.push(t[o.id]);
        });
        return t[root].comments;
    }(data, 0);

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