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 }