From 313ed1ed9498f977262e180a080c7748197ced5c Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Sun, 9 Oct 2005 17:21:47 +0000
Subject: [PATCH] Fix (hopefully for the last time) problems with datetime
 values displaying like '23:59:60' because of fractional-second roundoff
 problems.  Trying to control this upstream of the actual display code was
 hopeless; the right way is to explicitly round fractional seconds in the
 display code and then refigure the results if the fraction rounds up to 1. 
 Per bug #1927.

---
 contrib/btree_gist/btree_ts.c              |  2 -
 src/backend/utils/adt/date.c               | 18 +++++-
 src/backend/utils/adt/datetime.c           |  8 +--
 src/backend/utils/adt/timestamp.c          | 68 ++++++++++++++--------
 src/include/utils/date.h                   |  6 +-
 src/include/utils/timestamp.h              |  9 ++-
 src/interfaces/ecpg/pgtypeslib/dt.h        |  7 ++-
 src/interfaces/ecpg/pgtypeslib/dt_common.c |  3 +-
 src/interfaces/ecpg/pgtypeslib/interval.c  | 11 +++-
 src/interfaces/ecpg/pgtypeslib/timestamp.c | 49 +++++++++++-----
 10 files changed, 125 insertions(+), 56 deletions(-)

diff --git a/contrib/btree_gist/btree_ts.c b/contrib/btree_gist/btree_ts.c
index 6c9481b4b2e..119c45093eb 100644
--- a/contrib/btree_gist/btree_ts.c
+++ b/contrib/btree_gist/btree_ts.c
@@ -122,9 +122,7 @@ tstz_to_ts_gmt(Timestamp *gmt, TimestampTz *ts)
 		*gmt -= (tz * INT64CONST(1000000));
 #else
 		*gmt -= tz;
-		*gmt = JROUND(*gmt);
 #endif
-
 	}
 	return gmt;
 }
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index b36ee180929..ec1d808544b 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/utils/adt/date.c,v 1.120 2005/09/09 02:31:49 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/utils/adt/date.c,v 1.121 2005/10/09 17:21:46 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -944,10 +944,18 @@ time2tm(TimeADT time, struct pg_tm *tm, fsec_t *fsec)
 #else
 	double		trem;
 
+recalc:
 	trem = time;
 	TMODULO(trem, tm->tm_hour, (double)SECS_PER_HOUR);
 	TMODULO(trem, tm->tm_min, (double)SECS_PER_MINUTE);
 	TMODULO(trem, tm->tm_sec, 1.0);
+	trem = TIMEROUND(trem);
+	/* roundoff may need to propagate to higher-order fields */
+	if (trem >= 1.0)
+	{
+		time = ceil(time);
+		goto recalc;
+	}
 	*fsec = trem;
 #endif
 
@@ -1837,9 +1845,17 @@ timetz2tm(TimeTzADT *time, struct pg_tm *tm, fsec_t *fsec, int *tzp)
 #else
 	double		trem = time->time;
 
+recalc:
 	TMODULO(trem, tm->tm_hour, (double)SECS_PER_HOUR);
 	TMODULO(trem, tm->tm_min, (double)SECS_PER_MINUTE);
 	TMODULO(trem, tm->tm_sec, 1.0);
+	trem = TIMEROUND(trem);
+	/* roundoff may need to propagate to higher-order fields */
+	if (trem >= 1.0)
+	{
+		trem = ceil(time->time);
+		goto recalc;
+	}
 	*fsec = trem;
 #endif
 
diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c
index 9e5be7d4cff..74dda5441f1 100644
--- a/src/backend/utils/adt/datetime.c
+++ b/src/backend/utils/adt/datetime.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/utils/adt/datetime.c,v 1.157 2005/07/23 14:25:33 momjian Exp $
+ *	  $PostgreSQL: pgsql/src/backend/utils/adt/datetime.c,v 1.158 2005/10/09 17:21:46 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -3488,8 +3488,8 @@ EncodeTimeOnly(struct pg_tm *tm, fsec_t fsec, int *tzp, int style, char *str)
 	sprintf(str, "%02d:%02d", tm->tm_hour, tm->tm_min);
 
 	/*
-	 * Print fractional seconds if any.  The field widths here should be
-	 * at least equal to the larger of MAX_TIME_PRECISION and
+	 * Print fractional seconds if any.  The fractional field widths
+	 * here should be equal to the larger of MAX_TIME_PRECISION and
 	 * MAX_TIMESTAMP_PRECISION.
 	 */
 	if (fsec != 0)
