用于构建类似 object 层次结构的树的递归方法无法按预期工作

Recursive approach for building a tree like object hierarchy does not work as expected

我正在尝试编写类似这样的功能:

输入:

['A', '-B1', '--B2', '-C1', 'D']

输出:

{'A': {'B1': {'B2': {}}, 'C1': {}}, 'D': {}}

如您所见,B1 和 C1 是 A 的 children,B2 是 B1 的 child。它可以根据 -

嵌套到任何级别

我为此编写了一个 Javascript 代码,但在创建 children 时似乎有问题。这是我的代码:

function fetch_levels(li, chr='-') {
    var res_list = []
    for (i=0; i<li.length; i++) {
        item = li[i]
        level = item.length - item.replace(RegExp(`^${chr}+`), '').length
        key = item.replace(RegExp(`^${chr}+`), '')
        value = {}
        res_list.push({
            level: level,
            name: key,
            value: value
        })
    }
    return res_list
}

function ttree_to_json(ttree, level=0) {
    result = {}
    for (i = 0; i < ttree.length; i++) {
        cn = ttree[i]
        nn = ttree[i+1] || {'level': -1}

        // Edge cases
        if (cn['level'] > level) {
            continue
        }
        if (cn['level'] < level) {
            return result
        }

        // Recursion
        if (nn['level'] == level) {
            result[cn['name']] = cn['value']
        } else if (nn['level'] > level) {
            rr = ttree_to_json(ttree.slice(i+1), level=nn['level'])
            result[cn['name']] = rr
        }
        else {
            result[cn['name']] = cn['value']
            return result
        }
    }
    return result
}

调用函数的方式如下:

console.log(ttree_to_json(fetch_levels(['A', '-B1', '--B2', '-C', 'D'])))

我得到的输出是这样的:

Object { B2: {…}, C: {} }

这是错误的。

谁能帮我弄清楚 JavaScript 代码有什么问题?

解决方案的灵感来自提到的示例代码here:

编辑(2022 年 5 月 11 日)

我的错我没有给出实际问题。为了使问题更简短和准确,我给出了不同的数据结构。此处给出的所有答案都适用于上述 DS。但是我无法使我的实际 DS 获得所需的输出。

这里是实际输入:

[
    {
        "componentId": "1067256",
        "componentName": "Readiness",
        "shortHandName": "GTM"
    },
    {
        "componentId": "1067343",
        "componentName": "-Business Planning and Commercialization - BPC",
        "shortHandName": "BPC"
    },
    {
        "componentId": "1068213",
        "componentName": "-SKU Life Cycle Management (SLM)",
        "shortHandName": "SLM"
    },
    {
        "componentId": "1068210",
        "componentName": "--Partner Programs",
        "shortHandName": "Partner"
    },
    {
        "componentId": "1067317",
        "componentName": "--Activation",
        "shortHandName": "Activation"
    },
    {
        "componentId": "1067346",
        "componentName": "Sales Compensation",
        "shortHandName": "Sales Comp"
    }
]

预期输出:

{
    "GTM": {
        "componentId": "1067256",
        "componentName": "Readiness",
        "shortHandName": "GTM",
        "children": {
            "BPC": {
                "componentId": "1067343",
                "componentName": "Business Planning and Commercialization - BPC",
                "shortHandName": "BPC",
                "children": {
                    "Partner": {
                        "componentId": "1068210",
                        "componentName": "Partner Programs",
                        "shortHandName": "Partner",
                        "children": {}
                    },
                    "Activation": {
                        "componentId": "1067317",
                        "componentName": "Activation",
                        "shortHandName": "Activation",
                        "children": {}
                    }
                }
            },
            "SLM": {
                "componentId": "1068213",
                "componentName": "SKU Life Cycle Management (SLM)",
                "shortHandName": "SLM",
                "children": {}
            }
        }
    },
    "Sales Comp": {
        "componentId": "1067346",
        "componentName": "Sales Compensation",
        "shortHandName": "Sales Comp",
        "children": {}
    }
}

