Fix division-by-zero error in to_char() with 'EEEE' format.
authorDean Rasheed <[email protected]>
Thu, 5 Aug 2021 08:35:46 +0000 (09:35 +0100)
committerDean Rasheed <[email protected]>
Thu, 5 Aug 2021 08:35:46 +0000 (09:35 +0100)
This fixes a long-standing bug when using to_char() to format a
numeric value in scientific notation -- if the value's exponent is
less than -NUMERIC_MAX_DISPLAY_SCALE-1 (-1001), it produced a
division-by-zero error.

The reason for this error was that get_str_from_var_sci() divides its
input by 10^exp, which it produced using power_var_int(). However, the
underflow test in power_var_int() causes it to return zero if the
result scale is too small. That's not a problem for power_var_int()'s
only other caller, power_var(), since that limits the rscale to 1000,
but in get_str_from_var_sci() the exponent can be much smaller,
requiring a much larger rscale. Fix by introducing a new function to
compute 10^exp directly, with no rscale limit. This also allows 10^exp
to be computed more efficiently, without any numeric multiplication,
division or rounding.

Discussion: https://postgr.es/m/CAEZATCWhojfH4whaqgUKBe8D5jNHB8ytzemL-PnRx+KCTyMXmg@mail.gmail.com

src/backend/utils/adt/numeric.c
src/test/regress/expected/numeric.out
src/test/regress/sql/numeric.sql

index 1dc0dd32e3b36c656ebea6d9d87965ce7e260aca..98574669a25166860a51d5dc516b8ba9048e273f 100644 (file)
@@ -337,16 +337,6 @@ static NumericDigit const_two_data[1] = {2};
 static NumericVar const_two =
 {1, 0, NUMERIC_POS, 0, NULL, const_two_data};
 
-#if DEC_DIGITS == 4 || DEC_DIGITS == 2
-static NumericDigit const_ten_data[1] = {10};
-static NumericVar const_ten =
-{1, 0, NUMERIC_POS, 0, NULL, const_ten_data};
-#elif DEC_DIGITS == 1
-static NumericDigit const_ten_data[1] = {1};
-static NumericVar const_ten =
-{1, 1, NUMERIC_POS, 0, NULL, const_ten_data};
-#endif
-
 #if DEC_DIGITS == 4
 static NumericDigit const_zero_point_five_data[1] = {5000};
 #elif DEC_DIGITS == 2
@@ -477,6 +467,7 @@ static void log_var(NumericVar *base, NumericVar *num, NumericVar *result);
 static void power_var(NumericVar *base, NumericVar *exp, NumericVar *result);
 static void power_var_int(NumericVar *base, int exp, NumericVar *result,
              int rscale);
+static void power_ten_int(int exp, NumericVar *result);
 
 static int cmp_abs(NumericVar *var1, NumericVar *var2);
 static int cmp_abs_common(const NumericDigit *var1digits, int var1ndigits,
@@ -5767,9 +5758,7 @@ static char *
 get_str_from_var_sci(NumericVar *var, int rscale)
 {
    int32       exponent;
-   NumericVar  denominator;
-   NumericVar  significand;
-   int         denom_scale;
+   NumericVar  tmp_var;
    size_t      len;
    char       *str;
    char       *sig_out;
@@ -5806,25 +5795,16 @@ get_str_from_var_sci(NumericVar *var, int rscale)
    }
 
    /*
-    * 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(&tmp_var);
 
-   init_var(&denominator);
-   init_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);
 
-   power_var_int(&const_ten, exponent, &denominator, denom_scale);
-   div_var(var, &denominator, &significand, rscale, true);
-   sig_out = get_str_from_var(&significand);
-
-   free_var(&denominator);
-   free_var(&significand);
+   free_var(&tmp_var);
 
    /*
     * Allocate space for the result.
@@ -8310,6 +8290,34 @@ power_var_int(NumericVar *base, int exp, NumericVar *result, int rscale)
        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;
+}
+
 
 /* ----------------------------------------------------------------------
  *
index 7150a68cb2b0accdbb6192c29a72108e247cdbe5..e983ef5a940915446d526a85798b03abf3e1f907 100644 (file)
@@ -1217,6 +1217,39 @@ SELECT '' AS to_char_26, to_char('100'::numeric, 'FM999');
             | 100
 (1 row)
 
+-- Test scientific notation with various exponents
+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)
+
 -- TO_NUMBER()
 --
 SELECT '' AS to_number_1,  to_number('-34,338,492', '99G999G999');
index 83a4f3c89d4a3a7e3af2711ff7a323c137781ce7..7a1134f57315baf6d40b8d3c7739d2656cc02976 100644 (file)
@@ -786,6 +786,14 @@ SELECT '' AS to_char_24, to_char('100'::numeric, 'FM999.9');
 SELECT '' AS to_char_25, to_char('100'::numeric, 'FM999.');
 SELECT '' AS to_char_26, to_char('100'::numeric, 'FM999');
 
+-- Test scientific notation with various exponents
+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;
+
 -- TO_NUMBER()
 --
 SELECT '' AS to_number_1,  to_number('-34,338,492', '99G999G999');