UNPKG

voca

Version:

The ultimate JavaScript string library

680 lines (595 loc) 21.9 kB
'use strict'; 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>&blank;</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;