Skip to content

Commit ecbdbdf

Browse files
committed
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
1 parent fa604e0 commit ecbdbdf

File tree

3 files changed

+76
-29
lines changed

3 files changed

+76
-29
lines changed

src/backend/utils/adt/numeric.c

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -428,16 +428,6 @@ static const NumericDigit const_two_data[1] = {2};
428428
static const NumericVar const_two =
429429
{1, 0, NUMERIC_POS, 0, NULL, (NumericDigit *) const_two_data};
430430

431-
#if DEC_DIGITS == 4 || DEC_DIGITS == 2
432-
static const NumericDigit const_ten_data[1] = {10};
433-
static const NumericVar const_ten =
434-
{1, 0, NUMERIC_POS, 0, NULL, (NumericDigit *) const_ten_data};
435-
#elif DEC_DIGITS == 1
436-
static const NumericDigit const_ten_data[1] = {1};
437-
static const NumericVar const_ten =
438-
{1, 1, NUMERIC_POS, 0, NULL, (NumericDigit *) const_ten_data};
439-
#endif
440-
441431
#if DEC_DIGITS == 4
442432
static const NumericDigit const_zero_point_nine_data[1] = {9000};
443433
#elif DEC_DIGITS == 2
@@ -579,6 +569,7 @@ static void power_var(const NumericVar *base, const NumericVar *exp,
579569
NumericVar *result);
580570
static void power_var_int(const NumericVar *base, int exp, NumericVar *result,
581571
int rscale);
572+
static void power_ten_int(int exp, NumericVar *result);
582573

583574
static int cmp_abs(const NumericVar *var1, const NumericVar *var2);
584575
static int cmp_abs_common(const NumericDigit *var1digits, int var1ndigits,
@@ -7215,9 +7206,7 @@ static char *
72157206
get_str_from_var_sci(const NumericVar *var, int rscale)
72167207
{
72177208
int32 exponent;
7218-
NumericVar denominator;
7219-
NumericVar significand;
7220-
int denom_scale;
7209+
NumericVar tmp_var;
72217210
size_t len;
72227211
char *str;
72237212
char *sig_out;
@@ -7254,25 +7243,16 @@ get_str_from_var_sci(const NumericVar *var, int rscale)
72547243
}
72557244

72567245
/*
7257-
* The denominator is set to 10 raised to the power of the exponent.
7258-
*
7259-
* We then divide var by the denominator to get the significand, rounding
7260-
* to rscale decimal digits in the process.
7246+
* Divide var by 10^exponent to get the significand, rounding to rscale
7247+
* decimal digits in the process.
72617248
*/
7262-
if (exponent < 0)
7263-
denom_scale = -exponent;
7264-
else
7265-
denom_scale = 0;
7266-
7267-
init_var(&denominator);
7268-
init_var(&significand);
7249+
init_var(&tmp_var);
72697250

7270-
power_var_int(&const_ten, exponent, &denominator, denom_scale);
7271-
div_var(var, &denominator, &significand, rscale, true);
7272-
sig_out = get_str_from_var(&significand);
7251+
power_ten_int(exponent, &tmp_var);
7252+
div_var(var, &tmp_var, &tmp_var, rscale, true);
7253+
sig_out = get_str_from_var(&tmp_var);
72737254

7274-
free_var(&denominator);
7275-
free_var(&significand);
7255+
free_var(&tmp_var);
72767256

72777257
/*
72787258
* Allocate space for the result.
@@ -10480,6 +10460,34 @@ power_var_int(const NumericVar *base, int exp, NumericVar *result, int rscale)
1048010460
round_var(result, rscale);
1048110461
}
1048210462

10463+
/*
10464+
* power_ten_int() -
10465+
*
10466+
* Raise ten to the power of exp, where exp is an integer. Note that unlike
10467+
* power_var_int(), this does no overflow/underflow checking or rounding.
10468+
*/
10469+
static void
10470+
power_ten_int(int exp, NumericVar *result)
10471+
{
10472+
/* Construct the result directly, starting from 10^0 = 1 */
10473+
set_var_from_var(&const_one, result);
10474+
10475+
/* Scale needed to represent the result exactly */
10476+
result->dscale = exp < 0 ? -exp : 0;
10477+
10478+
/* Base-NBASE weight of result and remaining exponent */
10479+
if (exp >= 0)
10480+
result->weight = exp / DEC_DIGITS;
10481+
else
10482+
result->weight = (exp + 1) / DEC_DIGITS - 1;
10483+
10484+
exp -= result->weight * DEC_DIGITS;
10485+
10486+
/* Final adjustment of the result's single NBASE digit */
10487+
while (exp-- > 0)
10488+
result->digits[0] *= 10;
10489+
}
10490+
1048310491

1048410492
/* ----------------------------------------------------------------------
1048510493
*

src/test/regress/expected/numeric.out

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1794,6 +1794,38 @@ FROM v;
17941794
NaN | #.####### | #.####### | #.#######
17951795
(7 rows)
17961796

1797+
WITH v(exp) AS
1798+
(VALUES(-16379),(-16378),(-1234),(-789),(-45),(-5),(-4),(-3),(-2),(-1),(0),
1799+
(1),(2),(3),(4),(5),(38),(275),(2345),(45678),(131070),(131071))
1800+
SELECT exp,
1801+
to_char(('1.2345e'||exp)::numeric, '9.999EEEE') as numeric
1802+
FROM v;
1803+
exp | numeric
1804+
--------+----------------
1805+
-16379 | 1.235e-16379
1806+
-16378 | 1.235e-16378
1807+
-1234 | 1.235e-1234
1808+
-789 | 1.235e-789
1809+
-45 | 1.235e-45
1810+
-5 | 1.235e-05
1811+
-4 | 1.235e-04
1812+
-3 | 1.235e-03
1813+
-2 | 1.235e-02
1814+
-1 | 1.235e-01
1815+
0 | 1.235e+00
1816+
1 | 1.235e+01
1817+
2 | 1.235e+02
1818+
3 | 1.235e+03
1819+
4 | 1.235e+04
1820+
5 | 1.235e+05
1821+
38 | 1.235e+38
1822+
275 | 1.235e+275
1823+
2345 | 1.235e+2345
1824+
45678 | 1.235e+45678
1825+
131070 | 1.235e+131070
1826+
131071 | 1.235e+131071
1827+
(22 rows)
1828+
17971829
WITH v(val) AS
17981830
(VALUES('0'::numeric),('-4.2'),('4.2e9'),('1.2e-5'),('inf'),('-inf'),('nan'))
17991831
SELECT val,

src/test/regress/sql/numeric.sql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,13 @@ SELECT val,
939939
to_char(val::float4, '9.999EEEE') as float4
940940
FROM v;
941941

942+
WITH v(exp) AS
943+
(VALUES(-16379),(-16378),(-1234),(-789),(-45),(-5),(-4),(-3),(-2),(-1),(0),
944+
(1),(2),(3),(4),(5),(38),(275),(2345),(45678),(131070),(131071))
945+
SELECT exp,
946+
to_char(('1.2345e'||exp)::numeric, '9.999EEEE') as numeric
947+
FROM v;
948+
942949
WITH v(val) AS
943950
(VALUES('0'::numeric),('-4.2'),('4.2e9'),('1.2e-5'),('inf'),('-inf'),('nan'))
944951
SELECT val,

0 commit comments

Comments
 (0)