Have I found an interval arithmetic bug?

Started by Bryn Llewellynabout 5 years ago99 messageshackersgeneral
Jump to latest
#1Bryn Llewellyn
bryn@yugabyte.com
hackersgeneral

Or am I misunderstanding something?

Try this. The result of each “select” is shown as the trailing comment on the same line. I added whitespace by hand to line up the fields.

select interval '-1.7 years'; -- -1 years -8 mons

select interval '29.4 months'; -- 2 years 5 mons 12 days

select interval '-1.7 years 29.4 months'; -- 8 mons 12 days << wrong
select interval '29.4 months -1.7 years'; -- 9 mons 12 days

select interval '-1.7 years' + interval '29.4 months'; -- 9 mons 12 days
select interval '29.4 months' + interval '-1.7 years'; -- 9 mons 12 days

As I reason it, the last four “select” statements are all semantically the same. They’re just different syntaxes to add the two intervals the the first two “select” statements use separately. There’s one odd man out. And I reason this one to be wrong. Is there a flaw in my reasoning?

Further… there’s a notable asymmetry. The fractional part of “1.7 years” is 8.4 months. But the fractional part of the months value doesn’t spread further down into days. However, the fractional part of “29.4 months” (12 days) _does_ spread further down into days. What’s the rationale for this asymmetry?

I can’t see that my observations here can be explained by the difference between calendar time and clock time. Here I’m just working with non-metric units like feet and inches. One year is just defined as 12 months. And one month is just defined as 30 days. All that stuff about adding a month to 3-Feb-2020 taking you to 3-Mar-2020 (same for leap years an non-leap years) , and that other stuff about adding one day to 23:00 on the day before the “spring forward” moment taking you to 23:00 on the next day (i.w. when intervals are added to timestamps) is downstream of simply adding two intervals.

#2Bruce Momjian
bruce@momjian.us
In reply to: Bryn Llewellyn (#1)
hackersgeneral
Re: Have I found an interval arithmetic bug?

On Thu, Apr 1, 2021 at 09:46:58PM -0700, Bryn Llewellyn wrote:

Or am I misunderstanding something?

Try this. The result of each “select” is shown as the trailing comment on the
same line. I added whitespace by hand to line up the fields.

select interval '-1.7 years'; -- -1 years -8 mons

select interval '29.4 months'; -- 2 years 5 mons 12
days

select interval '-1.7 years 29.4 months'; -- 8 mons 12
days << wrong
select interval '29.4 months -1.7 years'; -- 9 mons 12
days

select interval '-1.7 years' + interval '29.4 months'; -- 9 mons 12
days
select interval '29.4 months' + interval '-1.7 years'; -- 9 mons 12
days

As I reason it, the last four “select” statements are all semantically the
same. They’re just different syntaxes to add the two intervals the the first
two “select” statements use separately. There’s one odd man out. And I reason
this one to be wrong. Is there a flaw in my reasoning?

[Thread moved to hackers.]

Looking at your report, I thought I could easily find the cause, but it
wasn't obvious. What is happening is that when you cast a float to an
integer in C, it rounds toward zero, meaning that 8.4 rounds to 8 and
-8.4 rounds to -8. The big problem here is that -8.4 is rounding in a
positive direction, while 8.4 rounds in a negative direction. See this:

int(int(-8.4) + 29)
21
int(int(29) + -8.4)
20

When you do '-1.7 years' first, it become -8, and then adding 29 yields
21. In the other order, it is 29 - 8.4, which yields 20.6, which
becomes 20. I honestly had never studied this interaction, though you
would think I would have seen it before. One interesting issue is that
it only happens when the truncations happen to values with different
signs --- if they are both positive or negative, it is fine.

The best fix I think is to use rint()/round to round to the closest
integer, not toward zero. The attached patch does this in a few places,
but the code needs more research if we are happy with this approach,
since there are probably other cases. Using rint() does help to produce
more accurate results, thought the regression tests show no change from
this patch.

Further… there’s a notable asymmetry. The fractional part of “1.7 years” is 8.4
months. But the fractional part of the months value doesn’t spread further down
into days. However, the fractional part of “29.4 months” (12 days) _does_
spread further down into days. What’s the rationale for this asymmetry?

Yes, looking at the code, it seems we only spill down to one unit, not
more. I think we need to have a discussion if we want to change that.
I think the idea was that if you specify a non-whole number, you
probably want to spill down one level, but don't want it spilling all
the way to milliseconds, which is certainly possible.

I can’t see that my observations here can be explained by the difference
between calendar time and clock time. Here I’m just working with non-metric
units like feet and inches. One year is just defined as 12 months. And one
month is just defined as 30 days. All that stuff about adding a month to
3-Feb-2020 taking you to 3-Mar-2020 (same for leap years an non-leap years) ,
and that other stuff about adding one day to 23:00 on the day before the
“spring forward” moment taking you to 23:00 on the next day (i.w. when
intervals are added to timestamps) is downstream of simply adding two
intervals.

Ah, seems you have done some research. ;-)

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

Attachments:

