Skip to content

Commit 786c3c0

Browse files
committed
Fix imprecision from interval rounding of multiplication/division.
Bruce, Michael Glaesemann
1 parent 548e2c0 commit 786c3c0

File tree

2 files changed

+40
-26
lines changed

2 files changed

+40
-26
lines changed

src/backend/utils/adt/timestamp.c

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*
99
*
1010
* IDENTIFICATION
11-
* $PostgreSQL: pgsql/src/backend/utils/adt/timestamp.c,v 1.166 2006/09/03 03:34:04 momjian Exp $
11+
* $PostgreSQL: pgsql/src/backend/utils/adt/timestamp.c,v 1.167 2006/09/05 01:13:39 momjian Exp $
1212
*
1313
*-------------------------------------------------------------------------
1414
*/
@@ -2514,28 +2514,34 @@ interval_mul(PG_FUNCTION_ARGS)
25142514
/*
25152515
* Fractional months full days into days.
25162516
*
2517-
* The remainders suffer from float rounding, so instead of
2518-
* doing the computation using just the remainder, we calculate
2519-
* the total number of days and subtract. Specifically, we are
2520-
* multipling by DAYS_PER_MONTH before dividing by factor.
2521-
* This greatly reduces rounding errors.
2517+
* Floating point calculation are inherently inprecise, so these
2518+
* calculations are crafted to produce the most reliable result
2519+
* possible. TSROUND() is needed to more accurately produce whole
2520+
* numbers where appropriate.
25222521
*/
2523-
month_remainder_days = (orig_month * (double)DAYS_PER_MONTH) * factor -
2524-
result->month * (double)DAYS_PER_MONTH;
2525-
sec_remainder = (orig_day * (double)SECS_PER_DAY) * factor -
2526-
result->day * (double)SECS_PER_DAY +
2527-
(month_remainder_days - (int32) month_remainder_days) * SECS_PER_DAY;
2522+
month_remainder_days = (orig_month * factor - result->month) * DAYS_PER_MONTH;
2523+
month_remainder_days = TSROUND(month_remainder_days);
2524+
sec_remainder = (orig_day * factor - result->day +
2525+
month_remainder_days - (int)month_remainder_days) * SECS_PER_DAY;
2526+
sec_remainder = TSROUND(sec_remainder);
2527+
2528+
/*
2529+
* Might have 24:00:00 hours due to rounding, or >24 hours because of
2530+
* time cascade from months and days. It might still be >24 if the
2531+
* combination of cascade and the seconds factor operation itself.
2532+
*/
2533+
if (Abs(sec_remainder) >= SECS_PER_DAY)
2534+
{
2535+
result->day += (int)(sec_remainder / SECS_PER_DAY);
2536+
sec_remainder -= (int)(sec_remainder / SECS_PER_DAY) * SECS_PER_DAY;
2537+
}
25282538

25292539
/* cascade units down */
25302540
result->day += (int32) month_remainder_days;
25312541
#ifdef HAVE_INT64_TIMESTAMP
25322542
result->time = rint(span->time * factor + sec_remainder * USECS_PER_SEC);
25332543
#else
2534-
/*
2535-
* TSROUND() needed to prevent -146:23:60.00 output on PowerPC for
2536-
* SELECT interval '-41 mon -12 days -360:00' * 0.3;
2537-
*/
2538-
result->time = span->time * factor + TSROUND(sec_remainder);
2544+
result->time = span->time * factor + sec_remainder;
25392545
#endif
25402546

25412547
PG_RETURN_INTERVAL_P(result);
@@ -2574,19 +2580,24 @@ interval_div(PG_FUNCTION_ARGS)
25742580
* Fractional months full days into days. See comment in
25752581
* interval_mul().
25762582
*/
2577-
month_remainder_days = (orig_month * (double)DAYS_PER_MONTH) / factor -
2578-
result->month * (double)DAYS_PER_MONTH;
2579-
sec_remainder = (orig_day * (double)SECS_PER_DAY) / factor -
2580-
result->day * (double)SECS_PER_DAY +
2581-
(month_remainder_days - (int32) month_remainder_days) * SECS_PER_DAY;
2583+
month_remainder_days = (orig_month / factor - result->month) * DAYS_PER_MONTH;
2584+
month_remainder_days = TSROUND(month_remainder_days);
2585+
sec_remainder = (orig_day / factor - result->day +
2586+
month_remainder_days - (int)month_remainder_days) * SECS_PER_DAY;
2587+
sec_remainder = TSROUND(sec_remainder);
2588+
if (Abs(sec_remainder) >= SECS_PER_DAY)
2589+
{
2590+
result->day += (int)(sec_remainder / SECS_PER_DAY);
2591+
sec_remainder -= (int)(sec_remainder / SECS_PER_DAY) * SECS_PER_DAY;
2592+
}
25822593

25832594
/* cascade units down */
25842595
result->day += (int32) month_remainder_days;
25852596
#ifdef HAVE_INT64_TIMESTAMP
25862597
result->time = rint(span->time / factor + sec_remainder * USECS_PER_SEC);
25872598
#else
25882599
/* See TSROUND comment in interval_mul(). */
2589-
result->time = span->time / factor + TSROUND(sec_remainder);
2600+
result->time = span->time / factor + sec_remainder;
25902601
#endif
25912602

25922603
PG_RETURN_INTERVAL_P(result);

src/include/utils/timestamp.h

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group
77
* Portions Copyright (c) 1994, Regents of the University of California
88
*
9-
* $PostgreSQL: pgsql/src/include/utils/timestamp.h,v 1.62 2006/07/13 16:49:20 momjian Exp $
9+
* $PostgreSQL: pgsql/src/include/utils/timestamp.h,v 1.63 2006/09/05 01:13:40 momjian Exp $
1010
*
1111
*-------------------------------------------------------------------------
1212
*/
@@ -161,11 +161,14 @@ typedef int32 fsec_t;
161161

162162
typedef double fsec_t;
163163

164-
/* round off to MAX_TIMESTAMP_PRECISION decimal places */
165-
/* note: this is also used for rounding off intervals */
164+
#endif
165+
166+
/*
167+
* Round off to MAX_TIMESTAMP_PRECISION decimal places.
168+
* Note: this is also used for rounding off intervals.
169+
*/
166170
#define TS_PREC_INV 1000000.0
167171
#define TSROUND(j) (rint(((double) (j)) * TS_PREC_INV) / TS_PREC_INV)
168-
#endif
169172

170173
#define TIMESTAMP_MASK(b) (1 << (b))
171174
#define INTERVAL_MASK(b) (1 << (b))

0 commit comments

Comments
 (0)