Fix format: float zero-padding, nested replacement width, octal alternate form, and extreme doubles (#1442)

format.h:
- Fix float zero-padding ({:010f}): output sign first, then zero-fill,
  then the unsigned formatted value, matching std::format behavior.
- Fix nested replacement fields ({:{}d}): consume the value's auto-index
  in parse_format_spec before parsing nested width/precision fields, so
  auto-indexing order matches the C++ standard.
- Fix {:#o} with value 0: produce "0" instead of "00" by skipping the
  octal prefix when the value is zero.
- Fix format_floating_default overflow for extreme doubles (DBL_MIN,
  DBL_MAX): fall back to scientific notation for values >= 1e18 or
  tiny positives < 1e-6, delegating to format_floating_e.
- Fix format_floating_e precision loss: replace iterative multiply-by-10
  normalization loop with O(1) log10/pow/floor computation.
- Add resolve_nested_replacements helper to extract width/precision
  from format args at formatting time.

test_format.cpp:
- Add tests for float zero-padding, nested replacement width, octal
  alternate form with zero, float sign/width/alignment, negative floats,
  scientific notation for large/small values, default-to-scientific
  switch, positive zero, brace escaping, and integer limits.

format.h + platform.h:
- log10l fix for different toolchain support:
  Define ETL_FORMAT_NO_LONG_DOUBLE_MATH in the profile if libm doesn't
  provide log10l. This is identified by linker error missing this symbol.
  It was identified with the llvm/clang cross toolchain for ARM.
This commit is contained in:
Roland Reichwein 2026-05-26 05:19:42 +02:00 committed by GitHub
parent 41174ed7f6
commit 9765cbf764
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 414 additions and 55 deletions

View File

@ -1104,7 +1104,20 @@ namespace etl
format_spec = format_spec_t(); // reset format_spec to defaults
format_spec.index = parse_num(parse_ctx); // optional
format_spec.index = parse_num(parse_ctx); // optional explicit index
// Consume the value's auto-index before parsing the format spec body,
// so that nested replacement fields for width/precision get correct
// auto-indices. Per C++ standard, in {:{}}, the value arg is consumed
// first (arg 0), then the width arg (arg 1).
if (!format_spec.index.has_value())
{
format_spec.index = parse_ctx.next_arg_id();
}
else
{
parse_ctx.check_arg_id(*format_spec.index);
}
bool colon = parse_char(parse_ctx, ':');
if (colon)
@ -1239,7 +1252,7 @@ namespace etl
}
template <typename OutputIt, typename T>
void format_alternate_form(OutputIt& it, const format_spec_t& spec)
void format_alternate_form(OutputIt& it, T value, const format_spec_t& spec)
{
if (spec.hash && spec.type.has_value())
{
@ -1247,7 +1260,13 @@ namespace etl
{
case 'b': format_sequence(it, "0b"); break;
case 'B': format_sequence(it, "0B"); break;
case 'o': format_sequence(it, "0"); break;
case 'o':
// Per C++ standard, # for octal adds leading 0 only if not already present
if (value != 0)
{
format_sequence(it, "0");
}
break;
case 'x': format_sequence(it, "0x"); break;
case 'X':
format_sequence(it, "0X");
@ -1401,23 +1420,93 @@ namespace etl
{
size_t width = 0;
format_sign<OutputIt, T>(it, value, spec);
format_alternate_form<OutputIt, T>(it, spec);
format_alternate_form<OutputIt, T>(it, value, spec);
adjust_width_from_spec(spec, width);
check_precision(spec);
format_plain_num(it, value, spec, width);
}
#if ETL_USING_FORMAT_FLOATING_POINT
#if ETL_NOT_USING_FORMAT_LONG_DOUBLE_MATH
//***********************************
// Math function wrappers to handle toolchains that don't provide
// long double math functions (log10l, floorl, powl, modfl, roundl).
// When ETL_FORMAT_NO_LONG_DOUBLE_MATH is defined, long double overloads
// cast through double. For float and double, the standard functions are
// called directly via the template versions.
//***********************************
inline long double format_log10(long double value)
{
return static_cast<long double>(::log10(static_cast<double>(value)));
}
inline long double format_floor(long double value)
{
return static_cast<long double>(::floor(static_cast<double>(value)));
}
inline long double format_pow(long double base, long double exp)
{
return static_cast<long double>(::pow(static_cast<double>(base), static_cast<double>(exp)));
}
inline long double format_round(long double value)
{
return static_cast<long double>(::round(static_cast<double>(value)));
}
inline long double format_modf(long double value, long double* iptr)
{
double d_iptr;
double result = ::modf(static_cast<double>(value), &d_iptr);
*iptr = static_cast<long double>(d_iptr);
return static_cast<long double>(result);
}
#endif
template <typename T>
T format_log10(T value)
{
return ::log10(value);
}
template <typename T>
T format_floor(T value)
{
return ::floor(value);
}
template <typename T>
T format_pow(T base, T exp)
{
return ::pow(base, exp);
}
template <typename T>
T format_round(T value)
{
return ::round(value);
}
template <typename T>
T format_modf(T value, T* iptr)
{
return ::modf(value, iptr);
}
template <typename OutputIt, typename T>
void format_floating_default(OutputIt& it, T value, const format_spec_t& spec)
{
const size_t fractional_decimals = 6; // default
// Detect sign using signbit to correctly handle -0.0
bool sign = signbit(value);
bool sign = signbit(value);
T abs_value = sign ? -value : value;
// Use scientific notation for values that would overflow unsigned long long
// or that are too small for meaningful fixed-point digits
if (abs_value >= static_cast<T>(1e18) || (abs_value > static_cast<T>(0) && abs_value < static_cast<T>(1e-6)))
{
format_spec_t spec_e = spec;
spec_e.type = 'e';
format_floating_e(it, value, spec_e);
return;
}
T integral;
T fractional = modf(value, &integral);
T fractional = format_modf(value, &integral);
// Take absolute values to avoid casting negative values to unsigned
if (sign)
@ -1427,7 +1516,7 @@ namespace etl
}
unsigned long long int scale = int_pow<unsigned long long int>(10, fractional_decimals);
unsigned long long int fractional_int = static_cast<unsigned long long int>(round(fractional * scale));
unsigned long long int fractional_int = static_cast<unsigned long long int>(format_round(fractional * scale));
unsigned long long int integral_int = static_cast<unsigned long long int>(integral);
if (fractional_int == scale)
@ -1454,20 +1543,20 @@ namespace etl
bool sign = signbit(value);
T integral;
T fractional = modf(value, &integral);
T fractional = format_modf(value, &integral);
while (value >= 0x10 || value <= -0x10)
{
++exponent_int;
value /= 0x10;
fractional = modf(value, &integral);
fractional = format_modf(value, &integral);
}
while ((value > 0.0000000000001 && value < 1) || (value < -0.0000000000001 && value > -1))
{
--exponent_int;
value *= 0x10;
fractional = modf(value, &integral);
fractional = format_modf(value, &integral);
}
// Take absolute values to avoid casting negative values to unsigned
@ -1478,7 +1567,7 @@ namespace etl
}
unsigned long long int scale = int_pow<unsigned long long int>(0x10, fractional_decimals);
unsigned long long int fractional_int = static_cast<unsigned long long int>(round(fractional * scale));
unsigned long long int fractional_int = static_cast<unsigned long long int>(format_round(fractional * scale));
unsigned long long int integral_int = static_cast<unsigned long long int>(integral);
if (fractional_int == scale)
@ -1516,40 +1605,48 @@ namespace etl
long long int exponent_int = 0;
// Detect sign using signbit to correctly handle -0.0
bool sign = std::signbit(value);
bool sign = signbit(value);
T abs_value = sign ? -value : value;
if (abs_value > static_cast<T>(0))
{
exponent_int = static_cast<long long int>(format_floor(format_log10(abs_value)));
value = abs_value / format_pow(static_cast<T>(10), static_cast<T>(exponent_int));
// Correct for floating-point rounding in log10/pow
if (value >= static_cast<T>(10))
{
value /= static_cast<T>(10);
++exponent_int;
}
else if (value < static_cast<T>(1))
{
value *= static_cast<T>(10);
--exponent_int;
}
}
else
{
value = static_cast<T>(0);
}
T integral;
T fractional = modf(value, &integral);
while (value >= 10 || value <= -10)
{
++exponent_int;
value /= 10;
fractional = modf(value, &integral);
}
while ((value > 0.0000000000001 && value < 1) || (value < -0.0000000000001 && value > -1))
{
--exponent_int;
value *= 10;
fractional = modf(value, &integral);
}
// Take absolute values to avoid casting negative values to unsigned
if (sign)
{
fractional = -fractional;
integral = -integral;
}
T fractional = format_modf(value, &integral);
unsigned long long int scale = int_pow<unsigned long long int>(10, fractional_decimals);
unsigned long long int fractional_int = static_cast<unsigned long long int>(round(fractional * scale));
unsigned long long int fractional_int = static_cast<unsigned long long int>(format_round(fractional * scale));
unsigned long long int integral_int = static_cast<unsigned long long int>(integral);
if (fractional_int == scale)
{
fractional_int = 0;
++integral_int;
if (integral_int == 10)
{
integral_int = 1;
++exponent_int;
}
}
private_format::format_sign<OutputIt, int>(it, sign ? -1 : 0, spec);
@ -1572,10 +1669,10 @@ namespace etl
const size_t fractional_decimals = 6; // default
// Detect sign using signbit to correctly handle -0.0
bool sign = std::signbit(value);
bool sign = signbit(value);
T integral;
T fractional = modf(value, &integral);
T fractional = format_modf(value, &integral);
// Take absolute values to avoid casting negative values to unsigned
if (sign)
@ -1585,7 +1682,7 @@ namespace etl
}
unsigned long long int scale = int_pow<unsigned long long int>(10, fractional_decimals);
unsigned long long int fractional_int = static_cast<unsigned long long int>(round(fractional * scale));
unsigned long long int fractional_int = static_cast<unsigned long long int>(format_round(fractional * scale));
unsigned long long int integral_int = static_cast<unsigned long long int>(integral);
if (fractional_int == scale)
@ -1833,6 +1930,64 @@ namespace etl
fmt_context.advance_to(tmp);
}
// Visitor to extract an integer value as size_t from a format arg (for nested replacement fields)
struct size_t_extractor
{
size_t value;
size_t_extractor()
: value(0)
{
}
void operator()(int v)
{
value = static_cast<size_t>(v);
}
void operator()(unsigned int v)
{
value = static_cast<size_t>(v);
}
void operator()(long long int v)
{
value = static_cast<size_t>(v);
}
void operator()(unsigned long long int v)
{
value = static_cast<size_t>(v);
}
// All other types are invalid for width/precision - ignore
template <typename T>
void operator()(T)
{
}
};
// Resolve nested replacement fields for width and precision in the format spec.
// When width_nested_replacement or precision_nested_replacement is true, the
// width/precision value holds the arg index, which must be resolved to the actual value.
template <class OutputIt>
void resolve_nested_replacements(format_spec_t& spec, format_args<OutputIt>& args)
{
if (spec.width_nested_replacement && spec.width.has_value())
{
format_arg<OutputIt> width_arg = args.get(spec.width.value());
size_t_extractor ext;
width_arg.template visit<void>(ext);
spec.width = ext.value;
spec.width_nested_replacement = false;
}
if (spec.precision_nested_replacement && spec.precision.has_value())
{
format_arg<OutputIt> prec_arg = args.get(spec.precision.value());
size_t_extractor ext;
prec_arg.template visit<void>(ext);
spec.precision = ext.value;
spec.precision_nested_replacement = false;
}
}
// Compute prefix/suffix padding sizes for alignment.
// default_align_start: if true, NONE defaults to left-align (START); otherwise right-align (END).
inline void compute_padding(size_t pad, spec_align_t align, bool default_align_start, size_t& prefix_size, size_t& suffix_size)
@ -1900,6 +2055,13 @@ namespace etl
size_t prefix_size = 0;
size_t suffix_size = 0;
// For zero-padding ({:0Nf}), use '0' as fill and right-align (padding after sign)
char_type fill_char = fmt_ctx.format_spec.fill;
if (fmt_ctx.format_spec.zero && fmt_ctx.format_spec.align == spec_align_t::NONE)
{
fill_char = '0';
}
if (fmt_ctx.format_spec.width)
{
private_format::counter_iterator counter;
@ -1908,15 +2070,61 @@ namespace etl
if (counter.value() < fmt_ctx.format_spec.width.value())
{
size_t pad = fmt_ctx.format_spec.width.value() - counter.value();
compute_padding(pad, fmt_ctx.format_spec.align, false, prefix_size, suffix_size);
if (fmt_ctx.format_spec.zero && fmt_ctx.format_spec.align == spec_align_t::NONE)
{
// Zero-padding: all padding goes between sign and digits
prefix_size = pad;
}
else
{
compute_padding(pad, fmt_ctx.format_spec.align, false, prefix_size, suffix_size);
}
}
}
// actual output
OutputIt it = fmt_ctx.out();
private_format::fill<OutputIt>(it, prefix_size, fmt_ctx.format_spec.fill);
private_format::format_floating<OutputIt, Float>(it, arg, fmt_ctx.format_spec);
private_format::fill<OutputIt>(it, suffix_size, fmt_ctx.format_spec.fill);
if (fmt_ctx.format_spec.zero && fmt_ctx.format_spec.align == spec_align_t::NONE)
{
// Output sign first, then zero-fill, then the unsigned part
bool sign = signbit(arg);
if (sign || fmt_ctx.format_spec.sign != spec_sign_t::MINUS)
{
// Output the sign character
char_type sc = '\0';
if (sign)
{
sc = '-';
}
else
{
switch (fmt_ctx.format_spec.sign)
{
case spec_sign_t::PLUS: sc = '+'; break;
case spec_sign_t::SPACE: sc = ' '; break;
default: break;
}
}
if (sc != '\0')
{
*it = sc;
++it;
}
}
private_format::fill<OutputIt>(it, prefix_size, '0');
// Format without sign (sign already emitted)
format_spec_t no_sign_spec = fmt_ctx.format_spec;
no_sign_spec.sign = spec_sign_t::MINUS;
Float abs_arg = sign ? -arg : arg;
private_format::format_floating<OutputIt, Float>(it, abs_arg, no_sign_spec);
}
else
{
private_format::fill<OutputIt>(it, prefix_size, fill_char);
private_format::format_floating<OutputIt, Float>(it, arg, fmt_ctx.format_spec);
private_format::fill<OutputIt>(it, suffix_size, fill_char);
}
return it;
}
#endif
@ -2414,16 +2622,13 @@ namespace etl
else
{
private_format::parse_format_spec<OutputIt>(parse_context, fmt_context);
etl::optional<size_t> index = fmt_context.format_spec.index;
if (index.has_value())
{
parse_context.check_arg_id(*index);
}
else
{
index = parse_context.next_arg_id();
}
format_arg<OutputIt> arg = args.get(*index);
// Resolve nested replacement fields for width/precision
private_format::resolve_nested_replacements<OutputIt>(fmt_context.format_spec, args);
// Value index is always resolved in parse_format_spec
size_t index = fmt_context.format_spec.index.value();
format_arg<OutputIt> arg = args.get(index);
arg.template visit<void>(v);
ETL_ASSERT(*parse_context.begin() == '}', ETL_ERROR(bad_format_string_exception) /*"Closing brace missing"*/);

View File

@ -174,6 +174,19 @@ SOFTWARE.
#define ETL_NOT_USING_FORMAT_FLOATING_POINT 0
#endif
//*************************************
// Helper macro for ETL_FORMAT_NO_LONG_DOUBLE_MATH.
// Define ETL_FORMAT_NO_LONG_DOUBLE_MATH if the toolchain does not provide
// long double math functions (log10l, floorl, powl, modfl, roundl).
// When defined, long double arguments are cast to double for math operations.
#if defined(ETL_FORMAT_NO_LONG_DOUBLE_MATH)
#define ETL_USING_FORMAT_LONG_DOUBLE_MATH 0
#define ETL_NOT_USING_FORMAT_LONG_DOUBLE_MATH 1
#else
#define ETL_USING_FORMAT_LONG_DOUBLE_MATH 1
#define ETL_NOT_USING_FORMAT_LONG_DOUBLE_MATH 0
#endif
//*************************************
// Figure out things about the compiler, if haven't already done so in
// etl_profile.h

View File

@ -217,6 +217,8 @@ namespace
CHECK_EQUAL("1.234567", test_format(s, "{}", 1.234567499));
CHECK_EQUAL("1.234568", test_format(s, "{}", 1.234567501));
CHECK_EQUAL("1.5", test_format(s, "{}", 1.5));
CHECK_EQUAL("2.225074e-308", test_format(s, "{}", etl::numeric_limits<double>::min()));
CHECK_EQUAL("1.797693e+308", test_format(s, "{}", etl::numeric_limits<double>::max()));
}
//*************************************************************************
@ -240,6 +242,7 @@ namespace
CHECK_EQUAL("1.125000E+00", test_format(s, "{:E}", 1.125f));
CHECK_EQUAL("-2.533324e-05", test_format(s, "{:e}", -0.00002533324f));
CHECK_EQUAL("-2.500000e+11", test_format(s, "{:e}", -250000000000.0f));
CHECK_EQUAL("1.000000e+01", test_format(s, "{:e}", 9.9999996));
CHECK_EQUAL("1.000000", test_format(s, "{:f}", 1.0f));
CHECK_EQUAL("1.125000", test_format(s, "{:F}", 1.125f));
CHECK_EQUAL("1.000000", test_format(s, "{:g}", 1.0f));
@ -326,9 +329,9 @@ namespace
etl::string<100> s;
// 9.9999999: after normalization integral=9, fractional=0.9999999
// round(0.9999999 * 1e6) == 1000000 => must carry: 10.000000e+00
CHECK_EQUAL("10.000000e+00", test_format(s, "{:e}", 9.9999999));
CHECK_EQUAL("-10.000000e+00", test_format(s, "{:e}", -9.9999999));
// round(0.9999999 * 1e6) == 1000000 => must carry and renormalize: 1.000000e+01
CHECK_EQUAL("1.000000e+01", test_format(s, "{:e}", 9.9999999));
CHECK_EQUAL("-1.000000e+01", test_format(s, "{:e}", -9.9999999));
// 1.9999999: after normalization integral=1, fractional=0.9999999
CHECK_EQUAL("2.000000e+00", test_format(s, "{:e}", 1.9999999));
@ -787,6 +790,144 @@ namespace
CHECK_THROW(test_format(s, "{:+#05.5X}", 0xEF1), etl::bad_format_string_exception);
#endif
}
#if ETL_USING_FORMAT_FLOATING_POINT
//*************************************************************************
TEST(test_format_float_sign)
{
etl::string<100> s;
CHECK_EQUAL("+1.500000", test_format(s, "{:+f}", 1.5));
CHECK_EQUAL("1.500000", test_format(s, "{:-f}", 1.5));
CHECK_EQUAL(" 1.500000", test_format(s, "{: f}", 1.5));
CHECK_EQUAL("-1.500000", test_format(s, "{:+f}", -1.5));
CHECK_EQUAL("-1.500000", test_format(s, "{: f}", -1.5));
CHECK_EQUAL("+0.0", test_format(s, "{:+}", 0.0));
CHECK_EQUAL(" 0.0", test_format(s, "{: }", 0.0));
}
//*************************************************************************
TEST(test_format_float_width_align)
{
etl::string<100> s;
// {:f} with width
CHECK_EQUAL(" 1.500000", test_format(s, "{:10f}", 1.5));
CHECK_EQUAL("1.500000 ", test_format(s, "{:<10f}", 1.5));
CHECK_EQUAL(" 1.500000", test_format(s, "{:>10f}", 1.5));
CHECK_EQUAL(" 1.500000 ", test_format(s, "{:^10f}", 1.5));
CHECK_EQUAL("*1.500000*", test_format(s, "{:*^10f}", 1.5));
// {:f} with width and sign
CHECK_EQUAL(" +1.500000", test_format(s, "{:+10f}", 1.5));
CHECK_EQUAL(" -1.500000", test_format(s, "{:+10f}", -1.5));
// {:e} with width
CHECK_EQUAL(" 1.500000e+00", test_format(s, "{:15e}", 1.5));
CHECK_EQUAL("1.500000e+00 ", test_format(s, "{:<15e}", 1.5));
CHECK_EQUAL(" 1.500000e+00", test_format(s, "{:>15e}", 1.5));
CHECK_EQUAL(" 1.500000e+00 ", test_format(s, "{:^15e}", 1.5));
}
//*************************************************************************
TEST(test_format_negative_floats)
{
etl::string<100> s;
CHECK_EQUAL("-1.5", test_format(s, "{}", -1.5));
CHECK_EQUAL("-123.456", test_format(s, "{}", -123.456));
CHECK_EQUAL("-1.234560e+02", test_format(s, "{:e}", -123.456));
CHECK_EQUAL("-123.456000", test_format(s, "{:f}", -123.456));
}
//*************************************************************************
TEST(test_format_float_scientific_large_small)
{
etl::string<100> s;
// Large and small values with {:e}
CHECK_EQUAL("1.000000e+100", test_format(s, "{:e}", 1e100));
CHECK_EQUAL("1.000000e-100", test_format(s, "{:e}", 1e-100));
CHECK_EQUAL("-1.000000e+100", test_format(s, "{:e}", -1e100));
CHECK_EQUAL("-1.000000e-100", test_format(s, "{:e}", -1e-100));
}
//*************************************************************************
TEST(test_format_float_default_scientific_switch)
{
etl::string<100> s;
// Values at the boundary of fixed vs scientific in default presentation
CHECK_EQUAL("0.000001", test_format(s, "{}", 0.000001));
CHECK_EQUAL("1.000000e-07", test_format(s, "{}", 0.0000001));
CHECK_EQUAL("-1.000000e-07", test_format(s, "{}", -0.0000001));
CHECK_EQUAL("123456789012345.0", test_format(s, "{}", 123456789012345.0));
}
//*************************************************************************
TEST(test_format_positive_zero)
{
etl::string<100> s;
CHECK_EQUAL("0.0", test_format(s, "{}", 0.0));
CHECK_EQUAL("0.000000e+00", test_format(s, "{:e}", 0.0));
CHECK_EQUAL("0.000000", test_format(s, "{:f}", 0.0));
}
#endif
//*************************************************************************
TEST(test_format_brace_escaping)
{
etl::string<100> s;
CHECK_EQUAL("{}", test_format(s, "{{}}"));
CHECK_EQUAL("{42}", test_format(s, "{{{}}}", 42));
}
//*************************************************************************
TEST(test_format_integer_limits)
{
etl::string<100> s;
CHECK_EQUAL("-2147483648", test_format(s, "{}", etl::numeric_limits<int>::min()));
CHECK_EQUAL("2147483647", test_format(s, "{}", etl::numeric_limits<int>::max()));
CHECK_EQUAL("ffffffffffffffff", test_format(s, "{:x}", static_cast<unsigned long long>(0xFFFFFFFFFFFFFFFFULL)));
#if ETL_USING_CPP14
CHECK_EQUAL("0b0", test_format(s, "{:#b}", 0));
#endif
CHECK_EQUAL("0x0", test_format(s, "{:#x}", 0));
}
TEST(test_format_float_zero_padding)
{
etl::string<100> s;
CHECK_EQUAL("0000003.14", test_format(s, "{:010}", 3.14));
CHECK_EQUAL("001.500000", test_format(s, "{:010f}", 1.5));
CHECK_EQUAL("+01.500000", test_format(s, "{:+010f}", 1.5));
CHECK_EQUAL("-01.500000", test_format(s, "{:010f}", -1.5));
CHECK_EQUAL("+001.500000e+00", test_format(s, "{:+015e}", 1.5));
CHECK_EQUAL("-001.500000e+00", test_format(s, "{:+015e}", -1.5));
}
TEST(test_format_nested_replacement_width)
{
etl::string<100> s;
CHECK_EQUAL(" 42", test_format(s, "{:{}d}", 42, 10));
CHECK_EQUAL(" 42", test_format(s, "{0:{1}d}", 42, 10));
CHECK_EQUAL("hello ", test_format(s, "{:{}}", "hello", 10));
CHECK_EQUAL("x 42", test_format(s, "{} {:{}d}", "x", 42, 10));
}
TEST(test_format_octal_alternate_zero)
{
etl::string<100> s;
CHECK_EQUAL("0", test_format(s, "{:#o}", 0));
CHECK_EQUAL("07", test_format(s, "{:#o}", 7));
CHECK_EQUAL("010", test_format(s, "{:#o}", 8));
}
}
} // namespace