Thread: sign function with INTERVAL?
Hi all, Is there a good reason why the SIGN() function does not work with the INTERVAL type? (It is only defined for numeric types.) (http://www.postgresql.org/docs/9.5/static/functions-math.html) select sign(-3); -- okay select sign(interval '4 years'); -- ERROR: function sign(interval) does not exist I'm trying to find a straightforward and reliable way to differentiate positive, negative, and zero time intervals while handling NULL in the same way as the SIGN() function. What I have come up with is this rather inelegant and error-prone case statement: case when x is null then null x>interval '0' then +1 when x<interval '0' then -1 when x=interval '0' then 0 end Here's an example: with t as (select interval '4 years 2 months' as x, interval '-1 minute 2 seconds' as y, interval '0' as z, null::interval as w) select case when x is null then null when x>interval '0' then +1 when x<interval '0' then -1 when x=interval '0' then 0 end, case when y is null then null when y>interval '0' then +1 when y<interval '0' then -1 when y=interval '0' then 0 end, case when z is null then null when z>interval '0' then +1 when z<interval '0' then -1 when z=interval '0' then 0 end, case when w is null then null when w>interval '0' then +1 when w<interval '0' then -1 when w=interval '0' then 0 end from t Is there a more obvious way to do sign(interval)? Would it be technically difficult to make it "just work"? Thanks, Dan
On 2016-04-13 Daniel Lenski <dlenski@gmail.com> wrote: > Hi all, > I'm trying to find a straightforward and reliable way to differentiate > positive, negative, and zero time intervals while handling NULL in the > same way as the SIGN() function. I'm not sure that "positive time interval" is a thing. Witness: dakkar=> select interval '1 month - 30 days' > interval '0'; ┌──────────┐ │ ?column? │ ├──────────┤ │ f │ └──────────┘ (1 row) not positive? maybe it's negative? dakkar=> select interval '1 month - 30 days' < interval '0'; ┌──────────┐ │ ?column? │ ├──────────┤ │ f │ └──────────┘ (1 row) no, not negative either. Why? Well… dakkar=> select date '2016-03-01' + interval '1 month - 30 days'; ┌─────────────────────┐ │ ?column? │ ├─────────────────────┤ │ 2016-03-02 00:00:00 │ └─────────────────────┘ (1 row) when used this way, it looks positive, but dakkar=> select date '2016-02-01' + interval '1 month - 30 days'; ┌─────────────────────┐ │ ?column? │ ├─────────────────────┤ │ 2016-01-31 00:00:00 │ └─────────────────────┘ (1 row) when used this way, it looks negative. So I suspect the reason SIGN() is not defined for intervals is that it cannot be made to work in the general case. -- Dakkar - <Mobilis in mobile> GPG public key fingerprint = A071 E618 DD2C 5901 9574 6FE2 40EA 9883 7519 3F88 key id = 0x75193F88 Thrashing is just virtual crashing.
Attachment
On 4/13/16 1:36 PM, Daniel Lenski wrote: > Hi all, > Is there a good reason why the SIGN() function does not work with the > INTERVAL type? (It is only defined for numeric types.) > (http://www.postgresql.org/docs/9.5/static/functions-math.html) The only thing that comes to mind is you can get some strange circumstances with intervals, like '-1 mons +4 days'. I don't think that precludes sign() though. > What I have come up with is this rather inelegant and error-prone case > statement: How is it error prone? > case when x is null then null x>interval '0' then +1 when x<interval > '0' then -1 when x=interval '0' then 0 end You don't need to handle null explicitly. You could do SELECT CASE WHEN x > interval '0' THEN 1 WHEN x < interval '0' THEN -1 WHEN x = interval '0' THEN 0 END Or, you could do... CREATE FUNCTION sign(interval) RETURNS int LANGUAGE sql STRICT IMMUTABLE AS $$ SELECT CASE WHEN $1 > interval '0' THEN 1 WHEN x < interval '0' THEN -1 ELSE 0 END $$; That works because a STRICT function won't even be called if any of it's inputs are NULL. > Is there a more obvious way to do sign(interval)? Would it be > technically difficult to make it "just work"? Actually, after looking at the code for interval_lt, all that needs to happen to add this support is to expose interval_cmp_internal() as a strict function. It already does exactly what you want. -- Jim Nasby, Data Architect, Blue Treble Consulting, Austin TX Experts in Analytics, Data Architecture and PostgreSQL Data in Trouble? Get it in Treble! http://BlueTreble.com
Jim Nasby <Jim.Nasby@BlueTreble.com> writes: > Actually, after looking at the code for interval_lt, all that needs to > happen to add this support is to expose interval_cmp_internal() as a > strict function. It already does exactly what you want. interval_cmp() is already SQL-accessible. regards, tom lane
On Wed, Apr 13, 2016 at 12:35 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote: > Jim Nasby <Jim.Nasby@BlueTreble.com> writes: >> Actually, after looking at the code for interval_lt, all that needs to >> happen to add this support is to expose interval_cmp_internal() as a >> strict function. It already does exactly what you want. > > interval_cmp() is already SQL-accessible. Thanks! The interval_cmp() function does not appear in the 9.5 docs. http://www.postgresql.org/docs/9.5/static/functions-datetime.html On Wed, Apr 13, 2016 at 11:54 AM, Gianni Ceccarelli <dakkar@thenautilus.net> wrote: > I'm not sure that "positive time interval" is a thing. Witness: > > (snip) > > dakkar=> select date '2016-03-01' + interval '1 month - 30 days'; > ┌─────────────────────┐ > │ ?column? │ > ├─────────────────────┤ > │ 2016-03-02 00:00:00 │ > └─────────────────────┘ > (1 row) > > when used this way, it looks positive, but > > dakkar=> select date '2016-02-01' + interval '1 month - 30 days'; > ┌─────────────────────┐ > │ ?column? │ > ├─────────────────────┤ > │ 2016-01-31 00:00:00 │ > └─────────────────────┘ > (1 row) > > when used this way, it looks negative. > > So I suspect the reason SIGN() is not defined for intervals is that > it cannot be made to work in the general case. I hadn't considered this case of an interval like '1 month - 30 days', which could be either positive or negative depending on the starting date to which it is added. interval_cmp's handling of this case seems surprising to me. If I've got this right, it assumes that (interval '1 month' == interval '30 days') exactly: http://doxygen.postgresql.org/backend_2utils_2adt_2timestamp_8c_source.html#l02515 Do I have that right? I'm having trouble envisioning an application that would ever generate intervals that contain months and days without opposite signs, but it's useful to know that such a corner case could exist. Given this behavior, the only 100% reliable way to check whether an interval is forward, backwards, or zero would be to first add, and then subtract, the starting point: postgres=# select interval_cmp( (date '2016-02-01' + interval '1 month - 30 days') - date '2016-02-01', interval '0' ); interval_cmp -------------- -1 (1 row) postgres=# select interval_cmp( (date '2016-04-01' + interval '1 month - 30 days') - date '2016-04-01', interval '0' ); interval_cmp -------------- 0 (1 row) Thanks, Dan
On Wed, Apr 13, 2016 at 12:35 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
> Jim Nasby <Jim.Nasby@BlueTreble.com> writes:
>> Actually, after looking at the code for interval_lt, all that needs to
>> happen to add this support is to expose interval_cmp_internal() as a
>> strict function. It already does exactly what you want.
>
> interval_cmp() is already SQL-accessible.
Thanks! The interval_cmp() function does not appear in the 9.5 docs.
http://www.postgresql.org/docs/9.5/static/functions-datetime.html
On Wed, Apr 13, 2016 at 11:54 AM, Gianni Ceccarelli
<dakkar@thenautilus.net> wrote:
> I'm not sure that "positive time interval" is a thing. Witness:
>
> (snip)
>
> dakkar=> select date '2016-03-01' + interval '1 month - 30 days';
> ┌─────────────────────┐
> │ ?column? │
> ├─────────────────────┤
> │ 2016-03-02 00:00:00 │
> └─────────────────────┘
> (1 row)
>
> when used this way, it looks positive, but
>
> dakkar=> select date '2016-02-01' + interval '1 month - 30 days';
> ┌─────────────────────┐
> │ ?column? │
> ├─────────────────────┤
> │ 2016-01-31 00:00:00 │
> └─────────────────────┘
> (1 row)
>
> when used this way, it looks negative.
>
> So I suspect the reason SIGN() is not defined for intervals is that
> it cannot be made to work in the general case.
I hadn't considered this case of an interval like '1 month - 30 days',
which could be either positive or negative depending on the starting
date to which it is added.
interval_cmp's handling of this case seems surprising to me. If I've
got this right, it assumes that (interval '1 month' == interval '30
days') exactly:
http://doxygen.postgresql.org/backend_2utils_2adt_2timestamp_8c_source.html#l02515
I was trying to figure out how the months/year fit in here - or whether years are derived from days (i.e., 365 instead of 360)...
The anchored section of code only shows stand-alone conversion factors for days->hours and months-days
Do I have that right? I'm having trouble envisioning an application
that would ever generate intervals that contain months and days
without opposite signs, but it's useful to know that such a corner
case could exist.
Yes.
&
I want the date that is 1 month and 14 days (2 weeks) from now...
For added fun the SQL standard apparently disallows mixed signs (according to our docs)
>According to the SQL standard all fields of an interval value must have the same sign,
>so a leading negative sign applies to all fields; for example the negative sign in
>the interval literal '-1 2:03:04' applies to both the days and hour/minute/second parts.
Given this behavior, the only 100% reliable way to check whether an
interval is forward, backwards, or zero would be to first add, and
then subtract, the starting point:
postgres=# select interval_cmp( (date '2016-02-01' + interval '1 month
- 30 days') - date '2016-02-01', interval '0' );
interval_cmp
--------------
-1
(1 row)
postgres=# select interval_cmp( (date '2016-04-01' + interval '1 month
- 30 days') - date '2016-04-01', interval '0' );
interval_cmp
--------------
0
(1 row)
Yes, the dual nature of an interval, i.e., an assumed conversion factor (1m = 30d) if dealing with it independently but a conversion factor based on reality (feb has 28 days, typically) makes working with it complicated. There is not way you could write an operator that successfully handles the later situation since you cannot write a custom ternary operator that would take two intervals and a date. That said we already have rules that allow us to canonical-ize an interval so any form of two-interval comparison can be performed: but those results become invalidated if one has to apply the interval to a date.
In short, adding this feature would make it much easier for the inexperienced to use intervals unsafely without realizing it. It is possible to write custom functions that do exactly what is needed based upon the usage of intervals within the system under observation. Doability combined with ignorance hazard means that the status-quo seems acceptable.
I guess it would be nice to expose our conversion factors in such a way that a user can readily get the number of seconds represented by a given interval when considered without respect to a starting date. But since most uses of interval are related to dates it seems likely that comparing intervals by comparing the dates resulting from their application is the most reliable.
N.B. consider too that the signs are not the whole of it. Intervals allow for the word "ago" to be specified.
David J.