这个挑战真的不是那么微不足道。然而,该方法适用于人们可以考虑的、易于阅读和理解的,因此是可维护的子任务,以达到 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; }
...从上面的日志中可以看出,这些任务是...
清理和(重新)结构化/映射
- 通过删除可能的前导斜杠序列对每个路径进行清理/规范化。
- 会构建一个文件数据项列表,其中每个项都包含
pathName 和fileName 对应的路径项,采用后者的净化/规范化形式。
例如'/doc/data/dandu/sdasa.js' 被映射到 ...
{
"pathName": "doc/data/dandu",
"fileName": "sdasa.js"
}
排序
排序是通过以下方式比较两个当前映射的文件数据项的属性来完成的...
- 比较
pathName
- 按
fileName 比较,不带扩展名
- 按文件扩展名比较
因此一个看起来像这样的原始文件列表......
[
'/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"
]
}]
组装最终索引
最后一步是一个简单的列表缩减任务,因为此时,正确拆分和聚类每个项目的路径部分的最困难部分已经完成。