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