1

Empty strings '' are randomly placed in multiple arrays. I would like to replace the '' with values in data.X.

const data = {
  a: ['a', '', 'a', 'a', ''],
  b: ['b', 'b', ''],
  c: ['', '', 'c', 'c'],
  X: ['X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8']
}

I expect it to transform as follows. Please note that X1, X2 ... are placed across the arrays of a b c.

const dataTransformed = {
  a: ['a', 'X1', 'a', 'a', 'X2'],
  b: ['b', 'b', 'X3'],
  c: ['X4', 'X5', 'c', 'c']
}

You can suppose that there are enough length of X to satisfy the number of empty strings. I have been testing the R.lensProp, but it seems unable to take multiple keys.

const replaceEmptyString = X => R.map(R.replace(/''/g, X)) 
const setX = R.map(R.over(R.lensProp('a', 'b', 'c'), replaceEmptyString)) 

const replaceStringsLens = R.lens(replaceEmptyString, setX)
console.log (replaceStringsLens(data)) //ERROR

I appreciate your guidance. REPL

2 Answers 2

3

One interesting approach is to keep state in (defaulted) parameters:

const fillWithX = ({X, ...rest}, i = 0, next = () => X [i++] || '') => 
  R.map (R.map (v => v || next ()), rest)

const data = {
  a: ['a', '', 'a', 'a', ''],
  b: ['b', 'b', ''],
  c: ['', '', 'c', 'c'],
  X: ['X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8']
}

console .log (fillWithX (data))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>

(Note that the || '' handles the case where there aren't enough Xs. You might want a different default, or some other handling altogether.)

Here Ramda's map shines, especially for dealing with objects. We can skip Ramda by replacing the outer map with a dance of Object.entries -> Array.prototype.map -> Object.fromEntries and the inner one with Array.prototype.map, like this:

const fillWithX = ({X, ...rest}, i = 0, next = () => X [i++] || '') => 
  Object .fromEntries (
    Object .entries (rest) 
      .map (([k, vs]) => [k, vs .map (a => a || next ())])
  )

But that definitely seems less elegant than the Ramda approach.


There are two concerns here. First, there can be problems with default parameters. If you had three different data versions, you could not successfully call

[data1, data2, data3] .map (fillWithX)

because the index argument that Array.prototype.map supplies to its callback would override the i parameter to fillWithX and its array parameter would override next. And map is not the only place this could happen. I often choose to ignore this if I know how my function will be called. When I need to deal with it, the standard technique is to write a private helper and a public function that calls it with only the required parameters. But Ramda offers call, which can sometimes make this cleaner. We could fix it like this:

const fillWithX = (data) => R.call (
  ({X, ...rest}, i = 0, next = () => X [i++] || '') => 
    R.map (R.map (v => v || next ()), rest),
  data
)

Here we create a new function which takes our data parameter and calls R.call with the function above and the data argument. This is a fairly universal technique.

The second problem is more fundamental. I'm not sure that this function really makes sense. It's not the implementation; it's the basic idea. The issue is that for all reasonable purposes, these two objects are equivalent:

const data = {
  a: ['a', '', 'a', 'a', ''],
  b: ['b', 'b', ''],
  c: ['', '', 'c', 'c'],
  X: ['X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8']
}

and

const data = {
  b: ['b', 'b', ''],
  a: ['a', '', 'a', 'a', ''],
  c: ['', '', 'c', 'c'],
  X: ['X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8']
}

But this function will yield different values for the two simply because the keys are in different order.

If the goal is simply to fill all holes with distinct values from X, then this isn't an issue. But if the specific X values appearing in given holes is important, then there is a fundamental problem. And the only practical solution I see to that would involve sorting the keys -- an ugly and somewhat unintuitive process.

So I think you might want to give this issue a bit of thought before you use this or any other implementation of the idea.

Using an iterator

A comment from Thankyou points to a simpler way to handle that state, using an iterator. Any array can be turned into an iterator by calling .values(). This built-in approach is definitely cleaner:

const fillWithX = ({X, ...rest}, it = X .values()) => 
  R .map (R .map (v => v || it .next () .value || ''), rest)

This still has the issue with default parameters and can be solved the same way if desired. And of course it doesn't change the fundamental issue described above.

But it definitely is cleaner.

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

6 Comments

If you're not wedded to that data structure, and the order is important, you can make a minor change that would allow you to call with the equivalent of fillWithX (data.X, [data.a, data.b, data.c]), using only a change to the parameters: const fillWithX = (X, rest, i = 0, next = () => X [i++] || '') => R.map (R.map (v => v || next ()), rest)
fillWithX could be applied to numbers of 0 just like '' which is luckily useful to me. Thanks.
And if you need something more complex, it's easy enough to alter the body of nextl
Nice post, Scott. You may also like const fillWithX = ({X, ...rest}, i = X.values()) => R.map (R.map (v => v || i.next().value), rest)
@Thankyou: yes, that is nicer. Updated. I tend to avoid iterators precisely because of their stateful nature. But here, of course that's just what's wanted.
|
3

Since you need preserve the current place in X, or event mutate the array, I don't see any advantage in doing it with Ramda.

Use destructuring to separate X from the rest of the object, and clone it using spread, so we won't mutate the original object.

From the rest of the object, get an array of [key, value] pairs using Object.entries() (or R.toPairs), map the pairs, and map the array of each pair (v). If an item is empty, use Array.shift() to get the value from the cloned X.

Convert back to an object using Object.fromEntries().

const fn = ({ X, ...rest }) => {
  const x = [...X] // clone X to avoid mutating the original
  
  return Object.fromEntries( // convert back to object
    Object.entries(rest) // convert to pairs of [key, value]
      .map(([k, v]) => // map the pairs
        // map the values and replace empty values from the cloned X array
        [k, v.map(item => item || x.shift())]
      )
  )
}

const data = {
  a: ['a', '', 'a', 'a', ''],
  b: ['b', 'b', ''],
  c: ['', '', 'c', 'c'],
  X: ['X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8']
}

const result = fn(data)

console.log(result)

And as pointed @ScottSauyet, using R.map makes the less verbose:

const fn = ({ X, ...rest }) => {
  const x = [...X] // clone X to avoid mutating the original
  // map the values and replace empty values from the cloned X array
  return R.map(v => v.map(item => item || x.shift()), rest)
}

const data = {
  a: ['a', '', 'a', 'a', ''],
  b: ['b', 'b', ''],
  c: ['', '', 'c', 'c'],
  X: ['X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8']
}

const result = fn(data)

console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js" integrity="sha512-3sdB9mAxNh2MIo6YkY05uY1qjkywAlDfCf5u1cSotv6k9CZUSyHVf4BJSpTYgla+YHLaHG8LUpqV7MHctlYzlw==" crossorigin="anonymous"></script>

4 Comments

Ramda certainly won't make it easier to deal with mutating the Xs, But see my answer for why I think its map is still useful here.
R.map certainly works nicely, when you want to start and end with an object.
@OriDrori I read it over and over to understand what it was describing. It's always grateful to find your tips, sir.
Nice work, Ori. Could also use X.values() instead of copying X

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.