Unary map(parseInt) to Fix the Not-A-Number Issue

Background

Years ago I watched a YouTube video about the bad parts of JavaScript and the author listed a few examples. One of those impressed me:

1
2
3
var numbers = ['1', '2', '3'];
numbers.map(parseInt);
> [1, NaN, NaN]

Problem

In the first place the example showed how buggy was JavaScript. The Array.prototype.map method calls the provided parseInt function once for each element in the array and converts strings into integers as a result. The expectation therefore was that each string would be parsed into an integer but the actual result failed and made the last two elements NaN.

So why does parseInt regard the last two elements as Not A Number s ? Thanks to the hints from my colleague, let's have a closer look at the code.

Explanation

What arguments does the Array.prototype.map pass into the callback function? Three. One is the current element being processed, another is the index of the current element in the array and the other is the entire array which map was called upon. It's those three arguments that the parseInt method accepts (Perhaps now you know what happened under the hook :-)

Every step how parseInt is executed is illustrated as below:

1
2
3
parseInt('1', 0, numbers) // S#1
parseInt('2', 1, numbers)   // S#2
parseInt('3', 2, numbers) // S#3

The third argument numbers actually in every step is ignored and won't affect the result because parseInt only defines two parameters: string and radix.

Step One

the string is 1 and the radix is 0. As MDN describes, when the radix is 0 or undefined the method falls back to the default behavior and treats the input string as decimal unless the input string starts with a zero (octal) or 0x (hexadecimal). So parseInt('1', 0) is equal to parseInt('1', 10) and the result is a decimal 1 as expected.

If the input string begins with "0x" or "0X", radix is 16 (hexadecimal) and the remainder of the string is parsed.If the input string begins with "0", radix is eight (octal) or 10 (decimal). Exactly which radix is chosen is implementation-dependent. ECMAScript 5 specifies that 10 (decimal) is used, but not all browsers support this yet. For this reason always specify a radix when using parseInt.If the input string begins with any other value, the radix is 10 (decimal).
MDN parseInt()

Step Two

in this situation the radix is 1 and obviously it is not reasonable so the result is Not A Number as expected.

Here's how V8 deals with the 1 radix:

1
2
3
4
5
6
7
8
9
10
function GlobalParseInt(string, radix) {
  if (IS_UNDEFINED(radix) || radix === 10 || radix === 0) {
  ...
  } else {
    ...
    if (!(radix == 0 || (2 <= radix && radix <= 36))) {
      return NaN;
    }
  }
}

Step Three

in this situation the radix 2 is reasonable but the input string 3 is not. Binary numbers use only two different symbols: 0 and 1. So the result is Not A Number as expected.

Conclusion

The unexpected behavior of the code shown above is not caused by JavaScript but by the programmers. There's a famous saying by an elder in China, "The youth ought to level up their knowledge" :-)

Solutions

Lodash

What if parseInt only accepts one argument and ignores others? Then the problem described above won't bother us any more.

lodash implements the unary method which creates a function that accepts up to one argument. So the problem can be resolved in a lodash-ized way:

1
2
3
numbers.map(_.unary(parseInt))
// OR
_.map(numbers, _.unary(parseInt))

Vanilla JS

What if we use only Vanilla JavaScript? Then we can implement our own unary function. This is my version:

1
2
3
4
5
6
7
8
function unary (fn) {
  function wrapper () {
    return fn(arguments[0])
  }
  return wrapper
}

numbers.map(unary(parseInt))

Later I found that lodash implements another function ary to slice the number of arguments which the target function can be invoked with and lodash#unary calls it under the hook. So I refactor the original version with an ary-like function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ary (fn, n) {
  const argsLength = n ? n : fn.length

  function wrapper () {
    return fn.apply(this, Array.from(arguments).slice(0, argsLength)
  }

  return wrapper
}

function unary (fn) {
  return ary(fn, 1)
}

numbers.map(unary(parseInt))

Those solutions are a bit complicated and we can fix the problem in a much easier way:

1
numbers.map(str => parseInt(str, 10))

and done!