fennec
Loading...
Searching...
No Matches
format.h
Go to the documentation of this file.
1// =====================================================================================================================
2// fennec, a free and open source game engine
3// Copyright © 2025 Medusa Slockbower
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17// =====================================================================================================================
18
30
31#ifndef FENNEC_FORMAT_FORMAT_H
32#define FENNEC_FORMAT_FORMAT_H
33
34#include <fennec/string/string.h>
35
36#include <fennec/format/detail/_format.h>
38
39namespace fennec
40{
41
42template<typename...ArgsT>
43string format(const cstring& str, ArgsT&&...args) {
44 static constexpr size_t argc = sizeof...(ArgsT);
45 static constexpr format_arg default_fmt = {
46 .fill = ' ',
47 .align = '\0', // default to locale
48 .sign = '\0', // default to sign only for negative numbers, gets handled later in code
49 .alt = false, // default no prefix
50 .upper = false,
51 .width = 0,
52 .precision = 6, // default to 6 sigfigs
53 .base = 10,
54 .type = '\0',
55 };
56
57 // empty case
58 if constexpr(argc == 0) {
59 return str;
60 }
61
62 detail::_format_argarray<argc> argarray = { fennec::forward<ArgsT>(args)... };
63 string res;
64 size_t i = 0;
65 size_t arg_c = -1;
66
67 while (i <= str.length()) {
68 size_t brace = str.find('{', i);
69 size_t end = str.find('}', i);
70 format_arg fmt = default_fmt;
71
72 // check for '}}'
73 if (end < brace) {
74 if (str[end + 1] == '}') {
75 res += string(str.data() + i, end - i);
76 i = end + 2;
77 continue;
78 }
79 assertf(false, "fennec::format syntax error, encountered unexpected '{'")
80 }
81
82 // append string
83 if (brace >= str.length()) { // handle end case
84 res += string(str.data() + i, str.length() - i);
85 break;
86 }
87 res += string(str.data() + i, brace - i);
88
89 // next brace, validate escape
90 size_t next_brace = str.find('{', brace + 1);
91 if (brace + 1 == next_brace) {
92 res += '{';
93 i = next_brace + 1;
94 continue;
95 }
96
97 // find contained colon
98 size_t colon = str.find(':', brace);
99
100 // validate colon and brace location
101 assertf(colon < next_brace or end < next_brace, "fennec::format syntax error, mismatched '{}'");
102
103 // parse index if present
104 size_t id = min(colon, end) - 1;
105 if (id > brace) {
106 arg_c = 0;
107 } else {
108 ++arg_c;
109 }
110 for (size_t j = id, k = 1; j > brace; --j, k *= 10) {
111 size_t u = (str[j] - '0');
112 assertf(u < 10, "fennec::format syntax error, invalid argument index");
113 arg_c += k * u;
114 }
115
116 // store argument to allow nested replacement fields
117 size_t arg = arg_c;
118
119 // validate index
120 assertf(arg < argc, "fennec::format syntax error, invalid argument index");
121
122 // early return case for no colon
123 if (colon > end) {
124 fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
125 res += argarray.format(arg, fmt);
126 i = end + 1;
127 continue;
128 }
129
130 // parse format specifiers
131
132 // we're going to parse right-to-left since the valid combinations
133 // of specifiers change based on the type of the argument
134
135 // to compensate for this, the nested replacement fields need to be computed in this loop
136 // (nested replacement deduced)
137 size_t nrfd = 0;
138 size_t nnrf = 0;
139
140 // first find the matching '}' brace, e is not necessarily the matching brace
141 // since some specifiers allow nested replacement fields
142 size_t parse = colon;
143 while (str[parse + 1] != '}') {
144 if (next_brace < end) { // if the next brace is before the next closing brace
145 ++nnrf;
146 nrfd += str[end - 1] == '{';
147 parse = end + 1;
148 end = str.find('}', parse);
149 next_brace = str.find('{', parse);
150 } else {
151 parse = end - 1;
152 break;
153 }
154 }
155
156 assertf(nrfd <= 2 and parse < str.length() - 1 and str[parse + 1] == '}',
157 "fennec::format syntax error, mismatched '{}'");
158
159 // check type
160 switch (str[parse]) {
161 default: break;
162
163 case 's': case '?': // strings
164 case 'c': // char
165 fmt.type = str[parse--];
166 break;
167
168
169 case 'd': // decimal
170 fmt.base = 10;
171 fmt.type = str[parse--];
172 break;
173
174 case 'B': // binary
175 fmt.upper = true; [[fallthrough]];
176 case 'b':
177 fmt.base = 2;
178 fmt.type = str[parse--];
179 break;
180
181 case 'o': // octal
182 fmt.base = 8;
183 fmt.type = str[parse--];
184 break;
185
186 case 'X': // hex
187 fmt.upper = true; [[fallthrough]];
188 case 'x':
189 fmt.base = 16;
190 fmt.type = str[parse--];
191 break;
192
193
194 case 'A':
195 fmt.upper = true; [[fallthrough]];
196 case 'a': // float hex
197 fmt.base = 16;
198 fmt.type = str[parse--];
199 break;
200
201 case 'E': // scientific notation
202 fmt.upper = true; [[fallthrough]];
203 case 'e':
204 fmt.base = 16;
205 fmt.type = str[parse--];
206 break;
207
208 case 'F': // fixed precision
209 fmt.upper = true; [[fallthrough]];
210 case 'f':
211 fmt.base = 10;
212 fmt.type = str[parse--];
213 break;
214
215 case 'G': // general precision
216 fmt.upper = true; [[fallthrough]];
217 case 'g':
218 fmt.base = 10;
219 fmt.type = str[parse--];
220 break;
221 }
222
223 // early return
224 if (parse == colon) {
225 fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
226 res += argarray.format(arg, fmt);
227 i = end + 1;
228 continue;
229 }
230
231 // search for width and precision
232 size_t x = 0, j = 1;
233 bool found_decimal = false;
234 size_t num_decimals = 0;
235 bool is_float_t = detail::_isfmt_f(fmt.type);
236 bool is_str_t = fmt.type == 's';
237 bool is_integer_t = detail::_isfmt_i(fmt.type);
238 bool ded_width_f = false;
239 bool ded_width = false;
240 size_t ded_temp_i = 0;
241
242 // default "precision" for strings should be 0 for no limit
243 if (is_str_t) {
244 fmt.precision = 0;
245 }
246
247 // parse width and precision
248 while (isdigit(str[parse]) or (found_decimal = (str[parse] == '.')) or str[parse] == '{' or str[parse] == '}') {
249 // handle decimal point for precision
250 if (found_decimal) {
251 assertf(is_float_t or is_str_t, "fennec::format syntax error, encountered precision argument on non-floating point format");
252 assertf(num_decimals == 0, "fennec::format syntax error, multiple decimals detected in floating point format");
253 ++num_decimals;
254 found_decimal = false;
255
256 fmt.precision = x;
257 x = 0, j = 1;
258 --parse;
259 continue;
260 }
261
262 // check for nested replacement field
263 if (str[parse] == '{') {
264 assertf(str[parse - 1] == '0' or str[parse - 1] == '.' or not isdigit(str[parse - 1]),
265 "fennec::format syntax error, unexpected digit preceding nested replacement field");
266
267 bool prec = str[parse - 1] == '.';
268 bool ded = str[parse + 1] == '}';
269
270 size_t sub;
271 if (nrfd == 2) { // if both are deduced, parse normally. Hack with prefix and postfix.
272 sub = prec ? ++arg_c + 1 : arg_c++;
273 } else if (nrfd == 1 and nnrf == 2 and prec and ded) { // if only precision is nrf, deduce width first
274 ded_width_f = true;
275 ded_temp_i = parse;
276 continue;
277 } else { // otherwise deduce normally
278 sub = ded ? ++arg_c : x;
279 }
280
281 assertf(sub < argc, "fennec::format syntax error, argument index out of range in nested replacement field");
282 assertf(argarray.is_integer(sub), "fennec::format argument error, nested replacement field argument is not convertible to integral type");
283
284 (prec ? fmt.precision : fmt.width) = argarray.int_value(sub);
285 x = 0;
286
287 if (ded_width_f) {
288 ded_width_f = false;
289 ded_width = true;
290 swap(ded_temp_i, parse);
291 arg_c = sub;
292 continue;
293 }
294
295 if (ded_width) {
296 parse = ded_temp_i;
297 ded_width = false;
298 }
299
300 parse -= 1 + prec;
301 continue;
302 }
303
304 // ignore closing brace for nested replacement fields
305 if (str[parse] == '}') {
306 --parse;
307 continue;
308 }
309
310 // crude way to only handle 0 case if 0 is the last digit
311 fmt.fill = str[parse] == '0' ? '0' : ' ';
312
313 // parse the number
314 x += j * (str[parse] - '0');
315 j *= 10;
316 --parse;
317 }
318 if (x != 0) {
319 fmt.width = x;
320 }
321
322 // early return
323 if (parse == colon) {
324 fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
325 res += argarray.format(arg, fmt);
326 i = end + 1;
327 continue;
328 }
329
330 // check for alt form
331 if (str[parse] == '#') {
332 assertf(is_float_t or is_integer_t, "fennec::format syntax error, encountered alt spec ('#') with non-decimal type");
333 fmt.alt = true;
334 --parse;
335 }
336
337 // check for sign
338 if (str[parse] == '-' or str[parse] == '+' or str[parse] == ' ') {
339 fmt.sign = str[parse];
340 if (str[parse] == ' ') { // handle fill if only space, gets overwritten if encounters fill character
341 fmt.fill = ' ';
342 }
343 --parse;
344 }
345
346 // check for alignment
347 if (str[parse] == '<' or str[parse] == '>' or str[parse] == '^') {
348 fmt.align = str[parse];
349 --parse;
350 }
351
352 // fill character
353 if (str[parse] != ':') {
354 fmt.fill = str[parse];
355 if (str[parse] == ' ') {
356 fmt.sign = fmt.sign == '\0' ? ' ' : fmt.sign;
357 }
358 --parse;
359 }
360
361 // default sign
362 fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
363
364 // validate that we handled the entire format arg
365 assertf(parse == colon, "fennec::format syntax error, malformed format string detected, possible double colon");
366
367
368 // add formatted argument
369 res += argarray.format(arg, fmt);
370 i = end + 1;
371 }
372
373 return res;
374}
375
376}
377
378#endif // FENNEC_FORMAT_FORMAT_H
constexpr genType min(genType x, genType y)
Returns if otherwise it returns .
Definition common.h:688
constexpr size_t find(char c, size_t i=0) const
Finds the index of the first occurrence of c in the string.
Definition string.h:251
constexpr void swap(T &x, T &y) noexcept
Swaps x and y.
Definition utility.h:114