From 5e91331955452b03cab76aa52e905d311eaf155e Mon Sep 17 00:00:00 2001
From: Isaac Morland <isaac.morland@gmail.com>
Date: Wed, 31 Mar 2021 03:06:06 +0000
Subject: [PATCH] Add abs(interval) function and related @ operator.

---
 doc/src/sgml/func.sgml                 | 32 ++++++++++++++++++++++++++
 src/backend/utils/adt/timestamp.c      | 31 +++++++++++++++++++++++--
 src/include/catalog/pg_operator.dat    |  3 +++
 src/include/catalog/pg_proc.dat        |  6 +++++
 src/test/regress/expected/interval.out | 25 ++++++++++++++++++++
 src/test/regress/sql/interval.sql      |  7 ++++++
 6 files changed, 102 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index fbf6062d0a..62bd8b8375 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -8474,6 +8474,20 @@ SELECT regexp_match('abc01234xyz', '(?:(.*?)(\d+)(.*)){1,1}');
         </para></entry>
        </row>
 
+       <row>
+        <entry role="func_table_entry"><para role="func_signature">
+         <literal>@</literal> <type>interval</type>
+         <returnvalue>interval</returnvalue>
+        </para>
+        <para>
+         Find the absolute value of an interval
+        </para>
+        <para>
+         <literal>@ interval '-23 hours'</literal>
+         <returnvalue>23:00:00</returnvalue>
+        </para></entry>
+       </row>
+
        <row>
         <entry role="func_table_entry"><para role="func_signature">
          <type>date</type> <literal>-</literal> <type>date</type>
@@ -8644,6 +8658,24 @@ SELECT regexp_match('abc01234xyz', '(?:(.*?)(\d+)(.*)){1,1}');
       </thead>
 
       <tbody>
+       <row>
+        <entry role="func_table_entry"><para role="func_signature">
+         <indexterm>
+          <primary>abs</primary>
+         </indexterm>
+         <function>abs</function> ( <type>interval</type> )
+         <returnvalue>interval</returnvalue>
+        </para>
+        <para>
+         Compute the absolute value of an interval; that is, the greater of the input
+         and its negative.
+        </para>
+        <para>
+         <literal>abs(interval '-23 hours')</literal>
+         <returnvalue>23:00:00</returnvalue>
+        </para></entry>
+       </row>
+
        <row>
         <entry role="func_table_entry"><para role="func_signature">
          <indexterm>
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index f619b56d6f..1b336195b9 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -3041,10 +3041,21 @@ timestamptz_mi_interval(PG_FUNCTION_ARGS)
 }
 
 
+Interval *
+interval_um_internal(Interval *interval);
+
 Datum
 interval_um(PG_FUNCTION_ARGS)
 {
 	Interval   *interval = PG_GETARG_INTERVAL_P(0);
+	Interval   *result = interval_um_internal(interval);
+
+	PG_RETURN_INTERVAL_P(result);
+}
+
+Interval *
+interval_um_internal(Interval *interval)
+{
 	Interval   *result;
 
 	result = (Interval *) palloc(sizeof(Interval));
@@ -3066,7 +3077,7 @@ interval_um(PG_FUNCTION_ARGS)
 				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
 				 errmsg("interval out of range")));
 
-	PG_RETURN_INTERVAL_P(result);
+	return result;
 }
 
 
@@ -3168,10 +3179,26 @@ interval_mi(PG_FUNCTION_ARGS)
 }
 
 /*
- *	There is no interval_abs():  it is unclear what value to return:
+ *	Although some strangeness is possible with intervals:
  *	  http://archives.postgresql.org/pgsql-general/2009-10/msg01031.php
  *	  http://archives.postgresql.org/pgsql-general/2009-11/msg00041.php
+ *	... there is nevertheless nothing clearly better than the usual
+ *	definition of abs as the greater of the input and its negative.
  */
+Datum
+interval_abs(PG_FUNCTION_ARGS)
+{
+	Interval   *interval = PG_GETARG_INTERVAL_P(0);
+	Interval   *minterval = interval_um_internal(interval);
+	Interval   *result;
+
+	if (interval_cmp_internal(interval, minterval) >= 0)
+		result = interval;
+	else
+		result = minterval;
+
+	PG_RETURN_INTERVAL_P(result);
+}
 
 Datum
 interval_mul(PG_FUNCTION_ARGS)
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 85395a81ee..4eb28bad6e 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -1588,6 +1588,9 @@
 { oid => '1336', descr => 'negate',
   oprname => '-', oprkind => 'l', oprleft => '0', oprright => 'interval',
   oprresult => 'interval', oprcode => 'interval_um' },
