lordwilmoremc
lordwilmoremc

Reputation: 33

How to create a hierarchical tree from array of strings Javascript

I am trying to create a file hierarchy tree from an array of strings, however I am not getting it quite right and I know it is terribly inefficient, but not sure how to make it better.

Given I have:

const paths = [
  "test_data/new_directory/ok.txt",
  "test_data/reads_1.fq",
  "test_data/test_ref.fa",
  "test_data/dir2/dir3/dir4/dir5/file1.txt",
  "test_data/dir2/dir3/dir4/dir5/file2.txt",
  "test_data/new_directory/file2.txt",
  "other_dir/dir2/newfile.xls",
  "other_dir/sub_dir/file1.xls",
  "other_dir/sub_dir/file2.xls",
  "third_dir/first.xls"
];

I would like to end up with the following object:

{
  "/other_dir": {
    path: "/other_dir",
    type: "folder",
    isRoot: true,
    children: ["/other_dir/dir2"]
  },
  "/other_dir/dir2": {
    path: "/other_dir/dir2",
    type: "folder",
    children: ["/other_dir/dir2/newfile.xls"]
  },
  "/other_dir/dir2/newfile.xls": {
    path: "/other_dir/dir2/newfile.xls",
    type: "file",
    children: []
  },
  "/other_dir/sub_dir": {
    path: "/other_dir/sub_dir",
    type: "folder",
    children: ["/other_dir/sub_dir/file1.xls", "/other_dir/sub_dir/file2.xls"]
  },
  "/other_dir/sub_dir/file1.xls": {
    path: "/other_dir/sub_dir/file1.xls",
    type: "file",
    children: []
  },
  "/other_dir/sub_dir/file2.xls": {
    path: "/other_dir/sub_dir/file2.xls",
    type: "file",
    children: []
  },
  "/test_data": {
    path: "/test_data",
    type: "folder",
    isRoot: true,
    children: [
      "/test_data/dir2",
      "/test_data/new_directory",
      "/test_data/test_ref.fa",
      "/test_data/reads_1.fq"
    ]
  },
  "/test_data/dir2": {
    path: "/test_data/dir2",
    type: "folder",
    children: ["/test_data/dir2/dir3", "/test_data/file2.txt"]
  },
  "/test_data/file2.txt": {
    path: "/test_data/file2.txt",
    type: "file",
    children: []
  },
  "/test_data/dir2/dir3": {
    path: "/test_data/dir2/dir3",
    type: "folder",
    children: ["/test_data/dir2/dir3/dir4"]
  },
  "/test_data/dir2/dir3/dir4": {
    path: "/test_data/dir2/dir3/dir4",
    type: "folder",
    children: ["/test_data/dir2/dir3/dir4/dir5"]
  },
  "/test_data/dir2/dir3/dir4/dir5": {
    path: "/test_data/dir2/dir3/dir4/dir5",
    type: "folder",
    children: [
      "/test_data/dir2/dir3/dir4/dir5/file1.txt",
      "/test_data/dir2/dir3/dir4/dir5/file2.txt"
    ]
  },
  "/test_data/dir2/dir3/dir4/dir5/file1.txt": {
    path: "/test_data/dir2/dir3/dir4/dir5/file1.txt",
    type: "file",
    children: []
  },
  "/test_data/dir2/dir3/dir4/dir5/file2.txt": {
    path: "/test_data/dir2/dir3/dir4/dir5/file2.txt",
    type: "file",
    children: []
  },
  "/test_data/new_directory": {
    path: "/test_data/new_directory",
    type: "folder",
    children: [
      "/test_data/new_directory/ok.txt",
      "/test_data/new_directory/file2.txt"
    ]
  },
  "/test_data/new_directory/file2.txt": {
    path: "/test_data/new_directory/file2.txt",
    type: "file",
    children: []
  },
  "/test_data/new_directory/ok.txt": {
    path: "/test_data/new_directory/ok.txt",
    type: "file",
    children: []
  },
  "/test_data/reads_1.fq": {
    path: "/test_data/reads_1.fq",
    type: "file",
    children: []
  },
  "/test_data/test_ref.fa": {
    path: "/test_data/test_ref.fa",
    type: "file",
    children: []
  },
  "/third_dir": {
    path: "/third_dir",
    type: "folder",
    isRoot: true,
    children: ["/third_dir/first.xls"]
  },
  "/third_dir/first.xls": {
    path: "/third_dir/first.xls",
    type: "file",
    children: []
  }
};