解释:

对我来说,很难理解所有的答案,所以可以让它们适用于我提到的新 DS。

您可以通过为嵌套对象的级别引用获取一个数组来简化代码。

任何 属性 都被分配到给定的级别(破折号的数量),并将对象带到下一个级别。

const
    data = ['A', '-B1', '--B2', '-C1', 'D'],
    result = {},
    levels = [result];
    
data.forEach(s => {
    let level = 0;
    while (s[level] === '-') level++;
    s = s.slice(level);
    levels[level][s] = levels[level + 1] = {};
});

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

现实世界问题的解决方案

const
    data = [{ componentId: "1067256", componentName: "Readiness", shortHandName: "GTM" }, { componentId: "1067343", componentName: "-Business Planning and Commercialization - BPC", shortHandName: "BPC" }, { componentId: "1068213", componentName: "-SKU Life Cycle Management (SLM)", shortHandName: "SLM" }, { componentId: "1068210", componentName: "--Partner Programs", shortHandName: "Partner" }, { componentId: "1067317", componentName: "--Activation", shortHandName: "Activation" }, { componentId: "1067346", componentName: "Sales Compensation", shortHandName: "Sales Comp" }],
    getLevelString = (string, level = 0) => string[0] === '-'
        ? getLevelString(string.slice(1), level + 1)
        : { level, string },
    result = data
        .reduce((levels, o) => {
            const { level, string: componentName } = getLevelString(o.componentName);
            levels[level][o.shortHandName] = { ...o, componentName, children: levels[level + 1] = {} };
            return levels;
        }, [{}])
        [0];

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

不需要递归。由于有关要添加的(下一个)嵌套级别的唯一可靠信息是由破折号计数携带的,因此无论如何都可以将下一个级别添加到最近处理的 previous/lower 级别的对象引用中。

因此,以数组形式查找,通过其索引引用每个最近创建的 level/branch 是基于简单循环的解决方案所需的一切。

下一个提供的实现使用基于 reduce 的方法。

function aggregateObjectHierarchyLevel(
  { recentLevels, result = {} },
  item,
  idx,
) {
  // handle/synchronize the initial `result` reference.
  if (idx === 0) {
    recentLevels = [result];
  }
  const [_, dashes = '', key = ''] = item.match(/^([-]*)(.*)/) ?? [];
  const level = dashes.length;

  recentLevels[level + 1] = recentLevels[level][key] = {};

  return { recentLevels, result };
};


console.log(
  ['A', '-B1', '--B2', '-C1', 'D']
    .reduce(aggregateObjectHierarchyLevel, {})
    .result
)
console.log(
  ['A', '-B1', '--B2', '---B3', '-C1', 'D']
    .reduce(aggregateObjectHierarchyLevel, {})
    .result
)
console.log(
  ['A', '-B1', '--B2', '---B3', '--C2', '---C3', '-D1', 'E']
    .reduce(aggregateObjectHierarchyLevel, {})
    .result
)
.as-console-wrapper { min-height: 100%!important; top: 0; }

编辑 ...由于OP的真实世界数据变化

根据我上面的评论...

"@SauravKumar ... from what the OP was presenting before (and the base problem / hierarchy does not change) the expected output of what the OP refers to as "the actual input" does not match the basic building pattern. '--Partner Programs' and '--Activation' have to be the children of '-SKU Life Cycle Management (SLM)' and not of '-Business Planning and Commercialization - BPC'. If the OP wants the expected output, the input array needs to be changed to the two former mentioned child items become direct followers of the latter (parent) item."

update/refactoring从上面的代码到下一个提供的代码只用了 3 分钟,这证明了第一种方法的稳健性。

reducer 函数中的唯一变化是由于必要的对象解构和对象 re/assembling。无需更改任何时候的主要方法。

