1 module libd.data.format; 2 3 import libd.datastructures..string, libd.datastructures.sumtype, libd.datastructures.array, 4 libd.algorithm.search, libd.util.errorhandling, libd.data.conv; 5 6 private enum MAX_FORMAT_PARAMS = 5; 7 8 struct FormatSegment 9 { 10 bcstring formatter; // Can be null 11 bcstring[MAX_FORMAT_PARAMS] params; 12 ubyte paramCount; 13 ubyte formatItemIndex; 14 } 15 16 union FormatStringInfoValues 17 { 18 FormatSegment formatted; 19 bcstring unformatted; 20 } 21 22 alias FormatStringInfo = SumType!FormatStringInfoValues; 23 24 SimpleResult!String format(Params...)(scope bcstring spec, scope Params params) 25 { 26 String result; 27 BcError error; 28 bool throwError = false; 29 30 defaultFormatter(spec, result, error, throwError, params); 31 32 if(throwError) 33 return error.result!String; 34 35 return result.result; 36 } 37 /// 38 @("format (formatInfoPusher & defaultFormatter by proxy)") 39 unittest 40 { 41 struct DoeRayMe 42 { 43 string easyAs; 44 int oneTwoThree; 45 } 46 47 assert(format("abc").assumeValid == "abc"); 48 assert(format("abc {1:} {0}", 123, "easy as").assumeValid == "abc easy as 123"); 49 assert(format("abc {0}", DoeRayMe("hard as", 321)).assumeValid == `abc DoeRayMe("hard as", 321)`); 50 } 51 52 @nogc 53 void defaultFormatter(ResultT, Params...)(scope bcstring spec, scope ref ResultT result, scope ref BcError error, scope ref bool throwError, scope Params params) nothrow 54 { 55 formatInfoPusher(spec, (info) 56 { 57 if(throwError) 58 return; 59 if(!info.isValid) 60 { 61 throwError = true; 62 error = info.error; 63 return; 64 } 65 66 auto value = info.value; 67 value.visit!( 68 (bcstring raw) { result.put(raw); }, 69 (FormatSegment segment) 70 { 71 Switch: switch(segment.formatItemIndex) 72 { 73 static foreach(i; 0..Params.length) 74 { 75 case i: 76 defaultFormatterSegmentHandler(result, segment, params[i]); 77 break Switch; 78 } 79 80 default: 81 throwError = true; 82 error = raise("Parameter index out of bounds"); 83 break; 84 } 85 } 86 )(value); 87 }); 88 } 89 90 @nogc 91 private void defaultFormatterSegmentHandler(ResultT, ParamT)(scope ref ResultT result, const scope FormatSegment segment, scope auto ref ParamT param) nothrow 92 { 93 const formatter = segment.formatter; 94 if(formatter.length == 0) 95 { 96 static if(__traits(compiles, result.put(param)) && !is(ParamT : const bool)) 97 result.put(param); 98 else static if(__traits(compiles, to!String(param))) 99 result.put(to!String(param).range); 100 else static assert(false, "Don't know how to default format param of type "~ParamT.stringof); 101 } 102 else 103 { 104 displayError(raise( 105 String("An invalid formatter was passed: ")~String(formatter) 106 )); 107 assert(false, "An invalid formatter was passed."); 108 } 109 } 110 111 @nogc 112 void formatInfoPusher(scope bcstring format, scope void delegate(SimpleResult!FormatStringInfo info) @nogc nothrow handler) nothrow 113 { 114 size_t start = 0; 115 for(size_t cursor = 0; cursor < format.length;) 116 { 117 const startBracketIndex = format[cursor..$].indexOfAscii('{'); 118 if(startBracketIndex == INDEX_NOT_FOUND) 119 { 120 handler(FormatStringInfo(format[start..$]).result); 121 return; 122 } 123 124 const realStartBracketIndex = cursor+startBracketIndex; 125 handler(FormatStringInfo(format[start..realStartBracketIndex]).result); // Push prior chars. 126 start = realStartBracketIndex+1; 127 128 // Escaped '{' 129 if(start < format.length && format[start] == '{') 130 { 131 handler(FormatStringInfo(cast(bcstring)"{{").result); 132 continue; 133 } 134 135 FormatSegment segment; 136 bool foundIndex; 137 138 cursor = start; 139 Foreach: foreach(i, ch; format[start..$]) 140 { 141 switch(ch) 142 { 143 case '}': 144 if(!foundIndex) 145 { 146 foundIndex = true; 147 const convResult = format[start..cursor].to!ubyte; 148 if(!convResult.isValid) 149 { 150 handler(raise("Invalid parameter index.").result!FormatStringInfo); 151 return; 152 } 153 segment.formatItemIndex = convResult.value; 154 } 155 else 156 { 157 segment.formatter = format[start..cursor++]; 158 break Foreach; 159 } 160 cursor++; 161 break Foreach; 162 163 case ':': 164 if(!foundIndex) 165 { 166 foundIndex = true; 167 const convResult = format[start..cursor].to!ubyte; 168 if(!convResult.isValid) 169 { 170 handler(raise("Invalid parameter index.").result!FormatStringInfo); 171 return; 172 } 173 segment.formatItemIndex = convResult.value; 174 start = cursor+1; 175 } // Assume any extra colons are part of the arguments 176 break; 177 178 default: break; 179 } 180 cursor++; 181 } 182 183 handler(FormatStringInfo(segment).result); 184 start = cursor; 185 } 186 }