[英]Convert array of flat objects to nested objects
I have the following array (that's actually coming from a backend service):我有以下数组(实际上来自后端服务):
const flat: Item[] = [
{ id: 'a', name: 'Root 1', parentId: null },
{ id: 'b', name: 'Root 2', parentId: null },
{ id: 'c', name: 'Root 3', parentId: null },
{ id: 'a1', name: 'Item 1', parentId: 'a' },
{ id: 'a2', name: 'Item 1', parentId: 'a' },
{ id: 'b1', name: 'Item 1', parentId: 'b' },
{ id: 'b2', name: 'Item 2', parentId: 'b' },
{ id: 'b2-1', name: 'Item 2-1', parentId: 'b2' },
{ id: 'b2-2', name: 'Item 2-2', parentId: 'b2' },
{ id: 'b3', name: 'Item 3', parentId: 'b' },
{ id: 'c1', name: 'Item 1', parentId: 'c' },
{ id: 'c2', name: 'Item 2', parentId: 'c' }
];
where Item
is:其中Item
是:
interface Item {
id: string;
name: string;
parentId: string;
};
In order to be compatible with a component that displays a tree (folder like) view, it needs to be transformed into:为了兼容显示树(类文件夹)视图的组件,需要将其转换为:
const treeData: NestedItem[] = [
{
id: 'a',
name: 'Root 1',
root: true,
count: 2,
children: [
{
id: 'a1',
name: 'Item 1'
},
{
id: 'a2',
name: 'Item 2'
}
]
},
{
id: 'b',
name: 'Root 2',
root: true,
count: 5, // number of all children (direct + children of children)
children: [
{
id: 'b1',
name: 'Item 1'
},
{
id: 'b2',
name: 'Item 2',
count: 2,
children: [
{ id: 'b2-1', name: 'Item 2-1' },
{ id: 'b2-2', name: 'Item 2-2' },
]
},
{
id: 'b3',
name: 'Item 3'
},
]
},
{
id: 'c',
name: 'Root 3',
root: true,
count: 2,
children: [
{
id: 'c1',
name: 'Item 1'
},
{
id: 'c2',
name: 'Item 2'
}
]
}
];
where NestedItem
is:其中NestedItem
是:
interface NestedItem {
id: string;
name: string;
root?: boolean;
count?: number;
children?: NestedItem[];
}
All I've tried so far is something like:到目前为止,我所尝试的只是:
// Get roots first
const roots: NestedItem[] = flat
.filter(item => !item.parentId)
.map((item): NestedItem => {
return { id: item.id, name: item.name, root: true }
});
// Add "children" to those roots
const treeData = roots.map(node => {
const children = flat
.filter(item => item.parentId === node.id)
.map(item => {
return { id: item.id, name: item.name }
});
return {
...node,
children,
count: node.count ? node.count + children.length : children.length
}
});
But this only gets the first level of children, of course (direct children of root nodes).但这当然只能获得第一级子节点(根节点的直接子节点)。 It somehow needs to be recursive, but I have no idea how to accomplish that.它以某种方式需要递归,但我不知道如何实现。
Making no assumptions about the order of the flattened array or how deep a nested object can go:不对扁平数组的顺序或嵌套对象的深度做任何假设:
Array.prototype.reduce
is flexible enough to get this done. Array.prototype.reduce
足够灵活来完成这项工作。 If you are not familiar with Array.prototype.reduce
I recommend reading this .如果您不熟悉Array.prototype.reduce
我建议您阅读本文。 You could accomplish this by doing the following.您可以通过执行以下操作来完成此操作。
I have two functions that rely on recursion here: findParent
and checkLeftOvers
.我有两个依赖递归的函数: findParent
和checkLeftOvers
。 findParent
attempts to find the objects parent and returns true
or false
based on whether it finds it. findParent
尝试查找对象父对象,并根据是否找到它返回true
或false
。 In my reducer I add the current value to the array of left overs if findParent
returns false
.在我的减速器中,如果findParent
返回false
我会将当前值添加到剩余的数组中。 If findParent
returns true
I call checkLeftOvers
to see if any object in my array of left overs is the child of the object findParent
just added.如果findParent
返回true
我会调用checkLeftOvers
来查看我的剩余数组中是否有任何对象是刚刚添加的对象findParent
的子对象。
Note: I added { id: 'b2-2-1', name: 'Item 2-2-1', parentId: 'b2-2'}
to the flat
array to demonstrate that this will go as deep as you'd like.注意:我在flat
数组中添加了{ id: 'b2-2-1', name: 'Item 2-2-1', parentId: 'b2-2'}
以证明这将与您一样深入喜欢。 I also reordered flat
to demonstrate that this will work in that case as well.我还重新排序了flat
以证明这也适用于这种情况。 Hope this helps.希望这可以帮助。
const flat = [ { id: 'a2', name: 'Item 1', parentId: 'a' }, { id: 'b2-2-1', name: 'Item 2-2-1', parentId: 'b2-2'}, { id: 'a1', name: 'Item 1', parentId: 'a' }, { id: 'a', name: 'Root 1', parentId: null }, { id: 'b', name: 'Root 2', parentId: null }, { id: 'c', name: 'Root 3', parentId: null }, { id: 'b1', name: 'Item 1', parentId: 'b' }, { id: 'b2', name: 'Item 2', parentId: 'b' }, { id: 'b2-1', name: 'Item 2-1', parentId: 'b2' }, { id: 'b2-2', name: 'Item 2-2', parentId: 'b2' }, { id: 'b3', name: 'Item 3', parentId: 'b' }, { id: 'c1', name: 'Item 1', parentId: 'c' }, { id: 'c2', name: 'Item 2', parentId: 'c' } ]; function checkLeftOvers(leftOvers, possibleParent){ for (let i = 0; i < leftOvers.length; i++) { if(leftOvers[i].parentId === possibleParent.id) { delete leftOvers[i].parentId possibleParent.children ? possibleParent.children.push(leftOvers[i]) : possibleParent.children = [leftOvers[i]] possibleParent.count = possibleParent.children.length const addedObj = leftOvers.splice(i, 1) checkLeftOvers(leftOvers, addedObj[0]) } } } function findParent(possibleParents, possibleChild) { let found = false for (let i = 0; i < possibleParents.length; i++) { if(possibleParents[i].id === possibleChild.parentId) { found = true delete possibleChild.parentId if(possibleParents[i].children) possibleParents[i].children.push(possibleChild) else possibleParents[i].children = [possibleChild] possibleParents[i].count = possibleParents[i].children.length return true } else if (possibleParents[i].children) found = findParent(possibleParents[i].children, possibleChild) } return found; } const nested = flat.reduce((initial, value, index, original) => { if (value.parentId === null) { if (initial.left.length) checkLeftOvers(initial.left, value) delete value.parentId value.root = true; initial.nested.push(value) } else { let parentFound = findParent(initial.nested, value) if (parentFound) checkLeftOvers(initial.left, value) else initial.left.push(value) } return index < original.length - 1 ? initial : initial.nested }, {nested: [], left: []}) console.log(nested)
Assuming that the flat items array is always sorted like in your case (parents nodes are sorted before children nodes) .假设 flat items 数组总是像你的情况一样排序(父节点在子节点之前排序) 。 The code below should do the work.下面的代码应该可以完成工作。
First, I build the tree without the count
properties using reduce on the array to build a map to keeping a track of every node and linking parents to children:首先,我在没有count
属性的情况下使用数组上的 reduce 来构建树以构建映射以跟踪每个节点并将父节点链接到子节点:
type NestedItemMap = { [nodeId: string]: NestedItem };
let nestedItemMap: NestedItemMap = flat
.reduce((nestedItemMap: NestedItemMap, item: Item): NestedItemMap => {
// Create the nested item
nestedItemMap[item.id] = {
id: item.id,
name: item.name
}
if(item.parentId == null){
// No parent id, it's a root node
nestedItemMap[item.id].root = true;
}
else{
// Child node
let parentItem: NestedItem = nestedItemMap[item.parentId];
if(parentItem.children == undefined){
// First child, create the children array
parentItem.children = [];
parentItem.count = 0;
}
// Add the child node in it's parent children
parentItem.children.push(
nestedItemMap[item.id]
);
parentItem.count++;
}
return nestedItemMap;
}, {});
The fact that the parents node always come first when reducing the array ensures that the parent node is available in the nestedItemMap
when building the children.减少数组时父节点总是首先出现这一事实确保在构建子节点时父节点在nestedItemMap
可用。
Here we have the trees, but without the count
properties:这里我们有树,但没有count
属性:
let roots: NestedItem[] = Object.keys(nestedItemMap)
.map((key: string): NestedItem => nestedItemMap[key])
.filter((item: NestedItem): boolean => item.root);
To have the count
properties filled, I would personally prefer performing a post-order depth-first search on the trees.为了填充count
属性,我个人更喜欢在树上执行后序深度优先搜索。 But in your case, thanks to the node id namings (sorted, the parents nodes ids come first).但是在您的情况下,多亏了节点 id 命名(排序后,父节点 id 排在第一位)。 You can compute them using:您可以使用以下方法计算它们:
let roots: NestedItem[] = Object.keys(nestedItemMap)
.map((key: string): NestedItem => nestedItemMap[key])
.reverse()
.map((item: NestedItem): NestedItem => {
if(item.children != undefined){
item.count = item.children
.map((child: NestedItem): number => {
return 1 + (child.count != undefined ? child.count : 0);
})
.reduce((a, b) => a + b, 0);
}
return item;
})
.filter((item: NestedItem): boolean => item.root)
.reverse();
I just reverse the array to get all children first (like in a post-order DFS), and compute the count
value.我只是反转数组以首先获取所有子项(就像在后序 DFS 中一样),然后计算count
数值。 The last reverse is here just to be sorted like in your question :).最后一个反向在这里只是为了像你的问题一样排序:)。
You could a standard approach for a tree which takes a single loop and stores the relation between child and parent and between parent and child.您可以采用一种标准方法,用于采用单个循环并存储子级与父级以及父级与子级之间的关系的树。
For having root properties you need an additional check.为了拥有根属性,您需要进行额外的检查。
Then take an iterative and recursive approach for getting count.然后采用迭代和递归的方法来获取计数。
var data = [{ id: 'a', name: 'Root 1', parentId: null }, { id: 'b', name: 'Root 2', parentId: null }, { id: 'c', name: 'Root 3', parentId: null }, { id: 'a1', name: 'Item 1', parentId: 'a' }, { id: 'a2', name: 'Item 1', parentId: 'a' }, { id: 'b1', name: 'Item 1', parentId: 'b' }, { id: 'b2', name: 'Item 2', parentId: 'b' }, { id: 'b3', name: 'Item 3', parentId: 'b' }, { id: 'c1', name: 'Item 1', parentId: 'c' }, { id: 'c2', name: 'Item 2', parentId: 'c' }, { id: 'b2-1', name: 'Item 2-1', parentId: 'b2' }, { id: 'b2-2', name: 'Item 2-2', parentId: 'b2' },], tree = function (data, root) { function setCount(object) { return object.children ? (object.count = object.children.reduce((s, o) => s + 1 + setCount(o), 0)) : 0; } var t = {}; data.forEach(o => { Object.assign(t[o.id] = t[o.id] || {}, o); t[o.parentId] = t[o.parentId] || {}; t[o.parentId].children = t[o.parentId].children || []; t[o.parentId].children.push(t[o.id]); if (o.parentId === root) t[o.id].root = true; // extra }); setCount(t[root]); // extra return t[root].children; }(data, null); console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }
maybe this can help you, input is flat obj也许这可以帮助你,输入是平面 obj
nestData = (data, parentId = '') => {
return data.reduce((result, fromData) => {
const obj = Object.assign({}, fromData);
if (parentId === fromData.parent_id) {
const children = this.nestData(data, fromData.id);
if (children.length) {
obj.children = children;
} else {
obj.userData = [];
}
result.push(obj);
}
return result;
}, []);
}; };
If you have this much information in advance, you can build the tree backwards a lot easier.如果您事先有这么多信息,则可以更轻松地向后构建树。 Since you know the shape of the input so well and their relationships are clearly defined you can easily separate this into multiple arrays and build this from the bottom up:由于您非常了解输入的形状并且它们的关系已明确定义,因此您可以轻松地将其分成多个数组并自下而上构建它:
function buildTree(arr: Item[]): NestedItem[] {
/* first split the input into separate arrays based on their nested level */
const roots = arr.filter(r => /^\w{1}$/.test(r.id));
const levelOne = arr.filter(r => /^\w{1}\d{1}$/.test(r.id));
const levelTwo = arr.filter(r => /^\w{1}\d{1}-\d{1}$/.test(r.id));
/* then create the bottom most level based on their relationship to their parent*/
const nested = levelOne.map(item => {
const children = levelTwo.filter(c => c.parentId === item.id);
if (children) {
return {
...item,
count: children.length,
children
};
} else return item;
});
/* and finally do the same with the root items and return the result */
return roots.map(item => {
const children = nested.filter(c => c.parentId === item.id);
if (children) {
return {
...item,
count: children.length,
children,
root: true
};
} else return { ...item, root: true };
});
}
This might not be the most performant solution, and it would need some tweaking depending on the expected shape of the input, but it is a clean and readable solution.这可能不是性能最好的解决方案,它需要根据输入的预期形状进行一些调整,但它是一个干净且可读的解决方案。
Another approach might look like this:另一种方法可能如下所示:
const countKids = (nodes) => nodes.length + nodes.map(({children = []}) => countKids(children)).reduce((a, b) => a + b, 0) const makeForest = (id, xs) => xs .filter (({parentId}) => parentId == id) .map (({id, parentId, ...rest}) => { const kids = makeForest (id, xs) return {id, ...rest, ...(kids .length ? {count: countKids (kids), children: kids} : {})} }) const nest = (flat) => makeForest (null, flat) .map ((node) => ({...node, root: true})) const flat = [{id: "a", name: "Root 1", parentId: null}, {id: "b", name: "Root 2", parentId: null}, {id: "c", name: "Root 3", parentId: null}, {id: "a1", name: "Item 1", parentId: "a"}, {id: "a2", name: "Item 1", parentId: "a"}, {id: "b1", name: "Item 1", parentId: "b"}, {id: "b2", name: "Item 2", parentId: "b"}, {id: "b2-1", name: "Item 2-1", parentId: "b2"}, {id: "b2-2", name: "Item 2-2", parentId: "b2"}, {id: "b3", name: "Item 3", parentId: "b"}, {id: "c1", name: "Item 1", parentId: "c"}, {id: "c2", name: "Item 2", parentId: "c"}] console .log (nest (flat))
.as-console-wrapper {min-height: 100% !important; top: 0}
The main function ( makeForest
) finds all the children whose ids match the target (initially null
) and then recursively does the same with those children's ids.主函数 ( makeForest
) 查找所有 id 与目标匹配(最初为null
)的子项,然后递归地对这些子项的 id 执行相同的操作。
The only complexity here is in not including count
or children
if the children for a node is empty.这里唯一的复杂性在于如果节点的子节点为空,则不包括count
或children
节点。 If including them is not a problem, then this can be simplified.如果包含它们不是问题,那么这可以简化。
this.treeData = this.buildTreeData(
flat.filter(f => !f.parentId), flat
);
private buildTreeData(datagroup: Item[], flat: Item[]): any[] {
return datagroup.map((data) => {
const items = this.buildTreeData(
flat.filter((f) => f.parentId === data.id), flat
);
return {
...data,
root: !data.parentId,
count: items?.length || null
children: items,
};
});
}
Hi i tried the accepted answer by Cody and ran into some problems when data wasn't sorted and for nested data with level>2嗨,我尝试了 Cody 接受的答案,但在数据未排序和嵌套数据级别>2 时遇到了一些问题
in this sandbox: https://codesandbox.io/s/runtime-dew-g48sk?file=/src/index.js:1875-1890 i just changed the order a bit (id=3 was moved to the end of the list), see how in the console we now get that c has only 1 child在这个沙箱中: https : //codesandbox.io/s/runtime-dew-g48sk? file =/ src/index.js: 1875-1890我只是稍微改变了顺序(id=3 被移到了list),看看我们现在如何在控制台中得到 c 只有 1 个孩子
I had another problem where parents couldn't be found, because in findParent
function the found
var was reseted to false if the function was called recursivly with a first argument being an array longer than 1 (eg finding a parent for id=21 in:我遇到了另一个无法找到父对象的问题,因为在findParent
函数中,如果以第一个参数是一个长度超过 1 的数组(例如,在以下位置找到 id=21 的父对象)递归调用该函数,则found
var 将findParent
为 false:
{id: 1,parentId: null, children: [ { id: 10, parentId: 1, children: [] }, { id: 11, parentId: 1, children: [{ id: 21... }] } ]}
would fail会失败
anyway i think the flow itself was good just needed some minor fixes and renames, so here is what's worked for me, I removed some properties that I didn't use (like counter
) and added some of my own (like expanded
) but it obviously shouldn't matter at all, also im using TS (but i changed all my types to any
):无论如何,我认为流程本身很好,只需要一些小的修复和重命名,所以这对我有用,我删除了一些我没有使用的属性(如counter
)并添加了一些我自己的(如expanded
)但它显然根本不重要,我也在使用 TS(但我将所有类型更改为any
):
class NestService { public nestSearchResultsToTree(flatItemsPath: any[]) { const nested = flatItemsPath.reduce( ( initial: { nested: any[]; left: any[] }, value: any, index: number, original: any ) => { if (value.parentId === null) { if (initial.left.length) this.checkLeftOvers(initial.left, value); initial.nested.push(value); } else { const parentFound = this.findParent(initial.nested, value); if (parentFound) this.checkLeftOvers(initial.left, value); else initial.left.push(value); } return index < original.length - 1 ? initial : initial.nested; }, { nested: [], left: [] } ); return nested; } private checkLeftOvers(leftOvers: any[], possibleParent: any) { for (let i = 0; i < leftOvers.length; i++) { const possibleChild = leftOvers[i]; if (possibleChild.id === possibleParent.id) continue; if (possibleChild.parentId === possibleParent.id) { possibleParent.children ? possibleParent.children.push(possibleChild) : (possibleParent.children = [possibleChild]); possibleParent.expanded = true; possibleParent.isFetched = true; this.checkLeftOvers(leftOvers, possibleChild); } } } private findParent( possibleParents: any, child: any, isAlreadyFound?: boolean ): boolean { if (isAlreadyFound) return true; let found = false; for (let i = 0; i < possibleParents.length; i++) { const possibleParent = possibleParents[i]; if (possibleParent.id === child.parentId) { possibleParent.expanded = true; possibleParent.isFetched = true; found = true; if (possibleParent.children) possibleParent.children.push(child); else possibleParent.children = [child]; return true; } else if (possibleParent.children) found = this.findParent(possibleParent.children, child, found); } return found; } }
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.