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'); - <function>date_trunc</function> + <function>date_trunc</function>, <function>date_trunc_interval</function> 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