@@ -3497,7 +3497,7 @@ EncodeTimeOnly(struct pg_tm *tm, fsec_t fsec, int *tzp, int style, char *str)
 #ifdef HAVE_INT64_TIMESTAMP
 		sprintf(str + strlen(str), ":%02d.%06d", tm->tm_sec, fsec);
 #else
-		sprintf(str + strlen(str), ":%012.9f", tm->tm_sec + fsec);
+		sprintf(str + strlen(str), ":%013.10f", tm->tm_sec + fsec);
 #endif
 		TrimTrailingZeros(str);
 	}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 8f18b870b5e..73e7bb8ea8a 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.153 2005/09/09 06:46:14 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/utils/adt/timestamp.c,v 1.154 2005/10/09 17:21:46 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -998,10 +998,8 @@ dt2time(Timestamp jd, int *hour, int *min, int *sec, fsec_t *fsec)
 	*min = time / SECS_PER_MINUTE;
 	time -= (*min) * SECS_PER_MINUTE;
 	*sec = time;
-	*fsec = JROUND(time - *sec);
+	*fsec = time - *sec;
 #endif
-
-	return;
 }	/* dt2time() */
 
 
@@ -1038,8 +1036,8 @@ timestamp2tm(Timestamp dt, int *tzp, struct pg_tm *tm, fsec_t *fsec, char **tzn,
 #endif
 	}
 
-	time = dt;
 #ifdef HAVE_INT64_TIMESTAMP
+	time = dt;
 	TMODULO(time, date, USECS_PER_DAY);
 
 	if (time < INT64CONST(0))
@@ -1047,26 +1045,53 @@ timestamp2tm(Timestamp dt, int *tzp, struct pg_tm *tm, fsec_t *fsec, char **tzn,
 		time += USECS_PER_DAY;
 		date -= 1;
 	}
+
+	/* add offset to go from J2000 back to standard Julian date */
+	date += POSTGRES_EPOCH_JDATE;
+
+	/* Julian day routine does not work for negative Julian days */
+	if (date < 0 || date > (Timestamp) INT_MAX)
+		return -1;
+
+	j2date((int) date, &tm->tm_year, &tm->tm_mon, &tm->tm_mday);
+	dt2time(time, &tm->tm_hour, &tm->tm_min, &tm->tm_sec, fsec);
 #else
+	time = dt;
 	TMODULO(time, date, (double)SECS_PER_DAY);
 
 	if (time < 0)
 	{
 		time += SECS_PER_DAY;
-		date -=1;
+		date -= 1;
 	}
-#endif
 
 	/* add offset to go from J2000 back to standard Julian date */
 	date += POSTGRES_EPOCH_JDATE;
 
+recalc_d:
 	/* Julian day routine does not work for negative Julian days */
-	if (date <0 || date >(Timestamp) INT_MAX)
+	if (date < 0 || date > (Timestamp) INT_MAX)
 		return -1;
 
 	j2date((int) date, &tm->tm_year, &tm->tm_mon, &tm->tm_mday);
+recalc_t:
 	dt2time(time, &tm->tm_hour, &tm->tm_min, &tm->tm_sec, fsec);
 