interval.difftext/x-diff; charset=us-asciiDownload+6-6
#3Bruce Momjian
bruce@momjian.us
In reply to: Bryn Llewellyn (#1)
hackersgeneral
Re: Have I found an interval arithmetic bug?

Thread moved to hackers, with a patch.

---------------------------------------------------------------------------

On Thu, Apr 1, 2021 at 09:46:58PM -0700, Bryn Llewellyn wrote:

Or am I misunderstanding something?

Try this. The result of each “select” is shown as the trailing comment on the
same line. I added whitespace by hand to line up the fields.

select interval '-1.7 years'; -- -1 years -8 mons

select interval '29.4 months'; -- 2 years 5 mons 12
days

select interval '-1.7 years 29.4 months'; -- 8 mons 12
days << wrong
select interval '29.4 months -1.7 years'; -- 9 mons 12
days

select interval '-1.7 years' + interval '29.4 months'; -- 9 mons 12
days
select interval '29.4 months' + interval '-1.7 years'; -- 9 mons 12
days

As I reason it, the last four “select” statements are all semantically the
same. They’re just different syntaxes to add the two intervals the the first
two “select” statements use separately. There’s one odd man out. And I reason
this one to be wrong. Is there a flaw in my reasoning?

Further… there’s a notable asymmetry. The fractional part of “1.7 years” is 8.4
months. But the fractional part of the months value doesn’t spread further down
into days. However, the fractional part of “29.4 months” (12 days) _does_
spread further down into days. What’s the rationale for this asymmetry?

I can’t see that my observations here can be explained by the difference
between calendar time and clock time. Here I’m just working with non-metric
units like feet and inches. One year is just defined as 12 months. And one
month is just defined as 30 days. All that stuff about adding a month to
3-Feb-2020 taking you to 3-Mar-2020 (same for leap years an non-leap years) ,
and that other stuff about adding one day to 23:00 on the day before the
“spring forward” moment taking you to 23:00 on the next day (i.w. when
intervals are added to timestamps) is downstream of simply adding two
intervals.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

#4Bryn Llewellyn
bryn@yugabyte.com
In reply to: Bruce Momjian (#2)
hackersgeneral
Re: Have I found an interval arithmetic bug?

bruce@momjian.us wrote:

[Thread moved to hackers.] …The best fix I think is…

Bryn wrote: Further… there’s a notable asymmetry. The fractional part of “1.7 years” is 8.4 months. But the fractional part of the months value doesn’t spread further down into days. However, the fractional part of “29.4 months” (12 days) _does_ spread further down into days. What’s the rationale for this asymmetry?

Yes, looking at the code, it seems we only spill down to one unit, not more. I think we need to have a discussion if we want to change that. I think the idea was that if you specify a non-whole number, you probably want to spill down one level, but don't want it spilling all the way to milliseconds, which is certainly possible.

Thanks for the quick response, Bruce. I was half expecting (re the bug) an explanation that showed that I’d (once again) misunderstood a fundamental principle.

I should come clean about the larger context. I work for Yugabyte, Inc. We have a distributed SQL database that uses the Version 11.2 PostgreSQL C code for SQL processing “as is”.

https://blog.yugabyte.com/distributed-postgresql-on-a-google-spanner-architecture-query-layer/

The founders decided to document YugabyteDB’s SQL functionality explicitly rather than just to point to the published PostgreSQL doc. (There are some DDL differences that reflect the storage layer differences.) I’m presently documenting date-time functionality. This is why I’m so focused on understanding the semantics exactly and on understanding the requirements that the functionality was designed to meet. I’m struggling with interval functionality. I read this:

https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-INTERVAL-INPUT

« …field values can have fractional parts; for example '1.5 week' or '01:02:03.45'. Such input is converted to the appropriate number of months, days, and seconds for storage. When this would result in a fractional number of months or days, the fraction is added to the lower-order fields using the conversion factors 1 month = 30 days and 1 day = 24 hours. For example, '1.5 month' becomes 1 month and 15 days. Only seconds will ever be shown as fractional on output. »

Notice that the doc says that spill-down goes all the way to seconds and not just one unit. This simple test is consistent with the doc (output follows the dash-dash comment):

select ('6.54321 months'::interval)::text as i; -- 6 mons 16 days 07:06:40.32

You see similar spill-down with this:

select ('876.54321 days'::interval)::text as i; -- 876 days 13:02:13.344

And so on down through the remaining smaller units. It’s only this test that doesn’t spill down one unit:

select ('6.54321 years'::interval)::text as i; -- 6 years 6 mons

This does suggest a straight bug rather than a case for committee debate about what might have been intended. What do you think, Bruce?

#5Ken Tanzer
ken.tanzer@gmail.com
In reply to: Bruce Momjian (#3)
hackersgeneral
Re: Have I found an interval arithmetic bug?

On Fri, Apr 2, 2021 at 11:06 AM Bruce Momjian <bruce@momjian.us> wrote:

Thread moved to hackers, with a patch.
---------------------------------------------------------------------------

Here is a link to that thread, for others who might be curious about it as
I was:
/messages/by-id/20210402180549.GF9270@momjian.us

I get why it can make sense to move a thread. But if when doing so you
post a link to the new thread, that would be appreciated. Thanks!

Ken

--
AGENCY Software
A Free Software data system
By and for non-profits
*http://agency-software.org/ <http://agency-software.org/&gt;*
*https://demo.agency-software.org/client
<https://demo.agency-software.org/client&gt;*
ken.tanzer@agency-software.org
(253) 245-3801

Subscribe to the mailing list
<agency-general-request@lists.sourceforge.net?body=subscribe> to
learn more about AGENCY or
follow the discussion.

#6Zhihong Yu
zyu@yugabyte.com
In reply to: Bruce Momjian (#2)
hackersgeneral
Re: Have I found an interval arithmetic bug?

Bruce:
Thanks for tackling this issue.

The patch looks good to me.
When you have time, can you include the places which were not covered by
the current diff ?

Cheers

On Fri, Apr 2, 2021 at 11:06 AM Bruce Momjian <bruce@momjian.us> wrote:

Show quoted text

On Thu, Apr 1, 2021 at 09:46:58PM -0700, Bryn Llewellyn wrote:

Or am I misunderstanding something?

Try this. The result of each “select” is shown as the trailing comment

on the

same line. I added whitespace by hand to line up the fields.

select interval '-1.7 years'; -- -1 years -8

mons

select interval '29.4 months'; -- 2 years 5

mons 12

days

select interval '-1.7 years 29.4 months'; -- 8

mons 12

days << wrong
select interval '29.4 months -1.7 years'; -- 9

mons 12

days

select interval '-1.7 years' + interval '29.4 months'; -- 9

mons 12

days
select interval '29.4 months' + interval '-1.7 years'; -- 9

mons 12

days

As I reason it, the last four “select” statements are all semantically

the

same. They’re just different syntaxes to add the two intervals the the

first

two “select” statements use separately. There’s one odd man out. And I

reason

this one to be wrong. Is there a flaw in my reasoning?

[Thread moved to hackers.]

Looking at your report, I thought I could easily find the cause, but it
wasn't obvious. What is happening is that when you cast a float to an
integer in C, it rounds toward zero, meaning that 8.4 rounds to 8 and
-8.4 rounds to -8. The big problem here is that -8.4 is rounding in a
positive direction, while 8.4 rounds in a negative direction. See this:

int(int(-8.4) + 29)
21
int(int(29) + -8.4)
20

When you do '-1.7 years' first, it become -8, and then adding 29 yields
21. In the other order, it is 29 - 8.4, which yields 20.6, which
becomes 20. I honestly had never studied this interaction, though you
would think I would have seen it before. One interesting issue is that
it only happens when the truncations happen to values with different
signs --- if they are both positive or negative, it is fine.

The best fix I think is to use rint()/round to round to the closest
integer, not toward zero. The attached patch does this in a few places,
but the code needs more research if we are happy with this approach,
since there are probably other cases. Using rint() does help to produce
more accurate results, thought the regression tests show no change from
this patch.

Further… there’s a notable asymmetry. The fractional part of “1.7 years”

is 8.4

months. But the fractional part of the months value doesn’t spread

further down

into days. However, the fractional part of “29.4 months” (12 days) _does_
spread further down into days. What’s the rationale for this asymmetry?

Yes, looking at the code, it seems we only spill down to one unit, not
more. I think we need to have a discussion if we want to change that.
I think the idea was that if you specify a non-whole number, you
probably want to spill down one level, but don't want it spilling all
the way to milliseconds, which is certainly possible.

I can’t see that my observations here can be explained by the difference
between calendar time and clock time. Here I’m just working with

non-metric

units like feet and inches. One year is just defined as 12 months. And

one

month is just defined as 30 days. All that stuff about adding a month to
3-Feb-2020 taking you to 3-Mar-2020 (same for leap years an non-leap

years) ,

and that other stuff about adding one day to 23:00 on the day before the
“spring forward” moment taking you to 23:00 on the next day (i.w. when
intervals are added to timestamps) is downstream of simply adding two
intervals.

Ah, seems you have done some research. ;-)

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

#7John W Higgins
wishdev@gmail.com
In reply to: Bruce Momjian (#2)
hackersgeneral
Re: Have I found an interval arithmetic bug?

On Fri, Apr 2, 2021 at 11:05 AM Bruce Momjian <bruce@momjian.us> wrote:

On Thu, Apr 1, 2021 at 09:46:58PM -0700, Bryn Llewellyn wrote:

Or am I misunderstanding something?

Try this. The result of each “select” is shown as the trailing comment

on the

same line. I added whitespace by hand to line up the fields.

select interval '-1.7 years'; -- -1 years -8

mons

select interval '29.4 months'; -- 2 years 5

mons 12

days

select interval '-1.7 years 29.4 months'; -- 8

mons 12

days << wrong
select interval '29.4 months -1.7 years'; -- 9

mons 12

days

select interval '-1.7 years' + interval '29.4 months'; -- 9

mons 12

days
select interval '29.4 months' + interval '-1.7 years'; -- 9

mons 12

days

While maybe there is an argument to fixing the negative/positive rounding
issue - there is no way this gets solved without breaking the current
implementation

select interval '0.3 years' + interval '0.4 years' - interval '0.7 years' +
interval '0.1 years' should not equal 0 but it certainly does.

Unless we take the concept of 0.3 years = 3 months and move to something
along the lines of

1 year = 360 days
1 month = 30 days

so therefore

0.3 years = 360 days * 0.3 = 108 days = 3 months 18 days
0.4 years = 360 days * 0.4 = 144 days = 4 months 24 days
0.7 years = 360 days * 0.7 = 252 days = 8 months 12 days

Then, and only if we don't go to any more than tenths of a year, does the
math work. Probably this should resolve down to seconds and then work
backwards - but unless we're looking at breaking the entire way it
currently resolves things - I don't think this is of much value.

Doing math on intervals is like doing math on rounded numbers - there is
always going to be a pile of issues because the level of precision just is
not good enough.

John

#8Bruce Momjian
bruce@momjian.us
In reply to: Bryn Llewellyn (#4)
hackersgeneral
Re: Have I found an interval arithmetic bug?

On Fri, Apr 2, 2021 at 01:05:42PM -0700, Bryn Llewellyn wrote:

I should come clean about the larger context. I work for Yugabyte, Inc. We have
a distributed SQL database that uses the Version 11.2 PostgreSQL C code for SQL
processing “as is”.

https://blog.yugabyte.com/
distributed-postgresql-on-a-google-spanner-architecture-query-layer/

The founders decided to document YugabyteDB’s SQL functionality explicitly
rather than just to point to the published PostgreSQL doc. (There are some DDL
differences that reflect the storage layer differences.) I’m presently
documenting date-time functionality. This is why I’m so focused on
understanding the semantics exactly and on understanding the requirements that
the functionality was designed to meet. I’m struggling with interval
functionality. I read this:

[Sorry, also moved this to hackers. I might normally do all the
discussion on general, with patches, and then move it to hackers, but
our PG 14 deadline is next week, so it is best to move it now in hopes
it can be addressed in PG 14.]

Sure, seems like a good idea.

https://www.postgresql.org/docs/current/datatype-datetime.html#
DATATYPE-INTERVAL-INPUT

« …field values can have fractional parts; for example '1.5 week' or
'01:02:03.45'. Such input is converted to the appropriate number of months,
days, and seconds for storage. When this would result in a fractional number of
months or days, the fraction is added to the lower-order fields using the
conversion factors 1 month = 30 days and 1 day = 24 hours. For example, '1.5
month' becomes 1 month and 15 days. Only seconds will ever be shown as
fractional on output. »

Notice that the doc says that spill-down goes all the way to seconds and not
just one unit. This simple test is consistent with the doc (output follows the
dash-dash comment):

select ('6.54321 months'::interval)::text as i; -- 6 mons 16 days 07:06:40.32

You see similar spill-down with this:

select ('876.54321 days'::interval)::text as i; -- 876 days 13:02:13.344

And so on down through the remaining smaller units. It’s only this test that
doesn’t spill down one unit:

select ('6.54321 years'::interval)::text as i; -- 6 years 6 mons

This does suggest a straight bug rather than a case for committee debate about
what might have been intended. What do you think, Bruce?

So, that gets into more detail. When I said "spill down one unit", I
was not talking about _visible_ units, but rather the three internal
units used by Postgres:

https://www.postgresql.org/docs/13/datatype-datetime.html#DATATYPE-INTERVAL-INPUT
Internally interval values are stored as months, days, and seconds.
-------------------------

However, while that explains why years don't spill beyond months, it
doesn't explain why months would spill beyond days. This certainly
seems inconsistent.

I have modified the patch to prevent partial months from creating
partial hours/minutes/seconds, so the output is now at least consistent
based on the three units:

SELECT ('6.54321 years'::interval)::text as i;
i
----------------
6 years 7 mons

SELECT ('6.54321 months'::interval)::text as i;
i
----------------
6 mons 16 days

SELECT ('876.54321 days'::interval)::text as i;
i
-----------------------
876 days 13:02:13.344

Partial years keeps it in months, partial months takes it to days, and
partial days take it to hours/minutes/seconds. This seems like an
improvement.

This also changes the regression test output, I think for the better:

	 SELECT INTERVAL '1.5 weeks';
	          i
	 ------------------
	- 10 days 12:00:00
	+ 10 days

The new output is less precise, but probably closer to what the user
wanted.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

Attachments:

interval.difftext/x-diff; charset=us-asciiDownload+15-15
#9Bruce Momjian
bruce@momjian.us
In reply to: Ken Tanzer (#5)
hackersgeneral
Re: Have I found an interval arithmetic bug?

On Fri, Apr 2, 2021 at 01:20:26PM -0700, Ken Tanzer wrote:

On Fri, Apr 2, 2021 at 11:06 AM Bruce Momjian <bruce@momjian.us> wrote:

Thread moved to hackers, with a patch.
---------------------------------------------------------------------------

Here is a link to that thread, for others who might be curious about it as I
was:
/messages/by-id/20210402180549.GF9270@momjian.us
b3bdafbfeacab0dd8967ff2a3ebf7844

I get why it can make sense to move a thread.� But if when doing so you post a
link to the new thread, that would be appreciated.� Thanks!

I didn't think anyone but the original poster, who was copied in the new
thread, would really care about this thread, but it seems I was wrong.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

#10Bruce Momjian
bruce@momjian.us
In reply to: Zhihong Yu (#6)
hackersgeneral
Re: Have I found an interval arithmetic bug?

On Fri, Apr 2, 2021 at 01:27:33PM -0700, Zhihong Yu wrote:

Bruce:
Thanks for tackling this issue.

The patch looks good to me.
When you have time, can you include the places which were not covered by the
current diff ?

I have just posted a new version of the patch which I think covers all
the right areas.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

#11Bruce Momjian
bruce@momjian.us
In reply to: John W Higgins (#7)
hackersgeneral
Re: Have I found an interval arithmetic bug?

On Fri, Apr 2, 2021 at 02:00:03PM -0700, John W Higgins wrote:

On Fri, Apr 2, 2021 at 11:05 AM Bruce Momjian <bruce@momjian.us> wrote:
While maybe there is an argument to fixing the negative/positive rounding issue
- there is no way this gets solved without breaking the current implementation

select interval '0.3 years' + interval '0.4 years' - interval '0.7 years'�+
interval '0.1 years' should not equal 0 but it certainly does.

My new code returns 0.2 months for this, not zero:

SELECT interval '0.3 years' + interval '0.4 years' -
interval '0.7 years' + interval '0.1 years';
?column?
----------
2 mons

which is also wrong since:

SELECT interval '0.1 years';
interval
----------
1 mon

Unless we take the concept of 0.3 years = 3 months and move to something along
the lines of�

1 year = 360 days
1 month = 30 days�

so therefore�

0.3 years = 360 days * 0.3 = 108 days = 3 months 18 days�
0.4 years = 360 days * 0.4 = 144 days = 4 months 24 days
0.7 years = 360 days * 0.7 = 252 days = 8 months 12 days

Then, and only if we don't go to any more than tenths of a year, does the math
work. Probably this should resolve down to seconds and then work backwards -
but unless we're looking at breaking the entire way it currently resolves
things - I don't think this is of much value.

Doing math on intervals is like doing math on rounded numbers - there is always
going to be a pile of issues because the level of precision just is not good
enough.

I think the big question is what units do people want with fractional
values. I have posted a follow-up email that spills only for one unit,
which I think is the best approach.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

#12Zhihong Yu
zyu@yugabyte.com
In reply to: Bruce Momjian (#11)
hackersgeneral
Re: Have I found an interval arithmetic bug?

Hi,
bq. My new code returns 0.2 months for this, not zero

Can you clarify (the output below that was 2 mons, not 0.2) ?

Thanks

On Fri, Apr 2, 2021 at 4:58 PM Bruce Momjian <bruce@momjian.us> wrote:

Show quoted text

On Fri, Apr 2, 2021 at 02:00:03PM -0700, John W Higgins wrote:

On Fri, Apr 2, 2021 at 11:05 AM Bruce Momjian <bruce@momjian.us> wrote:
While maybe there is an argument to fixing the negative/positive

rounding issue

- there is no way this gets solved without breaking the current

implementation

select interval '0.3 years' + interval '0.4 years' - interval '0.7

years' +

interval '0.1 years' should not equal 0 but it certainly does.

My new code returns 0.2 months for this, not zero:

SELECT interval '0.3 years' + interval '0.4 years' -
interval '0.7 years' + interval '0.1 years';
?column?
----------
2 mons

which is also wrong since:

SELECT interval '0.1 years';
interval
----------
1 mon

Unless we take the concept of 0.3 years = 3 months and move to something

along

the lines of

1 year = 360 days
1 month = 30 days

so therefore

0.3 years = 360 days * 0.3 = 108 days = 3 months 18 days
0.4 years = 360 days * 0.4 = 144 days = 4 months 24 days
0.7 years = 360 days * 0.7 = 252 days = 8 months 12 days

Then, and only if we don't go to any more than tenths of a year, does

the math

work. Probably this should resolve down to seconds and then work

backwards -

but unless we're looking at breaking the entire way it currently resolves
things - I don't think this is of much value.

Doing math on intervals is like doing math on rounded numbers - there is

always

going to be a pile of issues because the level of precision just is not

good

enough.

I think the big question is what units do people want with fractional
values. I have posted a follow-up email that spills only for one unit,
which I think is the best approach.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

#13Bruce Momjian
bruce@momjian.us
In reply to: Bruce Momjian (#8)
hackersgeneral
Re: Have I found an interval arithmetic bug?

On Fri, Apr 2, 2021 at 07:47:32PM -0400, Bruce Momjian wrote:

I have modified the patch to prevent partial months from creating
partial hours/minutes/seconds, so the output is now at least consistent
based on the three units:

SELECT ('6.54321 years'::interval)::text as i;
i
----------------
6 years 7 mons

SELECT ('6.54321 months'::interval)::text as i;
i
----------------
6 mons 16 days

SELECT ('876.54321 days'::interval)::text as i;
i
-----------------------
876 days 13:02:13.344

Partial years keeps it in months, partial months takes it to days, and
partial days take it to hours/minutes/seconds. This seems like an
improvement.

This also changes the regression test output, I think for the better:

SELECT INTERVAL '1.5 weeks';
i
------------------
- 10 days 12:00:00
+ 10 days

The new output is less precise, but probably closer to what the user
wanted.

Thinking some more about this, the connection between months and days is
very inaccurate, 30 days/month, but the connection between days and
hours/minutes/seconds is pretty accurate, except for leap days.
Therefore, returning "10 days 12:00:00" is in many ways better, but
returning hours/minutes/seconds for fractional months is very arbitrary
and suggests an accuracy that doesn't exist. However, I am afraid that
trying to enforce that distinction in the Postgres behavior would appear
very arbitrary, so what I did above is proabably the best I can do.
Here is another example of what we have:

SELECT INTERVAL '1.5 years';
interval
---------------
1 year 6 mons

SELECT INTERVAL '1.5 months';
interval
---------------
1 mon 15 days

SELECT INTERVAL '1.5 weeks';
interval
----------
10 days

SELECT INTERVAL '1.5 days';
interval
----------------
1 day 12:00:00

SELECT INTERVAL '1.5 hours';
interval
----------
01:30:00

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

#14Bryn Llewellyn
bryn@yugabyte.com
In reply to: Bruce Momjian (#10)
hackersgeneral
Re: Have I found an interval arithmetic bug?

bruce@momjian.us wrote:

I have just posted a new version of the patch which I think covers all the right areas.

I found the relevant email from you to pgsql-hackers here:

/messages/by-id/20210402234732.GA29125@momjian.us

You said:

I have modified the patch to prevent partial months from creating partial hours/minutes/seconds… Partial years keeps it in months, partial months takes it to days, and partial days take it to hours/minutes/seconds. This seems like an improvement.

I have written some PL/pgSQL code that faithfully emulates the behavior that I see in my present vanilla PostgreSQL Version 13.2 system in a wide range of tests. This is the key part:

m1 int not null := trunc(p.mo);
m_remainder numeric not null := p.mo - m1::numeric;
m int not null := trunc(p.yy*12) + m1;

d_real numeric not null := p.dd + m_remainder*30.0;
d int not null := floor(d_real);
d_remainder numeric not null := d_real - d::numeric;

s numeric not null := d_remainder*24.0*60.0*60.0 +
p.hh*60.0*60.0 +
p.mi*60.0 +
p.ss;
begin
return (m, d, s)::modeled_interval_t;
end;

These quantities:

p.yy, p.mo, p.dd, p.hh, p.mi, and p.ss

are the user’s parameterization. All are real numbers. Because non-integral values for years, months, days, hours, and minutes are allowed when you specify a value using the ::interval typecast, my reference doc must state the rules. I would have struggled to express these rules in prose—especially given the use both of trunc() and floor(). I would have struggled more to explain what requirements these rules meet.

I gather that by the time YugabyteDB has adopted your patch, my PL/pgSQL will no longer be a correct emulation. So I’ll re-write it then.

I intend to advise users always to constrain the values, when they specify an interval value explicitly, so the the years, months, days, hours, and minutes are integers. This is, after all, the discipline that the make_interval() built-in imposes. So I might recommend using only that.

#15Bruce Momjian
bruce@momjian.us
In reply to: Bryn Llewellyn (#14)
hackersgeneral
Re: Have I found an interval arithmetic bug?

On Fri, Apr 2, 2021 at 05:50:59PM -0700, Bryn Llewellyn wrote:

are the user’s parameterization. All are real numbers. Because non-integral
values for years, months, days, hours, and minutes are allowed when you specify
a value using the ::interval typecast, my reference doc must state the rules. I
would have struggled to express these rules in prose—especially given the use
both of trunc() and floor(). I would have struggled more to explain what
requirements these rules meet.

The fundamental issue is that while months, days, and seconds are
consistent in their own units, when you have to cross from one unit to
another, it is by definition imprecise, since the interval is not tied
to a specific date, with its own days-of-the-month and leap days and
daylight savings time changes. It feels like it is going to be
imprecise no matter what we do.

Adding to this is the fact that interval values are stored in C 'struct
tm' defined in libc's ctime(), where months are integers, so carrying
around non-integer month values until we get a final result would add a
lot of complexity, and complexity to a system that is by definition
imprecise, which doesn't seem worth it.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

#16Zhihong Yu
zyu@yugabyte.com
In reply to: Zhihong Yu (#12)
hackersgeneral
Re: Have I found an interval arithmetic bug?

Hi,
I got a local build with second patch where:

yugabyte=# SELECT interval '0.3 years' + interval '0.4 years' -
interval '0.7 years';
?column?
----------
1 mon

I think the outcome is a bit unintuitive (I would expect result close to 0).

Cheers

On Fri, Apr 2, 2021 at 5:07 PM Zhihong Yu <zyu@yugabyte.com> wrote:

Show quoted text

Hi,
bq. My new code returns 0.2 months for this, not zero

Can you clarify (the output below that was 2 mons, not 0.2) ?

Thanks

On Fri, Apr 2, 2021 at 4:58 PM Bruce Momjian <bruce@momjian.us> wrote:

On Fri, Apr 2, 2021 at 02:00:03PM -0700, John W Higgins wrote:

On Fri, Apr 2, 2021 at 11:05 AM Bruce Momjian <bruce@momjian.us> wrote:
While maybe there is an argument to fixing the negative/positive

rounding issue

- there is no way this gets solved without breaking the current

implementation

select interval '0.3 years' + interval '0.4 years' - interval '0.7

years' +

interval '0.1 years' should not equal 0 but it certainly does.

My new code returns 0.2 months for this, not zero:

SELECT interval '0.3 years' + interval '0.4 years' -
interval '0.7 years' + interval '0.1 years';
?column?
----------
2 mons

which is also wrong since:

SELECT interval '0.1 years';
interval
----------
1 mon

Unless we take the concept of 0.3 years = 3 months and move to

something along

the lines of

1 year = 360 days
1 month = 30 days

so therefore

0.3 years = 360 days * 0.3 = 108 days = 3 months 18 days
0.4 years = 360 days * 0.4 = 144 days = 4 months 24 days
0.7 years = 360 days * 0.7 = 252 days = 8 months 12 days

Then, and only if we don't go to any more than tenths of a year, does

the math

work. Probably this should resolve down to seconds and then work

backwards -

but unless we're looking at breaking the entire way it currently

resolves

things - I don't think this is of much value.

Doing math on intervals is like doing math on rounded numbers - there

is always

going to be a pile of issues because the level of precision just is not

good

enough.

I think the big question is what units do people want with fractional
values. I have posted a follow-up email that spills only for one unit,
which I think is the best approach.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

#17Isaac Morland
isaac.morland@gmail.com
In reply to: Zhihong Yu (#16)
hackersgeneral
Re: Have I found an interval arithmetic bug?

On Fri, 2 Apr 2021 at 21:08, Zhihong Yu <zyu@yugabyte.com> wrote:

Hi,
I got a local build with second patch where:

yugabyte=# SELECT interval '0.3 years' + interval '0.4 years' -
interval '0.7 years';
?column?
----------
1 mon

I think the outcome is a bit unintuitive (I would expect result close to
0).

That's not fundamentally different from this:

odyssey=> select 12 * 3/10 + 12 * 4/10 - 12 * 7/10;
?column?
----------
-1
(1 row)

odyssey=>

And actually the result is pretty close to 0. I mean it’s less than 0.1
year.

I wonder if it might have been better if only integers had been accepted
for the components? If you want 0.3 years write 0.3 * '1 year'::interval.
But changing it now would be a pretty significant backwards compatibility
break.

There's really no avoiding counterintuitive behaviour though. Look at this:

odyssey=> select 0.3 * '1 year'::interval + 0.4 * '1 year'::interval - 0.7
* '1 year'::interval;
?column?
------------------
-1 mons +30 days
(1 row)

odyssey=> select 0.3 * '1 year'::interval + 0.4 * '1 year'::interval - 0.7
* '1 year'::interval = '0';
?column?
----------
t
(1 row)

odyssey=>

In other words, doing the “same” calculation but with multiplying 1 year
intervals by floats to get the values to add, you end up with an interval
that while not identical to 0 does compare equal to 0. So very close to 0;
in fact, as close to 0 as you can get without actually being identically 0.

#18Zhihong Yu
zyu@yugabyte.com
In reply to: Isaac Morland (#17)
hackersgeneral
Re: Have I found an interval arithmetic bug?

Hi,
The mix of interval and comparison with float is not easy to interpret. See
the following (I got 0.0833 since the result for interval '0.3 years' +
interval '0.4 years' - ... query was 1 month and 1/12 ~= 0.0833).

yugabyte=# select 0.3 * '1 year'::interval + 0.4 * '1 year'::interval - 0.7
* '1 year'::interval = '0.0833 year'::interval;
?column?
----------
f

As long as Bruce's patch makes improvements over the current behavior, I
think that's fine.

Cheers

On Fri, Apr 2, 2021 at 6:24 PM Isaac Morland <isaac.morland@gmail.com>
wrote:

Show quoted text

On Fri, 2 Apr 2021 at 21:08, Zhihong Yu <zyu@yugabyte.com> wrote:

Hi,
I got a local build with second patch where:

yugabyte=# SELECT interval '0.3 years' + interval '0.4 years' -
interval '0.7 years';
?column?
----------
1 mon

I think the outcome is a bit unintuitive (I would expect result close to
0).

That's not fundamentally different from this:

odyssey=> select 12 * 3/10 + 12 * 4/10 - 12 * 7/10;
?column?
----------
-1
(1 row)

odyssey=>

And actually the result is pretty close to 0. I mean it’s less than 0.1
year.

I wonder if it might have been better if only integers had been accepted
for the components? If you want 0.3 years write 0.3 * '1 year'::interval.
But changing it now would be a pretty significant backwards compatibility
break.

There's really no avoiding counterintuitive behaviour though. Look at this:

odyssey=> select 0.3 * '1 year'::interval + 0.4 * '1 year'::interval - 0.7
* '1 year'::interval;
?column?
------------------
-1 mons +30 days
(1 row)

odyssey=> select 0.3 * '1 year'::interval + 0.4 * '1 year'::interval - 0.7
* '1 year'::interval = '0';
?column?
----------
t
(1 row)

odyssey=>

In other words, doing the “same” calculation but with multiplying 1 year
intervals by floats to get the values to add, you end up with an interval
that while not identical to 0 does compare equal to 0. So very close to 0;
in fact, as close to 0 as you can get without actually being identically 0.

#19Bruce Momjian
bruce@momjian.us
In reply to: Zhihong Yu (#16)
hackersgeneral
Re: Have I found an interval arithmetic bug?

On Fri, Apr 2, 2021 at 06:11:08PM -0700, Zhihong Yu wrote:

Hi,
I got a local build with second patch where:

yugabyte=# SELECT �interval '0.3 years' + interval '0.4 years' -
� � � � � � � � interval '0.7 years';
�?column?
----------
�1 mon

I think the outcome is a bit unintuitive (I would expect result�close�to 0).

Uh, the current code returns:

SELECT interval '0.3 years' + interval '0.4 years' - interval '0.7 years';
?column?
----------
-1 mon

and with the patch it is:

SELECT interval '0.3 years' + interval '0.4 years' - interval '0.7 years';
?column?
----------
1 mon

What it isn't, is zero months, which is the obviously ideal answer.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

#20Bruce Momjian
bruce@momjian.us
In reply to: Zhihong Yu (#18)
hackersgeneral
Re: Have I found an interval arithmetic bug?

On Fri, Apr 2, 2021 at 07:06:08PM -0700, Zhihong Yu wrote:

Hi,
The mix of interval and comparison with float is not easy to interpret. See the
following (I got 0.0833 since the result for�interval '0.3 years' + interval
'0.4 years' - ... query was 1 month and 1/12 ~= 0.0833).

yugabyte=# select 0.3 * '1 year'::interval + 0.4 * '1 year'::interval - 0.7 *
'1 year'::interval = '0.0833 year'::interval;
�?column?
----------
�f

As long as Bruce's patch makes improvements over the current behavior, I think
that's fine.

I wish I could figure out how to improve it any futher. What is odd is
that I have never seen this reported as a problem before. I plan to
apply this early next week for PG 14.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

#21Zhihong Yu
zyu@yugabyte.com
In reply to: Bruce Momjian (#20)
hackersgeneral
#22Bruce Momjian
bruce@momjian.us
In reply to: Zhihong Yu (#21)
hackersgeneral
#23Justin Pryzby
pryzby@telsasoft.com
In reply to: Bruce Momjian (#22)
hackersgeneral
#24Bruce Momjian
bruce@momjian.us
In reply to: Justin Pryzby (#23)
hackersgeneral
#25Justin Pryzby
pryzby@telsasoft.com
In reply to: Bruce Momjian (#24)
hackersgeneral
#26Bruce Momjian
bruce@momjian.us
In reply to: Justin Pryzby (#25)
hackersgeneral
#27Bryn Llewellyn
bryn@yugabyte.com
In reply to: Bruce Momjian (#26)
hackersgeneral
#28Bruce Momjian
bruce@momjian.us
In reply to: Bryn Llewellyn (#27)
hackersgeneral
#29Bryn Llewellyn
bryn@yugabyte.com
In reply to: Bruce Momjian (#28)
hackersgeneral
#30Bruce Momjian
bruce@momjian.us
In reply to: Bruce Momjian (#24)
hackersgeneral
#31Bryn Llewellyn
bryn@yugabyte.com
In reply to: Bruce Momjian (#30)
hackersgeneral
#32Zhihong Yu
zyu@yugabyte.com
In reply to: Bruce Momjian (#30)
hackersgeneral
#33Zhihong Yu
zyu@yugabyte.com
In reply to: Zhihong Yu (#32)
hackersgeneral
#34Zhihong Yu
zyu@yugabyte.com
In reply to: Zhihong Yu (#33)
hackersgeneral
#35Bruce Momjian
bruce@momjian.us
In reply to: Zhihong Yu (#34)
hackersgeneral
#36Bryn Llewellyn
bryn@yugabyte.com
In reply to: Bruce Momjian (#35)
hackersgeneral
#37Bruce Momjian
bruce@momjian.us
In reply to: Bryn Llewellyn (#36)
hackersgeneral
#38Tom Lane
tgl@sss.pgh.pa.us
In reply to: Bruce Momjian (#37)
hackersgeneral
#39Bruce Momjian
bruce@momjian.us
In reply to: Tom Lane (#38)
hackersgeneral
#40Bryn Llewellyn
bryn@yugabyte.com
In reply to: Tom Lane (#38)
hackersgeneral
#41Bryn Llewellyn
bryn@yugabyte.com
In reply to: Bruce Momjian (#39)
hackersgeneral
#42Bruce Momjian
bruce@momjian.us
In reply to: Bryn Llewellyn (#41)
hackersgeneral
#43Zhihong Yu
zyu@yugabyte.com
In reply to: Bruce Momjian (#37)
hackersgeneral
#44Bryn Llewellyn
bryn@yugabyte.com
In reply to: Bruce Momjian (#42)
hackersgeneral
#45Bruce Momjian
bruce@momjian.us
In reply to: Zhihong Yu (#34)
hackersgeneral
#46Zhihong Yu
zyu@yugabyte.com
In reply to: Bryn Llewellyn (#44)
hackersgeneral
#47Zhihong Yu
zyu@yugabyte.com
In reply to: Bruce Momjian (#45)
hackersgeneral
#48Bruce Momjian
bruce@momjian.us
In reply to: Zhihong Yu (#47)
hackersgeneral
#49Daniel Gustafsson
daniel@yesql.se
In reply to: Bryn Llewellyn (#36)
hackersgeneral
#50Zhihong Yu
zyu@yugabyte.com
In reply to: Bruce Momjian (#48)
hackersgeneral
#51Bruce Momjian
bruce@momjian.us
In reply to: Daniel Gustafsson (#49)
hackersgeneral
#52Zhihong Yu
zyu@yugabyte.com
In reply to: Bruce Momjian (#51)
hackersgeneral
#53Zhihong Yu
zyu@yugabyte.com
In reply to: Zhihong Yu (#52)
hackersgeneral
#54Bruce Momjian
bruce@momjian.us
In reply to: Zhihong Yu (#53)
hackersgeneral
#55Zhihong Yu
zyu@yugabyte.com
In reply to: Bruce Momjian (#54)
hackersgeneral
#56Bruce Momjian
bruce@momjian.us
In reply to: Zhihong Yu (#55)
hackersgeneral
#57Zhihong Yu
zyu@yugabyte.com
In reply to: Bruce Momjian (#56)
hackersgeneral
#58Bruce Momjian
bruce@momjian.us
In reply to: Zhihong Yu (#57)
hackersgeneral
#59Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Bruce Momjian (#58)
hackersgeneral
#60Tom Lane
tgl@sss.pgh.pa.us
In reply to: Dean Rasheed (#59)
hackersgeneral
#61Bryn Llewellyn
bryn@yugabyte.com
In reply to: Tom Lane (#60)
hackersgeneral
#62Tom Lane
tgl@sss.pgh.pa.us
In reply to: Bryn Llewellyn (#61)
hackersgeneral
#63Bryn Llewellyn
bryn@yugabyte.com
In reply to: Dean Rasheed (#59)
hackersgeneral
#64Bruce Momjian
bruce@momjian.us
In reply to: Tom Lane (#62)
hackersgeneral
#65Bryn Llewellyn
bryn@yugabyte.com
In reply to: Bruce Momjian (#64)
hackersgeneral
#66Bruce Momjian
bruce@momjian.us
In reply to: Bryn Llewellyn (#65)
hackersgeneral
#67Zhihong Yu
zyu@yugabyte.com
In reply to: Bruce Momjian (#66)
hackersgeneral
#68Zhihong Yu
zyu@yugabyte.com
In reply to: Zhihong Yu (#67)
hackersgeneral
#69Bruce Momjian
bruce@momjian.us
In reply to: Zhihong Yu (#68)
hackersgeneral
#70Bryn Llewellyn
bryn@yugabyte.com
In reply to: Bruce Momjian (#69)
hackersgeneral
#71Bruce Momjian
bruce@momjian.us
In reply to: Bryn Llewellyn (#70)
hackersgeneral
#72Bryn Llewellyn
bryn@yugabyte.com
In reply to: Bruce Momjian (#71)
hackersgeneral
#73Bruce Momjian
bruce@momjian.us
In reply to: Bryn Llewellyn (#72)
hackersgeneral
#74Bruce Momjian
bruce@momjian.us
In reply to: Bruce Momjian (#15)
hackersgeneral
#75Tom Lane
tgl@sss.pgh.pa.us
In reply to: Bruce Momjian (#74)
hackersgeneral
#76Bruce Momjian
bruce@momjian.us
In reply to: Tom Lane (#75)
hackersgeneral
#77Bryn Llewellyn
bryn@yugabyte.com
In reply to: Bruce Momjian (#76)
hackersgeneral
#78John W Higgins
wishdev@gmail.com
In reply to: Bryn Llewellyn (#77)
hackersgeneral
#79Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: John W Higgins (#78)
hackersgeneral
#80John W Higgins
wishdev@gmail.com
In reply to: Dean Rasheed (#79)
hackersgeneral
#81Bruce Momjian
bruce@momjian.us
In reply to: Dean Rasheed (#79)
hackersgeneral
#82Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#75)
hackersgeneral
#83Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#82)
hackersgeneral
#84Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#83)
hackersgeneral
#85Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#84)
hackersgeneral
#86Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#85)
hackersgeneral
#87Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#86)
hackersgeneral
#88Bruce Momjian
bruce@momjian.us
In reply to: Bruce Momjian (#81)
hackersgeneral
#89Bruce Momjian
bruce@momjian.us
In reply to: Bruce Momjian (#88)
hackersgeneral
#90Tom Lane
tgl@sss.pgh.pa.us
In reply to: Bruce Momjian (#89)
hackersgeneral
#91Bruce Momjian
bruce@momjian.us
In reply to: Tom Lane (#90)
hackersgeneral
#92Robert Haas
robertmhaas@gmail.com
In reply to: Bruce Momjian (#91)
hackersgeneral
#93Bruce Momjian
bruce@momjian.us
In reply to: Robert Haas (#92)
hackersgeneral
#94Robert Haas
robertmhaas@gmail.com
In reply to: Bruce Momjian (#93)
hackersgeneral
#95Tom Lane
tgl@sss.pgh.pa.us
In reply to: Bruce Momjian (#93)
hackersgeneral
#96Bruce Momjian
bruce@momjian.us
In reply to: Robert Haas (#94)
hackersgeneral
#97Bruce Momjian
bruce@momjian.us
In reply to: Tom Lane (#95)
hackersgeneral
#98Tom Lane
tgl@sss.pgh.pa.us
In reply to: Bruce Momjian (#97)
hackersgeneral
#99Bruce Momjian
bruce@momjian.us
In reply to: Bruce Momjian (#91)
hackersgeneral