On Wed, Feb 23, 2022 at 7:42 PM Joseph Koshakow <kosh...@gmail.com> wrote:
>
> Hi all,
>
> I noticed something odd when going through some
> of the Interval code. The DAYS_PER_YEAR constant
> is defined in src/include/datatype/timestamp.h.
> > #define DAYS_PER_YEAR    365.25    /* assumes leap year every four years */
>
> We execute the EXTRACT and date_part functions in
> src/backend/utils/adt/timestamp.c in
> > static Datum
> > interval_part_common(PG_FUNCTION_ARGS, bool retnumeric)
>
> When executing date_part we multiply the total
> years in the Interval by DAYS_PER_YEAR
> > result += ((double) DAYS_PER_YEAR * SECS_PER_DAY) * (interval->month / 
> > MONTHS_PER_YEAR);
> > result += ((double) DAYS_PER_MONTH * SECS_PER_DAY) * (interval->month % 
> > MONTHS_PER_YEAR);
> > result += ((double) SECS_PER_DAY) * interval->day;
>
>
> However when executing EXTRACT we first truncate
> DAYS_PER_YEAR to an integer, and then multiply it
> by the total years in the Interval
> /* this always fits into int64 */
> > secs_from_day_month = ((int64) DAYS_PER_YEAR * (interval->month / 
> > MONTHS_PER_YEAR) +
> >                          (int64) DAYS_PER_MONTH * (interval->month % 
> > MONTHS_PER_YEAR) +
> >                           interval->day) * SECS_PER_DAY;
>
> Is this truncation on purpose? It seems like
> EXTRACT is not accounting for leap years in
> it's calculation.
>
> - Joe Koshakow

Oops I sent that to the wrong email. If this isn't intented I've created a patch
that fixes it, with the following two open questions
 * DAYS_PER_YEAR_NUM is recalculated every time. Is there anyway
to convert a float directly to a numeric to avoid this?
 * For some reason the change adds a lot of trailing zeros to the result. I'm
not sure why that is.

- Joe Koshakow
From 9ac8bc7e0fcd56343a9b659aa5c6624ec81c5ff2 Mon Sep 17 00:00:00 2001
From: Joseph Koshakow <kosh...@gmail.com>
Date: Wed, 23 Feb 2022 21:25:13 -0500
Subject: [PATCH] Consider leap years when extracting epoch from interval

---
 src/backend/utils/adt/timestamp.c      | 28 ++++++++++++++++++--------
 src/include/datatype/timestamp.h       |  5 +++--
 src/test/regress/expected/interval.out | 18 ++++++++++++++---
 src/test/regress/sql/interval.sql      |  3 +++
 4 files changed, 41 insertions(+), 13 deletions(-)

diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 36f8a84bcc..84396e385a 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -5264,13 +5264,17 @@ interval_part_common(PG_FUNCTION_ARGS, bool retnumeric)
 		if (retnumeric)
 		{
 			Numeric		result;
+			Numeric 	secs_from_year;
 			int64		secs_from_day_month;
 			int64		val;
 
 			/* this always fits into int64 */
-			secs_from_day_month = ((int64) DAYS_PER_YEAR * (interval->month / MONTHS_PER_YEAR) +
-								   (int64) DAYS_PER_MONTH * (interval->month % MONTHS_PER_YEAR) +
-								   interval->day) * SECS_PER_DAY;
+			secs_from_day_month = ((int64) DAYS_PER_MONTH * (interval->month % MONTHS_PER_YEAR) +
+								  interval->day) * SECS_PER_DAY;
+			secs_from_year = numeric_mul_opt_error(
+					DAYS_PER_YEAR_NUM,
+					int64_to_numeric(interval->month / MONTHS_PER_YEAR * SECS_PER_DAY),
+					NULL);
 
 			/*---
 			 * result = secs_from_day_month + interval->time / 1'000'000
@@ -5284,12 +5288,20 @@ interval_part_common(PG_FUNCTION_ARGS, bool retnumeric)
 			 */
 			if (!pg_mul_s64_overflow(secs_from_day_month, 1000000, &val) &&
 				!pg_add_s64_overflow(val, interval->time, &val))
-				result = int64_div_fast_to_numeric(val, 6);
+			{
+				result = numeric_add_opt_error(
+						secs_from_year,
+						int64_div_fast_to_numeric(val,6),
+						NULL);
+			}
 			else
-				result =
-					numeric_add_opt_error(int64_div_fast_to_numeric(interval->time, 6),
-										  int64_to_numeric(secs_from_day_month),
-										  NULL);
+			{
+				result = numeric_add_opt_error(
+						int64_div_fast_to_numeric(interval->time, 6),
+						int64_to_numeric(secs_from_day_month),
+						NULL);
+				result = numeric_add_opt_error(result, secs_from_year, NULL);
+			}
 
 			PG_RETURN_NUMERIC(result);
 		}
diff --git a/src/include/datatype/timestamp.h b/src/include/datatype/timestamp.h
index 5fa38d20d8..457f10dafc 100644
--- a/src/include/datatype/timestamp.h
+++ b/src/include/datatype/timestamp.h
@@ -65,8 +65,9 @@ typedef struct
  * Assorted constants for datetime-related calculations
  */
 
-#define DAYS_PER_YEAR	365.25	/* assumes leap year every four years */
-#define MONTHS_PER_YEAR 12
+#define DAYS_PER_YEAR		365.25	/* assumes leap year every four years */
+#define DAYS_PER_YEAR_NUM	(numeric_add_opt_error(int64_to_numeric(365), numeric_div_opt_error(int64_to_numeric(1), int64_to_numeric(4), NULL), NULL))
+#define MONTHS_PER_YEAR 	12
 /*
  *	DAYS_PER_MONTH is very imprecise.  The more accurate value is
  *	365.2425/12 = 30.436875, or '30 days 10:29:06'.  Right now we only
diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out
index accd4a7d90..98e5cb6967 100644
--- a/src/test/regress/expected/interval.out
+++ b/src/test/regress/expected/interval.out
@@ -1038,8 +1038,20 @@ SELECT f1,
 
 -- internal overflow test case
 SELECT extract(epoch from interval '1000000000 days');
-        extract        
------------------------
- 86400000000000.000000
+               extract               
+-------------------------------------
+ 86400000000000.00000000000000000000
+(1 row)
+
+SELECT extract(epoch from interval '4 years');
+            extract             
+--------------------------------
+ 126230400.00000000000000000000
+(1 row)
+
+SELECT date_part('epoch', interval '4 years');
+ date_part 
+-----------
+ 126230400
 (1 row)
 
diff --git a/src/test/regress/sql/interval.sql b/src/test/regress/sql/interval.sql
index 6d532398bd..f2abf1f7e4 100644
--- a/src/test/regress/sql/interval.sql
+++ b/src/test/regress/sql/interval.sql
@@ -355,3 +355,6 @@ SELECT f1,
 
 -- internal overflow test case
 SELECT extract(epoch from interval '1000000000 days');
+
+SELECT extract(epoch from interval '4 years');
+SELECT date_part('epoch', interval '4 years');
-- 
2.25.1

Reply via email to