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

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:

var numbers = ['1', '2', '3'];
numbers.map(parseInt);
> [1, NaN, NaN]
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 Array.prototype.map method calls the provided parseInt 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 NaN .

So why does parseInt 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 Array.prototype.map pass into the callback 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 map was called upon. It’s those three arguments that the parseInt parseInt method accepts (Perhaps now you know what happened under the hook :-)

Every step how parseInt parseInt is executed is illustrated as below:

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

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

Step One

the string is 1 1 and the radix is 0 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 0x (hexadecimal). So parseInt('1', 0) parseInt('1', 0) is equal to parseInt('1', 10) parseInt('1', 10) and the result is a decimal 1 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 Not A Number as expected.

Here’s how V8 deals with the 1 1 radix:

function GlobalParseInt(string, radix) {
  if (IS_UNDEFINED(radix) || radix === 10 || radix === 0) {
  ...
  } else {
    ...
    if (!(radix == 0 || (2 <= radix && radix <= 36))) {
      return NaN;
    }
  }
}
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 2 is reasonable but the input string 3 3 is not. Binary numbers use only two different symbols: 0 and 1. So the result is Not A Number 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 parseInt only accepts one argument and ignores others? Then the problem described above won’t bother us any more.

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

numbers.map(_.unary(parseInt));
// OR
_.map(numbers, _.unary(parseInt));
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 unary function. This is my version:

function unary(fn) {
  function wrapper() {
    return fn(arguments[0]);
  }
  return wrapper;
}

numbers.map(unary(parseInt));
function unary(fn) {
  function wrapper() {
    return fn(arguments[0]);
  }
  return wrapper;
}

numbers.map(unary(parseInt));

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

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))
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:

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

and done!

Category Programming
Published
Tags