Reputation: 107
I want to create a dynamic function that can simplify work with array-transforming callbacks in order to fill and expand 2d Array.
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".
// 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
}
// 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
}
// return given for the second row
callback3 => {
changes: {
"Email": ["[email protected]","[email protected]"],
"MailType": ["Work","Personal"]
}
};
// return given for the third row
callback3 => {
changes: {
"Email": ["[email protected]","[email protected]"],
"MailType": ["Work","Personal"]
}
};
finalFunction({ array, header, ...args }, callback3)
should return
{
array: [
["#","FirstName","LastName","Email","MailType"]
["1","Tim","foo","[email protected]","Work"],
["1","Tim","foo","[email protected]","Personal"],
["2","Kim","bar","[email protected]","Work"],
["2","Kim","bar","[email protected]","Personal"]
],
header: "FirstName",
...args
}
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.
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.
Upvotes: 0
Views: 398
Reputation: 50807
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]}[email protected]`],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: ["[email protected]", "[email protected]"],
MailType: ["Work", "Personal"]
}
into this:
[
{Email: "[email protected]", MailType: "Work"},
{Email: "[email protected]", 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", "[email protected]", "Work"],
["1", "Tim", "foo", "Tim foo", "[email protected]", "Personal"],
["2", "Kim", "bar", "Kim bar", "[email protected]", "Work"],
["2", "Kim", "bar", "Kim bar", "[email protected]", "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.
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]}[email protected]`],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>
Upvotes: 2
Reputation: 107
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
};
};
Upvotes: 2