Header logo.
Tonghe's Notes
HomeArchiveTagsAboutFeed

Emulating Python's zip() and zip_longest() in JavaScript

This is essentially how you emulate the way Python's zip() function works using 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:

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:

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. Unlike unpacking a list using the star operator (*) in Python, 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.)

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 👓.