voca
Version:
The ultimate JavaScript string library
680 lines (595 loc) • 21.9 kB
JavaScript
;
var is_nil = require('./internal/is_nil.js');
require('./is_string.js');
var coerce_to_string = require('./internal/coerce_to_string.js');
var _const = require('./internal/const.js');
var nil_default = require('./internal/nil_default.js');
var to_string = require('./internal/to_string.js');
var coerce_to_number = require('./internal/coerce_to_number.js');
require('./internal/to_integer.js');
var truncate = require('./truncate.js');
require('./repeat.js');
require('./internal/build_padding.js');
var pad_left = require('./pad_left.js');
var pad_right = require('./pad_right.js');
/**
* The current index.
*
* @ignore
* @name ReplacementIndex#index
* @type {number}
* @return {ReplacementIndex} ReplacementIndex instance.
*/
function ReplacementIndex() {
this.index = 0;
}
/**
* Increment the current index.
*
* @ignore
* @return {undefined}
*/
ReplacementIndex.prototype.increment = function () {
this.index++;
};
/**
* Increment the current index by position.
*
* @ignore
* @param {number} [position] The replacement position.
* @return {undefined}
*/
ReplacementIndex.prototype.incrementOnEmptyPosition = function (position) {
if (is_nil.isNil(position)) {
this.increment();
}
};
/**
* Get the replacement index by position.
*
* @ignore
* @param {number} [position] The replacement position.
* @return {number} The replacement index.
*/
ReplacementIndex.prototype.getIndexByPosition = function (position) {
return is_nil.isNil(position) ? this.index : position - 1;
};
// Type specifiers
var TYPE_INTEGER = 'i';
var TYPE_INTEGER_BINARY = 'b';
var TYPE_INTEGER_ASCII_CHARACTER = 'c';
var TYPE_INTEGER_DECIMAL = 'd';
var TYPE_INTEGER_OCTAL = 'o';
var TYPE_INTEGER_UNSIGNED_DECIMAL = 'u';
var TYPE_INTEGER_HEXADECIMAL = 'x';
var TYPE_INTEGER_HEXADECIMAL_UPPERCASE = 'X';
var TYPE_FLOAT_SCIENTIFIC = 'e';
var TYPE_FLOAT_SCIENTIFIC_UPPERCASE = 'E';
var TYPE_FLOAT = 'f';
var TYPE_FLOAT_SHORT = 'g';
var TYPE_FLOAT_SHORT_UPPERCASE = 'G';
var TYPE_STRING = 's'; // Simple literals
var LITERAL_SINGLE_QUOTE = "'";
var LITERAL_PLUS = '+';
var LITERAL_MINUS = '-';
var LITERAL_PERCENT_SPECIFIER = '%%'; // Radix constants to format numbers
var RADIX_BINARY = 2;
var RADIX_OCTAL = 8;
var RADIX_HEXADECIMAL = 16;
/**
* Aligns and pads `subject` string.
*
* @ignore
* @param {string} subject The subject string.
* @param {ConversionSpecification} conversion The conversion specification object.
* @return {string} Returns the aligned and padded string.
*/
function alignAndPad(subject, conversion) {
var width = conversion.width;
if (is_nil.isNil(width) || subject.length >= width) {
return subject;
}
var padType = conversion.alignmentSpecifier === LITERAL_MINUS ? pad_right : pad_left;
return padType(subject, width, conversion.getPaddingCharacter());
}
/**
* Add sign to the formatted number.
*
* @ignore
* @name addSignToFormattedNumber
* @param {number} replacementNumber The number to be replaced.
* @param {string} formattedReplacement The formatted version of number.
* @param {ConversionSpecification} conversion The conversion specification object.
* @return {string} Returns the formatted number string with a sign.
*/
function addSignToFormattedNumber(replacementNumber, formattedReplacement, conversion) {
if (conversion.signSpecifier === LITERAL_PLUS && replacementNumber >= 0) {
formattedReplacement = LITERAL_PLUS + formattedReplacement;
}
return formattedReplacement;
}
/**
* Formats a float type according to specifiers.
*
* @ignore
* @param {string} replacement The string to be formatted.
* @param {ConversionSpecification} conversion The conversion specification object.
* @return {string} Returns the formatted string.
*/
function float(replacement, conversion) {
var replacementNumber = parseFloat(replacement);
var formattedReplacement;
if (isNaN(replacementNumber)) {
replacementNumber = 0;
}
var precision = coerce_to_number.coerceToNumber(conversion.precision, 6);
switch (conversion.typeSpecifier) {
case TYPE_FLOAT:
formattedReplacement = replacementNumber.toFixed(precision);
break;
case TYPE_FLOAT_SCIENTIFIC:
formattedReplacement = replacementNumber.toExponential(precision);
break;
case TYPE_FLOAT_SCIENTIFIC_UPPERCASE:
formattedReplacement = replacementNumber.toExponential(precision).toUpperCase();
break;
case TYPE_FLOAT_SHORT:
case TYPE_FLOAT_SHORT_UPPERCASE:
formattedReplacement = formatFloatAsShort(replacementNumber, precision, conversion);
break;
}
formattedReplacement = addSignToFormattedNumber(replacementNumber, formattedReplacement, conversion);
return coerce_to_string.coerceToString(formattedReplacement);
}
/**
* Formats the short float.
*
* @ignore
* @param {number} replacementNumber The number to format.
* @param {number} precision The precision to format the float.
* @param {ConversionSpecification} conversion The conversion specification object.
* @return {string} Returns the formatted short float.
*/
function formatFloatAsShort(replacementNumber, precision, conversion) {
if (replacementNumber === 0) {
return '0';
}
var nonZeroPrecision = precision === 0 ? 1 : precision;
var formattedReplacement = replacementNumber.toPrecision(nonZeroPrecision).replace(_const.REGEXP_TRAILING_ZEROS, '');
if (conversion.typeSpecifier === TYPE_FLOAT_SHORT_UPPERCASE) {
formattedReplacement = formattedReplacement.toUpperCase();
}
return formattedReplacement;
}
/**
* Formats an integer type according to specifiers.
*
* @ignore
* @param {string} replacement The string to be formatted.
* @param {ConversionSpecification} conversion The conversion specification object.
* @return {string} Returns the formatted string.
*/
function integerBase(replacement, conversion) {
var integer = parseInt(replacement);
if (isNaN(integer)) {
integer = 0;
}
integer = integer >>> 0;
switch (conversion.typeSpecifier) {
case TYPE_INTEGER_ASCII_CHARACTER:
integer = String.fromCharCode(integer);
break;
case TYPE_INTEGER_BINARY:
integer = integer.toString(RADIX_BINARY);
break;
case TYPE_INTEGER_OCTAL:
integer = integer.toString(RADIX_OCTAL);
break;
case TYPE_INTEGER_HEXADECIMAL:
integer = integer.toString(RADIX_HEXADECIMAL);
break;
case TYPE_INTEGER_HEXADECIMAL_UPPERCASE:
integer = integer.toString(RADIX_HEXADECIMAL).toUpperCase();
break;
}
return coerce_to_string.coerceToString(integer);
}
/**
* Formats a decimal integer type according to specifiers.
*
* @ignore
* @param {string} replacement The string to be formatted.
* @param {ConversionSpecification} conversion The conversion specification object.
* @return {string} Returns the formatted string.
*/
function integerDecimal(replacement, conversion) {
var integer = parseInt(replacement);
if (isNaN(integer)) {
integer = 0;
}
return addSignToFormattedNumber(integer, to_string.toString(integer), conversion);
}
/**
* Formats a string type according to specifiers.
*
* @ignore
* @param {string} replacement The string to be formatted.
* @param {ConversionSpecification} conversion The conversion specification object.
* @return {string} Returns the formatted string.
*/
function stringFormat(replacement, conversion) {
var formattedReplacement = replacement;
var precision = conversion.precision;
if (!is_nil.isNil(precision) && formattedReplacement.length > precision) {
formattedReplacement = truncate(formattedReplacement, precision, '');
}
return formattedReplacement;
}
/**
* Returns the computed string based on format specifiers.
*
* @ignore
* @name computeReplacement
* @param {string} replacement The replacement value.
* @param {ConversionSpecification} conversion The conversion specification object.
* @return {string} Returns the computed string.
*/
function compute(replacement, conversion) {
var formatFunction;
switch (conversion.typeSpecifier) {
case TYPE_STRING:
formatFunction = stringFormat;
break;
case TYPE_INTEGER_DECIMAL:
case TYPE_INTEGER:
formatFunction = integerDecimal;
break;
case TYPE_INTEGER_ASCII_CHARACTER:
case TYPE_INTEGER_BINARY:
case TYPE_INTEGER_OCTAL:
case TYPE_INTEGER_HEXADECIMAL:
case TYPE_INTEGER_HEXADECIMAL_UPPERCASE:
case TYPE_INTEGER_UNSIGNED_DECIMAL:
formatFunction = integerBase;
break;
case TYPE_FLOAT:
case TYPE_FLOAT_SCIENTIFIC:
case TYPE_FLOAT_SCIENTIFIC_UPPERCASE:
case TYPE_FLOAT_SHORT:
case TYPE_FLOAT_SHORT_UPPERCASE:
formatFunction = float;
break;
}
var formattedString = formatFunction(replacement, conversion);
return alignAndPad(formattedString, conversion);
}
/**
* Construct the new conversion specification object.
*
* @ignore
* @param {Object} properties An object with properties to initialize.
* @return {ConversionSpecification} ConversionSpecification instance.
*/
function ConversionSpecification(properties) {
/**
* The percent characters from conversion specification.
*
* @ignore
* @name ConversionSpecification#percent
* @type {string}
*/
this.percent = properties.percent;
/**
* The sign specifier to force a sign to be used on a number.
*
* @ignore
* @name ConversionSpecification#signSpecifier
* @type {string}
*/
this.signSpecifier = properties.signSpecifier;
/**
* The padding specifier that says what padding character will be used.
*
* @ignore
* @name ConversionSpecification#paddingSpecifier
* @type {string}
*/
this.paddingSpecifier = properties.paddingSpecifier;
/**
* The alignment specifier that says if the result should be left-justified or right-justified.
*
* @ignore
* @name ConversionSpecification#alignmentSpecifier
* @type {string}
*/
this.alignmentSpecifier = properties.alignmentSpecifier;
/**
* The width specifier how many characters this conversion should result in.
*
* @ignore
* @name ConversionSpecification#width
* @type {number}
*/
this.width = properties.width;
/**
* The precision specifier says how many decimal digits should be displayed for floating-point numbers.
*
* @ignore
* @name ConversionSpecification#precision
* @type {number}
*/
this.precision = properties.precision;
/**
* The type specifier says what type the argument data should be treated as.
*
* @ignore
* @name ConversionSpecification#typeSpecifier
* @type {string}
*/
this.typeSpecifier = properties.typeSpecifier;
}
/**
* Check if the conversion specification is a percent literal "%%".
*
* @ignore
* @return {boolean} Returns true if the conversion is a percent literal, false otherwise.
*/
ConversionSpecification.prototype.isPercentLiteral = function () {
return LITERAL_PERCENT_SPECIFIER === this.percent;
};
/**
* Get the padding character from padding specifier.
*
* @ignore
* @returns {string} Returns the padding character.
*/
ConversionSpecification.prototype.getPaddingCharacter = function () {
var paddingCharacter = nil_default.nilDefault(this.paddingSpecifier, ' ');
if (paddingCharacter.length === 2 && paddingCharacter[0] === LITERAL_SINGLE_QUOTE) {
paddingCharacter = paddingCharacter[1];
}
return paddingCharacter;
};
/**
* Validates the specifier type and replacement position.
*
* @ignore
* @throws {Error} Throws an exception on insufficient arguments or unknown specifier.
* @param {number} index The index of the matched specifier.
* @param {number} replacementsLength The number of replacements.
* @param {ConversionSpecification} conversion The conversion specification object.
* @return {undefined}
*/
function validate(index, replacementsLength, conversion) {
if (is_nil.isNil(conversion.typeSpecifier)) {
throw new Error('sprintf(): Unknown type specifier');
}
if (index > replacementsLength - 1) {
throw new Error('sprintf(): Too few arguments');
}
if (index < 0) {
throw new Error('sprintf(): Argument number must be greater than zero');
}
}
/**
* Return the replacement for regular expression match of the conversion specification.
*
* @ignore
* @name matchReplacement
* @param {ReplacementIndex} replacementIndex The replacement index object.
* @param {string[]} replacements The array of replacements.
* @param {string} conversionSpecification The conversion specification.
* @param {string} percent The percent characters from conversion specification.
* @param {string} position The position to insert the replacement.
* @param {string} signSpecifier The sign specifier to force a sign to be used on a number.
* @param {string} paddingSpecifier The padding specifier that says what padding character will be used.
* @param {string} alignmentSpecifier The alignment specifier that says if the result should be left-justified or right-justified.
* @param {string} widthSpecifier The width specifier how many characters this conversion should result in.
* @param {string} precisionSpecifier The precision specifier says how many decimal digits should be displayed for floating-point numbers.
* @param {string} typeSpecifier The type specifier says what type the argument data should be treated as.
* @return {string} Returns the computed replacement.
*/
function match(replacementIndex, replacements, conversionSpecification, percent, position, signSpecifier, paddingSpecifier, alignmentSpecifier, widthSpecifier, precisionSpecifier, typeSpecifier) {
var conversion = new ConversionSpecification({
percent: percent,
signSpecifier: signSpecifier,
paddingSpecifier: paddingSpecifier,
alignmentSpecifier: alignmentSpecifier,
width: coerce_to_number.coerceToNumber(widthSpecifier, null),
precision: coerce_to_number.coerceToNumber(precisionSpecifier, null),
typeSpecifier: typeSpecifier
});
if (conversion.isPercentLiteral()) {
return conversionSpecification.slice(1);
}
var actualReplacementIndex = replacementIndex.getIndexByPosition(position);
replacementIndex.incrementOnEmptyPosition(position);
validate(actualReplacementIndex, replacements.length, conversion);
return compute(replacements[actualReplacementIndex], conversion);
}
/**
* Produces a string according to `format`.
*
* <div id="sprintf-format" class="smaller">
* `format` string is composed of zero or more directives: ordinary characters (not <code>%</code>), which are copied unchanged
* to the output string and <i>conversion specifications</i>, each of which results in fetching zero or more subsequent
* arguments. <br/> <br/>
*
* Each <b>conversion specification</b> is introduced by the character <code>%</code>, and ends with a <b>conversion
* specifier</b>. In between there may be (in this order) zero or more <b>flags</b>, an optional <b>minimum field width</b>
* and an optional <b>precision</b>.<br/>
* The syntax is: <b>ConversionSpecification</b> = <b>"%"</b> { <b>Flags</b> }
* [ <b>MinimumFieldWidth</b> ] [ <b>Precision</b> ] <b>ConversionSpecifier</b>, where curly braces { } denote repetition
* and square brackets [ ] optionality. <br/><br/>
*
* By default, the arguments are used in the given order.<br/>
* For argument numbering and swapping, `%m$` (where `m` is a number indicating the argument order)
* is used instead of `%` to specify explicitly which argument is taken. For instance `%1$s` fetches the 1st argument,
* `%2$s` the 2nd and so on, no matter what position the conversion specification has in `format`.
* <br/><br/>
*
* <b>The flags</b><br/>
* The character <code>%</code> is followed by zero or more of the following flags:<br/>
* <table class="light-params">
* <tr>
* <td><code>+</code></td>
* <td>
* A sign (<code>+</code> or <code>-</code>) should always be placed before a number produced by a
* signed conversion. By default a sign is used only for negative numbers.
* </td>
* </tr>
* <tr>
* <td><code>0</code></td>
* <td>The value should be zero padded.</td>
* </tr>
* <tr>
* <td><code>␣</code></td>
* <td>(a space) The value should be space padded.</td>
* </tr>
* <tr>
* <td><code>'</code></td>
* <td>Indicates alternate padding character, specified by prefixing it with a single quote <code>'</code>.</td>
* </tr>
* <tr>
* <td><code>-</code></td>
* <td>The converted value is to be left adjusted on the field boundary (the default is right justification).</td>
* </tr>
* </table>
*
* <b>The minimum field width</b><br/>
* An optional decimal digit string (with nonzero first digit) specifying a minimum field width. If the converted
* value has fewer characters than the field width, it will be padded with spaces on the left (or right, if the
* left-adjustment flag has been given).<br/><br/>
*
* <b>The precision</b><br/>
* An optional precision, in the form of a period `.` followed by an optional decimal digit string.<br/>
* This gives the number of digits to appear after the radix character for `e`, `E`, `f` and `F` conversions, the
* maximum number of significant digits for `g` and `G` conversions or the maximum number of characters to be printed
* from a string for `s` conversion.<br/><br/>
*
* <b>The conversion specifier</b><br/>
* A specifier that mentions what type the argument should be treated as:
*
* <table class="light-params">
* <tr>
* <td>`s`</td>
* <td>The string argument is treated as and presented as a string.</td>
* </tr>
* <tr>
* <td>`d` `i`</td>
* <td>The integer argument is converted to signed decimal notation.</td>
* </tr>
* <tr>
* <td>`b`</td>
* <td>The unsigned integer argument is converted to unsigned binary.</td>
* </tr>
* <tr>
* <td>`c`</td>
* <td>The unsigned integer argument is converted to an ASCII character with that number.</td>
* </tr>
* <tr>
* <td>`o`</td>
* <td>The unsigned integer argument is converted to unsigned octal.</td>
* </tr>
* <tr>
* <td>`u`</td>
* <td>The unsigned integer argument is converted to unsigned decimal.</td>
* </tr>
* <tr>
* <td>`x` `X`</td>
* <td>The unsigned integer argument is converted to unsigned hexadecimal. The letters `abcdef` are used for `x`
* conversions; the letters `ABCDEF` are used for `X` conversions.</td>
* </tr>
* <tr>
* <td>`f`</td>
* <td>
* The float argument is rounded and converted to decimal notation in the style `[-]ddd.ddd`, where the number of
* digits after the decimal-point character is equal to the precision specification. If the precision is missing,
* it is taken as 6; if the precision is explicitly zero, no decimal-point character appears.
* If a decimal point appears, at least one digit appears before it.
* </td>
* </tr>
* <tr>
* <td>`e` `E`</td>
* <td>
* The float argument is rounded and converted in the style `[-]d.ddde±dd`, where there is one digit
* before the decimal-point character and the number of digits after it is equal to the precision. If
* the precision is missing, it is taken as `6`; if the precision is zero, no decimal-point character
* appears. An `E` conversion uses the letter `E` (rather than `e`) to introduce the exponent.
* </td>
* </tr>
* <tr>
* <td>`g` `G`</td>
* <td>
* The float argument is converted in style `f` or `e` (or `F` or `E` for `G` conversions). The precision specifies
* the number of significant digits. If the precision is missing, `6` digits are given; if the
* precision is zero, it is treated as `1`. Style `e` is used if the exponent from its conversion is less
* than `-6` or greater than or equal to the precision. Trailing zeros are removed from the fractional
* part of the result; a decimal point appears only if it is followed by at least one digit.
* </td>
* </tr>
* <tr>
* <td>`%`</td>
* <td>A literal `%` is written. No argument is converted. The complete conversion specification is `%%`.</td>
* </tr>
*
* </table>
* </div>
*
* @function sprintf
* @static
* @since 1.0.0
* @memberOf Format
* @param {string} [format=''] The format string.
* @param {...*} replacements The replacements to produce the string.
* @return {string} Returns the produced string.
* @example
* v.sprintf('%s, %s!', 'Hello', 'World');
* // => 'Hello World!'
*
* v.sprintf('%s costs $%d', 'coffee', 2);
* // => 'coffee costs $2'
*
* v.sprintf('%1$s %2$s %1$s %2$s, watcha gonna %3$s', 'bad', 'boys', 'do')
* // => 'bad boys bad boys, watcha gonna do'
*
* v.sprintf('% 6s', 'bird');
* // => ' bird'
*
* v.sprintf('% -6s', 'crab');
* // => 'crab '
*
* v.sprintf("%'*5s", 'cat');
* // => '**cat'
*
* v.sprintf("%'*-6s", 'duck');
* // => 'duck**'
*
* v.sprintf('%d %i %+d', 15, -2, 25);
* // => '15 -2 +25'
*
* v.sprintf("%06d", 15);
* // => '000015'
*
* v.sprintf('0b%b 0o%o 0x%X', 12, 9, 155);
* // => '0b1100 0o11 0x9B'
*
* v.sprintf('%.2f', 10.469);
* // => '10.47'
*
* v.sprintf('%.2e %g', 100.5, 0.455);
* // => '1.01e+2 0.455'
*
*/
function sprintf(format) {
var formatString = coerce_to_string.coerceToString(format);
if (formatString === '') {
return formatString;
}
for (var _len = arguments.length, replacements = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
replacements[_key - 1] = arguments[_key];
}
var boundReplacementMatch = match.bind(undefined, new ReplacementIndex(), replacements);
return formatString.replace(_const.REGEXP_CONVERSION_SPECIFICATION, boundReplacementMatch);
}
module.exports = sprintf;