5
\$\begingroup\$

I have extensively reviewed 2 old articles on the subject: https://stackoverflow.com/questions/14766951/convert-digits-into-words-with-javascript and https://stackoverflow.com/questions/5529934/javascript-numbers-to-words and the answers therein using various methods for spelling a number into words in English.

I have tried to come-up with new different and simple method I am calling it a Single Loop String Triplets (SLST) and thus avoid the use of excessive arithmetic number operations, switches, array manipulations, reversing or splitting strings/arrays, or function recursion.

The method is not limited to JavaScript and can be used in other programming languages as the structure and flow is simple to code.

The principle applied here is to follow the human reading logic of pronouncing and writing the number (in US English) from Left to Right using the standard US English (i.e. without an “and” after the hundred parts).

The function is made to work for whole numbers (integers). But may be called twice for whole and fractional parts after a number split at the decimal point.

Also currency and sub-currency words could be added easily if a whole/fractional split is made.

It is not intended for the function to do everything or check everything as this could be left to a another higher function that will call this function, therefore the following are not accounted for simplicity:

- No checks for negative numbers.

- No checks for non-number (NaN) strings/data.

- No checks or conversion for exponential notations.

However, large numbers can be passed as a String if necessary.

The “Scale” Array may be increased by adding additional scales above “Decillion”.

It is simple to add a Comma "," after each scale words (except last) as some would prefer that.

Here how it works with an example:

Example Number: 1223000789

One Billion Two Hundred Twenty-Three Million Seven Hundred Eighty-Nine.

1. Stringfy and convert to shortest triplets padded with zero:

NumIn = "0".repeat(NumIn.length * 2 % 3) + NumIn;

The stringfied Number in Triplets is now (2 zeros added to the LH):

001223000789

In other words the number is now: enter image description here

In our example, no triplets exist for scales above Billions, so no Trillions or above num scales.

2. Get count of Triplets: in this case 4 Triplets (i.e. count 3 to 0):

Triplets = NumIn.length / 3 - 1

3. Loop starting from the Most Significant Triplet (MST) (i.e. like you read the number) and:

(a) Convert each Triplet number to words (1 to 999) and add the scale name after it.

(b) If a triplet is empty (i.e. 000) then skip it.

(c) Join the new Triplet words to the end of the previous one.

Line 7 of the code ensures a hyphen is inserted for numbers between 21 to 99 in accordance with English numerals writing (i.e. Twenty-One, Fifty-Seven, etc.). You could delete that if it does not apply to you together with the associated variable declaration.

Graphical Example of the above:

enter image description here

Result:

One Billion Two Hundred Twenty-Three Million Seven Hundred Eighty-Nine.

I have found this to be the simplest method to understand and code.

I have also coded the same function in VBA.

I would like the code to be reviewed for any bugs, optimization, or improvements. I am sure there is room for improvements and corrections.

Thanks in advance to all, your valuable inputs and feedback appreciated.

Mohsen Alyafei

function NumToWordsInt(NumIn) {
//-------------------------------------------------------
//Convert Integer Number to English Words
//Using a Single Loop String Triplets (SLST) Methods
//Mohsen Alyafei 10 July 2019
//Call it for a whole number and fractional separately
//-------------------------------------------------------

 if (NumIn==0) return "Zero";
 var  Ones = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"];
 var  Tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"];
 var Scale = ["", "Thousand", "Million", "Billion", "Trillion", "Quadrillion", "Quintillion", "Sextillion", "Septillion", "Octillion", "Nonillion", "Decillion"];
 var N1, N2, Sep, L, j, i, h,Trplt,tns="", NumAll = "";
 NumIn += "";                                            //NumIn=NumIn.toString()
//----------------- code start -------------------
 NumIn = "0".repeat(NumIn.length * 2 % 3) + NumIn;       //Create shortest string triplets 0 padded
 j = 0;                                                  //Start with the highest triplet from LH
    for (i = NumIn.length / 3 - 1; i >= 0; i--) {        //Loop thru number of triplets from LH most
      Trplt = NumIn.substring(j, j + 3);                 //Get a triplet number starting from LH
      if (Trplt != "000") {                              //Skip empty triplets
        h = ""; //Init hundreds                          //-------inner code for 1 triplet
        Trplt[2] != "0" ? Sep="-":Sep=" ";               //Only if hyphen needed for nums 21 to 99
        N1 = Number(Trplt[0]);                           //Get Hundreds digit
        N2 = Number(Trplt.substr(1));                    //Get 2 lowest digits (00 to 99) 
        N2 > 19 ? tns = Tens[Number(Trplt[1])] + Sep + Ones[Number(Trplt[2])]:tns = Ones[N2]
        if (N1 > 0) h = Ones[N1] + " Hundred"            //Add " hundred" if needed
        Trplt = (h + " " + tns).trim() + " " + Scale[i]; //Create number with scale ----inner code ends
        NumAll = NumAll + Trplt + " ";                   //join the triplets scales to previous
      }
      j += 3;                                            //Go for next lower triplets (move to RH)
    }
//----------------- code end --------------------- 
 return NumAll.trim();                                   //Return trimming excess spaces
}
//
//
//================= for testing ================

