【问题标题】:Convert array of paths into data structure将路径数组转换为数据结构
【发布时间】:2020-11-30 11:53:54
【问题描述】:

我有一个这样的路径数组:

/doc/data/main.js
/doc/data/xl.js
/doc/data/dandu/sdasa.js
/mnt/data/la.js

我正在尝试构建以下结构:

{
  "directories": {
    "/doc/data": {
      "directories": {
        "dandu": {
          "files": {
            "sdasa.js": 1
          }
        }
      },
      "files": {
        "main.js": 1,
        "xl.js": 1
      }
    },
    "/mnt/data": {
      "directories": {},
      "files": {
        "la.js": 1
      }
    }
  },
  "files": {}
}

请忽略该示例中文件的值。将来我会为此分配更复杂的数据。当前值为 1。

从之前的topic我发现我可以使用以下函数来获得类似的东西:

var parsePathArray = function() {
    var parsed = {};
    for(var i = 0; i < paths.length; i++) {
        var position = parsed;
        var split = paths[i].split('/');
        for(var j = 0; j < split.length; j++) {
            if(split[j] !== "") {
                if(typeof position[split[j]] === 'undefined')
                    position[split[j]] = {};
                position = position[split[j]];
            }
        }
    }
    return parsed;
}

该解决方案的主要问题是它拆分了每个目录。但我不想拆分每个目录,而是获取至少包含一个文件的目录。例如,/doc 在我的示例中没有文件(只有目录 - /data),所以我们继续。我试着稍微改变一下功能,但没有用:

var str = '';
for (var j = 0; j < split.length; j++) {
    if (j < split.length - 1 && typeof this.files[str] === 'undefined') {
        str += '/' + split[j];
        continue;
    }
    if (str !== '') {
        if (typeof this.files[str] === 'undefined')
            this.files[str] = {};
        this.files = this.files[str];
    }
}

将这些字符串转换为该数据结构的最佳方法是什么?

【问题讨论】:

  • 编写一个函数来过滤从您从该主题获得的代码接收到的输出?
  • ...任何给定的答案仍然没有达到验收标准吗?

标签: javascript arrays algorithm


【解决方案1】:

这是我想出的解决方案。它通过一次构建一条路径并将其与现有数据结构进行比较来工作。它还应该自己处理文件,因为您的原始帖子似乎暗示这是必要的。最后我决定把它分成两个函数,因为这样可能更容易解释。

守则:

const paths = [
    '/doc/data/main.js',
    'doc/data/xl.js',
    '/etc/further/owy.js',
    '/etc/further/abc.js',
    'etc/mma.js',
    '/mnt/data/it.js',
    '/mnt/data/path/is/long/la.js',
    'mnt/data/path/is/la.js',
    '/doc/data/dandu/sdasa.js',
    '/etc/i/j/k/l/thing.js',
    '/etc/i/j/areallylongname.js',
    'thing.js'
];

function buildStructure(paths) {
    let structure = {
        directories: {},
        files: {}
    };

    const compare = (a, b) => {
        return a.split('/').length - b.split('/').length;
    };

    [...paths]
    .map(path => path = path.charAt(0) === '/' ? path : `/${path}`)
    .sort((a, b) => compare(a, b)).forEach(path => {
        const nodes = path.split('/').slice(1);
        const file = nodes.pop();
        
        let pointer = findDirectory(nodes[0] ? structure.directories : structure, '', [...nodes]);

        pointer.files = pointer.files || {};
        pointer.files = {
            ...pointer.files,
            [file]: 1
        };
    });

    return structure;
};

function findDirectory(pointer, subPath, nodes) {
    if (nodes.length === 0) {
        if (subPath) {
            pointer[subPath] = {};
            pointer = pointer[subPath];
        };
        return pointer;
    };

    let newPath = `${subPath}/${nodes[0]}`;
    nodes.shift();

    if (pointer[newPath]) {
        pointer = pointer[newPath];

        if (nodes.length >= 1) {
            pointer.directories = pointer.directories || {};
            pointer = pointer.directories;
        };

        newPath = '';
    };

    return findDirectory(pointer, newPath, nodes);
};

