Project: YugabyteDB (App Definition and Development / Database)
File: moment-interval-overloads-of-plus-and-minus.md

title: The moment-interval overloads of the "+" and "-" operators [YSQL] headerTitle: The moment-interval overloads of the "+" and "-" operators for timestamptz, timestamp, and time linkTitle: Moment-interval overloads of "+" and "-" description: Explains the semantics of the moment-interval overloads of the "+" and "-" operators for the timestamptz, timestamp, and time data types. [YSQL] menu: v2.18: identifier: moment-interval-overloads-of-plus-and-minus parent: interval-arithmetic weight: 50 type: docs {{< tip title="Download and install the date-time utilities code." >}} The code on this page depends on the code presented in the section . This is included in the larger that includes all of the reusable code that the overall section describes and uses. {{< /tip >}} This section presents PL/pgSQL implementations that model the semantics of the interval-moment-overload of the `+` operator for three distinct kinds of interval value: pure seconds, pure days, and pure months. The semantics of the interval-moment-overloads of the `-` operator is implied by the semantics of the overload of the `+` operator. The code examples that this page shows elaborate on those that are shown on the main parent page . First, do this: ```plpgsql select ( '30 days '::interval = '720 hours'::interval and ' 1 month'::interval = '30 days'::interval and ' 1 month'::interval = '720 hours'::interval )::text; ``` The result is true. Now set up the test like this: ```plpgsql drop table if exists t; create table t( t0 timestamptz primary key, "t0 + 720 hours" timestamptz, "t0 + 30 days" timestamptz, "t0 + 1 month" timestamptz); -- Insensitive to the session TimeZone setting. insert into t(t0) values ('2021-02-19 12:00:00 America/Los_Angeles'); deallocate all; prepare update_table as update t set "t0 + 720 hours" = t0 + '720 hours'::interval, "t0 + 30 days" = t0 + '30 days'::interval, "t0 + 1 month" = t0 + '1 month'::interval; prepare inspect_result as select t0, "t0 + 720 hours", "t0 + 30 days", "t0 + 1 month" from t; ``` Now execute the test with the session timezone set to one that respects Daylight savings Time. In the 'America/LosAngeles' zone, this starts at 02:00 in the small hours of Sunday morning on 14-March-2021and so, with the chosen starting moment, each of the differently spelled interval_ values spans the "spring forward" moment. Notice, too, that the starting moment is chosen so that the duration contains the end of February and that February never has thirty days, leap year or not. ```plpgsql set timezone = 'America/Los_Angeles'; execute update_table; execute inspect_result; ``` This is the result: ```output t0 | t0 + 720 hours | t0 + 30 days | t0 + 1 month +++ 2021-02-19 12:00:00-08 | 2021-03-21 13:00:00-07 | 2021-03-21 12:00:00-07 | 2021-03-19 12:00:00-07 ``` All the three results are displayed with a negative seven hours UTC offset reflecting the fact that they occur during the Daylight Savings Time period in the United States. The time of day for "t0 + 720 hours" differs by one hour from that for "t0 + 30 days", but the date component is the same for each. The time of day for "t0 + 30 days" is the same as that for "t0 + 1 month", but the date component differs by two days. You have to think quite hard to understand displayed timestamptz values when you inspect them using a session whose TimeZone setting is not UTC because that setting affects the values that you

