sprintf.js

/**
 * @license Apache-2.0
 */


/**
 * @typedef {string}	FormatKeyword
 * The keyword for formating. 
 * These are the keyword supported:
 * 
 * <dl>
 * 	<dt>%</dt>	<dd>Returns the string <code>%</code>.</dd>
 * 	<dt>b</dt>	<dd>Convert the number to binery number.</dd>
 * 	<dt>o</dt>	<dd>Convert the number to octal numeral. </dd>
 * 	<dt>d</dt>	<dd>Convert the number to decimal integer.</dd>
 * 	<dt>x</dt>	<dd>Convert the number to hexadecimal number (lower case).</dd>
 * 	<dt>X</dt>	<dd>Convert the number to hexadecimal number (upper case).</dd>
 * 	<dt>e</dt>	<dd>Expressing the number by scientific notation (lower case).</dd>
 * 	<dt>E</dt>	<dd>Expressing the number by scientific notation (upper case).</dd>
 * 	<dt>f</dt>	<dd>Convert the number to a float number.</dd>
 * 	<dt>F</dt>	<dd>Convert the number to a float number.</dd>
 * 	<dt>g</dt>	<dd>Convert the number. If it is a large exponents, it is same as <code>%e</code>. Otherwise <code>%f</code></dd>
 * 	<dt>G</dt>	<dd>Convert the number. If it is a large exponents, it is same as <code>%E</code>. Otherwise <code>%f</code></dd>
 * 	<dt>T</dt>	<dd>Return the type of the argument. If the argument is an object, it will return the name of its constructor.</dd>
 * 	<dt>U</dt>	<dd>Expressing the number by unicode format (upper case). E.g. <code>U+1AF0</code>.</dd>
 * 	<dt>c</dt>	<dd>Convert the char code (number) to the char.</dd>
 * 	<dt>p</dt>	<dd></dd>
 * 	<dt>q</dt>	<dd>If the argument is a number, convert it to the char with single quote.</dd>
 *  			<dd>If the argument is a string, return it with double quote.</dd>
 * 	<dt>s</dt>	<dd>Return the string</dd>
 * 	<dt>t</dt>	<dd>Convert the argument as a boolean.</dd>
 * 	<dt>v</dt>	<dd>Return the value in its default format.</dd>
 *  			<dd>If the format string is with hash (<code>%#v</code>), it will be formated as a json.</dd>
 * </dl>
 */

/**
 *  To format the string using the specified format string and arguments.
 *  
 * @param	{string}	formatedString A format string
 * @param	{...*}  	args           Arguments referenced by the format specifiers in the format string. 
 * @returns	{string}	The string after formated
 */
