具有特定结构的 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 和 namecategorychildren 属性的数组。当类别为 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>

  1. 我首先将集合分成两组。那些有类别名称的,那些没有类别名称的。 (基于 "categoryName" 值的真实性。)

  2. 对有类别名称的集合和没有类别名称的集合执行不同的操作,连接结果值。

    对类别名为:

    的组执行以下操作
    1. 首先将类别名称按 "categoryName" 属性 分组。

    2. 将 key/value-pairs 转换回列表。

    3. 转换每个元素,使用组键 (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"}, /* ... */]}
      

    没有分类的组只进行一次操作:

    1. 为每个项目选择 "type" 值的 "name" 和 "category" 属性。 (删除 "seen" 属性。)
  3. 然后我们按以下对所有对象进行排序:类别值的 "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"))) }),
])))