This is my attempt, and I have spent way too long on this

const buildChildNodes = (arr, root) => {
  let a = []

  arr.map((n, idx) => {
    a.push('/' + root + '/' + arr[idx])
  });
  return a;
}

const createTree = paths => {
  let finalTree = {};


  paths.map(path => {
    let tree = {};

    let subTree = path.split('/')
    subTree.forEach((a,i) => {
      tree = {}
      let root = subTree[0]

      tree.path = '/' + subTree.slice(0, i+1).join('/')
      tree.type = subTree.slice(i+1).length > 0 ? 'folder' : 'file'
      if(i === 0){
        tree.isRoot = true
      }
      tree.children = buildChildNodes(subTree.slice(i+1), root)

      finalTree['/' + subTree.slice(0, i+1).join('/')] = tree
    })



  })
  return finalTree;
};
console.log(JSON.stringify(createTree(paths.sort()), null, 2));

And this is what I end up with, as you can see, the children nodes are not being created correctly:

{
  "/other_dir": {
    "path": "/other_dir",
    "type": "folder",
    "isRoot": true,
    "children": [
      "/other_dir/sub_dir",
      "/other_dir/file2.xls"
    ]
  },
  "/other_dir/dir2": {
    "path": "/other_dir/dir2",
    "type": "folder",
    "children": [
      "/other_dir/newfile.xls"
    ]
  },
  "/other_dir/dir2/newfile.xls": {
    "path": "/other_dir/dir2/newfile.xls",
    "type": "file",
    "children": []
  },
  "/other_dir/sub_dir": {
    "path": "/other_dir/sub_dir",
    "type": "folder",
    "children": [
      "/other_dir/file2.xls"
    ]
  },
  "/other_dir/sub_dir/file1.xls": {
    "path": "/other_dir/sub_dir/file1.xls",
    "type": "file",
    "children": []
  },
  "/other_dir/sub_dir/file2.xls": {
    "path": "/other_dir/sub_dir/file2.xls",
    "type": "file",
    "children": []
  },
  "/test_data": {
    "path": "/test_data",
    "type": "folder",
    "isRoot": true,
    "children": [
      "/test_data/test_ref.fa"
    ]
  },
  "/test_data/dir2": {
    "path": "/test_data/dir2",
    "type": "folder",
    "children": [
      "/test_data/dir3",
      "/test_data/dir4",
      "/test_data/dir5",
      "/test_data/file2.txt"
    ]
  },
  "/test_data/dir2/dir3": {
    "path": "/test_data/dir2/dir3",
    "type": "folder",
    "children": [
      "/test_data/dir4",
      "/test_data/dir5",
      "/test_data/file2.txt"
    ]
  },
  "/test_data/dir2/dir3/dir4": {
    "path": "/test_data/dir2/dir3/dir4",
    "type": "folder",
    "children": [
      "/test_data/dir5",
      "/test_data/file2.txt"
    ]
  },
  "/test_data/dir2/dir3/dir4/dir5": {
    "path": "/test_data/dir2/dir3/dir4/dir5",
    "type": "folder",
    "children": [
      "/test_data/file2.txt"
    ]
  },
  "/test_data/dir2/dir3/dir4/dir5/file1.txt": {
    "path": "/test_data/dir2/dir3/dir4/dir5/file1.txt",
    "type": "file",
    "children": []
  },
  "/test_data/dir2/dir3/dir4/dir5/file2.txt": {
    "path": "/test_data/dir2/dir3/dir4/dir5/file2.txt",
    "type": "file",
    "children": []
  },
  "/test_data/new_directory": {
    "path": "/test_data/new_directory",
    "type": "folder",
    "children": [
      "/test_data/ok.txt"
    ]
  },
  "/test_data/new_directory/file2.txt": {
    "path": "/test_data/new_directory/file2.txt",
    "type": "file",
    "children": []
  },
  "/test_data/new_directory/ok.txt": {
    "path": "/test_data/new_directory/ok.txt",
    "type": "file",
    "children": []
  },
  "/test_data/reads_1.fq": {
    "path": "/test_data/reads_1.fq",
    "type": "file",
    "children": []
  },
  "/test_data/test_ref.fa": {
    "path": "/test_data/test_ref.fa",
    "type": "file",
    "children": []
  },
  "/third_dir": {
    "path": "/third_dir",
    "type": "folder",
    "isRoot": true,
    "children": [
      "/third_dir/first.xls"
    ]
  },
  "/third_dir/first.xls": {
    "path": "/third_dir/first.xls",
    "type": "file",
    "children": []
  }
}

