How to dynamically fill/expand a 2d Array using a callback function in Ramda.js

自闭症网瘾萝莉.ら 提交于 2020-01-06 05:45:05

问题


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.


回答1:


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>



回答2:


Based on the great input from Scott, I wanted to share a version of this functionality which doesn't utilize flatMap, but Ramda functions instead (thereby allowing more environment support.

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
  };
};


来源:https://stackoverflow.com/questions/58362118/how-to-dynamically-fill-expand-a-2d-array-using-a-callback-function-in-ramda-js

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!