Why JavaScript base-36 conversion appears to be ambiguous
Asked Answered
Z

3

6

I am currently writing a piece of JavaScript that uses base 36 encoding.

I came across this problem:

parseInt("welcomeback",36).toString(36)

Appears to return "welcomebacg".

I tested this in the Chrome developer's console and Node.js with the same result.

Is there any logical explanation for this result?

Zipporah answered 1/8, 2020 at 0:6 Comment(1)
My first-thought is that it’s because the parsed value exceeds the maximum possible value that number can accurately represent (2^53, but your value is in the order of 36^11) Try using the newer BigInt type instead of number.Ardie
H
6

The result of parseInt("welcomeback",36) is larger than Number.MAX_SAFE_INTEGER (253-1) and thus cannot be accurately represented. A possible workaround is to perform base conversion with BigInt manually.

const str = "welcomeback";
const base = 36;
const res = [...str].reduce((acc,curr)=>
   BigInt(parseInt(curr, base)) + BigInt(base) * acc, 0n);
console.log(res.toString());
console.log(res.toString(36));
Hendren answered 1/8, 2020 at 0:21 Comment(0)
C
2

The number data type in JavaScript is a 64-bit floating point number, and it can safely represent integers only up to 2^53-1, see What is JavaScript's highest integer value that a number can go to without losing precision?

The result of parseInt("welcomeback",36) is above that limit. The result is going to be the closest number that can be represented.

A JS Number can safely hold 10 base-36 digits, so an efficient way to parse it into a BigInt is to split the string into chunks of 10 digits, and combine them. The other answers shows a similar technique using reduce, here's one using forEach:

function toBigInt36(str) {
    const multiplier = BigInt(Math.pow(36, 10));
    const chunks = str.match(/.{1,10}/g);
    let result = BigInt(0);
    chunks.forEach((chunk) => {
        result = result * multiplier + BigInt(parseInt(chunk, 36))
    });
    return result;
}

Colubrid answered 1/8, 2020 at 0:12 Comment(2)
How could I get around this?Zipporah
@Zipporah rule of thumb: whenever you find the numbers too big, work with the digits separately.Compensate
S
0

Or use the multiformats package:

npm install multiformats

And:

import { base36 } from "multiformats/bases/base36"

// you have to prepend your base36 string with "k":
const decoded = base36.decode("k" + "welcomebacklongtimenotseen")
console.log(decoded)

// don't forget to remove the leading "k":
const reEncoded = base36.encode(decoded).slice(1)
console.log(reEncoded)

This will give you the following output:

Uint8Array(17) [
   76, 249,  40, 198, 158, 140,
  198,  47,  99, 101, 143,  20,
   54,  15, 186, 196, 239
]
welcomebacklongtimenotseen
Sheerlegs answered 29/8, 2022 at 21:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.