From e5fee7bd76a0f0e612fa2d3a5d2354340cd6d273 Mon Sep 17 00:00:00 2001 From: hanjianqiao Date: Fri, 13 Nov 2026 22:11:09 +0800 Subject: [PATCH] Normalize numeric/bigint scientific notation after rounding get_str_from_var_sci() determines the exponent before rounding the significand. When rounding increases the significand's absolute value to ten, the original exponent is retained, leaving the output unnormalized. For example, to_char(999::numeric, '9.9EEEE') returns ' 10.0e+02' rather than ' 1.0e+03'. The bigint overload uses the same conversion path. Detect this carry, increment the exponent, and recompute the significand from the original input. Both representations have the same rounded numeric value; the change corrects the scientific notation formatting. Add regression coverage for numeric and bigint, including both signs, rounding boundaries, exponent transitions, zero, and non-carry cases. --- src/backend/utils/adt/numeric.c | 15 +++++++++++ src/test/regress/expected/numeric.out | 38 +++++++++++++++++++++++++++ src/test/regress/sql/numeric.sql | 30 +++++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c index 7deea2046f1..dc9279f6481 100644 --- a/src/backend/utils/adt/numeric.c +++ b/src/backend/utils/adt/numeric.c @@ -7514,6 +7514,21 @@ get_str_from_var_sci(const NumericVar *var, int rscale) power_ten_int(exponent, &tmp_var); div_var(var, &tmp_var, &tmp_var, rscale, true, true); + + /* + * Rounding can increase the absolute value of the significand to ten. + * If so, increase the exponent and recompute the significand from the + * original value. + */ + if (tmp_var.ndigits > 0 && + (tmp_var.weight > 0 || + (tmp_var.weight == 0 && tmp_var.digits[0] >= 10))) + { + exponent++; + power_ten_int(exponent, &tmp_var); + div_var(var, &tmp_var, &tmp_var, rscale, true, true); + } + sig_out = get_str_from_var(&tmp_var); free_var(&tmp_var); diff --git a/src/test/regress/expected/numeric.out b/src/test/regress/expected/numeric.out index 6269d5bb397..a4244927265 100644 --- a/src/test/regress/expected/numeric.out +++ b/src/test/regress/expected/numeric.out @@ -2044,6 +2044,44 @@ FROM v; NaN | #.####### | #.####### | #.####### (7 rows) +-- Rounding the significand must preserve normalised scientific notation. +WITH v(val, fmt, expected) AS + (VALUES (99::numeric, '9EEEE', ' 1e+02'), + (-99, '9EEEE', '-1e+02'), + (999, '9.9EEEE', ' 1.0e+03'), + (-999, '9.9EEEE', '-1.0e+03'), + (9999, '9.99EEEE', ' 1.00e+04'), + (-9999, '9.99EEEE', '-1.00e+04'), + (9.994, '9.99EEEE', ' 9.99e+00'), + (9.995, '9.99EEEE', ' 1.00e+01'), + (-9.994, '9.99EEEE', '-9.99e+00'), + (-9.995, '9.99EEEE', '-1.00e+01'), + (0.9994, '9.99EEEE', ' 9.99e-01'), + (0.9995, '9.99EEEE', ' 1.00e+00'), + (-0.9994, '9.99EEEE', '-9.99e-01'), + (-0.9995, '9.99EEEE', '-1.00e+00'), + (0, '9.99EEEE', ' 0.00e+00'), + (1, '9.99EEEE', ' 1.00e+00')) +SELECT bool_and(to_char(val, fmt) = expected) AS ok FROM v; + ok +---- + t +(1 row) + +-- The bigint overload uses the same scientific notation conversion. +WITH v(val, fmt, expected) AS + (VALUES (99::bigint, '9EEEE', ' 1e+02'), + (-99, '9EEEE', '-1e+02'), + (999, '9.9EEEE', ' 1.0e+03'), + (-999, '9.9EEEE', '-1.0e+03'), + (9999, '9.99EEEE', ' 1.00e+04'), + (-9999, '9.99EEEE', '-1.00e+04')) +SELECT bool_and(to_char(val, fmt) = expected) AS ok FROM v; + ok +---- + t +(1 row) + WITH v(exp) AS (VALUES(-16379),(-16378),(-1234),(-789),(-45),(-5),(-4),(-3),(-2),(-1),(0), (1),(2),(3),(4),(5),(38),(275),(2345),(45678),(131070),(131071)) diff --git a/src/test/regress/sql/numeric.sql b/src/test/regress/sql/numeric.sql index e63b21e4b6b..b7dad803f43 100644 --- a/src/test/regress/sql/numeric.sql +++ b/src/test/regress/sql/numeric.sql @@ -1010,6 +1010,36 @@ SELECT val, to_char(val::float4, '9.999EEEE') as float4 FROM v; +-- Rounding the significand must preserve normalised scientific notation. +WITH v(val, fmt, expected) AS + (VALUES (99::numeric, '9EEEE', ' 1e+02'), + (-99, '9EEEE', '-1e+02'), + (999, '9.9EEEE', ' 1.0e+03'), + (-999, '9.9EEEE', '-1.0e+03'), + (9999, '9.99EEEE', ' 1.00e+04'), + (-9999, '9.99EEEE', '-1.00e+04'), + (9.994, '9.99EEEE', ' 9.99e+00'), + (9.995, '9.99EEEE', ' 1.00e+01'), + (-9.994, '9.99EEEE', '-9.99e+00'), + (-9.995, '9.99EEEE', '-1.00e+01'), + (0.9994, '9.99EEEE', ' 9.99e-01'), + (0.9995, '9.99EEEE', ' 1.00e+00'), + (-0.9994, '9.99EEEE', '-9.99e-01'), + (-0.9995, '9.99EEEE', '-1.00e+00'), + (0, '9.99EEEE', ' 0.00e+00'), + (1, '9.99EEEE', ' 1.00e+00')) +SELECT bool_and(to_char(val, fmt) = expected) AS ok FROM v; + +-- The bigint overload uses the same scientific notation conversion. +WITH v(val, fmt, expected) AS + (VALUES (99::bigint, '9EEEE', ' 1e+02'), + (-99, '9EEEE', '-1e+02'), + (999, '9.9EEEE', ' 1.0e+03'), + (-999, '9.9EEEE', '-1.0e+03'), + (9999, '9.99EEEE', ' 1.00e+04'), + (-9999, '9.99EEEE', '-1.00e+04')) +SELECT bool_and(to_char(val, fmt) = expected) AS ok FROM v; + WITH v(exp) AS (VALUES(-16379),(-16378),(-1234),(-789),(-45),(-5),(-4),(-3),(-2),(-1),(0), (1),(2),(3),(4),(5),(38),(275),(2345),(45678),(131070),(131071)) -- 2.53.0