Skip to content

Instantly share code, notes, and snippets.

@djedr
Last active June 29, 2023 02:08
Show Gist options
  • Save djedr/7f7efb7e30608d36f4f2182d7dcafeb8 to your computer and use it in GitHub Desktop.
Save djedr/7f7efb7e30608d36f4f2182d7dcafeb8 to your computer and use it in GitHub Desktop.
Observations about JavaScript numbers and their parsing. Reply to https://mastodon.social/@rauschma@fosstodon.org/110622841244599511

This was originally a short reply to a toot by @rauschma@fosstodon.org, but it kinda grew.

Let me first quote the original toot:

Interesting difference when creating #JavaScript numbers:

Before number literals, plus and minus are unary operators (that coerce to number) and not part of the literal: https://tc39.es/ecma262/#sec-literals-numeric-literals

> +5

5

> -5

-5

> +true

1

> -true

-1

> +'12'

12

> -'12'

-12

When parsing strings, the grammar includes the plus and the minus: https://tc39.es/ecma262/#sec-tonumber-applied-to-the-string-type

> Number('+5')

5

> Number('-5')

-5

Indeed! Some more interesting observations:

  • When parsing strings as numbers, whitespace inbetween the sign and the number is not allowed. Can't repeat the sign or have more than one sign.

  • When parsing strings as numbers, whitespace around the number is allowed and ignored.

  • When parsing strings as numbers, _ separators are not allowed, because reasons.

  • Empty string parses to 0!

  • Number literal 012 is equal to 10 in decimal (because 012 is interpreted as octal). However '012' parsed according to the StringNumericLiteral grammar is 12 (because '012' is interpreted as decimal with a leading zero).

  • Infinity is not technically a number literal, but a global property; however when parsing strings, Infinity is interpreted as IEEE 754 Infinity; NB JavaScript engines tend to print the value of Infinity with the same syntax coloring as numbers; however as input Infinity may look like a keyword or a variable. Code editors, IDEs, and various syntax highlighters may highlight it like a number, a variable, or a keyword. For example in VSCode a JavaScript snippet inside a markdown has Infinity colored as a keyword when editing as a text file and as a variable when looking at a preview. Point is: the treatment of Infinity is very inconsistent.

  • NaN is also not a number literal, but a global property; when parsing strings it happens to parse as IEEE 754 NaN, but not because the StringNumericLiteral grammar specifies it as a literal, but because it is treated as garbage. Parsing garbage always returns NaN. Syntax coloring of NaN is inconsistent, similarly to Infinity, but not necessarily in the same way. E.g. the Firefox JS console prints Infinity colored like a number, but it colors NaN differently. Maybe that's right, since NaN means Not a Number after all. Chrome doesn't agree though. Maybe that's right, since NaN is an IEEE 754 number after all. Also, let's not forget that NaN !== NaN (as per IEEE 754).

  • BTW this is not the Stroop test.

  • While we're at NaN and IEEE 754:

    • NaN ** 0 === 1 is true

    • There are actually 9007199254740990 different NaNs according to IEEE 754. There is no way to enter a specific one in JS, unless we do some bit fiddling via typed arrays.

    • Since NaN !== NaN and NaN != NaN, we need Number.isNaN to check whether a value is a NaN. There is also global isNaN which behaves slightly differently. Because it performs confusing coercions, it Number.isNaN is recommended over it.

    • NaN is also available as Number.NaN. There is no Number.Infinity however. Instead, there is Number.POSITIVE_INFINITY and Number.NEGATIVE_INFINITY.

  • Number.parseInt and Number.parseFloat (and their global counterparts which, unlike Number.isNaN and global isNaN, behave identically) have their own grammars which are not like each other (obviously?), and also not like the StringNumericLiteral grammar. So a JS engine implements at least 4 different grammars for numbers. They overlap, but each has its own quirks and gotchas incompatible with the others (e.g. +'', Number(''), and new Number('') interpret the empty string as 0, whereas Number.parseInt and Number.parseFloat as NaN). parseInt isn't technically defined in terms of a formal grammar, but a special algorithm.

  • Some of the above is probably the reason why JSON doesn't have Infinity or NaN in its grammar (another reason being that Mr Douglas Crockford doesn't seem to be a fan of IEEE 754). JSON.stringify converts them to nulls. Which is rather unfortunate if you happen to be dealing with these values.

  • BTW, since JSON numbers have their own grammar and don't follow IEEE 754, they have unspecified precision. However JavaScript's JSON.parse always converts them to the native number type (IEEE 754), so big numbers lose precision. E.g. 64-bit integers produced outside of JavaScript and serialized to JSON are technically valid JSON, but JavaScript's JSON.parse will truncate them if they don't fit around 53 bits. E.g. JSON.parse('121232324132942198400') will become 121232324132942200000. This may lead to a bad time.

  • So, taking JSON.parse into account, we have at least 5 different number grammars in JavaScript.

  • Keeping track of all this is definitely easy and I totally didn't forget or mix anything up.

  • Computers are fun!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment