1 module des.stdx.pformat;
2
3 import std.traits;
4 import std.array;
5 import std.range;
6 import std.math;
7 import std.algorithm;
8 import std.exception;
9
10 import std.stdio;
11
12 import des.ts;
13
14 ///
15 enum PlusSig
16 {
17 NONE, ///
18 SPACE, ///
19 PLUS ///
20 }
21
22 ///
23 string intToStr(T)( in T val,
24 int width=0,
25 PlusSig plus_sig=PlusSig.NONE,
26 int base=10,
27 char fill_char=' ' ) pure nothrow
28 if( isIntegral!T )
29 in {
30 assert( val != T.min );
31 assert( base > 0, "base must be > 0" );
32 assert( base <= 16, "base must be <= 16" );
33 }
34 body {
35 enum tbl = [ 0:'0', 1:'1', 2:'2', 3:'3',
36 4:'4', 5:'5', 6:'6', 7:'7',
37 8:'8', 9:'9', 10:'A', 11:'B',
38 12:'C', 13:'D', 14:'E', 15:'F' ];
39
40 auto positive = val >= 0;
41 T value = positive ? val : -val;
42
43 string ret;
44 do
45 {
46 ret = tbl[value%base] ~ ret;
47 value /= base;
48 }
49 while( value > 0 );
50
51 return fmtNumericStr( ret, positive, width, plus_sig, fill_char );
52 }
53
54 ///
55 unittest
56 {
57 assertEq( intToStr(0), "0" );
58 assertEq( intToStr(123), "123" );
59 assertEq( intToStr(-16), "-16" );
60 assertEq( intToStr(-16, 5), " -16" );
61
62 assertEq( intToStr(16, 5, PlusSig.NONE), " 16" );
63 assertEq( intToStr(16, 5, PlusSig.SPACE), " 16" );
64 assertEq( intToStr(16, 5, PlusSig.PLUS), " +16" );
65
66 assertEq( intToStr(16, 0, PlusSig.NONE), "16" );
67 assertEq( intToStr(16, 0, PlusSig.SPACE), " 16" );
68 assertEq( intToStr(16, 0, PlusSig.PLUS), "+16" );
69
70 assertEq( intToStr(1234567, 5), "1234567" );
71 assertEq( intToStr(1234567, 5, PlusSig.PLUS), "+1234567" );
72 assertEq( intToStr(1234567, -5, PlusSig.PLUS), "+1234567" );
73 assertEq( intToStr(1234567, -5, PlusSig.SPACE), " 1234567" );
74
75 assertEq( intToStr(16, 5, PlusSig.NONE, 10, 'x'), "xxx16" );
76 assertEq( intToStr(0, 10, PlusSig.NONE, 10, '0'), "0000000000" );
77
78 assertEq( intToStr(3, 4, PlusSig.NONE, 2, '0'), "0011" );
79 assertEq( intToStr(3, 4, PlusSig.NONE, 8, '0'), "0003" );
80 assertEq( intToStr(9, 4, PlusSig.NONE, 8, '0'), "0011" );
81
82 assertEq( intToStr(255, 3, PlusSig.NONE, 16 ), " FF" );
83 assertEq( intToStr(256, 3, PlusSig.NONE, 16 ), "100" );
84 }
85
86 ///
87 string floatToStr(T)( in T val,
88 int width=0,
89 int after_point=6,
90 bool remove_trailing_zeros=true,
91 PlusSig plus_sig=PlusSig.NONE,
92 char fill_char=' ' ) pure nothrow
93 if( isNumeric!T )
94 in{ assert( after_point >= 0 ); } body
95 {
96 string ret = testFinite( val );
97 auto positive = isPositive( val );
98
99 if( ret.length == 0 )
100 {
101 auto apk = 10 ^^ after_point;
102 auto int_value = cast(long)( (positive ? val : -val) * apk );
103 ret = intToStr( int_value, after_point, PlusSig.NONE, 10, '0' );
104
105 ret = ret[0..($-after_point)] ~ '.' ~ ret[($-after_point)..$];
106
107 if( remove_trailing_zeros )
108 while( ret[$-1] == '0' )
109 {
110 if( ret[$-2] == '.' ) break;
111 ret = ret[0..$-1];
112 }
113 }
114
115 return fmtNumericStr( ret, positive, width, plus_sig, fill_char );
116 }
117
118 ///
119 unittest
120 {
121 assertEq( floatToStr(0), ".0" );
122 assertEq( floatToStr( 3.1415 ), "3.1415" );
123 assertEq( floatToStr( -3.1415 ), "-3.1415" );
124 assertEq( floatToStr( -3.1415, 6, 2 ), " -3.14" );
125 assertEq( floatToStr( 3.1415, 6, 2 ), " 3.14" );
126 assertEq( floatToStr( 3.1415, 6, 2, true, PlusSig.PLUS ), " +3.14" );
127 assertEq( floatToStr( 128, 6, 2, true, PlusSig.PLUS ), "+128.0" );
128 assertEq( floatToStr( 1286, 6, 2, true, PlusSig.PLUS ), "+1286.0" );
129 assertEq( floatToStr( 1286, 6, 0 ), " 1286." );
130 assertEq( floatToStr( 3.1415, 12, 8, false ), " 3.14150000" );
131 assertEq( floatToStr( -3.1415, 12, 8, false ), " -3.14150000" );
132 assertEq( floatToStr(float.nan), "nan" );
133 assertEq( floatToStr(-float.nan), "-nan" );
134 assertEq( floatToStr(float.infinity), "inf" );
135 assertEq( floatToStr(-float.infinity), "-inf" );
136
137 assertEq( floatToStr( 3.1415, 0, 2, true, PlusSig.NONE ), "3.14" );
138 assertEq( floatToStr( 3.1415, 0, 3, true, PlusSig.SPACE ), " 3.141" );
139 assertEq( floatToStr( 3.1415, 0, 2, true, PlusSig.PLUS ), "+3.14" );
140 }
141
142 unittest
143 {
144 assertEq( floatToStr(double.nan), "nan" );
145 assertEq( floatToStr(-double.nan), "-nan" );
146 assertEq( floatToStr(double.infinity), "inf" );
147 assertEq( floatToStr(-double.infinity), "-inf" );
148 assertEq( floatToStr(real.nan), "nan" );
149 assertEq( floatToStr(-real.nan), "-nan" );
150 assertEq( floatToStr(real.infinity), "inf" );
151 assertEq( floatToStr(-real.infinity), "-inf" );
152 }
153
154 ///
155 string floatToStrSci(T)( in T val,
156 int width=0,
157 int after_point=6,
158 PlusSig plus_sig=PlusSig.NONE,
159 char fill_char=' ' )
160 if( isNumeric!T )
161 in{ assert( after_point >= 0 ); } body
162 {
163 string ret = testFinite( val );
164 auto positive = isPositive( val );
165
166 if( ret.length == 0 )
167 {
168 auto positive_val = positive ? val : -val;
169
170 int exponent = val != 0 ? cast(int)(floor(log10(positive_val))) : 0;
171
172 auto exp_positive = exponent >= 0;
173
174 auto apk = 10 ^^ after_point / pow( 10.0, exponent );
175
176 auto int_value = cast(long)( positive_val * apk );
177 ret = intToStr( int_value, after_point+1, PlusSig.NONE, 10, '0' );
178 auto exp_str = intToStr( abs(exponent), 2, PlusSig.NONE, 10, '0' );
179
180 ret = ret[0..1] ~ '.' ~ ret[1..$] ~
181 "e" ~ ( exp_positive ? "+" : "-" ) ~ exp_str;
182 }
183
184 return fmtNumericStr( ret, positive, width, plus_sig, fill_char );
185 }
186
187 ///
188 unittest
189 {
190 assertEq( floatToStrSci( 0 ), "0.000000e+00" );
191 assertEq( floatToStrSci( 3.1415 ), "3.141500e+00" );
192 assertEq( floatToStrSci( 314.15 ), "3.141500e+02" );
193 assertEq( floatToStrSci( 0.0314 ), "3.139999e-02" );
194 assertEq( floatToStrSci( 3.14159e-8 ), "3.141590e-08" );
195 assertEq( floatToStrSci( 3.1415e11 ), "3.141500e+11" );
196 assertEq( floatToStrSci( 3.1415e11, 10, 3, PlusSig.PLUS ), "+3.141e+11" );
197
198 import std..string : format;
199
200 foreach( i; 0 .. 1000 )
201 {
202 auto v = .001 * i;
203 assertEqApprox( to!double( floatToStrSci(v) ), v, .001,
204 format( "%%s != %%s with i: %s", i ) );
205 }
206
207 assertEq( floatToStrSci( 3.141592, 12, 5, PlusSig.SPACE ), " 3.14159e+00" );
208 }
209
210 private string getPlusStr( PlusSig ps ) pure nothrow
211 {
212 final switch( ps )
213 {
214 case PlusSig.NONE: return "";
215 case PlusSig.SPACE: return " ";
216 case PlusSig.PLUS: return "+";
217 }
218 }
219
220 private string fmtNumericStr( string str,
221 bool positive,
222 int width,
223 PlusSig plus_sig,
224 char fill_char ) pure nothrow
225 {
226 auto ret = ( positive ? getPlusStr( plus_sig ) : "-" ) ~ str;
227 return fmtWidthStr( ret, width, fill_char );
228 }
229
230 private string fmtWidthStr( string str, int width, char fill_char ) pure nothrow
231 {
232 auto spaces = width - cast(int)str.length;
233 if( spaces <= 0 ) return str;
234 else return array( fill_char.repeat().take( spaces ) ).idup ~ str;
235 }
236
237 private string testFinite(T)( in T val ) pure nothrow
238 if( isNumeric!T )
239 {
240 static if( isFloatingPoint!T )
241 {
242 if( fabs(val) is T.nan ) return "nan";
243 if( fabs(val) is T.infinity ) return "inf";
244 }
245 return "";
246 }
247
248 private bool isPositive(T)( in T val ) pure nothrow
249 if( isNumeric!T )
250 {
251 static if( isFloatingPoint!T )
252 return signbit(val) == 0;
253 else return val >= 0;
254 }
255
256 /++++++++++++ pFormat ++++++++++++/
257
258 class PFormatException : Exception
259 {
260 this( string msg, string file=__FILE__, size_t line=__LINE__ ) pure @safe nothrow
261 { super( msg, file, line ); }
262 }
263
264 struct PFormatArg
265 {
266 long index=0;
267
268 int width=0;
269 int after_point=6;
270 PlusSig plus_sig=PlusSig.NONE;
271 char fill_char=' ';
272
273 enum Type
274 {
275 ERR, /// error type
276 ORI, /// original (part of format string)
277 UNI, /// universal %s
278 BIN, /// integer by base 2 %b
279 OCT, /// integer by base 8 %o
280 DEC, /// integer by base 10 %d
281 HEX, /// integer by base 16 %x
282 FLO, /// floating %f
283 SCI /// scientific floating %e
284 }
285
286 Type type=Type.ERR;
287
288 string fmt;
289 string result;
290 }
291
292 private PFormatArg[] parseFormatString( string fmt ) pure
293 {
294 PFormatArg[] ret;
295
296 int arg_index = 1;
297
298 while( !fmt.empty )
299 {
300 if( isStartFormatChar( fmt.front ) )
301 ret ~= parseFormatPart( fmt, arg_index );
302 else
303 ret ~= parseStringPart( fmt );
304 }
305
306 return ret;
307 }
308
309 private auto getAndPopFront(R)( ref R r ) @property if( isInputRange!R )
310 {
311 auto ret = r.front;
312 r.popFront();
313 return ret;
314 }
315
316 private PFormatArg parseStringPart( ref string fmt ) pure
317 {
318 PFormatArg arg;
319 arg.index = 0;
320 arg.type = PFormatArg.Type.ORI;
321
322 while( !( fmt.empty || isStartFormatChar( fmt.front ) ) )
323 arg.fmt ~= fmt.getAndPopFront();
324
325 arg.result = arg.fmt;
326 return arg;
327 }
328
329 private PFormatArg parseFormatPart( ref string fmt, ref int serial_index ) pure
330 in{ assert( fmt.front == '%' ); } body
331 {
332 PFormatArg arg;
333 arg.index = 0;
334
335 auto fmtGPFCE( string place )
336 {
337 scope(exit) if( fmt.empty ) throw new PFormatException( "format not complite: '" ~ arg.fmt ~ "' on " ~ place );
338 return fmt.getAndPopFront;
339 }
340
341 int parseNumber()
342 {
343 int n = 1;
344 int val = 0;
345 while( isNumericChar( fmt.front ) )
346 {
347 val = val * n + charToInt( fmt.front );
348 n *= 10;
349 arg.fmt ~= fmtGPFCE( "parse number" );
350 }
351 return val;
352 }
353
354 arg.fmt ~= fmtGPFCE( "get first format char" ); // first % symbol
355
356 // double % processing (%%)
357 if( isStartFormatChar( fmt.front ) )
358 {
359 arg.fmt ~= fmt.getAndPopFront;
360 arg.type = PFormatArg.Type.ORI;
361 arg.result = arg.fmt;
362 return arg;
363 }
364
365 // if option index is finded it will be reseted
366 if( isPlusSigChar( fmt.front ) )
367 {
368 arg.plus_sig = getPlusSig( fmt.front );
369 arg.fmt ~= fmtGPFCE( "first check sig char" );
370 }
371
372 int width = 0, after_point = -1, option_index;
373
374 if( isNumericChar( fmt.front ) ) width = parseNumber();
375
376 if( isOptionIndexChar( fmt.front ) )
377 {
378 option_index = width;
379 width = 0;
380 arg.fmt ~= fmtGPFCE( "option index char" );
381 arg.plus_sig = PlusSig.NONE;
382 }
383
384 // was reseted if option index
385 if( isPlusSigChar( fmt.front ) )
386 {
387 arg.plus_sig = getPlusSig( fmt.front );
388 arg.fmt ~= fmtGPFCE( "second check sig char" );
389 }
390
391 if( isNumericChar( fmt.front ) ) width = parseNumber();
392
393 if( fmt.front == '.' )
394 {
395 // after point number parse, expect only floating point format
396 arg.fmt ~= fmtGPFCE( "check dot char" );
397 if( isNumericChar( fmt.front ) ) after_point = parseNumber();
398 else throw new PFormatException( "after dot must be numbers" );
399 }
400
401 auto ftype = getFormatType( fmt.front );
402
403 if( ftype == PFormatArg.Type.ERR ) throw new PFormatException( "bad format" );
404
405 arg.fmt ~= fmt.getAndPopFront();
406
407 if( after_point != -1 && !( ftype == PFormatArg.Type.FLO || ftype == PFormatArg.Type.SCI ) )
408 throw new PFormatException( "dot in format specify only to floating point formating" );
409
410 arg.type = ftype;
411 arg.width = width;
412
413 if( after_point != -1 ) arg.after_point = after_point;
414
415 if( option_index != 0 )
416 arg.index = option_index;
417 else
418 {
419 arg.index = serial_index;
420 serial_index++;
421 }
422
423 return arg;
424 }
425
426 private
427 {
428 bool isStartFormatChar( dchar ch ) pure { return ch == '%'; }
429 bool isOptionIndexChar( dchar ch ) pure { return ch == '$'; }
430
431 PFormatArg.Type getFormatType( dchar ch ) pure
432 {
433 switch( ch )
434 {
435 case 's': return PFormatArg.Type.UNI;
436 case 'b': return PFormatArg.Type.BIN;
437 case 'o': return PFormatArg.Type.OCT;
438 case 'd': return PFormatArg.Type.DEC;
439 case 'x': return PFormatArg.Type.HEX;
440 case 'f': return PFormatArg.Type.FLO;
441 case 'e': return PFormatArg.Type.SCI;
442 default: return PFormatArg.Type.ERR;
443 }
444 }
445
446 bool isPlusSigChar( dchar ch ) pure { return ch == ' ' || ch == '+'; }
447
448 PlusSig getPlusSig( dchar ch ) pure
449 {
450 if( ch == ' ' ) return PlusSig.SPACE;
451 if( ch == '+' ) return PlusSig.PLUS;
452 assert( 0, "char isn't plus sig char" );
453 }
454
455 bool isNumericChar( dchar ch ) pure
456 { return '0' <= ch && ch <= '9'; }
457
458 int charToInt( dchar ch ) pure
459 out(n) { assert( 0 <= n && n <= 9 ); } body
460 { return ch - '0'; }
461 }
462
463 ///
464 string pFormat(Args...)( string fmt, Args args ) pure
465 {
466 auto fmt_args = parseFormatString( fmt );
467 fillArgsResult!0( fmt_args, args );
468 return fmt_args.map!(a=>a.result).join();
469 }
470
471 ///
472 unittest
473 {
474 assertEq( pFormat( "%4d", 10 ), " 10" );
475
476 assertEq( pFormat( "hello %6.4f world %3d ok", 3.141592, 12 ), "hello 3.1415 world 12 ok" );
477 assertEq( pFormat( "hello % 6.3f world %d ok", 3.141592, 12 ), "hello 3.141 world 12 ok" );
478 assertEq( pFormat( "hello % 6.3f world % d ok", -3.141592, 12 ), "hello -3.141 world 12 ok" );
479 assertEq( pFormat( "hello % 6.3f world % 4d ok", -3.141592, 12 ), "hello -3.141 world 12 ok" );
480 assertEq( pFormat( "hello % 6.3f world % d ok", -3.141592, -12 ), "hello -3.141 world -12 ok" );
481 assertEq( pFormat( "hello %+6.3f world %+d ok", 3.141592, 12 ), "hello +3.141 world +12 ok" );
482
483 assertEq( pFormat( "hello %+13.5e world 0b%b ok", 3.141592, 8 ), "hello +3.14159e+00 world 0b1000 ok" );
484 assertEq( pFormat( "%10s %s", "hello", "world" ), " hello world" );
485
486 assertEq( pFormat( "%2$10s %1$s", "hello", "world" ), " world hello" );
487 assertEq( pFormat( "%1$10s %1$s", "hello" ), " hello hello" );
488 assertEq( pFormat( "%1$10s %1$s %3$ 6.3f %2$d", "hello", 14, 2.718281828 ), " hello hello 2.718 14" );
489
490 // fmt args without option index starts with first from any place
491 assertEq( pFormat( "%1$10s %1$s %3$ 6.3f %s %d", "hello", 14, 2.718281828 ), " hello hello 2.718 hello 14" );
492 }
493
494 private void fillArgsResult(int index,TT...)( PFormatArg[] pfalist, TT arglist ) if( TT.length > 0 )
495 {
496 static if( TT.length > 1 )
497 {
498 fillArgsResult!index( pfalist, arglist[0] );
499 fillArgsResult!(index+1)( pfalist, arglist[1..$] );
500 }
501 else
502 {
503 auto fl = find!"(a.index-1)==b"( pfalist, index );
504
505 if( fl.empty ) throw new PFormatException( "no fmt in str for arg #" ~ intToStr(index) );
506
507 do
508 {
509 fl.front.result = procArg( arglist[0], fl.front );
510 fl.popFront();
511 fl = find!"(a.index-1)==b"( fl, index );
512 }
513 while( !fl.empty );
514 }
515 }
516
517 private string procArg(T)( T arg, in PFormatArg e )
518 {
519 static string i2s( T val, in PFormatArg pfa, int base )
520 {
521 static if( !isIntegral!T ) throw new PFormatException( "bad call i2s with type " ~ T.stringof );
522 else return intToStr( val, pfa.width, pfa.plus_sig, base, pfa.fill_char );
523 }
524
525 static string f2s( T val, in PFormatArg pfa )
526 {
527 static if( !isNumeric!T ) throw new PFormatException( "bad call f2s with type " ~ T.stringof );
528 else return floatToStr( val, pfa.width, pfa.after_point, true, pfa.plus_sig, pfa.fill_char );
529 }
530
531 static string f2ss( T val, in PFormatArg pfa )
532 {
533 static if( !isNumeric!T ) throw new PFormatException( "bad call f2s with type " ~ T.stringof );
534 else return floatToStrSci( val, pfa.width, pfa.after_point, pfa.plus_sig, pfa.fill_char );
535 }
536
537 static string any2s( T val, in PFormatArg pfa )
538 {
539 static if( !is( T == string ) )
540 {
541 /+ TODO
542 + TODO
543 + TODO
544 +/
545 assert( 0, "no implement for type '" ~ T.stringof ~ "'" );
546 }
547 else return fmtWidthStr( val, pfa.width, pfa.fill_char );
548 }
549
550 final switch( e.type )
551 {
552 case PFormatArg.Type.ORI: return e.result;
553 case PFormatArg.Type.BIN: return i2s( arg, e, 2 );
554 case PFormatArg.Type.OCT: return i2s( arg, e, 8 );
555 case PFormatArg.Type.DEC: return i2s( arg, e, 10 );
556 case PFormatArg.Type.HEX: return i2s( arg, e, 16 );
557 case PFormatArg.Type.FLO: return f2s( arg, e );
558 case PFormatArg.Type.SCI: return f2ss( arg, e );
559 case PFormatArg.Type.UNI: return any2s( arg, e );
560
561 case PFormatArg.Type.ERR: assert( 0, "error type" );
562 }
563 }