简体   繁体   English

将平面对象数组转换为嵌套对象

[英]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 .我有两个依赖递归的函数: findParentcheckLeftOvers findParent attempts to find the objects parent and returns true or false based on whether it finds it. findParent尝试查找对象父对象,并根据是否找到它返回truefalse 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.这里唯一的复杂性在于如果节点的子节点为空,则不包括countchildren节点。 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 时遇到了一些问题

  1. 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 个孩子

  2. 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.

相关问题 将平面对象数组转换为嵌套对象数组 - Convert flat array of objects into nested array of objects "将平面对象数组转换为嵌套对象" - Convert an array of flat objects to nested objects 将数组的平面数组转换为嵌套对象 - Convert flat array of array into nested objects 将对象的隐蔽平面数组转换为嵌套对象数组 - Covert flat array of objects to array of nested objects 将平面对象数组转换为嵌套对象 - Transform flat array of objects into nested objects 如何将对象的平面数组(可能有多个父对象)转换为嵌套的对象数组 - How to convert flat array of objects (with possibly multiple parents) into a nested array of objects 将平面对象数组转换为嵌套数组 - Converting flat array of objects into nested array 如何将类别和子类别的平面数组转换为嵌套对象数组,其中每个子类别都嵌套在其父项中? - How to convert a flat array of categories and subcategories into an array of nested objects, where each subcategory is nested inside it's parent? 从平面对象数组创建对象的嵌套数组(最多 5 个级别) - Create a nested array of objects (up to 5 levels) from flat array of objects 如何递归地将嵌套对象数组转换为平面对象数组? - How to recursively transform an array of nested objects into array of flat objects?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM