Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Fix division-by-zero error in to_char() with 'EEEE' format.
authorDean Rasheed <dean.a.rasheed@gmail.com>
Thu, 5 Aug 2021 08:29:13 +0000 (09:29 +0100)
committerDean Rasheed <dean.a.rasheed@gmail.com>
Thu, 5 Aug 2021 08:29:13 +0000 (09:29 +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 ff3a68745456e219e293696ba0d815dfbd406ca4..ba19cfb672b9711486bda258b98c118c2b672279 100644 (file)
@@ -383,16 +383,6 @@ static const NumericDigit const_two_data[1] = {2};
 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
@@ -526,6 +516,7 @@ static void power_var(const NumericVar *base, const NumericVar *exp,
                      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,
@@ -6354,9 +6345,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;
@@ -6393,25 +6382,16 @@ get_str_from_var_sci(const 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(&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.
@@ -9498,6 +9478,34 @@ power_var_int(const 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 f414308f84b06505fda00924244ad61674f8dd44..6e7c8e42ecbabbc6e225a26b01ef7669f4685952 100644 (file)
@@ -1278,6 +1278,39 @@ SELECT '' AS to_char_36, to_char('100'::numeric, 'f"ool\\"999');
             | fool\ 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()
 --
 SET lc_numeric = 'C';
index 972f4d88ac76966c3002a3f3116f73bc4098f2e9..1697a148ef5ca27257e94aa3d2e0b748281f01fa 100644 (file)
@@ -798,6 +798,14 @@ SELECT '' AS to_char_34, to_char('100'::numeric, 'f"\\ool"999');
 SELECT '' AS to_char_35, to_char('100'::numeric, 'f"ool\"999');
 SELECT '' AS to_char_36, to_char('100'::numeric, 'f"ool\\"999');
 
+-- 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()
 --
 SET lc_numeric = 'C';