最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Take top X total items in a round robin from multiple arrays with Ramda - Stack Overflow

programmeradmin3浏览0评论

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.

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.

Share Improve this question asked Oct 18, 2018 at 13:33 cpolivercpoliver 655 bronze badges
Add a ment  | 

4 Answers 4

Reset to default 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./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./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))))
)

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

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./ajax/libs/ramda/0.25.0/ramda.min.js"></script>

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

发布评论

评论列表(0)

  1. 暂无评论