将平面对象数组转换为嵌套对象

Convert an array of flat objects to nested objects

我有以下数组,它是扁平化的,所以我想将它转换为嵌套数组。

const data = [{
    "countryId": 9,
    "countryName": "Angola",
    "customerId": 516,
    "customerName": "CHEV",
    "fieldId": 2000006854,
    "fieldName": "A12"
},
{
    "countryId": 9,
    "countryName": "Angola",
    "customerId": 516,
    "customerName": "CHEV",
    "fieldId": 5037,
    "fieldName": "BANZ"
}
]

我想要以下界面

interface Option {
    value: number,
    viewText: string,
    ids?: Set<number>,
    children?: Option[]
}

所以输出应该是这样的

const output = [{
    "value": 9,
    "viewText": "Angola",
    "ids": [516],
    "children": [{
        "value": 516,
        "viewText": "CHEV",
        "ids": [2000006854],
        "children": [{
            "viewText": "A12",
            "value": 2000006854
        }]
    }]
}]

我一直在努力寻找解决方案,我不知道如何解决这个问题,我是否需要使用递归函数或其他东西,有什么想法吗?

更新:

这是我天真的解决方案。这种方法的问题是我手动处理嵌套结构。

function mapFlatToNested(data: typeof dataset) {
    const map = new Map<number, Option>()

    for (const item of data) {
        const itemFromMap = map.get(item.countryId);

        if (!itemFromMap) {
            map.set(item.countryId, {
                viewText: item.countryName,
                value: item.countryId,
                ids: new Set([item.customerId]),
                childrens: [{
                    viewText: item.customerName,
                    value: item.customerId,
                    ids: new Set([item.fieldId]),
                    childrens: [{
                        value: item.fieldId,
                        viewText: item.fieldName
                    }]
                }]
            })
        } else {
            if (!itemFromMap?.ids?.has(item.customerId)) {

                itemFromMap?.ids?.add(item.customerId);
                itemFromMap?.childrens?.push({
                    value: item.customerId,
                    viewText: item.customerName,
                    ids: new Set([item.fieldId]),
                    childrens: [{
                        value: item.fieldId,
                        viewText: item.fieldName
                    }]
                })

            } else {
                const customer = itemFromMap?.childrens?.find((customer) => customer.value === item.customerId);

                if (customer) {
                    if (!customer.ids?.has(item.fieldId)) {
                        customer.ids?.add(item.fieldId)
                        customer.childrens?.push({
                            value: item.fieldId,
                            viewText: item.fieldName,
                            ids: new Set([customer.value])
                        })
                    }
                }
            }

        }
    }
    return map
}

console.log(mapFlatToNested(dataset));

Here is the typescript playground

您需要映射数组:

data.map(cur=>({
  value:cur.countryId,
    viewText:cur.countryName,
    ids:[cur.customerId],
    children:[{
        value:cur.customerId,
        viewText:cur.customerName,
        ids:[cur.fieldId],
        children:[{
            viewText:cur.fieldName,
            value:cur.fieldId
        }]
    }]
}))

更新: 添加通用版本。总体上看起来更好,虽然不是针对原始问题。原答案如下。

普通版

这个想法的一个更通用的版本将允许您选择哪些输入字段是键,它们如何转换为输出字段,然后如何处理 children。

这是一种实现方式:

const omit = (names) => (o) => 
  Object .fromEntries (Object .entries (o) .filter (([k, v]) => !names .includes (k)))

const group = (fn) => (xs) =>
  Object .values (xs .reduce (
    (a, x, _, __, k = fn (x)) => ((a[k] = [... (a[k] || []), x]), a),
    {}
  ))

const nesting = (fields, descend) => (xs) => {
  const select = Object .entries (fields)
  const outers = select .map (([_, v]) => v)
  const descendents = Object .entries (descend)
  const makeKey = (x) => outers .map ((v) => x [v]) .join ('\u0000')
  return group (makeKey) (xs) .map ((group) => Object .assign (
    ... select .map (([k, v]) => ({[k]: group [0] [v]})),
    ... descendents .map (([k, v]) => ({[k]: v (group .map (omit (outers)))}))
  ))
}
    

const transform = nesting ({
  value: 'countryId', 
  viewText: 'countryName'
}, {
  ids: (xs) => xs .map (x => x.customerId),
  children: nesting ({
    value: 'customerId', 
    viewText: 'customerName',
  }, {
    ids: (xs) => xs .map (x => x.fieldId),
    // or: `children: (xs) => xs` to get all remaining fields intact.
    children: nesting ({
      value: 'fieldId', 
      viewText: 'fieldName'   
    }, {})
  })
})