const structure = buildStructure(paths);
console.log(structure);
.as-console-wrapper { min-height: 100%!important; top: 0; }

解释:

这最终比我开始研究它时的想象要复杂得多(也更有趣)。一旦你开始连接目录,操作顺序就很重要了。

buildStructure 开始,我们映射路径数组以捕获没有前导斜杠的任何条目。然后,根据它们引用的目录数量对它们进行排序。这样我们就可以确定我们是从结构的顶部向底部工作的。

将每个路径分成一个节点数组,并弹出文件字符串。你会得到这样的东西:

const nodes = ['doc', 'data'];
const file = 'main.js';

现在我们必须通过findDirectory 提供这些节点以查找/创建文件的位置。变量pointer 用于跟踪我们在structure 对象中的位置,并且我们对指针所做的任何更改都将复制到结构中,因为它们共享引用相等。

findDirectory 函数递归处理每个节点,以逐步构建路径,使其恢复完整长度。每当我们创建一个已经存在于structures 目录中的路径时,我们就会在其中移动并重新开始构建路径以尝试找到下一个路径。如果我们找不到它,那么我们就有了一个全新的目录。目的是当我们退出函数时总是在正确的目录中结束 - 如果需要,可以在此过程中创建它。

为了简单起见,假设我们只有两条记录路径:

const paths = [
  'doc/data/main.js',
  'doc/data/dandu/sdasa.js'
];

对于第一条路径,findDirectory 将进行三遍。这些是每次传递时将提供给它的参数:

pointer = structure.directories > same > same

subPath = '' > '/doc' > '/doc/data'

nodes = ['doc', 'data'] > ['data'] > []

我们从未找到匹配项,因此当函数退出时,它会在 structure.directories 上创建该目录。现在,第二条路径将进行四遍:

pointer = 
  structure.directories > 
  same > 
  structure.directories./doc/data.directories > 
  same

subPath = '' > '/doc' > '' > '/dandu' 

nodes = ['doc', 'data', 'dandu'] > ['data', 'dandu'] > ['dandu'] > []

如您所见,在第二遍中,我们创建了字符串/doc/data,它确实存在于structure.directories 上。所以我们进入它,因为有更多的节点要处理,我们在那里创建一个新的目录对象并输入它。如果没有更多节点要处理,我们就知道我们已经到达了正确的级别,这将是不必要的。从这里开始,只需重新构建路径并重复该过程即可。

一旦我们在正确的目录中,我们可以将文件直接放在指针上,它将被注册到结构上。一旦我们移动到下一条路径,指针将再次指向structure.directories

如果没有要处理的节点(仅文件名)- 改为传递 findDirectory 整个结构对象,文件将进入对象的顶层。


希望这可以很好地解释事情并对您有用。我很喜欢这方面的工作,并且很高兴收到有关如何改进它的任何建议。

【讨论】:

  • 感谢您提供'/etc''/etc/further''/etc/i/j''/etc/i/j/k/l'的文件结构。没有它,我错过了第一种方法中的设计缺陷,即如何正确计算路径部分(单个或集群)。这一点以及为您的答案/解决方案所做的努力至少值得一票。
【解决方案2】:

这个挑战真的不是那么微不足道。然而,该方法适用于人们可以考虑的、易于阅读和理解的,因此是可维护的子任务,以达到 OP 的目标......

const pathList = [
  '/doc/data/main.js',
  '/doc/data/fame.js',
  '/doc/data/fame.es',
  '/doc/data/xl.js',
  '/doc/data/dandu/sdasa.js',

  '/mnt/data/la.js',
  '/mnt/la.es',

  'foo/bar/baz/biz/foo.js',
  'foo/bar/baz/biz/bar.js',
  '/foo/bar.js',
  '/foo/bar/baz/foo.js',
  'foo/bar/baz/bar.js',
  'foo/bar/baz/biz.js',

  '/foobar.js',
  'bazbiz.js',

  '/etc/further/owy.js',
  '/etc/further/abc.js',
  'etc/mma.js',
  '/etc/i/j/k/l/thing.js',
  '/etc/i/j/areallylongname.js'
];


