Skip to content

Commit ef8258f

Browse files
kukawskikvz
authored andcommitted
sprintf: improve PHP compatibility (locutusjs#356)
- %1% should move args counter forward, where %% should not - %0'#d should pad with # instead of 0 - unknown specifier should consume that char and return empty substring - removed custom binary, octal and hex prefixes - added some error handling which makes sprintf return false - don't allow padding ints with 0's on the right side, use spaces instead
1 parent 8966990 commit ef8258f

File tree

1 file changed

+79
-70
lines changed

1 file changed

+79
-70
lines changed

src/php/strings/sprintf.js

Lines changed: 79 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = function sprintf () {
1010
// improved by: Allidylls
1111
// input by: Paulo Freitas
1212
// input by: Brett Zamir (http://brett-zamir.me)
13+
// improved by: Rafał Kukawski (http://kukawski.pl)
1314
// example 1: sprintf("%01.2f", 123.1)
1415
// returns 1: '123.10'
1516
// example 2: sprintf("[%10s]", 'monkey')
@@ -20,11 +21,19 @@ module.exports = function sprintf () {
2021
// returns 4: '123456789012345'
2122
// example 5: sprintf('%-03s', 'E')
2223
// returns 5: 'E00'
24+
// example 6: sprintf('%+010d', 9)
25+
// returns 6: '+000000009'
26+
// example 7: sprintf('%+0\'@10d', 9)
27+
// returns 7: '@@@@@@@@+9'
28+
// example 8: sprintf('%.f', 3.14)
29+
// returns 8: '3.140000'
30+
// example 9: sprintf('%% %2$d', 1, 2)
31+
// returns 9: '% 2'
2332

24-
var regex = /%%|%(\d+\$)?([-+'#0 ]*)(\*\d+\$|\*|\d+)?(?:\.(\d*))?([scboxXuideEfFgG])/g
25-
var a = arguments
33+
var regex = /%%|%(?:(\d+)\$)?((?:[-+#0 ]|'[\s\S])*)(\d+)?(?:\.(\d*))?([\s\S])/g
34+
var args = arguments
2635
var i = 0
27-
var format = a[i++]
36+
var format = args[i++]
2837

2938
var _pad = function (str, len, chr, leftJustify) {
3039
if (!chr) {
@@ -34,153 +43,153 @@ module.exports = function sprintf () {
3443
return leftJustify ? str + padding : padding + str
3544
}
3645

37-
var justify = function (value, prefix, leftJustify, minWidth, zeroPad, customPadChar) {
46+
var justify = function (value, prefix, leftJustify, minWidth, padChar) {
3847
var diff = minWidth - value.length
3948
if (diff > 0) {
40-
if (leftJustify || !zeroPad) {
41-
value = _pad(value, minWidth, customPadChar, leftJustify)
42-
} else {
49+
// when padding with zeros
50+
// on the left side
51+
// keep sign (+ or -) in front
52+
if (!leftJustify && padChar === '0') {
4353
value = [
4454
value.slice(0, prefix.length),
4555
_pad('', diff, '0', true),
4656
value.slice(prefix.length)
4757
].join('')
58+
} else {
59+
value = _pad(value, minWidth, padChar, leftJustify)
4860
}
4961
}
5062
return value
5163
}
5264

53-
var _formatBaseX = function (value, base, prefix, leftJustify, minWidth, precision, zeroPad) {
65+
var _formatBaseX = function (value, base, leftJustify, minWidth, precision, padChar) {
5466
// Note: casts negative numbers to positive ones
5567
var number = value >>> 0
56-
prefix = (prefix && number && {
57-
'2': '0b',
58-
'8': '0',
59-
'16': '0x'
60-
}[base]) || ''
61-
value = prefix + _pad(number.toString(base), precision || 0, '0', false)
62-
return justify(value, prefix, leftJustify, minWidth, zeroPad)
68+
value = _pad(number.toString(base), precision || 0, '0', false)
69+
return justify(value, '', leftJustify, minWidth, padChar)
6370
}
6471

6572
// _formatString()
66-
var _formatString = function (value, leftJustify, minWidth, precision, zeroPad, customPadChar) {
73+
var _formatString = function (value, leftJustify, minWidth, precision, customPadChar) {
6774
if (precision !== null && precision !== undefined) {
6875
value = value.slice(0, precision)
6976
}
70-
return justify(value, '', leftJustify, minWidth, zeroPad, customPadChar)
77+
return justify(value, '', leftJustify, minWidth, customPadChar)
7178
}
7279

7380
// doFormat()
74-
var doFormat = function (substring, valueIndex, flags, minWidth, precision, type) {
81+
var doFormat = function (substring, argIndex, modifiers, minWidth, precision, specifier) {
7582
var number, prefix, method, textTransform, value
7683

7784
if (substring === '%%') {
7885
return '%'
7986
}
8087

81-
// parse flags
88+
// parse modifiers
89+
var padChar = ' ' // pad with spaces by default
8290
var leftJustify = false
83-
var positivePrefix = ''
84-
var zeroPad = false
85-
var prefixBaseX = false
86-
var customPadChar = ' '
87-
var flagsl = flags.length
88-
var j
89-
for (j = 0; j < flagsl; j++) {
90-
switch (flags.charAt(j)) {
91+
var positiveNumberPrefix = ''
92+
var j, l
93+
94+
for (j = 0, l = modifiers.length; j < l; j++) {
95+
switch (modifiers.charAt(j)) {
9196
case ' ':
92-
positivePrefix = ' '
97+
case '0':
98+
padChar = modifiers.charAt(j)
9399
break
94100
case '+':
95-
positivePrefix = '+'
101+
positiveNumberPrefix = '+'
96102
break
97103
case '-':
98104
leftJustify = true
99105
break
100106
case "'":
101-
customPadChar = flags.charAt(j + 1)
102-
break
103-
case '0':
104-
zeroPad = true
105-
customPadChar = '0'
106-
break
107-
case '#':
108-
prefixBaseX = true
107+
if (j + 1 < l) {
108+
padChar = modifiers.charAt(j + 1)
109+
j++
110+
}
109111
break
110112
}
111113
}
112114

113-
// parameters may be null, undefined, empty-string or real valued
114-
// we want to ignore null, undefined and empty-string values
115115
if (!minWidth) {
116116
minWidth = 0
117-
} else if (minWidth === '*') {
118-
minWidth = +a[i++]
119-
} else if (minWidth.charAt(0) === '*') {
120-
minWidth = +a[minWidth.slice(1, -1)]
121117
} else {
122118
minWidth = +minWidth
123119
}
124120

125-
// Note: undocumented perl feature:
126-
if (minWidth < 0) {
127-
minWidth = -minWidth
128-
leftJustify = true
129-
}
130-
131121
if (!isFinite(minWidth)) {
132-
throw new Error('sprintf: (minimum-)width must be finite')
122+
throw new Error('Width must be finite')
133123
}
134124

135125
if (!precision) {
136-
precision = 'fFeE'.indexOf(type) > -1 ? 6 : (type === 'd') ? 0 : undefined
126+
precision = (specifier === 'd') ? 0 : 'fFeE'.indexOf(specifier) > -1 ? 6 : undefined
137127
} else {
138128
precision = +precision
139129
}
140130

141-
// grab value using valueIndex if required?
142-
value = valueIndex ? a[valueIndex.slice(0, -1)] : a[i++]
131+
if (argIndex && +argIndex === 0) {
132+
throw new Error('Argument number must be greater than zero')
133+
}
134+
135+
if (argIndex && +argIndex >= args.length) {
136+
throw new Error('Too few arguments')
137+
}
143138

144-
switch (type) {
139+
value = argIndex ? args[+argIndex] : args[i++]
140+
141+
switch (specifier) {
142+
case '%':
143+
return '%'
145144
case 's':
146-
return _formatString(value + '', leftJustify, minWidth, precision, zeroPad, customPadChar)
145+
return _formatString(value + '', leftJustify, minWidth, precision, padChar)
147146
case 'c':
148-
return _formatString(String.fromCharCode(+value), leftJustify, minWidth, precision, zeroPad)
147+
return _formatString(String.fromCharCode(+value), leftJustify, minWidth, precision, padChar)
149148
case 'b':
150-
return _formatBaseX(value, 2, prefixBaseX, leftJustify, minWidth, precision, zeroPad)
149+
return _formatBaseX(value, 2, leftJustify, minWidth, precision, padChar)
151150
case 'o':
152-
return _formatBaseX(value, 8, prefixBaseX, leftJustify, minWidth, precision, zeroPad)
151+
return _formatBaseX(value, 8, leftJustify, minWidth, precision, padChar)
153152
case 'x':
154-
return _formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad)
153+
return _formatBaseX(value, 16, leftJustify, minWidth, precision, padChar)
155154
case 'X':
156-
return _formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad)
157-
.toUpperCase()
155+
return _formatBaseX(value, 16, leftJustify, minWidth, precision, padChar)
156+
.toUpperCase()
158157
case 'u':
159-
return _formatBaseX(value, 10, prefixBaseX, leftJustify, minWidth, precision, zeroPad)
158+
return _formatBaseX(value, 10, leftJustify, minWidth, precision, padChar)
160159
case 'i':
161160
case 'd':
162161
number = +value || 0
163162
// Plain Math.round doesn't just truncate
164163
number = Math.round(number - number % 1)
165-
prefix = number < 0 ? '-' : positivePrefix
164+
prefix = number < 0 ? '-' : positiveNumberPrefix
166165
value = prefix + _pad(String(Math.abs(number)), precision, '0', false)
167-
return justify(value, prefix, leftJustify, minWidth, zeroPad)
166+
167+
if (leftJustify && padChar === '0') {
168+
// can't right-pad 0s on integers
169+
padChar = ' '
170+
}
171+
return justify(value, prefix, leftJustify, minWidth, padChar)
168172
case 'e':
169173
case 'E':
170174
case 'f': // @todo: Should handle locales (as per setlocale)
171175
case 'F':
172176
case 'g':
173177
case 'G':
174178
number = +value
175-
prefix = number < 0 ? '-' : positivePrefix
176-
method = ['toExponential', 'toFixed', 'toPrecision']['efg'.indexOf(type.toLowerCase())]
177-
textTransform = ['toString', 'toUpperCase']['eEfFgG'.indexOf(type) % 2]
179+
prefix = number < 0 ? '-' : positiveNumberPrefix
180+
method = ['toExponential', 'toFixed', 'toPrecision']['efg'.indexOf(specifier.toLowerCase())]
181+
textTransform = ['toString', 'toUpperCase']['eEfFgG'.indexOf(specifier) % 2]
178182
value = prefix + Math.abs(number)[method](precision)
179-
return justify(value, prefix, leftJustify, minWidth, zeroPad)[textTransform]()
183+
return justify(value, prefix, leftJustify, minWidth, padChar)[textTransform]()
180184
default:
181-
return substring
185+
// unknown specifier, consume that char and return empty
186+
return ''
182187
}
183188
}
184189

185-
return format.replace(regex, doFormat)
190+
try {
191+
return format.replace(regex, doFormat)
192+
} catch (err) {
193+
return false
194+
}
186195
}

0 commit comments

Comments
 (0)