numbr
Version:
A fast number formatting library, based on Numeral.js
467 lines (412 loc) • 12 kB
JavaScript
;
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;