23

In the question Iterate a list as pair (current, next) in Python, the OP is interested in iterating a Python list as a series of current, next pairs. I have the same problem, but I'd like to do it in JavaScript in the cleanest way possible, perhaps using lodash.

It is easy to do this with a simple for loop, but it doesn't feel very elegant.

for (var i = 0; i < arr.length - 1; i++) {
  var currentElement = arr[i];
  var nextElement = arr[i + 1];
}

Lodash almost can do this:

_.forEach(_.zip(arr, _.rest(arr)), function(tuple) {
  var currentElement = tuple[0];
  var nextElement = tuple[1];
})

The subtle problem with this that on the last iteration, nextElement will be undefined.

Of course the ideal solution would simply be a pairwise lodash function that only looped as far as necessary.

_.pairwise(arr, function(current, next) {
  // do stuff 
});

Are there any existing libraries that do this already? Or is there another nice way to do pairwise iteration in JavaScript that I haven't tried?


Clarification: If arr = [1, 2, 3, 4], then my pairwise function would iterate as follows: [1, 2], [2, 3], [3, 4], not [1, 2], [3, 4]. This is what the OP was asking about in the original question for Python.

Community
  • 1
  • 1
therealrootuser
  • 7,249
  • 6
  • 27
  • 43
  • 3
    Really not sure why you're investing so much thought in this. The idomatic JavaScript method is simply `array.forEach(function (item, index) { var next = array[index + 1]; ... });` – meager Aug 12 '15 at 18:58
  • It may not be important, but I'm curious what the overall goal is in iterating in this way? – sparrow Aug 12 '15 at 19:20
  • 2
    @sparrow -- Today it was asserting the call order of some stubs in a unit test. I've needed pairwise iteration in several applications in the past in other languages (for example bioinformatics code in Python), but I've never been wholly satisfied with the JavaScript solutions out there. – therealrootuser Aug 12 '15 at 19:34

14 Answers14

17

Just make the "ugly" part into a function and then it looks nice:

arr = [1, 2, 3, 4];

function pairwise(arr, func){
    for(var i=0; i < arr.length - 1; i++){
        func(arr[i], arr[i + 1])
    }
}

pairwise(arr, function(current, next){
    console.log(current, next)
})

You can even slightly modify it to be able to make iterate all i, i+n pairs, not just the next one:

function pairwise(arr, func, skips){
    skips = skips || 1;
    for(var i=0; i < arr.length - skips; i++){
        func(arr[i], arr[i + skips])
    }
}

pairwise([1, 2, 3, 4, 5, 6, 7], function(current,next){
    console.log(current, next) // displays (1, 3), (2, 4), (3, 5) , (4, 6), (5, 7)
}, 2)
meetar
  • 6,683
  • 7
  • 39
  • 68
juvian
  • 15,212
  • 2
  • 30
  • 35
  • Perhaps a better third parameters would be a `windowLength` to adjust the size of the sliding window. That way someone could not just do `pairwise` iteration, but do a sliding group like `[1, 2, 3]`, `[2, 3, 4]`. But I like the general idea here. Would you consider submitting a PR on lodash? – therealrootuser Aug 12 '15 at 19:22
  • @mattingly890 yeah that would probably be more useful, change it to func(arr.slice(i,i+size)) and receive the array. Never used lodash, but feel free to submit it yourseld ;) – juvian Aug 12 '15 at 19:32
  • I might just do that :) – therealrootuser Aug 12 '15 at 19:37
12

In Ruby, this is called each_cons:

(1..5).each_cons(2).to_a # => [[1, 2], [2, 3], [3, 4], [4, 5]]

It was proposed for Lodash, but rejected; however, there's an each-cons module on npm:

const eachCons = require('each-cons')

eachCons([1, 2, 3, 4, 5], 2) // [[1, 2], [2, 3], [3, 4], [4, 5]]

There's also an aperture function in Ramda which does the same thing:

const R = require('ramda')

R.aperture(2, [1, 2, 3, 4, 5]) // [[1, 2], [2, 3], [3, 4], [4, 5]]
chocolateboy
  • 1,278
  • 1
  • 15
  • 19
5

This answer is inspired by an answer I saw to a similar question but in Haskell: https://stackoverflow.com/a/4506000/5932012

We can use helpers from Lodash to write the following:

const zipAdjacent = function<T> (ts: T[]): [T, T][] {
  return zip(dropRight(ts, 1), tail(ts));
};
zipAdjacent([1,2,3,4]); // => [[1,2], [2,3], [3,4]]

