MattisW
MattisW

Reputation: 107

How to use Ramda.js to dynamically insert into a 2d Array in Javascript

I have the following state

{
  "array": [
    [
      "Name",
      "Phone",
      "Email"
    ]
  ],
  "indexes": {
    "Name": 0,
    "Phone": 1,
    "Email": 2
  },
  "tempInput": ["[email protected]","[email protected]"],
  "tempDestination": "Email"
}

Now I want to create a function taking the object and dynamically inserting the input values as new rows into the 2d array with at the designated destination, ultimately returning

{
  "array": [
    [
      "Name",
      "Phone",
      "Email"
    ],
    [
      "",
      "",
      "[email protected]"
    ],
    [ 
      "",
      "",
      "[email protected]"
    ]
  ],
  "indexes": {
    "Name": 0,
    "Phone": 1,
    "Email": 2
  }
}

To solve this I've been taking a look at the docs and discovered

R.lensProp and R.view. This combination provides me with the starting ground (i.e. getting the correct index for the provided destination, but I'm stuck from there onwards.

const addInputToArray = ({ array, indexes, tempDestination, tempInput, }) => {
  // Use Lense and R.view to get the correct index
  const xLens = R.lensProp(tempDestination);
  R.view(xLens, indexes), // contains index

  // Insert the 2 Rows into newArray - I'm lost on this part.
  const newArray = array.push( // newRows )

  return {
    array: newArray,
    indexes: indexes
  }
}

I know that I somehow have to loop over the input, for example with a map function. But I'm lost on what the map function should execute to get to the correct array result.

It'd be awesome if you could help me out here?

Upvotes: 2

Views: 620

Answers (1)

Scott Sauyet
Scott Sauyet

Reputation: 50807

Update

Comments have asked for additional requirements (ones I did expect.) That requires a slightly different approach. Here's my take:

const addInputToArray = (
  { array, indexes, tempDestination, tempInput, ...rest},
  index = indexes[tempDestination]
) => ({
  array: tempInput .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]) )] ),
    array
  ),
  indexes,
  ...rest
})

const state = {array: [["Name", "Phone", "Email"]], indexes: {Name: 0,
Phone: 1, Email: 2}, tempInput: ["[email protected]","[email protected]"],
tempDestination: "Email"}

const state2 = addInputToArray (state)

console .log (
  state2
)

const state3 = addInputToArray({
  ...state2,
  tempInput: ['Wilma', 'Fred', 'Betty'],
  tempDestination: 'Name'
})

console .log (
  state3
)

const state4 = addInputToArray({
  ...state3,
  tempInput: [123, , 456],
  //              ^------------- Note the gap here
  tempDestination: 'Phone'
})

console .log (
  state4
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script> const {update, concat, map, always} = R;                    </script>

Note that in the original version (below), I found no need for Ramda functions. Here, update makes it cleaner, and if I'm using Ramda, I might as well use it wherever it simplifies things, so I also use concat in place of Array.prototype.concat and use map (always(''), array[0]) instead of something like Array (array [0] .length) .fill (''). I find those make the code somewhat easier to read.

You could very easily remove those last ones, but if you were to write this without a library, I would suggest that you write something similar to update, as calling that makes the code cleaner than it likely would be with this inlined.

Alternative API

I could be way off-base here, but I do suspect that the API you're attempting to write here is still more complex than your basic requirements would imply. That list of indices strikes me as a code smell, a workaround more than a solution. (And in fact, it's easily derivable from the first row of the array.)

For instance, I might prefer an API like this:

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

const state = {
  array: [["Name", "Phone", "Email"]], 
  changes: {Email: ["[email protected]","[email protected]"], Name: ['Wilma', 'Fred', 'Betty']}
}

const state2 = addInputToArray (state)

