1 /// Author: Aziz Köksal
2 /// License: GPL3
3 /// $(Maturity average)
4 module util.Format;
5 
6 import std.format : FormatSpec, formatValue;
7 
8 alias FSpec = FormatSpec!char;
9 
10 /// Parses a Tango-style format string fragment.
11 /// Regex: \{(\d*)\s*([,.]\s*-?\d*\s*)?(:[^}]*)?\}
12 /// Returns: null if not found, an escaped "{{", or a format string "{...}".
13 const(C)[] parseFmt(C=char)(const(C)[] fmt, ref FSpec fs)
14 {
15   auto p = fmt.ptr;
16   auto end = fmt.ptr + fmt.length;
17 
18   auto inc = () => (assert(p < end), ++p, true);
19   bool loop(lazy bool pred) { return pred() && loop(pred); }
20   auto loopUntil = (lazy bool pred) => loop(!pred() && p < end && inc());
21   auto current = (C c) => p < end && *p == c;
22   auto next = (C c) => p+1 < end && p[1] == c;
23   auto skipped = (C c) => current(c) && inc();
24   ubyte digit; // Scanned single digit.
25   auto isdigit = () => p < end && (digit = cast(ubyte)(*p-'0')) < 10 && inc();
26   int number; // Scanned number.
27   auto adddigit = () => isdigit() && ((number = number * 10 + digit), true);
28   auto isnumber = () => isdigit() && ((number = digit), loop(adddigit()), true);
29 
30   // Start scanning.
31   loopUntil(current('{'));
32   if (next('{'))
33     return p[0..2]; // "{{"
34 
35   auto begin = p++;
36 
37   if (p >= end)
38     return null;
39 
40   if (isnumber())
41     fs.indexStart = fs.indexEnd = cast(ubyte)(number + 1);
42 
43   loopUntil(!current(' '));
44 
45   if (skipped(',') || skipped('.'))
46   {
47     C minmaxChar = *(p-1);
48     loopUntil(!current(' '));
49     fs.flDash = skipped('-');
50     if (isnumber())
51     {
52       if (minmaxChar == ',')
53         fs.width = number;
54       else // TODO: '.'
55       {}
56     }
57     loopUntil(!current(' '));
58   }
59 
60   if (skipped(':'))
61   {
62     auto fmtBegin = p;
63     loopUntil(current('}'));
64     auto end2 = p;
65     p = fmtBegin;
66     if (p < end2)
67     {
68       if (cast(ubyte)((*p | 0x20) - 'a') <= 'z'-'a') // Letter?
69         fs.spec = *p++;
70       if (isnumber())
71         fs.precision = number;
72       foreach (c; p[0..end2-p])
73         if (c == '+')
74           fs.flPlus = true;
75         else if (c == ' ')
76           fs.flSpace = true;
77         else if (c == '0')
78           fs.flZero = true;
79         //else if (c == '.')
80         //{} // Strips trailing zeros.
81         else if (c == '#')
82           fs.flHash = true;
83     }
84   }
85 
86   skipped('}');
87   return begin[0..p-begin];
88 }
89 
90 void formatTangoActual(C=char, Writer)
91   (ref Writer w, const(C)[] fmt, void delegate(ref FormatSpec!C)[] fmtFuncs)
92 {
93   ubyte argIndex; // 0-based.
94   while (fmt.length)
95   {
96     FSpec fs;
97     auto fmtSlice = parseFmt(fmt, fs);
98 
99     if (fmtSlice == "{{")
100     {
101       auto fmtIndex = fmtSlice.ptr - fmt.ptr + 1; // +1 includes first '{'.
102       w ~= fmt[0..fmtIndex];
103       fmt = fmt[fmtIndex+1 .. $]; // +1 skips second '{'.
104       continue;
105     }
106     if (fmtSlice is null)
107       break;
108 
109     if (fs.indexStart) // 1-based.
110       argIndex = cast(ubyte)(fs.indexStart - 1);
111 
112     auto fmtIndex = fmtSlice.ptr - fmt.ptr;
113     if (fmtIndex)
114       w ~= fmt[0..fmtIndex]; // Append previous non-format string.
115 
116     if (argIndex < fmtFuncs.length)
117       fmtFuncs[argIndex](fs); // Write the formatted value.
118     else
119     {} // TODO: emit error string?
120 
121     fmt = fmt[fmtIndex+fmtSlice.length .. $];
122 
123     argIndex++;
124   }
125   if (fmt.length)
126     w ~= fmt;
127 }
128 
129 void formatTango(C=char, Writer, AS...)(ref Writer w, const(C)[] fmt, AS as)
130 {
131   void delegate(ref FormatSpec!C)[AS.length] fmtFuncs;
132   foreach (i, A; AS)
133     fmtFuncs[i] = (ref fs) => formatValue(w, as[i], fs);
134   formatTangoActual(w, fmt, fmtFuncs);
135 }
136 
137 void testFormatTango()
138 {
139   import std.stdio;
140   import dil.Array;
141   CharArray a;
142   struct CharArrayWriter
143   {
144     CharArray* a;
145     ref CharArray a_ref()
146     {
147       return *a;
148     }
149     alias a_ref this;
150     void put(dchar dc)
151     {
152       import dil.Unicode : encode;
153       ensureOrGrow(4);
154       auto utf8Chars = encode(a.cur, dc);
155       a.cur += utf8Chars.length;
156       assert(utf8Chars.length <= 4);
157     }
158   }
159   auto w = CharArrayWriter(&a);
160   formatTango(w, "test{}a{{0}}>{,6}<", 12345, "läla");
161   assert(a[] == "test12345a{0}}> läla<");
162   writefln("a[]='%s'", a[]);
163 }