Emulating Python's zip() and zip_longest() in JavaScript
Tags: javascript
Published: 2021-09-21 22:28:24

This is essentially how you emulate the way Python's `zip()` function works using JavaScript. 

```javascript
let a = [1, 3, 5];
let b = [2, 4, 6];
const mapping = a.map((item, idx) => [item, b[idx]]);
console.log(mapping);
// result
[ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ] ]
```

What makes this possible is the fact that JavaScript array methods — `map()`, `forEach()` and even `filter()` — take up to three arguments (element, index, array) while they iterate through the array. 

In which `element` is the element that the iterator points to at each step, `index` is the index of this item in the original array. And it's worth noting the third argument is the original array.

Since `map()` method of the JavaScript array is more flexible than `zip()`, we can do more stuff to the iterated item or the array. But if you want to do anything with `array`, remember the original array changes accordingly. In this case, `a`:

```javascript
let a = [1, 3, 5];
let b = [2, 4, 6];
const mapping = a.map((item, idx, array) => { 
    array.push(item*2);
    return [item, b[idx]]} );
console.log(mapping);
// Result
[ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ] ]
console.log(a);
// Result
[ 1, 3, 5, 2, 6, 10 ]
```

As we can see, integers 2, 6 and 10 are pushed to the end of the array. 

----

Python provides a `zip_longest()` function in `itertools`. 

While `zip()` cuts off longer lists to fit the shortest, `zip_longest()` stretches shorter lists to match the longest. Empty slots in shorter lists are filled with a given value. By default this `fillvalue` is `None`.

This is how this function works:

```python
import itertools

a = [1, 3, 5, 7, 9]
b = [-1, -3, -5]
c = itertools.zip_longest(a, b)

print(list(c))
# Result
[(1, -1), (3, -3), (5, -5), (7, None), (9, None)]
```

Below I try to emulate this function in JavaScript.

First let's take a look at line 👀. Notably, In order to take indefinite number of arguments, I'm using the [ellipsis operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax). Unlike unpacking a list using the [star operator (*) in Python](https://docs.python.org/3/tutorial/controlflow.html#unpacking-argument-lists), using something like `...args, fillvalue=null` will raise an error. So I have to use a pair of square brackets to allow for a `fillvalue` argument. (Let's give it a duck.)

```js
let a = [5, 3, 1, -1, -3, -5]
let b = [6, 4, 2, 0, -2, -4, -6, -8]
let c = ['a', 'b', 'c', 'd']

const zipLongest = ([...args], fillvalue=null) => {  👀
    const result = []
    let i=0
    while (args.some( argArray=> argArray[i])) {
        const ithColumn = args.map(
            argArray=> {
              item = (typeof argArray[i] === 'undefined')  👓
              ? fillvalue
              : argArray[i]
              return item
            }
            )
            result.push(ithColumn)
            i++
    }
    return result
}

console.log(zipLongest([a, b, c], fillvalue="duck"))

// Result
[
  [ 5, 6, 'a' ],
  [ 3, 4, 'b' ],
  [ 1, 2, 'c' ],
  [ -1, 0, 'd' ],  🙄
  [ -3, -2, 'duck' ],
  [ -5, -4, 'duck' ],
  [ 'duck', -6, 'duck' ],
  [ 'duck', -8, 'duck' ]
]
```

Then let's move to line 👓. Empty slots in the JavaScript array are considered `undefined`.

But `undefined` and other falsy values, eg empty strings and 0, all evaluates to `false`. This will confused this ternary conditional. As a result we will get `[ -1, undefined, 'd' ]` on line 🙄. 

A better way is to check the type of item as we iterate. However, a wrong way to do it is using `typeof nonExistentValue === undefined`. 

Although the `nonExistentValue` is indeed not defined, `typeof` returns `"undefined"`, which is a string. And this string is a string. It is by no means equal to the reserved word for non-existent things. Thus the condition we used on line 👓.