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 }