将平面对象数组转换为嵌套对象
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));
您需要映射数组:
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}
我们首先创建 id
和 name
值。在第一级,它们将是 countryId
和 countryName
,然后是 customerId
和 customerName
,依此类推。
然后,如果我们在底层(没有更多层次可以嵌套),我们只需 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,但我还没有完全弄明白。
我认为这将是一个有趣且有用的功能,但我还没有弄清楚细节。也许我很快就会回来。
我有以下数组,它是扁平化的,所以我想将它转换为嵌套数组。
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));
您需要映射数组:
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
。但是我们也可以做其他的事情,比如把所有的 childid
拉到一个字段中,就像这里所做的那样。
在这个版本中,外部节点的省略很重要,因为我看到的大多数类似这样的用途都会涉及 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}
我们首先创建 id
和 name
值。在第一级,它们将是 countryId
和 countryName
,然后是 customerId
和 customerName
,依此类推。
然后,如果我们在底层(没有更多层次可以嵌套),我们只需 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,但我还没有完全弄明白。
我认为这将是一个有趣且有用的功能,但我还没有弄清楚细节。也许我很快就会回来。