The OpenD Programming Language

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