While testing the numeric_power() patch in [1], I found this problem trying to use to_char() to format very small numbers:
SELECT to_char(1.2e-1001, '9.9EEEE'); -- OK to_char ------------ 1.2e-1001 SELECT to_char(1.2e-1002, '9.9EEEE'); -- fails ERROR: division by zero It turns out that the problem is in get_str_from_var_sci() which attempts to divide the input by 1e-1002 to get the significand. However, it is using power_var_int() to compute 1e-1002, which has a maximum rscale of NUMERIC_MAX_DISPLAY_SCALE (1000), so it returns 0, which is the correct answer to that scale, and then get_str_from_var_sci() attempts to divide by that. Rather than messing with power_var_int(), I think the simplest solution is to just introduce a new local function, as in the attached patch. This directly constructs 10^n, for integer n, which is pretty trivial, and doesn't need any numeric multiplication or rounding. Regards, Dean [1] https://www.postgresql.org/message-id/CAEZATCUWUV_BP41Ob7QY12oF%2BqDxjTWfDpkdkcOOuojrDvOLxw%40mail.gmail.com
diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c new file mode 100644 index faff09f..e05e5d7 --- a/src/backend/utils/adt/numeric.c +++ b/src/backend/utils/adt/numeric.c @@ -428,16 +428,6 @@ static const NumericDigit const_two_data 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 @@ -582,6 +572,7 @@ static void power_var(const NumericVar * 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, @@ -7213,9 +7204,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; @@ -7252,25 +7241,16 @@ get_str_from_var_sci(const NumericVar *v } /* - * 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. @@ -10468,6 +10448,34 @@ power_var_int(const NumericVar *base, in 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 new file mode 100644 index cc11995..8f0b40a --- a/src/test/regress/expected/numeric.out +++ b/src/test/regress/expected/numeric.out @@ -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, diff --git a/src/test/regress/sql/numeric.sql b/src/test/regress/sql/numeric.sql new file mode 100644 index 14b4acf..c9730bc --- a/src/test/regress/sql/numeric.sql +++ b/src/test/regress/sql/numeric.sql @@ -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,