PHP's sprintf in TypeScript

✓ Verified: PHP 8.3
Examples tested against actual runtime. CI re-verifies continuously. Only documented examples are tested.
Rosetta Stone: c/sprintf · perl/sprintf

How to use

Install via yarn add locutus and import: import { sprintf } from 'locutus/php/strings/sprintf'.

Or with CommonJS: const { sprintf } = require('locutus/php/strings/sprintf')

Use a bundler that supports tree-shaking so you only ship the functions you actually use. Vite, webpack, Rollup, and Parcel all handle this. For server-side use this is less of a concern.

Examples

These examples are extracted from test cases that automatically verify our functions against their native counterparts.

#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'

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

import type { PhpRuntimeValue } from '../_helpers/_phpTypes.ts'

type SprintfValue = PhpRuntimeValue

export function sprintf(format: string, ...args: SprintfValue[]): string | false {
// discuss at: https://locutus.io/php/sprintf/
// parity verified: PHP 8.3
// 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 callArgs: SprintfValue[] = [format, ...args]
let i = 1

const _pad = function (str: string, len: number, chr: string, leftJustify: boolean): string {
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: string,
prefix: string,
leftJustify: boolean,
minWidth: number,
padChar: string,
): string {
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: SprintfValue,
base: number,
leftJustify: boolean,
minWidth: number,
precision: number | undefined,
padChar: string,
): string {
// Note: casts negative numbers to positive ones
const number = Number(value) >>> 0
const padded = _pad(number.toString(base), precision || 0, '0', false)
return justify(padded, '', leftJustify, minWidth, padChar)
}

// _formatString()
const _formatString = function (
value: string,
leftJustify: boolean,
minWidth: number,
precision: number | null | undefined,
customPadChar: string,
) {
if (precision !== null && precision !== undefined) {
value = value.slice(0, precision)
}
return justify(value, '', leftJustify, minWidth, customPadChar)
}

// doFormat()
const doFormat = function (
substring: string,
argIndex: string | undefined,
modifiers: string,
minWidthRaw: string | undefined,
precisionRaw: string | undefined,
specifier: string,
): string {
let number = 0
let prefix = ''
let value: SprintfValue

if (substring === '%%') {
return '%'
}

// parse modifiers
let padChar = ' ' // pad with spaces by default
let leftJustify = false
let positiveNumberPrefix = ''
let j = 0
let l = 0

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
}
}

let minWidthNum = 0
if (!minWidthRaw) {
minWidthNum = 0
} else {
minWidthNum = +minWidthRaw
}

if (!isFinite(minWidthNum)) {
throw new Error('Width must be finite')
}

let precisionNum: number | undefined
if (!precisionRaw) {
precisionNum = specifier === 'd' ? 0 : 'fFeE'.includes(specifier) ? 6 : undefined
} else {
precisionNum = +precisionRaw
}

if (argIndex && +argIndex === 0) {
throw new Error('Argument number must be greater than zero')
}

if (argIndex && +argIndex >= callArgs.length) {
throw new Error('Too few arguments')
}

value = argIndex ? callArgs[+argIndex] : callArgs[i++]

switch (specifier) {
case '%':
return '%'
case 's':
return _formatString(String(value), leftJustify, minWidthNum, precisionNum, padChar)
case 'c':
return _formatString(String.fromCharCode(+String(value)), leftJustify, minWidthNum, precisionNum, padChar)
case 'b':
return _formatBaseX(value, 2, leftJustify, minWidthNum, precisionNum, padChar)
case 'o':
return _formatBaseX(value, 8, leftJustify, minWidthNum, precisionNum, padChar)
case 'x':
return _formatBaseX(value, 16, leftJustify, minWidthNum, precisionNum, padChar)
case 'X':
return _formatBaseX(value, 16, leftJustify, minWidthNum, precisionNum, padChar).toUpperCase()
case 'u':
return _formatBaseX(value, 10, leftJustify, minWidthNum, precisionNum, padChar)
case 'i':
case 'd':
number = +String(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)), precisionNum ?? 0, '0', false)

if (leftJustify && padChar === '0') {
// can't right-pad 0s on integers
padChar = ' '
}
return justify(String(value), prefix, leftJustify, minWidthNum, padChar)
case 'e':
case 'E':
case 'f': // @todo: Should handle locales (as per setlocale)
case 'F':
case 'g':
case 'G': {
number = +String(value)
prefix = number < 0 ? '-' : positiveNumberPrefix
const methodIndex = 'efg'.indexOf(specifier.toLowerCase())
const absNumber = Math.abs(number)
let floatResult = ''
if (methodIndex === 0) {
floatResult = absNumber.toExponential(precisionNum)
} else if (methodIndex === 1) {
floatResult = absNumber.toFixed(precisionNum)
} else {
floatResult = absNumber.toPrecision(precisionNum)
}
const justified = justify(prefix + floatResult, prefix, leftJustify, minWidthNum, padChar)
return 'eEfFgG'.indexOf(specifier) % 2 === 0 ? justified : justified.toUpperCase()
}
default:
// unsupported specifier, consume that char and return empty
return ''
}
}

try {
return format.replace(regex, doFormat)
} catch (_err) {
return false
}
}

Improve this function

Locutus is a community effort following The McDonald's Theory: we ship first iterations, hoping others will improve them. If you see something that could be better, we'd love your contribution.

View on GitHub · Edit on GitHub · View Raw


« More PHP strings functions


Star