13
\$\begingroup\$

One day I saw a question on Stack Overflow asking about changing numbers to words, I thought about it, and next day I started coding. When I had the code working, I thought it could, most likely, be much better. Then I started looking for some existing code about it and found this one. I wasn't able to understand much of it, but I am sure it tackles the problem much differently than I did. So I got interested about knowing other possible solutions for it.

I am new to programming and this my first "useful" code, so it would be very rewarding to have my code criticized for things that should, or could, have been done in another way.

This doesn't work with , or . (1,000 or 0.50 won't work). I am not sure why, but passing a number like (036) will return wrong results if it is a number primitive. I think it is interpreting it as an octal, but I defined the radix, so it shouldn't.

function numToWords(number) {

  //Validates the number input and makes it a string
  if (typeof number === 'string') {
    number = parseInt(number, 10);
  }
  if (typeof number === 'number' && !isNaN(number) && isFinite(number)) {
    number = number.toString(10);
  } 
  else {
    return 'This is not a valid number';
  }

  //Creates an array with the number's digits and
  //adds the necessary amount of 0 to make it fully 
  //divisible by 3
  var digits = number.split('');
  var digitsNeeded = 3 - digits.length % 3;
  if (digitsNeeded !== 3) { //prevents this : (123) ---> (000123) 
    while (digitsNeeded > 0) {
      digits.unshift('0');
      digitsNeeded--;
    }
  }

  //Groups the digits in groups of three
  var digitsGroup = [];
  var numberOfGroups = digits.length / 3;
  for (var i = 0; i < numberOfGroups; i++) {
    digitsGroup[i] = digits.splice(0, 3);
  }
  console.log(digitsGroup) //debug

  //Change the group's numerical values to text
  var digitsGroupLen = digitsGroup.length;
  var numTxt = [
    [null,'one','two','three','four','five','six','seven','eight','nine'], //hundreds
    [null, 'ten', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'], //tens
    [null,'one','two','three','four','five','six','seven','eight','nine'] //ones
  ];
  var tenthsDifferent = ['ten','eleven','twelve','thirteen','fourteen','fifteen','sixteen','seventeen','eighteen','nineteen']

  // j maps the groups in the digitsGroup
  // k maps the element's position in the group to the numTxt equivalent
  // k values: 0 = hundreds, 1 = tens, 2 = ones
  for (var j = 0; j < digitsGroupLen; j++) {
    for (var k = 0; k < 3; k++) {
      var currentValue = digitsGroup[j][k];
      digitsGroup[j][k] = numTxt[k][currentValue]
      if (k === 0 && currentValue !== '0') { // !==0 avoids creating a string "null hundred"
        digitsGroup[j][k] += ' hundred ';
      } 
      else if (k === 1 && currentValue === '1') { //Changes the value in the tens place and erases the value in the ones place
        digitsGroup[j][k] = tenthsDifferent[digitsGroup[j][2]];
        digitsGroup[j][2] = 0; //Sets to null. Because it sets the next k to be evaluated, setting this to null doesn't work.
      }
    }
  }

  console.log(digitsGroup) //debug

  //Adds '-' for grammar, cleans all null values, joins the group's elements into a string
  for (var l = 0; l < digitsGroupLen; l++) {
    if (digitsGroup[l][1] && digitsGroup[l][2]) {
      digitsGroup[l][1] += '-';
    }
    digitsGroup[l].filter(function (e) {return e !== null});
    digitsGroup[l] = digitsGroup[l].join('');
  }

  console.log(digitsGroup) //debug

  //Adds thousand, millions, billion and etc to the respective string.
  var posfix = [null,'thousand','million','billion','trillion','quadrillion','quintillion','sextillion'];
  if (digitsGroupLen > 1) {
    var posfixRange = posfix.splice(0, digitsGroupLen).reverse();
    for (var m = 0; m < digitsGroupLen - 1; m++) { //'-1' prevents adding a null posfix to the last group
      if(digitsGroup[m]){ // avoids 10000000 being read (one billion million)
        digitsGroup[m] += ' ' + posfixRange[m];
      }
    }
  }

  console.log(digitsGroup) //debug

  //Joins all the string into one and returns it
  return digitsGroup.join(' ')

}; //End of numToWords function

JSFiddle

\$\endgroup\$
0

2 Answers 2

14
\$\begingroup\$

I want to suggest a different overall approach to this:

Your friend for this sort of stuff is the modulo operator. There's no need to treat the number as a string when breaking it apart, when a number can be broken apart with a little math.

If you've got your number (see thriggle's answer for parseInt usage), you can break it into "thousand-chunks" like so:

