从文件路径构建树

Build a tree from file paths

本文关键字:构建 路径 文件      更新时间:2023-09-26

我试图从文件路径创建一个树视图,它可以动态地添加和删除,例如:

A/B/C/D/file1.txt
A/B/D/E/file2.txt
A/B/D/G/file3.txt
A/B/D/G/file4.txt
然而,我的树有一个要求,没有子项(文件)的路径应该折叠在一个节点中。对于上面的路径,它将产生:
A/B
  |---C/D
       file1.txt   
  |---D
     |---E
     |    file2.txt
     |---G
          file3.txt
          file4.txt

任何想法吗?创建树很容易,但我无法克服这个额外条件……我假设我必须使用某种递归,当我添加项和打破路径时,我们发现某个路径有更多的子路径(然后做同样的递归?)我要不要试试?当同一路径可以有多个文件时,它会工作吗?谢谢!

让我们从一个简单的解决方案开始,打印树的实际情况:

function browseTree(node)
{
    // ...print node...
    // Visit recursively the children nodes:
    for (var child: node.children)
    {
        browseTree(child);
    }
}

现在,让我们将其修改为"缩短"单个文件夹路径:

function browseTree(node)
{
    // Before printing, accumulate as many straight folders as possible:
    var nodeName=node.name
    while (hasJustOneFolder(node))
    {
        // This loop steps deeper in the tree:
        node=node.children[0]
        nodeName+="/"+node.name;
    }
    // ...print node...
    // Last, visit recursively the non-unique children nodes:
    for (var child: node.children)
    {
        browseTree(child);
    }
}
function hasJustOneFolder(node)
{
    return node.children.length==1 && node.children[0].isFolder();
}

考虑到您的需求,似乎在C中添加一个新文件并不意味着要进行递归操作。

如果您将file5.txt添加到文件夹C,则必须将C/D转换为具有2个子节点的节点C: file5.txt和一个名为D的新节点。D将具有与旧节点C/D相同的子节点。然后可以擦除节点C/D

然而,这不会影响节点A/B,因为文件夹A仍然只有一个文件夹(B)作为子文件夹。因此,您可以仅通过局部更改来解决问题。

我用Map创建了一个自定义树节点格式的示例代码,打印函数是一个生成器函数,用于逐行获取树的路径。

// Node
class NodePath {
    constructor(e) {
        this.isFolder = e.isFolder;
        this.name = e.name;
        this.childs = new Map();
    }
}
// Make path tree
function makePathsTree(paths) {
    const treeRoot = new NodePath({isFolder: true, name: "*"});
    for (const path of paths) {
        // Set current post as root
        let curPos = treeRoot;
        // For each part
        const parts = path.split("/");
        while (parts.length) {
            // Get cur
            const curPart = parts.shift();
            // Get child node, create if not exists
            let childNode = curPos.childs.get(curPart);
            if (!childNode) {
                childNode = new NodePath({
                    isFolder: !!parts.length,
                    name: curPart,
                });
                curPos.childs.set(curPart, childNode)
            }
            // Update cur post to child node
            curPos = childNode;
        }
    }
    // Return tree
    return treeRoot;
}
// Generator function prevent huge large file system strings
function *printPathsTree(node, offset = 0, prev = "") {
    // Offset str
    const offsetStr = " ".repeat(offset);
    // Is folder
    if (!node.isFolder) {
        yield `${offsetStr}${prev}${node.name}`;
        return;
    }
    // If one child and is folder, merge paths
    if (node.childs.size === 1) {
        const child = node.childs.values().next().value;
        if (child.isFolder === true) {
            for (const childData of printPathsTree(child, offset, `${prev}${node.name}/`)) {
                yield childData;
            }
            return;
        }
    }
    // Print node name
    yield `${offsetStr}${prev}${node.name}`;
    // For each child, print data inside
    for (const child of node.childs.values()) {
        for (const childData of printPathsTree(child, offset + prev.length, "|---")) {
            yield childData;
        }
    }
}
// == CODE ==
console.log("WITH ROOT:");
const tree = makePathsTree([
    "A/B/C/D/file1.txt",
    "A/B/C/D/file2.txt",
    "A/B/D/E/file2.txt",
    "A/B/D/G/file3.txt",
    "A/B/D/G/file4.txt",
]);
// Print tree step by step
for(const nodePath of printPathsTree(tree)) {
    console.log(nodePath);
}
// Print with A as root
console.log("'nA AS ROOT:");
for(const nodePath of printPathsTree(tree.childs.values().next().value)) {
// for(const nodePath of printPathsTree(tree.childs.get("A"))) { // same
    console.log(nodePath);
}
输出:

WITH ROOT:
*/A/B
    |---C/D
          |---file1.txt
          |---file2.txt
    |---D
        |---E
            |---file2.txt
        |---G
            |---file3.txt
            |---file4.txt
A AS ROOT:
A/B
  |---C/D
        |---file1.txt
        |---file2.txt
  |---D
      |---E
          |---file2.txt
      |---G
          |---file3.txt
          |---file4.txt

您可以通过在前导路径名上递归分组,然后合并父-子名称(如果父级只有一个子级)来构建树:

var paths = ["A/B/C/D/file1.txt", "A/B/C/D/file2.txt", "A/B/D/E/file2.txt", "A/B/D/G/file3.txt", "A/B/D/G/file4.txt"]
function merge_paths(paths){
    var d = {};
    var new_d = {}
    for (var [a, ...b] of paths){
       d[a] = (a in d) ? [...d[a], b] : [b]
    }
    for (var i of Object.keys(d)){
       if (d[i].every(x => x.length === 1)){
           new_d[i] = d[i].map(x => x[0]);
       }
       else{
           var k = merge_paths(d[i])
           if (Object.keys(k).length > 1){
              new_d[i] = k
           }
           else{
              new_d[`${i}/${Object.keys(k)[0]}`] = k[Object.keys(k)[0]]
           }
       }
    }
    return new_d;
}
var result = merge_paths(paths.map(x => x.split('/')))
console.log(result)