Fix division-by-zero error in to_char() with 'EEEE' format.

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
This commit is contained in:
Dean Rasheed 2021-08-05 09:27:35 +01:00
parent fa604e0dd0
commit ecbdbdfd90
3 changed files with 76 additions and 29 deletions

View File

@ -428,16 +428,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
@ -579,6 +569,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,
@ -7215,9 +7206,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;
@ -7254,25 +7243,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(&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.
@ -10480,6 +10460,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;
}
/* ----------------------------------------------------------------------
*

View File

@ -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,

View File

@ -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,