1 // Written in the D programming language. 2 3 /** 4 This is a submodule of $(MREF std, format). 5 6 It centers around a struct called $(LREF FormatSpec), which takes a 7 $(MREF_ALTTEXT format string, std,format) and provides tools for 8 parsing this string. Additionally this module contains a function 9 $(LREF singleSpec) which helps treating a single format specifier. 10 11 Copyright: Copyright The D Language Foundation 2000-2013. 12 13 License: $(HTTP boost.org/LICENSE_1_0.txt, Boost License 1.0). 14 15 Authors: $(HTTP walterbright.com, Walter Bright), $(HTTP erdani.com, 16 Andrei Alexandrescu), and Kenji Hara 17 18 Source: $(PHOBOSSRC std/format/spec.d) 19 */ 20 module std.format.spec; 21 22 import std.traits : Unqual; 23 24 template FormatSpec(Char) 25 if (!is(Unqual!Char == Char)) 26 { 27 alias FormatSpec = FormatSpec!(Unqual!Char); 28 } 29 30 /** 31 A general handler for format strings. 32 33 This handler centers around the function $(LREF writeUpToNextSpec), 34 which parses the $(MREF_ALTTEXT format string, std,format) until the 35 next format specifier is found. After the call, it provides 36 information about this format specifier in its numerous variables. 37 38 Params: 39 Char = the character type of the format string 40 */ 41 struct FormatSpec(Char) 42 if (is(Unqual!Char == Char)) 43 { 44 import std.algorithm.searching : startsWith; 45 import std.ascii : isDigit; 46 import std.conv : parse, text, to; 47 import std.range.primitives; 48 49 /** 50 Minimum width. 51 52 _Default: `0`. 53 */ 54 int width = 0; 55 56 /** 57 Precision. Its semantic depends on the format character. 58 59 See $(MREF_ALTTEXT format string, std,format) for more details. 60 _Default: `UNSPECIFIED`. 61 */ 62 int precision = UNSPECIFIED; 63 64 /** 65 Number of elements between separators. 66 67 _Default: `UNSPECIFIED`. 68 */ 69 int separators = UNSPECIFIED; 70 71 /** 72 The separator charactar is supplied at runtime. 73 74 _Default: false. 75 */ 76 bool dynamicSeparatorChar = false; 77 78 /** 79 Set to `DYNAMIC` when the separator character is supplied at runtime. 80 81 _Default: `UNSPECIFIED`. 82 83 $(RED Warning: 84 `separatorCharPos` is deprecated. It will be removed in 2.107.0. 85 Please use `dynamicSeparatorChar` instead.) 86 */ 87 // @@@DEPRECATED_[2.107.0]@@@ 88 deprecated("separatorCharPos will be removed in 2.107.0. Please use dynamicSeparatorChar instead.") 89 int separatorCharPos() { return dynamicSeparatorChar ? DYNAMIC : UNSPECIFIED; } 90 91 /// ditto 92 // @@@DEPRECATED_[2.107.0]@@@ 93 deprecated("separatorCharPos will be removed in 2.107.0. Please use dynamicSeparatorChar instead.") 94 void separatorCharPos(int value) { dynamicSeparatorChar = value == DYNAMIC; } 95 96 /** 97 Character to use as separator. 98 99 _Default: `','`. 100 */ 101 dchar separatorChar = ','; 102 103 /** 104 Special value for `width`, `precision` and `separators`. 105 106 It flags that these values will be passed at runtime through 107 variadic arguments. 108 */ 109 enum int DYNAMIC = int.max; 110 111 /** 112 Special value for `precision` and `separators`. 113 114 It flags that these values have not been specified. 115 */ 116 enum int UNSPECIFIED = DYNAMIC - 1; 117 118 /** 119 The format character. 120 121 _Default: `'s'`. 122 */ 123 char spec = 's'; 124 125 /** 126 Index of the argument for positional parameters. 127 128 Counting starts with `1`. Set to `0` if not used. Default: `0`. 129 */ 130 ubyte indexStart; 131 132 /** 133 Index of the last argument for positional parameter ranges. 134 135 Counting starts with `1`. Set to `0` if not used. Default: `0`. 136 */ 137 ubyte indexEnd; 138 139 version (StdDdoc) 140 { 141 /// The format specifier contained a `'-'`. 142 bool flDash; 143 144 /// The format specifier contained a `'0'`. 145 bool flZero; 146 147 /// The format specifier contained a space. 148 bool flSpace; 149 150 /// The format specifier contained a `'+'`. 151 bool flPlus; 152 153 /// The format specifier contained a `'#'`. 154 bool flHash; 155 156 /// The format specifier contained a `'='`. 157 bool flEqual; 158 159 /// The format specifier contained a `','`. 160 bool flSeparator; 161 162 // Fake field to allow compilation 163 ubyte allFlags; 164 } 165 else 166 { 167 union 168 { 169 import std.bitmanip : bitfields; 170 mixin(bitfields!( 171 bool, "flDash", 1, 172 bool, "flZero", 1, 173 bool, "flSpace", 1, 174 bool, "flPlus", 1, 175 bool, "flHash", 1, 176 bool, "flEqual", 1, 177 bool, "flSeparator", 1, 178 ubyte, "", 1)); 179 ubyte allFlags; 180 } 181 } 182 183 /// The inner format string of a nested format specifier. 184 const(Char)[] nested; 185 186 /** 187 The separator of a nested format specifier. 188 189 `null` means, there is no separator. `empty`, but not `null`, 190 means zero length separator. 191 */ 192 const(Char)[] sep; 193 194 /// Contains the part of the format string, that has not yet been parsed. 195 const(Char)[] trailing; 196 197 /// Sequence `"["` inserted before each range or range like structure. 198 enum immutable(Char)[] seqBefore = "["; 199 200 /// Sequence `"]"` inserted after each range or range like structure. 201 enum immutable(Char)[] seqAfter = "]"; 202 203 /** 204 Sequence `":"` inserted between element key and element value of 205 an associative array. 206 */ 207 enum immutable(Char)[] keySeparator = ":"; 208 209 /** 210 Sequence `", "` inserted between elements of a range, a range like 211 structure or the elements of an associative array. 212 */ 213 enum immutable(Char)[] seqSeparator = ", "; 214 215 /** 216 Creates a new `FormatSpec`. 217 218 The string is lazily evaluated. That means, nothing is done, 219 until $(LREF writeUpToNextSpec) is called. 220 221 Params: 222 fmt = a $(MREF_ALTTEXT format string, std,format) 223 */ 224 this(in Char[] fmt) @safe pure 225 { 226 trailing = fmt; 227 } 228 229 /** 230 Writes the format string to an output range until the next format 231 specifier is found and parse that format specifier. 232 233 See the $(MREF_ALTTEXT description of format strings, std,format) for more 234 details about the format specifier. 235 236 Params: 237 writer = an $(REF_ALTTEXT output range, isOutputRange, std, range, primitives), 238 where the format string is written to 239 OutputRange = type of the output range 240 241 Returns: 242 True, if a format specifier is found and false, if the end of the 243 format string has been reached. 244 245 Throws: 246 A $(REF_ALTTEXT FormatException, FormatException, std,format) 247 when parsing the format specifier did not succeed. 248 */ 249 bool writeUpToNextSpec(OutputRange)(ref OutputRange writer) scope 250 { 251 import std.format : enforceFmt; 252 253 if (trailing.empty) 254 return false; 255 for (size_t i = 0; i < trailing.length; ++i) 256 { 257 if (trailing[i] != '%') continue; 258 put(writer, trailing[0 .. i]); 259 trailing = trailing[i .. $]; 260 enforceFmt(trailing.length >= 2, `Unterminated format specifier: "%"`); 261 trailing = trailing[1 .. $]; 262 263 if (trailing[0] != '%') 264 { 265 // Spec found. Fill up the spec, and bailout 266 fillUp(); 267 return true; 268 } 269 // Doubled! Reset and Keep going 270 i = 0; 271 } 272 // no format spec found 273 put(writer, trailing); 274 trailing = null; 275 return false; 276 } 277 278 private void fillUp() scope 279 { 280 import std.format : enforceFmt, FormatException; 281 282 // Reset content 283 if (__ctfe) 284 { 285 flDash = false; 286 flZero = false; 287 flSpace = false; 288 flPlus = false; 289 flEqual = false; 290 flHash = false; 291 flSeparator = false; 292 } 293 else 294 { 295 allFlags = 0; 296 } 297 298 width = 0; 299 indexStart = 0; 300 indexEnd = 0; 301 precision = UNSPECIFIED; 302 nested = null; 303 // Parse the spec (we assume we're past '%' already) 304 for (size_t i = 0; i < trailing.length; ) 305 { 306 switch (trailing[i]) 307 { 308 case '(': 309 // Embedded format specifier. 310 auto j = i + 1; 311 // Get the matching balanced paren 312 for (uint innerParens;;) 313 { 314 enforceFmt(j + 1 < trailing.length, 315 text("Incorrect format specifier: %", trailing[i .. $])); 316 if (trailing[j++] != '%') 317 { 318 // skip, we're waiting for %( and %) 319 continue; 320 } 321 if (trailing[j] == '-') // for %-( 322 { 323 ++j; // skip 324 enforceFmt(j < trailing.length, 325 text("Incorrect format specifier: %", trailing[i .. $])); 326 } 327 if (trailing[j] == ')') 328 { 329 if (innerParens-- == 0) break; 330 } 331 else if (trailing[j] == '|') 332 { 333 if (innerParens == 0) break; 334 } 335 else if (trailing[j] == '(') 336 { 337 ++innerParens; 338 } 339 } 340 if (trailing[j] == '|') 341 { 342 auto k = j; 343 for (++j;;) 344 { 345 if (trailing[j++] != '%') 346 continue; 347 if (trailing[j] == '%') 348 ++j; 349 else if (trailing[j] == ')') 350 break; 351 else 352 throw new FormatException( 353 text("Incorrect format specifier: %", 354 trailing[j .. $])); 355 } 356 nested = trailing[i + 1 .. k - 1]; 357 sep = trailing[k + 1 .. j - 1]; 358 } 359 else 360 { 361 nested = trailing[i + 1 .. j - 1]; 362 sep = null; // no separator 363 } 364 //this = FormatSpec(innerTrailingSpec); 365 spec = '('; 366 // We practically found the format specifier 367 trailing = trailing[j + 1 .. $]; 368 return; 369 case '-': flDash = true; ++i; break; 370 case '+': flPlus = true; ++i; break; 371 case '=': flEqual = true; ++i; break; 372 case '#': flHash = true; ++i; break; 373 case '0': flZero = true; ++i; break; 374 case ' ': flSpace = true; ++i; break; 375 case '*': 376 if (isDigit(trailing[++i])) 377 { 378 // a '*' followed by digits and '$' is a 379 // positional format 380 trailing = trailing[1 .. $]; 381 width = -parse!(typeof(width))(trailing); 382 i = 0; 383 enforceFmt(trailing[i++] == '$', 384 text("$ expected after '*", -width, "' in format string")); 385 } 386 else 387 { 388 // read result 389 width = DYNAMIC; 390 } 391 break; 392 case '1': .. case '9': 393 auto tmp = trailing[i .. $]; 394 const widthOrArgIndex = parse!uint(tmp); 395 enforceFmt(tmp.length, 396 text("Incorrect format specifier %", trailing[i .. $])); 397 i = trailing.length - tmp.length; 398 if (tmp.startsWith('$')) 399 { 400 // index of the form %n$ 401 indexEnd = indexStart = to!ubyte(widthOrArgIndex); 402 ++i; 403 } 404 else if (tmp.startsWith(':')) 405 { 406 // two indexes of the form %m:n$, or one index of the form %m:$ 407 indexStart = to!ubyte(widthOrArgIndex); 408 tmp = tmp[1 .. $]; 409 if (tmp.startsWith('$')) 410 { 411 indexEnd = indexEnd.max; 412 } 413 else 414 { 415 indexEnd = parse!(typeof(indexEnd))(tmp); 416 } 417 i = trailing.length - tmp.length; 418 enforceFmt(trailing[i++] == '$', 419 "$ expected"); 420 } 421 else 422 { 423 // width 424 width = to!int(widthOrArgIndex); 425 } 426 break; 427 case ',': 428 // Precision 429 ++i; 430 flSeparator = true; 431 432 if (trailing[i] == '*') 433 { 434 ++i; 435 // read result 436 separators = DYNAMIC; 437 } 438 else if (isDigit(trailing[i])) 439 { 440 auto tmp = trailing[i .. $]; 441 separators = parse!int(tmp); 442 i = trailing.length - tmp.length; 443 } 444 else 445 { 446 // "," was specified, but nothing after it 447 separators = 3; 448 } 449 450 if (trailing[i] == '?') 451 { 452 dynamicSeparatorChar = true; 453 ++i; 454 } 455 456 break; 457 case '.': 458 // Precision 459 if (trailing[++i] == '*') 460 { 461 if (isDigit(trailing[++i])) 462 { 463 // a '.*' followed by digits and '$' is a 464 // positional precision 465 trailing = trailing[i .. $]; 466 i = 0; 467 precision = -parse!int(trailing); 468 enforceFmt(trailing[i++] == '$', 469 "$ expected"); 470 } 471 else 472 { 473 // read result 474 precision = DYNAMIC; 475 } 476 } 477 else if (trailing[i] == '-') 478 { 479 // negative precision, as good as 0 480 precision = 0; 481 auto tmp = trailing[i .. $]; 482 parse!int(tmp); // skip digits 483 i = trailing.length - tmp.length; 484 } 485 else if (isDigit(trailing[i])) 486 { 487 auto tmp = trailing[i .. $]; 488 precision = parse!int(tmp); 489 i = trailing.length - tmp.length; 490 } 491 else 492 { 493 // "." was specified, but nothing after it 494 precision = 0; 495 } 496 break; 497 default: 498 // this is the format char 499 spec = cast(char) trailing[i++]; 500 trailing = trailing[i .. $]; 501 return; 502 } // end switch 503 } // end for 504 throw new FormatException(text("Incorrect format specifier: ", trailing)); 505 } 506 507 //-------------------------------------------------------------------------- 508 package bool readUpToNextSpec(R)(ref R r) scope 509 { 510 import std.ascii : isLower, isWhite; 511 import std.format : enforceFmt; 512 import std.utf : stride; 513 514 // Reset content 515 if (__ctfe) 516 { 517 flDash = false; 518 flZero = false; 519 flSpace = false; 520 flPlus = false; 521 flHash = false; 522 flEqual = false; 523 flSeparator = false; 524 } 525 else 526 { 527 allFlags = 0; 528 } 529 width = 0; 530 precision = UNSPECIFIED; 531 nested = null; 532 // Parse the spec 533 while (trailing.length) 534 { 535 const c = trailing[0]; 536 if (c == '%' && trailing.length > 1) 537 { 538 const c2 = trailing[1]; 539 if (c2 == '%') 540 { 541 assert(!r.empty, "Required at least one more input"); 542 // Require a '%' 543 enforceFmt (r.front == '%', 544 text("parseToFormatSpec: Cannot find character '", 545 c2, "' in the input string.")); 546 trailing = trailing[2 .. $]; 547 r.popFront(); 548 } 549 else 550 { 551 enforceFmt(isLower(c2) || c2 == '*' || c2 == '(', 552 text("'%", c2, "' not supported with formatted read")); 553 trailing = trailing[1 .. $]; 554 fillUp(); 555 return true; 556 } 557 } 558 else 559 { 560 if (c == ' ') 561 { 562 while (!r.empty && isWhite(r.front)) r.popFront(); 563 //r = std.algorithm.find!(not!(isWhite))(r); 564 } 565 else 566 { 567 enforceFmt(!r.empty && r.front == trailing.front, 568 text("parseToFormatSpec: Cannot find character '", 569 c, "' in the input string.")); 570 r.popFront(); 571 } 572 trailing = trailing[stride(trailing, 0) .. $]; 573 } 574 } 575 return false; 576 } 577 578 package string getCurFmtStr() const 579 { 580 import std.array : appender; 581 import std.format.write : formatValue; 582 583 auto w = appender!string(); 584 auto f = FormatSpec!Char("%s"); // for stringnize 585 586 put(w, '%'); 587 if (indexStart != 0) 588 { 589 formatValue(w, indexStart, f); 590 put(w, '$'); 591 } 592 if (flDash) put(w, '-'); 593 if (flZero) put(w, '0'); 594 if (flSpace) put(w, ' '); 595 if (flPlus) put(w, '+'); 596 if (flEqual) put(w, '='); 597 if (flHash) put(w, '#'); 598 if (width != 0) 599 formatValue(w, width, f); 600 if (precision != FormatSpec!Char.UNSPECIFIED) 601 { 602 put(w, '.'); 603 formatValue(w, precision, f); 604 } 605 if (flSeparator) put(w, ','); 606 if (separators != FormatSpec!Char.UNSPECIFIED) 607 formatValue(w, separators, f); 608 put(w, spec); 609 return w.data; 610 } 611 612 /** 613 Provides a string representation. 614 615 Returns: 616 The string representation. 617 */ 618 string toString() const @safe pure 619 { 620 import std.array : appender; 621 622 auto app = appender!string(); 623 app.reserve(200 + trailing.length); 624 toString(app); 625 return app.data; 626 } 627 628 /** 629 Writes a string representation to an output range. 630 631 Params: 632 writer = an $(REF_ALTTEXT output range, isOutputRange, std, range, primitives), 633 where the representation is written to 634 OutputRange = type of the output range 635 */ 636 void toString(OutputRange)(ref OutputRange writer) const 637 if (isOutputRange!(OutputRange, char)) 638 { 639 import std.format.write : formatValue; 640 641 auto s = singleSpec("%s"); 642 643 put(writer, "address = "); 644 formatValue(writer, &this, s); 645 put(writer, "\nwidth = "); 646 formatValue(writer, width, s); 647 put(writer, "\nprecision = "); 648 formatValue(writer, precision, s); 649 put(writer, "\nspec = "); 650 formatValue(writer, spec, s); 651 put(writer, "\nindexStart = "); 652 formatValue(writer, indexStart, s); 653 put(writer, "\nindexEnd = "); 654 formatValue(writer, indexEnd, s); 655 put(writer, "\nflDash = "); 656 formatValue(writer, flDash, s); 657 put(writer, "\nflZero = "); 658 formatValue(writer, flZero, s); 659 put(writer, "\nflSpace = "); 660 formatValue(writer, flSpace, s); 661 put(writer, "\nflPlus = "); 662 formatValue(writer, flPlus, s); 663 put(writer, "\nflEqual = "); 664 formatValue(writer, flEqual, s); 665 put(writer, "\nflHash = "); 666 formatValue(writer, flHash, s); 667 put(writer, "\nflSeparator = "); 668 formatValue(writer, flSeparator, s); 669 put(writer, "\nnested = "); 670 formatValue(writer, nested, s); 671 put(writer, "\ntrailing = "); 672 formatValue(writer, trailing, s); 673 put(writer, '\n'); 674 } 675 } 676 677 /// 678 @safe pure unittest 679 { 680 import std.array : appender; 681 682 auto a = appender!(string)(); 683 auto fmt = "Number: %6.4e\nString: %s"; 684 auto f = FormatSpec!char(fmt); 685 686 assert(f.writeUpToNextSpec(a) == true); 687 688 assert(a.data == "Number: "); 689 assert(f.trailing == "\nString: %s"); 690 assert(f.spec == 'e'); 691 assert(f.width == 6); 692 assert(f.precision == 4); 693 694 assert(f.writeUpToNextSpec(a) == true); 695 696 assert(a.data == "Number: \nString: "); 697 assert(f.trailing == ""); 698 assert(f.spec == 's'); 699 700 assert(f.writeUpToNextSpec(a) == false); 701 702 assert(a.data == "Number: \nString: "); 703 } 704 705 @safe unittest 706 { 707 import std.array : appender; 708 import std.conv : text; 709 import std.exception : assertThrown; 710 import std.format : FormatException; 711 712 auto w = appender!(char[])(); 713 auto f = FormatSpec!char("abc%sdef%sghi"); 714 f.writeUpToNextSpec(w); 715 assert(w.data == "abc", w.data); 716 assert(f.trailing == "def%sghi", text(f.trailing)); 717 f.writeUpToNextSpec(w); 718 assert(w.data == "abcdef", w.data); 719 assert(f.trailing == "ghi"); 720 // test with embedded %%s 721 f = FormatSpec!char("ab%%cd%%ef%sg%%h%sij"); 722 w.clear(); 723 f.writeUpToNextSpec(w); 724 assert(w.data == "ab%cd%ef" && f.trailing == "g%%h%sij", w.data); 725 f.writeUpToNextSpec(w); 726 assert(w.data == "ab%cd%efg%h" && f.trailing == "ij"); 727 // https://issues.dlang.org/show_bug.cgi?id=4775 728 f = FormatSpec!char("%%%s"); 729 w.clear(); 730 f.writeUpToNextSpec(w); 731 assert(w.data == "%" && f.trailing == ""); 732 f = FormatSpec!char("%%%%%s%%"); 733 w.clear(); 734 while (f.writeUpToNextSpec(w)) continue; 735 assert(w.data == "%%%"); 736 737 f = FormatSpec!char("a%%b%%c%"); 738 w.clear(); 739 assertThrown!FormatException(f.writeUpToNextSpec(w)); 740 assert(w.data == "a%b%c" && f.trailing == "%"); 741 } 742 743 // https://issues.dlang.org/show_bug.cgi?id=5237 744 @safe unittest 745 { 746 import std.array : appender; 747 748 auto w = appender!string(); 749 auto f = FormatSpec!char("%.16f"); 750 f.writeUpToNextSpec(w); // dummy eating 751 assert(f.spec == 'f'); 752 auto fmt = f.getCurFmtStr(); 753 assert(fmt == "%.16f"); 754 } 755 756 // https://issues.dlang.org/show_bug.cgi?id=14059 757 @safe unittest 758 { 759 import std.array : appender; 760 import std.exception : assertThrown; 761 import std.format : FormatException; 762 763 auto a = appender!(string)(); 764 765 auto f = FormatSpec!char("%-(%s%"); // %)") 766 assertThrown!FormatException(f.writeUpToNextSpec(a)); 767 768 f = FormatSpec!char("%(%-"); // %)") 769 assertThrown!FormatException(f.writeUpToNextSpec(a)); 770 } 771 772 @safe unittest 773 { 774 import std.array : appender; 775 import std.format : format; 776 777 auto a = appender!(string)(); 778 779 auto f = FormatSpec!char("%,d"); 780 f.writeUpToNextSpec(a); 781 782 assert(f.spec == 'd', format("%s", f.spec)); 783 assert(f.precision == FormatSpec!char.UNSPECIFIED); 784 assert(f.separators == 3); 785 786 f = FormatSpec!char("%5,10f"); 787 f.writeUpToNextSpec(a); 788 assert(f.spec == 'f', format("%s", f.spec)); 789 assert(f.separators == 10); 790 assert(f.width == 5); 791 792 f = FormatSpec!char("%5,10.4f"); 793 f.writeUpToNextSpec(a); 794 assert(f.spec == 'f', format("%s", f.spec)); 795 assert(f.separators == 10); 796 assert(f.width == 5); 797 assert(f.precision == 4); 798 } 799 800 @safe pure unittest 801 { 802 import std.algorithm.searching : canFind, findSplitBefore; 803 804 auto expected = "width = 2" ~ 805 "\nprecision = 5" ~ 806 "\nspec = f" ~ 807 "\nindexStart = 0" ~ 808 "\nindexEnd = 0" ~ 809 "\nflDash = false" ~ 810 "\nflZero = false" ~ 811 "\nflSpace = false" ~ 812 "\nflPlus = false" ~ 813 "\nflEqual = false" ~ 814 "\nflHash = false" ~ 815 "\nflSeparator = false" ~ 816 "\nnested = " ~ 817 "\ntrailing = \n"; 818 auto spec = singleSpec("%2.5f"); 819 auto res = spec.toString(); 820 // make sure the address exists, then skip it 821 assert(res.canFind("address")); 822 assert(res.findSplitBefore("width")[1] == expected); 823 } 824 825 // https://issues.dlang.org/show_bug.cgi?id=15348 826 @safe pure unittest 827 { 828 import std.array : appender; 829 import std.exception : collectExceptionMsg; 830 import std.format : FormatException; 831 832 auto w = appender!(char[])(); 833 auto f = FormatSpec!char("%*10d"); 834 835 assert(collectExceptionMsg!FormatException(f.writeUpToNextSpec(w)) 836 == "$ expected after '*10' in format string"); 837 } 838 839 // https://github.com/dlang/phobos/issues/10713 840 @safe pure unittest 841 { 842 import std.array : appender; 843 auto f = FormatSpec!char("%3$d%d"); 844 845 auto w = appender!(char[])(); 846 f.writeUpToNextSpec(w); 847 assert(f.indexStart == 3); 848 849 f.writeUpToNextSpec(w); 850 assert(w.data.length == 0); 851 assert(f.indexStart == 0); 852 } 853 854 /** 855 Helper function that returns a `FormatSpec` for a single format specifier. 856 857 Params: 858 fmt = a $(MREF_ALTTEXT format string, std,format) 859 containing a single format specifier 860 Char = character type of `fmt` 861 862 Returns: 863 A $(LREF FormatSpec) with the format specifier parsed. 864 865 Throws: 866 A $(REF_ALTTEXT FormatException, FormatException, std,format) when the 867 format string contains no format specifier or more than a single format 868 specifier or when the format specifier is malformed. 869 */ 870 FormatSpec!Char singleSpec(Char)(Char[] fmt) 871 { 872 import std.conv : text; 873 import std.format : enforceFmt; 874 import std.range.primitives : empty, front; 875 876 enforceFmt(fmt.length >= 2, "fmt must be at least 2 characters long"); 877 enforceFmt(fmt.front == '%', "fmt must start with a '%' character"); 878 enforceFmt(fmt[1] != '%', "'%%' is not a permissible format specifier"); 879 880 static struct DummyOutputRange 881 { 882 void put(C)(scope const C[] buf) {} // eat elements 883 } 884 auto a = DummyOutputRange(); 885 auto spec = FormatSpec!Char(fmt); 886 //dummy write 887 spec.writeUpToNextSpec(a); 888 889 enforceFmt(spec.trailing.empty, 890 text("Trailing characters in fmt string: '", spec.trailing)); 891 892 return spec; 893 } 894 895 /// 896 @safe pure unittest 897 { 898 import std.array : appender; 899 import std.format.write : formatValue; 900 901 auto spec = singleSpec("%10.3e"); 902 auto writer = appender!string(); 903 writer.formatValue(42.0, spec); 904 905 assert(writer.data == " 4.200e+01"); 906 } 907 908 @safe pure unittest 909 { 910 import std.exception : assertThrown; 911 import std.format : FormatException; 912 913 auto spec = singleSpec("%2.3e"); 914 915 assert(spec.trailing == ""); 916 assert(spec.spec == 'e'); 917 assert(spec.width == 2); 918 assert(spec.precision == 3); 919 920 assertThrown!FormatException(singleSpec("")); 921 assertThrown!FormatException(singleSpec("%")); 922 assertThrown!FormatException(singleSpec("%2.3")); 923 assertThrown!FormatException(singleSpec("2.3e")); 924 assertThrown!FormatException(singleSpec("Test%2.3e")); 925 assertThrown!FormatException(singleSpec("%2.3eTest")); 926 assertThrown!FormatException(singleSpec("%%")); 927 } 928 929 // @@@DEPRECATED_[2.107.0]@@@ 930 deprecated("enforceValidFormatSpec was accidentally made public and will be removed in 2.107.0") 931 void enforceValidFormatSpec(T, Char)(scope const ref FormatSpec!Char f) 932 { 933 import std.format.internal.write : evfs = enforceValidFormatSpec; 934 935 evfs!T(f); 936 } 937 938 @safe unittest 939 { 940 import std.exception : collectExceptionMsg; 941 import std.format : format, FormatException; 942 943 // width/precision 944 assert(collectExceptionMsg!FormatException(format("%*.d", 5.1, 2)) 945 == "integer width expected, not double for argument #1"); 946 assert(collectExceptionMsg!FormatException(format("%-1*.d", 5.1, 2)) 947 == "integer width expected, not double for argument #1"); 948 949 assert(collectExceptionMsg!FormatException(format("%.*d", '5', 2)) 950 == "integer precision expected, not char for argument #1"); 951 assert(collectExceptionMsg!FormatException(format("%-1.*d", 4.7, 3)) 952 == "integer precision expected, not double for argument #1"); 953 assert(collectExceptionMsg!FormatException(format("%.*d", 5)) 954 == "Orphan format specifier: %d"); 955 assert(collectExceptionMsg!FormatException(format("%*.*d", 5)) 956 == "Missing integer precision argument"); 957 958 // dynamicSeparatorChar 959 assert(collectExceptionMsg!FormatException(format("%,?d", 5)) 960 == "separator character expected, not int for argument #1"); 961 assert(collectExceptionMsg!FormatException(format("%,?d", '?')) 962 == "Orphan format specifier: %d"); 963 assert(collectExceptionMsg!FormatException(format("%.*,*?d", 5)) 964 == "Missing separator digit width argument"); 965 } 966