4

Is there an elegant, functional way to turn this array:

[ 1, 5, 9, 21 ]

into this

[ [1, 5], [5, 9], [9, 21] ]

I know I could forEach the array and collect the values to create a new array. Is there an elegant way to do that in _.lodash without using a forEach?

pyronaur
  • 3,515
  • 6
  • 35
  • 52
  • why do you want this ? what's your goal ? – niceman May 24 '17 at 14:45
  • 1
    https://lodash.com/docs/4.17.4#chunk. BTW, name of the operation is *usually* called `partition`, that link was the first google hit for 'lodash partition array'. – Jared Smith May 24 '17 at 14:46
  • 3
    @niceman - the function would best be called 'adjacent pairs' - I've used that concept before. Jared Smith - that's not the same. Look at OP's example – aaaaaa May 24 '17 at 14:47
  • @aaaaaa you're right, I didn't look closely enough at the desired output. – Jared Smith May 24 '17 at 14:47
  • duplicate:https://stackoverflow.com/questions/31973278/iterate-an-array-as-a-pair-current-next-in-javascript – Tree Nguyen May 24 '17 at 14:51
  • 1
    @TreeNguyen that's not an exact duplicate but a similar question, that question wants just to iterate, OP here wants to return a new array – niceman May 24 '17 at 14:54
  • @Norris - keep in mind this is low-enough level functionality that you shouldn't be terribly concerned about readability (unless you're just trying to learn). It's something you'd wrap in an intuitive function name, write tests for, and call it good. – aaaaaa May 24 '17 at 15:08
  • 1
    @aaaaaa Yeah, I'm always trying to learn, so doing things the hard/readable way is the path I'm on. Otherwise - the ramda solution would work out just flawlessly. – pyronaur May 24 '17 at 15:11

8 Answers8

6

You could map a spliced array and check the index. If it is not zero, take the predecessor, otherwise the first element of the original array.

var array = [1, 5, 9, 21],
    result = array.slice(1).map((a, i, aa) => [i ? aa[i - 1] : array[0], a]);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

An even shorter version, as suggested by Bergi:

var array = [1, 5, 9, 21],
    result = array.slice(1).map((a, i) => [array[i], a]);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
5

A fast approach using map would be:

const arr = [ 1, 5, 9, 21 ];

const grouped = arr.map((el, i) => [el, arr[i+1]]).slice(0, -1);

console.log(grouped);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Diego
  • 816
  • 7
  • 17
2

This is easily done with array.reduce. What the following does is use an array as aggregator, skips the first item, then for each item after that pushes previous item and the current item as a pair to the array.

const arr = [ 1, 5, 9, 21 ];
const chunked = arr.reduce((p, c, i, a) => i === 0 ? p : (p.push([c, a[i-1]]), p), []);

console.log(chunked);

An expanded version would look like:

const arr = [1, 5, 9, 21];
const chunked = arr.reduce(function(previous, current, index, array) {
  if(index === 0){
    return previous;
  } else {
    previous.push([ current, array[index - 1]]);
    return previous;
  }
}, []);

console.log(chunked);
Joseph
  • 117,725
  • 30
  • 181
  • 234
1

If you're willing to use another functional library 'ramda', aperture is the function you're looking for.

Example usage taken from the ramda docs:

R.aperture(2, [1, 2, 3, 4, 5]); //=> [[1, 2], [2, 3], [3, 4], [4, 5]]
R.aperture(3, [1, 2, 3, 4, 5]); //=> [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
R.aperture(7, [1, 2, 3, 4, 5]); //=> []
aaaaaa
  • 1,233
  • 13
  • 24
  • Ramda actually looks really nice. And it does exactly what I was looking for. – pyronaur May 24 '17 at 14:55
  • I much prefer it over lodash/fp - but the maintainers are less active and spend a lot of time discussing as opposed to coding. Choose your poison :) – aaaaaa May 24 '17 at 14:56
0

I noticed that the current solutions, in a way, all look ahead or behind (arr[i + 1] or arr[i - 1]).

It might be useful to also explore an approach that uses reduce and an additional array, defined within a function's closure, to store a to-be-completed partition.