+{ oid => '9000', descr => 'absolute value',
+  oprname => '@', oprkind => 'l', oprleft => '0', oprright => 'interval',
+  oprresult => 'interval', oprcode => 'interval_abs' },
 { oid => '1337', descr => 'add',
   oprname => '+', oprleft => 'interval', oprright => 'interval',
   oprresult => 'interval', oprcom => '+(interval,interval)',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index bfb89e0575..92718e32a5 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -2330,6 +2330,12 @@
 { oid => '1168',
   proname => 'interval_um', prorettype => 'interval', proargtypes => 'interval',
   prosrc => 'interval_um' },
+{ oid => '9001',
+  proname => 'interval_abs', prorettype => 'interval', proargtypes => 'interval',
+  prosrc => 'interval_abs' },
+{ oid => '9002', descr => 'absolute value',
+  proname => 'abs', prorettype => 'interval', proargtypes => 'interval',
+  prosrc => 'interval_abs' },
 { oid => '1169',
   proname => 'interval_pl', prorettype => 'interval',
   proargtypes => 'interval interval', prosrc => 'interval_pl' },
diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out
index acc353a0dc..1aaeb2d3f8 100644
--- a/src/test/regress/expected/interval.out
+++ b/src/test/regress/expected/interval.out
@@ -207,6 +207,31 @@ SELECT r1.*, r2.*
  34 years        | 6 years
 (45 rows)
 
+SELECT abs (f1), abs (-f1), @ f1, @ -f1 FROM interval_tbl;
+       abs       |       abs       |    ?column?     |    ?column?     
+-----------------+-----------------+-----------------+-----------------
+ 00:01:00        | 00:01:00        | 00:01:00        | 00:01:00
+ 05:00:00        | 05:00:00        | 05:00:00        | 05:00:00
+ 10 days         | 10 days         | 10 days         | 10 days
+ 34 years        | 34 years        | 34 years        | 34 years
+ 3 mons          | 3 mons          | 3 mons          | 3 mons
+ 00:00:14        | 00:00:14        | 00:00:14        | 00:00:14
+ 1 day 02:03:04  | 1 day 02:03:04  | 1 day 02:03:04  | 1 day 02:03:04
+ 6 years         | 6 years         | 6 years         | 6 years
+ 5 mons          | 5 mons          | 5 mons          | 5 mons
+ 5 mons 12:00:00 | 5 mons 12:00:00 | 5 mons 12:00:00 | 5 mons 12:00:00
+(10 rows)
+
+WITH t AS (
+   SELECT '1 month -30 days'::interval AS i
+)
+SELECT i = -i, i = '0'::interval, abs (i), abs (-i)
+   FROM t;
+ ?column? | ?column? |      abs       |       abs        
+----------+----------+----------------+------------------
+ t        | t        | 1 mon -30 days | -1 mons +30 days
+(1 row)
+
 -- Test intervals that are large enough to overflow 64 bits in comparisons
 CREATE TEMP TABLE INTERVAL_TBL_OF (f1 interval);
 INSERT INTO INTERVAL_TBL_OF (f1) VALUES
diff --git a/src/test/regress/sql/interval.sql b/src/test/regress/sql/interval.sql
index 11c1929bef..ebcd5250e3 100644
--- a/src/test/regress/sql/interval.sql
+++ b/src/test/regress/sql/interval.sql
@@ -59,6 +59,13 @@ SELECT r1.*, r2.*
    WHERE r1.f1 > r2.f1
    ORDER BY r1.f1, r2.f1;
 
+SELECT abs (f1), abs (-f1), @ f1, @ -f1 FROM interval_tbl;
+WITH t AS (
+   SELECT '1 month -30 days'::interval AS i
+)
+SELECT i = -i, i = '0'::interval, abs (i), abs (-i)
+   FROM t;
+
 -- Test intervals that are large enough to overflow 64 bits in comparisons
 CREATE TEMP TABLE INTERVAL_TBL_OF (f1 interval);
 INSERT INTO INTERVAL_TBL_OF (f1) VALUES
-- 
2.25.1