function sprintf(formatedString) {
	"use strict";
	/** 
	 * Get the formated error message for formating issue.
	 * @param   {string} [message=""]     the error message
	 * @param   {FormatKeyword} [format=""]            
	 * @param   {string} extraInformation 
	 * @returns {string} Error message
	 */
	function errorMessage(message, format, extraInformation){
		message = message || ""
		format = format || ""
		if(extraInformation){
			if(typeof extraInformation == "object"){
				extraInformation = ": " + extraInformation.constructor.name
			}else{
				extraInformation = ": " + extraInformation
			}
			
		}else{
			extraInformation = ""
		}
		return "%!" + format + "(" + message + extraInformation + ")"
	}
	
	var replaceReg = /((%([\[\]\d*.+\-# ]+|)[vT%tbcdoqxXUeEfFgGsptg])|%%|%)/g,
	    formatKeyWordsReg = /[vT%tbcdoqxXUeEfFgGsq]/
	
	// 浮點數的默認精確度
	var DEFAULT_PRECISION = 6
	
	/** format 參數 */
	var args = Array.prototype.slice.call(arguments, 1)
	args.splice(0, 0, null)
	/** 檢索中的參數的 index */
	var argIndex = 0
	
	var hasBracket = formatedString.search(/[\[\]]/g) >= 0
	
	formatedString = formatedString.replace(replaceReg, function(format, formatIndex, formatedStr){
		if(format == "%"){
			return errorMessage("NOVERB")
		}
		
		/** @type FormatKeyword */
		var formatKeyWord = format.charAt(format.length - 1)	
		
		if(!formatKeyWord.match(formatKeyWordsReg)){
			return errorMessage("WRONG FORMAT", formatKeyWord)
		}
		
		if(formatKeyWord == "%"){
			return "%"
		}
		
		var width = format.substr(1, format.length - 2)	
		
		var fmt = {
			minus: false,
			plus: false,
			sharp: false,
			space: false,
			zero: false
		}
		
		// get flags
		var flagString = ""
		if(width.search(/[\[\]1-9.]/g) >= 0){
			flagString = width.substr(0, width.search(/[\[\]1-9.]/g))
		}else{
			flagString = width
		}
		(flagString.match(/[\+\-# 0]/g) || []).forEach(function(flag){
			switch(flag){
				case "+":
					fmt.plus = true
					break
				case "-":
					fmt.minus = true
					break
				case "#":
					fmt.sharp = true
					break
				case " ":
					fmt.space = true
					break
				case "0":
					fmt.zero = true
					break
			}
		})
		width = width.replace(flagString, "")
		
		// searching for formats like %[2]*.4d
		while(width.indexOf("*") >= 0){
			var starIndex = width.indexOf("*"),
			    leftBracketIndex = width.indexOf("["),
			    rightBracketIndex = width.indexOf("]")
			
			if(leftBracketIndex > starIndex || rightBracketIndex > starIndex || rightBracketIndex + 1 != starIndex){
				return errorMessage("MISSING INDEX", formatKeyWord)
			}
			
			var explicitArgIndex = width.substr(leftBracketIndex + 1, rightBracketIndex - leftBracketIndex - 1)
			if(isNaN(Number(explicitArgIndex)) || Number(explicitArgIndex) < 1 || Number(explicitArgIndex) != parseInt(explicitArgIndex)){
				return errorMessage("BAD WIDTH", formatKeyWord)
			}
			
			if(explicitArgIndex >= args.length){
				return errorMessage("BAD PRECISION", formatKeyWord)
			}
			
			argIndex = explicitArgIndex
			
			if(isNaN(Number(args[argIndex])) || Number(args[argIndex]) < 1 || Number(args[argIndex]) != parseInt(args[argIndex])){
				return errorMessage("BAD PRECISION", formatKeyWord, args[argIndex])
			}
			
			// replace the width
			// e.g. width is "[2]*.[5]*[3]", and arguments are [11,22,33,44,55,66] 
			//      after first replacing, with will become "22.[5]*[3]"
			width = width.replace(
				width.substring(leftBracketIndex, starIndex + 1),
				args[argIndex]
			)
		}
		
		// get the argument
		if(width.match(/[\[\]]/g) && width.match(/[\[\]]/g).length == 2){
			var leftBracketIndex = width.indexOf("["),
			    rightBracketIndex = width.indexOf("]")
			
			if(leftBracketIndex > rightBracketIndex){
				return errorMessage("MISSING INDEX", formatKeyWord)
			}
			var explicitArgIndex = width.substr(leftBracketIndex + 1, rightBracketIndex - leftBracketIndex - 1)
			
			if(isNaN(explicitArgIndex) || explicitArgIndex >= args.length || Number(explicitArgIndex) != parseInt(explicitArgIndex)){
				return errorMessage("BAD INDEX", formatKeyWord, explicitArgIndex)
			}
			
			argIndex = Number(explicitArgIndex)
			
			// clean the width 
			// e.g. if width is "+23.5[2]", replace it to "+23.5"
			width = width.replace(
				width.substring(leftBracketIndex, explicitArgIndex + 1),
				""
			)
		}else{
			argIndex++
			if(argIndex >= args.length){
				return errorMessage("BAD INDEX", formatKeyWord)
			}
		}
		
		if(width.search(/[^\d\.]/g) >= 0){
			return errorMessage("BAD WIDTH", formatKeyWord, width)
		}
		
		/** 要格式化的 Argument */
		var formatArgument = args[argIndex]
		
		if(formatArgument === undefined){
			return errorMessage("undefined")
		}
		if(formatArgument === null){
			return errorMessage("null")
		}
		
		var precision = DEFAULT_PRECISION
		if(width == ""){
			width = "1"
		}else if(width.includes(".")){
			precision = width.substr(width.indexOf(".") + 1)
			width = width.substr(0, width.indexOf("."))
		}
		precision = Number(precision)
		
		// get argument
		switch(formatKeyWord){
			case "%":
				break
			case "f":
			case "F":
				// float
				if(isNaN(Number(formatArgument))){
					return errorMessage("WRONG TYPE", formatKeyWord, formatArgument)
				}
				try{
					// JS 要 +1 ,因為 JS的精確度算法和 golang不同
					// 例如 Math.PI.toPrecision(2),JS會是 "3.1"; golang會是 "3.14"
					formatArgument = (Number(formatArgument) >= 0 && fmt.plus? "+": "") + parseFloat(formatArgument).toPrecision(precision + 1)
				}catch(error){
					return errorMessage("BAD PRECISION", formatArgument, precision)
				}
				
				break
			case "T":
				// type of value
				if(typeof formatArgument == "object"){
					// if argument is an Object, return the name of its' constructor
					formatArgument = formatArgument.constructor.name
				}else{
					formatArgument = typeof formatArgument
				}
				break
			case "U":
				// Unicode format. E.g. U+12AB
				if(isNaN(Number(formatArgument))){
					return errorMessage("WRONG TYPE", formatKeyWord, formatArgument)
				}
				// the hex of the Argument 
				var hexOfArg = parseInt(formatArgument).toString(16).toUpperCase()
				
				formatArgument = (Number(formatArgument) >= 0 && fmt.plus? "+": "") + "U+" + "0".repeat(4 - hexOfArg.length >= 0? 4 - hexOfArg.length: 0) + hexOfArg
				break
			case "b":
				// 2進制
				if(isNaN(Number(formatArgument))){
					return errorMessage("WRONG TYPE", formatKeyWord, formatArgument)
				}
				formatArgument = (Number(formatArgument) >= 0 && fmt.plus? "+": "") + Number(formatArgument).toString(2)
				
				break
			case "c":
				// 將 char code 轉為 char
				if(!isNaN(Number(formatArgument))){
					formatArgument = String.fromCharCode(Number(formatArgument))
				}else if(typeof formatArgument == "string"){
					formatArgument = formatArgument.charAt(0)
					if(formatArgument == ""){
						return errorMessage("Empty Character", formatKeyWord, formatArgument)
					}
				}else{
					return errorMessage("WRONG TYPE", formatKeyWord, formatArgument)
				}
				
				break
			case "d":
				// 10進制整數
				if(isNaN(Number(formatArgument))){
					return errorMessage("WRONG TYPE", formatKeyWord, formatArgument)
				}
				formatArgument = (Number(formatArgument) >= 0 && fmt.plus? "+": "") + parseInt(formatArgument)
				break
			case "o":
				// 8進制
				if(isNaN(Number(formatArgument))){
					return errorMessage("WRONG TYPE", formatKeyWord, formatArgument)
				}
				formatArgument = (Number(formatArgument) >= 0 && fmt.plus? "+": "") + Number(formatArgument).toString(8)
				
				break
			case "x":
			case "X":
				// 16進制
				if(isNaN(Number(formatArgument))){
					return errorMessage("WRONG TYPE", formatKeyWord, formatArgument)
				}
				formatArgument = (Number(formatArgument) >= 0 && fmt.plus? "+": "") + Number(formatArgument).toString(16)
				
				if(formatKeyWord == "X"){
					formatArgument = formatArgument.toUpperCase()
				}
				break
			case "e":
			case "E":
				// 轉為科學計數法表示的數字,例如:1.024e3
				if(isNaN(Number(formatArgument))){
					return errorMessage("WRONG TYPE", formatKeyWord, formatArgument)
				}
				try{
					formatArgument = (Number(formatArgument) >= 0 && fmt.plus? "+": "") + parseFloat(formatArgument).toExponential(precision)
				}catch(error){
					return errorMessage("BAD PRECISION", formatArgument, precision)
				}
				
				if(formatKeyWord == "E"){
					formatArgument = formatArgument.toUpperCase()
				}
				break
			case "g":
			case "G":
				// 當數字 >= 1000000 ,等同 %e / %E
				// 反之等同 %d / %f
				if(isNaN(Number(formatArgument))){
					return errorMessage("WRONG TYPE", formatKeyWord, formatArgument)
				}
				if(Number(formatArgument) >= 1000000){
					//TODO
				}else if(Number(formatArgument) == parseInt(formatArgument)){
					// is int
					formatArgument = (Number(formatArgument) >= 0 && fmt.plus? "+": "") + parseInt(formatArgument)
				}else{
					formatArgument = (Number(formatArgument) >= 0 && fmt.plus? "+": "") + Number(formatArgument).toPrecision(precision)
				}
				
				break
			case "p":
				break
			case "q":
				// 若參數為數字,將其轉為 char 並加上單引號 ''
				// 若參數為Sting,將其轉為 char 並加上雙引號 ""
				switch(typeof formatArgument){
					case "number":
						// 強制將數字轉為正整數,再轉成 char
						formatArgument = "'" + String.fromCharCode(Math.floor(Math.abs(formatArgument))) + "'"
						break
					case "string":
						formatArgument = "\"" + formatArgument + "\""
						break
					default:
						return errorMessage("WRONG TYPE", formatKeyWord, formatArgument)
						break
				}
				break
				
			case "s":
				// print string
				formatArgument = formatArgument + ""
				break
			case "t":
				// print boolean
				formatArgument = Boolean(formatArgument)
				break
			case "v":
				// the value in a default format
				// 若是以 %#v ,則 print JSON
				if(fmt.sharp && typeof formatArgument == "object"){
					formatArgument = JSON.stringify(formatArgument)
				}else if(Number(formatArgument) == parseInt(formatArgument)){
					formatArgument = (Number(formatArgument) >= 0 && fmt.plus? "+": "") + parseInt(formatArgument)
				}else if(!isNaN(Number(formatArgument))){
					formatArgument = (Number(formatArgument) >= 0 && fmt.plus? "+": "") + parseFloat(formatArgument).toPrecision(precision + 1)
				}else{
					formatArgument = formatArgument + ""
				}
				break
				
		}
		// 要補的 char。
		// 舉例來說如果是 %0d 的話,就是 '0';否則為 " "
		var fillChar = " "
		if(fmt.zero){
			fillChar = "0"
		}
		
		if(fmt.space){
			formatArgument = " " + formatArgument
		}
	
		var fillLength = parseInt(width) - (formatArgument + "").length 
		if(fillLength < 0){
			fillLength = 0
		}
		
		if(fmt.minus){
			formatArgument = formatArgument + "" + fillChar.repeat(fillLength)
		}else{
			formatArgument = fillChar.repeat(fillLength) + "" + formatArgument
		}
		return formatArgument
	})
	
	var unusedArguments = args.slice(argIndex + 1, args.length)
	if(!hasBracket && unusedArguments.length){
		var argsMessages = []
		unusedArguments.forEach(function(uarg){
			argsMessages.push((typeof uarg) + "=" + (typeof uarg == "object"? JSON.stringify(uarg): uarg ))
		})
		
		formatedString += " " + errorMessage("Unformated Arguments: " + argsMessages.join(", "))
	}
	return formatedString
}