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