+	*fsec = TSROUND(*fsec);
+	/* roundoff may need to propagate to higher-order fields */
+	if (*fsec >= 1.0)
+	{
+		time = ceil(time);
+		if (time >= (double)SECS_PER_DAY)
+		{
+			time = 0;
+			date += 1;
+			goto recalc_d;
+		}
+		goto recalc_t;
+	}
+#endif
+
 	/* Done if no TZ conversion wanted */
 	if (tzp == NULL)
 	{
@@ -1216,9 +1241,17 @@ interval2tm(Interval span, struct pg_tm *tm, fsec_t *fsec)
 	tm->tm_sec = time / USECS_PER_SEC;
 	*fsec = time - (tm->tm_sec * USECS_PER_SEC);
 #else
+recalc:
 	TMODULO(time, tm->tm_hour, (double)SECS_PER_HOUR);
 	TMODULO(time, tm->tm_min, (double)SECS_PER_MINUTE);
 	TMODULO(time, tm->tm_sec, 1.0);
+	time = TSROUND(time);
+	/* roundoff may need to propagate to higher-order fields */
+	if (time >= 1.0)
+	{
+		time = ceil(span.time);
+		goto recalc;
+	}
 	*fsec = time;
 #endif
 
@@ -1237,8 +1270,7 @@ tm2interval(struct pg_tm *tm, fsec_t fsec, Interval *span)
 #else
 	span->time = (((tm->tm_hour * (double)MINS_PER_HOUR) +
 						tm->tm_min) * (double)SECS_PER_MINUTE) +
-						tm->tm_sec;
-	span->time = JROUND(span->time + fsec);
+						tm->tm_sec + fsec;
 #endif
 
 	return 0;
@@ -1266,7 +1298,6 @@ dt2local(Timestamp dt, int tz)
 	dt -= (tz * USECS_PER_SEC);
 #else
 	dt -= tz;
-	dt = JROUND(dt);
 #endif
 	return dt;
 }	/* dt2local() */
@@ -1901,11 +1932,7 @@ timestamp_mi(PG_FUNCTION_ARGS)
 				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
 				 errmsg("cannot subtract infinite timestamps")));
 
-#ifdef HAVE_INT64_TIMESTAMP
 	result->time = dt1 - dt2;
-#else
-	result->time = JROUND(dt1 - dt2);
-#endif
 
 	result->month = 0;
 	result->day = 0;
@@ -2224,11 +2251,7 @@ interval_pl(PG_FUNCTION_ARGS)
 
 	result->month = span1->month + span2->month;
 	result->day = span1->day + span2->day;
-#ifdef HAVE_INT64_TIMESTAMP
 	result->time = span1->time + span2->time;
-#else
-	result->time = JROUND(span1->time + span2->time);
-#endif
 
 	PG_RETURN_INTERVAL_P(result);
 }
@@ -2244,11 +2267,7 @@ interval_mi(PG_FUNCTION_ARGS)
 
 	result->month = span1->month - span2->month;
 	result->day = span1->day - span2->day;
-#ifdef HAVE_INT64_TIMESTAMP
 	result->time = span1->time - span2->time;
-#else
-	result->time = JROUND(span1->time - span2->time);
-#endif
 
 	PG_RETURN_INTERVAL_P(result);
 }
@@ -2280,7 +2299,7 @@ interval_mul(PG_FUNCTION_ARGS)
 #ifdef HAVE_INT64_TIMESTAMP
 	result->time = rint(span->time * factor + day_remainder * USECS_PER_DAY);
 #else
