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