1

I'm trying to morph a series of file paths into a nested recursive structure for an Angular Material Tree component. The structure needs to be this:

interface FileTreeNode {
    name: string;
    children?: FileTreeNode[];
}

So a list of file paths such as ['path/to/file/01.txt', 'path/to/file/02.txt', 'another/to/file/03.txt'] will transform into:

[
    {name: 'path', children: [
        {name: 'to', children: [
            {name: 'file', children: [
                {name: '01.txt'}, {name: '02.txt'}
                ]
            }]
        }]
    },
    {name: 'another', children: [
        {name: 'to', children: [
            {name: 'file', children: [
                {name: '03.txt'}
                ]
            }]
        }]
    }
]

I saw a great answer for this problem here, which uses the built-in Array.reduce() method, but I'd like to accomplish this with RxJs operators.

Here is the code I'm trying to replicate:

var paths = ["path/to/file1.doc", "path/to/file2.doc", "foo/bar.doc"],
    result = [];
    
paths.reduce((r, path) => {
    path.split('/').reduce((o, name) => {
        var temp = (o.children = o.children || []).find(q => q.name === name);
        if (!temp) o.children.push(temp = { name });
        return temp;
    }, r);
    return r;
}, { children: result });

console.log(result);

Here's what I have so far:

reduce((acc, curr) => {
    return curr.split('/').reduce((obj, name) => {
        let temp = (obj.children != null ? obj.children : obj.children = []).find(node => node.name === name);
        if (temp == null) {
            obj.children.push(temp = {name});
        }
        return temp;
    }, acc);
}, {children: []} as FileTreeNode),

But this is only returning the last node in the observable (eg. {name: 03.txt}). What I want to be passing along is the final value of acc as the above code does. I also would like to accomplish this using just the RxJs operators, instead of having to rely on the internal Js Array.reduce() function if possible.

Thanks!

FerdTurgusen
  • 320
  • 5
  • 13
  • It's an interesting exercise, but unless you have an asynchronous stream of file paths segments, you should stick with `Array.prototype.reduce` – Aluan Haddad Jan 18 '21 at 23:02

1 Answers1

1

Use reduce exactly the same way, The difference is that you need to convert the array to a stream. You can use from operator

  paths = ["path/to/file1.doc", "path/to/file2.doc", "foo/bar.doc"];
  paths$ = from(this.paths).pipe(
    reduce(
      (r, path) => {
        path.split("/").reduce((o, name) => {
          var temp = (o.children = o.children || []).find(q => q.name === name);
          if (!temp) o.children.push((temp = { name }));
          return temp;
        }, r);
        return r;
      },
      { children: [] }
    )
  );

See this demo

Owen Kelvin
  • 14,054
  • 10
  • 41
  • 74
  • Thank you. The issue was that my sonar lint goes haywire because I'm not returning the value of the reduce, but rather the accumulator object. That's what kept me from doing the `return r;` statement as you have. – FerdTurgusen Jan 19 '21 at 04:10