-	result->time = JROUND(span->time * factor + day_remainder * SECS_PER_DAY);
+	result->time = span->time * factor + day_remainder * SECS_PER_DAY;
 #endif
 
 	result = DatumGetIntervalP(DirectFunctionCall1(interval_justify_hours,
@@ -2332,7 +2351,6 @@ interval_div(PG_FUNCTION_ARGS)
 	result->time += rint(day_remainder * USECS_PER_DAY);
 #else
 	result->time += day_remainder * SECS_PER_DAY;
-	result->time = JROUND(result->time);
 #endif
 
 	result = DatumGetIntervalP(DirectFunctionCall1(interval_justify_hours,
diff --git a/src/include/utils/date.h b/src/include/utils/date.h
index c3c4a06d871..869e2ade29b 100644
--- a/src/include/utils/date.h
+++ b/src/include/utils/date.h
@@ -7,7 +7,7 @@
  * Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/include/utils/date.h,v 1.30 2005/02/25 16:13:29 teodor Exp $
+ * $PostgreSQL: pgsql/src/include/utils/date.h,v 1.31 2005/10/09 17:21:47 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -60,6 +60,10 @@ typedef struct
 
 #define MAX_TIME_PRECISION 10
 
+/* round off to MAX_TIME_PRECISION decimal places */
+#define TIME_PREC_INV 10000000000.0
+#define TIMEROUND(j) (rint(((double) (j)) * TIME_PREC_INV) / TIME_PREC_INV)
+
 #define DatumGetDateADT(X)	  ((DateADT) DatumGetInt32(X))
 #define DatumGetTimeADT(X)	  ((TimeADT) DatumGetFloat8(X))
 #define DatumGetTimeTzADTP(X) ((TimeTzADT *) DatumGetPointer(X))
diff --git a/src/include/utils/timestamp.h b/src/include/utils/timestamp.h
index 14c8f6c91b9..dc218f3b28f 100644
--- a/src/include/utils/timestamp.h
+++ b/src/include/utils/timestamp.h
@@ -6,7 +6,7 @@
  * Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/include/utils/timestamp.h,v 1.55 2005/10/07 20:13:16 momjian Exp $
+ * $PostgreSQL: pgsql/src/include/utils/timestamp.h,v 1.56 2005/10/09 17:21:47 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -163,8 +163,11 @@ typedef int32 fsec_t;
 
 typedef double fsec_t;
 
-#define TIME_PREC_INV 1000000.0
-#define JROUND(j) (rint(((double) (j)) * TIME_PREC_INV) / TIME_PREC_INV)
+/* 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))
diff --git a/src/interfaces/ecpg/pgtypeslib/dt.h b/src/interfaces/ecpg/pgtypeslib/dt.h
index 3c9dd4e0186..d7ca2d5bf2f 100644
--- a/src/interfaces/ecpg/pgtypeslib/dt.h
+++ b/src/interfaces/ecpg/pgtypeslib/dt.h
@@ -13,8 +13,11 @@ typedef int32 fsec_t;
 
 typedef double fsec_t;
 
-#define TIME_PREC_INV 1000000.0
-#define JROUND(j) (rint(((double) (j)) * TIME_PREC_INV) / TIME_PREC_INV)
+/* 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 USE_POSTGRES_DATES				0
diff --git a/src/interfaces/ecpg/pgtypeslib/dt_common.c b/src/interfaces/ecpg/pgtypeslib/dt_common.c
index 7cb65f7e962..305f192a7bd 100644
--- a/src/interfaces/ecpg/pgtypeslib/dt_common.c
+++ b/src/interfaces/ecpg/pgtypeslib/dt_common.c
@@ -1255,9 +1255,8 @@ dt2time(double jd, int *hour, int *min, int *sec, fsec_t *fsec)
 	*min = time / SECS_PER_MINUTE;
 	time -= (*min) * SECS_PER_MINUTE;
 	*sec = time;
-	*fsec = JROUND(time - *sec);
+	*fsec = time - *sec;
 #endif
-	return;
 }	/* dt2time() */
 
 
diff --git a/src/interfaces/ecpg/pgtypeslib/interval.c b/src/interfaces/ecpg/pgtypeslib/interval.c
index a9ed260e70a..93a9d3b45e1 100644
--- a/src/interfaces/ecpg/pgtypeslib/interval.c
+++ b/src/interfaces/ecpg/pgtypeslib/interval.c
@@ -702,10 +702,18 @@ interval2tm(interval span, struct tm *tm, fsec_t *fsec)
 	tm->tm_sec = time / USECS_PER_SEC;
 	*fsec = time - (tm->tm_sec * USECS_PER_SEC);
 #else
+recalc:
 	TMODULO(time, tm->tm_mday, (double)SECS_PER_DAY);
 	TMODULO(time, tm->tm_hour, (double)SECS_PER_HOUR);
 	TMODULO(time, tm->tm_min, (double)SECS_PER_MINUTE);
 	TMODULO(time, tm->tm_sec, 1.0);
+	time = TSROUND(time);
+	/* roundoff may need to propagate to higher-order fields */
+	if (time >= 1.0)
+	{
+		time = ceil(span.time);
+		goto recalc;
+	}
 	*fsec = time;
 #endif
 
@@ -725,8 +733,7 @@ tm2interval(struct tm *tm, fsec_t fsec, interval *span)
 	span->time = (((((tm->tm_mday * (double)HOURS_PER_DAY) +
 						tm->tm_hour) * (double)MINS_PER_HOUR) +
 						tm->tm_min) * (double)SECS_PER_MINUTE) +
-						tm->tm_sec;
-	span->time = JROUND(span->time + fsec);
+						tm->tm_sec + fsec;
 #endif
 
 	return 0;
diff --git a/src/interfaces/ecpg/pgtypeslib/timestamp.c b/src/interfaces/ecpg/pgtypeslib/timestamp.c
index 74b024d0a9b..5b7928b182e 100644
--- a/src/interfaces/ecpg/pgtypeslib/timestamp.c
+++ b/src/interfaces/ecpg/pgtypeslib/timestamp.c
@@ -38,7 +38,6 @@ dt2local(timestamp dt, int tz)
 	dt -= (tz * USECS_PER_SEC);
 #else
 	dt -= tz;
-	dt = JROUND(dt);
 #endif
 	return dt;
 }	/* dt2local() */
@@ -124,9 +123,8 @@ dt2time(timestamp jd, int *hour, int *min, int *sec, fsec_t *fsec)
 	*min = time / SECS_PER_MINUTE;
 	time -= (*min) * SECS_PER_MINUTE;
 	*sec = time;
-	*fsec = JROUND(time - *sec);
+	*fsec = time - *sec;
 #endif
-	return;
 }	/* dt2time() */
 
 /* timestamp2tm()
@@ -144,7 +142,7 @@ static int
 timestamp2tm(timestamp dt, int *tzp, struct tm *tm, fsec_t *fsec, char **tzn)
 {
 #ifdef HAVE_INT64_TIMESTAMP
-	int			dDate,
+	int64		dDate,
 				date0;
 	int64		time;
 #else
@@ -160,8 +158,8 @@ timestamp2tm(timestamp dt, int *tzp, struct tm *tm, fsec_t *fsec, char **tzn)
 
 	date0 = date2j(2000, 1, 1);
 
-	time = dt;
 #ifdef HAVE_INT64_TIMESTAMP
+	time = dt;
 	TMODULO(time, dDate, USECS_PER_DAY);
 
 	if (time < INT64CONST(0))
@@ -169,7 +167,18 @@ timestamp2tm(timestamp dt, int *tzp, struct tm *tm, fsec_t *fsec, char **tzn)
 		time += USECS_PER_DAY;
 		dDate -= 1;
 	}
+
+	/* add offset to go from J2000 back to standard Julian date */
+	dDate += date0;
+
+	/* Julian day routine does not work for negative Julian days */
+	if (dDate < 0 || dDate > (timestamp) INT_MAX)
+		return -1;
+
+	j2date((int) dDate, &tm->tm_year, &tm->tm_mon, &tm->tm_mday);
+	dt2time(time, &tm->tm_hour, &tm->tm_min, &tm->tm_sec, fsec);
 #else
+	time = dt;
 	TMODULO(time, dDate, (double)SECS_PER_DAY);
 
 	if (time < 0)
@@ -177,18 +186,34 @@ timestamp2tm(timestamp dt, int *tzp, struct tm *tm, fsec_t *fsec, char **tzn)
 		time += SECS_PER_DAY;
 		dDate -= 1;
 	}
-#endif
-
-	/* Julian day routine does not work for negative Julian days */
-	if (dDate < -date0)
-		return -1;
 
 	/* add offset to go from J2000 back to standard Julian date */
 	dDate += date0;
 
+recalc_d:
+	/* Julian day routine does not work for negative Julian days */
+	if (dDate < 0 || dDate > (timestamp) INT_MAX)
+		return -1;
+
 	j2date((int) dDate, &tm->tm_year, &tm->tm_mon, &tm->tm_mday);
+recalc_t:
 	dt2time(time, &tm->tm_hour, &tm->tm_min, &tm->tm_sec, fsec);
 
+	*fsec = TSROUND(*fsec);
+	/* roundoff may need to propagate to higher-order fields */
+	if (*fsec >= 1.0)
+	{
+		time = ceil(time);
+		if (time >= (double)SECS_PER_DAY)
+		{
+			time = 0;
+			dDate += 1;
+			goto recalc_d;
+		}
+		goto recalc_t;
+	}
+#endif
+
 	if (tzp != NULL)
 	{
 		/*
@@ -791,11 +816,7 @@ PGTYPEStimestamp_sub(timestamp *ts1, timestamp *ts2, interval *iv)
 	if (TIMESTAMP_NOT_FINITE(*ts1) || TIMESTAMP_NOT_FINITE(*ts2))
 		return PGTYPES_TS_ERR_EINFTIME;
 	else
-#ifdef HAVE_INT64_TIMESTAMP
 		iv->time = (ts1 - ts2);
-#else
-		iv->time = JROUND(ts1 - ts2);
-#endif
 
 	iv->month = 0;
 
-- 
GitLab