383 lines
10 KiB
C++
383 lines
10 KiB
C++
// =====================================================================================================================
|
|
// fennec, a free and open source game engine
|
|
// Copyright © 2025 - 2026 Medusa Slockbower
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
// =====================================================================================================================
|
|
|
|
///
|
|
/// \file fennec/format/format.h
|
|
/// \brief
|
|
///
|
|
///
|
|
/// \details
|
|
/// \author Medusa Slockbower
|
|
///
|
|
/// \copyright Copyright © 2025 - 2026 Medusa Slockbower ([GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html))
|
|
///
|
|
///
|
|
|
|
#ifndef FENNEC_FORMAT_FORMAT_H
|
|
#define FENNEC_FORMAT_FORMAT_H
|
|
|
|
#include <fennec/string/string.h>
|
|
|
|
#include <fennec/format/detail/_format.h>
|
|
#include <fennec/format/formatter.h>
|
|
|
|
namespace fennec
|
|
{
|
|
///
|
|
/// \brief C++ 20 format specification
|
|
/// \tparam ArgsT The argument types
|
|
/// \param str The format string
|
|
/// \param args The argument values
|
|
/// \returns A formatted string using the C++20 format specification
|
|
template<typename...ArgsT>
|
|
string format(const cstring& str, ArgsT&&...args) {
|
|
static constexpr size_t argc = sizeof...(ArgsT);
|
|
static constexpr format_arg default_fmt = {
|
|
.fill = ' ',
|
|
.align = '\0', // default to locale
|
|
.sign = '\0', // default to sign only for negative numbers, gets handled later in code
|
|
.alt = false, // default no prefix
|
|
.upper = false,
|
|
.width = 0,
|
|
.precision = 6, // default to 6 sigfigs
|
|
.base = 10,
|
|
.type = '\0',
|
|
};
|
|
|
|
// empty case
|
|
if constexpr(argc == 0) {
|
|
return str;
|
|
}
|
|
|
|
detail::_format_argarray<argc> argarray = { fennec::forward<ArgsT>(args)... };
|
|
string res;
|
|
size_t i = 0;
|
|
size_t arg_c = -1;
|
|
|
|
while (i <= str.length()) {
|
|
size_t brace = str.find('{', i);
|
|
size_t end = str.find('}', i);
|
|
format_arg fmt = default_fmt;
|
|
|
|
// check for '}}'
|
|
if (end < brace) {
|
|
if (str[end + 1] == '}') {
|
|
res += string(str.data() + i, end - i);
|
|
i = end + 2;
|
|
continue;
|
|
}
|
|
assertf(false, "fennec::format syntax error, encountered unexpected '{'")
|
|
}
|
|
|
|
// append string
|
|
if (brace >= str.length()) { // handle end case
|
|
res += string(str.data() + i, str.length() - i);
|
|
break;
|
|
}
|
|
res += string(str.data() + i, brace - i);
|
|
|
|
// next brace, validate escape
|
|
size_t next_brace = str.find('{', brace + 1);
|
|
if (brace + 1 == next_brace) {
|
|
res += '{';
|
|
i = next_brace + 1;
|
|
continue;
|
|
}
|
|
|
|
// find contained colon
|
|
size_t colon = str.find(':', brace);
|
|
|
|
// validate colon and brace location
|
|
assertf(colon < next_brace or end < next_brace, "fennec::format syntax error, mismatched '{}'");
|
|
|
|
// parse index if present
|
|
size_t id = min(colon, end) - 1;
|
|
if (id > brace) {
|
|
arg_c = 0;
|
|
} else {
|
|
++arg_c;
|
|
}
|
|
for (size_t j = id, k = 1; j > brace; --j, k *= 10) {
|
|
size_t u = (str[j] - '0');
|
|
assertf(u < 10, "fennec::format syntax error, invalid argument index");
|
|
arg_c += k * u;
|
|
}
|
|
|
|
// store argument to allow nested replacement fields
|
|
size_t arg = arg_c;
|
|
|
|
// validate index
|
|
assertf(arg < argc, "fennec::format syntax error, invalid argument index");
|
|
|
|
// early return case for no colon
|
|
if (colon > end) {
|
|
fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
|
|
res += argarray.format(arg, fmt);
|
|
i = end + 1;
|
|
continue;
|
|
}
|
|
|
|
// parse format specifiers
|
|
|
|
// we're going to parse right-to-left since the valid combinations
|
|
// of specifiers change based on the type of the argument
|
|
|
|
// to compensate for this, the nested replacement fields need to be computed in this loop
|
|
// (nested replacement deduced)
|
|
size_t nrfd = 0;
|
|
size_t nnrf = 0;
|
|
|
|
// first find the matching '}' brace, e is not necessarily the matching brace
|
|
// since some specifiers allow nested replacement fields
|
|
size_t parse = colon;
|
|
while (str[parse + 1] != '}') {
|
|
if (next_brace < end) { // if the next brace is before the next closing brace
|
|
++nnrf;
|
|
nrfd += str[end - 1] == '{';
|
|
parse = end + 1;
|
|
end = str.find('}', parse);
|
|
next_brace = str.find('{', parse);
|
|
} else {
|
|
parse = end - 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
assertf(nrfd <= 2 and parse < str.length() - 1 and str[parse + 1] == '}',
|
|
"fennec::format syntax error, mismatched '{}'");
|
|
|
|
// check type
|
|
switch (str[parse]) {
|
|
default: break;
|
|
|
|
case 's': case '?': // strings
|
|
case 'c': // char
|
|
fmt.type = str[parse--];
|
|
break;
|
|
|
|
|
|
case 'd': // decimal
|
|
fmt.base = 10;
|
|
fmt.type = str[parse--];
|
|
break;
|
|
|
|
case 'B': // binary
|
|
fmt.upper = true; [[fallthrough]];
|
|
case 'b':
|
|
fmt.base = 2;
|
|
fmt.type = str[parse--];
|
|
break;
|
|
|
|
case 'o': // octal
|
|
fmt.base = 8;
|
|
fmt.type = str[parse--];
|
|
break;
|
|
|
|
case 'X': // hex
|
|
fmt.upper = true; [[fallthrough]];
|
|
case 'x':
|
|
fmt.base = 16;
|
|
fmt.type = str[parse--];
|
|
break;
|
|
|
|
|
|
case 'A':
|
|
fmt.upper = true; [[fallthrough]];
|
|
case 'a': // float hex
|
|
fmt.base = 16;
|
|
fmt.type = str[parse--];
|
|
break;
|
|
|
|
case 'E': // scientific notation
|
|
fmt.upper = true; [[fallthrough]];
|
|
case 'e':
|
|
fmt.base = 16;
|
|
fmt.type = str[parse--];
|
|
break;
|
|
|
|
case 'F': // fixed precision
|
|
fmt.upper = true; [[fallthrough]];
|
|
case 'f':
|
|
fmt.base = 10;
|
|
fmt.type = str[parse--];
|
|
break;
|
|
|
|
case 'G': // general precision
|
|
fmt.upper = true; [[fallthrough]];
|
|
case 'g':
|
|
fmt.base = 10;
|
|
fmt.type = str[parse--];
|
|
break;
|
|
}
|
|
|
|
// early return
|
|
if (parse == colon) {
|
|
fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
|
|
res += argarray.format(arg, fmt);
|
|
i = end + 1;
|
|
continue;
|
|
}
|
|
|
|
// search for width and precision
|
|
size_t x = 0, j = 1;
|
|
bool found_decimal = false;
|
|
size_t num_decimals = 0;
|
|
bool is_float_t = detail::_isfmt_f(fmt.type);
|
|
bool is_str_t = fmt.type == 's';
|
|
bool is_integer_t = detail::_isfmt_i(fmt.type);
|
|
bool ded_width_f = false;
|
|
bool ded_width = false;
|
|
size_t ded_temp_i = 0;
|
|
|
|
// default "precision" for strings should be 0 for no limit
|
|
if (is_str_t) {
|
|
fmt.precision = 0;
|
|
}
|
|
|
|
// parse width and precision
|
|
while (isdigit(str[parse]) or (found_decimal = (str[parse] == '.')) or str[parse] == '{' or str[parse] == '}') {
|
|
// handle decimal point for precision
|
|
if (found_decimal) {
|
|
assertf(is_float_t or is_str_t, "fennec::format syntax error, encountered precision argument on non-floating point format");
|
|
assertf(num_decimals == 0, "fennec::format syntax error, multiple decimals detected in floating point format");
|
|
++num_decimals;
|
|
found_decimal = false;
|
|
|
|
fmt.precision = x;
|
|
x = 0, j = 1;
|
|
--parse;
|
|
continue;
|
|
}
|
|
|
|
// check for nested replacement field
|
|
if (str[parse] == '{') {
|
|
assertf(str[parse - 1] == '0' or str[parse - 1] == '.' or not isdigit(str[parse - 1]),
|
|
"fennec::format syntax error, unexpected digit preceding nested replacement field");
|
|
|
|
bool prec = str[parse - 1] == '.';
|
|
bool ded = str[parse + 1] == '}';
|
|
|
|
size_t sub;
|
|
if (nrfd == 2) { // if both are deduced, parse normally. Hack with prefix and postfix.
|
|
sub = prec ? ++arg_c + 1 : arg_c++;
|
|
} else if (nrfd == 1 and nnrf == 2 and prec and ded) { // if only precision is nrf, deduce width first
|
|
ded_width_f = true;
|
|
ded_temp_i = parse;
|
|
continue;
|
|
} else { // otherwise deduce normally
|
|
sub = ded ? ++arg_c : x;
|
|
}
|
|
|
|
assertf(sub < argc, "fennec::format syntax error, argument index out of range in nested replacement field");
|
|
assertf(argarray.is_integer(sub), "fennec::format argument error, nested replacement field argument is not convertible to integral type");
|
|
|
|
(prec ? fmt.precision : fmt.width) = argarray.int_value(sub);
|
|
x = 0;
|
|
|
|
if (ded_width_f) {
|
|
ded_width_f = false;
|
|
ded_width = true;
|
|
swap(ded_temp_i, parse);
|
|
arg_c = sub;
|
|
continue;
|
|
}
|
|
|
|
if (ded_width) {
|
|
parse = ded_temp_i;
|
|
ded_width = false;
|
|
}
|
|
|
|
parse -= 1 + prec;
|
|
continue;
|
|
}
|
|
|
|
// ignore closing brace for nested replacement fields
|
|
if (str[parse] == '}') {
|
|
--parse;
|
|
continue;
|
|
}
|
|
|
|
// crude way to only handle 0 case if 0 is the last digit
|
|
fmt.fill = str[parse] == '0' ? '0' : ' ';
|
|
|
|
// parse the number
|
|
x += j * (str[parse] - '0');
|
|
j *= 10;
|
|
--parse;
|
|
}
|
|
if (x != 0) {
|
|
fmt.width = x;
|
|
}
|
|
|
|
// early return
|
|
if (parse == colon) {
|
|
fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
|
|
res += argarray.format(arg, fmt);
|
|
i = end + 1;
|
|
continue;
|
|
}
|
|
|
|
// check for alt form
|
|
if (str[parse] == '#') {
|
|
assertf(is_float_t or is_integer_t, "fennec::format syntax error, encountered alt spec ('#') with non-decimal type");
|
|
fmt.alt = true;
|
|
--parse;
|
|
}
|
|
|
|
// check for sign
|
|
if (str[parse] == '-' or str[parse] == '+' or str[parse] == ' ') {
|
|
fmt.sign = str[parse];
|
|
if (str[parse] == ' ') { // handle fill if only space, gets overwritten if encounters fill character
|
|
fmt.fill = ' ';
|
|
}
|
|
--parse;
|
|
}
|
|
|
|
// check for alignment
|
|
if (str[parse] == '<' or str[parse] == '>' or str[parse] == '^') {
|
|
fmt.align = str[parse];
|
|
--parse;
|
|
}
|
|
|
|
// fill character
|
|
if (str[parse] != ':') {
|
|
fmt.fill = str[parse];
|
|
if (str[parse] == ' ') {
|
|
fmt.sign = fmt.sign == '\0' ? ' ' : fmt.sign;
|
|
}
|
|
--parse;
|
|
}
|
|
|
|
// default sign
|
|
fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
|
|
|
|
// validate that we handled the entire format arg
|
|
assertf(parse == colon, "fennec::format syntax error, malformed format string detected, possible double colon");
|
|
|
|
|
|
// add formatted argument
|
|
res += argarray.format(arg, fmt);
|
|
i = end + 1;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
}
|
|
|
|
#endif // FENNEC_FORMAT_FORMAT_H
|