如何使用 Ramda.js 中的回调函数动态填充/扩展二维数组 [英] How to dynamically fill/expand a 2d Array using a callback function in Ramda.js

查看:30
本文介绍了如何使用 Ramda.js 中的回调函数动态填充/扩展二维数组的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想创建一个动态函数,该函数可以简化使用数组转换回调的工作,以便填充和扩展 2d 数组.

概述挑战

我想创建一个这样的函数

finalFunction({ array, header, ...args }, callbackFunctionToTransformArray)

<块引用>

限制

  • 给定的数组总是一个二维数组
  • 标头作为字符串提供给回调函数
  • 回调函数总是必须返回一个包含标题作为键的更改"对象.每个键的值包含要插入的值的数组

给定以下设置的输入参数(输入对象的一部分),它可以通过所有三个场景:

{数组 = [[#"、名字"、姓氏"]["1","tim","foo"],["2","kim","bar"]],标题:名字",...参数}

<块引用>

重要

挑战不在于回调函数的创建,而在于finalFunction"的创建.

场景 1:转换现有数组而不扩展

//返回数组的第二行回调1 =>{变化: {名字:[蒂姆"]}};//返回数组的第三行回调1 =>{变化: {名字:[金"]}};finalFunction({ array, header, ...args }, callback1)

应该返回

{大批: [[#"、名字"、姓氏"]["1","Tim","foo"],[2",金",酒吧"]],标题:名字",...参数}

场景 2:通过水平扩展转换现有数组

//返回给第二行回调2 =>{变化: {全名:[Tim Foo"]}};//返回给定的第三行回调2 =>{变化: {全名:["Kim Bar"]}};finalFunction({ array, header, ...args }, callback2)

应该返回

{大批: [["#","FirstName","LastName","FullName"]["1","Tim","foo","Tim Foo"],["2","Kim","bar","Kim Bar"]],标题:名字",...参数}

场景 3:通过垂直和水平扩展转换现有 Array

//返回给第二行回调3 =>{变化: {"电子邮件": ["tim.foo@stackoverflow.com","timmy@gmail.com"],邮件类型":[工作",个人"]}};//返回给定的第三行回调3 =>{变化: {"电子邮件": ["kim.bar@stackoverflow.com","kimmy@aol.com"],邮件类型":[工作",个人"]}};finalFunction({ array, header, ...args }, callback3)

应该返回

{大批: [["#","FirstName","LastName","Email","MailType"]["1","Tim","foo","tim.foo@stackoverflow.com","工作"],["1","Tim","foo","timmy@gmail.com","个人"],["2","Kim","bar","kim.bar@stackoverflow.com","工作"],["2","Kim","bar","kimmy@aol.com","个人"]],标题:名字",...参数}

目前的进展

精彩的@Scott Sauyet 帮助我在二维数组和更改对象之间创建了一个合并函数:

const addInputToArray = ({ array, changes, ...rest}) =>({数组:对象 .entries (changes) .reduce ((a, [k, vs], _, __, index = array [0] .indexOf (k)) =>与减少((a, v, i) =>(i + 1) 在一个?更新 ((i + 1), 更新 (index, v, a [i + 1] ), a): concat (a, [update (index, v, map (always (''), array [0]) )] ),一种),大批),...休息})

这适用于场景 #1.但是,如果标题不是原始数组的一部分,我似乎无法获得自动创建标题的解决方案.

然而,我在场景 3 中描述的垂直扩展方面取得了进展.

const expandVertically = ({ array, header, index = array[0].indexOf(header), ...args }, callback) =>({数组:array.reduce((a, v, i) => {如果(我 === 0){a.推(v);} 别的 {const arrayBlock = R.repeat(v, callback(v[index]).length);arrayBlock.unshift(array[0]);const 结果 = addInputToArray({更改:回调(v[index]).更改,数组:数组块}).大批;结果.shift();result.map(x => a.push(x));}返回一个;}, []),标题,...参数})

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

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

当前的挑战

  1. 当前的垂直扩展解决方案要求回调在其结果中提供长度"参数,以便为每个源行获取正确的重复次数.
  2. 如果在源数组的第一行中找不到新的标头,则将更改"与 sourceArray 合并的当前函数不会自动创建新标头.

我觉得这是可行的,它将为我正在从事的当前项目提供巨大的好处,因为它为所有数组填充/扩展应用了一个标准化的接口.

但是我觉得很困难,尤其是关于如何在一个函数中涵盖所有 3 个场景.

任何想法或见解将不胜感激.

解决方案

这是一个尝试.我可能仍然在这里遗漏了一些东西,因为我完全忽略了您的 header 参数.是否有必要,或者该功能现在是否已被回调函数生成的 change 对象中的键捕获?

//辅助函数const transposeObj = (obj, len = Object .values (obj) [0] .length) =>[... Array (len)] .map ((_, i) =>对象 .entries (obj) .reduce ((a, [k, v]) =>({... a , [k]: v[i] }),{}))//主功能const finalFunction = ({array: [headers, ...rows], ...rest},打回来,changes = rows.map(r => transposeObj(callback(r).changes)),allHeaders = [...标题,...变化.flatMap (t => t .flatMap (Object.keys) ).filter (k => !headers .includes (k)).filter ((x, i, a) => a .indexOf (x) == i)],) =>({大批: [所有标题,...行 .flatMap ((行, i) =>更改 [i] .map (改变 =>对象 .entries (change) .reduce ((r, [k, v]) =>[...r.slice(0, allHeaders .indexOf (k)),Ⅴ、...r.slice(allHeaders .indexOf (k) + 1)],row.slice(0))))],...休息})const data = {array: [["#", "FirstName", "LastName"], ["1", "tim", "foo"], ["2", "kim", "bar"]],更多:'东西',去:'这里'}//伪装成 attmepconst callback1 = (row) =>({更改:{名字:[row[1][0].toUpperCase() + row[1].slice(1)]}})const callback2 = (row) =>({更改:{全名:[`${row[1]} ${row[2]}`]}})const callback3 = (row) =>({更改: {电子邮件: [`${row[1]}.${row[2]}@stackoverflow.com`,`${row[1]}my@gmail.com`],MailType: ["工作","个人"]}})控制台 .log (finalFunction (data, callback1))控制台 .log (finalFunction (data, callback2))控制台 .log (finalFunction (data, callback3))

这使用了辅助函数transposeObj,它将changes 列表转换成我觉得更有用的东西.它变成了这样:

{电子邮件:[tim.foo@stackoverflow.com",timmy@gmail.com"],邮件类型:[工作",个人"]}

进入这个:

[{电子邮件:tim.foo@stackoverflow.com",邮件类型:工作"},{电子邮件:timmy@gmail.com",邮件类型:个人"}]

main 函数接受您的回调和一个带有 array 参数的数据对象,从中提取 headersrows 数组(以及作为跟踪 rest 中剩余的属性.)它通过针对 changes 调用 transposeObj 助手来派生 changes> 针对每一行调用回调的属性结果.使用该数据,它通过获取 changes 对象中的所有键来查找新标头,并删除数组中已有的所有键,然后减少到一组唯一值.然后它将这些新的附加到现有的标头以产生 allHeaders.

在函数体中,我们使用 ...rest 作为其他参数返回一个新对象,并从这个新的头列表开始更新 array然后使用一个函数对 rows 进行平面映射,该函数接受每个转置对象并将其所有属性添加到当前行的副本中,将索引与 allHeaders 匹配到将它们放在正确的位置.

请注意,如果转置的更改对象的键已经存在,则此技术将简单地更新输出中的相应索引.

我们使用三个虚拟回调函数进行了上面的测试,目的是勉强涵盖您的示例.它们不应该看起来像您的生产代码.

我们针对您的输入分别运行它们,生成三个独立的结果对象.请注意,这不会修改您的输入数据.如果您想按顺序应用它们,您可以执行以下操作:

const data1 = finalFunction (data, callback1)console.log (data1, '-----------------------------------')const data2 = finalFunction (data1, callback2)console.log (data2, '-----------------------------------')const data3 = finalFunction (data2, callback3)console.log (data3, '-----------------------------------')

得到如下结果:

<代码>{大批: [["#", "FirstName", "LastName"],[1",蒂姆",富"],[2",金",酒吧"]],更多东西",去:这里"}---------------------{大批: [["#", "FirstName", "LastName", "FullName"],["1", "Tim","foo", "Tim foo"],[2",金",酒吧",金酒吧"]],更多东西",去:这里"}---------------------{大批: [["#", "FirstName", "LastName", "FullName", "Email", "MailType"],["1", "Tim", "foo", "Tim foo", "Tim.foo@stackoverflow.com", "工作"],["1", "Tim", "foo", "Tim foo", "Timmy@gmail.com", "个人"],["2", "Kim", "bar", "Kim bar", "Kim.bar@stackoverflow.com", "工作"],["2", "Kim", "bar", "Kim bar", "Kimmy@gmail.com", "个人"]],更多东西",去:这里"}---------------------

或者,当然,您可以开始 let data = ... 然后在某种循环中执行 data = finalFunction(data, nextCallback).

此功能严重依赖于 flatMap,并非在所有环境中都可用.MDN 页面建议替代方案,如果你需要他们.如果您仍在使用 Ramda,则可以使用 chain 函数.

<小时>

更新

您的回复选择使用 Ramda 而不是这个原始的 ES6 版本.我认为,如果你打算使用 Ramda,你可能可以通过大量的 Ramda 函数来简化很多.我猜可以做更多的事情,但我认为这更干净:

//辅助函数const transposeObj = (obj) =>地图 ((i) =>reduce((a, [k, v]) => ({ ...a, [k]: v[i] }), {}, toPairs(obj)),范围 (0, 长度 (值 (obj) [0]) ))//主功能const finalFunction = ({ array: [headers, ...rows], ...rest },打回来,变化 = 地图(管道(回调,道具('变化'),转置对象),行),allHeaders = uniq (concat (headers, chain (chain (keys), changes)))) =>({数组: concat([allHeaders], chain((行) =>地图 (管道 (对,reduce((r, [k, v]) => assocPath([indexOf(k, allHeaders)], v, r), row)),变化[indexOf(行,行)]),行)),...休息})const data = {array: [["#", "FirstName", "LastName"], ["1", "tim", "foo"], ["2", "kim", "bar"]],更多:'东西',去:'这里'}//伪装成 attmepconst callback1 = (row) =>({更改:{名字:[row[1][0].toUpperCase() + row[1].slice(1)]}})const callback2 = (row) =>({更改:{全名:[`${row[1]} ${row[2]}`]}})const callback3 = (row) =>({更改: {电子邮件: [`${row[1]}.${row[2]}@stackoverflow.com`,`${row[1]}my@gmail.com`],MailType: ["工作","个人"]}})控制台 .log (finalFunction (data, callback1))控制台 .log (finalFunction (data, callback2))控制台 .log (finalFunction (data, callback3))

<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></脚本><script>const {map, reduce, toPairs, range, length, values, pipe, prop, uniq, concat, chain, keys, assocPath, indexOf} = R </script>

I want to create a dynamic function that can simplify work with array-transforming callbacks in order to fill and expand 2d Array.

Outlining the challenge

I would like to create a function like this

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

which can pass all three scenarios given the following set input parameters (part of an input 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".

Scenario 1: Transforming existing Array without expansion

// 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) 

should return

{
  array: [
  ["#","FirstName","LastName"]
  ["1","Tim","foo"],
  ["2","Kim","bar"]
  ],
  header: "FirstName",
  ...args
}

Scenario 2: Transforming existing Array with horizontal expansion

// 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) 

should return

{
  array: [
  ["#","FirstName","LastName","FullName"]
  ["1","Tim","foo","Tim Foo"],
  ["2","Kim","bar","Kim Bar"]
  ],
  header: "FirstName",
  ...args
}

Scenario 3: Transforming existing Array with vertical and horizontal expansion

// return given for the second row
callback3 => {
  changes: {
    "Email": ["tim.foo@stackoverflow.com","timmy@gmail.com"],
    "MailType": ["Work","Personal"]
  }
};
// return given for the third row
callback3 => {
  changes: {
    "Email": ["kim.bar@stackoverflow.com","kimmy@aol.com"],
    "MailType": ["Work","Personal"]
  }
};

finalFunction({ array, header, ...args }, callback3) 

should return

{
  array: [
  ["#","FirstName","LastName","Email","MailType"]
  ["1","Tim","foo","tim.foo@stackoverflow.com","Work"],
  ["1","Tim","foo","timmy@gmail.com","Personal"],
  ["2","Kim","bar","kim.bar@stackoverflow.com","Work"],
  ["2","Kim","bar","kimmy@aol.com","Personal"]
  ],
  header: "FirstName",
  ...args
}

Current progress

The wonderful @Scott Sauyet has helped me create a merging function between a 2d array and a changes 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
})

This works great for scenario #1. However, I can't seem to get this solution to autocreate headers if they are not part of the original array.

I have however made progress on the Vertical expansion described in scenario 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
    })

In my mind, the newly created logic would have to.

  1. Call the callback Function in order to retrieve the entries that could be missing for the first Header row
  2. Add missing keys of "changes" object to the header row
  3. Reduce over the array skipping the first row
  4. Always assume an arrayblock (as it's fine if an arrayblock only has the length one, which would cover scenarios #1 and #2)
  5. Assure that the arrayblock length doesn't need "length" parameter to be supplied by the callback, but rather be captured from the arraylength of values supplied for each key in the "changes" obj

Current Challenges

  1. The current solution of vertical expansion requires the callback to provide a "length" parameter in it's result in order to get the correct number of repetitions for each source row.
  2. The current function to merge the "changes" with the sourceArray does not autocreate new Headers if they couldn't be found in the first row of the source array.

I feel that this is doable and it would provide great benefits to the current project I am working on, as it applies a standardized interface for all array-fillings/expansions.

However I feel stuck, particularly on how to cover all 3 scenarios in a single function.

Any ideas or insights would be greatly appreciated.

解决方案

Here's one attempt. I may still be missing something here, because I entirely ignore your header parameter. Is it somehow necessary, or has that functionality now been captured by the keys in the change objects generated by your callback functions?

// 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]}@stackoverflow.com`,`${row[1]}my@gmail.com`],MailType: ["Work","Personal"]}}) 

console .log (finalFunction (data, callback1))
console .log (finalFunction (data, callback2))
console .log (finalFunction (data, callback3))

This uses the helper function transposeObj, which converts the changes lists into something I find more useful. It turns this:

{
  Email: ["tim.foo@stackoverflow.com", "timmy@gmail.com"],
  MailType: ["Work", "Personal"]
}

into this:

[
  {Email: "tim.foo@stackoverflow.com", MailType: "Work"}, 
  {Email: "timmy@gmail.com",           MailType: "Personal"}
]

The main function accepts your callback and a data object with an array parameter, from which it extracts headers and rows arrays (as well as keeping track of the remaining properties in rest.) It derives the changes by calling the transposeObj helper against the changes property result of calling the callback against each row. Using that data it finds the new headers by getting all the keys in the changes objects, and removing all that are already in the array then reducing to a set of unique values. Then it appends these new ones to the existing headers to yield allHeaders.

In the body of the function, we return a new object using ...rest for the other parameters, and update array by starting with this new list of headers then flat-mapping rows with a function that takes each of those transposed object and adding all of its properties to a copy of the current row, matching indices with the the allHeaders to put them in the right place.

Note that if the keys of the transposed change object already exists, this technique will simply update the corresponding index in the output.

We test above with three dummy callback functions meant to just barely cover your examples. They are not supposed to look anything like your production code.

We run each of them separately against your input, generating three separate result objects. Note that this does not modify your input data. If you want to apply them sequentially, you could do something like:

const data1 = finalFunction (data, callback1)
console.log (data1, '-----------------------------------')
const data2 = finalFunction (data1, callback2)
console.log (data2, '-----------------------------------')
const data3 = finalFunction (data2, callback3)
console.log (data3, '-----------------------------------')

to get a result something like:

{
    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@stackoverflow.com", "Work"],
        ["1", "Tim", "foo", "Tim foo", "Timmy@gmail.com", "Personal"],
        ["2", "Kim", "bar", "Kim bar", "Kim.bar@stackoverflow.com", "Work"],
        ["2", "Kim", "bar", "Kim bar", "Kimmy@gmail.com", "Personal"]
    ],
    more: "stuff",
    goes: "here"
}
-----------------------------------

Or, of course, you could just start let data = ... and then do data = finalFunction(data, nextCallback) in some sort of loop.

This function depends heavily on flatMap, which isn't available in all environments. The MDN page suggests alternatives if you need them. If you're still using Ramda, the chain function will serve.


Update

Your response chose to use Ramda instead of this raw ES6 version. I think that if you are going to use Ramda, you can probably simplify quite a bit with a heavier dose of Ramda functions. I'm guessing more can be done, but I think this is cleaner:

// 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]}@stackoverflow.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>

这篇关于如何使用 Ramda.js 中的回调函数动态填充/扩展二维数组的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