pat
Version:
Formats data described by format strings
1,395 lines (1,379 loc) • 46.4 kB
JavaScript
/**
* Formatting data described by format specifiers of a certain flavor.
*
* @module pat
* @submodule flavors
*/
/**
* Represents a data formatter that parses format specifiers known from Java's
* Formatter class (java.util.Formatter).
*
* The module includes a Scanner, a Parser and the format function. It exports
* itself as a Node module, an AMD module or to the global scope, depending on
* the environment.
*
* @static
* @class java
* @namespace flavors
*/
/**
* @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, //module to export
Formatter,
Scanner,
Parser,
format,
parser,
__ = {}; //private section
/*
* Throws an error with the given message.
*/
function err(msg) {
throw new Error(msg);
}
/*
* Throws an error corresponding to invalid Scanner/Parser input.
*/
function inputErr(input, start, hint) {
var i = input,
n = 10, //display input[i] and the next n chars max
len = input.length,
msg = ['Invalid format specifier at zero based index ',
start,
'.\n',
input[start]].join('');
if (i + n < len) { len = i + n; }
while (i < len) { msg += input[i++]; }
err(msg + '...\n^\n' + hint);
}
/**
* Java flavored data formatter.
* @static
* @class flavors.java.Formatter
*/
Formatter = {};
/**
* Utility functions.
* @static
* @class flavors.java.Formatter.util
*/
Formatter.util = {};
/**
* Appends or prepends spaces to the given array until it has the given
* length.
* @static
* @chainable
* @method spacePad
* @param {Array} array
* @param {Number} width Resulting width of the given array
* @param {Boolean} [prepend = false] Whether to append or prepend to the
* given array
* @return {Formatter.util}
*/
Formatter.util.spacePad = function(array, width, prepend) {
prepend = Boolean(prepend);
var delta = width - array.length,
i;
if (delta > 0) {
for (i = 0; i < delta; i++) {
if (prepend) {
array.unshift(' ');
} else {
array.push(' ');
}
}
}
return this;
};
/**
* Java flavored number formatter.
* @static
* @class flavors.java.Formatter.number
*/
Formatter.number = {
/**
* Default mantissa precision for number strings in computerized
* scientific notation.
* @final @static
* @property DEFAULT_PRECISION
* @type {Number}
*/
DEFAULT_PRECISION: 6,
/**
* Default exponent width (excluding 'e' and sign) for number strings
* in computerized scientific notation.
* @final @static
* @property DEFAULT_MIN_EXPONENT_WIDTH
* @type {Number}
*/
DEFAULT_MIN_EXPONENT_WIDTH: 2
};
/**
* Java number localization algorithm.
* @static
* @class flavors.java.Formatter.number.localize
*/
Formatter.number.localize = {
/**
* @final @static
* @property ASCII_ZERO ASCII code for '0' (48)
* @type {Number}
*/
ASCII_0: 48,
/**
* @final @static
* @property ASCII_9 ASCII code for '9' (57)
* @type {Number}
*/
ASCII_9: 57
};
/**
* Java number localization algorithm.
* Each digit character d in the string is replaced by a culture-specific
* digit computed relative to the current culture's zero digit z; that is
* d - '0' + z.
* @static
* @method digits
* @param {Array} arg Character array representing the Number to be localized
* @para {Object} culture Culture specific information
* @return undefined
*/
Formatter.number.localize.digits = function(arg, culture) {
var i = arg.length - 1,
z = culture.zeroDigit.charCodeAt(0),
c;
while (i >= 0) {
c = arg[i].charCodeAt(0);
if (this.ASCII_0 <= c && c <= this.ASCII_9) { //localize digits
arg[i] = String.fromCharCode(c - this.ASCII_0 + z);
}
i--;
}
};
/**
* Java number localization algorithm.
* If a decimal separator is present, a culture-specific decimal separator
* is substituted.
* @static
* @method decimalSeparator
* @param {Array} arg Character array representing the Number to be localized
* @param {Object} culture Culture information
* @return undefined
*/
Formatter.number.localize.decimalSeparator = function(arg, culture) {
var pos = arg.indexOf('.');
if (pos >= 0) {
arg[pos] = culture.decimalSeparator;
}
};
/**
* Java number localization algorithm.
* If the ',' ('\u002c') flag is given, then the culture-specific grouping
* separator is inserted by scanning the integer part of the string from
* least significant to most significant digits and inserting a separator
* at intervals defined by the culture's grouping size.
* @static
* @method groupingSeparator
* @param {Array} arg Character array representing the Number to be localized
* @param {Object} culture Culture information
* @result undefined
*/
Formatter.number.localize.groupingSeparator = function(arg, culture) {
var i = arg.length - 1,
j = culture.groupingSize - 1;
while (i >= 0) {
if (j === 0 && i !== 0) {
arg.splice(i, 0, culture.groupingSeparator);
j = culture.groupingSize;
}
i--;
j--;
}
};
/**
* Java number localization algorithm.
* If the '0' flag is given, then the culture-specific zero digits are
* inserted after the sign character, if any, and before the first non-zero
* digit, until the length of the string is equal to the requested field
* width.
* @static
* @method localize
* @param {Array} arg Character array representing the Number to be localized
* @param {Number} width Length of the resulting string
* @param {Object} culture Culture information
* @return undefined
*/
Formatter.number.localize.zeroPad = function(arg, width, culture) {
var len = arg.length,
delta = width - len,
pos = 0,
i = 0;
if (delta > 0) {
while (i < len && (
arg[i] === '-' ||
arg[i] === '+' ||
arg[i] === '0')) { i++; }
if (i < len) {
pos = i;
for (i = 0; i < delta; i++) {
arg.splice(pos, 0, culture.zeroDigit);
}
}
}
};
/**
* Java flavored number localization algorithm.
*
* If the value is negative and the '(' flag is given, then a '(' is
* prepended and a ')' is appended.
*
* If the value is negative and '(' flag is not given, then a '-' is
* prepended.
*
* If the '+' flag is given and the value is positive or zero, then a '+'
* will be prepended.
*
* If the ' ' flag is given and the value is positive or zero, then a ' '
* will be prepended.
*
* @static
* @method sign
* @param {Array} arg Character array representing the Number to be localized
* @param {Object} opt Sign options
* @return undefined
*/
Formatter.number.localize.sign = function(arg, opt) {
if (arg[0] === '-') { //negative value
if (opt.parenthesesWhenNegative) {
arg[0] = '(';
arg.push(')');
}
} else { //positive value
if (opt.leadingPlusWhenPositive) {
arg.unshift('+');
} else if (opt.leadingSpaceWhenPositive) {
arg.unshift(' ');
}
}
};
/**
* Java localization algorithm for number strings.
* @static
* @method localizeNumber
* @param {String} arg Number string to be localized
* @param {Object} culture Culture-specific information
* @param {Number} width The result's minimum width
* @param {Object} options Localization options (Options of a parsed token)
* @return {String} The localized number string
*/
Formatter.number.localize.localizeNumber = function(
arg,
culture,
width,
options) {
var r = arg.split('');
this.digits(r, culture);
this.decimalSeparator(r, culture);
if (options.localizedGroupingSeparator) {
this.groupingSeparator(r, culture);
}
if (options.zeroPad) {
this.zeroPad(r, width, culture);
}
this.sign(r, options);
if (!options.zeroPad && width) { //pad with spaces
Formatter.util.spacePad(r, width, !options.leftJustify);
}
return r.join('');
};
/**
* Allocates a new Scanner for Java format specifiers.
* @constructor
* @class flavors.java.Scanner
* @param {String|Array} input String or character array to be scanned
* @return {Scanner} New Scanner instance
*/
Scanner = function(input) {
if (!(this instanceof Scanner)) { return new Scanner(input); }
this.input(input);
};
/**
* Different categories of scanned tokens.
* @static
* @class flavors.java.Scanner.tokenCategories
*/
Scanner.tokenCategories = {
/**
* Represents a token of general conversion type.
* @final @static
* @property general
* @type {Number}
*/
general: 0,
/**
* Represents a token to be formatted as a character.
* @final @static
* @property character
* @type {Number}
*/
character: 1,
/**
* Represents a token to be formatted as an integer.
* @final @static
* @property integral
* @type {Number}
*/
integral: 2,
/**
* Represents a token to be formatted as a floating point number.
* @final @static
* @property floatingPoint
* @type {Number}
*/
floatingPoint: 3,
/**
* Represents a token to be formatted as a date.
* @final @static
* @property datetime
* @type {Number}
*/
datetime: 4,
/**
* Represents the percent literal token.
* @final @static
* @property percent
* @type {Number}
*/
percent: 5,
/**
* Represents the line separator literal token.
* @final @static
* @property lineSeparator
* @type {Number}
*/
lineSeparator: 6,
/**
* Represents a string token.
* @final @static
* @property text
* @type {Number}
*/
text: 7
};
/**
* Resets this Scanner's state.
* @chainable
* @method reset
* @for flavors.java.Scanner
* @return {Scanner}
*/
Scanner.prototype.reset = function() {
this._in = [];
this._ch = ''; //current input character
this._iNextCh = 0; //points to the next input character
this._iArg = -1;
this._iPrevArg = -1;
this._token = null;
return this;
};
/**
* Sets or returns this Scanner's input.
* @chainable
* @method input
* @for flavors.java.Scanner
* @param {String|Array} [input] String or character array to be scanned
* @return {Scanner|Array} This Scanner if called as setter, this Scanner's
* current input if called as getter.
*/
Scanner.prototype.input = function(input) {
if (arguments.length === 0) { return this._in; }
this.reset();
if (input) { this._in = __.fmt.util.toArray(input); }
};
/**
* Throws an error using the given hint for the error message.
* The error message includes the relevant input substring, the index of
* the error prone character within the input, and the given hint.
* @method err
* @for flavors.java.Scanner
* @param {String} hint To be included in the error message
*/
Scanner.prototype.err = function(hint) {
inputErr(this._input, this._iNextCh - 1, hint);
};
/*
* Reads the next character of the input.
* @chainable
* @method readCh
* @for flavors.java.Scanner
* @return {Scanner}
*/
Scanner.prototype.readCh = function() {
this._ch = this._in[this._iNextCh++];
};
/*
* Returns true if the current character is a digit, false otherwise.
* @method isDigit
* @for flavors.java.Scanner
* @return {Boolean}
*/
Scanner.prototype.isDigit = function() {
return '0' <= this._ch && this._ch <= '9';
};
/*
* Throws an error if the current character does not represent a digit.
* @chainable
* @method expectDigit
* @for flavors.java.Scanner
* @return {Scanner}
*/
Scanner.prototype.expectDigit = function() {
if (!this.isDigit()) { this.err('Digit expected.'); }
return this;
};
/*
* Throws an error if the current character is not equal to the given one.
* @chainable
* @method expectCh
* @for flavors.java.Scanner
* @param {String} val Expected character
* @return {Scanner}
*/
Scanner.prototype.expectCh = function(val) {
if (this._ch !== val) { this.err("'" + val + "'" + ' expected.'); }
return this;
};
/*
* Throws an error if the current character is falsy, e.g. after the last
* input character has been read.
* @chainable
* @method expectAnyCh
* @for flavors.java.Scanner
* @return {Scanner}
*/
Scanner.prototype.expectAnyCh = function() {
if (!this._ch) { this.err('Unexpected string end.'); }
return this;
};
/*
* Reads digit after digit and returns the value as a Number. Expects the
* current character to be a digit. Stops reading as soon as the current
* character does not represent a digit.
* @method readNumber
* @for flavors.java.Scanner
* @return {Number}
*/
Scanner.prototype.readNumber = function() {
var result = this._ch;
this.readCh();
while(this.isDigit()) {
result += this._ch;
this.readCh();
}
return Number(result);
};
/*
* Reads the character sequence that starts with the current character and
* ends with the character before the next '%'. If there is not a next '%'
* sign then it will read the substring from the current character to the
* input end.
* @chainable
* @method readText
* @for flavors.java.Scanner
* @return {Scanner}
*/
Scanner.prototype.readText = function() {
var end = this._in.indexOf('%', this._iNextCh);
if (end >= 0) {
this._token.value = this._in.slice(this._iNextCh - 1, end).join('');
this._iNextCh = end;
} else {
this._token.value = this._in.slice(this._iNextCh - 1).join('');
this._iNextCh = this._in.length;
}
return this;
};
/*
* Extracts the conversion sequence.
* @chainable
* @method readConversion
* @for flavors.java.Scanner
* @return {Scanner}
*/
Scanner.prototype.readConversion = function() {
function set(token, conversion, category, upperCase) {
token.conversion = conversion;
token.category = category;
token.upperCase = Boolean(upperCase);
}
switch (this._ch) {
case 'b': case 's':
set(this._token, this._ch, Scanner.tokenCategories.general);
break;
case 'B': case 'S':
set(this._token, this._ch.toLowerCase(),
Scanner.tokenCategories.general, true);
break;
case 'c':
set(this._token, this._ch, Scanner.tokenCategories.character);
break;
case 'C':
set(this._token, this._ch.toLowerCase(),
Scanner.tokenCategories.character, true);
break;
case 'd': case 'o': case 'x':
set(this._token, this._ch, Scanner.tokenCategories.integral);
break;
case 'X':
set(this._token, this._ch.toLowerCase(),
Scanner.tokenCategories.integral, true);
break;
case 'e': case 'f': case 'g': case 'a':
set(this._token, this._ch, Scanner.tokenCategories.floatingPoint);
break;
case 'E': case 'G': case 'A':
set(this._token, this._ch.toLowerCase(),
Scanner.tokenCategories.floatingPoint, true);
break;
case 't': case 'T':
set(this._token, 't', Scanner.tokenCategories.datetime,
this._ch === 'T');
this.readCh();
this.expectAnyCh();
switch (this._ch) {
//time
case 'H': case 'I': case 'k': case 'l': case 'M': case 'S':
case 'L': case 'p': case 'z': case 'Z': case 's': case 'Q':
//date
case 'B': case 'b': case 'h': case 'A': case 'a': case 'C':
case 'Y': case 'y': case 'j': case 'm': case 'd': case 'e':
case 'V':
//compositions
case 'R': case 'T': case 'r': case 'D': case 'F': case 'c':
this._token.conversion += this._ch;
break;
default:
this.err('Unexpected datetime conversion: \'' + this._ch + '\'.');
break;
}
break;
default:
this.err('Unexpected conversion: \'' + this._ch + '\'.');
break;
}
return this;
};
/*
* Extracts the precision.
* @chainable
* @method readPrecision
* @for flavors.java.Scanner
* @return {Scanner}
*/
Scanner.prototype.readPrecision = function() {
if (this._ch === '.') {
this.readCh();
this.expectDigit();
this._token.precision = this.readNumber();
}
return this;
};
/*
* Extracts the width.
* @chainable
* @method readWidth
* @for flavors.java.Scanner
* @return {Scanner}
*/
Scanner.prototype.readWidth = function() {
if (this.isDigit()) { this._token.width = this.readNumber(); }
return this;
};
/*
* Extracts the flags.
* @chainable
* @method readFlags
* @for flavors.java.Scanner
* @return {Scanner}
*/
Scanner.prototype.readFlags = function() {
switch(this._ch) {
case '-':
this._token.options.leftJustify = true;
this.readCh();
this.readFlags();
break;
case '#':
this._token.options.alternateForm = true;
this.readCh();
this.readFlags();
break;
case '+':
this._token.options.leadingPlusWhenPositive = true;
this.readCh();
this.readFlags();
break;
case ' ':
this._token.options.leadingSpaceWhenPositive = true;
this.readCh();
this.readFlags();
break;
case '0':
this._token.options.zeroPad = true;
this.readCh();
this.readFlags();
break;
case ',':
this._token.options.localizedGroupingSeparator = true;
this.readCh();
this.readFlags();
break;
case '(':
this._token.options.parenthesesWhenNegative = true;
this.readCh();
this.readFlags();
break;
}
return this;
};
/*
* Sets the argument index. Used when the format specifier does not include
* an explicit or a relative index.
* @chainable
* @method implicitArgumentIndex
* @for flavors.java.Scanner
* @return {Scanner}
*/
Scanner.prototype.implicitArgumentIndex = function() {
this._iArg++;
this._token.argumentIndex = this._iArg;
this._iPrevArg = this._token.argumentIndex;
return this;
};
/*
* Extracts the argument index.
* @chainable
* @method readArgumentIndex
* @for flavors.java.Scanner
* @return {Scanner}
*/
Scanner.prototype.readArgumentIndex = function() {
var number = 0,
iTmp = this._iNextCh;
if (this.isDigit()) {
if (this._ch !== 0) {
number = this.readNumber();
if (this._ch === '$') { //absolute index
this._token.argumentIndex = number - 1;
this._iPrevArg = this._token.argumentIndex;
this.readCh();
this.expectAnyCh();
} else { //implicit index
this.implicitArgumentIndex();
// number read was width => rewind
this._iNextCh = iTmp;
this._ch = this._in[iTmp - 1];
}
} else { //implicit index
this.implicitArgumentIndex();
}
} else if (this._ch === '<') { //relative index
if (this._iPrevArg < 0) {
this.err('Missing previous format specifier');
}
this._token.argumentIndex = this._iPrevArg;
this.readCh();
this.expectAnyCh();
} else { //implicit index
this.implicitArgumentIndex();
}
return this;
};
/**
* Returns true if it's possible to scan another token, false otherwise.
* @method hasNext
* @for flavors.java.Scanner
* @return {Boolean}
*/
Scanner.prototype.hasNext = function() {
return this._in && this._in.length > 0 &&
//iNextCh points to the first char or to any char but the last
(this._iNextCh === 0 || this._iNextCh < this._in.length);
};
/**
* Returns the next token or undefined if there is no more text to scan.
* @method next
* @for flavors.java.Scanner
* @return {Object}
*/
Scanner.prototype.next = function() {
if (!this.hasNext()) { return undefined; }
this._token = {
category: Scanner.tokenCategories.general,
argumentIndex: -1,
options: {
leftJustify: false,
alternateForm: false,
leadingPlusWhenPositive: false,
leadingSpaceWhenPositive: false,
zeroPad: false,
localizedGroupingSeparator: false,
parenthesesWhenNegative: false
},
width: false,
precision: false,
conversion: '',
upperCase: false,
startIndex: 0,
value: null
};
this._token.startIndex = this._iNextCh;
this.readCh();
if (this._ch === '%') {
this.readCh();
this.expectAnyCh();
switch (this._ch) {
case '%':
this._token.conversion = this._ch;
this._token.category = Scanner.tokenCategories.percent;
break;
case 'n':
this._token.conversion = this._ch;
this._token.category = Scanner.tokenCategories.lineSeparator;
break;
default:
this.readArgumentIndex();
this.readFlags();
this.readWidth();
this.readPrecision();
this.readConversion();
break;
}
} else {
this._token.category = Scanner.tokenCategories.text;
this.readText();
}
return this._token;
};
/**
* Allocates a new Parser for Java format specifiers.
* @constructor
* @class flavors.java.Parser
* @return {Parser} New Parser instance
*/
Parser = function() {
if (!(this instanceof Parser)) { return new Parser(); }
this._scanner = new Scanner();
this._target = [];
};
/**
* Parses a format specifier of conversion type 'general'.
* @chainable
* @method parseGeneral
* @for flavors.java.Parser
* @param {Object} token Token to be parsed
* @param {any} fmtArg Value to be formatted
* @return {Parser}
*/
Parser.prototype.parseGeneral = function(token, fmtArg) {
var val = null;
switch (token.conversion) {
case 'b': case 'B':
val = fmtArg ? 'true' : 'false';
break;
case 's': case 'S':
val = String(fmtArg);
break;
}
if (token.upperCase) {
val = val.toUpperCase();
}
if (token.precision && token.precision < val.length) {
val = val.substr(0, token.precision);
}
if (token.width) {
val = __.fmt.util.pad(val, ' ', token.width, !token.options.leftJustify);
}
this._target.push(val);
return this;
};
/**
* Parses a format specifier of conversion type 'character'
* @chainable
* @method parseCharacter
* @for flavors.java.Parser
* @param {Object} token Token to be parsed
* @param {String|Number} fmtArg String or Unicode code point
* @return {Parser}
*/
Parser.prototype.parseCharacter = function(token, fmtArg) {
var val = null;
if (typeof fmtArg === 'string') {
val = fmtArg.charAt(0);
} else if (typeof fmtArg === 'number') {
val = String.fromCharCode(Math.floor(fmtArg));
} else {
err('Invalid argument. String or Number expected.');
}
if (token.upperCase) {
val = val.toUpperCase();
}
if (token.width) {
val = __.fmt.util.pad(val, ' ', token.width, !token.options.leftJustify);
}
this._target.push(val);
return this;
};
/**
* Parses a symbolic number.
* @chainable
* @method parseSymbolicNumber
* @for flavors.java.Parser
* @param {Object} token Token to be parsed
* @param {any} fmtArg Value to be formatted
* @return {Parser}
*/
Parser.prototype.parseSymbolicNumber = function(token, fmtArg) {
if (Number(fmtArg) === Number.NEGATIVE_INFINITY &&
token.options.parenthesesWhenNegative) {
this._target.push('(Infinity)');
} else {
this._target.push(String(Number(fmtArg)));
}
return this;
};
/**
* Parses a decimal integer.
* @chainable
* @method parseDecInt
* @for flavors.java.Parser
* @param {Object} token Token to be parsed
* @param {Number} fmtArg Value to be formatted
* @param {Object} fmtOpt Format options
* @return {Parser}
*/
Parser.prototype.parseDecInt = function(token, fmtArg, fmtOpt) {
this._target.push(
Formatter.number.localize.localizeNumber(
__.fmt.number.toDecimal(Math.floor(fmtArg)),
fmtOpt.culture(),
token.width,
token.options));
return this;
};
/**
* Parses an octal integer.
* @chainable
* @method parseOctInt
* @for flavors.java.Parser
* @param {Object} token Token to be parsed
* @param {Number} fmtArg Value to be formatted
* @return {Parser}
*/
Parser.prototype.parseOctInt = function(token, fmtArg) {
var val = null;
if (fmtArg >= 0) {
val = Math.floor(fmtArg).toString(8);
} else {
val = Number(__.fmt.util.number.twosComplement(-1 * fmtArg))
.toString(8);
}
if (token.options.alternateForm) { //with '0' prefix
val = '0' + val;
}
if (token.width) { //space or zero pads
if (token.options.zeroPad) {
val = __.fmt.util.padLeft(val, '0', token.width);
} else {
val = __.fmt.util.padLeft(val, ' ', token.width);
}
}
this._target.push(val);
return this;
};
/**
* Parses a hexadecimal integer.
* @chainable
* @method parseHexInt
* @for flavors.java.Parser
* @param {Object} token Token to be parsed
* @param {Number} fmtArg Value to be formatted
* @return {Parser}
*/
Parser.prototype.parseHexInt = function(token, fmtArg) {
var val = null;
if (fmtArg >= 0) {
val = Math.floor(fmtArg).toString(16);
} else {
val = Number(__.fmt.util.number.twosComplement(-1 * fmtArg))
.toString(16);
}
//alternate form: with '0x' prefix
if (token.options.alternateForm) {
if (token.width) {
val = token.options.zeroPad ?
'0x' + __.fmt.util.padLeft(val, '0', token.width - 2) :
__.fmt.util.padLeft('0x' + val, ' ', token.width);
} else {
val = '0x' + val;
}
} else { //default form: naked value
if (token.width) { //space or zero pad
val = token.options.zeroPad ?
__.fmt.util.padLeft(val, '0', token.width) :
__.fmt.util.padLeft(val, ' ', token.width);
}
}
if (token.upperCase) { val = val.toUpperCase(); }
this._target.push(val);
return this;
};
/**
* Parses a format specifier of conversion type 'integral'.
* @chainable
* @method parseIntegral
* @for flavors.java.Parser
* @param {Object} token Token to be parsed
* @param {any} fmtArg Value to be formatted
* @param {Object} fmtOpt Format options
* @return {Parser}
*/
Parser.prototype.parseIntegral = function(token, fmtArg, fmtOpt) {
if (__.fmt.util.number.isSymbolicNumber(fmtArg)) {
this.parseSymbolicNumber(token, fmtArg);
} else {
switch (token.conversion) {
case 'd': //decimal notation
this.parseDecInt(token, Number(fmtArg), fmtOpt);
break;
case 'o': //octal notation
this.parseOctInt(token, Number(fmtArg));
break;
case 'x': //hex notation
this.parseHexInt(token, Number(fmtArg));
break;
}
}
return this;
};
/**
* Parses decimal floats.
* @chainable
* @method parseDecimalFloat
* @param {Object} token Token to be parsed
* @param {Number} fmtArg Value to be formatted
* @param {Object} fmtOpt Format options
* @return {Parser}
*/
Parser.prototype.parseDecimalFloat = function(token, fmtArg, fmtOpt) {
this._target.push(
Formatter.number.localize.localizeNumber(
__.fmt.number.toDecimal(fmtArg, {
precision: token.precision || token.precision === 0 ?
token.precision :
Formatter.number.DEFAULT_PRECISION,
considerZeroSign: true
}),
fmtOpt.culture(),
token.width,
token.options));
return this;
};
/**
* Parses computerized scientific floats.
* @chainable
* @method parseComputerizedScientificFloat
* @param {Object} token Token to be parsed
* @param {Number} fmtArg Value to be formatted
* @param {Object} fmtOpt Format options
* @return {Parser}
*/
Parser.prototype.parseScientificFloat = function(token, fmtArg, fmtOpt) {
//numbers in scientific notation do not have groups
token.options.localizedGroupingSeparator = false;
this._target.push(Formatter.number.localize.localizeNumber(
__.fmt.number.toScientific(fmtArg, {
precision: token.precision || token.precision === 0 ?
token.precision :
Formatter.number.DEFAULT_PRECISION,
expMinWidth: Formatter.number.DEFAULT_MIN_EXPONENT_WIDTH,
upperCase: token.upperCase,
considerZeroSign: true
}),
fmtOpt.culture(),
token.width,
token.options));
};
/**
* Parses computerized scientific floats.
* @chainable
* @method parseGeneralScientificFloat
* @param {Object} token Token to be parsed
* @param {Number} fmtArg Value to be formatted
* @param {Object} fmtOpt Format options
* @return {Parser}
*/
Parser.prototype.parseGeneralScientificFloat = function(token, fmtArg, fmtOpt) {
var r,
nIntDigits,
lowerBound,
upperBound,
roundPrec;
r = Math.abs(Number(fmtArg));
nIntDigits = r === 0 ?
1 :
Math.floor(Math.log(r) / Math.log(10)) + 1;
if (token.precision === 0) { token.precision = 1; }
if (!token.precision) {
token.precision = Formatter.number.DEFAULT_PRECISION;
}
lowerBound = Math.pow(10, -4);
upperBound = Math.pow(10, token.precision);
roundPrec = 0 < r && r < 1 ?
token.precision :
token.precision - nIntDigits;
if (roundPrec < 0) { roundPrec = 0; }
r = __.fmt.util.number.round(fmtArg, roundPrec);
if (r === 0 || (lowerBound <= Math.abs(r) && Math.abs(r) < upperBound)) {
token.precision = roundPrec;
this.parseDecimalFloat(token, r, fmtOpt);
} else {
token.precision--;
this.parseScientificFloat(token, r, fmtOpt);
}
return this;
};
/**
* Parses hexadecimal exponential floats.
* @chainable
* @method parseGeneralScientificFloat
* @param {Number} fmtArg Value to be formatted
* @return {Parser}
*/
Parser.prototype.parseHexExp = function(fmtArg) {
this._target.push(__.fmt.number.toHexExp(fmtArg));
return this;
};
/**
* Parses a format specifier of conversion type 'floatingPoint'.
* @chainable
* @method parseFloatingPoint
* @for flavors.java.Parser
* @param {Object} token Token to be parsed
* @param {any} fmtArg Value to be formatted
* @param {Object} fmtOpt Format options
* @return {Parser}
*/
Parser.prototype.parseFloatingPoint = function(token, fmtArg, fmtOpt) {
if (__.fmt.util.number.isSymbolicNumber(fmtArg)) {
this.parseSymbolicNumber(token, fmtArg);
} else {
switch (token.conversion) {
case 'f': //decimal format
this.parseDecimalFloat(token, Number(fmtArg), fmtOpt);
break;
case 'e': //computerized scientific notation
this.parseScientificFloat(token, Number(fmtArg), fmtOpt);
break;
case 'g': //general scientific notation
this.parseGeneralScientificFloat(token, Number(fmtArg), fmtOpt);
break;
case 'a': //hexadecimal exponential form
this.parseHexExp(token, Number(fmtArg), fmtOpt);
break;
}
}
return this;
};
/**
* Parses a format specifier of conversion type 'datetime'
* @chainable
* @method parseDatetime
* @for flavors.java.Parser
* @param {Object} token Token to be parsed
* @param {Date} fmtArg Value to be formatted
* @param {Object} fmtOpt Format options
* @return {Parser}
*/
Parser.prototype.parseDatetime = function(token, fmtArg, fmtOpt) {
var val = null;
if (!fmtArg || !(fmtArg instanceof Date)) {
err('Invalid argument. Date expected.');
}
//Should the token be formatted based on the time zone provided by the
//JavaScript interpreter's host OS?
if (token.options.alternateForm) {
fmtArg = new Date(fmtArg.valueOf() +
//tz offset from UTC in milliseconds
fmtArg.getTimezoneOffset() * -60000);
}
switch (token.conversion) {
//time
case 'tH':
val = __.fmt.date.hours(fmtArg, true, false);
break;
case 'tI':
val = __.fmt.date.hours(fmtArg, true, true);
break;
case 'tk':
val = __.fmt.date.hours(fmtArg, false, false);
break;
case 'tl':
val = __.fmt.date.hours(fmtArg, false, true);
break;
case 'tM':
val = __.fmt.date.minutes(fmtArg, true);
break;
case 'tS':
val = __.fmt.date.seconds(fmtArg, true);
break;
case 'tL':
val = __.fmt.date.milliseconds(fmtArg, true);
break;
case 'tp':
val = (token.upperCase ?
__.fmt.date.timeDesignator(fmtArg, fmtOpt.culture()).toUpperCase() :
__.fmt.date.timeDesignator(fmtArg, fmtOpt.culture()));
break;
case 'tz':
val = (token.options.alternateForm ?
__.fmt.date.timezoneOffset(fmtArg) :
'+0000');
break;
case 'tZ':
val = (token.options.alternateForm ?
__.fmt.date.abbreviatedTimezone(fmtArg) :
'GMT');
break;
case 'ts':
val = __.fmt.util.date.timestamp(fmtArg);
break;
case 'tQ':
val = fmtArg.valueOf();
break;
//date
case 'tB':
val = (token.upperCase ?
__.fmt.date.monthName(fmtArg, fmtOpt.culture()).toUpperCase() :
__.fmt.date.monthName(fmtArg, fmtOpt.culture()));
break;
case 'tb': //fall through
case 'th':
val = (token.upperCase ?
__.fmt.date.abbreviatedMonthName(fmtArg, fmtOpt.culture())
.toUpperCase() :
__.fmt.date.abbreviatedMonthName(fmtArg, fmtOpt.culture()));
break;
case 'tA':
val = (token.upperCase ?
__.fmt.date.weekdayName(fmtArg, fmtOpt.culture()).toUpperCase() :
__.fmt.date.weekdayName(fmtArg, fmtOpt.culture()));
break;
case 'ta':
val = (token.upperCase ?
__.fmt.date.abbreviatedWeekdayName(fmtArg, fmtOpt.culture())
.toUpperCase() :
__.fmt.date.abbreviatedWeekdayName(fmtArg, fmtOpt.culture()));
break;
case 'tC':
val = String(__.fmt.util.date.pastCenturies(fmtArg));
if (val.length === 1) {
val = __.fmt.util.padLeft(val, '0', 2);
}
break;
case 'tY':
val = __.fmt.date.year(fmtArg, {
maxDigits: 4,
leadingZeros: true
});
break;
case 'ty':
val = __.fmt.date.year(fmtArg, {
maxDigits: 2,
leadingZeros: true
});
break;
case 'tj':
val = __.fmt.date.dayOfYear(fmtArg, true);
break;
case 'tm':
val = __.fmt.date.month(fmtArg, true);
break;
case 'td':
val = __.fmt.date.dayOfMonth(fmtArg, true);
break;
case 'te':
val = __.fmt.date.dayOfMonth(fmtArg, false);
break;
case 'tV':
val = String(__.fmt.util.date.isoWeek(fmtArg));
break;
//compositions
case 'tR': //"%tH:%tM"
val = __.fmt.date.hours(fmtArg, true, false) + ':' +
__.fmt.date.minutes(fmtArg, true);
break;
case 'tT': //"%tH:%tM:%tS"
val = __.fmt.date.time(fmtArg, true);
break;
case 'tr': //"%tI:%tM:%tS %Tp"
val = __.fmt.date.hours(fmtArg, true, true) + ':' +
__.fmt.date.minutes(fmtArg, true) + ':' +
__.fmt.date.seconds(fmtArg, true) + ' ' +
__.fmt.date.timeDesignator(fmtArg, fmtOpt.culture()).toUpperCase();
break;
case 'tD': //"%tm/%td/%ty"
val = __.fmt.date.month(fmtArg, true) + '/' +
__.fmt.date.dayOfMonth(fmtArg, true) + '/' +
__.fmt.date.year(fmtArg, {
maxDigits: 2,
leadingZeros: true
});
break;
case 'tF': //"%tY-%tm-%td"
val = __.fmt.date.year(fmtArg, {
maxDigits: 4,
leadingZeros: true
}) + '-' +
__.fmt.date.month(fmtArg, true) + '-' +
__.fmt.date.dayOfMonth(fmtArg, true);
break;
case 'tc': //"%ta %tb %td %tT %tZ %tY"
val = [
__.fmt.date.abbreviatedWeekdayName(fmtArg, fmtOpt.culture()),
__.fmt.date.abbreviatedMonthName(fmtArg, fmtOpt.culture()),
__.fmt.date.dayOfMonth(fmtArg, true),
__.fmt.date.time(fmtArg, true),
(token.options.alternateForm ?
__.fmt.date.abbreviatedTimezone(fmtArg, fmtOpt.culture()) :
'GMT'),
__.fmt.date.year(fmtArg, {
maxDigits: 4,
leadingZeros: true
})
].join(' ');
break;
}
if (token.width) {
val = __.fmt.util.pad(val, ' ', token.width, !token.options.leftJustify);
}
this._target.push(val);
return this;
};
/**
* Applies the given token to the given format argument.
* @chainable
* @method parseToken
* @for flavors.java.Parser
* @param {Object} token Token to be parsed
* @param {any} [fmtArg] Value to be formatted
* @param {Object} [fmtOpt] Format options
* @return {Parser}
*/
Parser.prototype.parseToken = function(token, fmtArg, fmtOpt) {
switch (token.category) {
case Scanner.tokenCategories.general:
this.parseGeneral(token, fmtArg);
break;
case Scanner.tokenCategories.character:
this.parseCharacter(token, fmtArg);
break;
case Scanner.tokenCategories.integral:
this.parseIntegral(token, fmtArg, fmtOpt);
break;
case Scanner.tokenCategories.floatingPoint:
this.parseFloatingPoint(token, fmtArg, fmtOpt);
break;
case Scanner.tokenCategories.datetime:
this.parseDatetime(token, fmtArg, fmtOpt);
break;
case Scanner.tokenCategories.percent:
this._target.push('%');
break;
case Scanner.tokenCategories.lineSeparator:
this._target.push(fmtOpt.lineSeparator());
break;
case Scanner.tokenCategories.text:
this._target.push(token.value);
break;
}
return this;
};
/**
* Parses the given input.
* @method parse
* @for flavors.java.Parser
* @param {String|Array} input String or character array to be parsed
* @param {Array} [fmtArgs] Data to be formatted
* @param {Object} [fmtOpt] Format options
* @return {String} Formatted data
*/
Parser.prototype.parse = function(input, fmtArgs, fmtOpt) {
var t = null; //token
this._target = [];
this._scanner.input(input);
while (this._scanner.hasNext()) {
t = this._scanner.next();
if (t.argumentIndex >= 0 &&
t.argumentIndex > fmtArgs.length) {
inputErr(input, t.startIndex, 'Invalid argument index');
}
try {
this.parseToken(t,
t.argumentIndex >= 0 ? fmtArgs[t.argumentIndex] : undefined,
fmtOpt);
} catch (e) {
inputErr(input, t.startIndex, e.message);
}
}
return this._target.join('');
};
/**
* Formats the given arguments described by the given formatstring.
* @method format
* @for flavors.java
* @param {String} fstr Format string
* @param {Array} [args] Data to be formatted
* @return {String} Formatted data
*/
format = function(fstr, formatter, args, options) {
if (!fstr || !formatter) { return undefined; }
if (!parser) { parser = new Parser(); }
__.fmt = formatter;
fstr = __.fmt.util.toArray(fstr);
return parser.parse(fstr, args, options);
};
mod = {
id: 'java',
Formatter: Formatter,
Scanner: Scanner,
Parser: Parser,
format: format,
_private: __
};
/* Node.js */
if (typeof module !== 'undefined') {
module.exports = mod;
}
/* AMD */
else if (typeof define !== 'undefined' && define.amd) { //AMD
define(mod);
}
/* Browser */
else if (root) {
if (!root.pat) { root.pat = {}; }
if (!root.pat._private) { root.pat._private = {}; }
if (!root.pat._private.flavors) {
root.pat._private.flavors = {};
root.pat._private.preferredFlavorId = mod.id;
}
root.pat._private.flavors.java = mod;
}
}(this));