Notes:

  • Not a one liner, but hopefully easy to understand
  • part doesn't have to be an array when working with only 2 items, but by using an array, we extend the method to work for n-sized sets of items
  • If you're not a fan of shift, you can use a combination of slice and redefine part, but I think it's safe here.
  • partitions with a length less than the required number of elements are not returned

const partition = partitionSize => arr => {
  const part = [];
  
  return arr.reduce((parts, x) => {
    part.push(x);
    
    if (part.length === partitionSize) {
      parts.push(part.slice());
      part.shift();
    }
    
    return parts;
  }, []);
};

const makePairs = partition(2);
const makeTrios = partition(3);

const pairs = makePairs([1,2,3,4,5,6]);
const trios = makeTrios([1,2,3,4,5,6]);


console.log("partition(2)", JSON.stringify(pairs));
console.log("partition(3)", JSON.stringify(trios));
user3297291
  • 22,592
  • 4
  • 29
  • 45
0

You may do as follows with just a sinle liner of .reduce() with no initial;

var arr = [ 1, 5, 9, 21 ],
  pairs = arr.reduce((p,c,i) => i == 1 ? [[p,c]] : p.concat([[p[p.length-1][1],c]]));
console.log(pairs);
Redu
  • 25,060
  • 6
  • 56
  • 76
  • That doesn't work on empty arrays. Why not just use an initial argument? It's an even shorter oneliner. – Bergi May 24 '17 at 20:05
  • @Bergi Yes of course obviously if you don't want to fall into the pit of "Reduce of empty array with no initial value" you better take precautions.. but this one is obviously a mock up pseudo code. However the main problem is basically when the input is `[1]`. – Redu May 24 '17 at 20:22
0

I'm sure there is an elegant way, programmatically, but, mathematically I can't help seeing that each new pair has an index difference of 1 from the original array.

If you (later) have the need to turn your array [ 1, 5, 9, 21, 33 ] into [ [1, 9], [5, 21], [9, 33] ], you can use the fact that the difference between the indices is 2.

If you create code for the index difference of 1, extending this would be easy.

0

Here's slide which has two parameters to control the size of the slice and how many elements are dropped between slices

slide differs from other answers here by giving you these control parameters. other answers here are limited to producing only a slices of 2, or incrementing the slice by 1 each time

// take :: (Int, [a]) -> [a]
const take = (n, xs) =>
  xs.slice(0, n)

// drop :: (Int, [a]) -> [a]
const drop = (n, xs) =>
  xs.slice(n)
  
// slice :: (Int, Int, [a]) -> [[a]]
const slide = (m, n, xs) =>
  xs.length > m
    ? [take(m, xs), ...slide(m, n, drop(n, xs))]
    : [xs]
    
const arr = [0,1,2,3,4,5,6]

// log helper improves readability of output in stack snippet
const log = x => console.log(JSON.stringify(x))

log(slide(1, 1, arr))
// [[0],[1],[2],[3],[4],[5],[6]]

log(slide(1, 2, arr))
// [[0],[2],[4],[6]]

log(slide(2, 1, arr))
// [[0,1],[1,2],[2,3],[3,4],[4,5],[5,6]]

log(slide(2, 2, arr))
// [[0,1],[2,3],[4,5],[6]]

log(slide(3, 1, arr))
// [[0,1,2],[1,2,3],[2,3,4],[3,4,5],[4,5,6]]

log(slide(3, 2, arr))
// [[0,1,2],[2,3,4],[4,5,6]]

log(slide(3, 3, arr))
// [[0,1,2],[3,4,5],[6]] 

If for some reason you didn't want slide to include partial slices, (slices smaller than m), we could edit it as such

// slice :: (Int, Int, [a]) -> [[a]]
const slide = (m, n, xs) =>
  xs.length > m
    ? [take(m, xs), ...slide(m, n, drop(n, xs))]
    : [] // <- return [] instead of [xs]

log(slide(2, 2, arr))
// now prints: [[0,1],[2,3],[4,5]]
// instead of: [[0,1],[2,3],[4,5],[6]]
Mulan
  • 129,518
  • 31
  • 228
  • 259