Fix imprecision from interval rounding of multiplication/division.
authorBruce Momjian
Tue, 5 Sep 2006 01:13:40 +0000 (01:13 +0000)
committerBruce Momjian
Tue, 5 Sep 2006 01:13:40 +0000 (01:13 +0000)
Bruce, Michael Glaesemann

src/backend/utils/adt/timestamp.c
src/include/utils/timestamp.h

index 35886fd200e58e53573a6684599ebc2a7d33be06..fcd90432af965ebddeb7c3111356788a975fdd82 100644 (file)
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *   $PostgreSQL: pgsql/src/backend/utils/adt/timestamp.c,v 1.166 2006/09/03 03:34:04 momjian Exp $
+ *   $PostgreSQL: pgsql/src/backend/utils/adt/timestamp.c,v 1.167 2006/09/05 01:13:39 momjian Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -2514,28 +2514,34 @@ interval_mul(PG_FUNCTION_ARGS)
    /*
     *  Fractional months full days into days.
     *
-    *  The remainders suffer from float rounding, so instead of
-    *  doing the computation using just the remainder, we calculate
-    *  the total number of days and subtract.  Specifically, we are
-    *  multipling by DAYS_PER_MONTH before dividing by factor.
-    *  This greatly reduces rounding errors.
+    *  Floating point calculation are inherently inprecise, so these
+    *  calculations are crafted to produce the most reliable result
+    *  possible.  TSROUND() is needed to more accurately produce whole
+    *  numbers where appropriate.
     */
-   month_remainder_days = (orig_month * (double)DAYS_PER_MONTH) * factor -
-           result->month * (double)DAYS_PER_MONTH;
-   sec_remainder = (orig_day * (double)SECS_PER_DAY) * factor -
-           result->day * (double)SECS_PER_DAY +
-           (month_remainder_days - (int32) month_remainder_days) * SECS_PER_DAY;
+   month_remainder_days = (orig_month * factor - result->month) * DAYS_PER_MONTH;
+   month_remainder_days = TSROUND(month_remainder_days);
+   sec_remainder = (orig_day * factor - result->day +
+           month_remainder_days - (int)month_remainder_days) * SECS_PER_DAY;
+   sec_remainder = TSROUND(sec_remainder);
+
+   /*
+    *  Might have 24:00:00 hours due to rounding, or >24 hours because of
+    *  time cascade from months and days.  It might still be >24 if the
+    *  combination of cascade and the seconds factor operation itself.
+    */
+   if (Abs(sec_remainder) >= SECS_PER_DAY)
+   {
+       result->day += (int)(sec_remainder / SECS_PER_DAY);
+       sec_remainder -= (int)(sec_remainder / SECS_PER_DAY) * SECS_PER_DAY;
+   }
 
    /* cascade units down */
    result->day += (int32) month_remainder_days;
 #ifdef HAVE_INT64_TIMESTAMP
    result->time = rint(span->time * factor + sec_remainder * USECS_PER_SEC);
 #else
-   /*
-    *  TSROUND() needed to prevent -146:23:60.00 output on PowerPC for
-    *  SELECT interval '-41 mon -12 days -360:00' * 0.3;
-    */
-   result->time = span->time * factor + TSROUND(sec_remainder);
+   result->time = span->time * factor + sec_remainder;
 #endif
 
    PG_RETURN_INTERVAL_P(result);
@@ -2574,11 +2580,16 @@ interval_div(PG_FUNCTION_ARGS)
     *  Fractional months full days into days.  See comment in 
     *  interval_mul().
     */
-   month_remainder_days = (orig_month * (double)DAYS_PER_MONTH) / factor -
-           result->month * (double)DAYS_PER_MONTH;
-   sec_remainder = (orig_day * (double)SECS_PER_DAY) / factor -
-           result->day * (double)SECS_PER_DAY +
-           (month_remainder_days - (int32) month_remainder_days) * SECS_PER_DAY;
+   month_remainder_days = (orig_month / factor - result->month) * DAYS_PER_MONTH;
+   month_remainder_days = TSROUND(month_remainder_days);
+   sec_remainder = (orig_day / factor - result->day +
+           month_remainder_days - (int)month_remainder_days) * SECS_PER_DAY;
+   sec_remainder = TSROUND(sec_remainder);
+   if (Abs(sec_remainder) >= SECS_PER_DAY)
+   {
+       result->day += (int)(sec_remainder / SECS_PER_DAY);
+       sec_remainder -= (int)(sec_remainder / SECS_PER_DAY) * SECS_PER_DAY;
+   }
 
    /* cascade units down */
    result->day += (int32) month_remainder_days;
@@ -2586,7 +2597,7 @@ interval_div(PG_FUNCTION_ARGS)
    result->time = rint(span->time / factor + sec_remainder * USECS_PER_SEC);
 #else
    /* See TSROUND comment in interval_mul(). */
-   result->time = span->time / factor + TSROUND(sec_remainder);
+   result->time = span->time / factor + sec_remainder;
 #endif
 
    PG_RETURN_INTERVAL_P(result);
index 2497f4256147fe55d1c0b38107341ac766bb2e21..45a742075146d3f098ddde2f521b38dd1eacf30d 100644 (file)
@@ -6,7 +6,7 @@
  * Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/include/utils/timestamp.h,v 1.62 2006/07/13 16:49:20 momjian Exp $
+ * $PostgreSQL: pgsql/src/include/utils/timestamp.h,v 1.63 2006/09/05 01:13:40 momjian Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -161,11 +161,14 @@ typedef int32 fsec_t;
 
 typedef double fsec_t;
 
-/* round off to MAX_TIMESTAMP_PRECISION decimal places */
-/* note: this is also used for rounding off intervals */
+#endif
+
+/*
+ * Round off to MAX_TIMESTAMP_PRECISION decimal places.
+ * Note: this is also used for rounding off intervals.
+ */
 #define TS_PREC_INV 1000000.0
 #define TSROUND(j) (rint(((double) (j)) * TS_PREC_INV) / TS_PREC_INV)
-#endif
 
 #define TIMESTAMP_MASK(b) (1 << (b))
 #define INTERVAL_MASK(b) (1 << (b))