From 9553f9b6621fdba4ad500c575b5325cd24549854 Mon Sep 17 00:00:00 2001 From: Medusa Slockbower Date: Mon, 8 Dec 2025 12:13:51 -0500 Subject: [PATCH] - formatting implemented for floating point types - fixed some bugs with width and precision specifiers: * Evaluation order of nested replacement fields --- include/fennec/format/format.h | 126 ++++++++++++++++++++---------- include/fennec/format/formatter.h | 67 +++++++++++++++- test/printing.h | 7 +- test/tests/test_format.h | 16 ++++ 4 files changed, 168 insertions(+), 48 deletions(-) diff --git a/include/fennec/format/format.h b/include/fennec/format/format.h index 5451173..b55b9ed 100644 --- a/include/fennec/format/format.h +++ b/include/fennec/format/format.h @@ -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 diff --git a/include/fennec/format/formatter.h b/include/fennec/format/formatter.h index b93a215..9bbda89 100644 --- a/include/fennec/format/formatter.h +++ b/include/fennec/format/formatter.h @@ -163,12 +163,75 @@ struct formatter { 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; } diff --git a/test/printing.h b/test/printing.h index f7736f0..b6c9690 100644 --- a/test/printing.h +++ b/test/printing.h @@ -19,6 +19,7 @@ #ifndef FENNEC_TEST_PRINTING_H #define FENNEC_TEST_PRINTING_H +#include #include #include @@ -63,17 +64,17 @@ inline std::ostream& operator<<(std::ostream& os, const quaternion& 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 diff --git a/test/tests/test_format.h b/test/tests/test_format.h index 5890d41..a38ceb8 100644 --- a/test/tests/test_format.h +++ b/test/tests/test_format.h @@ -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()), string(" 3.141593")); + fennec_test_run(fennec::format("{:{}f}", fennec::pi(), 10), string(" 3.141593")); + fennec_test_run(fennec::format("{:.5f}", fennec::pi()), string("3.14159")); + fennec_test_run(fennec::format("{:.{}f}", fennec::pi(), 5), string("3.14159")); + fennec_test_run(fennec::format("{:10.5f}", fennec::pi()), string(" 3.14159")); + fennec_test_run(fennec::format("{:{}.{}f}", fennec::pi(), 10, 5), string(" 3.14159")); + + fennec_test_spacer(1); + + fennec_test_run(fennec::format("{:.5f}", fennec::pi()), string("3.14159")); + fennec_test_run(fennec::format("{:.5a}", fennec::pi()), string("1.921fbp+1")); + fennec_test_run(fennec::format("{:.5g}", fennec::pi()), string("3.1416")); + fennec_test_run(fennec::format("{:.5e}", fennec::pi()), string("3.14159e+00")); } }