function aggregateObjectHierarchyLevel(
  { recentLevels, result = {} },
  { shortHandName, componentName: name , ...itemEntriesRest },
  idx,
) {
  // handle/synchronize the initial `result` reference.
  if (idx === 0) {
    recentLevels = [result];
  }
  const [_, dashes = '', componentName = ''] = name.match(/^([-]*)(.*)/) ?? [];
  const level = dashes.length;

  recentLevels[level][shortHandName] = {
    shortHandName,
    componentName,
    ...itemEntriesRest,
    children: recentLevels[level + 1] = {},
  };
  return { recentLevels, result };
};

const data = [
  { componentId: "1067256", componentName: "Readiness", shortHandName: "GTM" },
  { componentId: "1067343", componentName: "-Business Planning and Commercialization - BPC", shortHandName: "BPC" },
  { componentId: "1068213", componentName: "-SKU Life Cycle Management (SLM)", shortHandName: "SLM" },
  { componentId: "1068210", componentName: "--Partner Programs", shortHandName: "Partner" },
  { componentId: "1067317", componentName: "--Activation", shortHandName: "Activation" },
  { componentId: "1067346", componentName: "Sales Compensation", shortHandName: "Sales Comp" }
];

console.log(
  data
    .reduce(aggregateObjectHierarchyLevel, {})
    .result
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

另一个版本,在设计上与 Peter 和 Nina 的非常相似,但编码风格不同,如下所示:

const nest = (strings, result = {}) => strings .reduce (
  (levels, str, _, __, [___, hyphens, val] = str .match (/(\-*)(.+)/), lvl = hyphens .length) => (
    (levels [lvl] [val] = levels [lvl + 1] = {}), 
    levels
  ), [result]
) [0]

console .log (nest (['A', '-B1', '--B2', '-C1', 'D']))

我们使用与这两个答案相同的嵌套结构,并使用与 Peter 类似的正则表达式来分隔字符串的两个部分,以及与这两个答案相同的更新逻辑。这里我们在 .reduce 调用中执行此操作,仅修改隐藏的累加器。

我独立于那些写了这篇文章,回来后发现已经有两个很好的答案,但我认为这已经足够不同了,所以也可以包括在内。 (我还修复了它以窃取 Peter 对正则表达式 match 调用的解构。比我原来的 .slice (1, 3)-then-destructure 方法好多了!)

有趣的是,三种截然不同的编码风格都使用相同的基本设计来解决问题![​​=17=]

更新

此版本处理问题中的更新要求。将来,有了那么大的变化,请简单地开始第二个问题,并包括一个 link 回到原来的上下文。 (您可能仍想这样做,因为新问题会受到更多关注。)

const nest = (input, result = {children: {}}) => input .reduce ((
  levels, 
  {componentId, componentName, shortHandName}, 
   _, __, [___, hyphens, val] = componentName .match (/(\-*)(.+)/), lvl = hyphens .length
) => (
  (levels [lvl] .children [shortHandName] = levels [lvl + 1] = {
    componentId, componentName: val, shortHandName, children: {}
  }), 
  levels
), [result]) [0] .children

const input = [{componentId: "1067256", componentName: "Readiness", shortHandName: "GTM"}, {componentId: "1067343", componentName: "-Business Planning and Commercialization - BPC", shortHandName: "BPC"}, {componentId: "1068213", componentName: "-SKU Life Cycle Management (SLM)", shortHandName: "SLM"}, {componentId: "1068210", componentName: "--Partner Programs", shortHandName: "Partner"}, {componentId: "1067317", componentName: "--Activation", shortHandName: "Activation"}, {componentId: "1067346", componentName: "Sales Compensation", shortHandName: "Sales Comp"}]

console .log (nest (input))
.as-console-wrapper {max-height: 100% !important; top: 0}

请注意,这与您的目标输出不匹配,将 Partner 和 Activation 嵌套在 SLM 中,而不是 BPC 中。但考虑到输入数据,这似乎是正确的。如果您想切换它,那么 SL​​M 必须在输入数组中稍后出现。

关于设计就不用多说了。它与上面的设计完全相同,只是有更多的字段,包括嵌套的 children 一个。