locutus
Version:
Locutus other languages' standard libraries to JavaScript for fun and educational purposes
193 lines (192 loc) • 7.91 kB
JavaScript
export function sprintf(format, ...args) {
// 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 = [format, ...args];
let i = 1;
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 = Number(value) >>> 0;
const padded = _pad(number.toString(base), precision || 0, '0', false);
return justify(padded, '', 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, minWidthRaw, precisionRaw, specifier) {
let number = 0;
let prefix = '';
let value;
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;
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;
}
}