document.getElementById('number').onkeyup = function () {
    document.getElementById('words').innerHTML = NumToWordsInt(document.getElementById('number').value);
};
<span id="words"></span>
<input id="number" type="text" />

\$\endgroup\$
  • \$\begingroup\$ You are partially skipping non-number glyphs. They are taken into account for the magnitude of the number, but not the triplets. Is this as designed? For instance, 1pppp0 yields One Hundred undefined Thousand undefined \$\endgroup\$ – dfhwze Jul 12 at 5:42
  • \$\begingroup\$ @dfhwze not sure what is "1pppp0" is this an integer in any form or language? \$\endgroup\$ – Mohsen Alyafei Jul 12 at 7:24
  • \$\begingroup\$ It is probably not, but I wonder whether you have implemented behavior for negative paths like this, or whether you don't care about bad input. That's all. \$\endgroup\$ – dfhwze Jul 12 at 7:27
  • \$\begingroup\$ @dfhwze Thanks. It is intended that bad inputs including negative numbers, fractional numbers, etc. are caught at a higher level function before called this function \$\endgroup\$ – Mohsen Alyafei Jul 12 at 10:54
1
\$\begingroup\$

The following code with revised variable names and less coding

function NumToWordsInt(NumIn) {
//---------------------------------------
//Convert Integer Number to English Words
//Using a Loop String Triplets
//Mohsen Alyafei 10 July 2019
//Call for whole and for fractional parts
//---------------------------------------

 if (NumIn==0) return "Zero";
 var  Small = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"];
 var  Tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"];
 var Scale = ["", "Thousand", "Million", "Billion", "Trillion", "Quadrillion", "Quintillion", "Sextillion", "Septillion", "Octillion", "Nonillion", "Decillion"];
 var NHundred, NSmall, Sep, TripletPos, WHundred,TotalTriplets,Triplet,WordSmall="", NumAll = "";
 NumIn+=""                                               //NumIn=NumIn.toString()
//----------------- code start -------------------
 NumIn = "0".repeat(NumIn.length * 2 % 3) + NumIn;       //Create shortest string triplets 0 padded
 TripletPos = 0;                                                  //Start with the highest triplet from LH
    for (TotalTriplets = NumIn.length / 3 - 1; TotalTriplets >= 0; TotalTriplets--) {   //Loop thru number of triplets from LH most
      Triplet = NumIn.substring(TripletPos, TripletPos + 3);                 //Get a triplet number starting from LH
      if (Triplet != "000") {                              //Skip empty triplets
//------- One Triplet Loop decode ---------
        Triplet[2] != "0" ? Sep="-":Sep=" ";               //Only for dash for 21 to 99
        NHundred = Number(Triplet[0]);                     //Get Hundreds digit
        NSmall = Number(Triplet.substr(1));                //Get 2 lowest digits (00 to 99) 
        NSmall > 19 ? WordSmall = Tens[Number(Triplet[1])] + Sep + Small[Number(Triplet[2])]:WordSmall = Small[NSmall]
        //Add " hundred" if needed, Create number with scale, and join the Triplet scales to previous
        NumAll = NumAll + ((NHundred>0 ? WHundred = Small[NHundred] + " Hundred": WHundred="") + " " + WordSmall).trim() + " " + Scale[TotalTriplets]+ " "; 
      }
      TripletPos += 3;                                     //Go for next lower triplets (move to RH)
    }
//----------------- code end --------------------- 
 return NumAll.trim();                                     //Return trimming excess spaces
}
<input type="text" name="number" placeholder="Number" onkeyup="word.innerHTML=NumToWordsInt(this.value)" />
<div id="word"></div>

