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 }