|
10 | 10 | root.numbered = factory(); |
11 | 11 | } |
12 | 12 | })(this, function () { |
13 | | - var numbers = { |
| 13 | + var NUMBER_MAP = { |
14 | 14 | '.': 'point', |
15 | 15 | '-': 'negative', |
16 | 16 | 0: 'zero', |
|
44 | 44 | }; |
45 | 45 |
|
46 | 46 | // http://en.wikipedia.org/wiki/English_numerals#Cardinal_numbers |
47 | | - var helpers = {}; |
48 | | - // Store the helpers in the power of tens |
49 | | - helpers[2] = 'hundred'; |
50 | | - helpers[3] = 'thousand'; |
51 | | - helpers[6] = 'million'; |
52 | | - helpers[9] = 'billion'; |
53 | | - helpers[12] = 'trillion'; |
54 | | - helpers[15] = 'quadrillion'; |
55 | | - helpers[18] = 'quintillion'; |
56 | | - helpers[21] = 'sextillion'; |
57 | | - helpers[24] = 'septillion'; |
58 | | - helpers[27] = 'octillion'; |
59 | | - helpers[30] = 'nonillion'; |
60 | | - helpers[33] = 'decillion'; |
61 | | - helpers[36] = 'undecillion'; |
62 | | - helpers[39] = 'duodecillion'; |
63 | | - helpers[42] = 'tredecillion'; |
64 | | - helpers[45] = 'quattuordecillion'; |
65 | | - helpers[48] = 'quindecillion'; |
66 | | - helpers[51] = 'sexdecillion'; |
67 | | - helpers[54] = 'septendecillion'; |
68 | | - helpers[57] = 'octodecillion'; |
69 | | - helpers[60] = 'novemdecillion'; |
70 | | - helpers[63] = 'vigintillion'; |
71 | | - helpers[100] = 'googol'; |
72 | | - helpers[303] = 'centillion'; |
73 | | - |
74 | | - // Make a hash of the numbers and helper numbers reversed |
75 | | - // E.g. The key as the word and value as the number |
76 | | - var numbersMap = {}; |
77 | | - numbersMap.nil = 0; |
78 | | - numbersMap.naught = 0; |
79 | | - numbersMap.period = '.'; |
80 | | - numbersMap.decimal = '.'; |
81 | | - |
82 | | - Object.keys(numbers).forEach(function (num) { |
83 | | - numbersMap[numbers[num]] = isNaN(+num) ? num : +num; |
| 47 | + var CARDINAL_MAP = { |
| 48 | + 2: 'hundred', |
| 49 | + 3: 'thousand', |
| 50 | + 6: 'million', |
| 51 | + 9: 'billion', |
| 52 | + 12: 'trillion', |
| 53 | + 15: 'quadrillion', |
| 54 | + 18: 'quintillion', |
| 55 | + 21: 'sextillion', |
| 56 | + 24: 'septillion', |
| 57 | + 27: 'octillion', |
| 58 | + 30: 'nonillion', |
| 59 | + 33: 'decillion', |
| 60 | + 36: 'undecillion', |
| 61 | + 39: 'duodecillion', |
| 62 | + 42: 'tredecillion', |
| 63 | + 45: 'quattuordecillion', |
| 64 | + 48: 'quindecillion', |
| 65 | + 51: 'sexdecillion', |
| 66 | + 54: 'septendecillion', |
| 67 | + 57: 'octodecillion', |
| 68 | + 60: 'novemdecillion', |
| 69 | + 63: 'vigintillion', |
| 70 | + 100: 'googol', |
| 71 | + 303: 'centillion' |
| 72 | + }; |
| 73 | + |
| 74 | + // Make a hash of words back to their numeric value. |
| 75 | + var WORD_MAP = { |
| 76 | + nil: 0, |
| 77 | + naught: 0, |
| 78 | + period: '.', |
| 79 | + decimal: '.' |
| 80 | + }; |
| 81 | + |
| 82 | + Object.keys(NUMBER_MAP).forEach(function (num) { |
| 83 | + WORD_MAP[NUMBER_MAP[num]] = isNaN(+num) ? num : +num; |
84 | 84 | }); |
85 | 85 |
|
86 | | - Object.keys(helpers).forEach(function (num) { |
87 | | - numbersMap[helpers[num]] = isNaN(+num) ? num : Math.pow(10, +num); |
| 86 | + Object.keys(CARDINAL_MAP).forEach(function (num) { |
| 87 | + WORD_MAP[CARDINAL_MAP[num]] = isNaN(+num) ? num : Math.pow(10, +num); |
88 | 88 | }); |
89 | 89 |
|
90 | 90 | /** |
91 | | - * Returns the number of significant figures for the number |
| 91 | + * Returns the number of significant figures for the number. |
| 92 | + * |
92 | 93 | * @param {number} num |
93 | 94 | * @return {number} |
94 | 95 | */ |
95 | | - var intervals = function (num) { |
96 | | - var match; |
97 | | - if ((match = ('' + num).match(/e\+(\d+)/))) { |
98 | | - return match[1]; |
99 | | - } |
| 96 | + function intervals (num) { |
| 97 | + var match = String(num).match(/e\+(\d+)/); |
100 | 98 |
|
101 | | - return ('' + num).length - 1; |
102 | | - }; |
| 99 | + if (match) return match[1]; |
| 100 | + |
| 101 | + return String(num).length - 1; |
| 102 | + } |
| 103 | + |
| 104 | + /** |
| 105 | + * Calculate the value of the current stack. |
| 106 | + * |
| 107 | + * @param {Array} stack |
| 108 | + * @param {number} largest |
| 109 | + */ |
| 110 | + function totalStack (stack, largest) { |
| 111 | + var total = stack.reduceRight(function (prev, num, index) { |
| 112 | + if (num > stack[index + 1]) { |
| 113 | + return prev * num; |
| 114 | + } |
| 115 | + |
| 116 | + return prev + num; |
| 117 | + }, 0); |
| 118 | + |
| 119 | + return total * largest; |
| 120 | + } |
103 | 121 |
|
104 | 122 | /** |
105 | | - * Accepts both a string and number type - and return the opposite |
| 123 | + * Accepts both a string and number type, and return the opposite. |
| 124 | + * |
106 | 125 | * @param {string|number} num |
107 | 126 | * @return {string|number} |
108 | 127 | */ |
109 | | - var numberWords = function (num) { |
110 | | - if (typeof num === 'string') { |
111 | | - return numberWords.parse(num); |
112 | | - } |
113 | | - if (typeof num === 'number') { |
114 | | - return numberWords.stringify(num); |
115 | | - } |
116 | | - throw new Error('Number words can handle handle numbers and/or strings'); |
117 | | - }; |
| 128 | + function numbered (num) { |
| 129 | + if (typeof num === 'string') return numbered.parse(num); |
| 130 | + if (typeof num === 'number') return numbered.stringify(num); |
| 131 | + |
| 132 | + throw new Error('Numbered can only parse strings or stringify numbers'); |
| 133 | + } |
118 | 134 |
|
119 | 135 | /** |
120 | | - * Turn a number into a string representation |
| 136 | + * Turn a number into a string representation. |
| 137 | + * |
121 | 138 | * @param {number} num |
122 | 139 | * @return {string} |
123 | 140 | */ |
124 | | - numberWords.stringify = function (num) { |
125 | | - var word = [], |
126 | | - interval, |
127 | | - remaining; |
128 | | - |
129 | | - num = isNaN(+num) ? num : +num; |
130 | | - |
131 | | - // Numbers are super buggy in JS over 10^20 |
132 | | - if (typeof num !== 'number') { return false; } |
133 | | - // If the number is in the numbers object, we can quickly return |
134 | | - if (numbers[num]) { return numbers[num]; } |
135 | | - // If the number is a negative value |
136 | | - if (num < 0) { |
137 | | - return numbers['-'] + ' ' + numberWords.stringify(num * -1); |
138 | | - } |
| 141 | + numbered.stringify = function (value) { |
| 142 | + var num = Number(value); |
| 143 | + var floor = Math.floor(num); |
| 144 | + |
| 145 | + // If the number is in the numbers object, we quickly return. |
| 146 | + if (NUMBER_MAP[num]) return NUMBER_MAP[num]; |
| 147 | + |
| 148 | + // If the number is a negative value. |
| 149 | + if (num < 0) return NUMBER_MAP['-'] + ' ' + numbered.stringify(-num); |
| 150 | + |
| 151 | + // Check if we have decimals. |
| 152 | + if (floor !== num) { |
| 153 | + var words = [numbered.stringify(floor), NUMBER_MAP['.']]; |
| 154 | + var chars = String(num).split('.').pop(); |
| 155 | + |
| 156 | + for (var i = 0; i < chars.length; i++) { |
| 157 | + words.push(numbered.stringify(+chars[i])); |
| 158 | + } |
139 | 159 |
|
140 | | - // Check if we have decimals |
141 | | - if (num % 1) { |
142 | | - word.push(numberWords.stringify(Math.floor(num))); |
143 | | - word.push(numbers['.']); |
144 | | - word = word.concat(('' + num).split('.')[1].split('').map(numberWords.stringify)); |
145 | | - return word.join(' '); |
| 160 | + return words.join(' '); |
146 | 161 | } |
147 | 162 |
|
148 | | - interval = intervals(num); |
149 | | - // It's below one hundred, but greater than nine |
| 163 | + var interval = intervals(num); |
| 164 | + |
| 165 | + // It's below one hundred, but greater than nine. |
150 | 166 | if (interval === 1) { |
151 | | - word.push(numbers[Math.floor(num / 10) * 10] + '-' + numberWords.stringify(Math.floor(num % 10))); |
152 | | - } |
153 | | - // Simple check to find the closest full number helper |
154 | | - while (interval > 3 && !helpers[interval]) { |
155 | | - interval -= 1; |
| 167 | + return NUMBER_MAP[Math.floor(num / 10) * 10] + '-' + numbered.stringify(Math.floor(num % 10)); |
156 | 168 | } |
157 | 169 |
|
158 | | - if (helpers[interval]) { |
159 | | - remaining = Math.floor(num % Math.pow(10, interval)); |
160 | | - word.push(numberWords.stringify(Math.floor(num / Math.pow(10, interval)))); |
161 | | - word.push(helpers[interval] + (remaining > 99 ? ',' : '')); |
| 170 | + var sentence = []; |
| 171 | + |
| 172 | + // Simple check to find the closest full number helper. |
| 173 | + while (!CARDINAL_MAP[interval]) interval -= 1; |
| 174 | + |
| 175 | + if (CARDINAL_MAP[interval]) { |
| 176 | + var remaining = Math.floor(num % Math.pow(10, interval)); |
| 177 | + |
| 178 | + sentence.push(numbered.stringify(Math.floor(num / Math.pow(10, interval)))); |
| 179 | + sentence.push(CARDINAL_MAP[interval] + (remaining > 99 ? ',' : '')); |
| 180 | + |
162 | 181 | if (remaining) { |
163 | | - if (remaining < 100) { word.push('and'); } |
164 | | - word.push(numberWords.stringify(remaining)); |
| 182 | + if (remaining < 100) sentence.push('and'); |
| 183 | + |
| 184 | + sentence.push(numbered.stringify(remaining)); |
165 | 185 | } |
166 | 186 | } |
167 | 187 |
|
168 | | - return word.join(' '); |
| 188 | + return sentence.join(' '); |
169 | 189 | }; |
170 | 190 |
|
171 | 191 | /** |
172 | 192 | * Turns a string representation of a number into a number type |
173 | 193 | * @param {string} num |
174 | 194 | * @return {number} |
175 | 195 | */ |
176 | | - numberWords.parse = function (num) { |
177 | | - if (typeof num !== 'string') { return false; } |
178 | | - |
179 | | - var modifier = 1, |
180 | | - largest = 0, |
181 | | - largestInterval = 0, |
182 | | - zeros = 0, // Keep track of the number of leading zeros in the decimal |
183 | | - stack = []; |
184 | | - |
185 | | - var totalStack = function () { |
186 | | - var total = stack.reduceRight(function (memo, num, index, array) { |
187 | | - if (num > array[index + 1]) { |
188 | | - return memo * num; |
| 196 | + numbered.parse = function (num) { |
| 197 | + var modifier = 1; |
| 198 | + var largest = 0; |
| 199 | + var largestInterval = 0; |
| 200 | + var zeros = 0; // Track leading zeros in a decimal. |
| 201 | + var stack = []; |
| 202 | + |
| 203 | + var total = num.split(/\W+/g) |
| 204 | + .map(function (word) { |
| 205 | + var num = word.toLowerCase(); |
| 206 | + |
| 207 | + return WORD_MAP[num] !== undefined ? WORD_MAP[num] : num; |
| 208 | + }) |
| 209 | + .filter(function (num) { |
| 210 | + if (num === '-') modifier = -1; |
| 211 | + if (num === '.') return true; // Decimal points are a special case. |
| 212 | + |
| 213 | + return typeof num === 'number'; |
| 214 | + }) |
| 215 | + .reduceRight(function (memo, num) { |
| 216 | + var interval = intervals(num); |
| 217 | + |
| 218 | + // Check the interval is smaller than the largest one, then create a stack. |
| 219 | + if (typeof num === 'number' && interval < largestInterval) { |
| 220 | + stack.push(num); |
| 221 | + if (stack.length === 1) return memo - largest; |
| 222 | + return memo; |
189 | 223 | } |
190 | | - return memo + num; |
191 | | - }, 0); |
192 | 224 |
|
193 | | - return total * largest; |
194 | | - }; |
| 225 | + memo += totalStack(stack, largest); |
| 226 | + stack = []; // Reset the stack for more computations. |
195 | 227 |
|
196 | | - var total = num.split(/\W+/g).map(function (num) { |
197 | | - num = num.toLowerCase(); // Make life easier |
198 | | - return numbersMap[num] != null ? numbersMap[num] : num; |
199 | | - }).filter(function (num) { |
200 | | - if (num === '-') { |
201 | | - modifier = -1; |
202 | | - } |
203 | | - if (num === '.') { |
204 | | - return true; // Decimal points are a special case |
205 | | - } |
206 | | - return isFinite(num); // Remove numbers we don't understand |
207 | | - }).reduceRight(function (memo, num) { |
208 | | - var interval = intervals(num), |
209 | | - decimals, |
210 | | - output; |
211 | | - |
212 | | - // Check the interval is smaller than the largest one, then create a stack |
213 | | - if (typeof num === 'number' && interval < largestInterval) { |
214 | | - if (!stack.length) { memo = memo - largest; } |
215 | | - stack.push(num); |
216 | | - return memo; |
217 | | - } |
| 228 | + // If the number is a decimal, transform everything we have worked with. |
| 229 | + if (num === '.') { |
| 230 | + var decimals = zeros + String(memo).length; |
218 | 231 |
|
219 | | - memo = memo + totalStack(); |
220 | | - stack = []; // Reset the stack for more computations |
221 | | - |
222 | | - // If the number is a decimal, transform everything we were just working with |
223 | | - if (num === '.') { |
224 | | - decimals = zeros + ('' + memo).length; |
225 | | - zeros = 0; |
226 | | - // Reset the largest intervals and stuff |
227 | | - largest = 0; |
228 | | - largestInterval = 0; |
229 | | - return memo * Math.pow(10, decimals * -1); |
230 | | - } |
| 232 | + zeros = 0; |
| 233 | + largest = 0; |
| 234 | + largestInterval = 0; |
231 | 235 |
|
232 | | - // Keep a count of zeros we encountered |
233 | | - if (num === 0) { |
234 | | - zeros += 1; |
235 | | - return memo; |
236 | | - } |
| 236 | + return memo * Math.pow(10, -decimals); |
| 237 | + } |
237 | 238 |
|
238 | | - // Shove the number on the front if the intervals match and the number is a whole |
239 | | - if (memo >= 1 && interval === largestInterval) { |
240 | | - output = '' + memo; |
241 | | - // Decrement the zeros count while adding zeros to the front of the number |
242 | | - while (zeros && zeros--) { |
243 | | - output = '0' + output; |
| 239 | + // Buffer encountered zeros. |
| 240 | + if (num === 0) { |
| 241 | + zeros += 1; |
| 242 | + return memo; |
244 | 243 | } |
245 | | - return +(num + output); |
246 | | - } |
247 | 244 |
|
248 | | - // Store the largest number for future use |
249 | | - largest = num; |
250 | | - largestInterval = intervals(largest); |
| 245 | + // Shove the number on the front if the intervals match and the number whole. |
| 246 | + if (memo >= 1 && interval === largestInterval) { |
| 247 | + var output = ''; |
251 | 248 |
|
252 | | - return (memo + num) * Math.pow(10, zeros); |
253 | | - }, 0); |
| 249 | + while (zeros > 0) { |
| 250 | + zeros -= 1; |
| 251 | + output += '0'; |
| 252 | + } |
| 253 | + |
| 254 | + return Number(String(num) + output + String(memo)); |
| 255 | + } |
| 256 | + |
| 257 | + largest = num; |
| 258 | + largestInterval = intervals(largest); |
| 259 | + |
| 260 | + return (memo + num) * Math.pow(10, zeros); |
| 261 | + }, 0); |
254 | 262 |
|
255 | | - return modifier * (total + totalStack()); |
| 263 | + return modifier * (total + totalStack(stack, largest)); |
256 | 264 | }; |
257 | 265 |
|
258 | | - return numberWords; |
| 266 | + return numbered; |
259 | 267 | }); |
0 commit comments