console .log (
  state2
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script> const {update, concat, map, always} = R;                    </script>

But regardless, it still led to an interesting problem, so thanks!

Explanation

A comment asked about the parameters to reduce in this version. To explain, I'll first take one step back. I'm a big fan of functional programming. That has a lot of meanings, and a lot of implications, but the relevant one here is that I prefer to write as much as possible with expressions rather than statements. Statements, such as foo = 1, or if (a) {doB()} are not susceptible to easy analysis, since they introduce timing and sequencing to an analysis that otherwise could proceed in a mathematical fashion.

To support this, when I can, I write functions whose bodies consist of a single expression, even if it's fairly complex. I can't always do this in a readable manner, and in those cases, I choose readability instead. Often I can, though, as I manage to do here, but in order to support that, I might add default parameters to functions to support what would otherwise be assignment statements. The purely functional language Haskell, has a convenient syntax for such temporary assignments:

let two = 2; three = 3 
    in two * three  -- 6

Javascript does not offer such a syntax. (Or really that syntax had such problems that it's been deprecated.) Adding a variable with a default value in a parameter is a reasonable work-around. It allows me to do the equivalent of defining a local variable to avoid repeated expressions.

If we had this:

const foo = (x) =>
  (x + 1) * (x + 1) 

We do a repeated calculation here (x + 1). Obviously here that's minor, but in other cases, they could be expensive, so we might write this replacement:

const foo = (x) => {
  const next = x + 1
  return next * next
}

But now we have multiple statements, something I like to avoid when possible. If instead, we write this:

const foo = (x, next = x + 1) =>
  next * next

we still save the repeated calculation, but have code more susceptible to more straightforward analysis. (I know that in these simple cases, the analysis is still straightforward, but it's easy to envision how this can grow more complex.)

Back to the actual problem. I wrote code like this:

<expression1> .reduce ((a, [k, vs], _, __, index = array [0] .indexOf (k)) => <expression2>

As you point out, Array.prototype.reduce takes up to four parameters, the accumulator, the current value, the current index, and the initial array. I add index as a new default parameter to avoid either calculating it several times or adding a temporary variable. But I don't care about the current index or the initial array. I could have written this as ((a, [k, vs], ci, ia, index = <expression>) (with "ci" for "current index" and "ia" for "initial array") or anything similar. I have to supply these if I want to add index as a fifth parameter, but I don't care about them. I won't use those variables.

Some languages with pattern-matching syntaxes offer the underscore as a useful placeholder here, representing a variable that is supplied by the caller but not used. While JS doesn't support that syntactically, the underscore (_) is a legal variable name, as is a pair of them (__). People doing functional programming often use them as they would in pattern-matching languages. They simply announce that something will be passed here but I don't care any more about it. There are proposals1 to add a similar syntactic feature to JS, and if that comes to pass I will likely use that instead.

So if you see _ as a parameter or __ or (rarely) _1, _2, _3, etc., they are often a simple workaround for the lack of a placeholder in JS. There are other uses of the _: as you note there is a convention of using it to prefix private object properties. It is also the default variable name for the library Underscore as well as for its clone-that's-grown, lodash. But there is little room for confusion between them. While you might conceivably pass Underscore as an argument to a function, you will then use it as a variable in the body, and it should be clear what the meaning is.

(And to think I was originally going to write this explanation in a comment!)


1 If you're interested, you can see a discussion of the various proposals

Original Answer

Without more information on the underlying requirements, I would start simple. This function seems to do what you want:

const addInputToArray = (
  { array, indexes, tempDestination, tempInput, ...rest}, 
  index = indexes[tempDestination]
) => ({
  array: [
    array [0], 
    ...tempInput .map (v => array [0] .map ((s, i) => i == index ? v : ''))
  ],
  indexes,
  ...rest
})

const state = {array: [["Name", "Phone", "Email"]], indexes: {Name: 0, Phone: 1, Email: 2}, tempInput: ["[email protected]","[email protected]"], tempDestination: "Email"}

console .log (
  addInputToArray(state)
)

But I wouldn't be surprised to find that there are more requirements not yet expressed. This version builds up the additional elements from scratch, but it seems likely that you might want to call it again with a different tempInput and tempDestination and then append to those. If that's the case, then this won't do. But it might serve as a good starting place.

Upvotes: 1

Related Questions