Related PR
#23718
Problem
datafusion/spark/src/function/string/format_string.rs has separate sign and width-padding logic in format_float, format_decimal, and integer decimal formatting. These paths all need to preserve the same Java/Spark formatting contract for flags such as (, +, space, 0, -, and width, but they duplicate the final assembly of prefix + number + suffix.
A recent decimal bug was caused by this drift: format_decimal missed negative_in_parentheses suffix handling that already existed in format_float. The bug is now fixed, but the underlying refactor opportunity remains: the shared final-assembly invariant is still encoded separately in multiple paths.
Why it matters
Duplicated tail-formatting logic makes future fixes easy to apply to one numeric type but miss another. This is a correctness risk for SQL-visible format_string behavior across Float16/32/64, Decimal128/256, and integer inputs.
Invariant / desired behavior
After a numeric value has been converted into its unsigned/absolute textual number, all numeric formatting paths should apply sign, parentheses suffix, zero padding, left adjustment, and width using one canonical helper.
Type-specific number generation must remain separate:
- float finite / NaN / Infinity behavior
- decimal exact BigDecimal formatting
- integer base formatting
%g / scientific precision decisions
Proposed direction
Add a narrow helper for final numeric assembly and padding, not a broad helper that tries to merge float and decimal formatting.
Suggested helper responsibility:
- accept
prefix, number, suffix
- account for
width, left_adj, and zero_pad
- write the final string
- allow callers to disable zero padding when required, e.g. non-finite floats
Keep format_float and format_decimal responsible for producing number. Use the helper only after type-specific formatting is complete.
Scope
In
- Add one small helper in
ConversionSpecifier for prefix + number + suffix width/padding assembly.
- Use it from
format_float and format_decimal.
- Consider using it from signed integer decimal formatting if it reduces duplication without changing behavior.
- Preserve all existing outputs.
- Keep or add focused regression coverage where helper behavior could drift, especially negative parentheses with width and zero padding.
Out
- Do not merge decimal and float number-generation logic.
- Do not change Spark/Java compatibility semantics.
- Do not rewrite parsing or validation of format specifiers.
- Do not alter scientific / compact
%g precision behavior.
- Do not introduce a broad abstraction with many primitive flags beyond final assembly/padding.
Acceptance criteria
Tests / verification
- Run targeted Rust tests for Spark format string:
cargo test -p datafusion-spark format_string --lib
- At minimum, include/keep regression coverage for:
- Decimal128 negative
%(,.2f
- Decimal128 negative
%(,15.2f
- Float negative
%(,15.2f
- zero padding with sign/parentheses where supported
- If integer formatting is touched, run existing integer
format_string tests and add a focused case only if current coverage does not protect sign + width placement.
Notes / open questions
- Decimal256 uses the same
format_decimal path as Decimal128. A small Decimal256 regression case may be useful if the refactor touches decimal dispatch or tests are otherwise too Decimal128-specific.
Related PR
#23718
Problem
datafusion/spark/src/function/string/format_string.rshas separate sign and width-padding logic informat_float,format_decimal, and integer decimal formatting. These paths all need to preserve the same Java/Spark formatting contract for flags such as(,+, space,0,-, and width, but they duplicate the final assembly ofprefix + number + suffix.A recent decimal bug was caused by this drift:
format_decimalmissednegative_in_parenthesessuffix handling that already existed informat_float. The bug is now fixed, but the underlying refactor opportunity remains: the shared final-assembly invariant is still encoded separately in multiple paths.Why it matters
Duplicated tail-formatting logic makes future fixes easy to apply to one numeric type but miss another. This is a correctness risk for SQL-visible
format_stringbehavior across Float16/32/64, Decimal128/256, and integer inputs.Invariant / desired behavior
After a numeric value has been converted into its unsigned/absolute textual
number, all numeric formatting paths should apply sign, parentheses suffix, zero padding, left adjustment, and width using one canonical helper.Type-specific number generation must remain separate:
%g/ scientific precision decisionsProposed direction
Add a narrow helper for final numeric assembly and padding, not a broad helper that tries to merge float and decimal formatting.
Suggested helper responsibility:
prefix,number,suffixwidth,left_adj, andzero_padKeep
format_floatandformat_decimalresponsible for producingnumber. Use the helper only after type-specific formatting is complete.Scope
In
ConversionSpecifierforprefix + number + suffixwidth/padding assembly.format_floatandformat_decimal.Out
%gprecision behavior.Acceptance criteria
format_floatandformat_decimaluse the same final sign/suffix/width-padding helper.format_stringtests pass unchanged unless expected strings are proven wrong against Spark/Java.Tests / verification
cargo test -p datafusion-spark format_string --lib%(,.2f%(,15.2f%(,15.2fformat_stringtests and add a focused case only if current coverage does not protect sign + width placement.Notes / open questions
format_decimalpath as Decimal128. A small Decimal256 regression case may be useful if the refactor touches decimal dispatch or tests are otherwise too Decimal128-specific.