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