function chunk(number) {
  var number = 23456098325,
      thousands = [];

  while(number > 0) {
    thousands.push(number % 1000);
    number = Math.floor(number / 1000);
  }

  return thousands;
}

chunk(23456098325) // => [ 325, 98, 456, 23 ]

Note that it's "backwards": The lowest part of the number first, than the thousands, then the millions, etc..

Now, you have two tasks: Covert each chunk into English, and then add a scale (thousand, million, billion, etc.) to each of them.

For the first task, we can again use the modulo operator, since we want hundreds, tens, and single digits. The only exception is for numbers below 20, whose names don't follow the same system as the later ones.

So if you have an array of the words "one" to "nineteen", and another for the words for "twenty", "thirty", etc. up to "ninety", you can take any 1-999 number and turn it into words. And since the first bit of code breaks a large number into chunk of 1-999, that's what we need.

Final bit is to add the scale ("thousand", "million", "billion" etc.), which we can do based on the index of the chunks in the array.

So for instance, we can do this:

var ONE_TO_NINETEEN = [
  "one", "two", "three", "four", "five",
  "six", "seven", "eight", "nine", "ten",
  "eleven", "twelve", "thirteen", "fourteen", "fifteen",
  "sixteen", "seventeen", "eighteen", "nineteen"
];

var TENS = [
  "ten", "twenty", "thirty", "forty", "fifty",
  "sixty", "seventy", "eighty", "ninety"
];

var SCALES = ["thousand", "million", "billion", "trillion"];

// helper function for use with Array.filter
function isTruthy(item) {
  return !!item;
}

// convert a number into "chunks" of 0-999
function chunk(number) {
  var thousands = [];

  while(number > 0) {
    thousands.push(number % 1000);
    number = Math.floor(number / 1000);
  }

  return thousands;
}

// translate a number from 1-999 into English
function inEnglish(number) {
  var thousands, hundreds, tens, ones, words = [];

  if(number < 20) {
    return ONE_TO_NINETEEN[number - 1]; // may be undefined
  }

  if(number < 100) {
    ones = number % 10;
    tens = number / 10 | 0; // equivalent to Math.floor(number / 10)

    words.push(TENS[tens - 1]);
    words.push(inEnglish(ones));

    return words.filter(isTruthy).join("-");
  }

  hundreds = number / 100 | 0;
  words.push(inEnglish(hundreds));
  words.push("hundred");
  words.push(inEnglish(number % 100));

  return words.filter(isTruthy).join(" ");
}

// append the word for a scale. Made for use with Array.map
function appendScale(chunk, exp) {
  var scale;
  if(!chunk) {
    return null;
  }
  scale = SCALES[exp - 1];
  return [chunk, scale].filter(isTruthy).join(" ");
}

Worth noting:

  • inEnglish recurses for numbers >= 20.

  • inEnglish will return a false'y for the number zero. That's why I'm using Array.filter to remove false'y values before I join the array. For instance, the number 300 is (through some recursion) more or less constructed as [ ONE_TO_NINETEEN[3-1], "hundred", TENS[0-1], ONE_TO_NINETEEN[0-1] ]. This'll become ["three", "hundred", undefined, undefined], so we can't just join that because we'd get some trailing nonsense. So the undefined values are removed before joining.

  • I'm using Math.floor in chunk, but the bitwise-floor-trick (| 0) elsewhere. The reason is that the bitwise-floor-trick can't handle numbers larger than 2,147,483,647 (max value of a signed 32-bit integer, which is what bitwise operators work on), so it would break for large numbers. But in inEnglish we assume that the input is 0-999, so we can safely use the bitwise trick.

With that, you can take numbers and convert them to English like so:

