From: Bruce Momjian Date: Sun, 3 Sep 2006 03:34:04 +0000 (+0000) Subject: Properly round months into days and into seconds for interval X-Git-Tag: REL8_2_BETA1~181 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=fc51c9186a095581beb40d0a46ee13ffb1f99211;p=postgresql Properly round months into days and into seconds for interval multiplication/division queries like select '41 mon 10:00:00'::interval / 10 as "pos". Report from Michael Glaesemann --- diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c index 73f341a035..35886fd200 100644 --- a/src/backend/utils/adt/timestamp.c +++ b/src/backend/utils/adt/timestamp.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/utils/adt/timestamp.c,v 1.165 2006/07/13 16:49:16 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/timestamp.c,v 1.166 2006/09/03 03:34:04 momjian Exp $ * *------------------------------------------------------------------------- */ @@ -2492,19 +2492,14 @@ interval_mul(PG_FUNCTION_ARGS) { Interval *span = PG_GETARG_INTERVAL_P(0); float8 factor = PG_GETARG_FLOAT8(1); - double month_remainder, - day_remainder, - month_remainder_days; + double month_remainder_days, sec_remainder; + int32 orig_month = span->month, orig_day = span->day; Interval *result; result = (Interval *) palloc(sizeof(Interval)); - month_remainder = span->month * factor; - day_remainder = span->day * factor; - result->month = (int32) month_remainder; - result->day = (int32) day_remainder; - month_remainder -= result->month; - day_remainder -= result->day; + result->month = (int32) (span->month * factor); + result->day = (int32) (span->day * factor); /* * The above correctly handles the whole-number part of the month and day @@ -2516,16 +2511,31 @@ interval_mul(PG_FUNCTION_ARGS) * using justify_hours and/or justify_days. */ - /* fractional months full days into days */ - month_remainder_days = month_remainder * DAYS_PER_MONTH; - result->day += (int32) month_remainder_days; - /* fractional months partial days into time */ - day_remainder += month_remainder_days - (int32) month_remainder_days; + /* + * 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. + */ + 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; + /* cascade units down */ + result->day += (int32) month_remainder_days; #ifdef HAVE_INT64_TIMESTAMP - result->time = rint(span->time * factor + day_remainder * USECS_PER_DAY); + result->time = rint(span->time * factor + sec_remainder * USECS_PER_SEC); #else - result->time = span->time * factor + day_remainder * SECS_PER_DAY; + /* + * 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); #endif PG_RETURN_INTERVAL_P(result); @@ -2546,11 +2556,10 @@ interval_div(PG_FUNCTION_ARGS) { Interval *span = PG_GETARG_INTERVAL_P(0); float8 factor = PG_GETARG_FLOAT8(1); - double month_remainder, - day_remainder, - month_remainder_days; + double month_remainder_days, sec_remainder; + int32 orig_month = span->month, orig_day = span->day; Interval *result; - + result = (Interval *) palloc(sizeof(Interval)); if (factor == 0.0) @@ -2558,27 +2567,26 @@ interval_div(PG_FUNCTION_ARGS) (errcode(ERRCODE_DIVISION_BY_ZERO), errmsg("division by zero"))); - month_remainder = span->month / factor; - day_remainder = span->day / factor; - result->month = (int32) month_remainder; - result->day = (int32) day_remainder; - month_remainder -= result->month; - day_remainder -= result->day; + result->month = (int32) (span->month / factor); + result->day = (int32) (span->day / factor); /* - * Handle any fractional parts the same way as in interval_mul. + * 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; - /* fractional months full days into days */ - month_remainder_days = month_remainder * DAYS_PER_MONTH; + /* cascade units down */ result->day += (int32) month_remainder_days; - /* fractional months partial days into time */ - day_remainder += month_remainder_days - (int32) month_remainder_days; - #ifdef HAVE_INT64_TIMESTAMP - result->time = rint(span->time / factor + day_remainder * USECS_PER_DAY); + result->time = rint(span->time / factor + sec_remainder * USECS_PER_SEC); #else - result->time = span->time / factor + day_remainder * SECS_PER_DAY; + /* See TSROUND comment in interval_mul(). */ + result->time = span->time / factor + TSROUND(sec_remainder); #endif PG_RETURN_INTERVAL_P(result); diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out index 7a43e90d5d..0adda4a981 100644 --- a/src/test/regress/expected/interval.out +++ b/src/test/regress/expected/interval.out @@ -218,7 +218,7 @@ SELECT '' AS ten, * FROM INTERVAL_TBL; select avg(f1) from interval_tbl; avg ------------------------------------------------- - @ 4 years 1 mon 9 days 28 hours 18 mins 23 secs + @ 4 years 1 mon 10 days 4 hours 18 mins 23 secs (1 row) -- test long interval input