From: Dean Rasheed Date: Thu, 5 Aug 2021 08:35:46 +0000 (+0100) Subject: Fix division-by-zero error in to_char() with 'EEEE' format. X-Git-Tag: REL9_6_23~9 X-Git-Url: http://git.postgresql.org/gitweb/?a=commitdiff_plain;h=ed3e1663c031e82def8c50ef31ee704777059459;p=postgresql.git 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 --- diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c index 1dc0dd32e3b..98574669a25 100644 --- a/src/backend/utils/adt/numeric.c +++ b/src/backend/utils/adt/numeric.c @@ -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; +} + /* ---------------------------------------------------------------------- * diff --git a/src/test/regress/expected/numeric.out b/src/test/regress/expected/numeric.out index 7150a68cb2b..e983ef5a940 100644 --- a/src/test/regress/expected/numeric.out +++ b/src/test/regress/expected/numeric.out @@ -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'); diff --git a/src/test/regress/sql/numeric.sql b/src/test/regress/sql/numeric.sql index 83a4f3c89d4..7a1134f5731 100644 --- a/src/test/regress/sql/numeric.sql +++ b/src/test/regress/sql/numeric.sql @@ -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');