If anyone could help me out on this I would really appreciate it!

Thank you!

Upvotes: 1

Views: 131

Answers (2)

Nenad Vracar
Nenad Vracar

Reputation: 122135

You could use two nested reduce method where you add path to the children array only if there is next part of the path (which means that the current elements has child) and also if that child is not included already in the array.

const paths = [
  "test_data/new_directory/ok.txt",
  "test_data/reads_1.fq",
  "test_data/test_ref.fa",
  "test_data/dir2/dir3/dir4/dir5/file1.txt",
  "test_data/dir2/dir3/dir4/dir5/file2.txt",
  "test_data/new_directory/file2.txt",
  "other_dir/dir2/newfile.xls",
  "other_dir/sub_dir/file1.xls",
  "other_dir/sub_dir/file2.xls",
  "third_dir/first.xls"
];

const result = paths.reduce((acc, e) => {
  let prev = ''

  e.split('/').reduce((r, path, i, a) => {
    prev += '/' + path;

    if (!r[prev]) {
      const children = []
      const type = a[i + 1] ? 'folder' : 'file'
      const obj = { path: prev, type, children }

      if (i == 0) {
        obj.isRoot = true
      }

      r[prev] = obj
    }

    if (a[i + 1]) {
      const child = prev + '/' + a[i + 1];
      if (!r[prev].children.includes(child)) {
        r[prev].children.push(child)
      }
    }

    return r
  }, acc)

  return acc
}, {})

console.log(result)

Upvotes: 0

Nina Scholz
Nina Scholz

Reputation: 386868

You could get the sub parts of the strings, check if the path exits or add a new path. Then take the previous path and add the childrens.

const
    paths = ["test_data/new_directory/ok.txt", "test_data/reads_1.fq", "test_data/test_ref.fa", "test_data/dir2/dir3/dir4/dir5/file1.txt", "test_data/dir2/dir3/dir4/dir5/file2.txt", "test_data/new_directory/file2.txt", "other_dir/dir2/newfile.xls", "other_dir/sub_dir/file1.xls", "other_dir/sub_dir/file2.xls", "third_dir/first.xls"],
    result = paths.sort().reduce((r, p) => {
        p.split(/\//).forEach((_, i, p) => {
            var path = '/' + p.slice(0, i + 1).join('/');
            r[path] = r[path] || { path, type: i + 1 === p.length ? 'file' : 'folder', children: [] };
            if (i)  {
                const prev = '/' + p.slice(0, i).join('/');
                if (!r[prev].children.includes(path)) r[prev].children.push(path);
            } else {
                r[path].isRoot = true;
            }
        });
        return r;
    }, {});

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

Upvotes: 2

Related Questions