This is very much intended and reflects the raison dtre of the data type. See the section . In particular, this is why the moment before "spring forward" is displayed with an offset of negative eight hours while the moments after "spring forward" are displayed with an offset of negative seven hours. The semantics that brings the outcome that you see here was designed for a common use case: Look for "Are you postponing an appointment by one day... or are you making a journey (like a flight) that you know takes twenty-four hours?" and, later, "Are you setting up reminders to tell you to water your hardy succulents every month...or are you taking a package tour that is advertised to last thirty days?" in the section on the main parent page . Here, you conceive of the problem in the timezone where you are (and maybe, too, in the timezone where you'll end up) and you want to know what a clock on the wall where you happen to be will show when you look at it. So the application that implements the problem's solution will ensure that it sets the session's timezone appropriately. Bear in mind that many different displayed timestamptz values can all correspond to the same underlying representation because, for example, '2021-02-19 12:00:00-08' (noon on the US West Coast in winter) is the same absolute moment as '2021-02-19 15:00:00-05' (three o'clock in the afternoon on the same date on the US East Coast). To save yourself the effort of mental arithmetic, it's best to observe the results of tests like the present one using a session whose timezone is set to UTC'which has an offset of zero, meaning that the bare time-of-day conveys all the information. Inspect the outcome using UTC: ```plpgsql set timezone = 'UTC'; execute inspect_result; ``` This is the result: ```output t0 | t0 + 720 hours | t0 + 30 days | t0 + 1 month +++ 2021-02-19 20:00:00+00 | 2021-03-21 20:00:00+00 | 2021-03-21 19:00:00+00 | 2021-03-19 19:00:00+00 ``` The three interval arithmetic results all differ from each other. in other words, the semantic effect of adding the "same" interval value to the same timestamptz value is different for each of the three nominally interchangeable spellings, '720 hours', '30 days', or '1 month', of the interval value. Now re-execute the prepared statement after setting the session's timezone to one that does not respect Daylight savings Time. ```plpgsql set timezone = 'Asia/Shanghai'; execute update_table; ``` Once again, inspect the outcome using UTC: ```plpgsql set timezone = 'UTC'; execute inspect_result; ``` This is the new result: ```output t0 | t0 + 720 hours | t0 + 30 days | t0 + 1 month +++ 2021-02-19 20:00:00+00 | 2021-03-21 20:00:00+00 | 2021-03-21 20:00:00+00 | 2021-03-19 20:00:00+00 ``` This time, the columns "t0 + 720 hours" and "t0 + 30 days" have the same value as each other while these still differ from the "t0 + 1 month" value. The reason that the three different interval value spellings can have different effects is given by comparing them using the . ```plpgsql select ( not '30 days '::interval == '720 hours'::interval and not ' 1 month'::interval == ' 30 days '::interval and not ' 1 month'::interval == '720 hours'::interval )::text; ``` The result is truemeaning that no two from the three differently spelled interval values are equal in the strict