var string = chunk(810238903242)
  .map(inEnglish)
  .map(appendScale)
  .filter(isTruthy)
  .reverse()
  .join(" ");

which yields:

eight hundred ten billion two hundred thirty-eight million nine hundred three thousand two hundred fourty-two

Of course, there are few preliminary checks you might want to make:

  • If the input number is zero return "zero".

  • If the input number is negative, you can complain to the user, or you can use Math.abs, convert the result to English like above, and prepend "negative" afterward.

  • That the input number is between Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER. Otherwise things may get weird because 64-bit floats (as all JS numbers are) can no longer accurately represent the value. Not all runtimes have those two constants though, but you can make them yourself.

Incidentally, Number.MAX_SAFE_INTEGER is nine quadrillion seven trillion one hundred ninety-nine billion two hundred fifty-four million seven hundred forty thousand nine hundred ninety-one. So there's that.

\$\endgroup\$
5
\$\begingroup\$

Here are a few suggestions to start off:

Understanding the Base Number conversions

The base number system should be irrelevant by the time you have a number as a primitive type. It only matters when you're converting strings to numbers and vice versa. JavaScript can't tell the difference between 036 and 30; in both cases the underlying number is the binary 11110... the other formats are just to make it human-readable.

So parseInt(036,10) is functionally equivalent to parseInt(30,10)... in both cases, you're saying here's the number 30 (or 11110 if you like), please give it to me in decimal format; thus the output is always the number 30.

The same is true when you reverse it with .toString(): both 036.toString(10) and (30).toString(10) will evaluate to "30".

The question to ask yourself is: "Under what circumstances would an octal primitive be passed to my numToWords function?"

Since JavaScript itself doesn't distinguish between octal, decimal, and binary numbers when it handles them in code, the question doesn't make sense except insofar as someone's JavaScript code could explicitly pass 036 as a parameter. In such a case, the error would not be in your function but in the logic of the author who types 036 and expects it to be parsed as a decimal instead of the octal number that it is.

Determining the Class of an Object

It's tempting to use typeof to determine the type underlying a variable (that's what you'd expect the typeof operator to do, after all), but its output is not always consistent. For example, typeof new Number(1.2) will return object when the underlying class is actually Number. On top of this, the return values of the typeof operation are not defined in the ECMAScript specification, and could thus be different in different JavaScript implementations. Yikes!

The only thing for which you should use typeof is checking for an undefined variable, using the syntax typeof foo !== 'undefined'

Instead of using typeof to determine the class of an object use Object.prototype.toString. Here's the syntax for that:

Object.prototype.toString.call(obj).slice(8, -1)

where obj is your variable. The reason we call slice at the end is because the function call would otherwise return "[object Class]" when all we care about is "Class".

Semicolon Best Practices

While JavaScript requires semicolons, the JavaScript parser is pretty forgiving about them: if semicolons are missing, it'll try to extrapolate the correct semicolon placement and insert them as needed. However, it's a best practice to explicitly identify the ends of lines. This not only saves the parser some work, but prevents it from making mistakes that can change the behavior of your code.

Since you already have your code in jsfiddle, go ahead and click the "JSHint" button in the nav bar. It will identify lines where you excluded semicolons (such as at the end of a variable assignment) or added unnecessary ones (such as at the end of a function declaration).

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Thanks for the feedback! The way you explained about the base numbers conversion really helped me understand it. In the code, though, parseInt is only run when the input is a string, so parseInt('036', 10) works. But I got your point, and now I understand why toString doesn't work. \$\endgroup\$
    – bpbutti
    May 11, 2015 at 21:44
  • 1
    \$\begingroup\$ Also, I had never heard of Object.prototype.toString.call(obj).slice(8, -1), but wouldn't it be better to just use (object.constructor === something) ? \$\endgroup\$
    – bpbutti
    May 11, 2015 at 21:46
  • \$\begingroup\$ Yeah, directly comparing the constructors would work too, is certainly faster, and is definitely better from a readability perspective. The only reason to be wary of that approach is that it can fail if an object has a property defined with the key constructor e.g. var obj = {name:"bob",constructor:"the builder"} \$\endgroup\$
    – Thriggle
    May 11, 2015 at 22:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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