Fix formatting of very long fixed-point numbers

This commit is contained in:
Rangi42
2025-09-04 12:42:54 -04:00
parent bdc885bd69
commit 891e6f98df
3 changed files with 30 additions and 37 deletions

View File

@@ -29,50 +29,42 @@ static size_t parseNumber(char const *spec, size_t &value) {
size_t FormatSpec::parseSpec(char const *spec) { size_t FormatSpec::parseSpec(char const *spec) {
size_t i = 0; size_t i = 0;
// <sign> // <sign>
if (char c = spec[i]; c == ' ' || c == '+') { if (char c = spec[i]; c == ' ' || c == '+') {
++i; ++i;
sign = c; sign = c;
} }
// <exact> // <exact>
if (spec[i] == '#') { if (spec[i] == '#') {
++i; ++i;
exact = true; exact = true;
} }
// <align> // <align>
if (spec[i] == '-') { if (spec[i] == '-') {
++i; ++i;
alignLeft = true; alignLeft = true;
} }
// <pad> // <pad>
if (spec[i] == '0') { if (spec[i] == '0') {
++i; ++i;
padZero = true; padZero = true;
} }
// <width> // <width>
if (isDigit(spec[i])) { if (isDigit(spec[i])) {
i += parseNumber(&spec[i], width); i += parseNumber(&spec[i], width);
} }
// <frac> // <frac>
if (spec[i] == '.') { if (spec[i] == '.') {
++i; ++i;
hasFrac = true; hasFrac = true;
i += parseNumber(&spec[i], fracWidth); i += parseNumber(&spec[i], fracWidth);
} }
// <prec> // <prec>
if (spec[i] == 'q') { if (spec[i] == 'q') {
++i; ++i;
hasPrec = true; hasPrec = true;
i += parseNumber(&spec[i], precision); i += parseNumber(&spec[i], precision);
} }
// <type> // <type>
switch (char c = spec[i]; c) { switch (char c = spec[i]; c) {
case 'd': case 'd':
@@ -87,7 +79,7 @@ size_t FormatSpec::parseSpec(char const *spec) {
type = c; type = c;
break; break;
} }
// Done parsing
parsed = true; parsed = true;
return i; return i;
} }
@@ -188,36 +180,32 @@ void FormatSpec::appendNumber(std::string &str, uint32_t value) const {
if (useType == 'd' || useType == 'f') { if (useType == 'd' || useType == 'f') {
if (int32_t v = value; v < 0) { if (int32_t v = value; v < 0) {
signChar = '-'; signChar = '-';
if (v != INT32_MIN) { if (v != INT32_MIN) { // -INT32_MIN is UB
value = -v; value = -v;
} }
} }
} }
char prefixChar = !useExact ? 0 // The longest possible formatted number is fixed-point with 10 digits, 255 fractional digits,
: useType == 'X' ? '$' // and a precision suffix, for 270 total bytes (counting the NUL terminator).
: useType == 'x' ? '$' // (Actually 269 since a 2-digit precision cannot reach 10 integer digits.)
: useType == 'b' ? '%' // Make the buffer somewhat larger just in case.
: useType == 'o' ? '&' char valueBuf[300];
: 0;
char valueBuf[262]; // Max 5 digits + decimal + 255 fraction digits + terminator
if (useType == 'b') { if (useType == 'b') {
// Special case for binary // Special case for binary (since `snprintf` doesn't support it)
char *ptr = valueBuf;
// Buffer the digits from least to greatest
char *ptr = valueBuf;
do { do {
*ptr++ = (value & 1) + '0'; *ptr++ = (value & 1) + '0';
value >>= 1; value >>= 1;
} while (value); } while (value);
// Reverse the digits // Reverse the digits and terminate the string
std::reverse(valueBuf, ptr); std::reverse(valueBuf, ptr);
*ptr = '\0'; *ptr = '\0';
} else if (useType == 'f') { } else if (useType == 'f') {
// Special case for fixed-point // Special case for fixed-point (since it needs fractional part and precision)
// Default fractional width (C++'s is 6 for "%f"; here 5 is enough for Q16.16) // Default fractional width (C++'s is 6 for "%f"; here 5 is enough for Q16.16)
size_t useFracWidth = hasFrac ? fracWidth : 5; size_t useFracWidth = hasFrac ? fracWidth : 5;
@@ -226,6 +214,7 @@ void FormatSpec::appendNumber(std::string &str, uint32_t value) const {
useFracWidth = 255; useFracWidth = 255;
} }
// Default precision taken from default `-Q` option
size_t defaultPrec = options.fixPrecision; size_t defaultPrec = options.fixPrecision;
size_t usePrec = hasPrec ? precision : defaultPrec; size_t usePrec = hasPrec ? precision : defaultPrec;
if (usePrec < 1 || usePrec > 31) { if (usePrec < 1 || usePrec > 31) {
@@ -237,29 +226,30 @@ void FormatSpec::appendNumber(std::string &str, uint32_t value) const {
usePrec = defaultPrec; usePrec = defaultPrec;
} }
// Floating-point formatting works for all fixed-point values
double fval = fabs(value / pow(2.0, usePrec)); double fval = fabs(value / pow(2.0, usePrec));
if (int fracWidthArg = static_cast<int>(useFracWidth); useExact) { if (int fracWidthArg = static_cast<int>(useFracWidth); useExact) {
snprintf(valueBuf, sizeof(valueBuf), "%.*fq%zu", fracWidthArg, fval, usePrec); snprintf(valueBuf, sizeof(valueBuf), "%.*fq%zu", fracWidthArg, fval, usePrec);
} else { } else {
snprintf(valueBuf, sizeof(valueBuf), "%.*f", fracWidthArg, fval); snprintf(valueBuf, sizeof(valueBuf), "%.*f", fracWidthArg, fval);
} }
} else if (useType == 'd') {
// Decimal numbers may be formatted with a '-' sign by `snprintf`, so `abs` prevents that,
// with a special case for `INT32_MIN` since `labs(INT32_MIN)` is UB. The sign will be
// printed later from `signChar`.
uint32_t uval =
value != static_cast<uint32_t>(INT32_MIN) ? labs(static_cast<int32_t>(value)) : value;
snprintf(valueBuf, sizeof(valueBuf), "%" PRIu32, uval);
} else { } else {
char const *spec = useType == 'u' ? "%" PRIu32 // `value` has already been made non-negative, so type 'd' is OK here even for `INT32_MIN`.
: useType == 'X' ? "%" PRIX32 // The sign will be printed later from `signChar`.
: useType == 'x' ? "%" PRIx32 char const *spec = useType == 'd' || useType == 'u' ? "%" PRIu32
: useType == 'o' ? "%" PRIo32 : useType == 'X' ? "%" PRIX32
: "%" PRIu32; : useType == 'x' ? "%" PRIx32
: useType == 'o' ? "%" PRIo32
: "%" PRIu32;
snprintf(valueBuf, sizeof(valueBuf), spec, value); snprintf(valueBuf, sizeof(valueBuf), spec, value);
} }
char prefixChar = !useExact ? 0
: useType == 'X' || useType == 'x' ? '$'
: useType == 'b' ? '%'
: useType == 'o' ? '&'
: 0;
size_t valueLen = strlen(valueBuf); size_t valueLen = strlen(valueBuf);
size_t numLen = (signChar != 0) + (prefixChar != 0) + valueLen; size_t numLen = (signChar != 0) + (prefixChar != 0) + valueLen;
size_t totalLen = width > numLen ? width : numLen; size_t totalLen = width > numLen ? width : numLen;

View File

@@ -6,3 +6,5 @@ ENDM
test $8000_0000 ; INT32_MIN test $8000_0000 ; INT32_MIN
test $0000_0000 ; UINT32_MIN test $0000_0000 ; UINT32_MIN
test $ffff_ffff ; UINT32_MAX test $ffff_ffff ; UINT32_MAX
println strfmt("%#.255q1f", $7fff_ffff)

View File

@@ -2,3 +2,4 @@ $7fffffff = &17777777777 = %01111111111111111111111111111111 = 2147483647U = +21
$80000000 = &20000000000 = %10000000000000000000000000000000 = 2147483648U = -2147483648 = -32768.0000000000000000 $80000000 = &20000000000 = %10000000000000000000000000000000 = 2147483648U = -2147483648 = -32768.0000000000000000
$00000000 = &00000000000 = %00000000000000000000000000000000 = 0U = +0 = +0.0000000000000000 $00000000 = &00000000000 = %00000000000000000000000000000000 = 0U = +0 = +0.0000000000000000
$ffffffff = &37777777777 = %11111111111111111111111111111111 = 4294967295U = -1 = -0.0000152587890625 $ffffffff = &37777777777 = %11111111111111111111111111111111 = 4294967295U = -1 = -0.0000152587890625
1073741823.500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000q1