如何在 Ramda.js 中使用回调函数动态 fill/expand 二维数组
How to dynamically fill/expand a 2d Array using a callback function in Ramda.js
我想创建一个动态函数来简化 array-transforming 回调的工作,以便填充和扩展二维数组。
概述挑战
我想创建一个这样的函数
finalFunction({ array, header, ...args }, callbackFunctionToTransformArray)
Restrictions
- The given array is always a 2d array
- The header is supplied as a string to be passed onto the callbackFunction
- The callback function always has to return a "changes" Object containing the headers as Keys. The values for each key contain an array of the values to be inserted
在给定以下设置输入参数(输入 object 的一部分)的情况下可以通过所有三种情况:
{
array = [
["#","FirstName","LastName"]
["1","tim","foo"],
["2","kim","bar"]
],
header: "FirstName",
...args
}
Important
The challenges is not in the creation of the callback functions, but rather in the creation of the "finalFunction".
场景 1:在不扩展的情况下转换现有数组
// return for the second row of the array
callback1 => {
changes: {
FirstName: ["Tim"]
}
};
// return for the third row of the array
callback1 => {
changes: {
FirstName: ["Kim"]
}
};
finalFunction({ array, header, ...args }, callback1)
应该return
{
array: [
["#","FirstName","LastName"]
["1","Tim","foo"],
["2","Kim","bar"]
],
header: "FirstName",
...args
}
场景 2:通过水平扩展转换现有数组
// return given for the second row
callback2 => {
changes: {
FullName: ["Tim Foo"]
}
};
// return given for the third row
callback2 => {
changes: {
FullName: ["Kim Bar"]
}
};
finalFunction({ array, header, ...args }, callback2)
应该return
{
array: [
["#","FirstName","LastName","FullName"]
["1","Tim","foo","Tim Foo"],
["2","Kim","bar","Kim Bar"]
],
header: "FirstName",
...args
}
场景 3:通过垂直和水平扩展转换现有数组
// return given for the second row
callback3 => {
changes: {
"Email": ["tim.foo@whosebug.com","timmy@gmail.com"],
"MailType": ["Work","Personal"]
}
};
// return given for the third row
callback3 => {
changes: {
"Email": ["kim.bar@whosebug.com","kimmy@aol.com"],
"MailType": ["Work","Personal"]
}
};
finalFunction({ array, header, ...args }, callback3)
应该return
{
array: [
["#","FirstName","LastName","Email","MailType"]
["1","Tim","foo","tim.foo@whosebug.com","Work"],
["1","Tim","foo","timmy@gmail.com","Personal"],
["2","Kim","bar","kim.bar@whosebug.com","Work"],
["2","Kim","bar","kimmy@aol.com","Personal"]
],
header: "FirstName",
...args
}
当前进度
出色的@Scott Sauyet 帮助我在二维数组和更改之间创建了一个合并函数 object:
const addInputToArray = ({ array, changes, ...rest}) => ({
array: Object .entries (changes) .reduce ((a, [k, vs], _, __, index = array [0] .indexOf (k)) =>
vs.reduce(
(a, v, i) =>
(i + 1) in a
? update ((i + 1), update (index, v, a [i + 1] ), a)
: concat (a, [update (index, v, map (always (''), array [0]) )] ),
a),
array
),
...rest
})
这对场景 #1 非常有用。但是,如果它们不是原始数组的一部分,我似乎无法获得自动创建 headers 的解决方案。
然而,我在方案 3 中描述的垂直扩展方面取得了进展。
const expandVertically = ({ array, header, index = array[0].indexOf(header), ...args }, callback) => ({
array: array.reduce((a, v, i) => {
if (i === 0) {
a.push(v);
} else {
const arrayBlock = R.repeat(v, callback(v[index]).length);
arrayBlock.unshift(array[0]);
const result = addInputToArray({
changes: callback(v[index]).changes,
array: arrayBlock
}).array;
result.shift();
result.map(x => a.push(x));
}
return a;
}, []),
header,
...args
})
在我看来,新创建的逻辑必须如此。
- 调用回调函数以检索第一个 Header 行可能丢失的条目
- 将 "changes" object 的缺失键添加到 header 行
- 减少跳过第一行的数组
- 始终假设一个数组块(因为如果数组块只有长度 1 就可以了,这将涵盖场景 #1 和 #2)
- 确保数组块长度不需要回调提供 "length" 参数,而是从为 "changes" obj[= 中的每个键提供的值的数组长度中捕获71=]
当前的挑战
- 当前的垂直扩展解决方案要求回调在其结果中提供一个 "length" 参数,以便为每个源行获得正确的重复次数。
- 当前将 "changes" 与 sourceArray 合并的函数不会自动创建新的 Header,如果它们不能在源数组的第一行中找到的话。
我觉得这是可行的,它会为我目前正在进行的项目提供很大的好处,因为它为所有 array-fillings/expansions 应用了一个标准化的接口。
但是我觉得卡住了,尤其是关于如何在一个函数中涵盖所有 3 个场景。
任何想法或见解将不胜感激。
这是一次尝试。我可能仍然遗漏了一些东西,因为我完全忽略了你的 header
参数。是否有必要,或者该功能现在已被回调函数生成的 change
object 中的键捕获?
// Helper function
const transposeObj = (obj, len = Object .values (obj) [0] .length) =>
[... Array (len)] .map (
(_, i) => Object .entries (obj) .reduce (
(a, [k, v]) => ({... a , [k]: v[i] }),
{}
)
)
// Main function
const finalFunction = (
{array: [headers, ...rows], ...rest},
callback,
changes = rows.map(r => transposeObj(callback(r).changes)),
allHeaders = [
...headers,
...changes
.flatMap (t => t .flatMap (Object.keys) )
.filter (k => !headers .includes (k))
.filter ((x, i, a) => a .indexOf (x) == i)
],
) => ({
array: [
allHeaders,
...rows .flatMap (
(row, i) => changes [i] .map (
change => Object .entries (change) .reduce (
(r, [k, v]) => [
...r.slice(0, allHeaders .indexOf (k)),
v,
...r.slice(allHeaders .indexOf (k) + 1)
],
row.slice(0)
)
)
)
],
...rest
})
const data = {array: [["#", "FirstName", "LastName"], ["1", "tim", "foo"], ["2", "kim", "bar"]], more: 'stuff', goes: 'here'}
// Faked out to attmep
const callback1 = (row) => ({changes: {FirstName: [row[1][0].toUpperCase() + row[1].slice(1)]}})
const callback2 = (row) => ({changes: {FullName: [`${row[1]} ${row[2]}`]}})
const callback3 = (row) => ({changes: {Email: [`${row[1]}.${row[2]}@whosebug.com`,`${row[1]}my@gmail.com`],MailType: ["Work","Personal"]}})
console .log (finalFunction (data, callback1))
console .log (finalFunction (data, callback2))
console .log (finalFunction (data, callback3))
这使用辅助函数 transposeObj
,它将 changes
列表转换为我认为更有用的内容。变成这样:
{
Email: ["tim.foo@whosebug.com", "timmy@gmail.com"],
MailType: ["Work", "Personal"]
}
进入这个:
[
{Email: "tim.foo@whosebug.com", MailType: "Work"},
{Email: "timmy@gmail.com", MailType: "Personal"}
]
主函数接受您的回调和带有 array
参数的数据 object,它从中提取 headers
和 rows
数组(以及跟踪rest
中的剩余属性。)它通过调用 transposeObj
帮助程序来派生 changes
对 changes
属性 调用每一行回调的结果。它使用该数据找到新的 headers,方法是获取 changes
object 中的所有键,并删除数组中已有的所有键,然后减少为一组唯一值。然后它将这些新的附加到现有的 headers 以产生 allHeaders
.
在函数的 body 中,我们 return 一个新的 object 使用 ...rest
作为其他参数,并通过开始更新 array
这个 headers 然后 flat-mapping rows
的新列表带有一个函数,该函数接受每个转置 object 并将其所有属性添加到当前行的副本,匹配索引使用 allHeaders
将它们放在正确的位置。
请注意,如果转置更改的键 object 已经存在,此技术将简单地更新输出中的相应索引。
我们在上面使用三个虚拟回调函数进行了测试,目的只是勉强涵盖您的示例。它们不应该看起来像您的生产代码。
我们 运行 分别根据您的输入分别生成三个单独的结果 object。请注意,这不会修改您的输入数据。如果你想按顺序应用它们,你可以这样做:
const data1 = finalFunction (data, callback1)
console.log (data1, '-----------------------------------')
const data2 = finalFunction (data1, callback2)
console.log (data2, '-----------------------------------')
const data3 = finalFunction (data2, callback3)
console.log (data3, '-----------------------------------')
得到如下结果:
{
array: [
["#", "FirstName", "LastName"],
["1", "Tim", "foo"],
["2", "Kim", "bar"]
],
more: "stuff",
goes: "here"
}
-----------------------------------
{
array: [
["#", "FirstName", "LastName", "FullName"],
["1", "Tim","foo", "Tim foo"],
["2", "Kim", "bar", "Kim bar"]
],
more: "stuff",
goes: "here"
}
-----------------------------------
{
array: [
["#", "FirstName", "LastName", "FullName", "Email", "MailType"],
["1", "Tim", "foo", "Tim foo", "Tim.foo@whosebug.com", "Work"],
["1", "Tim", "foo", "Tim foo", "Timmy@gmail.com", "Personal"],
["2", "Kim", "bar", "Kim bar", "Kim.bar@whosebug.com", "Work"],
["2", "Kim", "bar", "Kim bar", "Kimmy@gmail.com", "Personal"]
],
more: "stuff",
goes: "here"
}
-----------------------------------
或者,当然,您可以只开始 let data = ...
,然后在某种循环中执行 data = finalFunction(data, nextCallback)
。
此功能在很大程度上取决于 flatMap
,它并非在所有环境中都可用。 MDN page suggests alternatives if you need them. If you're still using Ramda, the chain
函数将起作用。
更新
您的回复选择使用 Ramda 而不是这个原始的 ES6 版本。我认为如果你打算使用 Ramda,你可能可以通过大量的 Ramda 函数来简化很多。我猜还可以做更多,但我认为这样更简洁:
// Helper function
const transposeObj = (obj) =>
map (
(i) => reduce((a, [k, v]) => ({ ...a, [k]: v[i] }), {}, toPairs(obj)),
range (0, length (values (obj) [0]) )
)
// Main function
const finalFunction = (
{ array: [headers, ...rows], ...rest },
callback,
changes = map (pipe (callback, prop('changes'), transposeObj), rows),
allHeaders = uniq (concat (headers, chain (chain (keys), changes)))
) => ({
array: concat([allHeaders], chain(
(row) => map (
pipe (
toPairs,
reduce((r, [k, v]) => assocPath([indexOf(k, allHeaders)], v, r), row)
),
changes[indexOf(row, rows)]
),
rows
)),
...rest
})
const data = {array: [["#", "FirstName", "LastName"], ["1", "tim", "foo"], ["2", "kim", "bar"]], more: 'stuff', goes: 'here'}
// Faked out to attmep
const callback1 = (row) => ({changes: {FirstName: [row[1][0].toUpperCase() + row[1].slice(1)]}})
const callback2 = (row) => ({changes: {FullName: [`${row[1]} ${row[2]}`]}})
const callback3 = (row) => ({changes: {Email: [`${row[1]}.${row[2]}@whosebug.com`,`${row[1]}my@gmail.com`],MailType: ["Work","Personal"]}})
console .log (finalFunction (data, callback1))
console .log (finalFunction (data, callback2))
console .log (finalFunction (data, callback3))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {map, reduce, toPairs, range, length, values, pipe, prop, uniq, concat, chain, keys, assocPath, indexOf} = R </script>
基于 Scott 的重要意见,我想分享此功能的一个版本,它不使用 flatMap,而是使用 Ramda 函数(从而允许更多环境支持。
const R = require('ramda')
// Helper function
const transposeObj = (obj, len = Object.values(obj)[0].length) =>
[...Array(len)].map((_, i) => Object.entries(obj).reduce((a, [k, v]) => ({ ...a, [k]: v[i] }), {}));
// Main function
const finalFunction = (
{ array: [headers, ...rows], ...rest },
callback,
changes = rows.map(r => transposeObj(callback(r).changes)),
allHeaders = R.flatten([
...headers,
R.chain(t => R.chain(Object.keys, t), [...changes])
.filter(k => !headers.includes(k))
.filter((x, i, a) => a.indexOf(x) == i)
])
) => {
const resultRows = R.chain(
(row, i = R.indexOf(row, [...rows])) =>
changes[i].map(change =>
Object.entries(change).reduce(
(r, [k, v]) => [...r.slice(0, allHeaders.indexOf(k)), v, ...r.slice(allHeaders.indexOf(k) + 1)],
row.slice(0)
)
),
[...rows]
);
return {
array: [allHeaders, ...resultRows],
...rest
};
};
我想创建一个动态函数来简化 array-transforming 回调的工作,以便填充和扩展二维数组。
概述挑战
我想创建一个这样的函数
finalFunction({ array, header, ...args }, callbackFunctionToTransformArray)
Restrictions
- The given array is always a 2d array
- The header is supplied as a string to be passed onto the callbackFunction
- The callback function always has to return a "changes" Object containing the headers as Keys. The values for each key contain an array of the values to be inserted
在给定以下设置输入参数(输入 object 的一部分)的情况下可以通过所有三种情况:
{
array = [
["#","FirstName","LastName"]
["1","tim","foo"],
["2","kim","bar"]
],
header: "FirstName",
...args
}
Important
The challenges is not in the creation of the callback functions, but rather in the creation of the "finalFunction".
场景 1:在不扩展的情况下转换现有数组
// return for the second row of the array
callback1 => {
changes: {
FirstName: ["Tim"]
}
};
// return for the third row of the array
callback1 => {
changes: {
FirstName: ["Kim"]
}
};
finalFunction({ array, header, ...args }, callback1)
应该return
{
array: [
["#","FirstName","LastName"]
["1","Tim","foo"],
["2","Kim","bar"]
],
header: "FirstName",
...args
}
场景 2:通过水平扩展转换现有数组
// return given for the second row
callback2 => {
changes: {
FullName: ["Tim Foo"]
}
};
// return given for the third row
callback2 => {
changes: {
FullName: ["Kim Bar"]
}
};
finalFunction({ array, header, ...args }, callback2)
应该return
{
array: [
["#","FirstName","LastName","FullName"]
["1","Tim","foo","Tim Foo"],
["2","Kim","bar","Kim Bar"]
],
header: "FirstName",
...args
}
场景 3:通过垂直和水平扩展转换现有数组
// return given for the second row
callback3 => {
changes: {
"Email": ["tim.foo@whosebug.com","timmy@gmail.com"],
"MailType": ["Work","Personal"]
}
};
// return given for the third row
callback3 => {
changes: {
"Email": ["kim.bar@whosebug.com","kimmy@aol.com"],
"MailType": ["Work","Personal"]
}
};
finalFunction({ array, header, ...args }, callback3)
应该return
{
array: [
["#","FirstName","LastName","Email","MailType"]
["1","Tim","foo","tim.foo@whosebug.com","Work"],
["1","Tim","foo","timmy@gmail.com","Personal"],
["2","Kim","bar","kim.bar@whosebug.com","Work"],
["2","Kim","bar","kimmy@aol.com","Personal"]
],
header: "FirstName",
...args
}
当前进度
出色的@Scott Sauyet 帮助我在二维数组和更改之间创建了一个合并函数 object:
const addInputToArray = ({ array, changes, ...rest}) => ({
array: Object .entries (changes) .reduce ((a, [k, vs], _, __, index = array [0] .indexOf (k)) =>
vs.reduce(
(a, v, i) =>
(i + 1) in a
? update ((i + 1), update (index, v, a [i + 1] ), a)
: concat (a, [update (index, v, map (always (''), array [0]) )] ),
a),
array
),
...rest
})
这对场景 #1 非常有用。但是,如果它们不是原始数组的一部分,我似乎无法获得自动创建 headers 的解决方案。
然而,我在方案 3 中描述的垂直扩展方面取得了进展。
const expandVertically = ({ array, header, index = array[0].indexOf(header), ...args }, callback) => ({
array: array.reduce((a, v, i) => {
if (i === 0) {
a.push(v);
} else {
const arrayBlock = R.repeat(v, callback(v[index]).length);
arrayBlock.unshift(array[0]);
const result = addInputToArray({
changes: callback(v[index]).changes,
array: arrayBlock
}).array;
result.shift();
result.map(x => a.push(x));
}
return a;
}, []),
header,
...args
})
在我看来,新创建的逻辑必须如此。
- 调用回调函数以检索第一个 Header 行可能丢失的条目
- 将 "changes" object 的缺失键添加到 header 行
- 减少跳过第一行的数组
- 始终假设一个数组块(因为如果数组块只有长度 1 就可以了,这将涵盖场景 #1 和 #2)
- 确保数组块长度不需要回调提供 "length" 参数,而是从为 "changes" obj[= 中的每个键提供的值的数组长度中捕获71=]
当前的挑战
- 当前的垂直扩展解决方案要求回调在其结果中提供一个 "length" 参数,以便为每个源行获得正确的重复次数。
- 当前将 "changes" 与 sourceArray 合并的函数不会自动创建新的 Header,如果它们不能在源数组的第一行中找到的话。
我觉得这是可行的,它会为我目前正在进行的项目提供很大的好处,因为它为所有 array-fillings/expansions 应用了一个标准化的接口。
但是我觉得卡住了,尤其是关于如何在一个函数中涵盖所有 3 个场景。
任何想法或见解将不胜感激。
这是一次尝试。我可能仍然遗漏了一些东西,因为我完全忽略了你的 header
参数。是否有必要,或者该功能现在已被回调函数生成的 change
object 中的键捕获?
// Helper function
const transposeObj = (obj, len = Object .values (obj) [0] .length) =>
[... Array (len)] .map (
(_, i) => Object .entries (obj) .reduce (
(a, [k, v]) => ({... a , [k]: v[i] }),
{}
)
)
// Main function
const finalFunction = (
{array: [headers, ...rows], ...rest},
callback,
changes = rows.map(r => transposeObj(callback(r).changes)),
allHeaders = [
...headers,
...changes
.flatMap (t => t .flatMap (Object.keys) )
.filter (k => !headers .includes (k))
.filter ((x, i, a) => a .indexOf (x) == i)
],
) => ({
array: [
allHeaders,
...rows .flatMap (
(row, i) => changes [i] .map (
change => Object .entries (change) .reduce (
(r, [k, v]) => [
...r.slice(0, allHeaders .indexOf (k)),
v,
...r.slice(allHeaders .indexOf (k) + 1)
],
row.slice(0)
)
)
)
],
...rest
})
const data = {array: [["#", "FirstName", "LastName"], ["1", "tim", "foo"], ["2", "kim", "bar"]], more: 'stuff', goes: 'here'}
// Faked out to attmep
const callback1 = (row) => ({changes: {FirstName: [row[1][0].toUpperCase() + row[1].slice(1)]}})
const callback2 = (row) => ({changes: {FullName: [`${row[1]} ${row[2]}`]}})
const callback3 = (row) => ({changes: {Email: [`${row[1]}.${row[2]}@whosebug.com`,`${row[1]}my@gmail.com`],MailType: ["Work","Personal"]}})
console .log (finalFunction (data, callback1))
console .log (finalFunction (data, callback2))
console .log (finalFunction (data, callback3))
这使用辅助函数 transposeObj
,它将 changes
列表转换为我认为更有用的内容。变成这样:
{
Email: ["tim.foo@whosebug.com", "timmy@gmail.com"],
MailType: ["Work", "Personal"]
}
进入这个:
[
{Email: "tim.foo@whosebug.com", MailType: "Work"},
{Email: "timmy@gmail.com", MailType: "Personal"}
]
主函数接受您的回调和带有 array
参数的数据 object,它从中提取 headers
和 rows
数组(以及跟踪rest
中的剩余属性。)它通过调用 transposeObj
帮助程序来派生 changes
对 changes
属性 调用每一行回调的结果。它使用该数据找到新的 headers,方法是获取 changes
object 中的所有键,并删除数组中已有的所有键,然后减少为一组唯一值。然后它将这些新的附加到现有的 headers 以产生 allHeaders
.
在函数的 body 中,我们 return 一个新的 object 使用 ...rest
作为其他参数,并通过开始更新 array
这个 headers 然后 flat-mapping rows
的新列表带有一个函数,该函数接受每个转置 object 并将其所有属性添加到当前行的副本,匹配索引使用 allHeaders
将它们放在正确的位置。
请注意,如果转置更改的键 object 已经存在,此技术将简单地更新输出中的相应索引。
我们在上面使用三个虚拟回调函数进行了测试,目的只是勉强涵盖您的示例。它们不应该看起来像您的生产代码。
我们 运行 分别根据您的输入分别生成三个单独的结果 object。请注意,这不会修改您的输入数据。如果你想按顺序应用它们,你可以这样做:
const data1 = finalFunction (data, callback1)
console.log (data1, '-----------------------------------')
const data2 = finalFunction (data1, callback2)
console.log (data2, '-----------------------------------')
const data3 = finalFunction (data2, callback3)
console.log (data3, '-----------------------------------')
得到如下结果:
{
array: [
["#", "FirstName", "LastName"],
["1", "Tim", "foo"],
["2", "Kim", "bar"]
],
more: "stuff",
goes: "here"
}
-----------------------------------
{
array: [
["#", "FirstName", "LastName", "FullName"],
["1", "Tim","foo", "Tim foo"],
["2", "Kim", "bar", "Kim bar"]
],
more: "stuff",
goes: "here"
}
-----------------------------------
{
array: [
["#", "FirstName", "LastName", "FullName", "Email", "MailType"],
["1", "Tim", "foo", "Tim foo", "Tim.foo@whosebug.com", "Work"],
["1", "Tim", "foo", "Tim foo", "Timmy@gmail.com", "Personal"],
["2", "Kim", "bar", "Kim bar", "Kim.bar@whosebug.com", "Work"],
["2", "Kim", "bar", "Kim bar", "Kimmy@gmail.com", "Personal"]
],
more: "stuff",
goes: "here"
}
-----------------------------------
或者,当然,您可以只开始 let data = ...
,然后在某种循环中执行 data = finalFunction(data, nextCallback)
。
此功能在很大程度上取决于 flatMap
,它并非在所有环境中都可用。 MDN page suggests alternatives if you need them. If you're still using Ramda, the chain
函数将起作用。
更新
您的回复选择使用 Ramda 而不是这个原始的 ES6 版本。我认为如果你打算使用 Ramda,你可能可以通过大量的 Ramda 函数来简化很多。我猜还可以做更多,但我认为这样更简洁:
// Helper function
const transposeObj = (obj) =>
map (
(i) => reduce((a, [k, v]) => ({ ...a, [k]: v[i] }), {}, toPairs(obj)),
range (0, length (values (obj) [0]) )
)
// Main function
const finalFunction = (
{ array: [headers, ...rows], ...rest },
callback,
changes = map (pipe (callback, prop('changes'), transposeObj), rows),
allHeaders = uniq (concat (headers, chain (chain (keys), changes)))
) => ({
array: concat([allHeaders], chain(
(row) => map (
pipe (
toPairs,
reduce((r, [k, v]) => assocPath([indexOf(k, allHeaders)], v, r), row)
),
changes[indexOf(row, rows)]
),
rows
)),
...rest
})
const data = {array: [["#", "FirstName", "LastName"], ["1", "tim", "foo"], ["2", "kim", "bar"]], more: 'stuff', goes: 'here'}
// Faked out to attmep
const callback1 = (row) => ({changes: {FirstName: [row[1][0].toUpperCase() + row[1].slice(1)]}})
const callback2 = (row) => ({changes: {FullName: [`${row[1]} ${row[2]}`]}})
const callback3 = (row) => ({changes: {Email: [`${row[1]}.${row[2]}@whosebug.com`,`${row[1]}my@gmail.com`],MailType: ["Work","Personal"]}})
console .log (finalFunction (data, callback1))
console .log (finalFunction (data, callback2))
console .log (finalFunction (data, callback3))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {map, reduce, toPairs, range, length, values, pipe, prop, uniq, concat, chain, keys, assocPath, indexOf} = R </script>
基于 Scott 的重要意见,我想分享此功能的一个版本,它不使用 flatMap,而是使用 Ramda 函数(从而允许更多环境支持。
const R = require('ramda')
// Helper function
const transposeObj = (obj, len = Object.values(obj)[0].length) =>
[...Array(len)].map((_, i) => Object.entries(obj).reduce((a, [k, v]) => ({ ...a, [k]: v[i] }), {}));
// Main function
const finalFunction = (
{ array: [headers, ...rows], ...rest },
callback,
changes = rows.map(r => transposeObj(callback(r).changes)),
allHeaders = R.flatten([
...headers,
R.chain(t => R.chain(Object.keys, t), [...changes])
.filter(k => !headers.includes(k))
.filter((x, i, a) => a.indexOf(x) == i)
])
) => {
const resultRows = R.chain(
(row, i = R.indexOf(row, [...rows])) =>
changes[i].map(change =>
Object.entries(change).reduce(
(r, [k, v]) => [...r.slice(0, allHeaders.indexOf(k)), v, ...r.slice(allHeaders.indexOf(k) + 1)],
row.slice(0)
)
),
[...rows]
);
return {
array: [allHeaders, ...resultRows],
...rest
};
};