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.
- Clean the input (
$1,234.56→"1234.56") and split on the decimal point. - Keep the integer part as a digit string, or use
BigIntwhen you need arithmetic. - Walk the digits in groups — 3-digit groups for English short scale (thousand, million, billion = 10⁹, trillion = 10¹² …) and 4-digit groups for Chinese (万, 亿).
- 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.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.