Coverage

82%
226
186
40

printf.js

82%
226
186
40
LineHitsSource
1
21var util = require('util');
3
41var 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
121081 var tokens = [];
131081 var match, content, lastIndex = 0;
141081 while(match = re.exec(str)){
152086 content = str.slice(lastIndex, re.lastIndex - match[0].length);
162086 if(content.length){
171011 tokens.push(content);
18 }
192086 if(parseDelim){
202086 var parsed = parseDelim.apply(instance, match.slice(1).concat(tokens.length));
212086 if(typeof parsed != 'undefined'){
222086 if(parsed.specifier === '%'){
230 tokens.push('%');
24 }else{
252086 tokens.push(parsed);
26 }
27 }
28 }
292086 lastIndex = re.lastIndex;
30 }
311081 content = str.slice(lastIndex);
321081 if(content.length){
331010 tokens.push(content);
34 }
351081 return tokens;
36}
37
381var Formatter = function(/*String*/ format){
391081 var tokens = [];
401081 this._mapped = false;
411081 this._format = format;
421081 this._tokens = tokenize(format, this._re, this._parseDelim, this);
43}
44
451Formatter.prototype._re = /\%(?:\(([\w_]+)\)|([1-9]\d*)\$)?([0 +\-\#]*)(\*|\d+)?(\.)?(\*|\d+)?[hlL]?([\%bscdeEfFgGioOuxX])/g;
461Formatter.prototype._parseDelim = function(mapping, intmapping, flags, minWidth, period, precision, specifier){
472086 if(mapping){
489 this._mapped = true;
49 }
502086 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};
601Formatter.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){
906 if(!isNaN(token.arg)){
915 var num = parseInt(token.arg);
925 if(num < 0 || num > 127){
932 throw new Error('invalid character code passed to %c in printf');
94 }
953 token.arg = isNaN(num) ? '' + num : String.fromCharCode(num);
96 }
97 }
98 },
99 s: {
100 setMaxWidth: function(token){
1012013 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){
1291 token.arg = util.inspect(token.arg, true, null);
130 }
131 },
132};
1331Formatter.prototype.format = function(/*mixed...*/ filler){
1342080 if(this._mapped && typeof filler != 'object'){
1352 throw new Error('format requires a mapping');
136 }
137
1382078 var str = '';
1392078 var position = 0;
1402078 for(var i = 0, token; i < this._tokens.length; i++){
1418099 token = this._tokens[i];
142
1438099 if(typeof token == 'string'){
1444018 str += token;
145 }else{
1464081 if(this._mapped){
1476 if(typeof filler[token.mapping] == 'undefined'){
1480 throw new Error('missing key ' + token.mapping);
149 }
1506 token.arg = filler[token.mapping];
151 }else{
1524075 if(token.intmapping){
1537 var position = parseInt(token.intmapping) - 1;
154 }
1554075 if(position >= arguments.length){
1563 throw new Error('got ' + arguments.length + ' printf arguments, insufficient for \'' + this._format + '\'');
157 }
1584072 token.arg = arguments[position++];
159 }
160
1614078 if(!token.compiled){
1622080 token.compiled = true;
1632080 token.sign = '';
1642080 token.zeroPad = false;
1652080 token.rightJustify = false;
1662080 token.alternative = false;
167
1682080 var flags = {};
1692080 for(var fi = token.flags.length; fi--;){
17052 var flag = token.flags.charAt(fi);
17152 flags[flag] = true;
17252 switch(flag){
173 case ' ':
1746 token.sign = ' ';
1756 break;
176 case '+':
1777 token.sign = '+';
1787 break;
179 case '0':
18020 token.zeroPad = (flags['-']) ? false : true;
18120 break;
182 case '-':
18319 token.rightJustify = true;
18419 token.zeroPad = false;
18519 break;
186 case '\#':
1870 token.alternative = true;
1880 break;
189 default:
1900 throw Error('bad formatting flag \'' + token.flags.charAt(fi) + '\'');
191 }
192 }
193
1942080 token.minWidth = (token._minWidth) ? parseInt(token._minWidth) : 0;
1952080 token.maxWidth = -1;
1962080 token.toUpper = false;
1972080 token.isUnsigned = false;
1982080 token.isInt = false;
1992080 token.isDouble = false;
2002080 token.precision = 1;
2012080 if(token.period == '.'){
20217 if(token._precision){
20316 token.precision = parseInt(token._precision);
204 }else{
2051 token.precision = 0;
206 }
207 }
208
2092080 var mixins = this._specifiers[token.specifier];
2102080 if(typeof mixins == 'undefined'){
2110 throw new Error('unexpected specifier \'' + token.specifier + '\'');
212 }
2132080 if(mixins.extend){
2142 var s = this._specifiers[mixins.extend];
2152 for(var k in s){
2164 mixins[k] = s[k]
217 }
2182 delete mixins.extend;
219 }
2202080 for(var k in mixins){
2213139 token[k] = mixins[k];
222 }
223 }
224
2254078 if(typeof token.setArg == 'function'){
2267 token.setArg(token);
227 }
228
2294076 if(typeof token.setMaxWidth == 'function'){
2302013 token.setMaxWidth(token);
231 }
232
2334076 if(token._minWidth == '*'){
2347 if(this._mapped){
2351 throw new Error('* width not supported in mapped formats');
236 }
2376 token.minWidth = parseInt(arguments[position++]);
2386 if(isNaN(token.minWidth)){
2391 throw new Error('the argument for * width at position ' + position + ' is not a number in ' + this._format);
240 }
241 // negative width means rightJustify
2425 if (token.minWidth < 0) {
2430 token.rightJustify = true;
2440 token.minWidth = -token.minWidth;
245 }
246 }
247
2484074 if(token._precision == '*' && token.period == '.'){
2494 if(this._mapped){
2500 throw new Error('* precision not supported in mapped formats');
251 }
2524 token.precision = parseInt(arguments[position++]);
2534 if(isNaN(token.precision)){
2540 throw Error('the argument for * precision at position ' + position + ' is not a number in ' + this._format);
255 }
256 // negative precision means unspecified
2574 if (token.precision < 0) {
2580 token.precision = 1;
2590 token.period = '';
260 }
261 }
2624074 if(token.isInt){
263 // a specified precision means no zero padding
2642043 if(token.period == '.'){
2652 token.zeroPad = false;
266 }
2672043 this.formatInt(token);
2682031 }else if(token.isDouble){
26915 if(token.period != '.'){
2700 token.precision = 6;
271 }
27215 this.formatDouble(token);
273 }
2744073 this.fitField(token);
275
2764073 str += '' + token.arg;
277 }
278 }
279
2802070 return str;
281};
2821Formatter.prototype._zeros10 = '0000000000';
2831Formatter.prototype._spaces10 = ' ';
2841Formatter.prototype.formatInt = function(token) {
2852043 var i = parseInt(token.arg);
2862043 if(!isFinite(i)){ // isNaN(f) || f == Number.POSITIVE_INFINITY || f == Number.NEGATIVE_INFINITY)
287 // allow this only if arg is number
2880 if(typeof token.arg != 'number'){
2890 throw new Error('format argument \'' + token.arg + '\' not an integer; parseInt returned ' + i);
290 }
291 //return '' + i;
2920 i = 0;
293 }
294
295 // if not base 10, make negatives be positive
296 // otherwise, (-10).toString(16) is '-a' instead of 'fffffff6'
2972043 if(i < 0 && (token.isUnsigned || token.base != 10)){
2980 i = 0xffffffff + i + 1;
299 }
300
3012043 if(i < 0){
30218 token.arg = (- i).toString(token.base);
30318 this.zeroPad(token);
30418 token.arg = '-' + token.arg;
305 }else{
3062025 token.arg = i.toString(token.base);
307 // need to make sure that argument 0 with precision==0 is formatted as ''
3082025 if(!i && !token.precision){
3090 token.arg = '';
310 }else{
3112025 this.zeroPad(token);
312 }
3132025 if(token.sign){
3146 token.arg = token.sign + token.arg;
315 }
316 }
3172043 if(token.base == 16){
3180 if(token.alternative){
3190 token.arg = '0x' + token.arg;
320 }
3210 token.arg = token.toUpper ? token.arg.toUpperCase() : token.arg.toLowerCase();
322 }
3232043 if(token.base == 8){
3240 if(token.alternative && token.arg.charAt(0) != '0'){
3250 token.arg = '0' + token.arg;
326 }
327 }
328};
3291Formatter.prototype.formatDouble = function(token) {
33015 var f = parseFloat(token.arg);
33115 if(!isFinite(f)){ // isNaN(f) || f == Number.POSITIVE_INFINITY || f == Number.NEGATIVE_INFINITY)
332 // allow this only if arg is number
3331 if(typeof token.arg != 'number'){
3341 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;
3410 f = 0;
342 }
343
34414 switch(token.doubleNotation) {
345 case 'e': {
3460 token.arg = f.toExponential(token.precision);
3470 break;
348 }
349 case 'f': {
35014 token.arg = f.toFixed(token.precision);
35114 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.
3570 if(Math.abs(f) < 0.0001){
358 //print('forcing exponential notation for f=' + f);
3590 token.arg = f.toExponential(token.precision > 0 ? token.precision - 1 : token.precision);
360 }else{
3610 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.
3660 if(!token.alternative){
367 //print('replacing trailing 0 in \'' + s + '\'');
3680 token.arg = token.arg.replace(/(\..*[^0])0*/, '$1');
369 // if fractional part is entirely 0, remove it and decimal point
3700 token.arg = token.arg.replace(/\.0*e/, 'e').replace(/\.0$/,'');
371 }
3720 break;
373 }
3740 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.
38114 token.arg = token.arg.replace(/e\+(\d)$/, 'e+0$1').replace(/e\-(\d)$/, 'e-0$1');
382
383 // if alt, ensure a decimal point
38414 if(token.alternative){
3850 token.arg = token.arg.replace(/^(\d+)$/,'$1.');
3860 token.arg = token.arg.replace(/^(\d+)e/,'$1.e');
387 }
388
38914 if(f >= 0 && token.sign){
3901 token.arg = token.sign + token.arg;
391 }
392
39314 token.arg = token.toUpper ? token.arg.toUpperCase() : token.arg.toLowerCase();
394};
3951Formatter.prototype.zeroPad = function(token, /*Int*/ length) {
3962051 length = (arguments.length == 2) ? length : token.precision;
3972051 var negative = false;
3982051 if(typeof token.arg != "string"){
3990 token.arg = "" + token.arg;
400 }
4012051 if (token.arg.substr(0,1) === '-') {
4023 negative = true;
4033 token.arg = token.arg.substr(1);
404 }
405
4062051 var tenless = length - 10;
4072051 while(token.arg.length < tenless){
4082 token.arg = (token.rightJustify) ? token.arg + this._zeros10 : this._zeros10 + token.arg;
409 }
4102051 var pad = length - token.arg.length;
4112051 token.arg = (token.rightJustify) ? token.arg + this._zeros10.substring(0, pad) : this._zeros10.substring(0, pad) + token.arg;
4122054 if (negative) token.arg = '-' + token.arg;
413};
4141Formatter.prototype.fitField = function(token) {
4154073 if(token.maxWidth >= 0 && token.arg.length > token.maxWidth){
4160 return token.arg.substring(0, token.maxWidth);
417 }
4184073 if(token.zeroPad){
4198 this.zeroPad(token, token.minWidth);
4208 return;
421 }
4224065 this.spacePad(token);
423};
4241Formatter.prototype.spacePad = function(token, /*Int*/ length) {
4254065 length = (arguments.length == 2) ? length : token.minWidth;
4264065 if(typeof token.arg != 'string'){
4271 token.arg = '' + token.arg;
428 }
4294065 var tenless = length - 10;
4304065 while(token.arg.length < tenless){
43110 token.arg = (token.rightJustify) ? token.arg + this._spaces10 : this._spaces10 + token.arg;
432 }
4334065 var pad = length - token.arg.length;
4344065 token.arg = (token.rightJustify) ? token.arg + this._spaces10.substring(0, pad) : this._spaces10.substring(0, pad) + token.arg;
435};
436
437
4381module.exports = function(){
4391080 var args = Array.prototype.slice.call(arguments),
440 stream, format;
4411080 if(args[0] instanceof require('stream').Stream){
4420 stream = args.shift();
443 }
4441080 format = args.shift();
4451080 var formatter = new Formatter(format);
4461080 var string = formatter.format.apply(formatter, args);
4471070 if(stream){
4480 stream.write(string);
449 }else{
4501070 return string;
451 }
452};
453
4541module.exports.Formatter = Formatter;
455