Dev.to WebDev 🛠 Dev 👁 0 📖 2 min read

parseFloat() will quietly change your 17-digit numbers — spelling out amounts with BigInt instead

Try this in any browser console: parseFloat("9007199254740993") // 9007199254740992 No error, no warning — the last digit just changes. JavaScript numbers are IEEE-754 doubles with a 53-bit significand, so integ

Try this in any browser console:

parseFloat("9007199254740993")
// 9007199254740992

No error, no warning — the last digit just changes. JavaScript numbers are IEEE-754 doubles with a 53-bit significand, so integers above Number.MAX_SAFE_INTEGER (9,007,199,254,740,991) can't all be represented.

Usually that doesn't matter. It matters a lot when you convert a number into words: check amounts, invoice lines, contract values, or Chinese RMB financial uppercase (人民币大写). If the number is wrong before you start, every word after it is wrong too.

Approach: treat the input as a string

The fix is simple: never turn the input into a Number.

  1. Clean the input ($1,234.56 → "1234.56") and split on the decimal point.
  2. Keep the integer part as a digit string, or use BigInt when you need arithmetic.
  3. Walk the digits in groups — 3-digit groups for English short scale (thousand, million, billion = 10⁹, trillion = 10¹² …) and 4-digit groups for Chinese (万, 亿).
  4. Format the fractional part as digits too, never as 0.56 * 100.

A minimal grouping helper:

function groups(intStr, size = 3) {
  const out = [];
  for (let i = intStr.length; i > 0; i -= size) {
    out.unshift(intStr.slice(Math.max(0, i - size), i));
  }
  return out; // "1234567" -> ["1", "234", "567"]
}

const ones = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine",
  "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen",
  "Eighteen", "Nineteen"];
const tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"];

function threeDigits(s) {
  const n = Number(s); // safe: always < 1000
  const h = Math.floor(n / 100), r = n % 100;
  const parts = [];
  if (h) parts.push(ones[h] + " Hundred");
  if (r < 20) { if (r) parts.push(ones[r]); }
  else parts.push(tens[Math.floor(r / 10)] + (r % 10 ? "-" + ones[r % 10] : ""));
  return parts.join(" ");
}

Using Number() inside a three-digit chunk is fine, because each chunk is less than 1000. The precision bug only appears when the whole value goes through floating point.

Check-writing rules people miss

  • On US checks, the word "and" separates dollars from cents: One Thousand Two Hundred Thirty-Four and 56/100. Avoid "One Hundred and Twenty" on the amount line.
  • Compound numbers from 21 to 99 take a hyphen: Forty-Two.
  • It's Forty, not "Fourty". That's the most common spelling mistake on handwritten checks.
  • In RMB uppercase, whole-yuan amounts end with 元整, while amounts that end in 分 drop the 整.

Test values worth keeping

1234.56                  -> One Thousand Two Hundred Thirty-Four and 56/100
100200.05                -> watch the zero handling in both English and Chinese
9007199254740993         -> fails with parseFloat; must survive unchanged
24-digit integers        -> forces BigInt/string path

If you just need the output

Disclosure: I build ToolKnit, and this post grew out of writing its Number to Words converter. It uses the string/BigInt approach above for up to 36 integer digits and 12 decimals, and outputs English words, an XX/100 check line (USD/EUR/GBP/CAD/AUD), ordinals and RMB uppercase. It runs in the browser. You should still check your bank's or contract's own formatting rules before issuing a real check.

If you write your own, start with that 9007199254740993 test case.

Drafted with AI assistance; code and facts reviewed before publishing.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.