const dataset = [{countryId: 59, countryName: "Algeria", customerId: 6959, customerName: "ABERDEEN DRILLING SCHOOL LTD", fieldId: 8627, fieldName: " BAGAN"}, {countryId: 59, countryName: "Algeria", customerId: 2730, customerName: "ABU DHABI COMPANY FOR ONSHORE OIL OPERATIONS", fieldId: 6158, fieldName: "BAB"}, {countryId: 59, countryName: "Algeria", customerId: 3457, customerName: "AGIP - ALGIERS", fieldId: 9562, fieldName: "LESS"}, {countryId: 9, countryName: "Angola", customerId: 516, customerName: "CHEVRON (SASBU) - ANGOLA", fieldId: 2000006854, fieldName: "A12"}, {countryId: 9, countryName: "Angola", customerId: 516, customerName: "CHEVRON (SASBU) - ANGOLA", fieldId: 5037, fieldName: "BANZALA"}]

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

我们有两个通用的辅助函数。 omit 采用键列表,return 是一个采用 object 的函数,return 是不包含这些键的克隆。 group 采用 key-generation 函数和 returns 函数,该函数采用值数组并将它们按由该键编辑的值 return 分组到数组中。

我们的主要函数是nesting,它有两个参数:

  • 描述用于分组的字段的 object。它的键成为输出中的字段,它的值是来自输入的属性object;它们组合在一起以创建分组键。输出值取自组的第一个元素。

  • 描述将添加的其他字段的 object。它的键也是输出中的字段,但它的值是从组到某个输出字段的函数。这是我们处理递归的地方,只需再次调用 nested。但是我们也可以做其他的事情,比如把所有的 child id 拉到一个字段中,就像这里所做的那样。

在这个版本中,外部节点的省略很重要,因为我看到的大多数类似这样的用途都会涉及 grouping/nesting 在几个层面上,然后 return 剩下的值完好无损。我们可以通过用身份函数替换我们最里面的 children 来做到这一点。

原版

此版本允许您像这样配置函数:

const transform = nestedGroup ('country', 'customer', 'field')
transform (dataset)

不是特别通用,在代码中嵌入'value''viewText',并假设原始字段看起来像<something>Id<something>Name。但它完成了工作,并且受制于这些限制,是可配置的。

const nestedGroup = (level, ...levels) => (xs, id = level + 'Id', name = level + 'Name') =>
  level == undefined 
    ? xs
    : Object .values (xs .reduce (
        (a, x, _, __, k = x [id] + '~' + x [name]) => ((a [k] = [... (a [k] || []), x]), a), 
        {}
      )) .map (xs => ({
        value: xs [0] [id], 
        viewText: xs [0] [name],
        ... (levels .length > 0 ? {
          ids: xs .map (x => x [`${levels [0]}Id`]),
          children: nestedGroup (...levels)(xs)
        } : {}),
      }))


const transform = nestedGroup ('country', 'customer', 'field')

const dataset = [{countryId: 59, countryName: "Algeria", customerId: 6959, customerName: "ABERDEEN DRILLING SCHOOL LTD", fieldId: 8627, fieldName: " BAGAN"}, {countryId: 59, countryName: "Algeria", customerId: 2730, customerName: "ABU DHABI COMPANY FOR ONSHORE OIL OPERATIONS", fieldId: 6158, fieldName: "BAB"}, {countryId: 59, countryName: "Algeria", customerId: 3457, customerName: "AGIP - ALGIERS", fieldId: 9562, fieldName: "LESS"}, {countryId: 9, countryName: "Angola", customerId: 516, customerName: "CHEVRON (SASBU) - ANGOLA", fieldId: 2000006854, fieldName: "A12"}, {countryId: 9, countryName: "Angola", customerId: 516, customerName: "CHEVRON (SASBU) - ANGOLA", fieldId: 5037, fieldName: "BANZALA"}]

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

我们首先创建 idname 值。在第一级,它们将是 countryIdcountryName,然后是 customerIdcustomerName,依此类推。

然后,如果我们在底层(没有更多层次可以嵌套),我们只需 return 我们的输入。否则,我们将这些值与相同的 [id][name] 值组合在一起,然后对于每个组,我们提取公共字段,以及(如果有更多嵌套要做,ids 并且递归地,children.

让它更通用会很有趣。我认为尝试提出一个采用这种配置的函数会很有趣:

const transform = nestedGroup ('children', [
  {value: 'countryId', viewText: 'countryName', ['ids?']: (xs) => xs .map (x => x.id)},
  {value: 'customerId', viewText: 'customerName', ['ids?']: (xs) => xs .map (x => x.id)},
  {value: 'fieldId', viewText: 'fieldName', ['ids?']: (xs) => xs .map (x => x.id)},
])

ids 上的 '?' 将其标记为可选:仅在有更多嵌套级别时使用。然后我们可以将您的转换写成一个简单的注释,它接受 ['country', 'customer', 'field'] 并生成上面的内容。这将使我们可以根据需要对尽可能多的字段进行分组,然后根据我们的选择重命名它们。第一个参数 'children' 只是为了让我们命名后代节点。最好也包括 per-level,但我还没有完全弄明白。

我认为这将是一个有趣且有用的功能,但我还没有弄清楚细节。也许我很快就会回来。