- 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 // we're going to parse right-to-left since the valid combinations
// of specifiers change based on the type of the argument // 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 // first find the matching '}' brace, e is not necessarily the matching brace
// since some specifiers allow nested replacement fields // since some specifiers allow nested replacement fields
size_t spec = colon; size_t parse = colon;
while (str[spec + 1] != '}') { while (str[parse + 1] != '}') {
if (next_brace < end) { // if the next brace is before the next closing brace if (next_brace < end) { // if the next brace is before the next closing brace
spec = end + 1; ++nnrf;
end = str.find('}', spec); nrfd += str[end - 1] == '{';
next_brace = str.find('{', spec); parse = end + 1;
end = str.find('}', parse);
next_brace = str.find('{', parse);
} else { } else {
spec = end - 1; parse = end - 1;
break; 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 // check type
switch (str[spec]) { switch (str[parse]) {
default: break; default: break;
case 's': case '?': // strings case 's': case '?': // strings
case 'c': // char case 'c': // char
fmt.type = str[spec--]; fmt.type = str[parse--];
break; break;
case 'd': // decimal case 'd': // decimal
fmt.base = 10; fmt.base = 10;
fmt.type = str[spec--]; fmt.type = str[parse--];
break; break;
case 'B': // binary case 'B': // binary
fmt.upper = true; [[fallthrough]]; fmt.upper = true; [[fallthrough]];
case 'b': case 'b':
fmt.base = 2; fmt.base = 2;
fmt.type = str[spec--]; fmt.type = str[parse--];
break; break;
case 'o': // octal case 'o': // octal
fmt.base = 8; fmt.base = 8;
fmt.type = str[spec--]; fmt.type = str[parse--];
break; break;
case 'X': // hex case 'X': // hex
fmt.upper = true; [[fallthrough]]; fmt.upper = true; [[fallthrough]];
case 'x': case 'x':
fmt.base = 16; fmt.base = 16;
fmt.type = str[spec--]; fmt.type = str[parse--];
break; break;
@@ -187,33 +195,33 @@ string format(const cstring& str, ArgsT&&...args) {
fmt.upper = true; [[fallthrough]]; fmt.upper = true; [[fallthrough]];
case 'a': // float hex case 'a': // float hex
fmt.base = 16; fmt.base = 16;
fmt.type = str[spec--]; fmt.type = str[parse--];
break; break;
case 'E': // scientific notation case 'E': // scientific notation
fmt.upper = true; [[fallthrough]]; fmt.upper = true; [[fallthrough]];
case 'e': case 'e':
fmt.base = 16; fmt.base = 16;
fmt.type = str[spec--]; fmt.type = str[parse--];
break; break;
case 'F': // fixed precision case 'F': // fixed precision
fmt.upper = true; [[fallthrough]]; fmt.upper = true; [[fallthrough]];
case 'f': case 'f':
fmt.base = 10; fmt.base = 10;
fmt.type = str[spec--]; fmt.type = str[parse--];
break; break;
case 'G': // general precision case 'G': // general precision
fmt.upper = true; [[fallthrough]]; fmt.upper = true; [[fallthrough]];
case 'g': case 'g':
fmt.base = 10; fmt.base = 10;
fmt.type = str[spec--]; fmt.type = str[parse--];
break; break;
} }
// early return // early return
if (spec == colon) { if (parse == colon) {
fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign; fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
res += argarray.format(arg, fmt); res += argarray.format(arg, fmt);
i = end + 1; 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_float_t = detail::_isfmt_f(fmt.type);
bool is_str_t = fmt.type == 's'; bool is_str_t = fmt.type == 's';
bool is_integer_t = detail::_isfmt_i(fmt.type); 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 // default "precision" for strings should be 0 for no limit
if (is_str_t) { if (is_str_t) {
@@ -234,7 +245,7 @@ string format(const cstring& str, ArgsT&&...args) {
} }
// parse width and precision // 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 // handle decimal point for precision
if (found_decimal) { if (found_decimal) {
assertf(is_float_t or is_str_t, "fennec::format syntax error, encountered precision argument on non-floating point format"); 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; fmt.precision = x;
x = 0, j = 1; x = 0, j = 1;
--spec; --parse;
continue; continue;
} }
// check for nested replacement field // check for nested replacement field
if (str[spec] == '{') { if (str[parse] == '{') {
assertf(str[spec - 1] == '0' or str[spec - 1] == '.' or not isdigit(str[spec - 1]), 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"); "fennec::format syntax error, unexpected digit preceding nested replacement field");
bool prec = str[spec - 1] == '.'; bool prec = str[parse - 1] == '.';
size_t sub = str[spec + 1] == '}' ? ++arg_c : x; 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(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"); 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); (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 // ignore closing brace for nested replacement fields
if (str[spec] == '}') { if (str[parse] == '}') {
--parse;
continue; continue;
} }
// crude way to only handle 0 case if 0 is the last digit // 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 // parse the number
x += j * (str[spec] - '0'); x += j * (str[parse] - '0');
j *= 10; j *= 10;
--spec; --parse;
} }
if (x != 0) { if (x != 0) {
fmt.width = x; fmt.width = x;
} }
// early return // early return
if (spec == colon) { if (parse == colon) {
fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign; fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
res += argarray.format(arg, fmt); res += argarray.format(arg, fmt);
i = end + 1; i = end + 1;
@@ -288,41 +328,41 @@ string format(const cstring& str, ArgsT&&...args) {
} }
// check for alt form // 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"); assertf(is_float_t or is_integer_t, "fennec::format syntax error, encountered alt spec ('#') with non-decimal type");
fmt.alt = true; fmt.alt = true;
--spec; --parse;
} }
// check for sign // check for sign
if (str[spec] == '-' or str[spec] == '+' or str[spec] == ' ') { if (str[parse] == '-' or str[parse] == '+' or str[parse] == ' ') {
fmt.sign = str[spec]; fmt.sign = str[parse];
if (str[spec] == ' ') { // handle fill if only space, gets overwritten if encounters fill character if (str[parse] == ' ') { // handle fill if only space, gets overwritten if encounters fill character
fmt.fill = ' '; fmt.fill = ' ';
} }
--spec; --parse;
} }
// check for alignment // check for alignment
if (str[spec] == '<' or str[spec] == '>' or str[spec] == '^') { if (str[parse] == '<' or str[parse] == '>' or str[parse] == '^') {
fmt.align = str[spec]; fmt.align = str[parse];
--spec; --parse;
} }
// fill character // fill character
if (str[spec] != ':') { if (str[parse] != ':') {
fmt.fill = str[spec]; fmt.fill = str[parse];
if (str[spec] == ' ') { if (str[parse] == ' ') {
fmt.sign = fmt.sign == '\0' ? ' ' : fmt.sign; fmt.sign = fmt.sign == '\0' ? ' ' : fmt.sign;
} }
--spec; --parse;
} }
// default sign // default sign
fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign; fmt.sign = fmt.sign == '\0' ? '-' : fmt.sign;
// validate that we handled the entire format arg // 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 // add formatted argument

View File

@@ -163,12 +163,75 @@ struct formatter<FloatT> {
string operator()(const format_arg& fmt, FloatT x) { string operator()(const format_arg& fmt, FloatT x) {
// nan & inf cases // nan & inf cases
if (isnan(x)) { if (fennec::isnan(x)) {
return string("nan"); return string("nan");
} }
if (isinf(x)) { if (fennec::isinf(x)) {
return string("inf"); 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 #ifndef FENNEC_TEST_PRINTING_H
#define FENNEC_TEST_PRINTING_H #define FENNEC_TEST_PRINTING_H
#include <iomanip>
#include <iostream> #include <iostream>
#include <fennec/filesystem/path.h> #include <fennec/filesystem/path.h>
@@ -63,17 +64,17 @@ inline std::ostream& operator<<(std::ostream& os, const quaternion<ScalarT>& q)
// Helper for printing strings // Helper for printing strings
inline std::ostream& operator<<(std::ostream& os, const cstring& str) { inline std::ostream& operator<<(std::ostream& os, const cstring& str) {
return os << str.data(); return os << std::quoted(str.data());
} }
// Helper for printing strings // Helper for printing strings
inline std::ostream& operator<<(std::ostream& os, const string& str) { inline std::ostream& operator<<(std::ostream& os, const string& str) {
return os << str.cstr(); return os << std::quoted(str.cstr());
} }
// Helper for printing strings // Helper for printing strings
inline std::ostream& operator<<(std::ostream& os, const path& str) { inline std::ostream& operator<<(std::ostream& os, const path& str) {
return os << str.str(); return os << std::quoted(str.cstr());
} }
// Helper for printing types // 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("{},{}", true, false), string("true,false"));
fennec_test_run(fennec::format("{:#06b},{:#06b}", true, false), string("0b0001,0b0000")); 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"));
} }
} }