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.