<script>
function NumToWordsInt(NumIn) {
//---------------------------------------
//Convert Integer Number to English Words
//Using a Loop String Triplets
//Mohsen Alyafei 10 July 2019
//Call for whole and for fractional parts
//---------------------------------------

 if (NumIn==0) return "Zero";
 var  Small = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"];
 var  Tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"];
 var Scale = ["", "Thousand", "Million", "Billion", "Trillion", "Quadrillion", "Quintillion", "Sextillion", "Septillion", "Octillion", "Nonillion", "Decillion"];
 var NHundred, NSmall, Sep, TripletPos, WHundred,TotalTriplets,Triplet,WordSmall="", NumAll = "";
 NumIn+=""                                               //NumIn=NumIn.toString()
//----------------- code start -------------------
 NumIn = "0".repeat(NumIn.length * 2 % 3) + NumIn;       //Create shortest string triplets 0 padded
 TripletPos = 0;                                                  //Start with the highest triplet from LH
    for (TotalTriplets = NumIn.length / 3 - 1; TotalTriplets >= 0; TotalTriplets--) {   //Loop thru number of triplets from LH most
      Triplet = NumIn.substring(TripletPos, TripletPos + 3);                 //Get a triplet number starting from LH
      if (Triplet != "000") {                              //Skip empty triplets
//------- One Triplet Loop decode ---------
        Triplet[2] != "0" ? Sep="-":Sep=" ";               //Only for dash for 21 to 99
        NHundred = Number(Triplet[0]);                     //Get Hundreds digit
        NSmall = Number(Triplet.substr(1));                //Get 2 lowest digits (00 to 99) 
        NSmall > 19 ? WordSmall = Tens[Number(Triplet[1])] + Sep + Small[Number(Triplet[2])]:WordSmall = Small[NSmall]
        //Add " hundred" if needed, Create number with scale, and join the Triplet scales to previous
        NumAll = NumAll + ((NHundred>0 ? WHundred = Small[NHundred] + " Hundred": WHundred="") + " " + WordSmall).trim() + " " + Scale[TotalTriplets]+ " "; 
      }
      TripletPos += 3;                                     //Go for next lower triplets (move to RH)
    }
//----------------- code end --------------------- 
 return NumAll.trim();                                     //Return trimming excess spaces
}
</script>

\$\endgroup\$
  • \$\begingroup\$ thanks for this. Really helps make my variables human :-) and the shorter 3 line less code. \$\endgroup\$ – Mohsen Alyafei Jul 14 at 12:54
  • \$\begingroup\$ I like this one \$\endgroup\$ – Mohsen Alyafei Jul 14 at 13:57
  • 2
    \$\begingroup\$ This answer is basically a code dump, or alternate solution. It would be a great answer on stackoverflow but it might be considered a poor answer on code review. The goal on code review is to make observations about the code to help the original poster improve their code generally. Please see the code review guidelines at codereview.stackexchange.com/help/how-to-answer. \$\endgroup\$ – pacmaninbw Jul 15 at 14:21
3
\$\begingroup\$

Your code is quite short, which is good. On the other hand, it is equally unreadable as short, which makes it worse.

  • You chose really bad variable names. Most of them are one-letter variables and do not tell the reader anything about what they contain or what their purpose is.

  • Your code looks inconsistent. Sometimes you write a space around operators, like in h = "", and sometimes you leave out the space, as in Sep="-".

  • Your habit of adding a comment to every line of code might come from the 1960s, where many programs were written in assembly language and were not abstracted enough to be understandable without a detailed explanation. 60 years later, the programming languages have evolved and are much more expressive. Having this many comments is a sign that the code is not as clearly written as possible.

  • You are using the == and != operators, which should not be used in reliable JavaScript programs. Prefer to use the === and !== operators instead.

