3

I have an array of arrays and want to write a function that returns the top x number of items, by taking items from each array in order.

Here is an example of what I'm after:

    const input = [
      ["1a", "2a", "3a", "4a", "5a"],
      ["1b", "2b", "3b", "4b", "5b"],
      ["1c", "2c", "3c", "4c", "5c"],
      ["1d", "2d", "3d", "4d", "5d"]
    ];

    const takeRoundRobin = count => arr => {
      // implementation here
    };

    const actual = takeRoundRobin(5)(input);

    const expected = [
      "1a", "1b", "1c", "1d", "2a"
    ];

I saw a suggestion to a Scala question that solved this using zip but in Ramda you can only pass 2 lists to zip.

cpoliver
  • 65
  • 5

4 Answers4

6

Here, Ramda's transpose can be your base. Add a dollop of unnest, a dash of take, and you get this:

const {take, unnest, transpose} = R

const takeRoundRobin = (n) => (vals) => take(n, unnest(transpose(vals)))

const input = [
  ['1a', '2a', '3a', '4a', '5a'],
  ['1b', '2b', '3b', '4b', '5b'],
  ['1c', '2c', '3c', '4c', '5c'],
  ['1d', '2d', '3d', '4d', '5d']
]

console.log(takeRoundRobin(5)(input))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.js"></script>

Note also that this can handle arrays of varying lengths:


If you want to be able to wrap around to the beginning and continue taking values, you could replace take with a recursiveTake like this:

const {take, unnest, transpose, concat } = R

//recursive take
const recursiveTake = (n) => (vals) => {
  const recur = (n,vals,result) =>
    (n<=0)
      ? result
      : recur(n-vals.length,vals,result.concat(take(n,vals)))
  return recur(n,vals,[]);
};

const takeRoundRobin = (n) => (vals) => 
  recursiveTake(n)(unnest(transpose(vals)));

const input = [
  ['1a', '2a', '3a', '4a'],
  ['1b'],
  ['1c', '2c', '3c', '4c', '5c'],
  ['1d', '2d']
]

console.log(takeRoundRobin(14)(input))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.js"></script>

Another version of that function, without the explicit recursion would look like:

