This seems like a popular topic lately, so I took a crack at it.
Related questions are Number to Words and Converting a number to the text representation.
I used this article as a guideline. The article mostly explains by example, but some key points were:
Use a hyphen to separate numbers like twenty-two.
Using and is not a requirement, so 2014 can be written as two thousand fourteen.
No use of commas is mentioned, other than as placeholders in numbers represented with numerals.
It mentions that numbers may be spoken like two thousand and fourteen in the last footnote; maybe this could be an optional feature added later. If someone finds a better reference showing that any of this is incorrect, please let me know.
(function (global) {
'use strict';
var scalePrefix = ['m', 'b', 'tr', 'quadr', 'quint',
'sext', 'sept', 'oct', 'non', 'dec'];
var digits = ['', 'one', 'two', 'three', 'four',
'five', 'six', 'seven', 'eight', 'nine'];
var tens = ['', 'ten', 'twenty', 'thirty', 'forty', 'fifty',
'sixty', 'seventy', 'eighty', 'ninety'];
var teens = { 0: 'ten', 1: 'eleven', 2: 'twelve',
3: 'thir', 5: 'fif', 8: 'eigh' };
function nameDigit(digit) {
return digits[digit];
}
function nameTenToNineteen(number) {
var prefix = teens[number] || digits[number];
return number < 3 ? prefix : prefix + 'teen';
}
function nameTripletScale(digitCount) {
return digitCount < 3 ? '' :
digitCount < 6 ? 'thousand' :
scalePrefix[digitCount / 3 - 2] + 'illion';
}
function nameTripletHead(number) {
return +number ? (nameDigit(number) + ' hundred') : '';
}
function nameTripletTail(digitPair) {
var separator = +digitPair[1] ? '-' : '';
return digitPair[0] == 0 ? nameDigit(digitPair[1]) :
digitPair[0] == 1 ? nameTenToNineteen(digitPair[1]) :
tens[digitPair[0]] + separator + digits[digitPair[1]];
}
function extractSymbols(number) {
var symbols, digitCount;
number = ('' + number);
if (!number.match(/^(-?)[\d,]+/)) {
throw new Error('invalid number: ' + number);
}
symbols = number.replace(/,/g, '').split('');
digitCount = symbols.length - (symbols[0] == '-');
if (digitCount / 3 > scalePrefix.length + 2) {
throw new Error('number out of range: ' + number);
}
return symbols;
}
function nameNumber(number) {
var symbols = extractSymbols(number),
sign = '',
segments = [],
segment;
function append(item) {
item && segments.push(item);
}
if (symbols[0] == '-') {
sign = 'negative ';
symbols.shift();
}
while (symbols.length % 3) {
symbols.unshift(0);
}
while (symbols.length) {
segment = symbols.splice(0, 3);
append(nameTripletHead(segment[0]));
append(nameTripletTail(segment.slice(1)));
if (segment.join('') > 0) {
append(nameTripletScale(symbols.length));
}
}
return sign + (segments.join(' ') || 'zero');
}
global['nameNumber'] = nameNumber;
}(this));
I am especially interested in feedback on better names for things, especially things like
teens
which are not quite accurate. I'm also looking for feedback on any bugs or mistakes. The internalnameNumber
also looks a little bit too busy, maybe it should be broken up more but I wasn't really happy with anything I tried so far.I am especially uninterested in hearing about unreadable/unmaintainable ternary conditionals. They look fine to me. If you can suggest a different way to write it that's still readable and doesn't double the number of relevant LOC, though, I might be interested after all.
Test it out here; there's a test harness and a passing test case for each example from the linked article.
nameTripletTail
doesn't account for a trailing zero sonameNumber("20")
returns"twenty-"
. \$\endgroup\$