UNPKG

22.7 kBJavaScriptView Raw
1/*! @preserve
2 * numeral.js
3 * version : 2.0.4
4 * author : Adam Draper
5 * license : MIT
6 * http://adamwdraper.github.com/Numeral-js/
7 */
8
9(function (global, factory) {
10 if (typeof define === 'function' && define.amd) {
11 define(factory);
12 } else if (typeof module === 'object' && module.exports) {
13 module.exports = factory();
14 } else {
15 global.numeral = factory();
16 }
17}(this, function () {
18 /************************************
19 Variables
20 ************************************/
21
22 var numeral,
23 _,
24 VERSION = '2.0.4',
25 formats = {},
26 locales = {},
27 defaults = {
28 currentLocale: 'en',
29 zeroFormat: null,
30 nullFormat: null,
31 defaultFormat: '0,0'
32 },
33 options = {
34 currentLocale: defaults.currentLocale,
35 zeroFormat: defaults.zeroFormat,
36 nullFormat: defaults.nullFormat,
37 defaultFormat: defaults.defaultFormat
38 };
39
40
41 /************************************
42 Constructors
43 ************************************/
44
45 // Numeral prototype object
46 function Numeral(input, number) {
47 this._input = input;
48
49 this._value = number;
50 }
51
52 numeral = function(input) {
53 var value,
54 kind,
55 unformatFunction,
56 regexp;
57
58 if (numeral.isNumeral(input)) {
59 value = input.value();
60 } else if (input === 0 || typeof input === 'undefined') {
61 value = 0;
62 } else if (input === null || _.isNaN(input)) {
63 value = null;
64 } else if (typeof input === 'string') {
65 if (options.zeroFormat && input === options.zeroFormat) {
66 value = 0;
67 } else if (options.nullFormat && input === options.nullFormat || !input.replace(/[^0-9]+/g, '').length) {
68 value = null;
69 } else {
70 for (kind in formats) {
71 regexp = typeof formats[kind].regexps.unformat === 'function' ? formats[kind].regexps.unformat() : formats[kind].regexps.unformat;
72
73 if (regexp && input.match(regexp)) {
74 unformatFunction = formats[kind].unformat;
75
76 break;
77 }
78 }
79
80 unformatFunction = unformatFunction || numeral._.stringToNumber;
81
82 value = unformatFunction(input);
83 }
84 } else {
85 value = Number(input)|| null;
86 }
87
88 return new Numeral(input, value);
89 };
90
91 // version number
92 numeral.version = VERSION;
93
94 // compare numeral object
95 numeral.isNumeral = function(obj) {
96 return obj instanceof Numeral;
97 };
98
99 // helper functions
100 numeral._ = _ = {
101 // formats numbers separators, decimals places, signs, abbreviations
102 numberToFormat: function(value, format, roundingFunction) {
103 var locale = locales[numeral.options.currentLocale],
104 negP = false,
105 optDec = false,
106 abbr = '',
107 trillion = 1000000000000,
108 billion = 1000000000,
109 million = 1000000,
110 thousand = 1000,
111 decimal = '',
112 neg = false,
113 abbrForce, // force abbreviation
114 abs,
115 min,
116 max,
117 power,
118 int,
119 precision,
120 signed,
121 thousands,
122 output;
123
124 // make sure we never format a null value
125 value = value || 0;
126
127 abs = Math.abs(value);
128
129 // see if we should use parentheses for negative number or if we should prefix with a sign
130 // if both are present we default to parentheses
131 if (numeral._.includes(format, '(')) {
132 negP = true;
133 format = format.replace(/[\(|\)]/g, '');
134 } else if (numeral._.includes(format, '+') || numeral._.includes(format, '-')) {
135 signed = numeral._.includes(format, '+') ? format.indexOf('+') : value < 0 ? format.indexOf('-') : -1;
136 format = format.replace(/[\+|\-]/g, '');
137 }
138
139 // see if abbreviation is wanted
140 if (numeral._.includes(format, 'a')) {
141 abbrForce = format.match(/a(k|m|b|t)?/);
142
143 abbrForce = abbrForce ? abbrForce[1] : false;
144
145 // check for space before abbreviation
146 if (numeral._.includes(format, ' a')) {
147 abbr = ' ';
148 }
149
150 format = format.replace(new RegExp(abbr + 'a[kmbt]?'), '');
151
152 if (abs >= trillion && !abbrForce || abbrForce === 't') {
153 // trillion
154 abbr += locale.abbreviations.trillion;
155 value = value / trillion;
156 } else if (abs < trillion && abs >= billion && !abbrForce || abbrForce === 'b') {
157 // billion
158 abbr += locale.abbreviations.billion;
159 value = value / billion;
160 } else if (abs < billion && abs >= million && !abbrForce || abbrForce === 'm') {
161 // million
162 abbr += locale.abbreviations.million;
163 value = value / million;
164 } else if (abs < million && abs >= thousand && !abbrForce || abbrForce === 'k') {
165 // thousand
166 abbr += locale.abbreviations.thousand;
167 value = value / thousand;
168 }
169 }
170
171 // check for optional decimals
172 if (numeral._.includes(format, '[.]')) {
173 optDec = true;
174 format = format.replace('[.]', '.');
175 }
176
177 // break number and format
178 int = value.toString().split('.')[0];
179 precision = format.split('.')[1];
180 thousands = format.indexOf(',');
181
182 if (precision) {
183 if (numeral._.includes(precision, '[')) {
184 precision = precision.replace(']', '');
185 precision = precision.split('[');
186 decimal = numeral._.toFixed(value, (precision[0].length + precision[1].length), roundingFunction, precision[1].length);
187 } else {
188 decimal = numeral._.toFixed(value, precision.length, roundingFunction);
189 }
190
191 int = decimal.split('.')[0];
192
193 if (numeral._.includes(decimal, '.')) {
194 decimal = locale.delimiters.decimal + decimal.split('.')[1];
195 } else {
196 decimal = '';
197 }
198
199 if (optDec && Number(decimal.slice(1)) === 0) {
200 decimal = '';
201 }
202 } else {
203 int = numeral._.toFixed(value, null, roundingFunction);
204 }
205
206 // check abbreviation again after rounding
207 if (abbr && !abbrForce && Number(int) >= 1000 && abbr !== locale.abbreviations.trillion) {
208 int = String(Number(int) / 1000);
209
210 switch (abbr) {
211 case locale.abbreviations.thousand:
212 abbr = locale.abbreviations.million;
213 break;
214 case locale.abbreviations.million:
215 abbr = locale.abbreviations.billion;
216 break;
217 case locale.abbreviations.billion:
218 abbr = locale.abbreviations.trillion;
219 break;
220 }
221 }
222
223
224 // format number
225 if (numeral._.includes(int, '-')) {
226 int = int.slice(1);
227 neg = true;
228 }
229
230 if (thousands > -1) {
231 int = int.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1' + locale.delimiters.thousands);
232 }
233
234 if (format.indexOf('.') === 0) {
235 int = '';
236 }
237
238 output = int + decimal + (abbr ? abbr : '');
239
240 if (negP) {
241 output = (negP && neg ? '(' : '') + output + (negP && neg ? ')' : '');
242 } else {
243 if (signed >= 0) {
244 output = signed === 0 ? (neg ? '-' : '+') + output : output + (neg ? '-' : '+');
245 } else if (neg) {
246 output = '-' + output;
247 }
248 }
249
250 return output;
251 },
252 // unformats numbers separators, decimals places, signs, abbreviations
253 stringToNumber: function(string) {
254 var locale = locales[options.currentLocale],
255 stringOriginal = string,
256 abbreviations = {
257 thousand: 3,
258 million: 6,
259 billion: 9,
260 trillion: 12
261 },
262 abbreviation,
263 value,
264 i,
265 regexp;
266
267 if (options.zeroFormat && string === options.zeroFormat) {
268 value = 0;
269 } else if (options.nullFormat && string === options.nullFormat || !string.replace(/[^0-9]+/g, '').length) {
270 value = null;
271 } else {
272 value = 1;
273
274 if (locale.delimiters.decimal !== '.') {
275 string = string.replace(/\./g, '').replace(locale.delimiters.decimal, '.');
276 }
277
278 for (abbreviation in abbreviations) {
279 regexp = new RegExp('[^a-zA-Z]' + locale.abbreviations[abbreviation] + '(?:\\)|(\\' + locale.currency.symbol + ')?(?:\\))?)?$');
280
281 if (stringOriginal.match(regexp)) {
282 value *= Math.pow(10, abbreviations[abbreviation]);
283 break;
284 }
285 }
286
287 // check for negative number
288 value *= (string.split('-').length + Math.min(string.split('(').length - 1, string.split(')').length - 1)) % 2 ? 1 : -1;
289
290 // remove non numbers
291 string = string.replace(/[^0-9\.]+/g, '');
292
293 value *= Number(string);
294 }
295
296 return value;
297 },
298 isNaN: function(value) {
299 return typeof value === 'number' && isNaN(value);
300 },
301 includes: function(string, search) {
302 return string.indexOf(search) !== -1;
303 },
304 insert: function(string, subString, start) {
305 return string.slice(0, start) + subString + string.slice(start);
306 },
307 reduce: function(array, callback /*, initialValue*/) {
308 if (this === null) {
309 throw new TypeError('Array.prototype.reduce called on null or undefined');
310 }
311
312 if (typeof callback !== 'function') {
313 throw new TypeError(callback + ' is not a function');
314 }
315
316 var t = Object(array),
317 len = t.length >>> 0,
318 k = 0,
319 value;
320
321 if (arguments.length === 3) {
322 value = arguments[2];
323 } else {
324 while (k < len && !(k in t)) {
325 k++;
326 }
327
328 if (k >= len) {
329 throw new TypeError('Reduce of empty array with no initial value');
330 }
331
332 value = t[k++];
333 }
334 for (; k < len; k++) {
335 if (k in t) {
336 value = callback(value, t[k], k, t);
337 }
338 }
339 return value;
340 },
341 /**
342 * Computes the multiplier necessary to make x >= 1,
343 * effectively eliminating miscalculations caused by
344 * finite precision.
345 */
346 multiplier: function (x) {
347 var parts = x.toString().split('.');
348
349 return parts.length < 2 ? 1 : Math.pow(10, parts[1].length);
350 },
351 /**
352 * Given a variable number of arguments, returns the maximum
353 * multiplier that must be used to normalize an operation involving
354 * all of them.
355 */
356 correctionFactor: function () {
357 var args = Array.prototype.slice.call(arguments);
358
359 return args.reduce(function(accum, next) {
360 var mn = _.multiplier(next);
361 return accum > mn ? accum : mn;
362 }, 1);
363 },
364 /**
365 * Implementation of toFixed() that treats floats more like decimals
366 *
367 * Fixes binary rounding issues (eg. (0.615).toFixed(2) === '0.61') that present
368 * problems for accounting- and finance-related software.
369 */
370 toFixed: function(value, maxDecimals, roundingFunction, optionals) {
371 var splitValue = value.toString().split('.'),
372 minDecimals = maxDecimals - (optionals || 0),
373 boundedPrecision,
374 optionalsRegExp,
375 power,
376 output;
377
378 // Use the smallest precision value possible to avoid errors from floating point representation
379 if (splitValue.length === 2) {
380 boundedPrecision = Math.min(Math.max(splitValue[1].length, minDecimals), maxDecimals);
381 } else {
382 boundedPrecision = minDecimals;
383 }
384
385 power = Math.pow(10, boundedPrecision);
386
387 //roundingFunction = (roundingFunction !== undefined ? roundingFunction : Math.round);
388 // Multiply up by precision, round accurately, then divide and use native toFixed():
389 output = (roundingFunction(value * power) / power).toFixed(boundedPrecision);
390
391 if (optionals > maxDecimals - boundedPrecision) {
392 optionalsRegExp = new RegExp('\\.?0{1,' + (optionals - (maxDecimals - boundedPrecision)) + '}$');
393 output = output.replace(optionalsRegExp, '');
394 }
395
396 return output;
397 }
398 };
399
400 // avaliable options
401 numeral.options = options;
402
403 // avaliable formats
404 numeral.formats = formats;
405
406 // avaliable formats
407 numeral.locales = locales;
408
409 // This function sets the current locale. If
410 // no arguments are passed in, it will simply return the current global
411 // locale key.
412 numeral.locale = function(key) {
413 if (key) {
414 options.currentLocale = key.toLowerCase();
415 }
416
417 return options.currentLocale;
418 };
419
420 // This function provides access to the loaded locale data. If
421 // no arguments are passed in, it will simply return the current
422 // global locale object.
423 numeral.localeData = function(key) {
424 if (!key) {
425 return locales[options.currentLocale];
426 }
427
428 key = key.toLowerCase();
429
430 if (!locales[key]) {
431 throw new Error('Unknown locale : ' + key);
432 }
433
434 return locales[key];
435 };
436
437 numeral.reset = function() {
438 for (var property in defaults) {
439 options[property] = defaults[property];
440 }
441 };
442
443 numeral.zeroFormat = function(format) {
444 options.zeroFormat = typeof(format) === 'string' ? format : null;
445 };
446
447 numeral.nullFormat = function (format) {
448 options.nullFormat = typeof(format) === 'string' ? format : null;
449 };
450
451 numeral.defaultFormat = function(format) {
452 options.defaultFormat = typeof(format) === 'string' ? format : '0.0';
453 };
454
455 numeral.register = function(type, name, format) {
456 name = name.toLowerCase();
457
458 if (this[type + 's'][name]) {
459 throw new TypeError(name + ' ' + type + ' already registered.');
460 }
461
462 this[type + 's'][name] = format;
463
464 return format;
465 };
466
467
468 numeral.validate = function(val, culture) {
469 var _decimalSep,
470 _thousandSep,
471 _currSymbol,
472 _valArray,
473 _abbrObj,
474 _thousandRegEx,
475 localeData,
476 temp;
477
478 //coerce val to string
479 if (typeof val !== 'string') {
480 val += '';
481
482 if (console.warn) {
483 console.warn('Numeral.js: Value is not string. It has been co-erced to: ', val);
484 }
485 }
486
487 //trim whitespaces from either sides
488 val = val.trim();
489
490 //if val is just digits return true
491 if (!!val.match(/^\d+$/)) {
492 return true;
493 }
494
495 //if val is empty return false
496 if (val === '') {
497 return false;
498 }
499
500 //get the decimal and thousands separator from numeral.localeData
501 try {
502 //check if the culture is understood by numeral. if not, default it to current locale
503 localeData = numeral.localeData(culture);
504 } catch (e) {
505 localeData = numeral.localeData(numeral.locale());
506 }
507
508 //setup the delimiters and currency symbol based on culture/locale
509 _currSymbol = localeData.currency.symbol;
510 _abbrObj = localeData.abbreviations;
511 _decimalSep = localeData.delimiters.decimal;
512 if (localeData.delimiters.thousands === '.') {
513 _thousandSep = '\\.';
514 } else {
515 _thousandSep = localeData.delimiters.thousands;
516 }
517
518 // validating currency symbol
519 temp = val.match(/^[^\d]+/);
520 if (temp !== null) {
521 val = val.substr(1);
522 if (temp[0] !== _currSymbol) {
523 return false;
524 }
525 }
526
527 //validating abbreviation symbol
528 temp = val.match(/[^\d]+$/);
529 if (temp !== null) {
530 val = val.slice(0, -1);
531 if (temp[0] !== _abbrObj.thousand && temp[0] !== _abbrObj.million && temp[0] !== _abbrObj.billion && temp[0] !== _abbrObj.trillion) {
532 return false;
533 }
534 }
535
536 _thousandRegEx = new RegExp(_thousandSep + '{2}');
537
538 if (!val.match(/[^\d.,]/g)) {
539 _valArray = val.split(_decimalSep);
540 if (_valArray.length > 2) {
541 return false;
542 } else {
543 if (_valArray.length < 2) {
544 return ( !! _valArray[0].match(/^\d+.*\d$/) && !_valArray[0].match(_thousandRegEx));
545 } else {
546 if (_valArray[0].length === 1) {
547 return ( !! _valArray[0].match(/^\d+$/) && !_valArray[0].match(_thousandRegEx) && !! _valArray[1].match(/^\d+$/));
548 } else {
549 return ( !! _valArray[0].match(/^\d+.*\d$/) && !_valArray[0].match(_thousandRegEx) && !! _valArray[1].match(/^\d+$/));
550 }
551 }
552 }
553 }
554
555 return false;
556 };
557
558
559 /************************************
560 Numeral Prototype
561 ************************************/
562
563 numeral.fn = Numeral.prototype = {
564 clone: function() {
565 return numeral(this);
566 },
567 format: function(inputString, roundingFunction) {
568 var value = this._value,
569 format = inputString || options.defaultFormat,
570 kind,
571 output,
572 formatFunction;
573
574 // make sure we have a roundingFunction
575 roundingFunction = roundingFunction || Math.round;
576
577 // format based on value
578 if (value === 0 && options.zeroFormat !== null) {
579 output = options.zeroFormat;
580 } else if (value === null && options.nullFormat !== null) {
581 output = options.nullFormat;
582 } else {
583 for (kind in formats) {
584 if (format.match(formats[kind].regexps.format)) {
585 formatFunction = formats[kind].format;
586
587 break;
588 }
589 }
590
591 formatFunction = formatFunction || numeral._.numberToFormat;
592
593 output = formatFunction(value, format, roundingFunction);
594 }
595
596 return output;
597 },
598 value: function() {
599 return this._value;
600 },
601 input: function() {
602 return this._input;
603 },
604 set: function(value) {
605 this._value = Number(value);
606
607 return this;
608 },
609 add: function(value) {
610 var corrFactor = _.correctionFactor.call(null, this._value, value);
611
612 function cback(accum, curr, currI, O) {
613 return accum + Math.round(corrFactor * curr);
614 }
615
616 this._value = _.reduce([this._value, value], cback, 0) / corrFactor;
617
618 return this;
619 },
620 subtract: function(value) {
621 var corrFactor = _.correctionFactor.call(null, this._value, value);
622
623 function cback(accum, curr, currI, O) {
624 return accum - Math.round(corrFactor * curr);
625 }
626
627 this._value = _.reduce([value], cback, Math.round(this._value * corrFactor)) / corrFactor;
628
629 return this;
630 },
631 multiply: function(value) {
632 function cback(accum, curr, currI, O) {
633 var corrFactor = _.correctionFactor(accum, curr);
634 return Math.round(accum * corrFactor) * Math.round(curr * corrFactor) / Math.round(corrFactor * corrFactor);
635 }
636
637 this._value = _.reduce([this._value, value], cback, 1);
638
639 return this;
640 },
641 divide: function(value) {
642 function cback(accum, curr, currI, O) {
643 var corrFactor = _.correctionFactor(accum, curr);
644 return Math.round(accum * corrFactor) / Math.round(curr * corrFactor);
645 }
646
647 this._value = _.reduce([this._value, value], cback);
648
649 return this;
650 },
651 difference: function(value) {
652 return Math.abs(numeral(this._value).subtract(value).value());
653 }
654 };
655
656 /************************************
657 Default Locale && Format
658 ************************************/
659
660 numeral.register('locale', 'en', {
661 delimiters: {
662 thousands: ',',
663 decimal: '.'
664 },
665 abbreviations: {
666 thousand: 'k',
667 million: 'm',
668 billion: 'b',
669 trillion: 't'
670 },
671 ordinal: function(number) {
672 var b = number % 10;
673 return (~~(number % 100 / 10) === 1) ? 'th' :
674 (b === 1) ? 'st' :
675 (b === 2) ? 'nd' :
676 (b === 3) ? 'rd' : 'th';
677 },
678 currency: {
679 symbol: '$'
680 }
681 });
682
683 return numeral;
684}));