Try this to see yet more vividly what lies behind this outcome: ```postgresql select intervalmmdd_ss('720 hours'::interval) as "'720 hours'", intervalmmdd_ss(' 30 days '::interval) as "'30 days'", intervalmmdd_ss(' 1 month'::interval) as "'1 month'"; ``` This is the result: ```output '720 hours' | '30 days' | '1 month' +--+-- (0,0,2592000) | (0,30,0) | (1,0,0) ``` Each of the different spellings of the "same" interval value ("same, that is, with respect to the criterion for equality that the native interval-interval overload of `=` implements) is either a pure interval value with respectively: either just a non-zero ss field (a.k.a. a "pure seconds" interval value) or just a non-zero dd field (a.k.a. a "pure days" interval value) or just a non-zero mm field (a.k.a. a "pure months" interval value). This explains why the arithmetic semantics is different for the three kinds of pure interval value. The outcomes with a "pure seconds" interval value and a "pure days" interval value will differ from each other when they are used in a session whose TimeZone setting respects Daylight Savings Time and the durations of the interval values span the "spring forward" moment or the "fall back" moment (as long as the moments have the data type timestamptz). The outcomes with a "pure days" interval value and a "pure months" interval value will differ from each other when the durations of the interval values span the boundaries of months whose number of days differs from thirty. This will be the case irrespective of whether the session's timezone respects Daylight Savings Time and of whether the moments have the data type timestamptz or plain timestamp. The term hybrid interval value will be used to denote an interval value where more than one of the fields of the internal representation is non-zero. Try this. It uses hybrid "days-seconds" _interval values. ```plpgsql drop function if exists hybridddss_results() cascade; create function hybridddss_results() returns table(x text) language plpgsql as $body$ begin set timezone = 'America/Los_Angeles'; declare -- Five hours before DST Start. t0 constant timestamptz := '2021-03-13 21:00:00 America/Los_Angeles'; "1 day" constant interval := '1 day'; "9 hours" constant interval := '9 hours'; "1 day 9 hours" constant interval := '1 day 9 hours'; begin x := $$ t0 + '1 day 9 hours' : $$|| (t0 + "1 day 9 hours") ::text; return next; x := $$(t0 + '1 day') + '9 hours' : $$||((t0 + "1 day") + "9 hours") ::text; return next; x := $$(t0 + '9 hours') + '1 day' : $$||((t0 + "9 hours") + "1 day") ::text; return next; end; end; $body$; select x from hybridddss_results(); ``` This is the result: ```output t0 + '1 day 9 hours' : 2021-03-15 06:00:00-07 (t0 + '1 day') + '9 hours' : 2021-03-15 06:00:00-07 (t0 + '9 hours') + '1 day' : 2021-03-15 07:00:00-07 ``` Notice that the time of day for the third result, 07:00, is different from that for the first and second results, 06:00. Now try this similar test. It uses hybrid "months-days" _interval values. ```plpgsql drop function if exists hybridmmdd_results() cascade; create function hybridmmdd_results() returns table(x text) language plpgsql as $body$ begin set timezone = 'UTC'; declare -- The last day of a short

t0 constant timestamptz := '2021-02-28 12:00:00 UTC'; "1 month" constant interval := '1 month'; "9 days" constant interval := '9 days'; "1 month 9 days" constant interval := '1 month 9 days'; begin x := $$ t0 + '1 month 9 days' : $$|| (t0 + "1 month 9 days") ::text; return next; x := $$(t0 + '1 month') + '9 days' : $$||((t0 + "1 month") + "9 days") ::text; return next; x := $$(t0 + '9 days') + '1 month' : $$||((t0 + "9 days") + "1 month") ::text; return next; end; end; $body$; select x from hybridmmdd_results(); ``` This is the result: ```output t0 + '1 month 9 days' : 2021-04-06 12:00:00+00 (t0 + '1 month') + '9 days' : 2021-04-06 12:00:00+00 (t0 + '9 days') + '1 month' : 2021-04-09 12:00:00+00 ``` Notice that the date for the third result, 9-April, is different from that for the first and second results, 6-April. You might be tempted to speculate about priority rules for how the fields of a hybrid interval value are acted on. Don't do this. The PostgreSQL documentation doesn't state the rules and application code that used hybrid interval values would be hard to understand and likely, therefore, to be unreliable. {{< tip title="Avoid arithmetic that uses hybrid 'interval' values." >}} Yugabyte recommends that you avoid creating and using hybrid interval values; rather, you should adopt the practice that the section explains. {{< /tip >}} Create this function: ```plpgsql drop function if exists momentssinterval_addition(timestamptz, interval) cascade; create function momentssinterval_addition(t timestamptz, i interval) returns timestamptz language plpgsql as $body$ declare mmddss constant intervalmmddsst := intervalmmdd_ss(i); begin assert mmddss.ss <> 0 and mmddss.dd = 0 and mmddss.mm = 0, 'not a pure seconds interval'; declare ss_t constant double precision not null := extract(epoch from t); ssi constant double precision not null := mmdd_ss.ss::double precision; imodel constant timestamptz not null := totimestamp(sst + ssi); i_actual constant timestamptz not null := t + i; begin assert imodel = iactual, 'assert model does not hold'; return i_model; end; end; $body$; ``` Now test it like this: ```plpgsql set timezone = 'America/Los_Angeles'; select momentssintervaladdition('2021-02-19 12:00:00 America/LosAngeles'::timestamptz, '720 hours'::interval); ``` The function finishes without an assert violationshowing that a pure seconds interval value was supplied and that the semantic model that the function describes holds. This is the result: ```output 2021-03-21 13:00:00-07 ``` The PL/pgSQL code, by simply adding a number of seconds to the start moment to get the end moment in the regime of seconds since the start of the epoch, vividly makes the point that the moment-interval overloads of `+` and `-` for pure seconds interval values honor clock-time-semantics. Note: YSQL inherits the fact from the PostgreSQL implementation of the that are not supported. If they were, then the explanation of the semantics of moment-interval would demand yet more care. Now test it using a timezone that does not respect Daylight Savings time: ```plpgsql set timezone = 'Asia/Shanghai'; select momentssintervaladdition('2021-02-19 12:00:00 America/LosAngeles'::timestamptz, '720 hours'::interval); ``` Once again, the function finishes without an assert violationshowing that, again, the semantic model that the function describes holds. This is the result: ```output 2021-03-22 04:00:00+08 ``` The overload of the function momentssintervaladdition() for a plain timestamp moment is a more-or-less mechanical re-write of the overload for the timestamptz_ moment. Create it thus. (The function is defined in the section.) ```plpgsql drop function if exists momentssinterval_addition(timestamp, interval) cascade; create function momentssinterval_addition(t timestamp, i interval) returns timestamp language plpgsql as $body$ declare mmddss constant intervalmmddsst := intervalmmdd_ss(i); begin assert mmddss.ss <> 0 and mmddss.dd = 0 and mmddss.mm = 0, 'not a pure seconds interval'; declare ss_t constant double precision not null := extract(epoch from t); ssi constant double precision not null :=

precision; imodel constant timestamp not null := totimestampwithouttz(sst + ssi); i_actual constant timestamp not null := t + i; begin assert imodel = iactual, 'assert model does not hold'; return i_model; end; end; $body$; ``` Test it like this: ```plpgsql select momentssinterval_addition('2021-02-19 12:00:00'::timestamp, '720 hours'::interval); ``` Once again, the function finishes without an assert violationshowing that a pure seconds interval value was supplied and that the semantic model that the function describes holds. This is the result: ```output 2021-03-21 12:00:00 ``` The overload of the function momentssintervaladdition() for a plain time moment is derived from the overload for a plain timestamp moment. Create it thus. (The function _ is defined in the section.) ```plpgsql drop function if exists momentssinterval_addition(time, interval) cascade; create function momentssinterval_addition(t time, i interval) returns time language plpgsql as $body$ declare mmddss constant intervalmmddsst := intervalmmdd_ss(i); begin assert mmddss.ss <> 0 and mmddss.dd = 0 and mmddss.mm = 0, 'not a pure seconds interval'; declare -- no. of secs since midnight ss_t constant double precision not null := extract(epoch from t); ssi constant double precision not null := mmdd_ss.ss; imodel constant time not null := totime(sst + ssi); i_actual constant time not null := t + i; begin assert imodel = iactual, 'assert model does not hold'; return i_model; end; end; $body$; ``` Test it like this: ```plpgsql select momentssinterval_addition('17:43:37.123'::time, '10:13:41'::interval); ``` Once again, the function finishes without an assert violationshowing that a pure seconds interval value was supplied and that the semantic model that the function describes holds. This is the result: ```output 03:57:18.123 ``` {{< tip title="See the 'sensitivity of timestamptz-interval arithmetic to the current timezone' section for a complementary definition of the semantics of arithmetic that uses pure days 'interval' values." >}} The explanations that this page presents for the semantics of moment-interval arithmetic for the three different kinds of interval (pure seconds, pure days, and pure months) are oriented to the use-cases that motivate the distinctions. The explanation of the pure days semantics is inextricably bound up with the timezone notion and how this, in turn, determines the UTC offset. See the section in the "Timezones and UTC offsets" section for a code example and discussion that models the rules in a different, but ultimately equivalent, way from this present subsection's approach. For a complex topic like this, it helps to solidify your mental model by examining relevant scenarios from different angles. {{< /tip >}} Notice that it doesn't make sense to add a duration of one or several days to a time valueeven though the attempt doesn't cause an error and does produce a result. This section shows only PL/pgSQL models for the timestamptz and timestamp overloads. Create this function: ```plpgsql drop function if exists momentddinterval_addition(timestamptz, interval) cascade; create function momentddinterval_addition(t timestamptz, i interval) returns timestamptz language plpgsql as $body$ declare mmddss constant intervalmmddsst := intervalmmdd_ss(i); current_tz text not null := ''; begin assert mmddss.ss = 0 and mmddss.dd <> 0 and mmddss.mm = 0, 'not a pure days interval'; declare timepart constant text not null := tochar(t, 'hh24:mi:ss'); datepart0 constant date not null := t::date; datepart constant date not null := datepart0 +

imodel constant timestamptz not null := (datepart::text||' '||time_part::text)::timestamptz; i_actual constant timestamptz not null := t + i; begin assert imodel = iactual, 'assert model does not hold'; return i_model; end; end; $body$; ``` Test it thus: ```plpgsql set timezone = 'Asia/Shanghai'; select momentddinterval_addition('2021-02-19 12:00:00 Asia/Shanghai'::timestamptz, '30 days'::interval); ``` The function finishes without an assert violationshowing that a pure days interval value was supplied and that the semantic model that the function describes holds. This is the result: ```output 2021-03-21 12:00:00+08 ``` The PL/pgSQL code, by separating out the time-of-day before adding the number of days, vividly makes the point that the semantics of the moment-interval overloads of `+` and `-` for pure days interval values honors one flavor of calendar-timesemantics_. Now test it using a timezone that does not respect Daylight Savings time: ```plpgsql set timezone = 'Asia/Shanghai'; select momentddinterval_addition('2021-02-19 12:00:00 Asia/Shanghai'::timestamptz, '30 days'::interval); ``` Once again, the function finishes without an assert violationshowing that, again, the semantic model that the function describes holds. This is the result: ```output 2021-03-21 12:00:00+08 ``` Notice that when you do the operation and observe the result in a certain timezone (this example happens to implement doing and observing with the same statementbut this does not need generally to be the case), then adding some number of days takes you to the same time of day that many days later. The PL/pgSQL model does exactly this. This rule holds whether or nor the timezone that your session uses respects Daylight Savings Time and the interval spans the "spring forward" moment or the "fall back" moment. The overload of the function momentddintervaladdition() for a plain timestamp moment is a direct re-write of the overload for the timestamptz_ moment. Simply replace the text "timestamptz" with the text "timestamp" in the function's source code: ```plpgsql drop function if exists momentddinterval_addition(timestamp, interval) cascade; create function momentddinterval_addition(t timestamp, i interval) returns timestamp language plpgsql as $body$ declare mmddss constant intervalmmddsst := intervalmmdd_ss(i); current_tz text not null := ''; begin assert mmddss.ss = 0 and mmddss.dd <> 0 and mmddss.mm = 0, 'not a pure days interval'; declare timepart constant text not null := tochar(t, 'hh24:mi:ss'); datepart0 constant date not null := t::date; datepart constant date not null := datepart0 + mmdd_ss.dd; imodel constant timestamp not null := (datepart::text||' '||time_part::text)::timestamp; i_actual constant timestamp not null := t + i; begin assert imodel = iactual, 'assert model does not hold'; return i_model; end; end; $body$; ``` Then test it with trivially modified versions of the tests that were used for the timestamptz overload: ```plpgsql set timezone = 'Asia/Shanghai'; select momentddinterval_addition('2021-02-19 12:00:00'::timestamp, '30 days'::interval); set timezone = 'Asia/Shanghai'; select momentddinterval_addition('2021-02-19 12:00:00'::timestamp, '30 days'::interval); ``` You get the same result for each, independently (of course) of what the sessions' TimeZone setting is: ```output 2021-03-21 12:00:00 ``` Notice that, here too, it doesn't make sense to add a duration of one or several months to a time valueeven though the attempt doesn't cause an error and does produce a result. This section shows only PL/pgSQL models for the timestamptz and timestamp overloads. Create this function: ```plpgsql drop function if exists momentmminterval_addition(timestamptz, interval) cascade; create function momentmminterval_addition(t timestamptz, i interval) returns timestamptz language plpgsql as $body$ declare mmddss constant intervalmmddsst := intervalmmdd_ss(i); current_tz text not null := ''; begin assert mmddss.ss = 0 and mmddss.dd = 0 and mmddss.mm <> 0, 'not a pure months interval'; declare timepart constant text not null := tochar(t, 'hh24:mi:ss'); datepart0 constant date not null := t::date; year0 constant int not null := extract(year from datepart_0); month0 constant int not null := extract(month from datepart_0); day0 constant int not null := extract(day from datepart_0); year constant int not null := year0 + trunc(mmdd_ss.mm/12); month constant int not null := month0 + mod(mmdd_ss.mm, 12); -- Check that it's a legal

datepart constant date not null := year::text||'-'||month::text||'-'||day0::text; imodel constant timestamptz not null := (datepart::text||' '||time_part::text)::timestamptz; i_actual constant timestamptz not null := t + i; begin assert imodel = iactual, 'assert model does not hold'; return i_model; end; end; $body$; ``` Test it thus: ```plpgsql set timezone = 'America/Los_Angeles'; select momentmmintervaladdition('2021-02-19 12:00:00 America/LosAngeles'::timestamptz, '1 year 5 months'::interval); ``` The function finishes without an assert violationshowing that a pure months interval value was supplied and that the semantic model that the function describes holds. This is the result:: ```output 2022-07-19 12:00:00-07 ``` The PL/pgSQL code, by separating out the time-of-day before adding the number of months, vividly makes the point that the semantics of the moment-interval overloads of `+` and `-` for pure days interval values honors another flavor of calendar-time-semantics. Now test it using a timezone that does not respect Daylight Savings time: ```plpgsql set timezone = 'Asia/Shanghai'; select momentmminterval_addition('2021-02-19 12:00:00 Asia/Shanghai'::timestamptz, '1 year 5 months'::interval); ``` Once again, the function finishes without an assert violationshowing that, again, the semantic model that the function describes holds. This is the result: ```output 2022-07-19 12:00:00+08 ``` Notice that when you do the operation and observe the result in a certain timezone, adding some number of months takes you to the same day of the month and the same time of day that many months later. The PL/pgSQL model does exactly this. Of course, this rule holds whether or not the timezone that your session uses respects Daylight Savings Time"; "spring forward" or "fall back" moments are irrelevant, given the semantic definition. The definition, and the implemented model, make it crystal clear that the number of days between the starting and ending moments for a certain pure months interval value will vary according to the number of days in the various months that the interval happens to span. (The effect of leap years is implied by this statement.) The overload of the function momentmmintervaladdition() for a plain timestamp moment is a direct re-write of the overload for the timestamptz_ moment. Simply replace the text "timestamptz" with the text "timestamp" in the function's source code: ```plpgsql drop function if exists momentmminterval_addition(timestamp, interval) cascade; create function momentmminterval_addition(t timestamp, i interval) returns timestamp language plpgsql as $body$ declare mmddss constant intervalmmddsst := intervalmmdd_ss(i); current_tz text not null := ''; begin assert mmddss.ss = 0 and mmddss.dd = 0 and mmddss.mm <> 0, 'not a pure months interval'; declare timepart constant text not null := tochar(t, 'hh24:mi:ss'); datepart0 constant date not null := t::date; year0 constant int not null := extract(year from datepart_0); month0 constant int not null := extract(month from datepart_0); day0 constant int not null := extract(day from datepart_0); year constant int not null := year0 + trunc(mmdd_ss.mm/12); month constant int not null := month0 + mod(mmdd_ss.mm, 12); -- Check that it's a legal date. datepart constant date not null := year::text||'-'||month::text||'-'||day0::text; imodel constant timestamp not null := (datepart::text||' '||time_part::text)::timestamp; i_actual constant timestamp not null := t + i; begin assert imodel = iactual, 'assert model does not hold'; return i_model; end; end; $body$; ``` Then test it with trivially modified versions of the tests that were used for the timestamptz overload: ```plpgsql set timezone = 'America/Los_Angeles'; select momentmminterval_addition('2021-02-19 12:00:00'::timestamp, '1 year 5 months'::interval); set timezone = 'Asia/Shanghai'; select momentmminterval_addition('2021-02-19 12:00:00'::timestamp, '1 year 5 months'::interval); ``` You get the same result for each, independently (of course) of what the session's TimeZone setting is: ```output 2022-07-19 12:00:00 ```