const takeCyclic = (n) => (vals) => take(
  n,
  unnest(times(always(vals), Math.ceil(n / (vals.length || 1))))
)
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • I've got to an answer similar to yours (I've used flatten instead of unnest), but I was missing the circularity ([HMR's answer](https://stackoverflow.com/a/52875954/5157454) takes 22 from a structure of 20). How would you do that in ramda? – Ori Drori Oct 19 '18 at 04:26
  • 1
    @OriDrori I have updated Scott's answer (2nd example) that will recursively generate output. – HMR Oct 19 '18 at 07:25
  • 2
    @HMR: Another version of `recursiveTake`, without the explicit recursion, would look like `(n) => (vals) => take(n, unnest(times(always(vals), Math.ceil(n / (vals.length || 1)))))` ` – Scott Sauyet Oct 19 '18 at 13:20
  • 1
    @OriDrori: the suggestion from HMR or my version in the comments would do it. Note that using `unnest` here rather than `flatten` could be important. If the values in the inner arrays of the input were themselves arrays, `flatten` would merge them too. – Scott Sauyet Oct 19 '18 at 13:24
  • @ScottSauyet - nice! I'm still a ramda novice, I wasn't aware of unnest (and always, and times). How would you implement HMRs recursive solution with ramda? – Ori Drori Oct 19 '18 at 14:29
  • @OriDrori: Well, Ramda is not particularly designed for recursion, but, you could start with something like `takeRecursive = (n, vals, res = []) => n < 1 ? res : takeRecursive(n - vals.length, vals, concat(res, take(n, vals)))`, but, as with HMR's solution, this would have to be fixed to handle an empty input. – Scott Sauyet Oct 19 '18 at 16:09
2

Here's one way you can do it using recursion –

const None =
  Symbol ()

const roundRobin = ([ a = None, ...rest ]) =>
  // base: no `a`
  a === None
    ? []
  // inductive: some `a`
  : isEmpty (a)
    ? roundRobin (rest)
  // inductive: some non-empty `a`
  : [ head (a), ...roundRobin ([ ...rest, tail (a) ]) ]  

It works in a variety of cases –

const data =
  [ [ 1 , 4 , 7 , 9 ]
  , [ 2 , 5 ]
  , [ 3 , 6 , 8 , 10 , 11 , 12 ]
  ]

console.log (roundRobin (data))
// => [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ]

console.log (roundRobin ([ [ 1 , 2 , 3 ] ]))
// => [ 1 , 2 , 3 ]

console.log (roundRobin ([]))
// => []

Free variables are defined using prefix notation which is more familiar with functional style –

const isEmpty = xs =>
  xs.length === 0

const head = xs => 
  xs [0]

const tail = xs =>
  xs .slice (1)

Verify it works in your browser below –

const None =
  Symbol ()
  
const roundRobin = ([ a = None, ...rest ]) =>
  a === None
    ? []
  : isEmpty (a)
    ? roundRobin (rest)
  : [ head (a), ...roundRobin ([ ...rest, tail (a) ]) ]  

const isEmpty = xs =>
  xs.length === 0
  
const head = xs => 
  xs [0]
  
const tail = xs =>
  xs .slice (1)

const data =
  [ [ 1 , 4 , 7 , 9 ]
  , [ 2 , 5 ]
  , [ 3 , 6 , 8 , 10 , 11 , 12 ]
  ]
                   
console.log (roundRobin (data))
// => [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ]

console.log (roundRobin ([ [ 1 , 2 , 3 ] ]))
// => [ 1 , 2 , 3 ]

console.log (roundRobin ([]))
// => []

Here's another way using a secondary parameter with default assignment –

const roundRobin = ([ a = None, ...rest ], acc = []) =>
  // no `a`
  a === None
    ? acc
  // some `a`
  : isEmpty (a)
    ? roundRobin (rest, acc)
  // some non-empty `a`
  : roundRobin
      ( append (rest, tail (a))
      , append (acc, head (a))
      )

const append = (xs, x) =>
  xs .concat ([ x ])
Mulan
  • 129,518
  • 31
  • 228
  • 259
2

To demonstrate what you may have seen as the implementation in other languages, the applicative instance for a ZipList can be used to transpose the array, where a ZipList applies the functions contained in the ZipList in a pair-wise manner with the corresponding ZipList of values unlike the standard permutative version of ap for lists.

const ZipList = xs => ({
  getZipList: xs,
  map: f => ZipList(R.map(f, xs)),
  ap: other => ZipList(R.zipWith(R.applyTo, other.getZipList, xs))
})

ZipList.of = x => ZipList(new Proxy([], {
  get: (target, prop) =>
    prop == 'length' ? Infinity : /\d+/.test(prop) ? x : target[prop]
}))

This has an interesting requirement which is somewhat clunky to represent in JS, where the of function to produce a "pure" value needs to produce a ZipList containing a repeating list of the "pure" value, implemented here using a Proxy instance of an array.

The transpose can then be formed via:

xs => R.unnest(R.traverse(ZipList.of, ZipList, xs).getZipList)

After all of this, we have just reinvented R.transpose as per the answer from @scott-sauyet.

It is nevertheless an interesting implementation to be aware of.

(full example below)

const ZipList = xs => ({
  getZipList: xs,
  map: f => ZipList(R.map(f, xs)),
  ap: other => ZipList(R.zipWith(R.applyTo, other.getZipList, xs))
})

ZipList.of = x => ZipList(new Proxy([], {
  get: (target, prop) =>
    prop == 'length' ? Infinity : /\d+/.test(prop) ? x : target[prop]
}))

const fn = xs => R.unnest(R.traverse(ZipList.of, ZipList, xs).getZipList)

const input = [
  ["1a", "2a", "3a", "4a", "5a"],
  ["1b", "2b", "3b", "4b", "5b"],
  ["1c", "2c", "3c", "4c", "5c"],
  ["1d", "2d", "3d", "4d", "5d"]
];

const expected = [
  "1a", "1b", "1c", "1d", "2a"
];

const actual = R.take(5, fn(input))

console.log(R.equals(expected, actual))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>
Scott Christopher
  • 6,458
  • 23
  • 26
1

Not sure what Ramda functions to use to address this particular problem but here is an answer not using Ramda that'll only work if all arrays are the same length:

const input = [
  ['1a', '2a', '3a', '4a', '5a'],
  ['1b', '2b', '3b', '4b', '5b'],
  ['1c', '2c', '3c', '4c', '5c'],
  ['1d', '2d', '3d', '4d', '5d'],
];

const takeRoundRobin = (count) => (arr) => {
  const recur = (arr, current, count, result) =>
    (current === count)
      ? result 
      : recur(
        arr,
        current + 1,
        count,
        result.concat(
          arr
            [current % arr.length]//x value
            [//y value
              Math.floor(current / arr.length) %
                (arr.length + 1)
            ],
        ),
      );
  return recur(arr, 0, count, []);
};

console.log(takeRoundRobin(22)(input));
HMR
  • 37,593
  • 24
  • 91
  • 160
  • I'm not sure "according to Ramda" means anything. Ramda (disclaimer: I'm a Ramda author) is simply a library that offers a collection of useful FP functions. – Scott Sauyet Oct 18 '18 at 19:19
  • @ScottSauyet What I meant was I don't know what functions to use from Ramda that'll help (updated my answer). Changed your answer to address OP's question (hope you don't mind). – HMR Oct 19 '18 at 07:26