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 }