While testing the numeric_power() patch in [1], I found this problem
trying to use to_char() to format very small numbers:

SELECT to_char(1.2e-1001, '9.9EEEE'); -- OK
  to_char
------------
  1.2e-1001

SELECT to_char(1.2e-1002, '9.9EEEE'); -- fails
ERROR:  division by zero

It turns out that the problem is in get_str_from_var_sci() which
attempts to divide the input by 1e-1002 to get the significand.
However, it is using power_var_int() to compute 1e-1002, which has a
maximum rscale of NUMERIC_MAX_DISPLAY_SCALE (1000), so it returns 0,
which is the correct answer to that scale, and then
get_str_from_var_sci() attempts to divide by that.

Rather than messing with power_var_int(), I think the simplest
solution is to just introduce a new local function, as in the attached
patch. This directly constructs 10^n, for integer n, which is pretty
trivial, and doesn't need any numeric multiplication or rounding.

Regards,
Dean

[1] 
https://www.postgresql.org/message-id/CAEZATCUWUV_BP41Ob7QY12oF%2BqDxjTWfDpkdkcOOuojrDvOLxw%40mail.gmail.com
diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c
new file mode 100644
index faff09f..e05e5d7
--- a/src/backend/utils/adt/numeric.c
+++ b/src/backend/utils/adt/numeric.c
@@ -428,16 +428,6 @@ static const NumericDigit const_two_data
 static const NumericVar const_two =
 {1, 0, NUMERIC_POS, 0, NULL, (NumericDigit *) const_two_data};
 
-#if DEC_DIGITS == 4 || DEC_DIGITS == 2
-static const NumericDigit const_ten_data[1] = {10};
-static const NumericVar const_ten =
-{1, 0, NUMERIC_POS, 0, NULL, (NumericDigit *) const_ten_data};
-#elif DEC_DIGITS == 1
-static const NumericDigit const_ten_data[1] = {1};
-static const NumericVar const_ten =
-{1, 1, NUMERIC_POS, 0, NULL, (NumericDigit *) const_ten_data};
-#endif
-
 #if DEC_DIGITS == 4
 static const NumericDigit const_zero_point_nine_data[1] = {9000};
 #elif DEC_DIGITS == 2
@@ -582,6 +572,7 @@ static void power_var(const NumericVar *
 					  NumericVar *result);
 static void power_var_int(const NumericVar *base, int exp, NumericVar *result,
 						  int rscale);
+static void power_ten_int(int exp, NumericVar *result);
 
 static int	cmp_abs(const NumericVar *var1, const NumericVar *var2);
 static int	cmp_abs_common(const NumericDigit *var1digits, int var1ndigits,
@@ -7213,9 +7204,7 @@ static char *
 get_str_from_var_sci(const NumericVar *var, int rscale)
 {
 	int32		exponent;
-	NumericVar	denominator;
-	NumericVar	significand;
-	int			denom_scale;
+	NumericVar	tmp_var;
 	size_t		len;
 	char	   *str;
 	char	   *sig_out;
@@ -7252,25 +7241,16 @@ get_str_from_var_sci(const NumericVar *v
 	}
 
 	/*
-	 * The denominator is set to 10 raised to the power of the exponent.
-	 *
-	 * We then divide var by the denominator to get the significand, rounding
-	 * to rscale decimal digits in the process.
+	 * Divide var by 10^exponent to get the significand, rounding to rscale
+	 * decimal digits in the process.
 	 */
-	if (exponent < 0)
-		denom_scale = -exponent;
-	else
-		denom_scale = 0;
-
-	init_var(&denominator);
-	init_var(&significand);
+	init_var(&tmp_var);
 
-	power_var_int(&const_ten, exponent, &denominator, denom_scale);
-	div_var(var, &denominator, &significand, rscale, true);
-	sig_out = get_str_from_var(&significand);
+	power_ten_int(exponent, &tmp_var);
+	div_var(var, &tmp_var, &tmp_var, rscale, true);
+	sig_out = get_str_from_var(&tmp_var);
 
-	free_var(&denominator);
-	free_var(&significand);
+	free_var(&tmp_var);
 
 	/*
 	 * Allocate space for the result.
@@ -10468,6 +10448,34 @@ power_var_int(const NumericVar *base, in
 		round_var(result, rscale);
 }
 
+/*
+ * power_ten_int() -
+ *
+ *	Raise ten to the power of exp, where exp is an integer.  Note that unlike
+ *	power_var_int(), this does no overflow/underflow checking or rounding.
+ */
+static void
+power_ten_int(int exp, NumericVar *result)
+{
+	/* Construct the result directly, starting from 10^0 = 1 */
+	set_var_from_var(&const_one, result);
+
+	/* Scale needed to represent the result exactly */
+	result->dscale = exp < 0 ? -exp : 0;
+
+	/* Base-NBASE weight of result and remaining exponent */
+	if (exp >= 0)
+		result->weight = exp / DEC_DIGITS;
+	else
+		result->weight = (exp + 1) / DEC_DIGITS - 1;
+
+	exp -= result->weight * DEC_DIGITS;
+
+	/* Final adjustment of the result's single NBASE digit */
+	while (exp-- > 0)
+		result->digits[0] *= 10;
+}
+
 
 /* ----------------------------------------------------------------------
  *
diff --git a/src/test/regress/expected/numeric.out b/src/test/regress/expected/numeric.out
new file mode 100644
index cc11995..8f0b40a
--- a/src/test/regress/expected/numeric.out
+++ b/src/test/regress/expected/numeric.out
@@ -1794,6 +1794,38 @@ FROM v;
         NaN |  #.####### |  #.####### |  #.#######
 (7 rows)
 
+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))
+SELECT exp,
+  to_char(('1.2345e'||exp)::numeric, '9.999EEEE') as numeric
+FROM v;
+  exp   |    numeric     
+--------+----------------
+ -16379 |  1.235e-16379
+ -16378 |  1.235e-16378
+  -1234 |  1.235e-1234
+   -789 |  1.235e-789
+    -45 |  1.235e-45
+     -5 |  1.235e-05
+     -4 |  1.235e-04
+     -3 |  1.235e-03
+     -2 |  1.235e-02
+     -1 |  1.235e-01
+      0 |  1.235e+00
+      1 |  1.235e+01
+      2 |  1.235e+02
+      3 |  1.235e+03
+      4 |  1.235e+04
+      5 |  1.235e+05
+     38 |  1.235e+38
+    275 |  1.235e+275
+   2345 |  1.235e+2345
+  45678 |  1.235e+45678
+ 131070 |  1.235e+131070
+ 131071 |  1.235e+131071
+(22 rows)
+
 WITH v(val) AS
   (VALUES('0'::numeric),('-4.2'),('4.2e9'),('1.2e-5'),('inf'),('-inf'),('nan'))
 SELECT val,
diff --git a/src/test/regress/sql/numeric.sql b/src/test/regress/sql/numeric.sql
new file mode 100644
index 14b4acf..c9730bc
--- a/src/test/regress/sql/numeric.sql
+++ b/src/test/regress/sql/numeric.sql
@@ -939,6 +939,13 @@ SELECT val,
   to_char(val::float4, '9.999EEEE') as float4
 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))
+SELECT exp,
+  to_char(('1.2345e'||exp)::numeric, '9.999EEEE') as numeric
+FROM v;
+
 WITH v(val) AS
   (VALUES('0'::numeric),('-4.2'),('4.2e9'),('1.2e-5'),('inf'),('-inf'),('nan'))
 SELECT val,

Reply via email to