使用 lodash 将数组分组为树形“子”结构

Use lodash to group array into tree “children” structure

我正在尝试使用下面的 json 个对象数组创建一棵树。我想将一个类别设置为另一个类别的子项,如果它 sub_category_1 并且我希望 sub_category_2 也是该 sub_category_1

的子项
[
  { 
    category: 'CS',
    sub_category_1: null,
    sub_category_2: null
  }, {
    category: 'TS',
    sub_category_1: null,
    sub_category_2: null
  }, {
    category: 'CS',
    sub_category_1: 'Accuracy',
    sub_category_2: null
  }, {
    category: 'CS',
    sub_category_1: 'Accuracy',
    sub_category_2: 'Members Accuracy'
  }
]

我尝试链接 lodash 方法,如 groupBy 和转换,但很难获得我需要的结果格式。

这是我前进方向的框架:

_(arr).groupBy('category').transform(function(result, obj, type) {
 return result.push({
    name: type,
    children: obj
 });
}).value();

预期输出:

[{
    category: 'CS',
        children: [
            {
                category: 'Accuracy',
                children: [
                    {
                        category: 'Members Accuracy'
                    }
                    ...
                ]
            }
            ...
        ]
    }, {
    category: 'TS'
 }]

我同意最好的解决方案是将基础数据源重构为 return 更有用的结果。

但除非您可以使用直接的嵌套 reduce() 调用。

我正在对每个顶级类别的子键进行排序以避免依赖对象 属性 排序,并假设每个元素将以顶级 category.

const
  input = [{ category: 'CS', sub_category_1: null, sub_category_2: null }, { category: 'TS', sub_category_1: null, sub_category_2: null }, { category: 'CS', sub_category_1: 'Accuracy', sub_category_2: null }, { category: 'CS', sub_category_1: 'Accuracy', sub_category_2: 'Members Accuracy' }],

  tree = Object.values(
    input.reduce((acc, { category, ...children }) => {
      acc[category] ??= { category, children: [] };

      // sort child keys so as to not rely on object property ordering
      const subcategories = Object.entries(children).sort(([a], [b]) => a.localeCompare(b));

      // reduce child keys into category
      subcategories.reduce((_acc, [, subcategory]) => {
        if (subcategory === null) return _acc;

        let sub = _acc.children.find(({ category }) => category === subcategory);
        if (!sub) {
          sub = { category: subcategory, children: [] };
          _acc.children.push(sub);
        }
        return sub
      }, acc[category]);

      return acc;
    }, {})
  );


console.log(JSON.stringify(tree, null, 2));
.as-console-wrapper { max-height: 100% !important; top: 0; }

如果您确定类别描述的顺序正确(主要,然后是子类别,然后是子子类别),您可以使用 vanilla JavaScript 来构建树:

// data constructor for a category
const Category = (category, children = []) =>
  ({ category, children });

// 1. Ensure only the `sub_category_n` values are taken and
// 2. they are in ascending order and
// 3. any null values are removed
const subCategories = obj => 
  Object.entries(obj)
    .filter(([key]) => /sub_category_\d+/.test(key))
    .filter(([, value]) => value != null)
    .sort(([keyA], [keyB]) => keyA.localeCompare(keyB, {numeric: true}))
    .map(([, subCategoryName]) => subCategoryName);

// take a category descriptor and turn it into full path of category names:
// { category: "X", sub_category_1: "Y", sub_category_1: "Z" }
// turns into ["X", "Y", "Z"]
const toPath = ({category, ...sub}) =>
  [category, ...subCategories(sub)];

// create a key from the path elements separated by Unit Separator characters
// or return a unique symbol for no parent
const toKey = (path) =>
  path.join("\u241F") || Symbol("no parent");
  
const toHierarchy = arr => {
  const result = [];
  //keep track of categories that have been created
  const seen = new Map();
    
  for (const item of arr) {
    const path = toPath(item);
    //last item in the path is what we want to create
    const childName = path[path.length-1];
    
    //parent key is the path without the last item
    const parentKey = toKey(path.slice(0, -1));
    //the child key is the full path
    const childKey = toKey(path)
    
    //skip if it's seen
    if (seen.has(childKey))
      continue;
      
    const child = Category(childName);
    seen.set(childKey, child);

    //if there is no parent, add as a main category. Otherwise as a child
    const parentList = seen.get(parentKey)?.children ?? result;
    parentList.push(child);
  }
  
  return result;
}


const input = [
  { 
    category: 'CS',
    sub_category_1: null,
    sub_category_2: null
  }, {
    category: 'TS',
    sub_category_1: null,
    sub_category_2: null
  }, {
    category: 'CS',
    sub_category_1: 'Accuracy',
    sub_category_2: null
  }, {
    category: 'CS',
    sub_category_1: 'Accuracy',
    sub_category_2: 'Members Accuracy'
  }
];

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

如果类别列表可能是无序的,您可以分析整个路径并创建任何缺失的类别,而不是每个对象一个:

const Category = (category, children = []) =>
  ({ category, children });

const subCategories = obj => 
  Object.entries(obj)
    .filter(([key]) => /sub_category_\d+/.test(key))
    .filter(([, value]) => value != null)
    .sort(([keyA], [keyB]) => keyA.localeCompare(keyB, {numeric: true}))
    .map(([, subCategoryName]) => subCategoryName);
    
const toPath = ({category, ...sub}) =>
  [category, ...subCategories(sub)];
  
const toKey = (path, key = []) =>
  path.concat(key).join("\u241F") || Symbol("no parent");

const toHierarchy = arr => {
  const result = [];
  const seen = new Map()
    
  for (const item of arr) {
    const path = toPath(item);
    for (const [index, childName] of path.entries()) {
      const parentKey = toKey(path.slice(0, index));
      const childKey = toKey(path.slice(0, index+1));
      if (!seen.has(childKey)) {
        const child = Category(childName);
        seen.set(childKey, child);
        
        const parentList = seen.get(parentKey)?.children ?? result;
        parentList.push(child);
      }
    }
  }
  
  return result;
}


const input = [
  {
    category: 'CS',
    sub_category_1: 'Accuracy',
    sub_category_2: null
  }, { 
    category: 'CS',
    sub_category_1: null,
    sub_category_2: null
  }, {
    category: 'TS',
    sub_category_1: null,
    sub_category_2: null
  }, {
    category: 'CS',
    sub_category_1: 'Accuracy',
    sub_category_2: 'Members Accuracy'
  }
];

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