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 precision = UNSPECIFIED; 300 nested = null; 301 // Parse the spec (we assume we're past '%' already) 302 for (size_t i = 0; i < trailing.length; ) 303 { 304 switch (trailing[i]) 305 { 306 case '(': 307 // Embedded format specifier. 308 auto j = i + 1; 309 // Get the matching balanced paren 310 for (uint innerParens;;) 311 { 312 enforceFmt(j + 1 < trailing.length, 313 text("Incorrect format specifier: %", trailing[i .. $])); 314 if (trailing[j++] != '%') 315 { 316 // skip, we're waiting for %( and %) 317 continue; 318 } 319 if (trailing[j] == '-') // for %-( 320 { 321 ++j; // skip 322 enforceFmt(j < trailing.length, 323 text("Incorrect format specifier: %", trailing[i .. $])); 324 } 325 if (trailing[j] == ')') 326 { 327 if (innerParens-- == 0) break; 328 } 329 else if (trailing[j] == '|') 330 { 331 if (innerParens == 0) break; 332 } 333 else if (trailing[j] == '(') 334 { 335 ++innerParens; 336 } 337 } 338 if (trailing[j] == '|') 339 { 340 auto k = j; 341 for (++j;;) 342 { 343 if (trailing[j++] != '%') 344 continue; 345 if (trailing[j] == '%') 346 ++j; 347 else if (trailing[j] == ')') 348 break; 349 else 350 throw new FormatException( 351 text("Incorrect format specifier: %", 352 trailing[j .. $])); 353 } 354 nested = trailing[i + 1 .. k - 1]; 355 sep = trailing[k + 1 .. j - 1]; 356 } 357 else 358 { 359 nested = trailing[i + 1 .. j - 1]; 360 sep = null; // no separator 361 } 362 //this = FormatSpec(innerTrailingSpec); 363 spec = '('; 364 // We practically found the format specifier 365 trailing = trailing[j + 1 .. $]; 366 return; 367 case '-': flDash = true; ++i; break; 368 case '+': flPlus = true; ++i; break; 369 case '=': flEqual = true; ++i; break; 370 case '#': flHash = true; ++i; break; 371 case '0': flZero = true; ++i; break; 372 case ' ': flSpace = true; ++i; break; 373 case '*': 374 if (isDigit(trailing[++i])) 375 { 376 // a '*' followed by digits and '$' is a 377 // positional format 378 trailing = trailing[1 .. $]; 379 width = -parse!(typeof(width))(trailing); 380 i = 0; 381 enforceFmt(trailing[i++] == '$', 382 text("$ expected after '*", -width, "' in format string")); 383 } 384 else 385 { 386 // read result 387 width = DYNAMIC; 388 } 389 break; 390 case '1': .. case '9': 391 auto tmp = trailing[i .. $]; 392 const widthOrArgIndex = parse!uint(tmp); 393 enforceFmt(tmp.length, 394 text("Incorrect format specifier %", trailing[i .. $])); 395 i = trailing.length - tmp.length; 396 if (tmp.startsWith('$')) 397 { 398 // index of the form %n$ 399 indexEnd = indexStart = to!ubyte(widthOrArgIndex); 400 ++i; 401 } 402 else if (tmp.startsWith(':')) 403 { 404 // two indexes of the form %m:n$, or one index of the form %m:$ 405 indexStart = to!ubyte(widthOrArgIndex); 406 tmp = tmp[1 .. $]; 407 if (tmp.startsWith('$')) 408 { 409 indexEnd = indexEnd.max; 410 } 411 else 412 { 413 indexEnd = parse!(typeof(indexEnd))(tmp); 414 } 415 i = trailing.length - tmp.length; 416 enforceFmt(trailing[i++] == '$', 417 "$ expected"); 418 } 419 else 420 { 421 // width 422 width = to!int(widthOrArgIndex); 423 } 424 break; 425 case ',': 426 // Precision 427 ++i; 428 flSeparator = true; 429 430 if (trailing[i] == '*') 431 { 432 ++i; 433 // read result 434 separators = DYNAMIC; 435 } 436 else if (isDigit(trailing[i])) 437 { 438 auto tmp = trailing[i .. $]; 439 separators = parse!int(tmp); 440 i = trailing.length - tmp.length; 441 } 442 else 443 { 444 // "," was specified, but nothing after it 445 separators = 3; 446 } 447 448 if (trailing[i] == '?') 449 { 450 dynamicSeparatorChar = true; 451 ++i; 452 } 453 454 break; 455 case '.': 456 // Precision 457 if (trailing[++i] == '*') 458 { 459 if (isDigit(trailing[++i])) 460 { 461 // a '.*' followed by digits and '$' is a 462 // positional precision 463 trailing = trailing[i .. $]; 464 i = 0; 465 precision = -parse!int(trailing); 466 enforceFmt(trailing[i++] == '$', 467 "$ expected"); 468 } 469 else 470 { 471 // read result 472 precision = DYNAMIC; 473 } 474 } 475 else if (trailing[i] == '-') 476 { 477 // negative precision, as good as 0 478 precision = 0; 479 auto tmp = trailing[i .. $]; 480 parse!int(tmp); // skip digits 481 i = trailing.length - tmp.length; 482 } 483 else if (isDigit(trailing[i])) 484 { 485 auto tmp = trailing[i .. $]; 486 precision = parse!int(tmp); 487 i = trailing.length - tmp.length; 488 } 489 else 490 { 491 // "." was specified, but nothing after it 492 precision = 0; 493 } 494 break; 495 default: 496 // this is the format char 497 spec = cast(char) trailing[i++]; 498 trailing = trailing[i .. $]; 499 return; 500 } // end switch 501 } // end for 502 throw new FormatException(text("Incorrect format specifier: ", trailing)); 503 } 504 505 //-------------------------------------------------------------------------- 506 package bool readUpToNextSpec(R)(ref R r) scope 507 { 508 import std.ascii : isLower, isWhite; 509 import std.format : enforceFmt; 510 import std.utf : stride; 511 512 // Reset content 513 if (__ctfe) 514 { 515 flDash = false; 516 flZero = false; 517 flSpace = false; 518 flPlus = false; 519 flHash = false; 520 flEqual = false; 521 flSeparator = false; 522 } 523 else 524 { 525 allFlags = 0; 526 } 527 width = 0; 528 precision = UNSPECIFIED; 529 nested = null; 530 // Parse the spec 531 while (trailing.length) 532 { 533 const c = trailing[0]; 534 if (c == '%' && trailing.length > 1) 535 { 536 const c2 = trailing[1]; 537 if (c2 == '%') 538 { 539 assert(!r.empty, "Required at least one more input"); 540 // Require a '%' 541 enforceFmt (r.front == '%', 542 text("parseToFormatSpec: Cannot find character '", 543 c2, "' in the input string.")); 544 trailing = trailing[2 .. $]; 545 r.popFront(); 546 } 547 else 548 { 549 enforceFmt(isLower(c2) || c2 == '*' || c2 == '(', 550 text("'%", c2, "' not supported with formatted read")); 551 trailing = trailing[1 .. $]; 552 fillUp(); 553 return true; 554 } 555 } 556 else 557 { 558 if (c == ' ') 559 { 560 while (!r.empty && isWhite(r.front)) r.popFront(); 561 //r = std.algorithm.find!(not!(isWhite))(r); 562 } 563 else 564 { 565 enforceFmt(!r.empty && r.front == trailing.front, 566 text("parseToFormatSpec: Cannot find character '", 567 c, "' in the input string.")); 568 r.popFront(); 569 } 570 trailing = trailing[stride(trailing, 0) .. $]; 571 } 572 } 573 return false; 574 } 575 576 package string getCurFmtStr() const 577 { 578 import std.array : appender; 579 import std.format.write : formatValue; 580 581 auto w = appender!string(); 582 auto f = FormatSpec!Char("%s"); // for stringnize 583 584 put(w, '%'); 585 if (indexStart != 0) 586 { 587 formatValue(w, indexStart, f); 588 put(w, '$'); 589 } 590 if (flDash) put(w, '-'); 591 if (flZero) put(w, '0'); 592 if (flSpace) put(w, ' '); 593 if (flPlus) put(w, '+'); 594 if (flEqual) put(w, '='); 595 if (flHash) put(w, '#'); 596 if (width != 0) 597 formatValue(w, width, f); 598 if (precision != FormatSpec!Char.UNSPECIFIED) 599 { 600 put(w, '.'); 601 formatValue(w, precision, f); 602 } 603 if (flSeparator) put(w, ','); 604 if (separators != FormatSpec!Char.UNSPECIFIED) 605 formatValue(w, separators, f); 606 put(w, spec); 607 return w.data; 608 } 609 610 /** 611 Provides a string representation. 612 613 Returns: 614 The string representation. 615 */ 616 string toString() const @safe pure 617 { 618 import std.array : appender; 619 620 auto app = appender!string(); 621 app.reserve(200 + trailing.length); 622 toString(app); 623 return app.data; 624 } 625 626 /** 627 Writes a string representation to an output range. 628 629 Params: 630 writer = an $(REF_ALTTEXT output range, isOutputRange, std, range, primitives), 631 where the representation is written to 632 OutputRange = type of the output range 633 */ 634 void toString(OutputRange)(ref OutputRange writer) const 635 if (isOutputRange!(OutputRange, char)) 636 { 637 import std.format.write : formatValue; 638 639 auto s = singleSpec("%s"); 640 641 put(writer, "address = "); 642 formatValue(writer, &this, s); 643 put(writer, "\nwidth = "); 644 formatValue(writer, width, s); 645 put(writer, "\nprecision = "); 646 formatValue(writer, precision, s); 647 put(writer, "\nspec = "); 648 formatValue(writer, spec, s); 649 put(writer, "\nindexStart = "); 650 formatValue(writer, indexStart, s); 651 put(writer, "\nindexEnd = "); 652 formatValue(writer, indexEnd, s); 653 put(writer, "\nflDash = "); 654 formatValue(writer, flDash, s); 655 put(writer, "\nflZero = "); 656 formatValue(writer, flZero, s); 657 put(writer, "\nflSpace = "); 658 formatValue(writer, flSpace, s); 659 put(writer, "\nflPlus = "); 660 formatValue(writer, flPlus, s); 661 put(writer, "\nflEqual = "); 662 formatValue(writer, flEqual, s); 663 put(writer, "\nflHash = "); 664 formatValue(writer, flHash, s); 665 put(writer, "\nflSeparator = "); 666 formatValue(writer, flSeparator, s); 667 put(writer, "\nnested = "); 668 formatValue(writer, nested, s); 669 put(writer, "\ntrailing = "); 670 formatValue(writer, trailing, s); 671 put(writer, '\n'); 672 } 673 } 674 675 /// 676 @safe pure unittest 677 { 678 import std.array : appender; 679 680 auto a = appender!(string)(); 681 auto fmt = "Number: %6.4e\nString: %s"; 682 auto f = FormatSpec!char(fmt); 683 684 assert(f.writeUpToNextSpec(a)); 685 686 assert(a.data == "Number: "); 687 assert(f.trailing == "\nString: %s"); 688 assert(f.spec == 'e'); 689 assert(f.width == 6); 690 assert(f.precision == 4); 691 692 assert(f.writeUpToNextSpec(a)); 693 694 assert(a.data == "Number: \nString: "); 695 assert(f.trailing == ""); 696 assert(f.spec == 's'); 697 698 assert(!f.writeUpToNextSpec(a)); 699 700 assert(a.data == "Number: \nString: "); 701 } 702 703 @safe unittest 704 { 705 import std.array : appender; 706 import std.conv : text; 707 import std.exception : assertThrown; 708 import std.format : FormatException; 709 710 auto w = appender!(char[])(); 711 auto f = FormatSpec!char("abc%sdef%sghi"); 712 f.writeUpToNextSpec(w); 713 assert(w.data == "abc", w.data); 714 assert(f.trailing == "def%sghi", text(f.trailing)); 715 f.writeUpToNextSpec(w); 716 assert(w.data == "abcdef", w.data); 717 assert(f.trailing == "ghi"); 718 // test with embedded %%s 719 f = FormatSpec!char("ab%%cd%%ef%sg%%h%sij"); 720 w.clear(); 721 f.writeUpToNextSpec(w); 722 assert(w.data == "ab%cd%ef" && f.trailing == "g%%h%sij", w.data); 723 f.writeUpToNextSpec(w); 724 assert(w.data == "ab%cd%efg%h" && f.trailing == "ij"); 725 // https://issues.dlang.org/show_bug.cgi?id=4775 726 f = FormatSpec!char("%%%s"); 727 w.clear(); 728 f.writeUpToNextSpec(w); 729 assert(w.data == "%" && f.trailing == ""); 730 f = FormatSpec!char("%%%%%s%%"); 731 w.clear(); 732 while (f.writeUpToNextSpec(w)) continue; 733 assert(w.data == "%%%"); 734 735 f = FormatSpec!char("a%%b%%c%"); 736 w.clear(); 737 assertThrown!FormatException(f.writeUpToNextSpec(w)); 738 assert(w.data == "a%b%c" && f.trailing == "%"); 739 } 740 741 // https://issues.dlang.org/show_bug.cgi?id=5237 742 @safe unittest 743 { 744 import std.array : appender; 745 746 auto w = appender!string(); 747 auto f = FormatSpec!char("%.16f"); 748 f.writeUpToNextSpec(w); // dummy eating 749 assert(f.spec == 'f'); 750 auto fmt = f.getCurFmtStr(); 751 assert(fmt == "%.16f"); 752 } 753 754 // https://issues.dlang.org/show_bug.cgi?id=14059 755 @safe unittest 756 { 757 import std.array : appender; 758 import std.exception : assertThrown; 759 import std.format : FormatException; 760 761 auto a = appender!(string)(); 762 763 auto f = FormatSpec!char("%-(%s%"); // %)") 764 assertThrown!FormatException(f.writeUpToNextSpec(a)); 765 766 f = FormatSpec!char("%(%-"); // %)") 767 assertThrown!FormatException(f.writeUpToNextSpec(a)); 768 } 769 770 @safe unittest 771 { 772 import std.array : appender; 773 import std.format : format; 774 775 auto a = appender!(string)(); 776 777 auto f = FormatSpec!char("%,d"); 778 f.writeUpToNextSpec(a); 779 780 assert(f.spec == 'd', format("%s", f.spec)); 781 assert(f.precision == FormatSpec!char.UNSPECIFIED); 782 assert(f.separators == 3); 783 784 f = FormatSpec!char("%5,10f"); 785 f.writeUpToNextSpec(a); 786 assert(f.spec == 'f', format("%s", f.spec)); 787 assert(f.separators == 10); 788 assert(f.width == 5); 789 790 f = FormatSpec!char("%5,10.4f"); 791 f.writeUpToNextSpec(a); 792 assert(f.spec == 'f', format("%s", f.spec)); 793 assert(f.separators == 10); 794 assert(f.width == 5); 795 assert(f.precision == 4); 796 } 797 798 @safe pure unittest 799 { 800 import std.algorithm.searching : canFind, findSplitBefore; 801 802 auto expected = "width = 2" ~ 803 "\nprecision = 5" ~ 804 "\nspec = f" ~ 805 "\nindexStart = 0" ~ 806 "\nindexEnd = 0" ~ 807 "\nflDash = false" ~ 808 "\nflZero = false" ~ 809 "\nflSpace = false" ~ 810 "\nflPlus = false" ~ 811 "\nflEqual = false" ~ 812 "\nflHash = false" ~ 813 "\nflSeparator = false" ~ 814 "\nnested = " ~ 815 "\ntrailing = \n"; 816 auto spec = singleSpec("%2.5f"); 817 auto res = spec.toString(); 818 // make sure the address exists, then skip it 819 assert(res.canFind("address")); 820 assert(res.findSplitBefore("width")[1] == expected); 821 } 822 823 // https://issues.dlang.org/show_bug.cgi?id=15348 824 @safe pure unittest 825 { 826 import std.array : appender; 827 import std.exception : collectExceptionMsg; 828 import std.format : FormatException; 829 830 auto w = appender!(char[])(); 831 auto f = FormatSpec!char("%*10d"); 832 833 assert(collectExceptionMsg!FormatException(f.writeUpToNextSpec(w)) 834 == "$ expected after '*10' in format string"); 835 } 836 837 /** 838 Helper function that returns a `FormatSpec` for a single format specifier. 839 840 Params: 841 fmt = a $(MREF_ALTTEXT format string, std,format) 842 containing a single format specifier 843 Char = character type of `fmt` 844 845 Returns: 846 A $(LREF FormatSpec) with the format specifier parsed. 847 848 Throws: 849 A $(REF_ALTTEXT FormatException, FormatException, std,format) when the 850 format string contains no format specifier or more than a single format 851 specifier or when the format specifier is malformed. 852 */ 853 FormatSpec!Char singleSpec(Char)(Char[] fmt) 854 { 855 import std.conv : text; 856 import std.format : enforceFmt; 857 import std.range.primitives : empty, front; 858 859 enforceFmt(fmt.length >= 2, "fmt must be at least 2 characters long"); 860 enforceFmt(fmt.front == '%', "fmt must start with a '%' character"); 861 enforceFmt(fmt[1] != '%', "'%%' is not a permissible format specifier"); 862 863 static struct DummyOutputRange 864 { 865 void put(C)(scope const C[] buf) {} // eat elements 866 } 867 auto a = DummyOutputRange(); 868 auto spec = FormatSpec!Char(fmt); 869 //dummy write 870 spec.writeUpToNextSpec(a); 871 872 enforceFmt(spec.trailing.empty, 873 text("Trailing characters in fmt string: '", spec.trailing)); 874 875 return spec; 876 } 877 878 /// 879 @safe pure unittest 880 { 881 import std.array : appender; 882 import std.format.write : formatValue; 883 884 auto spec = singleSpec("%10.3e"); 885 auto writer = appender!string(); 886 writer.formatValue(42.0, spec); 887 888 assert(writer.data == " 4.200e+01"); 889 } 890 891 @safe pure unittest 892 { 893 import std.exception : assertThrown; 894 import std.format : FormatException; 895 896 auto spec = singleSpec("%2.3e"); 897 898 assert(spec.trailing == ""); 899 assert(spec.spec == 'e'); 900 assert(spec.width == 2); 901 assert(spec.precision == 3); 902 903 assertThrown!FormatException(singleSpec("")); 904 assertThrown!FormatException(singleSpec("%")); 905 assertThrown!FormatException(singleSpec("%2.3")); 906 assertThrown!FormatException(singleSpec("2.3e")); 907 assertThrown!FormatException(singleSpec("Test%2.3e")); 908 assertThrown!FormatException(singleSpec("%2.3eTest")); 909 assertThrown!FormatException(singleSpec("%%")); 910 } 911 912 // @@@DEPRECATED_[2.107.0]@@@ 913 deprecated("enforceValidFormatSpec was accidentally made public and will be removed in 2.107.0") 914 void enforceValidFormatSpec(T, Char)(scope const ref FormatSpec!Char f) 915 { 916 import std.format.internal.write : evfs = enforceValidFormatSpec; 917 918 evfs!T(f); 919 } 920 921 @safe unittest 922 { 923 import std.exception : collectExceptionMsg; 924 import std.format : format, FormatException; 925 926 // width/precision 927 assert(collectExceptionMsg!FormatException(format("%*.d", 5.1, 2)) 928 == "integer width expected, not double for argument #1"); 929 assert(collectExceptionMsg!FormatException(format("%-1*.d", 5.1, 2)) 930 == "integer width expected, not double for argument #1"); 931 932 assert(collectExceptionMsg!FormatException(format("%.*d", '5', 2)) 933 == "integer precision expected, not char for argument #1"); 934 assert(collectExceptionMsg!FormatException(format("%-1.*d", 4.7, 3)) 935 == "integer precision expected, not double for argument #1"); 936 assert(collectExceptionMsg!FormatException(format("%.*d", 5)) 937 == "Orphan format specifier: %d"); 938 assert(collectExceptionMsg!FormatException(format("%*.*d", 5)) 939 == "Missing integer precision argument"); 940 941 // dynamicSeparatorChar 942 assert(collectExceptionMsg!FormatException(format("%,?d", 5)) 943 == "separator character expected, not int for argument #1"); 944 assert(collectExceptionMsg!FormatException(format("%,?d", '?')) 945 == "Orphan format specifier: %d"); 946 assert(collectExceptionMsg!FormatException(format("%.*,*?d", 5)) 947 == "Missing separator digit width argument"); 948 } 949