3

The question has been asked for many languages, yet not for javascript.

Ruby has the method Enumerable#each_cons which look like that:

puts (0..5).each_cons(2).to_a
# [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]]
puts (0..5).each_cons(3).to_a
# [[1, 2, 3], [2, 3, 4], [3, 4, 5]]

How could I have a similar method in javascript for Array?

Ulysse BN
  • 10,116
  • 7
  • 54
  • 82

3 Answers3

7

Here is a function that will do it (ES6+):

// functional approach
const eachCons = (array, num) => {
    return Array.from({ length: array.length - num + 1 },
                      (_, i) => array.slice(i, i + num))
}

// prototype overriding approach
Array.prototype.eachCons = function(num) {
  return Array.from({ length: this.length - num + 1 },
                    (_, i) => this.slice(i, i + num))
}


const array = [0,1,2,3,4,5]
const log = data => console.log(JSON.stringify(data))

log(eachCons(array, 2))
log(eachCons(array, 3))

log(array.eachCons(2))
log(array.eachCons(3))

You have to guess the length of the resulting array (n = length - num + 1), and then you can take advantage of JavaScript's array.slice To get the chunks you need, iterating n times.

Ulysse BN
  • 10,116
  • 7
  • 54
  • 82
  • 2
    Alternate syntax for Array.from using internal mapper argument `Array.from({length:array.length - num + 1}, (_, i) => array.slice(i, i + num))` – charlietfl Aug 13 '19 at 12:56
  • 1
    @UlysseBN, you can exclude your `map` completely and use the internal mapper itself, in case you didn't notice. There is definitely a performance increase in comparison to the solution you have right now mixed together. – claasic Aug 13 '19 at 13:22
  • 1
    @assoron - edited. I'm soo glad I posted this Q&A, it keeps getting better every minute. – Ulysse BN Aug 13 '19 at 13:41
3

You could take the length, build a new array and map the sliced array with Array.from and the build in mapper.

Number.prototype[Symbol.iterator] = function* () {
    for (var i = 0; i < this; i++) yield i;
};

Array.prototype.eachCons = function (n) {
    return Array.from({ length: this.length - n + 1}, (_, i) => this.slice(i, i + n));
}

console.log([...10].eachCons(2));
console.log([...10].eachCons(3));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • 1
    That [Symbol.iterator] over `Number` is definitely an interesting approach, I didn't know it would actually work. – briosheje Aug 13 '19 at 13:30
  • (just a side note, it should be `<= this` in the Number iterator, since (0..5) actually is [0,1,2,3,4,5]). – briosheje Aug 13 '19 at 15:34
2

Here is a single line solution relying on function generators. Perhaps slightly different, but still worth to try out.

I've also added to the prototype, not sure why you would do that, but still..

This solution doesn't require ES6, it can be used with ES5 as well, with a bit of care for IE that doesn't support function generators at all.

function* eachCons(arr, num) {
  for (let counter = 0; counter <= arr.length - num; counter++) yield arr.slice(counter, counter + num);
}

Array.prototype.eachCons = function* (num) {
  for (let counter = 0; counter <= this.length - num; counter++) yield this.slice(counter, counter + num);
}

const array = [0,1,2,3,4,5];
const log = data => console.log(JSON.stringify(data))
log([...eachCons(array, 2)]);
log([...eachCons(array, 3)]);

// Iterable way
for (let [...pack] of eachCons(array, 2)) {
  console.log('pack is', pack);
}
// Prototype...
for (let [...pack] of array.eachCons(2)) {
  console.log('pack is', pack);
}
briosheje
  • 7,356
  • 2
  • 32
  • 54
  • I like this a lot! However, I'd rather use a solution that gives back an array depending on the situation. For instance, the generator solution doesn't allow `eachCons([1,2,3,3], 3).map((a) => a[0] + a[1])`. +1 however, for the versatility and cleverness of that solution. I wouldn't use the prototype solution, but I know some would so... – Ulysse BN Aug 13 '19 at 12:54
  • @UlysseBN Well, you could use: `Array.from` but you're right, that's a "downside" of generators :P – briosheje Aug 13 '19 at 12:58
  • @UlysseBN perhaps it would make sense to don't return the generator on the prototype but, to rather return an array, though this would bring to confusion and inconsistency, but still, that would solve the issue (and would make sense if used against an array). – briosheje Aug 13 '19 at 13:00
  • 1
    @UlysseBN as a side note, I've just noticed now that `to_a` is invoked in ruby, hence it's probably returning an iterator in ruby as well, but I'm not exactly sure. – briosheje Aug 13 '19 at 13:10
  • @charlietfl you're right, indeed, that's a bit too much. Let me edit that :). – briosheje Aug 13 '19 at 13:27
  • @briosheje it is returning an `Enumerator` in ruby. Which has access to the whole `Enumerable` interface. So not an `Iterator` js way, but not an `Array` either. I guess it really depends on the situation. – Ulysse BN Aug 13 '19 at 13:39
  • @UlysseBN I think a ruby `Enumerator` is actually quite similar to a javascript iterator, they should be quite similar as far as I can tell, and they should be similar to python's iterators and generators – briosheje Aug 13 '19 at 13:54