From e5fee7bd76a0f0e612fa2d3a5d2354340cd6d273 Mon Sep 17 00:00:00 2001
From: hanjianqiao <hanjianqiao@gmail.com>
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

