如何在 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
    })

在我看来,新创建的逻辑必须如此。

  1. 调用回调函数以检索第一个 Header 行可能丢失的条目
  2. 将 "changes" object 的缺失键添加到 header 行
  3. 减少跳过第一行的数组
  4. 始终假设一个数组块(因为如果数组块只有长度 1 就可以了,这将涵盖场景 #1 和 #2)
  5. 确保数组块长度不需要回调提供 "length" 参数,而是从为 "changes" obj[= 中的每个键提供的值的数组长度中捕获71=]

当前的挑战

  1. 当前的垂直扩展解决方案要求回调在其结果中提供一个 "length" 参数,以便为每个源行获得正确的重复次数。
  2. 当前将 "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,它从中提取 headersrows 数组(以及跟踪rest 中的剩余属性。)它通过调用 transposeObj 帮助程序来派生 changeschanges 属性 调用每一行回调的结果。它使用该数据找到新的 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
  };
};