如何过滤父子数组的多个属性,这些属性可能是几个树级别的深度
Posted
技术标签:
【中文标题】如何过滤父子数组的多个属性,这些属性可能是几个树级别的深度【英文标题】:How to filter multiple properties of a parent child array which could be several tree level deep 【发布时间】:2020-07-16 22:36:16 【问题描述】:TL;DR; 为了简单起见,我。这是针对数百个用户使用的开源数据网格库。
所以我有一个父/子引用数组,孩子自己也可以有孩子等等,树级别的深度没有限制。此外,我不仅需要能够过滤具有树结构的属性,还需要能够过滤该数组的任何属性,即网格中的列。
例如,我有这个数组,它代表一个文件资源管理器列表
const myFiles = [
id: 11, file: "Music", parentId: null ,
id: 12, file: "mp3", parentId: 11 ,
id: 14, file: "pop", parentId: 12 ,
id: 15, file: "theme.mp3", dateModified: "2015-03-01", size: 85, parentId: 14, ,
id: 16, file: "rock", parentId: 12 ,
id: 17, file: "soft.mp3", dateModified: "2015-05-13", size: 98, parentId: 16, ,
id: 18, file: "else.txt", dateModified: "2015-03-03", size: 90, parentId: null, ,
id: 21, file: "Documents", parentId: null, ,
id: 2, file: "txt", parentId: 21 ,
id: 3, file: "todo.txt", dateModified: "2015-05-12", size: 0.7, parentId: 2, ,
id: 4, file: "pdf", parentId: 21 ,
id: 22, file: "map2.pdf", dateModified: "2015-05-21", size: 2.9, parentId: 4 ,
id: 5, file: "map.pdf", dateModified: "2015-05-21", size: 3.1, parentId: 4, ,
id: 6, file: "internet-bill.pdf", dateModified: "2015-05-12", size: 1.4, parentId: 4, ,
id: 7, file: "xls", parentId: 21 ,
id: 8, file: "compilation.xls", dateModified: "2014-10-02", size: 2.3, parentId: 7, ,
id: 9, file: "misc", parentId: 21 ,
id: 10, file: "something.txt", dateModified: "2015-02-26", size: 0.4, parentId: 9, ,
]
数组看起来很扁平,但实际上,它是一个树视图结构,在数据网格中表示,如下所示。
我发现部分可行的是遍历整个数组并添加每个项目可以包含的文件的完整列表,例如,如果 Documents 有一个子 PDF,它本身有一个子 Map.pdf,然后树映射可以由 ["Documents", "PDF", "map.pdf"] 表示,我们将其存储在父对象上,然后在下一个子对象上存储 ["PDF", "map.pdf"]最后在我们存储的最后一个孩子上 ["map.pdf"] 像这样
id: 21, file: "Documents", parentId: null, treeMap: ["Documents", "PDF", "map.pdf"]
id: 4, file: "pdf", parentId: 21, treeMap: ["PDF", "map.pdf"]
id: 5, file: "map.pdf", dateModified: "2015-05-21", size: 3.1, parentId: 4, treeMap: ["map.pdf"]
这是允许我这样做的方法
export function modifyDatasetToAddTreeMapping(items: any[], treeViewColumn: Column, dataView: any)
for (let i = 0; i < items.length; i++)
items[i]['treeMap'] = [items[i][treeViewColumn.id]];
let item = items[i];
if (item['parentId'] !== null)
let parent = dataView.getItemById(item['parentId']);
while (parent)
parent['treeMap'] = dedupePrimitiveArray(parent['treeMap'].concat(item['treeMap']));
item = parent;
parent = dataView.getItemById(item['parentId']);
export function dedupePrimitiveArray(inputArray: Array<number | string>): Array<number | string>
const seen = ;
const out = [];
const len = inputArray.length;
let j = 0;
for (let i = 0; i < len; i++)
const item = inputArray[i];
if (seen[item] !== 1)
seen[item] = 1;
out[j++] = item;
return out;
然后datagrid lib使用我可以使用这种方式的Filter方法,其中columnFilters
是一个包含1个或多个过滤器的对象,例如const columnFilters = file: 'map', size: '>3'
数据网格是一个库(SlickGrid),它使用过滤器方法,就像dataView.setFilter(treeFilter);
function treeFilter(dataView: any, item: any)
const columnFilters = file: this.searchString.toLowerCase(), size: 2 ;
let filterCount = 0;
if (item[parentPropName] !== null)
let parent = dataView.getItemById(item['parentId']);
while (parent)
if (parent.__collapsed)
return false;
parent = dataView.getItemById(parent['parentId']);
for (const columnId in columnFilters)
if (columnId !== undefined && columnFilters[columnId] !== '')
filterCount++;
if (item.treeMap === undefined || !item.treeMap.find((itm: string) => itm.endsWith(columnFilters[columnId])))
return false;
return true;
调用modifyDatasetToAddTreeMapping()
如果我想过滤文件列,它可以正常工作,但如果我添加更多列过滤器,它就不能按预期工作。例如,如果您查看第二个打印屏幕,您会看到我输入了“map”,这将显示“Documents > PDF > map.pdf”,这很好,但如果添加的文件大小低于 3Mb,它应该'不显示“map.pdf”,因为该文件没有显示,并且“文档> PDF”不包含“map”这个词,所以什么都不应该显示,所以你可以看到过滤器的行为不正常。
所以对于当前的实现,我有 2 个问题
1. 不显示树节点时行为不正确,不应该显示它的父节点
2. 必须拨打modifyDatasetToAddTreeMapping()
是一个可能不需要的额外电话
3. 它还修改了源数组,我可以对数组进行深度克隆,但这将是另一个性能开销
在转换为层次结构(树)之后,这可能通过递归来实现,但是如果使用递归,我无法找出最好的算法,总是向下钻取树不是很昂贵查找项目?
最后,目的是将它与可能有 10k 甚至 50k 行的 SlickGrid 一起使用,因此它必须快速。你可以看到这个SlickGrid demo但是他们的过滤实现不正确,我也发现了在这个SO Answer中添加映射的方法
注意:我还想指出,解决此问题可能会使数百(或数千)用户受益,因为它将在 Angular-Slickgrid 和 Aurelia-Slickgrid 中实现,它们都是开源库和至少有 300 多名用户使用。
使用“地图”一词过滤不应在此处返回任何内容,因为没有任何节点/子节点具有该文本。
编辑
最好的代码是将完成这项工作的任何代码插入常规 JS filter
,这意味着最终解决方案将是一个方法 myFilter
,这将是一个 filter
回调方法。我坚持这一点的原因是因为我使用了一个外部库SlickGrid,并且我必须使用该库作为公开方法公开的内容。
function myFilter(item, args)
const columnFilters = args.columnFilters;
// iterate through each items of the dataset
// return true/false on each item
// to be used as a drop in
dataView.setFilterArgs( columnFilters: this._columnFilters );
dataView.setFilter(myFilter.bind(this));
如果我有 const columnFilters = file: "map", size: "<3.2" ;
,则数组的预期结果将是 4 行
// result
[
id: 21, file: "Documents", parentId: null ,
id: 4, file: "pdf", parentId: 21, ,
id: 22, file: "map2.pdf", dateModified: "2015-05-21", size: 2.9, parentId: 4 ,
id: 5, file: "map.pdf", dateModified: "2015-05-21", size: 3.1, parentId: 4,
]
如果我有 const columnFilters = file: "map", size: "<3" ;
,则数组的预期结果将是 3 行
// result
[
id: 21, file: "Documents", parentId: null ,
id: 4, file: "pdf", parentId: 21, ,
id: 22, file: "map2.pdf", dateModified: "2015-05-21", size: 2.9, parentId: 4 ,
]
最后,如果我有 const columnFilters = file: "map", size: ">3" ;
,那么预期的结果将是一个空数组,因为没有一个文件具有该字符和文件大小条件。
编辑 2
根据@AlexL 的回答,它开始起作用了。只是一些调整,但它看起来已经很有希望了
编辑 3
感谢 Alex 出色的工作,他的回答帮助我将其合并到我的开源库中。我现在有两个带有Parent/Child ref(平面数据集)和Hierarchical Dataset(树数据集)的现场演示。我希望我可以多次投票:)
【问题讨论】:
如果您将数据存储在具有树结构的复杂实体中,但传递给演示者(网格本身)以呈现核心实体的平面版本怎么办?如果需要,您可以将过滤器函数应用于复杂实体(仅修改平面对象;例如通过 ID 绑定)或直接应用于平面。 是的,datagrid lib 需要一个平面数据集,如果需要,我可以从层次结构传递到平面结构(并且我有方法),但我仍然无法找出最佳算法做过滤器,这主要是我在这个 SO 中寻找的东西......当然,良好的性能,因为数据库可能很大,它是一个公共库,很多用户都提出了各种更大的用例比我的,在我的用例中它可以大约 5k 行 【参考方案1】:我有办法。它应该是相当高效的,但我们可能还想将 map 和 reduce 等换成好的旧 for 循环以进一步优化速度(我看过各种博客和文章比较 forEach、map 等与 for-loop 和 for 的速度-loops 似乎赢了)
这是一个演示(也在这里:https://codepen.io/Alexander9111/pen/abvojzN):
const myFiles = [
id: 11, file: "Music", parentId: null ,
id: 12, file: "mp3", parentId: 11 ,
id: 14, file: "pop", parentId: 12 ,
id: 15, file: "theme.mp3", dateModified: "2015-03-01", size: 85, parentId: 14 ,
id: 16, file: "rock", parentId: 12 ,
id: 17, file: "soft.mp3", dateModified: "2015-05-13", size: 98, parentId: 16 ,
id: 18, file: "else.txt", dateModified: "2015-03-03", size: 90, parentId: null ,
id: 21, file: "Documents", parentId: null ,
id: 2, file: "txt", parentId: 21 ,
id: 3, file: "todo.txt", dateModified: "2015-05-12", size: 0.7, parentId: 2 ,
id: 4, file: "pdf", parentId: 21 ,
id: 22, file: "map2.pdf", dateModified: "2015-05-21", size: 2.9, parentId: 4 ,
id: 5, file: "map.pdf", dateModified: "2015-05-21", size: 3.1, parentId: 4 ,
id: 6, file: "internet-bill.pdf", dateModified: "2015-05-12", size: 1.4, parentId: 4 ,
id: 7, file: "xls", parentId: 21 ,
id: 8, file: "compilation.xls", dateModified: "2014-10-02", size: 2.3, parentId: 7 ,
id: 9, file: "misc", parentId: 21 ,
id: 10, file: "something.txt", dateModified: "2015-02-26", size: 0.4, parentId: 9
];
//example how to use the "<3" string - better way than using eval():
const columnFilters = file: "map", size: "<3.2" ; //, size: "<3"
const isSizeValid = Function("return " + myFiles[11].size + "<3")();
//console.log(isSizeValid);
const myObj = myFiles.reduce((aggObj, child) =>
aggObj[child.id] = child;
//the filtered data is used again as each subsequent letter is typed
//we need to delete the ._used property, otherwise the logic below
//in the while loop (which checks for parents) doesn't work:
delete aggObj[child.id]._used;
return aggObj;
, );
function filterMyFiles(myArray, columnFilters)
const filteredChildren = myArray.filter(a =>
for (let key in columnFilters)
//console.log(key)
if (a.hasOwnProperty(key))
const strContains = String(a[key]).includes(columnFilters[key]);
const re = /(?:(?:^|[-+<>=_*/])(?:\s*-?\d+(\.\d+)?(?:[eE][+-<>=]?\d+)?\s*))+$/;
const comparison = re.test(columnFilters[key]) && Function("return " + a[key] + columnFilters[key])();
if (strContains || comparison)
//don't return true as need to check other keys in columnFilters
else
//console.log('false', a)
return false;
else
return false;
//console.log('true', a)
return true;
)
return filteredChildren;
const initFiltered = filterMyFiles(myFiles, columnFilters);
const finalWithParents = initFiltered.map(child =>
const childWithParents = [child];
let parent = myObj[child.parentId];
while (parent)
//console.log('parent', parent)
parent._used || childWithParents.unshift(parent)
myObj[parent.id]._used = true;
parent = myObj[parent.parentId] || false;
return childWithParents;
).flat();
console.log(finalWithParents)
.as-console-wrapper max-height: 100% !important; top: 0;
基本上是设置一个对象,以便以后用于查找所有父母。
然后进行一次过滤(即数组的一次迭代)并过滤出与 columnFilters 对象中的条件匹配的那些。
然后在这个过滤后的数组上映射(即一次迭代)并使用在开始时创建的对象找到每个父项(因此嵌套迭代最多 N 个深度)。
使用 .flat() 将数组展平(假设最后一次迭代),然后我们就完成了。
如有任何问题,请告诉我。
更新 - For-loop 方法以及尝试减少对数组的迭代
删掉几个迭代:) (https://codepen.io/Alexander9111/pen/MWagdVz):
const myFiles = [
id: 11, file: "Music", parentId: null ,
id: 12, file: "mp3", parentId: 11 ,
id: 14, file: "pop", parentId: 12 ,
id: 15, file: "theme.mp3", dateModified: "2015-03-01", size: 85, parentId: 14 ,
id: 16, file: "rock", parentId: 12 ,
id: 17, file: "soft.mp3", dateModified: "2015-05-13", size: 98, parentId: 16 ,
id: 18, file: "else.txt", dateModified: "2015-03-03", size: 90, parentId: null ,
id: 21, file: "Documents", parentId: null ,
id: 2, file: "txt", parentId: 21 ,
id: 3, file: "todo.txt", dateModified: "2015-05-12", size: 0.7, parentId: 2 ,
id: 4, file: "pdf", parentId: 21 ,
id: 22, file: "map2.pdf", dateModified: "2015-05-21", size: 2.9, parentId: 4 ,
id: 5, file: "map.pdf", dateModified: "2015-05-21", size: 3.1, parentId: 4 ,
id: 6, file: "internet-bill.pdf", dateModified: "2015-05-12", size: 1.4, parentId: 4 ,
id: 7, file: "xls", parentId: 21 ,
id: 8, file: "compilation.xls", dateModified: "2014-10-02", size: 2.3, parentId: 7 ,
id: 9, file: "misc", parentId: 21 ,
id: 10, file: "something.txt", dateModified: "2015-02-26", size: 0.4, parentId: 9
];
const columnFilters = file: "map", size: "<3.2" ;
console.log(customLocalFilter(myFiles, columnFilters));
function customLocalFilter(array, filters)
const myObj = ;
for (let i = 0; i < myFiles.length; i++)
myObj[myFiles[i].id] = myFiles[i];
//the filtered data is used again as each subsequent letter is typed
//we need to delete the ._used property, otherwise the logic below
//in the while loop (which checks for parents) doesn't work:
delete myObj[myFiles[i].id]._used;
const filteredChildrenAndParents = [];
for (let i = 0; i < myFiles.length; i++)
const a = myFiles[i];
let matchFilter = true;
for (let key in columnFilters)
if (a.hasOwnProperty(key))
const strContains = String(a[key]).includes(columnFilters[key]);
const re = /(?:(?:^|[-+<>!=_*/])(?:\s*-?\d+(\.\d+)?(?:[eE][+-<>!=]?\d+)?\s*))+$/;
const comparison =
re.test(columnFilters[key]) &&
Function("return " + a[key] + columnFilters[key])();
if (strContains || comparison)
//don't return true as need to check other keys in columnFilters
else
matchFilter = false;
continue;
else
matchFilter = false;
continue;
if (matchFilter)
const len = filteredChildrenAndParents.length;
filteredChildrenAndParents.splice(len, 0, a);
let parent = myObj[a.parentId] || false;
while (parent)
//only add parent if not already added:
parent._used || filteredChildrenAndParents.splice(len, 0, parent);
//mark each parent as used so not used again:
myObj[parent.id]._used = true;
//try to find parent of the current parent, if exists:
parent = myObj[parent.parentId] || false;
return filteredChildrenAndParents;
.as-console-wrapper max-height: 100% !important; top: 0;
【讨论】:
是的 myObj 是用于稍后在 while 循环中获取父母的对象。我现在还用 for 循环重新编写了它,它应该减少一些内存分配和一些迭代以使其更快等。我也可以把它放到一个函数中,这样你就可以调用它一次,并且更容易与你的库集成。我会尝试看看你的演示,看看如何集成:) 更新了我的问题(滚动到最后)如果有帮助的话..非常感谢 好吧,明天去看看,我的大脑已经完成了这一天:) 谢谢大家,我看到你有一个 GitHub 帐户,所以我拿了你的代码并创建了一个快速演示,你可以从这个 repo 查看/克隆。如果你想直接在那里测试,它就像 d/l repo 并运行 html 文件一样简单,就是这样,SlickGrid 是旧的并且是在 html/js/jquery 中完成的,所以没有设置......我有一些工作但我想谈谈我在自述文件中写的 3 点。示例代码为here。如果你愿意,可以推送代码,我也可以让你成为合作者。 非常感谢,非常棒的工作,竖起大拇指。我希望我能给你更多的积分:)以上是关于如何过滤父子数组的多个属性,这些属性可能是几个树级别的深度的主要内容,如果未能解决你的问题,请参考以下文章