function NumToWordsInt(NumIn) {
//-------------------------------------------------------
//Convert Integer Number to English Words
//Using a Single Loop String Triplets (SLST) Methods
//Mohsen Alyafei 10 July 2019
//Call it for a whole number and fractional separately
//-------------------------------------------------------

Your introductory comment mentions that this function could be applied to fractions. This doesn't make sense. While 1.1 is pronounced as one dot one, the fraction 1.100 has the same mathematical value but would be pronounced as one dot one thousand. Therefore you should omit the last sentence from the documentation.

 if (NumIn==0) return "Zero";
 var  Ones = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"];
 var  Tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"];
 var Scale = ["", "Thousand", "Million", "Billion", "Trillion", "Quadrillion", "Quintillion", "Sextillion", "Septillion", "Octillion", "Nonillion", "Decillion"];

Your code currently creates these arrays anew at every call of the function. This is not necessary since these arrays are never modified. The JavaScript compiler should be smart enough to recognize this and optimize that part for you, so that these arrays are placed in some static storage. As of 2019, I don't know how optimizing the JavaScript compilers are, so if you benchmark your program and find out that this single function is the bottleneck, this might be a thing to optimize.

 var N1, N2, Sep, L, j, i, h,Trplt,tns="", NumAll = "";

It's hard to see what all these variables are used for. Regarding the names, you should not omit vowels. Say Triplet instead of Trplt, to clearly tell the reader that the code is not about Trumpletters.

//----------------- code start -------------------

Instead of this line you should rather just insert an empty line into the code. This makes it much more obvious that there is a pause here, and a new section starts.

        Trplt[2] != "0" ? Sep="-":Sep=" ";

The ?: operator is meant to be used for simple expressions, not as a way to structure code. Currently you mention Sep= twice, which can be rewritten like this:

       sep = triplet[2] !== '0' ? '-' : ' ';

This change makes the code look much lighter. The main action (assigning some value to sep) is clearly presented at the very left. The variable names do not use abbreviations, the !== operator makes the comparison predictable and the single quotes make the strings look lighter than the double quotes from before.

        N1 = Number(Trplt[0]);      //Get Hundreds digit

You could have omitted the comment Get Hundreds digit if you had renamed N1 to hundreds or hundredsDigit.

        N2 > 19 ? tns = Tens[Number(Trplt[1])] + Sep + Ones[Number(Trplt[2])]:tns = Ones[N2]

This line is very long and complicated. Can you read it aloud and remember what it does? I cannot, therefore I would write it like this:

        if (rem100 > 19)
            tens = Tens[+triplets[1]] + sep + Ones[+triplets[2]];
        else
            tens = Ones[+rem100];

Sure, it's a bit longer but the if-then-else structure is clearly visible, which allows a reader to quickly understand what is happening here. The ?: that is deeply hidden in the middle of the line is not that clear.

        (h + " " + tns).trim()
        return NumAll.trim();

When you explain to a human how to spell out the numbers, you would probably not need to mention that extraneous whitespace needs to be trimmed. Yet your code does exactly that. This is another sign that your code is not as human-like as it could be.

Since you didn't provide any unit tests, it is hard to see whether this code works as intended. It is also hard to read and hard to step through using a debugger, because of the many badly named variables.

To improve the code, I started with your code and finally arrived at the following code:

  • There are no comments because the code is expressive enough.
  • The code is structured into manageable pieces that fit on a single screen each.
  • There is one function for small numbers, one function for large numbers, and a self-test.
  • All variables are lowercase and have expressive names.
  • The constants are line-wrapped so that they fit comfortably on a screen.
  • The constants are arranged in groups of 5 (or 3 for the long words in scale).
(function () {
    "use strict";

    const ones = [
        "Zero", "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"
    ];
    const hundred = "Hundred";
    const scale = [
        "", "Thousand", "Million",
        "Billion", "Trillion", "Quadrillion",
        "Quintillion", "Sextillion", "Septillion",
        "Octillion", "Nonillion", "Decillion"
    ];

    function strRem1000(rem1000) {
        const result = [];
        if (rem1000 >= 100) {
            result.push(ones[rem1000 / 100 | 0], hundred);
        }

        const rem100 = rem1000 % 100;
        if (rem100 === 0) {
            // do nothing
        } else if (rem100 < 20) {
            result.push(ones[rem100]);
        } else if (rem100 % 10 === 0) {
            result.push(tens[rem100 / 10]);
        } else {
            result.push(tens[rem100 / 10 | 0] + '-' + ones[rem100 % 10]);
        }

        return result.join(' ');
    }

    function toLongNumber(n) {
        let result = [];

        if (n === '0') {
            return ones[0];
        }

        let scaleIndex = 0;
        for (let end = n.length; end > 0; end -= 3) {
            const start = Math.max(0, end - 3);

            let aaa = n.substring(start, end);
            let nnn = parseInt(aaa, 10);
            if (nnn > 0) {
                if (scaleIndex > 0) {
                    result.unshift(scale[scaleIndex]);
                }
                result.unshift(strRem1000(nnn));
            }
            scaleIndex++;
        }

        return result.join(' ');
    }

    function test() {
        function testcase(n, words) {
            const result = toLongNumber(n)
            if (result !== words) {
                console.log('expected', words, 'for', n, 'got', result);
            }
        }

        testcase('0', 'Zero');
        testcase('5', 'Five');
        testcase('10', 'Ten');
        testcase('20', 'Twenty');
        testcase('21', 'Twenty-One');
        testcase('75', 'Seventy-Five');
        testcase('100', 'One Hundred');
        testcase('150', 'One Hundred Fifty');
        testcase('157', 'One Hundred Fifty-Seven');
        testcase('999', 'Nine Hundred Ninety-Nine');
        testcase('1000', 'One Thousand');
        testcase('10000', 'Ten Thousand');
        testcase('123456', '' +
            'One Hundred Twenty-Three Thousand ' +
            'Four Hundred Fifty-Six');
        testcase('123456789', '' +
            'One Hundred Twenty-Three Million ' +
            'Four Hundred Fifty-Six Thousand ' +
            'Seven Hundred Eighty-Nine');
        testcase('1000000890', 'One Billion Eight Hundred Ninety');
        testcase('1000000000000000000000000000000000', 'One Decillion');
    }

    test();
})();
\$\endgroup\$
  • \$\begingroup\$ will take up your advice and recommendations with a revised code. Thanks. \$\endgroup\$ – Mohsen Alyafei Jul 15 at 7:12
2
\$\begingroup\$

I would use a Closure to avoid excessive memory usage and allow for reusability of the method. Try also to adhere to styling and naming conventions. I don't mind variable names a, i, j, ... Make sure you document them well. Replace var with respectively let and const.

Fiddle

(function() {
    "use strict";

    function toLongNumber() {

      return function() {

        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"];
        const scale = ["", "Thousand", "Million", "Billion", "Trillion", "Quadrillion", "Quintillion", "Sextillion", "Septillion", "Octillion", "Nonillion", "Decillion"];

        return function(n) {

          let n1, n2, s, i, h, triplet, j = 0, tns = "", m = "";
          n += "";
          n = "0".repeat(n.length * 2 % 3) + n;

          for (i = n.length / 3 - 1; i >= 0; i--) {
            triplet = n.substring(j, j + 3);
            if (triplet != "000") {
              h = "";
              triplet[2] != "0" ? s = " -" : s = " ";
              n1 = Number(triplet[0]);
              n2 = Number(triplet.substr(1));
              n2 > 19 ? tns = tens[Number(triplet[1])] +
                s + ones[Number(triplet[2])] : tns = ones[n2]
              if (n1 > 0) h = ones[n1] + " Hundred"
              triplet = (h + " " + tns).trim() + " " + scale[i];
              m = m + triplet + " ";
            }
            j += 3;
          }
          return m.trim();
        }
      }();
    }

    window.toLongNumber = toLongNumber();
})();

and usage..

word.innerHTML=toLongNumber(this.value)
\$\endgroup\$
  • \$\begingroup\$ Thanks for your additions and code improvements. \$\endgroup\$ – Mohsen Alyafei Jul 14 at 12:56
  • 1
    \$\begingroup\$ I added a fiddle to show how to reuse the function, declaring all arrays just once. \$\endgroup\$ – dfhwze Jul 14 at 13:00
  • \$\begingroup\$ Good point indeed. Thanks. \$\endgroup\$ – Mohsen Alyafei Jul 14 at 13:03
  • 1
    \$\begingroup\$ Why do you need to nest 4 functions? I think 2 are enough. \$\endgroup\$ – Roland Illig Jul 14 at 13:28
  • 1
    \$\begingroup\$ Be agile, when you need a const to get updated, replace it with let. \$\endgroup\$ – dfhwze Jul 15 at 5:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.