PHP's sprintf in JavaScript

Here’s what our current JavaScript equivalent to PHP's sprintf looks like.

module.exports = function sprintf () {
// discuss at: https://locutus.io/php/sprintf/
// original by: Ash Searle (https://hexmen.com/blog/)
// improved by: Michael White (https://getsprink.com)
// improved by: Jack
// improved by: Kevin van Zonneveld (https://kvz.io)
// improved by: Kevin van Zonneveld (https://kvz.io)
// improved by: Kevin van Zonneveld (https://kvz.io)
// improved by: Dj
// improved by: Allidylls
// input by: Paulo Freitas
// input by: Brett Zamir (https://brett-zamir.me)
// improved by: Rafał Kukawski (https://kukawski.pl)
// example 1: sprintf("%01.2f", 123.1)
// returns 1: '123.10'
// example 2: sprintf("[%10s]", 'monkey')
// returns 2: '[ monkey]'
// example 3: sprintf("[%'#10s]", 'monkey')
// returns 3: '[####monkey]'
// example 4: sprintf("%d", 123456789012345)
// returns 4: '123456789012345'
// example 5: sprintf('%-03s', 'E')
// returns 5: 'E00'
// example 6: sprintf('%+010d', 9)
// returns 6: '+000000009'
// example 7: sprintf('%+0\'@10d', 9)
// returns 7: '@@@@@@@@+9'
// example 8: sprintf('%.f', 3.14)
// returns 8: '3.140000'
// example 9: sprintf('%% %2$d', 1, 2)
// returns 9: '% 2'
const regex = /%%|%(?:(\d+)\$)?((?:[-+#0 ]|'[\s\S])*)(\d+)?(?:\.(\d*))?([\s\S])/g
const args = arguments
let i = 0
const format = args[i++]
const _pad = function (str, len, chr, leftJustify) {
if (!chr) {
chr = ' '
}
const padding = (str.length >= len) ? '' : new Array(1 + len - str.length >>> 0).join(chr)
return leftJustify ? str + padding : padding + str
}
const justify = function (value, prefix, leftJustify, minWidth, padChar) {
const diff = minWidth - value.length
if (diff > 0) {
// when padding with zeros
// on the left side
// keep sign (+ or -) in front
if (!leftJustify && padChar === '0') {
value = [
value.slice(0, prefix.length),
_pad('', diff, '0', true),
value.slice(prefix.length)
].join('')
} else {
value = _pad(value, minWidth, padChar, leftJustify)
}
}
return value
}
const _formatBaseX = function (value, base, leftJustify, minWidth, precision, padChar) {
// Note: casts negative numbers to positive ones
const number = value >>> 0
value = _pad(number.toString(base), precision || 0, '0', false)
return justify(value, '', leftJustify, minWidth, padChar)
}
// _formatString()
const _formatString = function (value, leftJustify, minWidth, precision, customPadChar) {
if (precision !== null && precision !== undefined) {
value = value.slice(0, precision)
}
return justify(value, '', leftJustify, minWidth, customPadChar)
}
// doFormat()
const doFormat = function (substring, argIndex, modifiers, minWidth, precision, specifier) {
let number, prefix, method, textTransform, value
if (substring === '%%') {
return '%'
}
// parse modifiers
let padChar = ' ' // pad with spaces by default
let leftJustify = false
let positiveNumberPrefix = ''
let j, l
for (j = 0, l = modifiers.length; j < l; j++) {
switch (modifiers.charAt(j)) {
case ' ':
case '0':
padChar = modifiers.charAt(j)
break
case '+':
positiveNumberPrefix = '+'
break
case '-':
leftJustify = true
break
case "'":
if (j + 1 < l) {
padChar = modifiers.charAt(j + 1)
j++
}
break
}
}
if (!minWidth) {
minWidth = 0
} else {
minWidth = +minWidth
}
if (!isFinite(minWidth)) {
throw new Error('Width must be finite')
}
if (!precision) {
precision = (specifier === 'd') ? 0 : 'fFeE'.indexOf(specifier) > -1 ? 6 : undefined
} else {
precision = +precision
}
if (argIndex && +argIndex === 0) {
throw new Error('Argument number must be greater than zero')
}
if (argIndex && +argIndex >= args.length) {
throw new Error('Too few arguments')
}
value = argIndex ? args[+argIndex] : args[i++]
switch (specifier) {
case '%':
return '%'
case 's':
return _formatString(value + '', leftJustify, minWidth, precision, padChar)
case 'c':
return _formatString(String.fromCharCode(+value), leftJustify, minWidth, precision, padChar)
case 'b':
return _formatBaseX(value, 2, leftJustify, minWidth, precision, padChar)
case 'o':
return _formatBaseX(value, 8, leftJustify, minWidth, precision, padChar)
case 'x':
return _formatBaseX(value, 16, leftJustify, minWidth, precision, padChar)
case 'X':
return _formatBaseX(value, 16, leftJustify, minWidth, precision, padChar)
.toUpperCase()
case 'u':
return _formatBaseX(value, 10, leftJustify, minWidth, precision, padChar)
case 'i':
case 'd':
number = +value || 0
// Plain Math.round doesn't just truncate
number = Math.round(number - number % 1)
prefix = number < 0 ? '-' : positiveNumberPrefix
value = prefix + _pad(String(Math.abs(number)), precision, '0', false)
if (leftJustify && padChar === '0') {
// can't right-pad 0s on integers
padChar = ' '
}
return justify(value, prefix, leftJustify, minWidth, padChar)
case 'e':
case 'E':
case 'f': // @todo: Should handle locales (as per setlocale)
case 'F':
case 'g':
case 'G':
number = +value
prefix = number < 0 ? '-' : positiveNumberPrefix
method = ['toExponential', 'toFixed', 'toPrecision']['efg'.indexOf(specifier.toLowerCase())]
textTransform = ['toString', 'toUpperCase']['eEfFgG'.indexOf(specifier) % 2]
value = prefix + Math.abs(number)[method](precision)
return justify(value, prefix, leftJustify, minWidth, padChar)[textTransform]()
default:
// unknown specifier, consume that char and return empty
return ''
}
}
try {
return format.replace(regex, doFormat)
} catch (err) {
return false
}
}
[ View on GitHub | Edit on GitHub | Source on GitHub ]

How to use

You you can install via npm install locutus and require it via require('locutus/php/strings/sprintf'). You could also require the strings module in full so that you could access strings.sprintf instead.

If you intend to target the browser, you can then use a module bundler such as Parcel, webpack, Browserify, or rollup.js. This can be important because Locutus allows modern JavaScript in the source files, meaning it may not work in all browsers without a build/transpile step. Locutus does transpile all functions to ES5 before publishing to npm.

A community effort

Not unlike Wikipedia, Locutus is an ongoing community effort. Our philosophy follows The McDonald’s Theory. This means that we don't consider it to be a bad thing that many of our functions are first iterations, which may still have their fair share of issues. We hope that these flaws will inspire others to come up with better ideas.

This way of working also means that we don't offer any production guarantees, and recommend to use Locutus inspiration and learning purposes only.

Examples

Please note that these examples are distilled from test cases that automatically verify our functions still work correctly. This could explain some quirky ones.

#codeexpected result
1sprintf("%01.2f", 123.1)'123.10'
2sprintf("[%10s]", 'monkey')'[ monkey]'
3sprintf("[%'#10s]", 'monkey')'[####monkey]'
4sprintf("%d", 123456789012345)'123456789012345'
5sprintf('%-03s', 'E')'E00'
6sprintf('%+010d', 9)'+000000009'
7sprintf('%+0\'@10d', 9)'@@@@@@@@+9'
8sprintf('%.f', 3.14)'3.140000'
9sprintf('%% %2$d', 1, 2)'% 2'

« More PHP strings functions


Star