UNPKG

numbr

Version:

A fast number formatting library, based on Numeral.js

467 lines (412 loc) 12 kB
"use strict"; var _ = require('lodash'), consumers = require('./lib/consumers'), Consumer = consumers.Consumer, Step = consumers.Step; var autoId = 0, zeroFormat, defaultFormat = '0.0', fmtConsumers = {}, defaultConsumers, compileFn = compileWithCache, formatCache = {}, languages = {}, globalLang = 'en'; /** * A wrapper for numeral-like formatting. * Stores the given value for further formatting. * * @param {number} value * @param {string} [lang] * @constructor */ function Numbr(value, lang){ this.num = value; this.lang = (lang || '').toLowerCase(); } /** * Formats the wrapped value according to fmt. * By default, the format is compiled into a series of transformations and then cached globally, if you'd like to * disable the caching feature, use {@link numbr.cacheEnabled}. * @param {string} [fmt=defaultFormat] * @param {function} [roundFn=Math.round] * @returns {string} the formatted number */ Numbr.prototype.format = function(fmt, roundFn){ return compileFn(fmt || defaultFormat).run(this.num, roundFn, this.lang); }; /** * Sets the language for this Numbr. * The provided language must be already loaded via {@link numbr.loadLang}. * @param lang the language code */ Numbr.prototype.setLang = function(lang){ this.lang = lang; }; /** * @returns {number} */ Numbr.prototype.valueOf = function(){ return this.num; }; /** * Access to the wrapped value * @returns {number} */ Numbr.prototype.value = function(){ return this.num; }; /** * Sets the wrapped value * @param {number} num */ Numbr.prototype.set = function(num){ this.num = num; }; /** * Returns a new Numbr object with the same value and language as this Numbr. * @returns {Numbr} */ Numbr.prototype.clone = function(){ return new Numbr(this.num, this.lang); }; /** * Returns a compiled format from the cache or compiles one on the fly. * @private * @param {string} fmt * @returns {CompiledFormat} */ function compileWithCache(fmt){ var compiled = formatCache[fmt]; if(!compiled){ compiled = formatCache[fmt] = compileFormat(fmt); } return compiled; } /** * Compiles the format string. * @private * @param fmt * @returns {CompiledFormat} */ function compileFormat(fmt){ var state = new ConsumerState(), steps = [], opts = {}, pos = {}; opts.pos = pos; for(var i = 0, len = fmt.length; i < len; ++i){ var char = fmt[i], consumers = fmtConsumers[char] || defaultConsumers, res = consumeInput(consumers, state, fmt, i) || consumeInput(defaultConsumers, state, fmt, i); if(res.consumed){ i += res.consumed; } if(res.step){ // Cache this step position following a natural order, so that the final output corresponds // to the input string and not the internal weight order pos[res.step.consumerId] = steps.push(res) - 1; if(res.opts){ _.extend(opts, res.opts); } } } steps = _(steps) .sortBy('weight') .pluck('step') .value(); var sortedPos = new Array(steps.length); for(i = 0, len = steps.length; i < len; ++i){ sortedPos[i] = pos[steps[i].consumerId]; } return new CompiledFormat(steps, opts, sortedPos); } /** * Finds an appropriate consumer object to swallow the current input cursor. * * @private * @param {Consumer[]} consumers An array of consumers * @param {ConsumerState} state A consumer state object * @param {string} fmt The format string * @param {number} pos The format string cursor * @returns {Step|null} Returns an appropriate step definition given by one of the consumers on the * supplied array, if any */ function consumeInput(consumers, state, fmt, pos){ for(var i = 0, len = consumers.length; i < len; ++i){ var consumer = consumers[i], res = consumer.consume(state, fmt, pos); if(res){ state.incConsumed(consumer.Id); return res; } } } /** * Simple counter for used consumers during a compile operation. * This can be used by individual consumers to check for other consumers' state. * @private * @constructor */ function ConsumerState(){ this.ranConsumers = {}; } /** * Returns whether the given consumer id has been used previously during the compile operation. * @param id The consumer Id * @returns {boolean} */ ConsumerState.prototype.isConsumed = function(id){ return !!this.ranConsumers[id]; }; /** * Increments the usage count of the given consumer id * @param id The consumer Id */ ConsumerState.prototype.incConsumed = function(id){ if(this.ranConsumers[id]){ ++this.ranConsumers[id]; } else { this.ranConsumers[id] = 1; } }; /** * Returns the total usage count for the given consumer id * @param id The consumer Id * @returns {number} */ ConsumerState.prototype.timesConsumed = function(id){ return this.ranConsumers[id] || 0; }; /** * A wrapper object for compiled formats. * Objects of this class should not be created directly. Use {@link numbr.compile} instead. * * @param {Step[]} steps An array of step functions to be called during {@link CompiledFormat#run} * @param {object} opts A options object that will be passed to every step function as part of the run state * @param {number[]} sortedPos An array of output position indexes of each step in steps * @constructor */ function CompiledFormat(steps, opts, sortedPos){ this.steps = steps; this.opts = opts; this.pos = opts.pos; this.sortedPos = sortedPos; } /** * Runs all the transformation step functions using the given number and rounding function. * * @param num The number to format * @param {function} roundFn A rounding function * @param {string} lang The language code * @returns {string} the formatted number */ CompiledFormat.prototype.run = function(num, roundFn, lang){ if(num === 0 && zeroFormat){ return zeroFormat; } var len = this.steps.length, i = 0, state = { num: num, signPos: -1, right: num % 1, rightStr: '', optionalDot: false, roundFn : roundFn || Math.round, lang: languages[lang || globalLang], opts: this.opts, pos: this.pos }, output = new Array(len); for(; i < len; ++i){ this.steps[i](state, output, this.sortedPos[i]); } return output.join(''); }; /** * Function wrapper for creating new Numbr instances, it also has useful static methods to * control the global module behaviour. * * All classes can be accessed via numbr.ClassName, e.g. numbr.Numbr, numbr.CompiledFormat, etc. * @namespace * @param {number} value * @param {string} [lang] * @returns {Numbr} */ function numbr(value, lang){ return new Numbr(value, lang); } /** * Compiles the given string into a {@link CompiledFormat} object ready to be used. * @param {string} fmt * @returns {CompiledFormat} */ numbr.compile = function(fmt){ return compileFn(fmt); }; /** * Sets the global zero format. * If defined, the zero format is used as the outout of {@link Numbr#format} whenever the wrapped value === 0. * @param fmt */ numbr.zeroFormat = function(fmt){ zeroFormat = _.isString(fmt)? fmt : null; }; /** * Sets the default format. * The default format is used if {@link Numbr#format} is called without arguments. By default, the format is '0.0'. * @param {string} fmt */ numbr.defaultFormat = function(fmt){ defaultFormat = _.isString(fmt)? fmt : '0.0'; }; /** * Stores the given language definition with the code langCode. * @param {string} langCode * @param {object} langDef */ numbr.loadLang = function(langCode, langDef){ languages[langCode.toLowerCase()] = langDef; }; /** * Access the global language code. * @returns {string} */ numbr.getGlobalLang = function(){ return globalLang; }; /** * Sets the global language. * @param langCode */ numbr.setGlobalLang = function(langCode){ langCode = langCode.toLowerCase(); if(!languages[langCode]){ throw new Error('Unknown language : ' + langCode); } globalLang = langCode; }; /** * Gets the global language, sets the global language or loads a language. * If called with no arguments, returns the global language. * If called with just the language code, it sets the global language. * If called with both arguments, the language is just loaded. * * @param {string} [langCode] The language code * @param {object} [langDef] A valid language definition */ numbr.language = function(langCode, langDef) { if(!langCode) { return globalLang; } if(langDef){ numbr.loadLang(langCode, langDef); } else { numbr.setGlobalLang(langCode); } }; /** * Enables or disables the format cache. * By default every format is compiled into a series of transformation functions that are cached and reused every * time {@link Numbr#format} is called. * * Disabling the cache may cause a significant performance hit and it is not recommended. Most applications will * probably use just a handful of formats, so the memory overhead is non-existent. * * @param {boolean} enabled Whether to enable or disable the cache */ numbr.cacheEnabled = function(enabled){ compileFn = enabled? compileWithCache : compileFormat; }; /** * Sets the default consumer. * The default consumer is used when no other consumer is able to consume a slice of the input format string. * By default, this is {@link numbr.echoConsumer}. * @param {Consumer} consumer */ numbr.setDefaultConsumer = function(consumer){ consumer.Id = ++autoId; defaultConsumers = [consumer]; }; /** * Adds a consumer to the list of global consumers. * Consumers are used to translate the string format input into actual transforming steps. * @param {Consumer} consumer */ numbr.addConsumer = function(consumer){ consumer.setId(++autoId); consumer.token.split('').forEach(function(char){ var consumerArr = fmtConsumers[char] || []; consumerArr.push(consumer); fmtConsumers[char] = _.sortBy(consumerArr, 'priority'); }); }; /** * A simple consumer that echoes back as many characters as possible in one step. * This is the default consumer. * @type {Consumer} */ numbr.echoConsumer = new Consumer('', { consume: function(state, input, pos){ for(var i = pos+1, len = input.length; i < len; ++i){ if(fmtConsumers[input[i]]){ break; } } var str = input.substring(pos, i), fn = function(state, output, pos){ output[pos] = str; }; fn.consumerId = ++autoId; return new Step(fn, 2000, null, i - pos - 1); } }); /** * A simple consumer that consumes the single character is given and does nothing else. * You can set this consumer as the default by using {@link numbr.setDefaultConsumer} * @type {Consumer} */ numbr.noopConsumer = new Consumer('', { consume: function(){ return {}; } }); numbr.Numbr = Numbr; numbr.CompiledFormat = CompiledFormat; numbr.Consumer = Consumer; numbr.Step = Step; numbr.standardConsumers = consumers; // Initializes the default and standard consumers numbr.setDefaultConsumer(numbr.echoConsumer); consumers.map(numbr.addConsumer); numbr.loadLang('en', { delimiters: { thousands: ',', decimal: '.' }, abbreviations: { thousand: 'k', million: 'm', billion: 'b', trillion: 't' }, ordinal: function(number) { var b = number % 10; return (~~(number % 100 / 10) === 1) ? 'th' : (b === 1) ? 'st' : (b === 2) ? 'nd' : (b === 3) ? 'rd' : 'th'; }, currency: { symbol: '$' } }); module.exports = numbr;