268 lines
6.5 KiB
JavaScript
268 lines
6.5 KiB
JavaScript
(function (root, factory) {
|
|
/* istanbul ignore else */
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory();
|
|
} else if (typeof define === 'function' && define.amd) {
|
|
// AMD, registers as an anonymous module.
|
|
define(factory);
|
|
} else {
|
|
// Browser global.
|
|
root.numbered = factory();
|
|
}
|
|
})(this, function () {
|
|
var NUMBER_MAP = {
|
|
'.': 'point',
|
|
'-': 'negative',
|
|
0: 'zero',
|
|
1: 'one',
|
|
2: 'two',
|
|
3: 'three',
|
|
4: 'four',
|
|
5: 'five',
|
|
6: 'six',
|
|
7: 'seven',
|
|
8: 'eight',
|
|
9: 'nine',
|
|
10: 'ten',
|
|
11: 'eleven',
|
|
12: 'twelve',
|
|
13: 'thirteen',
|
|
14: 'fourteen',
|
|
15: 'fifteen',
|
|
16: 'sixteen',
|
|
17: 'seventeen',
|
|
18: 'eighteen',
|
|
19: 'nineteen',
|
|
20: 'twenty',
|
|
30: 'thirty',
|
|
40: 'forty',
|
|
50: 'fifty',
|
|
60: 'sixty',
|
|
70: 'seventy',
|
|
80: 'eighty',
|
|
90: 'ninety'
|
|
};
|
|
|
|
// http://en.wikipedia.org/wiki/English_numerals#Cardinal_numbers
|
|
var CARDINAL_MAP = {
|
|
2: 'hundred',
|
|
3: 'thousand',
|
|
6: 'million',
|
|
9: 'billion',
|
|
12: 'trillion',
|
|
15: 'quadrillion',
|
|
18: 'quintillion',
|
|
21: 'sextillion',
|
|
24: 'septillion',
|
|
27: 'octillion',
|
|
30: 'nonillion',
|
|
33: 'decillion',
|
|
36: 'undecillion',
|
|
39: 'duodecillion',
|
|
42: 'tredecillion',
|
|
45: 'quattuordecillion',
|
|
48: 'quindecillion',
|
|
51: 'sexdecillion',
|
|
54: 'septendecillion',
|
|
57: 'octodecillion',
|
|
60: 'novemdecillion',
|
|
63: 'vigintillion',
|
|
100: 'googol',
|
|
303: 'centillion'
|
|
};
|
|
|
|
// Make a hash of words back to their numeric value.
|
|
var WORD_MAP = {
|
|
nil: 0,
|
|
naught: 0,
|
|
period: '.',
|
|
decimal: '.'
|
|
};
|
|
|
|
Object.keys(NUMBER_MAP).forEach(function (num) {
|
|
WORD_MAP[NUMBER_MAP[num]] = isNaN(+num) ? num : +num;
|
|
});
|
|
|
|
Object.keys(CARDINAL_MAP).forEach(function (num) {
|
|
WORD_MAP[CARDINAL_MAP[num]] = isNaN(+num) ? num : Math.pow(10, +num);
|
|
});
|
|
|
|
/**
|
|
* Returns the number of significant figures for the number.
|
|
*
|
|
* @param {number} num
|
|
* @return {number}
|
|
*/
|
|
function intervals (num) {
|
|
var match = String(num).match(/e\+(\d+)/);
|
|
|
|
if (match) return match[1];
|
|
|
|
return String(num).length - 1;
|
|
}
|
|
|
|
/**
|
|
* Calculate the value of the current stack.
|
|
*
|
|
* @param {Array} stack
|
|
* @param {number} largest
|
|
*/
|
|
function totalStack (stack, largest) {
|
|
var total = stack.reduceRight(function (prev, num, index) {
|
|
if (num > stack[index + 1]) {
|
|
return prev * num;
|
|
}
|
|
|
|
return prev + num;
|
|
}, 0);
|
|
|
|
return total * largest;
|
|
}
|
|
|
|
/**
|
|
* Accepts both a string and number type, and return the opposite.
|
|
*
|
|
* @param {string|number} num
|
|
* @return {string|number}
|
|
*/
|
|
function numbered (num) {
|
|
if (typeof num === 'string') return numbered.parse(num);
|
|
if (typeof num === 'number') return numbered.stringify(num);
|
|
|
|
throw new Error('Numbered can only parse strings or stringify numbers');
|
|
}
|
|
|
|
/**
|
|
* Turn a number into a string representation.
|
|
*
|
|
* @param {number} num
|
|
* @return {string}
|
|
*/
|
|
numbered.stringify = function (value) {
|
|
var num = Number(value);
|
|
var floor = Math.floor(num);
|
|
|
|
// If the number is in the numbers object, we quickly return.
|
|
if (NUMBER_MAP[num]) return NUMBER_MAP[num];
|
|
|
|
// If the number is a negative value.
|
|
if (num < 0) return NUMBER_MAP['-'] + ' ' + numbered.stringify(-num);
|
|
|
|
// Check if we have decimals.
|
|
if (floor !== num) {
|
|
var words = [numbered.stringify(floor), NUMBER_MAP['.']];
|
|
var chars = String(num).split('.').pop();
|
|
|
|
for (var i = 0; i < chars.length; i++) {
|
|
words.push(numbered.stringify(+chars[i]));
|
|
}
|
|
|
|
return words.join(' ');
|
|
}
|
|
|
|
var interval = intervals(num);
|
|
|
|
// It's below one hundred, but greater than nine.
|
|
if (interval === 1) {
|
|
return NUMBER_MAP[Math.floor(num / 10) * 10] + '-' + numbered.stringify(Math.floor(num % 10));
|
|
}
|
|
|
|
var sentence = [];
|
|
|
|
// Simple check to find the closest full number helper.
|
|
while (!CARDINAL_MAP[interval]) interval -= 1;
|
|
|
|
if (CARDINAL_MAP[interval]) {
|
|
var remaining = Math.floor(num % Math.pow(10, interval));
|
|
|
|
sentence.push(numbered.stringify(Math.floor(num / Math.pow(10, interval))));
|
|
sentence.push(CARDINAL_MAP[interval] + (remaining > 99 ? ',' : ''));
|
|
|
|
if (remaining) {
|
|
if (remaining < 100) sentence.push('and');
|
|
|
|
sentence.push(numbered.stringify(remaining));
|
|
}
|
|
}
|
|
|
|
return sentence.join(' ');
|
|
};
|
|
|
|
/**
|
|
* Turns a string representation of a number into a number type
|
|
* @param {string} num
|
|
* @return {number}
|
|
*/
|
|
numbered.parse = function (num) {
|
|
var modifier = 1;
|
|
var largest = 0;
|
|
var largestInterval = 0;
|
|
var zeros = 0; // Track leading zeros in a decimal.
|
|
var stack = [];
|
|
|
|
var total = num.split(/\W+/g)
|
|
.map(function (word) {
|
|
var num = word.toLowerCase();
|
|
|
|
return WORD_MAP[num] !== undefined ? WORD_MAP[num] : num;
|
|
})
|
|
.filter(function (num) {
|
|
if (num === '-') modifier = -1;
|
|
if (num === '.') return true; // Decimal points are a special case.
|
|
|
|
return typeof num === 'number';
|
|
})
|
|
.reduceRight(function (memo, num) {
|
|
var interval = intervals(num);
|
|
|
|
// Check the interval is smaller than the largest one, then create a stack.
|
|
if (typeof num === 'number' && interval < largestInterval) {
|
|
stack.push(num);
|
|
if (stack.length === 1) return memo - largest;
|
|
return memo;
|
|
}
|
|
|
|
memo += totalStack(stack, largest);
|
|
stack = []; // Reset the stack for more computations.
|
|
|
|
// If the number is a decimal, transform everything we have worked with.
|
|
if (num === '.') {
|
|
var decimals = zeros + String(memo).length;
|
|
|
|
zeros = 0;
|
|
largest = 0;
|
|
largestInterval = 0;
|
|
|
|
return memo * Math.pow(10, -decimals);
|
|
}
|
|
|
|
// Buffer encountered zeros.
|
|
if (num === 0) {
|
|
zeros += 1;
|
|
return memo;
|
|
}
|
|
|
|
// Shove the number on the front if the intervals match and the number whole.
|
|
if (memo >= 1 && interval === largestInterval) {
|
|
var output = '';
|
|
|
|
while (zeros > 0) {
|
|
zeros -= 1;
|
|
output += '0';
|
|
}
|
|
|
|
return Number(String(num) + output + String(memo));
|
|
}
|
|
|
|
largest = num;
|
|
largestInterval = intervals(largest);
|
|
|
|
return (memo + num) * Math.pow(10, zeros);
|
|
}, 0);
|
|
|
|
return modifier * (total + totalStack(stack, largest));
|
|
};
|
|
|
|
return numbered;
|
|
});
|