具有特定结构的 Ramda 分组
Ramda group by with specific structure
我正在尝试用 Ramda 对一些元素进行分组,并用它构建一个简单的嵌套菜单。我从后端得到这样的结构:
const testArray = [
{
"id":6,
"type":{
"name":"Test1",
"category":"Cat A"
},
"typeName":"Test1",
"categoryName":"Cat A"
},
{
"id":34,
"type":{
"name":"Test2",
"category":"Cat A"
},
"typeName":"Test2",
"categoryName":"Cat A"
},
{
"id":662,
"type":{
"name":"Test6",
"category":null
},
"typeName":"Test6",
"categoryName":null
},
{
"id":62,
"type":{
"name":"Test7",
"category":"Cat A"
},
"typeName":"Test7",
"categoryName":"Cat A"
},
{
"id":1190,
"type":{
"name":"Test8",
"category":null
},
"typeName":"Test8",
"categoryName":null
},
{
"id":"other",
"type":{
"name":"Others",
"seen":true
},
"typeName":"Others"
}
];
当我尝试使用以下方式对其进行分组时:
const testRamda = R.groupBy(R.prop('categoryName'));
我得到带有组的结果对象:Cat A,null,undefined 因为 'categoryName' 包含名称,null 或什么都没有,所以它是未定义的。
这就是我想要实现的目标。结构按字母顺序排序,"Others" 始终作为最后一个选项。
[
{
"name": "Cat A",
"category": "Cat A",
"children": [
{
"name": "Test1"
},
{
"name": "Test2"
},
{
"name": "Test7",
},
]
},
{
"name": "Test6",
"category": null
},
{
"name": "Test8",
"category": null
},
{
"name": "Others"
}
]
我将不胜感激任何帮助
在我看来,您希望做三件不同的事情。您想要按类别对元素进行分组。您想要对类别进行排序,使没有类别的类别排在最后,而带有 null
的类别排在最前面。并且您想要转换将类别分组为单个 object 的元素,并将其他元素分开。
这导致了一个有点奇怪的转变。但这并不奇怪,因为您的输入结构和输出结构都有些奇怪。
这是几乎您所问的一种方法:
const compare = ([a], [b]) =>
a == 'undefined' ? (b == 'undefined' ? '0' : 1) : b == 'undefined' ? -1
: a == 'null' ? (b == 'null' ? 0 : 1) : b == 'null' ? -1
: a < b ? -1 : a > b ? 1 : 0
const makeCat = ([key, nodes]) =>
key == 'null' || key == 'undefined'
? nodes .map (node => node .type)
: [{name: key, category: key, children: nodes .map (({type: {name}}) => ({name}))}]
const transform = pipe (
groupBy (prop ('categoryName')),
toPairs,
sort (compare),
chain (makeCat)
)
// changing this to demonstrate proper grouping.
// const testArray = [{id: 6, type: {name: "Test1", category: "Cat A"}, typeName: "Test1", categoryName: "Cat A"}, {id: 34, type: {name: "Test2", category: "Cat A"}, typeName: "Test2", categoryName: "Cat A"}, {id: 662, type: {name: "Test6", category: null}, typeName: "Test6", categoryName: null}, {id: 62, type: {name: "Test7", category: "Cat A"}, typeName: "Test7", categoryName: "Cat A"}, {id: 1190, type: {name: "Test8", category: null}, typeName: "Test8", categoryName: null}, {id: "other", type: {name: "Others", seen: true}, typeName: "Others"}];
const testArray = [{id: 662, type: {name: "Test6", category: null}, typeName: "Test6", categoryName: null}, {id: 6, type: {name: "Test1", category: "Cat A"}, typeName: "Test1", categoryName: "Cat A"}, {id: 34, type: {name: "Test2", category: "Cat A"}, typeName: "Test2", categoryName: "Cat A"}, {id: 62, type: {name: "Test7", category: "Cat A"}, typeName: "Test7", categoryName: "Cat A"}, {id: 1190, type: {name: "Test8", category: null}, typeName: "Test8", categoryName: null}, {id: "other", type: {name: "Others", seen: true}, typeName: "Others"}];
console .log (
transform (testArray)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
<script> const {pipe, groupBy, prop, toPairs, sort, chain} = R </script>
compare
用于排序。它将 undefined
个类别放在最后,null
个类别放在前面,其余的按自然排序。 (有一个很好的论据来制作一个更具声明性的版本,但我认为这是一个单独的问题。)
makeCat
从原始列表中获取类别名称和节点列表,并创建输出节点数组。它将 null
/undefined
案例与真正的类别案例分开处理。当存在真实类别时,它会创建一个包含 single-element 和 name
、category
和 children
属性的数组。当类别为 null
/undefined
时,我们只需从每个 child 中提取 type
属性,并 return 它们的数组。如果略有不同的输出不适合您,您可能希望更改以下内容。
transform
将所有这些在管道中滚动在一起,首先按 categoryName
属性 对元素进行分组, 然后将结果 object 转换为 key
-value
对的数组, 用 compare
对结果进行排序, 然后调用 chain
(类似于 Array.prototype.flatMap
在这种情况下)与结果数组上的 makeCat
。
我从您更新的输出请求中注意到的唯一行为差异是这包括最后一个节点中的 seen
属性。这是因为我们只是重用了项目的 type
。如果你想要一些不同的过程,那么你可以简单地用更合适的东西替换 node => node .type
。
为了扩展 Scott 的回答,我们将制作一个 Comparison
小模块。但在我们陷入实施泥潭之前,我首先要建立对它应该如何运作的期望。我们首先写两个独立的比较 -
const { empty, contramap, concat } =
Comparison
const hasUndefinedCategory =
contramap(empty, x => x.category === undefined)
const hasNullCategory =
contramap(empty, x => x.category === null)
接下来我们展示如何组合比较 -
// primary sort: hasUndefinedCategory
// secondary sort: hasNullCategory
arr.sort(concat(hasUndefinedCategory, hasNullCategory))
Comparison
应该遵守幺半群函数法则,因此我们可以期望组合任意数量的比较。 N 排序的工作原理如下 -
// primary sort: hasUndefinedCategory
// secondary sort: hasNullCategory
// tertiary sort: ...
arr.sort([ hasUndefinedCategory, hasNullCategory, ... ].reduce(concat, empty))
对于 children
的条目,您可以使用 sortByName
-
进行排序
const sortByName =
contramap(empty, x => x.name)
arr.forEach(({ children = [] }) => children.sort(sortByName))
最后,实现Comparison
模块-
const Comparison =
{ empty: (a, b) =>
a < b ? -1
: a > b ? 1
: 0
, contramap: (m, f) =>
(a, b) => m(f(a), f(b))
, concat: (m, n) =>
(a, b) => Ordered.concat(m(a, b), n(a, b))
}
这依赖于实现 Ordered
模块 -
const Ordered =
{ empty: 0
, concat: (a, b) =>
a === 0 ? b : a
}
现在你已经完成了,你可以返回并支持 reverse
排序之类的东西 -
const Comparison =
{ // ...
, reverse: (m) =>
(a, b) => m(b, a)
}
const { empty, contramap, concat, reverse } =
Comparison
// N-sort
// first: hasUndefinedCategory in descending order
// second: hasNullCategory in ascending order (default)
// third: ...
const complexSort =
[ reverse(hasUndefinedCategory)
, hasNullCategory
, ...
].reduce(concat, empty)
arr.sort(complexSort)
const {ascend, apply, applySpec, complement, compose, concat, descend, groupBy, has, head, last, map, partition, pick, pipe, prop, sortWith, toPairs, useWith} = R;
const testArray = [{"id":1192,"type":{"name":"Z1","category":null},"typeName":"Z1","categoryName":null},{"id":1191,"type":{"name":"A1","category":null},"typeName":"A1","categoryName":null},{"id":6,"type":{"name":"Test1","category":"Cat A"},"typeName":"Test1","categoryName":"Cat A"},{"id":34,"type":{"name":"Test2","category":"Cat A"},"typeName":"Test2","categoryName":"Cat A"},{"id":662,"type":{"name":"Test6","category":null},"typeName":"Test6","categoryName":null},{"id":62,"type":{"name":"Test7","category":"Cat A"},"typeName":"Test7","categoryName":"Cat A"},{"id":1190,"type":{"name":"Test8","category":null},"typeName":"Test8","categoryName":null},{"id":"other","type":{"name":"Others","seen":true},"typeName":"Others"}];
const withCategoryTransformation = pipe(
groupBy(prop("categoryName")),
toPairs,
map(applySpec({
name: head,
category: head,
children: pipe(last, map(compose(pick(["name"]), prop("type")))),
})),
);
const withoutCategoryTransformation =
map(compose(pick(["name", "category"]), prop("type")));
const sortCriteria = [
descend(has("category")), // presence of "category" property
ascend(complement(prop("category"))), // truthiness of "category" value
ascend(prop("name")), // "name" value
];
const toMenuStructure = pipe(
partition(prop("categoryName")),
apply(useWith(concat, [withCategoryTransformation, withoutCategoryTransformation])),
sortWith(sortCriteria),
);
console.log(toMenuStructure(testArray));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.min.js"></script>
我首先将集合分成两组。那些有类别名称的,那些没有类别名称的。 (基于 "categoryName" 值的真实性。)
对有类别名称的集合和没有类别名称的集合执行不同的操作,连接结果值。
对类别名为:
的组执行以下操作
首先将类别名称按 "categoryName" 属性 分组。
将 key/value-pairs 转换回列表。
转换每个元素,使用组键 (head
) 作为 "name" 和 "category" 值。然后为每个项目 (last
) 选择 "type" 值的 "name" 属性 并将其用作 "children".
// from
[
"Cat A",
[
{
id: 34,
type: {name: "Test1", category: "Cat A"},
typeName: "Test1",
categoryName: "Cat A"
},
/* ... */
]
]
// to
{name: "Cat A", category: "Cat A", children: [{name: "Test1"}, /* ... */]}
没有分类的组只进行一次操作:
- 为每个项目选择 "type" 值的 "name" 和 "category" 属性。 (删除 "seen" 属性。)
然后我们按以下对所有对象进行排序:类别值的 "category" 属性、truthiness/falseness 的存在,最后是 [= 的值69=]属性.
我选择通过检查 "category" 属性 的存在来将 "Others" 排序到最后。如果你更愿意专门过滤掉名字 "Others" 你可以
// replace
descend(has("category"))
// with
ascend(propEq("name", "Others"))
根据偏好,您还可以选择 withCategoryTransformation
的替代版本,这里有两个用于 pipe
中最后一个操作的替代版本:
// Non point-free, but uses a more native JavaScript style.
map(([category, items]) => ({
name: category,
category: category,
children: items.map(compose(pick(["name"]), prop("type"))),
}))
// A point-free version that executes separate operations for the category
// and items, then merges the results together.
map(apply(useWith(mergeLeft, [
applySpec({ name: identity, category: identity }),
applySpec({ children: map(compose(pick(["name"]), prop("type"))) }),
])))
我正在尝试用 Ramda 对一些元素进行分组,并用它构建一个简单的嵌套菜单。我从后端得到这样的结构:
const testArray = [
{
"id":6,
"type":{
"name":"Test1",
"category":"Cat A"
},
"typeName":"Test1",
"categoryName":"Cat A"
},
{
"id":34,
"type":{
"name":"Test2",
"category":"Cat A"
},
"typeName":"Test2",
"categoryName":"Cat A"
},
{
"id":662,
"type":{
"name":"Test6",
"category":null
},
"typeName":"Test6",
"categoryName":null
},
{
"id":62,
"type":{
"name":"Test7",
"category":"Cat A"
},
"typeName":"Test7",
"categoryName":"Cat A"
},
{
"id":1190,
"type":{
"name":"Test8",
"category":null
},
"typeName":"Test8",
"categoryName":null
},
{
"id":"other",
"type":{
"name":"Others",
"seen":true
},
"typeName":"Others"
}
];
当我尝试使用以下方式对其进行分组时:
const testRamda = R.groupBy(R.prop('categoryName'));
我得到带有组的结果对象:Cat A,null,undefined 因为 'categoryName' 包含名称,null 或什么都没有,所以它是未定义的。
这就是我想要实现的目标。结构按字母顺序排序,"Others" 始终作为最后一个选项。
[
{
"name": "Cat A",
"category": "Cat A",
"children": [
{
"name": "Test1"
},
{
"name": "Test2"
},
{
"name": "Test7",
},
]
},
{
"name": "Test6",
"category": null
},
{
"name": "Test8",
"category": null
},
{
"name": "Others"
}
]
我将不胜感激任何帮助
在我看来,您希望做三件不同的事情。您想要按类别对元素进行分组。您想要对类别进行排序,使没有类别的类别排在最后,而带有 null
的类别排在最前面。并且您想要转换将类别分组为单个 object 的元素,并将其他元素分开。
这导致了一个有点奇怪的转变。但这并不奇怪,因为您的输入结构和输出结构都有些奇怪。
这是几乎您所问的一种方法:
const compare = ([a], [b]) =>
a == 'undefined' ? (b == 'undefined' ? '0' : 1) : b == 'undefined' ? -1
: a == 'null' ? (b == 'null' ? 0 : 1) : b == 'null' ? -1
: a < b ? -1 : a > b ? 1 : 0
const makeCat = ([key, nodes]) =>
key == 'null' || key == 'undefined'
? nodes .map (node => node .type)
: [{name: key, category: key, children: nodes .map (({type: {name}}) => ({name}))}]
const transform = pipe (
groupBy (prop ('categoryName')),
toPairs,
sort (compare),
chain (makeCat)
)
// changing this to demonstrate proper grouping.
// const testArray = [{id: 6, type: {name: "Test1", category: "Cat A"}, typeName: "Test1", categoryName: "Cat A"}, {id: 34, type: {name: "Test2", category: "Cat A"}, typeName: "Test2", categoryName: "Cat A"}, {id: 662, type: {name: "Test6", category: null}, typeName: "Test6", categoryName: null}, {id: 62, type: {name: "Test7", category: "Cat A"}, typeName: "Test7", categoryName: "Cat A"}, {id: 1190, type: {name: "Test8", category: null}, typeName: "Test8", categoryName: null}, {id: "other", type: {name: "Others", seen: true}, typeName: "Others"}];
const testArray = [{id: 662, type: {name: "Test6", category: null}, typeName: "Test6", categoryName: null}, {id: 6, type: {name: "Test1", category: "Cat A"}, typeName: "Test1", categoryName: "Cat A"}, {id: 34, type: {name: "Test2", category: "Cat A"}, typeName: "Test2", categoryName: "Cat A"}, {id: 62, type: {name: "Test7", category: "Cat A"}, typeName: "Test7", categoryName: "Cat A"}, {id: 1190, type: {name: "Test8", category: null}, typeName: "Test8", categoryName: null}, {id: "other", type: {name: "Others", seen: true}, typeName: "Others"}];
console .log (
transform (testArray)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
<script> const {pipe, groupBy, prop, toPairs, sort, chain} = R </script>
compare
用于排序。它将undefined
个类别放在最后,null
个类别放在前面,其余的按自然排序。 (有一个很好的论据来制作一个更具声明性的版本,但我认为这是一个单独的问题。)makeCat
从原始列表中获取类别名称和节点列表,并创建输出节点数组。它将null
/undefined
案例与真正的类别案例分开处理。当存在真实类别时,它会创建一个包含 single-element 和name
、category
和children
属性的数组。当类别为null
/undefined
时,我们只需从每个 child 中提取type
属性,并 return 它们的数组。如果略有不同的输出不适合您,您可能希望更改以下内容。transform
将所有这些在管道中滚动在一起,首先按categoryName
属性 对元素进行分组, 然后将结果 object 转换为key
-value
对的数组, 用compare
对结果进行排序, 然后调用chain
(类似于Array.prototype.flatMap
在这种情况下)与结果数组上的makeCat
。
我从您更新的输出请求中注意到的唯一行为差异是这包括最后一个节点中的 seen
属性。这是因为我们只是重用了项目的 type
。如果你想要一些不同的过程,那么你可以简单地用更合适的东西替换 node => node .type
。
为了扩展 Scott 的回答,我们将制作一个 Comparison
小模块。但在我们陷入实施泥潭之前,我首先要建立对它应该如何运作的期望。我们首先写两个独立的比较 -
const { empty, contramap, concat } =
Comparison
const hasUndefinedCategory =
contramap(empty, x => x.category === undefined)
const hasNullCategory =
contramap(empty, x => x.category === null)
接下来我们展示如何组合比较 -
// primary sort: hasUndefinedCategory
// secondary sort: hasNullCategory
arr.sort(concat(hasUndefinedCategory, hasNullCategory))
Comparison
应该遵守幺半群函数法则,因此我们可以期望组合任意数量的比较。 N 排序的工作原理如下 -
// primary sort: hasUndefinedCategory
// secondary sort: hasNullCategory
// tertiary sort: ...
arr.sort([ hasUndefinedCategory, hasNullCategory, ... ].reduce(concat, empty))
对于 children
的条目,您可以使用 sortByName
-
const sortByName =
contramap(empty, x => x.name)
arr.forEach(({ children = [] }) => children.sort(sortByName))
最后,实现Comparison
模块-
const Comparison =
{ empty: (a, b) =>
a < b ? -1
: a > b ? 1
: 0
, contramap: (m, f) =>
(a, b) => m(f(a), f(b))
, concat: (m, n) =>
(a, b) => Ordered.concat(m(a, b), n(a, b))
}
这依赖于实现 Ordered
模块 -
const Ordered =
{ empty: 0
, concat: (a, b) =>
a === 0 ? b : a
}
现在你已经完成了,你可以返回并支持 reverse
排序之类的东西 -
const Comparison =
{ // ...
, reverse: (m) =>
(a, b) => m(b, a)
}
const { empty, contramap, concat, reverse } =
Comparison
// N-sort
// first: hasUndefinedCategory in descending order
// second: hasNullCategory in ascending order (default)
// third: ...
const complexSort =
[ reverse(hasUndefinedCategory)
, hasNullCategory
, ...
].reduce(concat, empty)
arr.sort(complexSort)
const {ascend, apply, applySpec, complement, compose, concat, descend, groupBy, has, head, last, map, partition, pick, pipe, prop, sortWith, toPairs, useWith} = R;
const testArray = [{"id":1192,"type":{"name":"Z1","category":null},"typeName":"Z1","categoryName":null},{"id":1191,"type":{"name":"A1","category":null},"typeName":"A1","categoryName":null},{"id":6,"type":{"name":"Test1","category":"Cat A"},"typeName":"Test1","categoryName":"Cat A"},{"id":34,"type":{"name":"Test2","category":"Cat A"},"typeName":"Test2","categoryName":"Cat A"},{"id":662,"type":{"name":"Test6","category":null},"typeName":"Test6","categoryName":null},{"id":62,"type":{"name":"Test7","category":"Cat A"},"typeName":"Test7","categoryName":"Cat A"},{"id":1190,"type":{"name":"Test8","category":null},"typeName":"Test8","categoryName":null},{"id":"other","type":{"name":"Others","seen":true},"typeName":"Others"}];
const withCategoryTransformation = pipe(
groupBy(prop("categoryName")),
toPairs,
map(applySpec({
name: head,
category: head,
children: pipe(last, map(compose(pick(["name"]), prop("type")))),
})),
);
const withoutCategoryTransformation =
map(compose(pick(["name", "category"]), prop("type")));
const sortCriteria = [
descend(has("category")), // presence of "category" property
ascend(complement(prop("category"))), // truthiness of "category" value
ascend(prop("name")), // "name" value
];
const toMenuStructure = pipe(
partition(prop("categoryName")),
apply(useWith(concat, [withCategoryTransformation, withoutCategoryTransformation])),
sortWith(sortCriteria),
);
console.log(toMenuStructure(testArray));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.min.js"></script>
我首先将集合分成两组。那些有类别名称的,那些没有类别名称的。 (基于 "categoryName" 值的真实性。)
对有类别名称的集合和没有类别名称的集合执行不同的操作,连接结果值。
对类别名为:
的组执行以下操作首先将类别名称按 "categoryName" 属性 分组。
将 key/value-pairs 转换回列表。
转换每个元素,使用组键 (
head
) 作为 "name" 和 "category" 值。然后为每个项目 (last
) 选择 "type" 值的 "name" 属性 并将其用作 "children".// from [ "Cat A", [ { id: 34, type: {name: "Test1", category: "Cat A"}, typeName: "Test1", categoryName: "Cat A" }, /* ... */ ] ] // to {name: "Cat A", category: "Cat A", children: [{name: "Test1"}, /* ... */]}
没有分类的组只进行一次操作:
- 为每个项目选择 "type" 值的 "name" 和 "category" 属性。 (删除 "seen" 属性。)
然后我们按以下对所有对象进行排序:类别值的 "category" 属性、truthiness/falseness 的存在,最后是 [= 的值69=]属性.
我选择通过检查 "category" 属性 的存在来将 "Others" 排序到最后。如果你更愿意专门过滤掉名字 "Others" 你可以
// replace
descend(has("category"))
// with
ascend(propEq("name", "Others"))
根据偏好,您还可以选择 withCategoryTransformation
的替代版本,这里有两个用于 pipe
中最后一个操作的替代版本:
// Non point-free, but uses a more native JavaScript style.
map(([category, items]) => ({
name: category,
category: category,
children: items.map(compose(pick(["name"]), prop("type"))),
}))
// A point-free version that executes separate operations for the category
// and items, then merges the results together.
map(apply(useWith(mergeLeft, [
applySpec({ name: identity, category: identity }),
applySpec({ children: map(compose(pick(["name"]), prop("type"))) }),
])))