- formatting implemented for floating point types

- fixed some bugs with width and precision specifiers:
    * Evaluation order of nested replacement fields
This commit is contained in:
2025-12-08 12:13:51 -05:00
parent 9645856554
commit 9553f9b662
4 changed files with 168 additions and 48 deletions

View File

@@ -132,54 +132,62 @@ string format(const cstring& str, ArgsT&&...args) {
// 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 spec = colon;
while (str[spec + 1] != '}') {
size_t parse = colon;
while (str[parse + 1] != '}') {
if (next_brace < end) { // if the next brace is before the next closing brace
spec = end + 1;
end = str.find('}', spec);
next_brace = str.find('{', spec);
++nnrf;
nrfd += str[end - 1] == '{';
parse = end + 1;
end = str.find('}', parse);
next_brace = str.find('{', parse);
} else {
spec = end - 1;
parse = end - 1;
break;
}
}
assert(spec < str.length() - 1 and str[spec+1] == '}', "fennec::format syntax error, mismatched '{}'");
assertf(nrfd <= 2 and parse < str.length() - 1 and str[parse + 1] == '}',
"fennec::format syntax error, mismatched '{}'");
// check type
switch (str[spec]) {
switch (str[parse]) {
default: break;
case 's': case '?': // strings
case 'c': // char
fmt.type = str[spec--];
fmt.type = str[parse--];
break;
case 'd': // decimal
fmt.base = 10;
fmt.type = str[spec--];
fmt.type = str[parse--];
break;
case 'B': // binary
fmt.upper = true; [[fallthrough]];
case 'b':
fmt.base = 2;
fmt.type = str[spec--];
fmt.type = str[parse--];
break;
case 'o': // octal
fmt.base = 8;
fmt.type = str[spec--];
fmt.type = str[parse--];
break;
case 'X': // hex
fmt.upper = true; [[fallthrough]];
case 'x':
fmt.base = 16;
fmt.type = str[spec--];
fmt.type = str[parse--];
break;
@@ -187,33 +195,33 @@ string format(const cstring& str, ArgsT&&...args) {
fmt.upper = true; [[fallthrough]];
case 'a': // float hex
fmt.base = 16;
fmt.type = str[spec--];
fmt.type = str[parse--];
break;
case 'E': // scientific notation
fmt.upper = true; [[fallthrough]];
case 'e':
fmt.base = 16;
fmt.type = str[spec--];
fmt.type = str[parse--];
break;
case 'F': // fixed precision
fmt.upper = true; [[fallthrough]];
case 'f':
fmt.base = 10;
fmt.type = str[spec--];
fmt.type = str[parse--];
break;
case 'G': // general precision
fmt.upper = true; [[fallthrough]];
case 'g':
fmt.base = 10;
fmt.type = str[spec--];
fmt.type = str[parse--];
break;
}
// early return
if (spec == colon) {
if (parse == colon) {
fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
res += argarray.format(arg, fmt);
i = end + 1;
@@ -227,6 +235,9 @@ string format(const cstring& str, ArgsT&&...args) {
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) {
@@ -234,7 +245,7 @@ string format(const cstring& str, ArgsT&&...args) {
}
// parse width and precision
while (isdigit(str[spec]) or (found_decimal = (str[spec] == '.')) or str[spec] == '{' or str[spec] == '}') {
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");
@@ -244,43 +255,72 @@ string format(const cstring& str, ArgsT&&...args) {
fmt.precision = x;
x = 0, j = 1;
--spec;
--parse;
continue;
}
// check for nested replacement field
if (str[spec] == '{') {
assertf(str[spec - 1] == '0' or str[spec - 1] == '.' or not isdigit(str[spec - 1]),
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[spec - 1] == '.';
size_t sub = str[spec + 1] == '}' ? ++arg_c : x;
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[spec] == '}') {
if (str[parse] == '}') {
--parse;
continue;
}
// crude way to only handle 0 case if 0 is the last digit
fmt.fill = str[spec] == '0' ? '0' : ' ';
fmt.fill = str[parse] == '0' ? '0' : ' ';
// parse the number
x += j * (str[spec] - '0');
x += j * (str[parse] - '0');
j *= 10;
--spec;
--parse;
}
if (x != 0) {
fmt.width = x;
}
// early return
if (spec == colon) {
if (parse == colon) {
fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
res += argarray.format(arg, fmt);
i = end + 1;
@@ -288,41 +328,41 @@ string format(const cstring& str, ArgsT&&...args) {
}
// check for alt form
if (str[spec] == '#') {
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;
--spec;
--parse;
}
// check for sign
if (str[spec] == '-' or str[spec] == '+' or str[spec] == ' ') {
fmt.sign = str[spec];
if (str[spec] == ' ') { // handle fill if only space, gets overwritten if encounters fill character
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 = ' ';
}
--spec;
--parse;
}
// check for alignment
if (str[spec] == '<' or str[spec] == '>' or str[spec] == '^') {
fmt.align = str[spec];
--spec;
if (str[parse] == '<' or str[parse] == '>' or str[parse] == '^') {
fmt.align = str[parse];
--parse;
}
// fill character
if (str[spec] != ':') {
fmt.fill = str[spec];
if (str[spec] == ' ') {
if (str[parse] != ':') {
fmt.fill = str[parse];
if (str[parse] == ' ') {
fmt.sign = fmt.sign == '\0' ? ' ' : fmt.sign;
}
--spec;
--parse;
}
// default sign
fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
// validate that we handled the entire format arg
assertf(spec == colon, "fennec::format syntax error, malformed format string detected, possible double colon");
assertf(parse == colon, "fennec::format syntax error, malformed format string detected, possible double colon");
// add formatted argument

View File

@@ -163,12 +163,75 @@ struct formatter<FloatT> {
string operator()(const format_arg& fmt, FloatT x) {
// nan & inf cases
if (isnan(x)) {
if (fennec::isnan(x)) {
return string("nan");
}
if (isinf(x)) {
if (fennec::isinf(x)) {
return string("inf");
}
char digits[128] = {};
auto chk = fennec::to_chars(digits, digits + sizeof(digits), fennec::abs(x), fmt.type, fmt.precision);
assertf(chk != nullptr, "fennec::format error, to_chars error");
size_t len = chk - digits;
// handle uppercase
if (fmt.upper) {
for (auto& digit : digits) {
if (digit == 0) {
break;
}
digit = toupper(digit);
}
}
const bool has_sign = (x < 0 or fmt.sign != '-');
const bool zero = fmt.fill == '0';
const size_t prefix = fmt.alt ? 2 : 0;
const size_t sgnlen = len + (zero ? has_sign + prefix : 0);
const size_t explen = fennec::max(sgnlen, fmt.width) + (zero ? 0 : has_sign + prefix);
const size_t fill = fmt.width > sgnlen ? fmt.width - sgnlen : 0;
size_t sign = 0;
string res = string(explen);
if (fill > 0) {
switch (fmt.align) {
case '<':
memcpy(res.data() + has_sign + prefix, digits, len);
memset(res.data() + has_sign + prefix + len, fmt.fill == '0' ? ' ' : fmt.fill, fill);
break;
case '>': case '\0':
memcpy(res.data() + explen - len, digits, len);
sign = fmt.fill == '0' ? 0 : explen - len - 1 - prefix;
memset(res.data(), fmt.fill, explen - len);
break;
case '^':
size_t bef = fill / 2 + has_sign + prefix;
size_t aft = explen - bef;
memcpy(res.data() + bef, digits, len);
sign = fmt.fill == '0' ? 0 : bef - 1 - prefix;
memset(res.data(), fmt.fill, bef);
memset(res.data() + bef + len, fmt.fill == '0' ? ' ' : fmt.fill, aft);
break;
}
} else {
memcpy(res.data() + has_sign + prefix, digits, len);
}
if (has_sign) {
res[sign] = (x < 0) ? '-' : fmt.sign;
}
if (prefix) {
res[sign + has_sign] = '0';
res[sign + has_sign + 1] = fmt.type + ('x' - 'a');
}
return res;
}

View File

@@ -19,6 +19,7 @@
#ifndef FENNEC_TEST_PRINTING_H
#define FENNEC_TEST_PRINTING_H
#include <iomanip>
#include <iostream>
#include <fennec/filesystem/path.h>
@@ -63,17 +64,17 @@ inline std::ostream& operator<<(std::ostream& os, const quaternion<ScalarT>& q)
// Helper for printing strings
inline std::ostream& operator<<(std::ostream& os, const cstring& str) {
return os << str.data();
return os << std::quoted(str.data());
}
// Helper for printing strings
inline std::ostream& operator<<(std::ostream& os, const string& str) {
return os << str.cstr();
return os << std::quoted(str.cstr());
}
// Helper for printing strings
inline std::ostream& operator<<(std::ostream& os, const path& str) {
return os << str.str();
return os << std::quoted(str.cstr());
}
// Helper for printing types

View File

@@ -83,6 +83,22 @@ inline void fennec_test_format() {
fennec_test_run(fennec::format("{},{}", true, false), string("true,false"));
fennec_test_run(fennec::format("{:#06b},{:#06b}", true, false), string("0b0001,0b0000"));
fennec_test_spacer(1);
fennec_test_run(fennec::format("{:10f}", fennec::pi<float>()), string(" 3.141593"));
fennec_test_run(fennec::format("{:{}f}", fennec::pi<float>(), 10), string(" 3.141593"));
fennec_test_run(fennec::format("{:.5f}", fennec::pi<float>()), string("3.14159"));
fennec_test_run(fennec::format("{:.{}f}", fennec::pi<float>(), 5), string("3.14159"));
fennec_test_run(fennec::format("{:10.5f}", fennec::pi<float>()), string(" 3.14159"));
fennec_test_run(fennec::format("{:{}.{}f}", fennec::pi<float>(), 10, 5), string(" 3.14159"));
fennec_test_spacer(1);
fennec_test_run(fennec::format("{:.5f}", fennec::pi<float>()), string("3.14159"));
fennec_test_run(fennec::format("{:.5a}", fennec::pi<float>()), string("1.921fbp+1"));
fennec_test_run(fennec::format("{:.5g}", fennec::pi<float>()), string("3.1416"));
fennec_test_run(fennec::format("{:.5e}", fennec::pi<float>()), string("3.14159e+00"));
}
}