added conversion between interval and DiffTime #115

Closed
wants to merge 1 commit into
from

Projects

None yet

3 participants

@kqr
kqr commented Jul 11, 2014

I needed to be able to access and store interval values in Postgres for a personal project, so I added conversion between interval and Data.Time.DiffTime. You might be interested in my code.

@sopvop
Contributor
sopvop commented Jul 11, 2014

In ToField instance you use double, however interval in postgresql can handle finer resolution than that (it uses 12 bytes).

Also, I myself had trouble with sending interval with seconds > maxBound :: Int32, postgres choked on parsing, guess interval parser does not support fractional seconds with significand bigger than int32.

I think it would be better to use Data.Fixed.divMod' to split out days, and send "interval ' 12 days 12.34 seconds'" to postgres.

@lpsmith
Owner
lpsmith commented Jul 11, 2014

This conversion isn't sound: for starters, with postgresql's interval type, a day isn't necessarily 24 hours:

$ psql 
psql (9.3.4)
Type "help" for help.

lpsmith=> set timezone to 'America/New_York';
SET
lpsmith=> select '2014-03-08 12:00:00'::timestamptz;
      timestamptz       
------------------------
 2014-03-08 12:00:00-05
(1 row)

lpsmith=> select '2014-03-08 12:00:00-05'::timestamptz + '1 day'::interval;
        ?column?        
------------------------
 2014-03-09 12:00:00-04
(1 row)

lpsmith=> select '2014-03-09 12:00:00-04'::timestamptz - '2014-03-08 12:00:00-05'::timestamptz;
 ?column? 
----------
 23:00:00
(1 row)

Most days are 24 hours, and most exceptions are 23 or 25 hours, but there's also a bunch of oddball days you can find by perusing the Olson tz database, when a locale switched time standards. Similarly, a month is not a set number of seconds, but rather 28, 29, 30, or 31 days.

I'd be delighted to add support for the interval type; unfortunately, high-fidelity support is a bit more subtle than it first appears. In particular, DiffTime is not really up to the task. If you find these instances useful, by all means, use them; you don't even have to modify postgresql-simple to use them. But as something to include in vanilla postgresql-simple, it's definitely not ready.

Also, to deal with the issues that sopvop brought up regarding precision, you might consider using Data.Fixed instead.

@kqr
kqr commented Jul 11, 2014

I had a hard time finding detailed information about the interval type, so this consists of a lot of guessing on my part. I realised the interval type is difficult, but I probably underestimated just how difficult it is.

I might look further into this in the future, but for the time being my instance is good enough for me so I'll roll with it. Thanks for the feedback!

@kqr kqr closed this Jul 11, 2014
@lpsmith
Owner
lpsmith commented Jul 11, 2014

Well, representing the interval type probably isn't too bad, all you really have to to is accurately represent all the components (e.g. months, days, hours, seconds). The tricky part is if you actually want to compute with those intervals in Haskell-land.

Although figuring out what the components are might take a little bit of effort; e.g. I think a week is always 7 days, and an hour is 3600 seconds, so perhaps you wouldn't need to explicitly represent weeks and hours, but I'm not sure.

(With respect to hours, I'm not sure how they interact with leap seconds, but on the other hand postgresql's time type doesn't take leap seconds into account.)

Time is pretty tricky, unfortunately.

@kqr
kqr commented Jul 11, 2014

Yeah, what I read was (my bold highlighting)

In the verbose input format, and in some fields of the more compact input formats, 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.

on the Postgres documentation chaptor on time types. That's why I decided to go with 12 months * 30 days * 24 hours instead of counting 365 (or for that matter 365.2425) days in a year or anything else. Though maybe those conversion factors are only used with decimal months/days, and not for anything else.

I additionally used a Double because

time, timestamp, and interval accept an optional precision value p which specifies the number of fractional digits retained in the seconds field. By default, there is no explicit bound on precision. The allowed range of p is from 0 to 6 for the timestamp and interval types.

where again I wasn't sure how to interpret it, but it sounded sort of like 6 decimals is the maximum precision of intervals. (Though now that I think about it, even a Double can't guarantee 6 decimals, although for any sane values in my particular application it happens to be enough.)

@lpsmith
Owner
lpsmith commented Jul 12, 2014

Well, I don't know much about the representation of the interval type, but I know the default representation of the timestamp (without tweaking compile-time options) since version 8.4 or 9.0 has been a 8-byte signed integer of microseconds since 2000-01-01 00:00:00+00, with the minimum time representing -infinity and the maximum time representing +infinity. (nulls are represented by a bit external to the representation of the value itself.) You can distinguish this representation from the older floating-point based representation by running SHOW integer_datetimes.

I don't know if there's a similar difference with the interval representation or not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment