UNPKG

pat

Version:

Formats data described by format strings

1,395 lines (1,379 loc) 46.4 kB
/** * 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));