# Coverage

82%
226
186
40

## printf.js

82%
226
186
40

Line | Hits | Source
--- | --- | ---
1 |  |
2 | 1 | var util = require('util');
3 |  |
4 | 1 | var tokenize = function(/*String*/ str, /*RegExp*/ re, /*Function?*/ parseDelim, /*Object?*/ instance){
5 |  | // summary:
6 |  | // Split a string by a regular expression with the ability to capture the delimeters
7 |  | // parseDelim:
8 |  | // Each group (excluding the 0 group) is passed as a parameter. If the function returns
9 |  | // a value, it's added to the list of tokens.
10 |  | // instance:
11 |  | // Used as the "this' instance when calling parseDelim
12 | 1081 | var tokens = [];
13 | 1081 | var match, content, lastIndex = 0;
14 | 1081 | while(match = re.exec(str)){
15 | 2086 | content = str.slice(lastIndex, re.lastIndex - match[0].length);
16 | 2086 | if(content.length){
17 | 1011 | tokens.push(content);
18 |  | }
19 | 2086 | if(parseDelim){
20 | 2086 | var parsed = parseDelim.apply(instance, match.slice(1).concat(tokens.length));
21 | 2086 | if(typeof parsed != 'undefined'){
22 | 2086 | if(parsed.specifier === '%'){
23 | 0 | tokens.push('%');
24 |  | }else{
25 | 2086 | tokens.push(parsed);
26 |  | }
27 |  | }
28 |  | }
29 | 2086 | lastIndex = re.lastIndex;
30 |  | }
31 | 1081 | content = str.slice(lastIndex);
32 | 1081 | if(content.length){
33 | 1010 | tokens.push(content);
34 |  | }
35 | 1081 | return tokens;
36 |  | }
37 |  |
38 | 1 | var Formatter = function(/*String*/ format){
39 | 1081 | var tokens = [];
40 | 1081 | this._mapped = false;
41 | 1081 | this._format = format;
42 | 1081 | this._tokens = tokenize(format, this._re, this._parseDelim, this);
43 |  | }
44 |  |
45 | 1 | Formatter.prototype._re = /\%(?:\(([\w_]+)\)|([1-9]\d*)\$)?([0 +\-\#]*)(\*|\d+)?(\.)?(\*|\d+)?[hlL]?([\%bscdeEfFgGioOuxX])/g;
46 | 1 | Formatter.prototype._parseDelim = function(mapping, intmapping, flags, minWidth, period, precision, specifier){
47 | 2086 | if(mapping){
48 | 9 | this._mapped = true;
49 |  | }
50 | 2086 | return {
51 |  | mapping: mapping,
52 |  | intmapping: intmapping,
53 |  | flags: flags,
54 |  | _minWidth: minWidth, // May be dependent on parameters
55 |  | period: period,
56 |  | _precision: precision, // May be dependent on parameters
57 |  | specifier: specifier
58 |  | };
59 |  | };
60 | 1 | Formatter.prototype._specifiers = {
61 |  | b: {
62 |  | base: 2,
63 |  | isInt: true
64 |  | },
65 |  | o: {
66 |  | base: 8,
67 |  | isInt: true
68 |  | },
69 |  | x: {
70 |  | base: 16,
71 |  | isInt: true
72 |  | },
73 |  | X: {
74 |  | extend: ['x'],
75 |  | toUpper: true
76 |  | },
77 |  | d: {
78 |  | base: 10,
79 |  | isInt: true
80 |  | },
81 |  | i: {
82 |  | extend: ['d']
83 |  | },
84 |  | u: {
85 |  | extend: ['d'],
86 |  | isUnsigned: true
87 |  | },
88 |  | c: {
89 |  | setArg: function(token){
90 | 6 | if(!isNaN(token.arg)){
91 | 5 | var num = parseInt(token.arg);
92 | 5 | if(num < 0 || num > 127){
93 | 2 | throw new Error('invalid character code passed to %c in printf');
94 |  | }
95 | 3 | token.arg = isNaN(num) ? '' + num : String.fromCharCode(num);
96 |  | }
97 |  | }
98 |  | },
99 |  | s: {
100 |  | setMaxWidth: function(token){
101 | 2013 | token.maxWidth = (token.period == '.') ? token.precision : -1;
102 |  | }
103 |  | },
104 |  | e: {
105 |  | isDouble: true,
106 |  | doubleNotation: 'e'
107 |  | },
108 |  | E: {
109 |  | extend: ['e'],
110 |  | toUpper: true
111 |  | },
112 |  | f: {
113 |  | isDouble: true,
114 |  | doubleNotation: 'f'
115 |  | },
116 |  | F: {
117 |  | extend: ['f']
118 |  | },
119 |  | g: {
120 |  | isDouble: true,
121 |  | doubleNotation: 'g'
122 |  | },
123 |  | G: {
124 |  | extend: ['g'],
125 |  | toUpper: true
126 |  | },
127 |  | O: {
128 |  | setArg: function(token){
129 | 1 | token.arg = util.inspect(token.arg, true, null);
130 |  | }
131 |  | },
132 |  | };
133 | 1 | Formatter.prototype.format = function(/*mixed...*/ filler){
134 | 2080 | if(this._mapped && typeof filler != 'object'){
135 | 2 | throw new Error('format requires a mapping');
136 |  | }
137 |  |
138 | 2078 | var str = '';
139 | 2078 | var position = 0;
140 | 2078 | for(var i = 0, token; i < this._tokens.length; i++){
141 | 8099 | token = this._tokens[i];
142 |  |
143 | 8099 | if(typeof token == 'string'){
144 | 4018 | str += token;
145 |  | }else{
146 | 4081 | if(this._mapped){
147 | 6 | if(typeof filler[token.mapping] == 'undefined'){
148 | 0 | throw new Error('missing key ' + token.mapping);
149 |  | }
150 | 6 | token.arg = filler[token.mapping];
151 |  | }else{
152 | 4075 | if(token.intmapping){
153 | 7 | var position = parseInt(token.intmapping) - 1;
154 |  | }
155 | 4075 | if(position >= arguments.length){
156 | 3 | throw new Error('got ' + arguments.length + ' printf arguments, insufficient for \'' + this._format + '\'');
157 |  | }
158 | 4072 | token.arg = arguments[position++];
159 |  | }
160 |  |
161 | 4078 | if(!token.compiled){
162 | 2080 | token.compiled = true;
163 | 2080 | token.sign = '';
164 | 2080 | token.zeroPad = false;
165 | 2080 | token.rightJustify = false;
166 | 2080 | token.alternative = false;
167 |  |
168 | 2080 | var flags = {};
169 | 2080 | for(var fi = token.flags.length; fi--;){
170 | 52 | var flag = token.flags.charAt(fi);
171 | 52 | flags[flag] = true;
172 | 52 | switch(flag){
173 |  | case ' ':
174 | 6 | token.sign = ' ';
175 | 6 | break;
176 |  | case '+':
177 | 7 | token.sign = '+';
178 | 7 | break;
179 |  | case '0':
180 | 20 | token.zeroPad = (flags['-']) ? false : true;
181 | 20 | break;
182 |  | case '-':
183 | 19 | token.rightJustify = true;
184 | 19 | token.zeroPad = false;
185 | 19 | break;
186 |  | case '\#':
187 | 0 | token.alternative = true;
188 | 0 | break;
189 |  | default:
190 | 0 | throw Error('bad formatting flag \'' + token.flags.charAt(fi) + '\'');
191 |  | }
192 |  | }
193 |  |
194 | 2080 | token.minWidth = (token._minWidth) ? parseInt(token._minWidth) : 0;
195 | 2080 | token.maxWidth = -1;
196 | 2080 | token.toUpper = false;
197 | 2080 | token.isUnsigned = false;
198 | 2080 | token.isInt = false;
199 | 2080 | token.isDouble = false;
200 | 2080 | token.precision = 1;
201 | 2080 | if(token.period == '.'){
202 | 17 | if(token._precision){
203 | 16 | token.precision = parseInt(token._precision);
204 |  | }else{
205 | 1 | token.precision = 0;
206 |  | }
207 |  | }
208 |  |
209 | 2080 | var mixins = this._specifiers[token.specifier];
210 | 2080 | if(typeof mixins == 'undefined'){
211 | 0 | throw new Error('unexpected specifier \'' + token.specifier + '\'');
212 |  | }
213 | 2080 | if(mixins.extend){
214 | 2 | var s = this._specifiers[mixins.extend];
215 | 2 | for(var k in s){
216 | 4 | mixins[k] = s[k]
217 |  | }
218 | 2 | delete mixins.extend;
219 |  | }
220 | 2080 | for(var k in mixins){
221 | 3139 | token[k] = mixins[k];
222 |  | }
223 |  | }
224 |  |
225 | 4078 | if(typeof token.setArg == 'function'){
226 | 7 | token.setArg(token);
227 |  | }
228 |  |
229 | 4076 | if(typeof token.setMaxWidth == 'function'){
230 | 2013 | token.setMaxWidth(token);
231 |  | }
232 |  |
233 | 4076 | if(token._minWidth == '*'){
234 | 7 | if(this._mapped){
235 | 1 | throw new Error('* width not supported in mapped formats');
236 |  | }
237 | 6 | token.minWidth = parseInt(arguments[position++]);
238 | 6 | if(isNaN(token.minWidth)){
239 | 1 | throw new Error('the argument for * width at position ' + position + ' is not a number in ' + this._format);
240 |  | }
241 |  | // negative width means rightJustify
242 | 5 | if (token.minWidth < 0) {
243 | 0 | token.rightJustify = true;
244 | 0 | token.minWidth = -token.minWidth;
245 |  | }
246 |  | }
247 |  |
248 | 4074 | if(token._precision == '*' && token.period == '.'){
249 | 4 | if(this._mapped){
250 | 0 | throw new Error('* precision not supported in mapped formats');
251 |  | }
252 | 4 | token.precision = parseInt(arguments[position++]);
253 | 4 | if(isNaN(token.precision)){
254 | 0 | throw Error('the argument for * precision at position ' + position + ' is not a number in ' + this._format);
255 |  | }
256 |  | // negative precision means unspecified
257 | 4 | if (token.precision < 0) {
258 | 0 | token.precision = 1;
259 | 0 | token.period = '';
260 |  | }
261 |  | }
262 | 4074 | if(token.isInt){
263 |  | // a specified precision means no zero padding
264 | 2043 | if(token.period == '.'){
265 | 2 | token.zeroPad = false;
266 |  | }
267 | 2043 | this.formatInt(token);
268 | 2031 | }else if(token.isDouble){
269 | 15 | if(token.period != '.'){
270 | 0 | token.precision = 6;
271 |  | }
272 | 15 | this.formatDouble(token);
273 |  | }
274 | 4073 | this.fitField(token);
275 |  |
276 | 4073 | str += '' + token.arg;
277 |  | }
278 |  | }
279 |  |
280 | 2070 | return str;
281 |  | };
282 | 1 | Formatter.prototype._zeros10 = '0000000000';
283 | 1 | Formatter.prototype._spaces10 = ' ';
284 | 1 | Formatter.prototype.formatInt = function(token) {
285 | 2043 | var i = parseInt(token.arg);
286 | 2043 | if(!isFinite(i)){ // isNaN(f) || f == Number.POSITIVE_INFINITY || f == Number.NEGATIVE_INFINITY)
287 |  | // allow this only if arg is number
288 | 0 | if(typeof token.arg != 'number'){
289 | 0 | throw new Error('format argument \'' + token.arg + '\' not an integer; parseInt returned ' + i);
290 |  | }
291 |  | //return '' + i;
292 | 0 | i = 0;
293 |  | }
294 |  |
295 |  | // if not base 10, make negatives be positive
296 |  | // otherwise, (-10).toString(16) is '-a' instead of 'fffffff6'
297 | 2043 | if(i < 0 && (token.isUnsigned || token.base != 10)){
298 | 0 | i = 0xffffffff + i + 1;
299 |  | }
300 |  |
301 | 2043 | if(i < 0){
302 | 18 | token.arg = (- i).toString(token.base);
303 | 18 | this.zeroPad(token);
304 | 18 | token.arg = '-' + token.arg;
305 |  | }else{
306 | 2025 | token.arg = i.toString(token.base);
307 |  | // need to make sure that argument 0 with precision==0 is formatted as ''
308 | 2025 | if(!i && !token.precision){
309 | 0 | token.arg = '';
310 |  | }else{
311 | 2025 | this.zeroPad(token);
312 |  | }
313 | 2025 | if(token.sign){
314 | 6 | token.arg = token.sign + token.arg;
315 |  | }
316 |  | }
317 | 2043 | if(token.base == 16){
318 | 0 | if(token.alternative){
319 | 0 | token.arg = '0x' + token.arg;
320 |  | }
321 | 0 | token.arg = token.toUpper ? token.arg.toUpperCase() : token.arg.toLowerCase();
322 |  | }
323 | 2043 | if(token.base == 8){
324 | 0 | if(token.alternative && token.arg.charAt(0) != '0'){
325 | 0 | token.arg = '0' + token.arg;
326 |  | }
327 |  | }
328 |  | };
329 | 1 | Formatter.prototype.formatDouble = function(token) {
330 | 15 | var f = parseFloat(token.arg);
331 | 15 | if(!isFinite(f)){ // isNaN(f) || f == Number.POSITIVE_INFINITY || f == Number.NEGATIVE_INFINITY)
332 |  | // allow this only if arg is number
333 | 1 | if(typeof token.arg != 'number'){
334 | 1 | throw new Error('format argument \'' + token.arg + '\' not a float; parseFloat returned ' + f);
335 |  | }
336 |  | // C99 says that for 'f':
337 |  | // infinity -> '[-]inf' or '[-]infinity' ('[-]INF' or '[-]INFINITY' for 'F')
338 |  | // NaN -> a string starting with 'nan' ('NAN' for 'F')
339 |  | // this is not commonly implemented though.
340 |  | //return '' + f;
341 | 0 | f = 0;
342 |  | }
343 |  |
344 | 14 | switch(token.doubleNotation) {
345 |  | case 'e': {
346 | 0 | token.arg = f.toExponential(token.precision);
347 | 0 | break;
348 |  | }
349 |  | case 'f': {
350 | 14 | token.arg = f.toFixed(token.precision);
351 | 14 | break;
352 |  | }
353 |  | case 'g': {
354 |  | // C says use 'e' notation if exponent is < -4 or is >= prec
355 |  | // ECMAScript for toPrecision says use exponential notation if exponent is >= prec,
356 |  | // though step 17 of toPrecision indicates a test for < -6 to force exponential.
357 | 0 | if(Math.abs(f) < 0.0001){
358 |  | //print('forcing exponential notation for f=' + f);
359 | 0 | token.arg = f.toExponential(token.precision > 0 ? token.precision - 1 : token.precision);
360 |  | }else{
361 | 0 | token.arg = f.toPrecision(token.precision);
362 |  | }
363 |  |
364 |  | // In C, unlike 'f', 'gG' removes trailing 0s from fractional part, unless alternative format flag ('#').
365 |  | // But ECMAScript formats toPrecision as 0.00100000. So remove trailing 0s.
366 | 0 | if(!token.alternative){
367 |  | //print('replacing trailing 0 in \'' + s + '\'');
368 | 0 | token.arg = token.arg.replace(/(\..*[^0])0*/, '$1');
369 |  | // if fractional part is entirely 0, remove it and decimal point
370 | 0 | token.arg = token.arg.replace(/\.0*e/, 'e').replace(/\.0$/,'');
371 |  | }
372 | 0 | break;
373 |  | }
374 | 0 | default: throw new Error('unexpected double notation \'' + token.doubleNotation + '\'');
375 |  | }
376 |  |
377 |  | // C says that exponent must have at least two digits.
378 |  | // But ECMAScript does not; toExponential results in things like '1.000000e-8' and '1.000000e+8'.
379 |  | // Note that s.replace(/e([\+\-])(\d)/, 'e$10$2') won't work because of the '$10' instead of '$1'.
380 |  | // And replace(re, func) isn't supported on IE50 or Safari1.
381 | 14 | token.arg = token.arg.replace(/e\+(\d)$/, 'e+0$1').replace(/e\-(\d)$/, 'e-0$1');
382 |  |
383 |  | // if alt, ensure a decimal point
384 | 14 | if(token.alternative){
385 | 0 | token.arg = token.arg.replace(/^(\d+)$/,'$1.');
386 | 0 | token.arg = token.arg.replace(/^(\d+)e/,'$1.e');
387 |  | }
388 |  |
389 | 14 | if(f >= 0 && token.sign){
390 | 1 | token.arg = token.sign + token.arg;
391 |  | }
392 |  |
393 | 14 | token.arg = token.toUpper ? token.arg.toUpperCase() : token.arg.toLowerCase();
394 |  | };
395 | 1 | Formatter.prototype.zeroPad = function(token, /*Int*/ length) {
396 | 2051 | length = (arguments.length == 2) ? length : token.precision;
397 | 2051 | var negative = false;
398 | 2051 | if(typeof token.arg != "string"){
399 | 0 | token.arg = "" + token.arg;
400 |  | }
401 | 2051 | if (token.arg.substr(0,1) === '-') {
402 | 3 | negative = true;
403 | 3 | token.arg = token.arg.substr(1);
404 |  | }
405 |  |
406 | 2051 | var tenless = length - 10;
407 | 2051 | while(token.arg.length < tenless){
408 | 2 | token.arg = (token.rightJustify) ? token.arg + this._zeros10 : this._zeros10 + token.arg;
409 |  | }
410 | 2051 | var pad = length - token.arg.length;
411 | 2051 | token.arg = (token.rightJustify) ? token.arg + this._zeros10.substring(0, pad) : this._zeros10.substring(0, pad) + token.arg;
412 | 2054 | if (negative) token.arg = '-' + token.arg;
413 |  | };
414 | 1 | Formatter.prototype.fitField = function(token) {
415 | 4073 | if(token.maxWidth >= 0 && token.arg.length > token.maxWidth){
416 | 0 | return token.arg.substring(0, token.maxWidth);
417 |  | }
418 | 4073 | if(token.zeroPad){
419 | 8 | this.zeroPad(token, token.minWidth);
420 | 8 | return;
421 |  | }
422 | 4065 | this.spacePad(token);
423 |  | };
424 | 1 | Formatter.prototype.spacePad = function(token, /*Int*/ length) {
425 | 4065 | length = (arguments.length == 2) ? length : token.minWidth;
426 | 4065 | if(typeof token.arg != 'string'){
427 | 1 | token.arg = '' + token.arg;
428 |  | }
429 | 4065 | var tenless = length - 10;
430 | 4065 | while(token.arg.length < tenless){
431 | 10 | token.arg = (token.rightJustify) ? token.arg + this._spaces10 : this._spaces10 + token.arg;
432 |  | }
433 | 4065 | var pad = length - token.arg.length;
434 | 4065 | token.arg = (token.rightJustify) ? token.arg + this._spaces10.substring(0, pad) : this._spaces10.substring(0, pad) + token.arg;
435 |  | };
436 |  |
437 |  |
438 | 1 | module.exports = function(){
439 | 1080 | var args = Array.prototype.slice.call(arguments),
440 |  | stream, format;
441 | 1080 | if(args[0] instanceof require('stream').Stream){
442 | 0 | stream = args.shift();
443 |  | }
444 | 1080 | format = args.shift();
445 | 1080 | var formatter = new Formatter(format);
446 | 1080 | var string = formatter.format.apply(formatter, args);
447 | 1070 | if(stream){
448 | 0 | stream.write(string);
449 |  | }else{
450 | 1070 | return string;
451 |  | }
452 |  | };
453 |  |
454 | 1 | module.exports.Formatter = Formatter;
455 |  |