使用 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; }
我正在尝试使用下面的 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; }