/** * @license Apache-2.0 * * Copyright (c) 2022 The Stdlib Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; // MODULES // var formatInteger = require( './format_integer.js' ); var isString = require( './is_string.js' ); var formatDouble = require( './format_double.js' ); var spacePad = require( './space_pad.js' ); var zeroPad = require( './zero_pad.js' ); // VARIABLES // var fromCharCode = String.fromCharCode; var isArray = Array.isArray; // NOTE: We use the global `Array.isArray` function here instead of `@stdlib/assert/is-array` to avoid circular dependencies. // FUNCTIONS // /** * Returns a boolean indicating whether a value is `NaN`. * * @private * @param {*} value - input value * @returns {boolean} boolean indicating whether a value is `NaN` * * @example * var bool = isnan( NaN ); * // returns true * * @example * var bool = isnan( 4 ); * // returns false */ function isnan( value ) { // explicitly define a function here instead of `@stdlib/math/base/assert/is-nan` in order to avoid circular dependencies return ( value !== value ); } /** * Initializes token object with properties of supplied format identifier object or default values if not present. * * @private * @param {Object} token - format identifier object * @returns {Object} token object */ function initialize( token ) { var out = {}; out.specifier = token.specifier; out.precision = ( token.precision === void 0 ) ? 1 : token.precision; out.width = token.width; out.flags = token.flags || ''; out.mapping = token.mapping; return out; } // MAIN // /** * Generates string from a token array by interpolating values. * * @param {Array} tokens - string parts and format identifier objects * @param {Array} ...args - variable values * @throws {TypeError} first argument must be an array * @throws {Error} invalid flags * @returns {string} formatted string * * @example * var tokens = [ 'beep ', { 'specifier': 's' } ]; * var out = formatInterpolate( tokens, 'boop' ); * // returns 'beep boop' */ function formatInterpolate( tokens ) { var hasPeriod; var flags; var token; var flag; var num; var out; var pos; var i; var j; if ( !isArray( tokens ) ) { throw new TypeError( 'invalid argument. First argument must be an array. Value: `' + tokens + '`.' ); } out = ''; pos = 1; for ( i = 0; i < tokens.length; i++ ) { token = tokens[ i ]; if ( isString( token ) ) { out += token; } else { hasPeriod = token.precision !== void 0; token = initialize( token ); if ( !token.specifier ) { throw new TypeError( 'invalid argument. Token is missing `specifier` property. Index: `'+ i +'`. Value: `' + token + '`.' ); } if ( token.mapping ) { pos = token.mapping; } flags = token.flags; for ( j = 0; j < flags.length; j++ ) { flag = flags.charAt( j ); switch ( flag ) { case ' ': token.sign = ' '; break; case '+': token.sign = '+'; break; case '-': token.padRight = true; token.padZeros = false; break; case '0': token.padZeros = flags.indexOf( '-' ) < 0; // NOTE: We use built-in `Array.prototype.indexOf` here instead of `@stdlib/assert/contains` in order to avoid circular dependencies. break; case '#': token.alternate = true; break; default: throw new Error( 'invalid flag: ' + flag ); } } if ( token.width === '*' ) { token.width = parseInt( arguments[ pos ], 10 ); pos += 1; if ( isnan( token.width ) ) { throw new TypeError( 'the argument for * width at position ' + pos + ' is not a number. Value: `' + token.width + '`.' ); } if ( token.width < 0 ) { token.padRight = true; token.width = -token.width; } } if ( hasPeriod ) { if ( token.precision === '*' ) { token.precision = parseInt( arguments[ pos ], 10 ); pos += 1; if ( isnan( token.precision ) ) { throw new TypeError( 'the argument for * precision at position ' + pos + ' is not a number. Value: `' + token.precision + '`.' ); } if ( token.precision < 0 ) { token.precision = 1; hasPeriod = false; } } } token.arg = arguments[ pos ]; switch ( token.specifier ) { case 'b': case 'o': case 'x': case 'X': case 'd': case 'i': case 'u': // Case: %b (binary), %o (octal), %x, %X (hexadecimal), %d, %i (decimal), %u (unsigned decimal) if ( hasPeriod ) { token.padZeros = false; } token.arg = formatInteger( token ); break; case 's': // Case: %s (string) token.maxWidth = ( hasPeriod ) ? token.precision : -1; token.arg = String( token.arg ); break; case 'c': // Case: %c (character) if ( !isnan( token.arg ) ) { num = parseInt( token.arg, 10 ); if ( num < 0 || num > 127 ) { throw new Error( 'invalid character code. Value: ' + token.arg ); } token.arg = ( isnan( num ) ) ? String( token.arg ) : fromCharCode( num ); // eslint-disable-line max-len } break; case 'e': case 'E': case 'f': case 'F': case 'g': case 'G': // Case: %e, %E (scientific notation), %f, %F (decimal floating point), %g, %G (uses the shorter of %e/E or %f/F) if ( !hasPeriod ) { token.precision = 6; } token.arg = formatDouble( token ); break; default: throw new Error( 'invalid specifier: ' + token.specifier ); } // Fit argument into field width... if ( token.maxWidth >= 0 && token.arg.length > token.maxWidth ) { token.arg = token.arg.substring( 0, token.maxWidth ); } if ( token.padZeros ) { token.arg = zeroPad( token.arg, token.width || token.precision, token.padRight ); // eslint-disable-line max-len } else if ( token.width ) { token.arg = spacePad( token.arg, token.width, token.padRight ); } out += token.arg || ''; pos += 1; } } return out; } // EXPORTS // module.exports = formatInterpolate;