doc/src/sgml/func.sgml | 65 ++++-
src/backend/utils/adt/timestamp.c | 440 ++++++++++++++++++++++++++++++
src/include/catalog/pg_proc.dat | 30 ++
src/include/datatype/timestamp.h | 8 +
src/test/regress/expected/timestamp.out | 209 ++++++++++++++
src/test/regress/expected/timestamptz.out | 149 ++++++++++
src/test/regress/sql/timestamp.sql | 118 ++++++++
src/test/regress/sql/timestamptz.sql | 90 ++++++
8 files changed, 1108 insertions(+), 1 deletion(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 7a0bb0c70a..68afdb9ee4 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -6976,6 +6976,42 @@ SELECT regexp_match('abc01234xyz', '(?:(.*?)(\d+)(.*)){1,1}');
2 days 03:00:00
+
+ date_trunc_interval(interval, timestamp)
+ timestamp
+ Truncate to specified interval; see
+
+ date_trunc_interval('15 minutes', timestamp '2001-02-16 20:38:40')
+ 2001-02-16 20:30:00
+
+
+
+ date_trunc_interval(interval, timestamp, timestamp)
+ timestamp
+ Truncate to specified interval aligned with specified origin; see
+
+ date_trunc_interval('15 minutes', timestamp '2001-02-16 20:38:40', timestamp '2001-02-16 20:05:00')
+ 2001-02-16 20:35:00
+
+
+
+ date_trunc_interval(interval, timestamp with time zone, text)
+ timestamp with time zone
+ Truncate to specified interval in the specified time zone; see
+
+ date_trunc_interval('6 hours', timestamptz '2001-02-16 20:38:40+00', 'Australia/Sydney')
+ 2001-02-16 19:00:00+00
+
+
+
+ date_trunc_interval(interval, timestamp with time zone, timestamp with time zone, text)
+ timestamp with time zone
+ Truncate to specified interval aligned with specified origin in the specified time zone; see
+
+ date_trunc_interval('6 hours', timestamptz '2001-02-16 20:38:40+00', timestamptz '2001-02-16 01:00:00+00', 'Australia/Sydney')
+ 2001-02-16 20:00:00+00
+
+
@@ -7845,7 +7881,7 @@ SELECT date_part('hour', INTERVAL '4 hours 3 minutes');
- date_trunc
+ date_trunc, date_trunc_interval
date_trunc
@@ -7929,6 +7965,33 @@ SELECT date_trunc('hour', INTERVAL '3 days 02:47:33');
Result: 3 days 02:00:00
+
+
+ The function date_trunc_interval is
+ similar to the date_trunc, except that it
+ truncates to an arbitrary interval.
+
+
+
+ Example:
+
+SELECT date_trunc_interval('5 minutes', TIMESTAMP '2001-02-16 20:38:40');
+Result: 2001-02-16 20:35:00
+
+
+
+
+ The boundaries of the interval to truncate on can be controlled by setting the optional origin parameter. If not specfied, the default origin is January 1st, 2001.
+
+
+
+ Example:
+
+SELECT date_trunc_interval('5 years'::interval, TIMESTAMP '2020-02-01', TIMESTAMP '2012-01-01');
+Result: 2017-01-01 00:00:00
+
+
+
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 4caffb5804..a6af9217d5 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -45,6 +45,13 @@
#define SAMESIGN(a,b) (((a) < 0) == ((b) < 0))
+/*
+ * The default origin is 2001-01-01, which matches date_trunc as far as
+ * aligning on ISO weeks. This numeric value should match the internal
+ * value of SELECT make_timestamp(2001, 1, 1, 0, 0, 0);
+ */
+#define DATE_TRUNC_DEFAULT_ORIGIN 31622400000000
+
/* Set at postmaster start */
TimestampTz PgStartTime;
@@ -3804,6 +3811,146 @@ timestamptz_age(PG_FUNCTION_ARGS)
*---------------------------------------------------------*/
+static Timestamp
+timestamp_trunc_interval_internal(Interval *stride,
+ Timestamp timestamp,
+ Timestamp origin)
+{
+ Timestamp result,
+ tm_diff,
+ tm_usecs,
+ tm_delta;
+ fsec_t ofsec,
+ tfsec;
+ int origin_months,
+ tm_months,
+ result_months,
+ month_diff,
+ month_delta = 0;
+
+ struct pg_tm ot, /* origin */
+ tt, /* input */
+ rt; /* result */
+
+ tm_diff = timestamp - origin;
+
+ /*
+ * For strides measured in days or smaller units, do one simple calculation
+ * on the time in microseconds.
+ */
+ if (stride->month == 0)
+ {
+ tm_usecs = stride->day * USECS_PER_DAY + stride->time;
+
+ /* trivial case of 1 usec */
+ if (tm_usecs == 1)
+ return timestamp;
+
+ tm_delta = tm_diff - tm_diff % tm_usecs;;
+ if (tm_diff < 0)
+ tm_delta -= tm_usecs;
+
+ result = origin + tm_delta;
+ }
+ else
+ {
+ /*
+ * For strides measured in years and/or months, convert origin and
+ * input timestamps to months.
+ */
+ if (stride->day != 0 || stride->time != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot mix year or month interval units with day units or smaller")));
+
+ if (timestamp2tm(timestamp, NULL, &tt, &tfsec, NULL, NULL) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+
+ if (timestamp2tm(origin, NULL, &ot, &ofsec, NULL, NULL) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+
+
+ origin_months = ot.tm_year * 12 + ot.tm_mon - 1;
+ tm_months = tt.tm_year * 12 + tt.tm_mon - 1;
+ month_diff = tm_months - origin_months;
+
+ /* take the origin's smaller units into account */
+ if (Minor_Units(tt, tfsec) < Minor_Units(ot, ofsec))
+ month_diff--;
+
+ month_delta = month_diff - month_diff % stride->month;
+
+ /*
+ * Make sure truncation happens in the right direction.
+ * XXX This is a bit of a hack.
+ */
+ if (month_diff < 0 && stride->month != 1)
+ month_delta -= stride->month;
+
+ result_months = origin_months + month_delta;
+
+ /* compute result fields of pg_tm struct */
+ rt.tm_year = result_months / 12;
+ rt.tm_mon = (result_months % 12) + 1;
+ /* align on origin's smaller units */
+ rt.tm_mday = ot.tm_mday;
+ rt.tm_hour = ot.tm_hour;
+ rt.tm_min = ot.tm_min;
+ rt.tm_sec = ot.tm_sec;
+
+ /* Note using the origin's fsec directly */
+ if (tm2timestamp(&rt, ofsec, NULL, &result) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+ }
+ return result;
+}
+
+/* timestamp_trunc_interval()
+ * Truncate timestamp to specified interval.
+ */
+Datum
+timestamp_trunc_interval(PG_FUNCTION_ARGS)
+{
+ Interval *stride = PG_GETARG_INTERVAL_P(0);
+ Timestamp timestamp = PG_GETARG_TIMESTAMP(1);
+ Timestamp result;
+
+ if (TIMESTAMP_NOT_FINITE(timestamp))
+ PG_RETURN_TIMESTAMP(timestamp);
+
+ result = timestamp_trunc_interval_internal(stride, timestamp,
+ DATE_TRUNC_DEFAULT_ORIGIN);
+
+ PG_RETURN_TIMESTAMP(result);
+}
+
+Datum
+timestamp_trunc_interval_origin(PG_FUNCTION_ARGS)
+{
+ Interval *stride = PG_GETARG_INTERVAL_P(0);
+ Timestamp timestamp = PG_GETARG_TIMESTAMP(1);
+ Timestamp origin = PG_GETARG_TIMESTAMP(2);
+ Timestamp result;
+
+ if (TIMESTAMP_NOT_FINITE(timestamp))
+ PG_RETURN_TIMESTAMP(timestamp);
+
+ if (TIMESTAMP_NOT_FINITE(origin))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+
+ result = timestamp_trunc_interval_internal(stride, timestamp, origin);
+
+ PG_RETURN_TIMESTAMP(result);
+}
+
/* timestamp_trunc()
* Truncate timestamp to specified units.
*/
@@ -3938,6 +4085,132 @@ timestamp_trunc(PG_FUNCTION_ARGS)
PG_RETURN_TIMESTAMP(result);
}
+/*
+ * Common code for timestamptz_trunc_interval*()
+ *
+ * tzp identifies the zone to truncate with respect to. We assume
+ * infinite timestamps have already been rejected.
+ */
+static TimestampTz
+timestamptz_trunc_interval_internal(Interval *stride,
+ TimestampTz timestamp,
+ pg_tz *tzp,
+ TimestampTz origin)
+{
+ TimestampTz result,
+ tm_diff,
+ tm_usecs,
+ tm_delta;
+ fsec_t ofsec,
+ tfsec;
+ int origin_months,
+ tm_months,
+ result_months,
+ month_diff,
+ month_delta = 0,
+ tz;
+
+ struct pg_tm ot, /* origin */
+ tt, /* input */
+ rt; /* result */
+
+ /* Convert input to pg_tm with timezone. */
+ if (timestamp2tm(timestamp, &tz, &tt, &tfsec, NULL, tzp) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+
+ // XXX it seems like we should pass TZ here, but it seems to break
+ // things
+ if (timestamp2tm(origin, NULL, &ot, &ofsec, NULL, tzp) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+
+ /*
+ * For strides measured in days or smaller units, do one simple calculation
+ * on the time in microseconds.
+ */
+ if (stride->month == 0)
+ {
+ tm_usecs = stride->day * USECS_PER_DAY + stride->time;
+
+ /* trivial case of 1 usec */
+ if (tm_usecs == 1)
+ return timestamp;
+
+ // got the idea from timestamp_zone() to just recovert to timestamp
+ // with null tz.
+ if (tm2timestamp(&tt, tfsec, NULL, ×tamp) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+
+ // WIP
+ // if (tm2timestamp(&ot, ofsec, NULL, &origin) != 0)
+ // ereport(ERROR,
+ // (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ // errmsg("timestamp out of range")));
+
+ tm_diff = timestamp - origin;
+
+ tm_delta = tm_diff - tm_diff % tm_usecs;;
+ if (tm_diff < 0)
+ tm_delta -= tm_usecs;
+
+
+ // XXX this seems mysterious, hopefully there's a more principled way
+ result = dt2local(origin + tm_delta, -tz);
+ }
+ else
+ {
+ /*
+ * For strides measured in years and/or months, convert origin and
+ * input timestamps to months.
+ */
+ if (stride->day != 0 || stride->time != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot mix year or month interval units with day units or smaller")));
+
+ origin_months = ot.tm_year * 12 + ot.tm_mon - 1;
+ tm_months = tt.tm_year * 12 + tt.tm_mon - 1;
+ month_diff = tm_months - origin_months;
+
+ /* take the origin's smaller units into account */
+ if (Minor_Units(tt, tfsec) < Minor_Units(ot, ofsec))
+ month_diff--;
+
+ month_delta = month_diff - month_diff % stride->month;
+
+ /*
+ * Make sure truncation happens in the right direction.
+ * XXX This is a bit of a hack.
+ */
+ if (month_diff < 0 && stride->month != 1)
+ month_delta -= stride->month;
+
+ result_months = origin_months + month_delta;
+
+ /* compute result fields of pg_tm struct */
+ rt.tm_year = result_months / 12;
+ rt.tm_mon = (result_months % 12) + 1;
+ /* align on origin's smaller units */
+ rt.tm_mday = ot.tm_mday;
+ rt.tm_hour = ot.tm_hour;
+ rt.tm_min = ot.tm_min;
+ rt.tm_sec = ot.tm_sec;
+
+ /* Note using the origin's fsec directly */
+ if (tm2timestamp(&rt, ofsec, &tz, &result) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+ }
+
+ return result;
+}
+
/*
* Common code for timestamptz_trunc() and timestamptz_trunc_zone().
*
@@ -4085,6 +4358,52 @@ timestamptz_trunc_internal(text *units, TimestampTz timestamp, pg_tz *tzp)
return result;
}
+/* timestamptz_trunc_interval()
+ * Truncate timestamptz to specified interval in session timezone.
+ */
+Datum
+timestamptz_trunc_interval(PG_FUNCTION_ARGS)
+{
+ Interval *stride = PG_GETARG_INTERVAL_P(0);
+ TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(1);
+ TimestampTz result;
+
+ if (TIMESTAMP_NOT_FINITE(timestamp))
+ PG_RETURN_TIMESTAMPTZ(timestamp);
+
+ result = timestamptz_trunc_interval_internal(stride, timestamp,
+ session_timezone,
+ DATE_TRUNC_DEFAULT_ORIGIN);
+
+ PG_RETURN_TIMESTAMPTZ(result);
+}
+
+/* timestamptz_trunc_interval_origin()
+ * Truncate timestamptz to specified interval in session timezone.
+ */
+Datum
+timestamptz_trunc_interval_origin(PG_FUNCTION_ARGS)
+{
+ Interval *stride = PG_GETARG_INTERVAL_P(0);
+ TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(1);
+ TimestampTz result;
+ TimestampTz origin = PG_GETARG_TIMESTAMPTZ(2);
+
+ if (TIMESTAMP_NOT_FINITE(timestamp))
+ PG_RETURN_TIMESTAMP(timestamp);
+
+ if (TIMESTAMP_NOT_FINITE(origin))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+
+ result = timestamptz_trunc_interval_internal(stride, timestamp,
+ session_timezone,
+ origin);
+
+ PG_RETURN_TIMESTAMPTZ(result);
+}
+
/* timestamptz_trunc()
* Truncate timestamptz to specified units in session timezone.
*/
@@ -4103,6 +4422,127 @@ timestamptz_trunc(PG_FUNCTION_ARGS)
PG_RETURN_TIMESTAMPTZ(result);
}
+/* timestamptz_trunc_interval_zone()
+ * Truncate timestamptz to specified interval in specified timezone.
+ */
+Datum
+timestamptz_trunc_interval_zone(PG_FUNCTION_ARGS)
+{
+ Interval *stride = PG_GETARG_INTERVAL_P(0);
+ TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(1);
+ text *zone = PG_GETARG_TEXT_PP(2);
+ TimestampTz result;
+ char tzname[TZ_STRLEN_MAX + 1];
+ char *lowzone;
+ int type,
+ val;
+ pg_tz *tzp;
+
+ if (TIMESTAMP_NOT_FINITE(timestamp))
+ PG_RETURN_TIMESTAMPTZ(timestamp);
+
+ /*
+ * Look up the requested timezone (see notes in timestamptz_zone()).
+ */
+ text_to_cstring_buffer(zone, tzname, sizeof(tzname));
+
+ /* DecodeTimezoneAbbrev requires lowercase input */
+ lowzone = downcase_truncate_identifier(tzname,
+ strlen(tzname),
+ false);
+
+ type = DecodeTimezoneAbbrev(0, lowzone, &val, &tzp);
+
+ if (type == TZ || type == DTZ)
+ {
+ /* fixed-offset abbreviation, get a pg_tz descriptor for that */
+ tzp = pg_tzset_offset(-val);
+ }
+ else if (type == DYNTZ)
+ {
+ /* dynamic-offset abbreviation, use its referenced timezone */
+ }
+ else
+ {
+ /* try it as a full zone name */
+ tzp = pg_tzset(tzname);
+ if (!tzp)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("time zone \"%s\" not recognized", tzname)));
+ }
+
+ result = timestamptz_trunc_interval_internal(stride, timestamp,
+ tzp,
+ DATE_TRUNC_DEFAULT_ORIGIN);
+
+ PG_RETURN_TIMESTAMPTZ(result);
+}
+
+/* timestamptz_trunc_interval_origin_zone()
+ * Truncate timestamptz to specified interval in specified timezone,
+ * aligned to the specified origin.
+ */
+Datum
+timestamptz_trunc_interval_origin_zone(PG_FUNCTION_ARGS)
+{
+ Interval *stride = PG_GETARG_INTERVAL_P(0);
+ TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(1);
+ TimestampTz origin = PG_GETARG_TIMESTAMPTZ(2);
+ text *zone = PG_GETARG_TEXT_PP(3);
+ TimestampTz result;
+ char tzname[TZ_STRLEN_MAX + 1];
+ char *lowzone;
+ int type,
+ val;
+ pg_tz *tzp;
+
+ if (TIMESTAMP_NOT_FINITE(timestamp))
+ PG_RETURN_TIMESTAMP(timestamp);
+
+ if (TIMESTAMP_NOT_FINITE(origin))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+
+ /*
+ * Look up the requested timezone (see notes in timestamptz_zone()).
+ */
+ text_to_cstring_buffer(zone, tzname, sizeof(tzname));
+
+ /* DecodeTimezoneAbbrev requires lowercase input */
+ lowzone = downcase_truncate_identifier(tzname,
+ strlen(tzname),
+ false);
+
+ type = DecodeTimezoneAbbrev(0, lowzone, &val, &tzp);
+
+ if (type == TZ || type == DTZ)
+ {
+ /* fixed-offset abbreviation, get a pg_tz descriptor for that */
+ tzp = pg_tzset_offset(-val);
+ }
+ else if (type == DYNTZ)
+ {
+ /* dynamic-offset abbreviation, use its referenced timezone */
+ }
+ else
+ {
+ /* try it as a full zone name */
+ tzp = pg_tzset(tzname);
+ if (!tzp)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("time zone \"%s\" not recognized", tzname)));
+ }
+
+ result = timestamptz_trunc_interval_internal(stride, timestamp,
+ tzp,
+ origin);
+
+ PG_RETURN_TIMESTAMPTZ(result);
+}
+
/* timestamptz_trunc_zone()
* Truncate timestamptz to specified units in specified timezone.
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 87d25d4a4b..e8aeb4801c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5663,6 +5663,36 @@
{ oid => '2020', descr => 'truncate timestamp to specified units',
proname => 'date_trunc', prorettype => 'timestamp',
proargtypes => 'text timestamp', prosrc => 'timestamp_trunc' },
+
+{ oid => '8989', descr => 'truncate timestamp to specified interval',
+ proname => 'date_trunc_interval', prorettype => 'timestamp',
+ proargtypes => 'interval timestamp', prosrc => 'timestamp_trunc_interval' },
+{ oid => '8990',
+ descr => 'truncate timestamp to specified interval and origin',
+ proname => 'date_trunc_interval', prorettype => 'timestamp',
+ proargtypes => 'interval timestamp timestamp',
+ prosrc => 'timestamp_trunc_interval_origin' },
+
+{ oid => '8991',
+ descr => 'truncate timestamp with time zone to specified interval',
+ proname => 'date_trunc_interval', prorettype => 'timestamptz',
+ proargtypes => 'interval timestamptz', prosrc => 'timestamptz_trunc_interval' },
+{ oid => '8992',
+ descr => 'truncate timestamp with time zone to specified interval, in specified time zone',
+ proname => 'date_trunc_interval', prorettype => 'timestamptz',
+ proargtypes => 'interval timestamptz text',
+ prosrc => 'timestamptz_trunc_interval_zone' },
+
+{ oid => '8993',
+ descr => 'truncate timestamp with time zone to specified interval and origin',
+ proname => 'date_trunc_interval', prorettype => 'timestamptz',
+ proargtypes => 'interval timestamptz timestamptz', prosrc => 'timestamptz_trunc_interval_origin' },
+{ oid => '8994',
+ descr => 'truncate timestamp with time zone to specified interval and origin, in specified time zone',
+ proname => 'date_trunc_interval', prorettype => 'timestamptz',
+ proargtypes => 'interval timestamptz timestamptz text',
+ prosrc => 'timestamptz_trunc_interval_origin_zone' },
+
{ oid => '2021', descr => 'extract field from timestamp',
proname => 'date_part', prorettype => 'float8',
proargtypes => 'text timestamp', prosrc => 'timestamp_part' },
diff --git a/src/include/datatype/timestamp.h b/src/include/datatype/timestamp.h
index 6be6d35d1e..2f897b7575 100644
--- a/src/include/datatype/timestamp.h
+++ b/src/include/datatype/timestamp.h
@@ -93,6 +93,14 @@ typedef struct
#define USECS_PER_MINUTE INT64CONST(60000000)
#define USECS_PER_SEC INT64CONST(1000000)
+/* compute total of non-month, non-year units in a pg_tm struct + fsec */
+#define Minor_Units(tm, usec) \
+ (tm.tm_mday - 1) * USECS_PER_DAY + \
+ tm.tm_hour * USECS_PER_HOUR + \
+ tm.tm_min * USECS_PER_MINUTE + \
+ tm.tm_sec * USECS_PER_SEC + \
+ usec
+
/*
* We allow numeric timezone offsets up to 15:59:59 either way from Greenwich.
* Currently, the record holders for wackiest offsets in actual use are zones
diff --git a/src/test/regress/expected/timestamp.out b/src/test/regress/expected/timestamp.out
index 5f97505a30..2fd9deae62 100644
--- a/src/test/regress/expected/timestamp.out
+++ b/src/test/regress/expected/timestamp.out
@@ -545,6 +545,215 @@ SELECT '' AS date_trunc_week, date_trunc( 'week', timestamp '2004-02-29 15:44:17
| Mon Feb 23 00:00:00 2004
(1 row)
+-- verify date_trunc_interval behaves the same as date_trunc (excluding decade)
+-- case 1: AD dates, origin < input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts) = date_trunc_interval(interval::interval, ts) AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamp '2020-02-29 15:44:17.71393') ts (ts);
+ str | interval | equal
+-------------+----------+-------
+ millennium | 1000 y | t
+ century | 100 y | t
+ year | 1 y | t
+ quarter | 3 mon | t
+ month | 1 mon | t
+ week | 7 d | t
+ day | 1 d | t
+ hour | 1 h | t
+ minute | 1 m | t
+ second | 1 s | t
+ millisecond | 1 ms | t
+ microsecond | 1 us | t
+(12 rows)
+
+-- case 2: BC dates, origin < input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts) = date_trunc_interval(interval::interval, ts, timestamp '2000-01-01 BC') AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamp '0055-6-10 15:44:17.71393 BC') ts (ts);
+ str | interval | equal
+-------------+----------+-------
+ millennium | 1000 y | t
+ century | 100 y | t
+ year | 1 y | t
+ quarter | 3 mon | t
+ month | 1 mon | t
+ week | 7 d | t
+ day | 1 d | t
+ hour | 1 h | t
+ minute | 1 m | t
+ second | 1 s | t
+ millisecond | 1 ms | t
+ microsecond | 1 us | t
+(12 rows)
+
+-- case 3: AD dates, origin > input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts) = date_trunc_interval(interval::interval, ts) AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamp '1999-12-31 15:44:17.71393') ts (ts);
+ str | interval | equal
+-------------+----------+-------
+ millennium | 1000 y | t
+ century | 100 y | t
+ year | 1 y | t
+ quarter | 3 mon | t
+ month | 1 mon | t
+ week | 7 d | t
+ day | 1 d | t
+ hour | 1 h | t
+ minute | 1 m | t
+ second | 1 s | t
+ millisecond | 1 ms | t
+ microsecond | 1 us | t
+(12 rows)
+
+-- case 4: BC dates, origin > input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts) = date_trunc_interval(interval::interval, ts) AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamp '0055-6-07 15:44:17.71393 BC') ts (ts);
+ str | interval | equal
+-------------+----------+-------
+ millennium | 1000 y | t
+ century | 100 y | t
+ year | 1 y | t
+ quarter | 3 mon | t
+ month | 1 mon | t
+ week | 7 d | t
+ day | 1 d | t
+ hour | 1 h | t
+ minute | 1 m | t
+ second | 1 s | t
+ millisecond | 1 ms | t
+ microsecond | 1 us | t
+(12 rows)
+
+-- truncate timestamps on arbitrary intervals
+SELECT
+ interval,
+ date_trunc_interval(interval::interval, ts)
+FROM (
+ VALUES
+ ('50 years'),
+ ('1.5 years'),
+ ('18 months'),
+ ('6 months'),
+ ('15 days'),
+ ('2 hours'),
+ ('15 minutes'),
+ ('10 seconds'),
+ ('100 milliseconds'),
+ ('250 microseconds')
+) intervals (interval),
+(SELECT TIMESTAMP '2020-02-11 15:44:17.71393') ts (ts);
+ interval | date_trunc_interval
+------------------+--------------------------------
+ 50 years | Mon Jan 01 00:00:00 2001
+ 1.5 years | Tue Jan 01 00:00:00 2019
+ 18 months | Tue Jan 01 00:00:00 2019
+ 6 months | Wed Jan 01 00:00:00 2020
+ 15 days | Thu Feb 06 00:00:00 2020
+ 2 hours | Tue Feb 11 14:00:00 2020
+ 15 minutes | Tue Feb 11 15:30:00 2020
+ 10 seconds | Tue Feb 11 15:44:10 2020
+ 100 milliseconds | Tue Feb 11 15:44:17.7 2020
+ 250 microseconds | Tue Feb 11 15:44:17.71375 2020
+(10 rows)
+
+-- shift bins using the origin parameter:
+SELECT date_trunc_interval('5 min'::interval, timestamp '2020-02-1 01:01:01', timestamp '2020-02-01 00:02:30');
+ date_trunc_interval
+--------------------------
+ Sat Feb 01 00:57:30 2020
+(1 row)
+
+SELECT date_trunc_interval('5 years'::interval, timestamp '2020-02-1 01:01:01', timestamp '2012-01-01');
+ date_trunc_interval
+--------------------------
+ Sun Jan 01 00:00:00 2017
+(1 row)
+
+SELECT date_trunc_interval('3 year', timestamp '2015-01-14 20:38:40', timestamp '2012-01-15 01:01:01.123');
+ date_trunc_interval
+------------------------------
+ Sun Jan 15 01:01:01.123 2012
+(1 row)
+
+SELECT date_trunc_interval('3 year', timestamp '2015-01-15 20:38:40', timestamp '2012-01-15 01:01:01.123');
+ date_trunc_interval
+------------------------------
+ Thu Jan 15 01:01:01.123 2015
+(1 row)
+
+-- not defined
+SELECT date_trunc_interval('1 month 1 day', timestamp '2001-02-16 20:38:40.123456');
+ERROR: cannot mix year or month interval units with day units or smaller
-- Test casting within a BETWEEN qualifier
SELECT '' AS "54", d1 - timestamp without time zone '1997-01-02' AS diff
FROM TIMESTAMP_TBL
diff --git a/src/test/regress/expected/timestamptz.out b/src/test/regress/expected/timestamptz.out
index 639b50308e..c2f3a55e62 100644
--- a/src/test/regress/expected/timestamptz.out
+++ b/src/test/regress/expected/timestamptz.out
@@ -663,6 +663,155 @@ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-
| Thu Feb 15 20:00:00 2001 PST
(1 row)
+-- verify date_trunc_interval behaves the same as date_trunc (excluding decade)
+-- case 1: AD dates, origin < input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts, 'Australia/Sydney') = date_trunc_interval(interval::interval, ts, 'Australia/Sydney') AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamptz '2020-02-29 15:44:17.71393+00') ts (ts);
+ str | interval | equal
+-------------+----------+-------
+ millennium | 1000 y | t
+ century | 100 y | t
+ year | 1 y | t
+ quarter | 3 mon | t
+ month | 1 mon | t
+ week | 7 d | t
+ day | 1 d | t
+ hour | 1 h | t
+ minute | 1 m | t
+ second | 1 s | t
+ millisecond | 1 ms | t
+ microsecond | 1 us | t
+(12 rows)
+
+-- case 2: BC dates, origin < input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts, 'Australia/Sydney') = date_trunc_interval(interval::interval, ts, timestamptz '2000-01-01+00 BC', 'Australia/Sydney') AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamptz '0055-6-10 15:44:17.71393+00 BC') ts (ts);
+ str | interval | equal
+-------------+----------+-------
+ millennium | 1000 y | t
+ century | 100 y | t
+ year | 1 y | t
+ quarter | 3 mon | t
+ month | 1 mon | t
+ week | 7 d | t
+ day | 1 d | t
+ hour | 1 h | t
+ minute | 1 m | t
+ second | 1 s | t
+ millisecond | 1 ms | t
+ microsecond | 1 us | t
+(12 rows)
+
+-- case 3: AD dates, origin > input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts, 'Australia/Sydney') = date_trunc_interval(interval::interval, ts, 'Australia/Sydney') AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamptz '1999-12-31 15:44:17.71393+00') ts (ts);
+ str | interval | equal
+-------------+----------+-------
+ millennium | 1000 y | t
+ century | 100 y | t
+ year | 1 y | t
+ quarter | 3 mon | t
+ month | 1 mon | t
+ week | 7 d | t
+ day | 1 d | t
+ hour | 1 h | t
+ minute | 1 m | t
+ second | 1 s | t
+ millisecond | 1 ms | t
+ microsecond | 1 us | t
+(12 rows)
+
+-- case 4: BC dates, origin > input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts, 'Australia/Sydney') = date_trunc_interval(interval::interval, ts, 'Australia/Sydney') AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamptz '0055-6-07 15:44:17.71393+00 BC') ts (ts);
+ str | interval | equal
+-------------+----------+-------
+ millennium | 1000 y | t
+ century | 100 y | t
+ year | 1 y | t
+ quarter | 3 mon | t
+ month | 1 mon | t
+ week | 7 d | t
+ day | 1 d | t
+ hour | 1 h | t
+ minute | 1 m | t
+ second | 1 s | t
+ millisecond | 1 ms | t
+ microsecond | 1 us | t
+(12 rows)
+
-- Test casting within a BETWEEN qualifier
SELECT '' AS "54", d1 - timestamp with time zone '1997-01-02' AS diff
FROM TIMESTAMPTZ_TBL
diff --git a/src/test/regress/sql/timestamp.sql b/src/test/regress/sql/timestamp.sql
index 7b58c3cfa5..d171bf95ec 100644
--- a/src/test/regress/sql/timestamp.sql
+++ b/src/test/regress/sql/timestamp.sql
@@ -166,6 +166,124 @@ SELECT '' AS "54", d1 - timestamp without time zone '1997-01-02' AS diff
SELECT '' AS date_trunc_week, date_trunc( 'week', timestamp '2004-02-29 15:44:17.71393' ) AS week_trunc;
+-- verify date_trunc_interval behaves the same as date_trunc (excluding decade)
+
+-- case 1: AD dates, origin < input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts) = date_trunc_interval(interval::interval, ts) AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamp '2020-02-29 15:44:17.71393') ts (ts);
+
+-- case 2: BC dates, origin < input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts) = date_trunc_interval(interval::interval, ts, timestamp '2000-01-01 BC') AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamp '0055-6-10 15:44:17.71393 BC') ts (ts);
+
+-- case 3: AD dates, origin > input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts) = date_trunc_interval(interval::interval, ts) AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamp '1999-12-31 15:44:17.71393') ts (ts);
+
+-- case 4: BC dates, origin > input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts) = date_trunc_interval(interval::interval, ts) AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamp '0055-6-07 15:44:17.71393 BC') ts (ts);
+
+-- truncate timestamps on arbitrary intervals
+SELECT
+ interval,
+ date_trunc_interval(interval::interval, ts)
+FROM (
+ VALUES
+ ('50 years'),
+ ('1.5 years'),
+ ('18 months'),
+ ('6 months'),
+ ('15 days'),
+ ('2 hours'),
+ ('15 minutes'),
+ ('10 seconds'),
+ ('100 milliseconds'),
+ ('250 microseconds')
+) intervals (interval),
+(SELECT TIMESTAMP '2020-02-11 15:44:17.71393') ts (ts);
+
+-- shift bins using the origin parameter:
+SELECT date_trunc_interval('5 min'::interval, timestamp '2020-02-1 01:01:01', timestamp '2020-02-01 00:02:30');
+SELECT date_trunc_interval('5 years'::interval, timestamp '2020-02-1 01:01:01', timestamp '2012-01-01');
+SELECT date_trunc_interval('3 year', timestamp '2015-01-14 20:38:40', timestamp '2012-01-15 01:01:01.123');
+SELECT date_trunc_interval('3 year', timestamp '2015-01-15 20:38:40', timestamp '2012-01-15 01:01:01.123');
+
+-- not defined
+SELECT date_trunc_interval('1 month 1 day', timestamp '2001-02-16 20:38:40.123456');
+
-- Test casting within a BETWEEN qualifier
SELECT '' AS "54", d1 - timestamp without time zone '1997-01-02' AS diff
FROM TIMESTAMP_TBL
diff --git a/src/test/regress/sql/timestamptz.sql b/src/test/regress/sql/timestamptz.sql
index 300302dafd..f25451fd0f 100644
--- a/src/test/regress/sql/timestamptz.sql
+++ b/src/test/regress/sql/timestamptz.sql
@@ -193,6 +193,96 @@ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-
SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'GMT') as gmt_trunc; -- fixed-offset abbreviation
SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'VET') as vet_trunc; -- variable-offset abbreviation
+-- verify date_trunc_interval behaves the same as date_trunc (excluding decade)
+
+-- case 1: AD dates, origin < input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts, 'Australia/Sydney') = date_trunc_interval(interval::interval, ts, 'Australia/Sydney') AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamptz '2020-02-29 15:44:17.71393+00') ts (ts);
+
+-- case 2: BC dates, origin < input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts, 'Australia/Sydney') = date_trunc_interval(interval::interval, ts, timestamptz '2000-01-01+00 BC', 'Australia/Sydney') AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamptz '0055-6-10 15:44:17.71393+00 BC') ts (ts);
+
+-- case 3: AD dates, origin > input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts, 'Australia/Sydney') = date_trunc_interval(interval::interval, ts, 'Australia/Sydney') AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamptz '1999-12-31 15:44:17.71393+00') ts (ts);
+
+-- case 4: BC dates, origin > input
+SELECT
+ str,
+ interval,
+ date_trunc(str, ts, 'Australia/Sydney') = date_trunc_interval(interval::interval, ts, 'Australia/Sydney') AS equal
+FROM (
+ VALUES
+ ('millennium', '1000 y'),
+ ('century', '100 y'),
+ ('year', '1 y'),
+ ('quarter', '3 mon'),
+ ('month', '1 mon'),
+ ('week', '7 d'),
+ ('day', '1 d'),
+ ('hour', '1 h'),
+ ('minute', '1 m'),
+ ('second', '1 s'),
+ ('millisecond', '1 ms'),
+ ('microsecond', '1 us')
+) intervals (str, interval),
+(SELECT timestamptz '0055-6-07 15:44:17.71393+00 BC') ts (ts);
+
-- Test casting within a BETWEEN qualifier
SELECT '' AS "54", d1 - timestamp with time zone '1997-01-02' AS diff
FROM TIMESTAMPTZ_TBL