(Unlike the Haskell equivalent, we need dropRight because Lodash's zip behaves differently to Haskell's`: it will use the length of the longest array instead of the shortest.)

The same in Ramda:

const zipAdjacent = function<T> (ts: T[]): [T, T][] {
  return R.zip(ts, R.tail(ts));
};
zipAdjacent([1,2,3,4]); // => [[1,2], [2,3], [3,4]]

Although Ramda already has a function that covers this called aperture. This is slightly more generic because it allows you to define how many consecutive elements you want, instead of defaulting to 2:

R.aperture(2, [1,2,3,4]); // => [[1,2], [2,3], [3,4]]
R.aperture(3, [1,2,3,4]); // => [[1,2,3],[2,3,4]]
Oliver Joseph Ash
  • 2,204
  • 1
  • 18
  • 38
3

Here's a generic functional solution without any dependencies:

const nWise = (n, array) => {
  iterators = Array(n).fill()
    .map(() => array[Symbol.iterator]());
  iterators
    .forEach((it, index) => Array(index).fill()
      .forEach(() => it.next()));
  return Array(array.length - n + 1).fill()
    .map(() => (iterators
      .map(it => it.next().value);
};

const pairWise = (array) => nWise(2, array);

I know doesn't look nice at all but by introducing some generic utility functions we can make it look a lot nicer:

const sizedArray = (n) => Array(n).fill();

I could use sizedArray combined with forEach for times implementation, but that'd be an inefficient implementation. IMHO it's ok to use imperative code for such a self-explanatory function:

const times = (n, cb) => {
  while (0 < n--) {
    cb();
  }
}

If you're interested in more hardcore solutions, please check this answer.

Unfortunately Array.fill only accepts a single value, not a callback. So Array(n).fill(array[Symbol.iterator]()) would put the same value in every position. We can get around this the following way:

const fillWithCb = (n, cb) => sizedArray(n).map(cb);

The final implementation:

const nWise = (n, array) => {
  iterators = fillWithCb(n, () => array[Symbol.iterator]());
  iterators.forEach((it, index) => times(index, () => it.next()));
  return fillWithCb(
    array.length - n + 1,
    () => (iterators.map(it => it.next().value),
  );
};

By changing the parameter style to currying, the definition of pairwise would look a lot nicer:

const nWise = n => array => {
  iterators = fillWithCb(n, () => array[Symbol.iterator]());
  iterators.forEach((it, index) => times(index, () => it.next()));
  return fillWithCb(
    array.length - n + 1,
    () => iterators.map(it => it.next().value),
  );
};

const pairWise = nWise(2);

And if you run this you get:

> pairWise([1, 2, 3, 4, 5]);
// [ [ 1, 2 ], [ 2, 3 ], [ 3, 4 ], [ 4, 5 ] ]
fodma1
  • 3,097
  • 1
  • 23
  • 41
3

d3.js provides a built-in version of what is called in certain languages a sliding:

console.log(d3.pairs([1, 2, 3, 4])); // [[1, 2], [2, 3], [3, 4]]
<script src="http://d3js.org/d3.v5.min.js"></script>

# d3.pairs(array[, reducer]) <>

For each adjacent pair of elements in the specified array, in order, invokes the specified reducer function passing the element i and element i - 1. If a reducer is not specified, it defaults to a function which creates a two-element array for each pair.

Xavier Guihot
  • 32,132
  • 15
  • 193
  • 118
3

Another solution using iterables and generator functions:

function * pairwise (iterable) {
    const iterator = iterable[Symbol.iterator]()
    let current = iterator.next()
    let next = iterator.next()
    while (!current.done) {
        yield [current.value, next.value]
        current = next
        next = iterator.next()
    }
}

console.log(...pairwise([]))
console.log(...pairwise(['apple']))
console.log(...pairwise(['apple', 'orange', 'kiwi', 'banana']))
console.log(...pairwise(new Set(['apple', 'orange', 'kiwi', 'banana'])))

Advantages:

  • Works on all iterables, not only arrays (eg. Sets).
  • Does not create any intermediate or temporary array.
  • Lazy evaluated, works efficiently on very large iterables.
Community
  • 1
  • 1
salomvary
  • 883
  • 6
  • 12
  • 1
    Nice answer! But I'd change the loop to `while (!next.done)` to avoid having the second element be `undefined` in the last iteration, as the OP requested – ronen Mar 20 '19 at 10:44
2

We can wrap Array.reduce a little to do this, and keep everything clean. Loop indices / loops / external libraries are not required.

If the result is required, just create an array to collect it.

function pairwiseEach(arr, callback) {
  arr.reduce((prev, current) => {
    callback(prev, current)
    return current
  })
}

function pairwise(arr, callback) {
  const result = []
  arr.reduce((prev, current) => {
    result.push(callback(prev, current))
    return current
  })
  return result
}

const arr = [1, 2, 3, 4]
pairwiseEach(arr, (a, b) => console.log(a, b))
const result = pairwise(arr, (a, b) => [a, b])

const output = document.createElement('pre')
output.textContent = JSON.stringify(result)
document.body.appendChild(output)
DarkKnight
  • 4,929
  • 2
  • 24
  • 34
2

Here's a simple one-liner:

[1,2,3,4].reduce((acc, v, i, a) => { if (i < a.length - 1) { acc.push([a[i], a[i+1]]) } return acc; }, []).forEach(pair => console.log(pair[0], pair[1]))

Or formatted:

[1, 2, 3, 4].
reduce((acc, v, i, a) => {
  if (i < a.length - 1) {
    acc.push([a[i], a[i + 1]]);
  }
  return acc;
}, []).
forEach(pair => console.log(pair[0], pair[1]));

which logs:

1 2
2 3
3 4
Robert Mitchell
  • 133
  • 1
  • 6
1

Here's my approach, using Array.prototype.shift:

Array.prototype.pairwise = function (callback) {
    const copy = [].concat(this);
    let next, current;

    while (copy.length) {
        current = next ? next : copy.shift();
        next = copy.shift();
        callback(current, next);
    }
};

This can be invoked as follows:

// output:
1 2
2 3
3 4
4 5
5 6

[1, 2, 3, 4, 5, 6].pairwise(function (current, next) {
    console.log(current, next);
});

So to break it down:

while (this.length) {

Array.prototype.shift directly mutates the array, so when no elements are left, length will obviously resolve to 0. This is a "falsy" value in JavaScript, so the loop will break.

current = next ? next : this.shift();

If next has been set previously, use this as the value of current. This allows for one iteration per item so that all elements can be compared against their adjacent successor.

The rest is straightforward.

James Wright
  • 2,937
  • 1
  • 16
  • 24
  • 1
    I'm hesitant to modify the prototype of `Array` even if it is a clever solution. http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml – therealrootuser Aug 12 '15 at 19:29
  • In that case, the same logic could be exposed as a function that takes an array and the callback as arguments e.g. `arrayUtils.pairwise(array, function[...]);` Personally, I'm not completely against modifying the prototypes of standard objects as long as one is extremely careful. I have had instances where the practice has bitten me on the behind, but in this case I wouldn't see it as that problematic. Nonetheless, the logic is there :) – James Wright Aug 12 '15 at 19:34
  • I'm also not convinced of the performance of `shift` in JavaScript. For example, see discussion at http://stackoverflow.com/questions/6501160/why-is-pop-faster-than-shift – therealrootuser Aug 12 '15 at 19:37
  • You asked for a more elegant approach than using `for`. `for` would be more performant on the basis that `shift`mutates the array. Readability and performance don't always go hand in hand. – James Wright Aug 12 '15 at 19:42
  • So you piqued my interest! Turns out (at least in Chrome) that the `for` loop is only slightly more performant: http://jsperf.com/javascript-pairwise-function-for-vs-shift – James Wright Aug 12 '15 at 20:26
0

My two cents. Basic slicing, generator version.

function* generate_windows(array, window_size) {
    const max_base_index = array.length - window_size;
    for(let base_index = 0; base_index <= max_base_index; ++base_index) {
        yield array.slice(base_index, base_index + window_size);
    }
}
const windows = generate_windows([1, 2, 3, 4, 5, 6, 7, 8, 9], 3);
for(const window of windows) {
    console.log(window);
}
0

Simply use forEach with all its parameters for this:

yourArray.forEach((current, idx, self) => {
  if (let next = self[idx + 1]) {
    //your code here
  }
})
Andreas Bolz
  • 109
  • 10
0

Hope it helps someone! (and likes)

arr = [1, 2, 3, 4];
output = [];
arr.forEach((val, index) => {
  if (index < (arr.length - 1) && (index % 2) === 0) {
    output.push([val, arr[index + 1]])
  }
})

console.log(output);
Lu Chinke
  • 502
  • 5
  • 24
0

A modifed zip:

const pairWise = a => a.slice(1).map((k,i) => [a[i], k]);

console.log(pairWise([1,2,3,4,5,6]));

Output:

[ [ 1, 2 ], [ 2, 3 ], [ 3, 4 ], [ 4, 5 ], [ 5, 6 ] ]

A generic version would be:

const nWise = n => a => a.slice(n).map((_,i) => a.slice(i, n+i));

console.log(nWise(3)([1,2,3,4,5,6,7,8]));
loop
  • 491
  • 3
  • 12
-2

Lodash does have a method that allows you to do this: https://lodash.com/docs#chunk

_.chunk(array, 2).forEach(function(pair) {
  var first = pair[0];
  var next = pair[1];
  console.log(first, next)
})
sparrow
  • 1,718
  • 10
  • 22