返回

扁平数据转层级树:高效算法与Typescript实现

javascript

扁平数据到层级树的转换

当处理包含层级关系的数据时,将其从扁平数组转换为层级树结构是一项常见的任务。这类问题常常出现在数据可视化、组织结构展示等应用场景中。 本文讨论一种使用包含枚举类型层级信息字段将扁平数据转换为层级树的通用方法。

问题

假设存在一个包含账户余额的扁平数组。每条余额记录包含一个 codes 字段,它是一个由枚举值标识层级的 Code 对象数组。目标是将此扁平数组转换为层级树,其中每个节点对应一个唯一的 Code , 并将属于该层级的所有 amountOut 值相加,存储在该节点的 amountOut 属性中。 节点的子节点放在 subAccounts 数组中。

这种转换的本质就是将一个线性的数据结构,根据codes 字段的层级信息,重新组织为嵌套的树状结构。 关键在于如何高效地根据 codeType 字段对 accountBalances 进行分组、归纳。

解决方案

针对此类问题,可以使用递归的方式构建树结构,同时,为优化性能,可引入辅助数据结构例如 Map 对象来存储已经访问过的节点,防止重复计算和提高查找效率。整体方案可归纳为以下步骤:

  1. 初始化根节点 : 创建一个空的 Map 作为缓存节点的数据结构。
  2. 遍历数据 : 遍历扁平的 accountBalances 数组。
  3. 构建节点 : 对于每一条账户余额记录,根据其codes 列表递归地创建对应的树节点,同时在创建的过程中,使用 Map 对象来缓存已经构建好的节点信息。
  4. 更新金额 : 在每个节点中,累加当前 AccountBalanceamountOut 属性值,得到正确的当前节点对应的 amountOut 总值。
  5. 返回结果 : 最终返回根节点树状结构。

代码示例

以下是一个使用 Typescript 实现该转换过程的函数。

interface AccountBalance {
    id: string;
    codes: Code[];
    amountOut: number;
}

type Code = {
    id: string;
    codeType: CodeTypes;
    code: string;
    description: string;
};

enum CodeTypes {
    account = "account",
    responsible = "responsible",
    project = "project",
    object = "object",
    counterPart = "counterPart",
    free = "free",
}

type AccountTree = {
    id: string;
    name: string;
    code: string;
    amountOut: number;
    subAccounts: AccountTree[];
};


function transformToAccountTree(accountBalances: AccountBalance[]): AccountTree | null {

    const cache = new Map<string, AccountTree>();
    let root: AccountTree | null = null;

    accountBalances.forEach(balance => {
        let currentLevelSubAccounts =  (root && root.subAccounts) || []; // 初始化根节点下的一级子节点集合

        for(const code of balance.codes) {
           const  nodeId = code.id;
           let existingNode = cache.get(nodeId); // 查找当前节点是否已经创建

           if (!existingNode) {

                // 首次构建此节点
               existingNode = {
                   id: code.id,
                   name: code.description,
                   code: code.code,
                   amountOut: 0,
                   subAccounts: []
                }

                cache.set(nodeId, existingNode)

               //  寻找其父节点并进行连接
                if (currentLevelSubAccounts) {
                    currentLevelSubAccounts.push(existingNode)
                }
                
           }


           existingNode.amountOut += balance.amountOut; // 累加当前节点余额

            if(!root){
              // 假设代码中默认第一个枚举就是 root, 或者使用函数参数传递根节点枚举类型
                if(code.codeType ===  CodeTypes.account) {
                    root = existingNode
                 }

            }


           currentLevelSubAccounts = existingNode.subAccounts // 下一个子层级的 subAccount
        }
    })

    return root;

}

const accountBalances: AccountBalance[] = [
            {
                id: "671769fbd36fcd6c2c7f2d9b",
                codes: [
                    {
                        id: "671769fbd36fcd6c2c7f2c2d",
                        codeType: CodeTypes.account,
                        code: "1250",
                        description: "Column A",
                    },
                    {
                        id: "671769fbd36fcd6c2c7f2bd5",
                        codeType: CodeTypes.responsible,
                        code: "17",
                        description: "Column B",
                    },
                    {
                        id: "671769fbd36fcd6c2c7f2bf7",
                        codeType: CodeTypes.counterPart,
                        code: "20",
                        description: "Column C",
                    },
                ],
                amountOut: 24510549,
            },
            {
                id: "671769fbd36fcd6c2c7f2d9c",
                codes: [
                    {
                        id: "671769fbd36fcd6c2c7f2c2d",
                        codeType: CodeTypes.account,
                        code: "1250",
                        description: "Column A",
                    },
                    {
                        id: "671769fbd36fcd6c2c7f2bee",
                        codeType: CodeTypes.responsible,
                        code: "40",
                        description: "Column B",
                    },
                    {
                        id: "671769fbd36fcd6c2c7f2c08",
                        codeType: CodeTypes.counterPart,
                        code: "S3",
                        description: "Column C",
                    },
                ],
                amountOut: 0,
            },
            {
                id: "671769fbd36fcd6c2c7f2d9d",
                codes: [
                    {
                        id: "671769fbd36fcd6c2c7f2c2d",
                        codeType: CodeTypes.account,
                        code: "1250",
                        description: "Column A",
                    },
                    {
                        id: "671769fbd36fcd6c2c7f2bdb",
                        codeType: CodeTypes.responsible,
                        code: "80",
                        description: "Column B",
                    },
                    {
                        id: "671769fbd36fcd6c2c7f2bdc",
                        codeType: CodeTypes.counterPart,
                        code: "52",
                        description: "Column C",
                    },
                ],
                amountOut: 6381398,
            },
        ];


const tree = transformToAccountTree(accountBalances);

console.log(JSON.stringify(tree, null, 4));

安全建议

  • 在实际应用中,要进行错误处理,例如确保 codes 字段存在,并有相应的 codeType 以及 description 值。
  • 对数据中的特殊字符和空值进行安全校验和处理。
  • 可以进一步使用参数控制层级的起始,提高代码的复用性。

总结

使用递归和缓存的方法,可以高效且清晰地将扁平的、包含层级信息的数据转换为层级树。这种方式不仅代码简洁,也具有较好的可读性和可维护性,适用与大部分的此类层级转换问题。