function createSeparatedPathAndFileData(path) {
  const regXReplace = (/^\/+/);     // for replacing leading slash sequences in `path`.
  const regXSplit = (/\/([^/]*)$/); // for retrieving separated path- and file-name data.
  
  const filePartials = path.replace(regXReplace, '').split(regXSplit);
  if (filePartials.length === 1) {

    // assure at least an empty `pathName`.
    filePartials.unshift('');
  }
  const [pathName, fileName] = filePartials;

  return {
    pathName,
    fileName
  };
}

function compareByPathAndFileNameAndExtension(a, b) {
  const regXSplit = (/\.([^.]*)$/); // split for filename and captured file extension. 

  const [aName, aExtension] = a.fileName.split(regXSplit);
  const [bName, bExtension] = b.fileName.split(regXSplit);

  return (
       a.pathName.localeCompare(b.pathName)
    || aName.localeCompare(bName)
    || aExtension.localeCompare(bExtension)
  )
}


function getRightPathPartial(root, pathName) {
  let rightPartial = null; // null || string.

  const partials = pathName.split(`${ root }\/`);
  if ((partials.length === 2) && (partials[0] === '')) {

    rightPartial = partials[1];
  }
  return rightPartial; // null || string.
}

function getPathPartials(previousPartials, pathName) {
  let pathPartials = Array.from(previousPartials);
  let rightPartial;

  while (!rightPartial && pathPartials.pop() && (pathPartials.length >= 1)) {

    rightPartial = getRightPathPartial(pathPartials.join('\/'), pathName);
  }
  if (pathPartials.length === 0) {

    pathPartials.push(pathName);

  } else if (rightPartial) {

    pathPartials = pathPartials.concat(rightPartial);
  }
  return pathPartials;
}

function createPathPartialDataFromCurrentAndPreviousItem(fileData, idx, list) {
  const previousItem = list[idx - 1];
  if (previousItem) {

    const previousPathName = previousItem.pathName;
    const currentPathName = fileData.pathName;

    if (previousPathName === currentPathName) {

      // duplicate/copy path partials.
      fileData.pathPartials = [].concat(previousItem.pathPartials);

    } else {
      // a) try an instant match first ...

      const rightPartial = getRightPathPartial(previousPathName, currentPathName);
      if (rightPartial || (previousPathName === currentPathName)) {

        // concat path partials.
        fileData.pathPartials = previousItem.pathPartials.concat(rightPartial);

      } else {
        // ... before b) programmatically work back the root-path
        //               and look each time for another partial match.

        fileData.pathPartials = getPathPartials(
          previousItem.pathPartials,
          fileData.pathName
        );
      }
    }
  } else {
    // initialize partials by adding path name.
    fileData.pathPartials = [fileData.pathName];
  }
  return fileData;
}


function isUnassignedIndex(index) {
  return (Object.keys(index).length === 0);
}
function assignInitialIndexProperties(index) {
  return Object.assign(index, {
    directories: {},
    files: {}
  });
}

function assignFileDataToIndex(index, fileData) {
  if (isUnassignedIndex(index)) {
    assignInitialIndexProperties(index);
  }
  const { pathPartials, fileName } = fileData;

  let path, directories;
  let subIndex = index;

  while (path = pathPartials.shift()) {
    directories = subIndex.directories;

    if (path in directories) {

      subIndex = directories[path];
    } else {
      subIndex = directories[path] = assignInitialIndexProperties({});
    }
  }
  subIndex.files[fileName] = 1;

  return index;
}


console.log(
  'input :: path list ...',
  pathList
  //.map(createSeparatedPathAndFileData)
  //.sort(compareByPathAndFileNameAndExtension)
  //.map(createPathPartialDataFromCurrentAndPreviousItem)
  //.reduce(assignFileDataToIndex, {})
);
console.log(
  '1st :: create separated path and file data from the original list ...',
  pathList
    .map(createSeparatedPathAndFileData)
  //.sort(compareByPathAndFileNameAndExtension)
  //.map(createPathPartialDataFromCurrentAndPreviousItem)
  //.reduce(assignFileDataToIndex, {})
);
console.log(
  '2nd :: sort previous data by comparing path- and file-names and its extensions ...',
  pathList
    .map(createSeparatedPathAndFileData)
    .sort(compareByPathAndFileNameAndExtension)
  //.map(createPathPartialDataFromCurrentAndPreviousItem)
  //.reduce(assignFileDataToIndex, {})
);
console.log(
  '3rd :: create partial path data from current/previous items of the sorted list ...',
  pathList
    .map(createSeparatedPathAndFileData)
    .sort(compareByPathAndFileNameAndExtension)
    .map(createPathPartialDataFromCurrentAndPreviousItem)
  //.reduce(assignFileDataToIndex, {})
);
console.log(
  '4th :: output :: assemble final index from before created list of partial path data ...',
  pathList
    .map(createSeparatedPathAndFileData)
    .sort(compareByPathAndFileNameAndExtension)
    .map(createPathPartialDataFromCurrentAndPreviousItem)
    .reduce(assignFileDataToIndex, {})
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

...从上面的日志中可以看出,这些任务是...

清理和(重新)结构化/映射

  1. 通过删除可能的前导斜杠序列对每个路径进行清理/规范化。
  2. 会构建一个文件数据项列表,其中每个项都包含pathNamefileName 对应的路径项,采用后者的净化/规范化形式。

例如'/doc/data/dandu/sdasa.js' 被映射到 ...

{
  "pathName": "doc/data/dandu",
  "fileName": "sdasa.js"
}

排序

排序是通过以下方式比较两个当前映射的文件数据项的属性来完成的...

  1. 比较pathName
  2. fileName 比较,不带扩展名
  3. 按文件扩展名比较

因此一个看起来像这样的原始文件列表......

[
  '/doc/data/main.js',
  '/doc/data/fame.js',
  '/doc/data/fame.es',
  '/doc/data/dandu/sdasa.js',
  'foo/bar/baz/biz/bar.js',
  '/foo/bar.js',
  'foo/bar/baz/biz.js',
  '/foobar.js'
]

... 将被(净化/标准化映射和)排序成类似的东西...

[{
  "pathName": "",
  "fileName": "foobar.js"
}, {
  "pathName": "doc/data",
  "fileName": "fame.es"
}, {
  "pathName": "doc/data",
  "fileName": "fame.js"
}, {
  "pathName": "doc/data",
  "fileName": "main.js"
}, {
  "pathName": "doc/data/dandu",
  "fileName": "sdasa.js"
}, {
  "pathName": "foo",
  "fileName": "bar.js"
}, {
  "pathName": "foo/bar/baz",
  "fileName": "biz.js"
}, {
  "pathName": "foo/bar/baz/biz",
  "fileName": "bar.js"
}]

排序是基础,因为紧随其后的算法依赖于整齐排序/对齐的pathNames。

路径部分的分割和聚类

为了保持这个任务愚蠢,它由一个映射过程完成,该过程不仅使用当前处理的项目,还使用这个项目的前一个兄弟(或前任)。

一个额外的pathPartials 列表将通过将当前pathName 与前一个拆分来构建。

例如'foo/bar/baz' 将与之前的 'foo' 拆分(通过正则表达式)。因此,'bar/baz' 已经是一个聚集的部分路径,将用于创建当前文件数据项的pathPartials 列表,方法是将这个非常部分连接到其先前兄弟的pathPartials 列表(此时为['foo']。因此前者的结果将是['foo', 'bar/baz']

同样的情况也发生在 'foo/bar/baz/biz' 上,之前的路径名是 'foo/bar/baz',之前的部分列表是 ['foo', 'bar/baz']。拆分结果为'biz',新的部分列表为['foo', 'bar/baz', 'biz']

上面排序的文件数据列表然后映射到这个新列表中......

[{
  "pathName": "",
  "fileName": "foobar.js",
  "pathPartials": [
    ""
  ]
}, {
  "pathName": "doc/data",
  "fileName": "fame.es",
  "pathPartials": [
    "doc/data"
  ]
}, {
  "pathName": "doc/data",
  "fileName": "fame.js",
  "pathPartials": [
    "doc/data"
  ]
}, {
  "pathName": "doc/data",
  "fileName": "main.js",
  "pathPartials": [
    "doc/data"
  ]
}, {
  "pathName": "doc/data/dandu",
  "fileName": "sdasa.js",
  "pathPartials": [
    "doc/data",
    "dandu"
  ]
}, {
  "pathName": "foo",
  "fileName": "bar.js",
  "pathPartials": [
    "foo"
  ]
}, {
  "pathName": "foo/bar/baz",
  "fileName": "biz.js",
  "pathPartials": [
    "foo",
    "bar/baz"
  ]
}, {
  "pathName": "foo/bar/baz/biz",
  "fileName": "bar.js",
  "pathPartials": [
    "foo",
    "bar/baz",
    "biz"
  ]
}]

组装最终索引

最后一步是一个简单的列表缩减任务,因为此时,正确拆分和聚类每个项目的路径部分的最困难部分已经完成。

【讨论】:

    【解决方案3】:

    你可以用一个有点递归的函数来完成它。请记住,这只是一种可能的解决方案,可能不是最好的解决方案。

    const workPath = (path, structure) => {
        if(!structure) structure = {};
    
        const folders = path.split("/");
        const file = folders.pop();
    
        // Check weather any of the possible paths are available
        let breakPoint = null;
        let tempPath;
        for(let i = 0; i< folders.length; i++){
            const copy = [... folders];
            tempPath = copy.splice(0, i+1).join("/");
    
            if(structure[tempPath]){
                breakPoint = i;
                break;
            }        
        }
    
        // If there was no path available, we create it in the structure
        if(breakPoint == null){
            const foldersPath = folders.join("/");
            structure[foldersPath]= {};
            structure[foldersPath]["files"] = {};
            structure[foldersPath]["files"][file] = 1;
        }
    
        // If there is a path inside of the structure, that also is the entire path we are working with,
        // We just add the file to the path
        else if(breakPoint && breakPoint == folders.length - 1){
            structure[folders.join("/")]["files"][file] = 1;
        }
        
        // If we get here, it means that some part of the path is available but not the entire path
        // So, we just call the workPath function recursively with only one portion of the path
        else{
            const subPath = folders.splice(breakPoint + 1).join("/") + "/" + file;
            
            structure[tempPath]["directories"] = workPath(subPath, structure[tempPath]["directories"]);  
        }
    
        return structure;
    }
    
    const convert = array => {
        let structure = {};
        for(let path of array){
            structure = workPath(path, structure);
        }
    
        return structure;
    }
    

    “转换”函数需要一个包含所有路径的数组。

    请记住,此解决方案不考虑其中没有文件的条目。

    【讨论】:

    • 上述解决方案已经因类似的事情而失败了......const pathList = ['/mnt/data/la.js', '/mnt/la.es']; convert(pathList);
    • 我得到了 {"/mnt/data":{"files":{"la.js":1}},"/mnt":{"files":{"la.es" :1}}},/mnt/data 不在 /mnt/ 内,因为它是在之后出现的,如果这就是您所指的问题
    • 没错,就是要识别这样的结构。在组装索引之前,可以帮助自己对路径列表进行排序。
    • 是的,如果您按长度对目录进行排序,则保证子目录在其父目录之后
    猜你喜欢
    • 2016-07-14
    • 2022-12-19
    • 2019-03-16
    • 1970-01-01
    • 2021-10-03
    • 2015-06-22
    • 1970-01-01
    • 2023-04-08
    • 1970-01-01
    相关资源
    最近更新 更多