2

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?

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

7 Comments

This is a great start Scott, thanks very much. I wasn't aware of the second argument available for the map function. Very handy indeed. You are right though. One requirement I have is that already existing row values are not overwritten So if the function ran over the following array js [["Name", "Phone", "Email"],["Peter","1234",""]
the result should be js [["Name", "Phone", "Email"],["Peter","1234","[email protected]"],["","","[email protected]] Right now the 2nd map always uses the header function for each new line, so I can't iterate over the original.
Ramda's list functions do not include that extra parameter, for some pretty good reasons, but Ramda has addIndex, which creates a function which does have it.
@user3052826: Updated with an answer that covers the new requirements.
Awesome, thank you so much! You are right, probably more requirements will show up, but I'll take an in-depth look at your solutions to understand more about reduce and how you solve the issue.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.