Skip to content

Commit a72ad63

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 47a573d commit a72ad63

File tree

3 files changed

+78
-29
lines changed

3 files changed

+78
-29
lines changed

src/backend/utils/adt/numeric.c

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

386-
#if DEC_DIGITS == 4 || DEC_DIGITS == 2
387-
static const NumericDigit const_ten_data[1] = {10};
388-
static const NumericVar const_ten =
389-
{1, 0, NUMERIC_POS, 0, NULL, (NumericDigit *) const_ten_data};
390-
#elif DEC_DIGITS == 1
391-
static const NumericDigit const_ten_data[1] = {1};
392-
static const NumericVar const_ten =
393-
{1, 1, NUMERIC_POS, 0, NULL, (NumericDigit *) const_ten_data};
394-
#endif
395-
396386
#if DEC_DIGITS == 4
397387
static const NumericDigit const_zero_point_nine_data[1] = {9000};
398388
#elif DEC_DIGITS == 2
@@ -526,6 +516,7 @@ static void power_var(const NumericVar *base, const NumericVar *exp,
526516
NumericVar *result);
527517
static void power_var_int(const NumericVar *base, int exp, NumericVar *result,
528518
int rscale);
519+
static void power_ten_int(int exp, NumericVar *result);
529520

530521
static int cmp_abs(const NumericVar *var1, const NumericVar *var2);
531522
static int cmp_abs_common(const NumericDigit *var1digits, int var1ndigits,
@@ -6354,9 +6345,7 @@ static char *
63546345
get_str_from_var_sci(const NumericVar *var, int rscale)
63556346
{
63566347
int32 exponent;
6357-
NumericVar denominator;
6358-
NumericVar significand;
6359-
int denom_scale;
6348+
NumericVar tmp_var;
63606349
size_t len;
63616350
char *str;
63626351
char *sig_out;
@@ -6393,25 +6382,16 @@ get_str_from_var_sci(const NumericVar *var, int rscale)
63936382
}
63946383

63956384
/*
6396-
* The denominator is set to 10 raised to the power of the exponent.
6397-
*
6398-
* We then divide var by the denominator to get the significand, rounding
6399-
* to rscale decimal digits in the process.
6385+
* Divide var by 10^exponent to get the significand, rounding to rscale
6386+
* decimal digits in the process.
64006387
*/
6401-
if (exponent < 0)
6402-
denom_scale = -exponent;
6403-
else
6404-
denom_scale = 0;
6405-
6406-
init_var(&denominator);
6407-
init_var(&significand);
6388+
init_var(&tmp_var);
64086389

6409-
power_var_int(&const_ten, exponent, &denominator, denom_scale);
6410-
div_var(var, &denominator, &significand, rscale, true);
6411-
sig_out = get_str_from_var(&significand);
6390+
power_ten_int(exponent, &tmp_var);
6391+
div_var(var, &tmp_var, &tmp_var, rscale, true);
6392+
sig_out = get_str_from_var(&tmp_var);
64126393

6413-
free_var(&denominator);
6414-
free_var(&significand);
6394+
free_var(&tmp_var);
64156395

64166396
/*
64176397
* Allocate space for the result.
@@ -9498,6 +9478,34 @@ power_var_int(const NumericVar *base, int exp, NumericVar *result, int rscale)
94989478
round_var(result, rscale);
94999479
}
95009480

9481+
/*
9482+
* power_ten_int() -
9483+
*
9484+
* Raise ten to the power of exp, where exp is an integer. Note that unlike
9485+
* power_var_int(), this does no overflow/underflow checking or rounding.
9486+
*/
9487+
static void
9488+
power_ten_int(int exp, NumericVar *result)
9489+
{
9490+
/* Construct the result directly, starting from 10^0 = 1 */
9491+
set_var_from_var(&const_one, result);
9492+
9493+
/* Scale needed to represent the result exactly */
9494+
result->dscale = exp < 0 ? -exp : 0;
9495+
9496+
/* Base-NBASE weight of result and remaining exponent */
9497+
if (exp >= 0)
9498+
result->weight = exp / DEC_DIGITS;
9499+
else
9500+
result->weight = (exp + 1) / DEC_DIGITS - 1;
9501+
9502+
exp -= result->weight * DEC_DIGITS;
9503+
9504+
/* Final adjustment of the result's single NBASE digit */
9505+
while (exp-- > 0)
9506+
result->digits[0] *= 10;
9507+
}
9508+
95019509

95029510
/* ----------------------------------------------------------------------
95039511
*

src/test/regress/expected/numeric.out

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,6 +1278,39 @@ SELECT '' AS to_char_36, to_char('100'::numeric, 'f"ool\\"999');
12781278
| fool\ 100
12791279
(1 row)
12801280

1281+
-- Test scientific notation with various exponents
1282+
WITH v(exp) AS
1283+
(VALUES(-16379),(-16378),(-1234),(-789),(-45),(-5),(-4),(-3),(-2),(-1),(0),
1284+
(1),(2),(3),(4),(5),(38),(275),(2345),(45678),(131070),(131071))
1285+
SELECT exp,
1286+
to_char(('1.2345e'||exp)::numeric, '9.999EEEE') as numeric
1287+
FROM v;
1288+
exp | numeric
1289+
--------+----------------
1290+
-16379 | 1.235e-16379
1291+
-16378 | 1.235e-16378
1292+
-1234 | 1.235e-1234
1293+
-789 | 1.235e-789
1294+
-45 | 1.235e-45
1295+
-5 | 1.235e-05
1296+
-4 | 1.235e-04
1297+
-3 | 1.235e-03
1298+
-2 | 1.235e-02
1299+
-1 | 1.235e-01
1300+
0 | 1.235e+00
1301+
1 | 1.235e+01
1302+
2 | 1.235e+02
1303+
3 | 1.235e+03
1304+
4 | 1.235e+04
1305+
5 | 1.235e+05
1306+
38 | 1.235e+38
1307+
275 | 1.235e+275
1308+
2345 | 1.235e+2345
1309+
45678 | 1.235e+45678
1310+
131070 | 1.235e+131070
1311+
131071 | 1.235e+131071
1312+
(22 rows)
1313+
12811314
-- TO_NUMBER()
12821315
--
12831316
SET lc_numeric = 'C';

src/test/regress/sql/numeric.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,14 @@ SELECT '' AS to_char_34, to_char('100'::numeric, 'f"\\ool"999');
798798
SELECT '' AS to_char_35, to_char('100'::numeric, 'f"ool\"999');
799799
SELECT '' AS to_char_36, to_char('100'::numeric, 'f"ool\\"999');
800800

801+
-- Test scientific notation with various exponents
802+
WITH v(exp) AS
803+
(VALUES(-16379),(-16378),(-1234),(-789),(-45),(-5),(-4),(-3),(-2),(-1),(0),
804+
(1),(2),(3),(4),(5),(38),(275),(2345),(45678),(131070),(131071))
805+
SELECT exp,
806+
to_char(('1.2345e'||exp)::numeric, '9.999EEEE') as numeric
807+
FROM v;
808+
801809
-- TO_NUMBER()
802810
--
803811
SET lc_numeric = 'C';

0 commit comments

Comments
 (0)