pat
Version:
Formats data described by format strings
1,443 lines (1,416 loc) • 54.3 kB
JavaScript
/**
* Represents a data formatter. Data to be formatted is described by format
* specifiers of a certain flavor.
*
* Supported format specifiers:
*
* + Java (java.util.Formatter)
*
* @module pat
*
*
* @license FreeBSD License
* @date 2012-06-27
* @author Michael Pecherstorfer
*/
/*global define, module*/
/*jslint nomen:true plusplus:true maxlen:85*/
(function(root) {
'use strict';
var mod = null,
//private section
__ = {
cultures: {}, //culture module cache
flavors: {} //flavor module cache
},
Formatter = null;
/* Directory containing the culture modules */
__.DIR_CULTURES = 'cultures/';
/* Directory containing the flavor modules */
__.DIR_FLAVORS = 'flavors/';
/* Default formatter instance */
__.instance = null;
/* Path to this module, relative to require's baseUrl */
__.modulePath = '';
/* This module's inital state */
__.initialState = {
cultures: {},
flavors: {},
modulePath: '',
set: function() {
this.cultures = __.deepCopy(__.cultures);
this.flavors = __.deepCopy(__.flavors);
this.modulePath = __.modulePath;
},
establish: function() {
Formatter.defaultOptions = __.initialDefaultFormatterOptions();
__.instance = new Formatter();
__.cultures = __.deepCopy(this.cultures);
__.flavors = __.deepCopy(this.flavors);
__.modulePath = this.modulePath;
}
};
/*
* Returns the initial default options for a new Formatter.
*/
__.initialDefaultFormatterOptions = function() {
return {
flavorId: 'java',
flavor: null,
cultureId: 'enUS',
culture: null,
lineSeparator: '\n'
};
};
/*
* Conveniance function throwing an error with the given message.
*/
__.err = function(msg) {
throw new Error(msg);
};
/*
* Binds the given context to the given function.
*/
__.bind = function(fn, context) {
return function() {
fn.apply(context, Array.prototype.slice.call(arguments));
};
};
/*
* Counts the given object's own properties.
*/
__.countOwnProperties = function(obj) {
var r = 0,
key;
for (key in obj) {
if (obj.hasOwnProperty(key)) { r++; }
}
return r;
};
/*
* Returns the given object's own property names.
*/
__.ownPropertyNames = function(obj) {
var r = [],
key;
for (key in obj) {
if (obj.hasOwnProperty(key)) { r.push(key); }
}
return r;
};
/*
* Returns a deep copy of the argument.
* Properties of the prototype chain are not considered.
*/
__.deepCopy = function(arg) {
if (typeof arg !== 'object') { return arg; }
var key,
result = {};
for (key in arg) {
if (arg.hasOwnProperty(key)) {
result[key] = __.deepCopy(arg[key]);
}
}
return result;
};
/*
* Returns an accessor for subject. The accessor behaves like a setter or
* a getter, depending whether it is called with or without an argument.
*
* When called as a setter, the given new value is passed to the accessor's
* 'syncSetter' or 'asyncSetter', depending whether one of those properties
* is defined.
*
* If 'syncSetter' is defined, the result of this function represents the
* value to be set. If 'syncSetter' is not defined, but 'asyncSetter' is
* defined, the new value to be set is passed as a parameter to the callback
* function of 'asyncSetter'. Otherwise the new value is set without
* delegating it to another function.
*/
__.accessor = function(subject) {
var result,
syncSetter,
asyncSetter;
result = function(newval, fn) {
//getter
if (arguments.length === 0) { return subject; }
//delegate synchronously
if (typeof syncSetter === 'function') {
subject = syncSetter(newval);
}
//delegate asynchronously
else if (typeof asyncSetter === 'function') {
asyncSetter(newval, function(newval) {
subject = newval;
if (typeof fn === 'function') { fn(); }
});
}
//set new value without delegating it
else { subject = newval; }
};
result.syncSetter = function(fn) {
if (arguments.length === 0) { return syncSetter; }
syncSetter = fn;
};
result.asyncSetter = function(fn) {
if (arguments.length === 0) { return asyncSetter; }
asyncSetter = fn;
};
return result;
};
/*
* Loads the specified module (Node or AMD environment) and applies the
* given callback afterwards.
*/
__.loadModule = function(path, fn) {
if (typeof module !== 'undefined') { //node.js
fn(require('./' + path));
} else if (typeof define !== 'undefined' && define.amd) { //AMD
//include relative to require's baseUrl
require(['./' + (__.modulePath === '' ?
path :
__.modulePath + '/' + path)], function(m) { fn(m); });
} else { //global scope
__.err('Include the necessary script.');
}
};
/*
* Loads the culture module with the specified ID and applies the given
* callback afterwards (if defined). Note that the module has to be named
* exactly after the given ID.
*/
__.loadCulture = function(cultureId, fn) {
var culture = __.cultures[cultureId];
if (!culture) {
try {
__.loadModule(__.DIR_CULTURES + cultureId, function(culture) {
Formatter.validateCulture(culture);
__.cultures[cultureId] = culture;
if (typeof fn === "function") { fn(culture); }
});
} catch (e) {
__.err('Failed to load culture ' + cultureId + ': ' + e.message);
}
} else {
fn(culture);
}
};
/*
* Loads the flavor module with the specified ID and applies the given
* callback. Note that the module file has to be named exactly after the
* given ID.
*/
__.loadFlavor = function(flavorId, fn) {
var flavor = __.flavors[flavorId];
if (!flavor) {
try {
__.loadModule(__.DIR_FLAVORS + flavorId, function(flavor) {
__.flavors[flavorId] = flavor;
if (typeof fn === "function") { fn(flavor); }
});
} catch (e) {
__.err('Failed to load flavor ' + flavorId + ': ' + e.message);
}
} else {
fn(flavor);
}
};
/**
* Allocates a new formatter with the specified options.
* @constructor
* @class Formatter
* @param {Object} [options]
* @return {Formatter}
*/
Formatter = function(options) {
this.options(options);
};
/**
* Default options for a new Formatter.
* Overwrite this property if you intend to allocate several formatters with
* default options different to those initially specified by this module.
* @static
* @property defaultOptions
* @type {Object}
*/
Formatter.defaultOptions = __.initialDefaultFormatterOptions();
/**
* Resets the Formatter.
* @static
* @chainable
* @method reset
* @return {Formatter}
*/
Formatter.reset = function() {
__.initialState.establish();
return Formatter;
};
/**
* Tests if the given argument represents a valid culture object.
* Throws an error if it is not valid.
* @static
* @chainable
* @method validateCulture
* @param {Object} culture
* @return {Formatter}
*/
Formatter.validateCulture = function(culture) {
var msg = 'Invalid culture: ';
if (typeof culture !== 'object') {
__.err(msg + 'Not an object');
}
if (culture.id === undefined) {
__.err(msg + 'Missing property "id"');
}
/* Numbers */
if (culture.zeroDigit === undefined) {
__.err(msg + 'Missing property "zeroDigit"');
}
if (culture.decimalSeparator === undefined) {
__.err(msg + 'Missing property "decimalSeparator"');
}
if (culture.groupingSeparator === undefined) {
__.err(msg + 'Missing property "groupingSeparator"');
}
if (culture.groupingSize === undefined) {
__.err(msg + 'Missing property "groupingSize"');
}
/* Currency */
if (culture.currencySymbol === undefined) {
__.err(msg + 'Missing property "currencySymbol"');
}
if (culture.currencyToken === undefined) {
__.err(msg + 'Missing property "currencyToken"');
}
/* Weekday names */
if (culture.weekdays === undefined) {
__.err(msg + 'Missing property "weekdays"');
}
if (culture.weekdaysAbbr === undefined) {
__.err(msg + 'Missing property "weekdaysAbbr"');
}
if (culture.firstDayOfWeek === undefined) {
__.err(msg + 'Missing property "firstDayOfWeek"');
}
/* Month names */
if (culture.months === undefined) {
__.err(msg + 'Missing property "months"');
}
if (culture.monthsAbbr === undefined) {
__.err(msg + 'Missing property "monthsAbbr"');
}
/* Morning/afternoon tokens */
if (culture.amToken === undefined) {
__.err(msg + 'Missing property "amToken"');
}
if (culture.pmToken === undefined) {
__.err(msg + 'Missing property "pmToken"');
}
return Formatter;
};
/**
* Sets the Formatter options or returns them if called without an argument.
* @static
* @chainable
* @method options
* @param {Object} [options] Formatter options:
*
{
path: './',
flavorId: 'flavorId',
cultureId: 'cultureId',
lineSeparator: '\n'
}
* @return {Formatter|Object}
* Formatter if called as setter, Formatter options if called as getter
*/
Formatter.options = function(options, fn) {
//delegate to the default formatter instance
if (arguments.length > 0) {
__.instance.options(options, fn);
return Formatter;
}
return __.instance.options();
};
/**
* Formats the given arguments described by the given formatstring.
* @static
* @method format
* @param {String} fstr Format string
* @param {any} [data]* Data to be formatted
* @return {String} Formatted data
*/
Formatter.format = function(fstr) {
//delegate to the default formatter instance
return __.instance.format.apply(__.instance, arguments);
};
/**
* Sets this Formatter's options or returns them if called without an
* argument.
* @chainable
* @method options
* @param {Object} [options] Formatter options:
{
path: './',
flavorId: 'flavorId',
cultureId: 'cultureId',
lineSeparator: '\n'
}
* @return {Object} This Formatter's options if called as a getter, `this`
* if called as a setter
*/
Formatter.prototype.options = function(options, fn) {
var key,
opt,
nAsyncReturns = 0,
done = function() {
if (--nAsyncReturns === 0 && typeof fn === 'function') { fn(); }
};
if (arguments.length === 0) { return this._options; }
if (options) {
//count options to be delegated asynchronously before setting them
for (key in options) {
if (options.hasOwnProperty(key) &&
this._options.hasOwnProperty(key) &&
this._options[key].asyncSetter() !== undefined) {
nAsyncReturns++;
}
}
//only set properties specified by the given options
for (key in options) {
if (options.hasOwnProperty(key) &&
this._options.hasOwnProperty(key)) {
this._options[key](options[key], done);
}
}
} else { //set default options
opt = __.deepCopy(Formatter.defaultOptions);
//turn option properties into accessors
for (key in opt) {
if (opt.hasOwnProperty(key)) { opt[key] = __.accessor(opt[key]); }
}
//hook module loaders for flavor and culture changes
opt.flavorId.asyncSetter(__.bind(function(flavorId, fn) {
__.loadFlavor(flavorId, __.bind(function(flavor) {
this.options({ flavor: flavor });
fn(flavorId);
}, this));
}, this));
opt.cultureId.asyncSetter(__.bind(function(cultureId, fn) {
__.loadCulture(cultureId, __.bind(function(culture) {
this.options({ culture: culture });
fn(cultureId);
}, this));
}, this));
this._options = opt;
}
return this;
};
/**
* Formats the given arguments described by the given formatstring.
* @method format
* @param {String} fstr Format string
* @param {any} [data]* Data to be formatted
* @return {String} Formatted data
*/
Formatter.prototype.format = function(fstr) {
if (arguments.length === 0) { return undefined; }
if (!__.cultures[this._options.cultureId()]) {
__.err('Define a culture first');
}
if (!__.flavors[this._options.flavorId()]) {
__.err('Define a flavor first');
}
//format
return __.flavors[this._options.flavorId()].format(
typeof fstr === 'string' ? fstr.split('') : fstr,
Formatter,
Array.prototype.slice.call(arguments, 1),
this.options());
};
/**
* Utility functions.
* @static
* @class Formatter.util
*/
Formatter.util = {};
/**
* Returns true if the given arg is an Array, false otherwise.
* @static
* @method isArray
* @param {any} arg
* @return {Boolean}
*/
Formatter.util.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
/**
* Returns true if the given arg is a String, false otherwise.
* @static
* @method isString
* @param {any} arg
* @return {Boolean}
*/
Formatter.util.isString = function(arg) {
return Object.prototype.toString.call(arg) === '[object String]';
};
/**
* Returns the argument, a character array or an array of length 1
* containing the argument depending whether the argument is an array,
* a string or any other value.
* @static
* @method toArray
* @param {any} arg
* @return {Array}
*/
Formatter.util.toArray = function(arg) {
if (this.isArray(arg)) { return arg; }
if (this.isString(arg)) { return arg.split(''); }
return [arg];
};
/**
* Concatenates the given argument n-1 times with itself and returns the
* resulting string.
* @static
* @method concat
* @param {String} arg
* @param {Number} [n = 1]
* @return {String}
*/
Formatter.util.concat = function(arg, n) {
n = n || 1;
return new Array(n + 1).join(arg);
};
/**
* Appends or prepends the given character to the given string until the
* resulting string has the specified length.
* @static
* @method pad
* @param {String} str Append or prepend to this string
* @param {String} [ch = ' '] Character to be appended or prepended
* @param {Number} [len = String(str).length] Length of the resulting string
* @param {Boolean} [left = false] Prepend if true, append otherwise
* @return {String}
*/
Formatter.util.pad = function pad(str, ch, len, left) {
str = String(str);
ch = ch || ' ';
len = len || str.length;
left = Boolean(left);
var delta = len - str.length;
if (delta <= 0) { return str; }
return (left ?
this.concat(ch, delta) + str :
str + this.concat(ch, delta));
};
/**
* Prepends the given character to the given string until the resulting
* string has the specified length.
* @static
* @method padLeft
* @param {String} str Prepend to this string
* @param {String} [ch = ' '] Character to be prepended
* @param {Number} [len = String(str).length] Length of the resulting string
* @return {String}
*/
Formatter.util.padLeft = function(str, ch, len) {
return this.pad(str, ch, len, true);
};
/**
* Appends the given character to the given string until the resulting
* string has the specified length.
* @static
* @method padRight
* @param {String} str Append to this string
* @param {String} [ch = ' '] Character to be appended
* @param {Number} [len = String(str).length] Length of the resulting string
* @return {String}
*/
Formatter.util.padRight = function(str, ch, len) {
return this.pad(str, ch, len);
};
/**
* Number utility functions.
* @static
* @class Formatter.util.number
*/
Formatter.util.number = {
/**
* Greatest precise integer value in JavaScript.
* @final
* @static
* @property MAX_INT
* @type {Number}
*/
MAX_INT: Math.pow(2, 53),
/**
* Greatest precise integer value in two's complement range.
* @final
* @static
* @property MAX_SIGNED_INT
* @type {Number}
*/
MAX_SIGNED_INT: Math.pow(2, 52) - 1,
/**
* Smallest precise integer value in two's complement range.
* @final
* @static
* @property MIN_SIGNED_INT
* @type {Number}
*/
MIN_SIGNED_INT: -Math.pow(2, 52)
};
/**
* Returns true if the given number is less than zero or negative zero.
* @static
* @method isSigned
* @param {Number} arg
* @return {Boolean}
*/
Formatter.util.number.isSigned = function(arg) {
if (arg === 0) { return 1/arg < 0; }
return arg < 0;
};
/**
* Returns the given argument rounded to the given precision.
* @static
* @method round
* @param {Number} arg Number to be rounded
* @param {Number} [precision=0] Number of precise fractional digits. A
* falsy value specifies fractional precision of 0.
* @return {Number}
*/
Formatter.util.number.round = function(arg, precision) {
if (!precision || precision < 0) { precision = 0; }
var fac = Math.pow(10, precision);
return Math.round(arg * fac) / fac;
};
/**
* Returns the given argument as a Number within the range
* [Formatter.util.number.MIN_SIGNED_INT, Formatter.util.number.MAX_SIGNED_INT].
* @static
* @method signedInt
* @param {Number} arg
* @return {Number}
*/
Formatter.util.number.signedInt = function(arg) {
var r = Number(arg);
if (r < this.MIN_SIGNED_INT) {
return this.MIN_SIGNED_INT;
}
if (r > this.MAX_SIGNED_INT) {
return this.MAX_SIGNED_INT;
}
return r;
};
/**
* Returns a decimal integer representing two's complement of the given
* number.
*
* A JavaScript Number is a double-precision floating-point as specified by
* the IEEE 754 standard. All positive integers up to 2^53 are represented
* precisely, numbers beyond that threshold get their least significant bits
* clipped (((Math.pow(2,53) + 1) - Math.pow(2,53) results to 0, not 1).
*
* The argument is therefore interpreted as an integer within the range
* [-2^52, 2^52-1]. A floating point argument is truncated, an argument out
* of the expected range is set to the smallest or to the greatest precise
* value depending on whether the argument is smaller than -2^52 or greater
* than 2^52-1.
*
* @static
* @method twosComplement
* @param {Number} arg
* @return {Number}
*/
Formatter.util.number.twosComplement = function(arg) {
var r = this.signedInt(arg);
if (r < 0) {
return r + this.MAX_INT;
}
return this.MAX_INT - r;
};
/**
* Returns true if the given argument represents a symbolic number (NaN,
* POSITIVE_INFINITY, NEGATIVE_INFINITY), false otherwise.
* @static
* @method isSymbolicNumber
* @param {Number} arg
* @return {Boolean}
*/
Formatter.util.number.isSymbolicNumber = function(arg) {
return String(Number(arg)) === "NaN" ||
Number(arg) === Number.POSITIVE_INFINITY ||
Number(arg) === Number.NEGATIVE_INFINITY;
};
/**
* Date utility functions for the Gregorian calendar.
* @static
* @class Formatter.util.date
*/
Formatter.util.date = {
/**
* Milliseconds per hour.
* @final
* @static
* @property MILLISECONDS_PER_HOUR
* @type Number
*/
MILLISECONDS_PER_HOUR: 3600000,
/**
* Milliseconds per day.
* @final
* @static
* @property MILLISECONDS_PER_DAY
* @type Number
*/
MILLISECONDS_PER_DAY: 86400000,
/**
* Milliseconds per week.
* @final
* @static
* @property MILLISECONDS_PER_WEEK
* @type Number
*/
MILLISECONDS_PER_WEEK: 604800000
};
/**
* Returns the UNIX timestamp of the given date. The UNIX timestamp
* describes a UTC date as number of seconds elapsed since the beginning
* of the UNIX epoche (Midnight, 1970-01-01). Milliseconds of the given
* date are truncated.
* @static
* @method timestamp
* @param {Date} date Interpreted as a UTC value
* @return {Number}
*/
Formatter.util.date.timestamp = function(date) {
return Math.floor(date.valueOf() / 1000);
};
/**
* Returns the number of days for the specified month.
* @static
* @method daysOfMonth
* @param {Date} date Interpreted as a UTC value
* @return {Number}
*/
Formatter.util.date.daysOfMonth = function(date) {
var d = new Date(date.valueOf());
d.setUTCDate(1);
d.setUTCMonth(d.getUTCMonth() + 1);
d.setUTCDate(0);
return d.getUTCDate();
};
/**
* Returns the number of days for the specified year.
* @static
* @method daysOfYear
* @param {Date|Number} arg (Date is interpreted as a UTC value)
* @return {Number}
*/
Formatter.util.date.daysOfYear = function(arg) {
return this.isLeapYear(arg) ? 366 : 365;
};
/**
* Returns true if the specified year is a leap year, false otherwise.
* @static
* @method isLeapYear
* @param {Date|Number} arg (Date is interpreted as a UTC value)
* @return {Boolean}
*/
Formatter.util.date.isLeapYear = function(arg) {
if (arg instanceof Date) { arg = arg.getUTCFullYear(); }
return arg % 4 === 0 && (arg % 100 !== 0 || arg % 400 === 0);
};
/**
* Returns the culture-specific weekday of the given date. The first day
* of the week corresponds to 0, the last day to 6.
* @static
* @method dayOfWeek
* @param {Date} date Interpreted as a UTC value
* @param {Object} culture Culture information
* @return {Number}
*/
Formatter.util.date.dayOfWeek = function(date, culture) {
return (date.getUTCDay() + 7 - culture.firstDayOfWeek) % 7;
};
/**
* Returns the day of the year specified by the given date. The first day
* of the year corresponds to 1.
* @static
* @method dayOfYear
* @param {Date} date Interpreted as a UTC value
* @return {Number}
*/
Formatter.util.date.dayOfYear = function(date) {
var result = date.getUTCDate(),
year = date.getUTCFullYear(),
month = date.getUTCMonth() - 1;
while (month >= 0) {
result += this.daysOfMonth(new Date(Date.UTC(year, month)));
month--;
}
return result;
};
/**
* Returns a date representing the n-th day of the week specified by the
* given date.
*
* @example
var d = new Date('2012-07-04T00:00Z'), //Wednesday
c = { firstDayOfWeek: 1 }; //culture with Monday as first weekday
Formatter.util.date.nthDayOfWeek(
d, c, 0); //Date representing '2012-07-02T00:00'
Formatter.util.date.nthDayOfWeek(
d, c, 6); //Date representing '2012-07-08T00:00'
* @static
* @method nthDayOfWeek
* @param {Date} date Interpreted as a UTC value
* @param {Object} culture Culture information
* @param {Number} n In [0,6]
* @return {Date}
*/
Formatter.util.date.nthDayOfWeek = function(date, culture, n) {
var d = new Date(date.valueOf());
d.setUTCDate(d.getUTCDate() - this.dayOfWeek(date, culture) + n);
return d;
};
/**
* Returns a date representing the first day of the week specified by
* the given date.
* @static
* @method firstDayOfWeek
* @param {Date} date Interpreted as a UTC value
* @param {Object} culture Culture information
* @return {Date}
*/
Formatter.util.date.firstDayOfWeek = function(date, culture) {
return this.nthDayOfWeek(date, culture, 0);
};
/**
* Returns a date representing the first day of the week specified by
* the given date.
* @static
* @method lastDayOfWeek
* @param {Date} date Interpreted as a UTC value
* @param {Object} culture Culture information
* @return {Date}
*/
Formatter.util.date.lastDayOfWeek = function(date, culture) {
return this.nthDayOfWeek(date, culture, 6);
};
/**
* Returns a date representing the first day of the month specified by
* the given date.
* @static
* @method firstDayOfMonth
* @param {Date} date Interpreted as a UTC value
* @return {Date}
*/
Formatter.util.date.firstDayOfMonth = function(date) {
return new Date(new Date(date.valueOf()).setUTCDate(1));
};
/**
* Returns a date representing the last day of the month specified by the
* given date.
* @static
* @method lastDayOfMonth
* @param {Date} date Interpreted as a UTC value
* @return {Date}
*/
Formatter.util.date.lastDayOfMonth = function(date) {
var d = new Date(date.valueOf());
d.setUTCDate(1);
d.setUTCMonth(d.getUTCMonth() + 1);
d.setUTCDate(0);
return d;
};
/**
* Returns a date representing the first day of the specified year.
* @static
* @method firstDayOfYear
* @param {Date|Number} arg (Date is interpreted as a UTC value)
* @return {Date}
*/
Formatter.util.date.firstDayOfYear = function(arg) {
if (!(arg instanceof Date)) { return new Date(Date.UTC(arg, 0, 1)); }
var d = new Date(arg.valueOf());
d.setUTCDate(1);
d.setUTCMonth(0);
return d;
};
/**
* Returns a date representing the last day of the specified year.
* @static
* @method lastDayOfYear
* @param {Date|Number} arg (Date is interpreted as a UTC value)
* @return {Date}
*/
Formatter.util.date.lastDayOfYear = function(arg) {
var d = (arg instanceof Date ?
new Date(arg.valueOf()) :
new Date(Date.UTC(arg, 0)));
d.setUTCDate(1);
d.setUTCMonth(0);
d.setUTCFullYear(d.getUTCFullYear() + 1);
d.setUTCDate(0);
return d;
};
/**
* Returns the ISO-8601 week specified by the given date.
* @static
* @method isoWeek
* @param {Date} date Interpreted as a UTC value
* @return {Number}
*/
Formatter.util.date.isoWeek = function(date) {
var d = new Date(Date.UTC(date.getUTCFullYear(), 0, 4)),
m, //monday, first calendar week
result;
m = this.nthDayOfWeek(d, {firstDayOfWeek: 1}, 0);
if (date.valueOf() < m.valueOf()) { //date before monday (1-3 Jan.)
d.setUTCFullYear(d.getUTCFullYear() - 1);
m = this.nthDayOfWeek(d, {firstDayOfWeek: 1}, 0);
}
return Math.floor(
(date.valueOf() - m.valueOf()) / this.MILLISECONDS_PER_WEEK) + 1;
};
/**
* Returns the century specified by the given date.
* @static
* @method century
* @param {Date} date Interpreted as a UTC value
* @return {Number}
*/
Formatter.util.date.century = function(date) {
return Math.floor(date.getUTCFullYear() / 100) + 1;
};
/**
* Returns the number of past centuries specified by the given date.
* @static
* @method pastCenturies
* @param {Date} date Interpreted as a UTC value
* @return {Number}
*/
Formatter.util.date.pastCenturies = function(date) {
return Math.floor(date.getUTCFullYear() / 100);
};
/**
* Returns true if the time specified by the given date is in the range
* [00:00, 12:00). Returns false otherwise.
* @static
* @method isAM
* @param {Date} date Interpreted as a UTC value
* @return {Boolean}
*/
Formatter.util.date.isAM = function(date) {
var d = new Date(Date.UTC(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate()
));
return (date.valueOf() - d.valueOf()) < (12 * this.MILLISECONDS_PER_HOUR);
};
/**
* Returns true if the time specified by the given date is in the range
* [12:00, 00:00). Returns false otherwise.
* @static
* @method isPM
* @param {Date} date Interpreted as a UTC value
* @return {Boolean}
*/
Formatter.util.date.isPM = function(date) {
var d = new Date(Date.UTC(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate()
));
return (date.valueOf() - d.valueOf()) >= (12 * this.MILLISECONDS_PER_HOUR);
};
/**
* Number formatter.
* @static
* @class Formatter.number
*/
Formatter.number = {};
/**
* Returns the given number in hexadecimal exponential form.
* <br/><br/>
* Details on hexadecimal exponential encoding:
* <ul>
* <li><a href="http://en.wikipedia.org/wiki/Hexadecimal#Hexadecimal_exponential_notation">http://en.wikipedia.org/wiki/Hexadecimal#Hexadecimal_exponential_notation</a></li>
* <li><a href="http://de.wikipedia.org/wiki/IEEE_754">http://de.wikipedia.org/wiki/IEEE_754</a></li>
* <li><a href="http://www.2ality.com/2012/04/number-encoding.html">http://www.2ality.com/2012/04/number-encoding.html</a></li>
* <li><a href="http://osr507doc.sco.com/en/topics/FltPtOps_DeNormNums.html">http://osr507doc.sco.com/en/topics/FltPtOps_DeNormNums.html</a></li>
* </ul>
* @static
* @method toHexExp
* @param {any} arg Number compatibel argument
* @return {String} Hex exponential form of the given string
*/
Formatter.number.toHexExp = function(arg) {
var r, //result
b, //string representing the given number in base 2
len, //b's length
pos, //floating point position in b
sign, //is the given number signed?
m, //mantissa
exp, //exponent
i,
ieee754_64_bias = 1023; //IEEE 754 (double precision) exponent bias
arg = Number(arg);
//return NaN, Infinity, -Infinity unchanged
if (Formatter.util.number.isSymbolicNumber(arg)) { return String(arg); }
//distinct negative zero from zero
if (arg === 0) {
return Formatter.util.number.isSigned(arg) ? '-0x0.0p0' : '0x0.0p0';
}
sign = arg < 0;
arg = Math.abs(arg);
b = arg.toString(2);
pos = b.indexOf('.');
if (pos < 0) { //integer
exp = b.length - 1;
} else {
if (pos === 1 && Number(b.charAt(0)) === 0) { //negative exponent
//find first fractional 1-bit
len = b.length;
i = 2;
while (i < len && b.charAt(i) !== '1') { i++; }
//consider exponent bias specified by IEEE 754 (double precision)
exp = i >= ieee754_64_bias ?
-(ieee754_64_bias - 1) :
-(i - 1);
} else { //positive exponent
exp = b.slice(0, pos).length - 1;
}
}
m = Number(arg / Math.pow(2, exp)).toString(16);
if (m.indexOf('.') < 0) { m = m + '.0'; }
r = '0x' + m + 'p' + exp;
return sign ? '-' + r : r;
};
/**
* Returns a string representing the given number in decimal form.
* @static
* @method toDecimal
* @param {any} arg Number compatible value to be formatted
* @param {Object} [options] Formatting options. Default values:
*
{
precision: undefined, // Number of significant fractional digits. Data
// type limited for falsy values other than 0.
considerZeroSign: false // Whether to return a sign for negative zero or not
}
* @return {String}
*/
Formatter.number.toDecimal = function(arg, options) {
options = options || {};
options.considerZeroSign = Boolean(options.considerZeroSign);
var r = '',
numStr,
sign,
m,
exp, //exponent
pos; //position of 'e' and '.'
//return NaN, Infinity, -Infinity unchanged
if (Formatter.util.number.isSymbolicNumber(arg)) { return String(arg); }
//round to precision fractional digits
if (options.precision || options.precision === 0) {
arg = Formatter.util.number.round(Number(arg), options.precision);
} else {
arg = Number(arg);
}
//signed argument?
sign = options.considerZeroSign ?
Formatter.util.number.isSigned(arg) :
arg < 0;
//since Number.toString returns decimal notation for small numbers and
//scientific notation for numbers greater than a certain threshold,
//parsing is done based on the exponential form of the given number.
numStr = arg.toExponential();
pos = numStr.indexOf('e');
m = numStr.slice((numStr.charAt(0) === '-' ? 1 : 0), pos);
exp = Number(numStr.substr(pos + 1));
pos = m.indexOf('.');
if (pos < 0) { //integer mantissa
r = m;
pos = r.length;
} else {
r = m.slice(0, pos) + m.substr(pos + 1);
}
pos = pos + exp;
if (0 < pos && pos < r.length) {
r = r.slice(0, pos) + '.' + r.substr(pos);
} else if (pos > r.length) {
r = Formatter.util.padRight(r, '0', pos);
} else if (pos <= 0) {
r = '0.' + Formatter.util.padLeft(r, '0', r.length - pos);
pos = 1;
}
//add fractional zero digits if the result's number of fractional digits
//is less than the given precision
if (options.precision &&
options.precision > 0 &&
r.length - pos - 1 < options.precision) {
if (pos === r.length) { r += '.'; }
r = Formatter.util.padRight(r, '0', pos + options.precision + 1);
}
//add sign
if (sign) { r = '-' + r; }
return r;
};
/**
* Returns a string representing the given number in scientific notation.
* @static
* @method toScientific
* @param {any} arg Number compatible value to be formatted
* @param {Object} [options] Formatting options. Default values:
*
{
precision: undefined, // Mantissa precision. Data type limited for falsy
// values other than 0.
expMinWidth: 1, // Min width of the exponent (excl. 'e' and sign).
upperCase: false, // Whether to use 'e' or 'E' for the exponent.
considerZeroSign: false // Whether to return a sign for negative zero or not
}
* @return {String}
*/
Formatter.number.toScientific = function(arg, options) {
options = options || {};
options.expMinWidth = options.expMinWidth || 1;
options.upperCase = Boolean(options.upperCase);
options.considerZeroSign = Boolean(options.considerZeroSign);
var r,
numStr,
sign,
m, //mantissa
exp,
expStr = options.upperCase ? 'E' : 'e',
len,
pos,
i;
arg = Number(arg);
numStr = String(arg);
if (Formatter.util.number.isSymbolicNumber(arg)) {
return numStr;
}
sign = options.considerZeroSign ?
Formatter.util.number.isSigned(Number(arg)) :
Number(arg) < 0;
//mantissa
pos = numStr.indexOf('e');
if (pos < 0) {
numStr = arg.toExponential();
pos = numStr.indexOf('e');
}
m = Number(numStr.slice(0, pos));
//exponent
expStr += numStr.charAt(pos + 1); //sign
exp = numStr.substr(pos + 2); //skip 'e', skip sign
len = options.expMinWidth - exp.length;
for (i = len; i > 0; i--) { expStr += '0'; }
expStr += exp;
//mantissa precision
if (options.precision || options.precision === 0) {
r = String(Formatter.util.number.round(m, options.precision));
if (options.precision > 0) {
pos = r.indexOf('.');
if (pos < 0) {
r += '.';
r = Formatter.util.padRight(r, '0', r.length +
options.precision);
} else {
r = Formatter.util.padRight(r, '0',
options.precision + pos + 1);
}
}
} else {
r = String(m);
}
if (m === 0 && sign && options.considerZeroSign) {
r = '-' + r;
}
return r + expStr;
};
/**
* Functions ought to format date components.
* @static
* @class Formatter.date
*/
Formatter.date = {};
/**
* Returns a string representing the specified year.
*
* Negative years are formatted with the prefix '-' by default. Set the
* option property `bcPrefix` or `bcPostfix` to change the default behavior.
* Note that setting a non-falsy postfix implies the prefix ''.
*
* The number of digits in the resulting string depends on the option
* properties `maxDigits` and `leadingZeros`.
* The resulting year is zero padded if `maxDigits` is greater than the
* number of year digits and `leadingZeros` is set to true.
* Most significant digits of the resulting year are truncated if `maxDigits`
* is less than the number of year digits. In that case leading zeros are
* also truncated except `leadingZeros` is set to true.
*
* @example
var d = new Date('2012-01-01T00:00Z');
Formatter.date.year(d); //'2012'
Formatter.date.year(d, {maxDigits:3}); //'12'
Formatter.date.year(d, {maxDigits:3, leadingZeros:true}); //'012'
Formatter.date.year(d, {maxDigits:5, leadingZeros:true}); //'02012'
d = new Date(Date.UTC(-2012, 1));
Formatter.date.year(d); //'-2012'
Formatter.date.year(d, {bcPostfix: ' BC.'}); //'2012 BC.'
Formatter.date.year(d, {bcPrefix: 'BC.'}); //'BC.2012'
* @static
* @method year
* @param {Date} date Interpreted as a UTC value
* @param {Object} [options] Format options. Default values are:
*
{
bcPrefix: '-',
bcPostfix: '',
leadingZeros: false
maxDigits: number of year digits
}
*
* @return {String}
*/
Formatter.date.year = function(date, options) {
var y = date.getUTCFullYear(),
bc = (y < 0),
opt = options || {};
opt.bcPostfix = opt.bcPostfix || '';
opt.bcPrefix = (opt.bcPostfix ? '' : (opt.bcPrefix || '-'));
opt.leadingZeros = opt.leadingZeros || false;
opt.maxDigits = opt.maxDigits ||
(bc ? String(y).length - 1 : String(y).length);
y = String(Math.abs(y % Math.pow(10, opt.maxDigits)));
if (opt.leadingZeros) {
y = Formatter.util.padLeft(y, '0', opt.maxDigits);
}
return (bc ? [opt.bcPrefix, y, opt.bcPostfix].join('') : y);
};
/**
* Returns a string representing the day of the year specified by the
* given date.
* @static
* @method dayOfYear
* @param {Date} date Interpreted as a UTC value
* @param {Boolean} leadingZeros Zero padded result?
* @return {String}
*/
Formatter.date.dayOfYear = function(date, leadingZeros) {
return leadingZeros ?
Formatter.util.padLeft(Formatter.util.date.dayOfYear(date), '0', 3) :
String(Formatter.util.date.dayOfYear(date));
};
/**
* Returns a string representing the month specified by the given date.
* @static
* @method month
* @param {Date} date Interpreted as a UTC value
* @param {Boolean} leadingZero Zero padded result?
* @return {String}
*/
Formatter.date.month = function(date, leadingZero) {
var m = String(date.getUTCMonth() + 1);
if (leadingZero && m.length < 2) { return '0' + m; }
return m;
};
/**
* Returns a string representing the day of the month specified by the
* given date.
* @static
* @method dayOfMonth
* @param {Date} date Interpreted as a UTC value
* @param {Boolean} leadingZero Zero padded result?
* @return {String}
*/
Formatter.date.dayOfMonth = function(date, leadingZero) {
var d = String(date.getUTCDate());
if (leadingZero && d.length < 2) { return '0' + d; }
return d;
};
/**
* Returns the culture-specific month name specfied by the given date.
* @static
* @method monthName
* @param {Date} date Interpreted as a UTC value
* @param {Object} culture Culture information
* @return {String}
*/
Formatter.date.monthName = function(date, culture) {
return culture.months[date.getUTCMonth()];
};
/**
* Returns the culture-specific abbreviated month name specfied by the
* given date.
* @static
* @method abbreviatedMonthName
* @param {Date} date Interpreted as a UTC value
* @param {Object} culture Culture information
* @return {String}
*/
Formatter.date.abbreviatedMonthName = function(date, culture) {
return culture.monthsAbbr[date.getUTCMonth()];
};
/**
* Returns the culture-specific weekday name specfied by the given date.
* @static
* @method weekdayName
* @param {Date} date Interpreted as a UTC value
* @param {Object} culture Culture information
* @return {String}
*/
Formatter.date.weekdayName = function(date, culture) {
return culture.weekdays[date.getUTCDay()];
};
/**
* Returns the culture-specific abbreviated weekday name specfied by
* the given date.
* @static
* @method abbreviatedWeekdayName
* @param {Date} date Interpreted as a UTC value
* @param {Object} culture Culture information
* @return {String}
*/
Formatter.date.abbreviatedWeekdayName = function(date, culture) {
return culture.weekdaysAbbr[date.getUTCDay()];
};
/**
* Returns the formatted hours specified by the given date.
* @static
* @method hours
* @param {Date} date Interpreted as a UTC value
* @param {Boolean} leadingZero Zero padded result?
* @param {Boolean} h12 Hours in [1,12]?
* @return {String}
*/
Formatter.date.hours = function(date, leadingZero, h12) {
var h = date.getUTCHours();
if (h12) {
h = h % 12;
if (h === 0) { h = 12; }
}
return String(leadingZero && h < 10 ? ['0', h].join('') : h);
};
/**
* Returns the formatted minutes specified by the given date.
* @static
* @method minutes
* @param {Date} date Interpreted as a UTC value
* @param {Boolean} leadingZero Zero padded result?
* @return {String}
*/
Formatter.date.minutes = function(date, leadingZero) {
var m = date.getUTCMinutes();
return String(leadingZero && m < 10 ? ['0', m].join('') : m);
};
/**
* Returns the formatted seconds specified by the given date.
* @static
* @method seconds
* @param {Date} date Interpreted as a UTC value
* @param {Boolean} leadingZero Zero padded result?
* @return {String}
*/
Formatter.date.seconds = function(date, leadingZero) {
var s = date.getUTCSeconds();
return String(leadingZero && s < 10 ? ['0', s].join('') : s);
};
/**
* Returns the formatted milliseconds specified by the given date.
* @static
* @method milliseconds
* @param {Date} date Interpreted as a UTC value
* @param {Boolean} leadingZeros Zero padded result?
* @return {String}
*/
Formatter.date.milliseconds = function(date, leadingZeros) {
var ms = date.getUTCMilliseconds();
if (leadingZeros) {
return Formatter.util.pad(ms, '0', 3, true);
}
return String(ms);
};
/**
* Returns a string representing the morning/afternoon designator for the
* time specified by the given date.
* @static
* @method timeDesignator
* @param {Date} date Interpreted as a UTC value
* @param {Object} culture Culture information
* @return {String}
*/
Formatter.date.timeDesignator = function(date, culture) {
return (Formatter.util.date.isAM(date) ?
culture.amToken :
culture.pmToken);
};
/**
* Returns a string representing the given date's time zone offset from UTC.
* @static
* @method timezoneOffset
* @param {Date} date Interpreted as a date with the same timezone as
* provided by the host OS.
* @return {String}
*/
Formatter.date.timezoneOffset = function(date) {
var tz = date.toTimeString().match(/GMT((?:\+|\-)\d{4})/);
return (Formatter.util.isArray(tz) && tz.length > 1 ? tz[1] : undefined);
};
/**
* Returns a string representing the time zone abbreviation specified by
* the given date.
* @static
* @method abbreviatedTimezone
* @param {Date} date Interpreted as a date with the same timezone as
* provided by the host OS.
* @return {String}
*/
Formatter.date.abbreviatedTimezone = function(date) {
var tz = date.toTimeString().match(/\((\w+)\)/);
return (Formatter.util.isArray(tz) && tz.length > 1 ? tz[1] : undefined);
};
/**
* Returns a string representing the 24h time specified by the given date.
* @static
* @method time
* @param {Date} date Interpreted as a UTC value
* @param {Boolean} leadingZeros Zero padded time components?
* @return {String}
*/
Formatter.date.time = function(date, leadingZeros) {
return this.hours(date, leadingZeros) + ':' +
this.minutes(date, leadingZeros) + ':' +
this.seconds(date, leadingZeros);
};
/**
* Returns a string representing the 12h time specified by the given date.
* @static
* @method time12
* @param {Date} date Interpreted as a UTC value
* @param {Boolean} leadingZeros Zero padded time components?
* @param {Boolean} designator Including time designator (AM/PM)?
* @return {String}
*/
Formatter.date.time12 = function(date, leadingZeros, designator, culture) {
var t = this.hours(date, leadingZeros, true) + ':' +
this.minutes(date, leadingZeros) + ':' +
this.seconds(date, leadingZeros);
if (designator) {
t += (' ' + this.timeDesignator(date, culture));
}
return t;
};
/*
* Exports this module to node.
*/
function exportNode(mod) {
//load default culture and flavor module
mod.Formatter.options({
cultureId: mod.Formatter.options().cultureId(),
flavorId: mod.Formatter.o