RE: [Haskell-cafe] Can't do basic time operations with System.Time

On 20 January 2005 22:12, John Goerzen wrote:
I have a simple desire. I have a string that I need to parse as a date/time string in the local timezone, then convert it to standard seconds-since-epoch format. This is trivial in C, Perl, Python, etc. but seems impossible to do reliably in Haskell. I'm hoping someone can tell me where I've gone wrong.
My initial idea was to parse the string, generate a CalendarTime object, use toClockTime to convert it to seconds-since-epoch, and then grab the value out of that. There were some problems, though: what to put in ctTZ and ctIsDST? I initially loaded them with 0 and False, though that turned out to be clearly wrong.
My next idea was to take the ctTZ result from toCalendarTime, then use that for all the CalendarTime objects I'd generate. That was wrong, too, during daylight savings time. I don't think I should have to re-implement the algorithms for determining whether a given date is under DST worldwide.
What I really need is just a wrapper around mktime() that just calls mktime and doesn't do anything else. All the post-processing that System.Time is doing is what's causing me trouble. I'd just like it to ignore ctTZ, ctTZName, and ctIsDST. That would yield behavior identical to the C library.
If you set ctTZ to the UTC offset outside DST, and set ctIsDST to False, then toClockTime will do the DST calculation for you. ctTZ is the actual UTC offset (ie. it is adjusted for DST). Hence, toClockTime ignores ctIsDST when performing the conversion: it isn't necessary. I can't tell what's wrong here, if anything (general brokenness in the System.Time API notwithstanding). Ah *lightbuilb* - I think I see: you want to take a calendar time without any TZ information, assume it is a time at the current geographical location, and produce a CalendarTime for that. Hmm, it looks like the current API doesn't support that - toClockTime assumes you know the timezone. That's a shame. There's one way that's almost correct: take your initial calendar time, set ctTZ to your non-DST UTC offset, then call toClockTime and toCalendarTime on the result, and you'll get an appropriate ctTZ for that time. Plug the new ctTZ into the original time, and toClockTime again (you can shortcut this by adjusting the ClockTime directly). I'll leave it up to you how to handle the boundaries properly :-) Cheers, Simon

On Fri, Jan 21, 2005 at 01:56:23PM -0000, Simon Marlow wrote:
I can't tell what's wrong here, if anything (general brokenness in the System.Time API notwithstanding). Ah *lightbuilb* - I think I see: you want to take a calendar time without any TZ information, assume it is a time at the current geographical location, and produce a CalendarTime for that. Hmm, it looks like the current API doesn't support that - toClockTime assumes you know the timezone. That's a shame.
That is exactly correct. The other point that I discovered after reading the Haskell library report is that ClockTime is supposed to be an opaque data type; that is, the trick of getting the seconds out of the (TOD Integer Integer) in GHC is non-portable. (Reference: http://www.haskell.org/onlinelibrary/time.html) So I wound up writing one myself:
There's one way that's almost correct: take your initial calendar time, set ctTZ to your non-DST UTC offset, then call toClockTime and toCalendarTime on the result, and you'll get an appropriate ctTZ for that time. Plug the new ctTZ into the original time, and toClockTime again (you can shortcut this by adjusting the ClockTime directly). I'll leave it up to you how to handle the boundaries properly :-)
I arrived at approximately the same conclusion last night and defined the boundaries as unimportant :-) I checked the following code into MissingH. I would be pleased if you could take it for fptools. I should note that I am using an Integer rather than an Int to represent seconds, since I think that is proper given the size of values we might be encountering. If you do take it for fptools, you could probably rewrite normalizeTimeDiff to use the very similar code in timeDiffToSecs (shameless almost-stealing here <g>) I've written a few unit tests for this and it seems to be doing the right thing (where the Right Thing is measured by what the C library is doing). Thanks, John module MissingH.Time( timelocal, timegm, timeDiffToSecs, epoch ) where import System.Time {- | January 1, 1970, midnight, UTC, represented as a CalendarTime. -} epoch :: CalendarTime epoch = CalendarTime { ctYear = 1970, ctMonth = January, ctDay = 1, ctHour = 0, ctMin = 0, ctSec = 0, ctPicosec = 0, ctWDay = Thursday, ctYDay = 0, ctTZName = "UTC", ctTZ = 0, ctIsDST = False} {- | Converts the specified CalendarTime (see System.Time) to seconds-since-epoch time. This conversion does respect the timezone specified on the input object. If you want a conversion from UTC, specify ctTZ = 0 and ctIsDST = False. When called like that, the behavior is equivolent to the GNU C function timegm(). Unlike the C library, Haskell's CalendarTime supports timezone information, so if such information is specified, it will impact the result. -} timegm :: CalendarTime -> Integer timegm ct = timeDiffToSecs (diffClockTimes (toClockTime ct) (toClockTime epoch)) {- | Converts the specified CalendarTime (see System.Time) to seconds-since-epoch format. The input CalendarTime is assumed to be the time as given in your local timezone. All timezone and DST fields in the object are ignored. This behavior is equivolent to the timelocal() and mktime() functions that C programmers are accustomed to. Please note that the behavior for this function during the hour immediately before or after a DST switchover may produce a result with a different hour than you expect. -} timelocal :: CalendarTime -> IO Integer timelocal ct = do guessct <- toCalendarTime guesscl let newct = ct {ctTZ = ctTZ guessct} return $ timegm newct where guesscl = toClockTime ct {- | Converts the given timeDiff to the number of seconds it represents. Uses the same algorithm as normalizeTimeDiff in GHC. -} timeDiffToSecs :: TimeDiff -> Integer timeDiffToSecs td = (fromIntegral $ tdSec td) + 60 * ((fromIntegral $ tdMin td) + 60 * ((fromIntegral $ tdHour td) + 24 * ((fromIntegral $ tdDay td) + 30 * ((fromIntegral $ tdMonth td) + 365 * (fromIntegral $ tdYear td)))))

John Goerzen writes:
timeDiffToSecs :: TimeDiff -> Integer timeDiffToSecs td = (fromIntegral $ tdSec td) + 60 * ((fromIntegral $ tdMin td) + 60 * ((fromIntegral $ tdHour td) + 24 * ((fromIntegral $ tdDay td) + 30 * ((fromIntegral $ tdMonth td) + 365 * (fromIntegral $ tdYear td)))))
I was wondering: Does this calculation account for leap years? Does it have to? As I see it, it's not possible to convert a TimeDiff accurately without knowing the point in time from which to "diff" from. How many days is "4 years from now"? It's almost certainly not 4*365 days. Uh, did I mention leap seconds? ;-) Peter

Peter Simons
I was wondering: Does this calculation account for leap years? Does it have to?
C itself leaves unspecified the question whether its time calculations take leap seconds into account. All other systems I know of ignore leap seconds: POSIX C, Common Lisp, Java, .NET. The NTP protocol also ignores leap seconds, i.e. its time is supposed to slow down near the transition. While supporting leap seconds is "more correct", it makes interoperability harder: Haskell would have a different view than the rest of the system about either the current calendar time or seconds since the Epoch. Another problem is that leap seconds are not known further in advance than about half of a year, so programs compiled using different versions of Haskell would have slightly different views about the current time. -- __("< Marcin Kowalczyk \__/ qrczak@knm.org.pl ^^ http://qrnik.knm.org.pl/~qrczak/

On 2005-01-21, Peter Simons
24 * ((fromIntegral $ tdDay td) + 30 * ((fromIntegral $ tdMonth td) + 365 * (fromIntegral $ tdYear td)))))
I was wondering: Does this calculation account for leap years? Does it have to?
I also wondered about all months being 30 days. But this exact algorithm is used in fptools' normalizeTimeDiff, and it seems to work out correctly, so I guess the timeDiff functions assume that every month has 30 days and every year has 365. When you make that assumption, leap seconds suddenly stop mattering :-) -- John
participants (4)
-
John Goerzen
-
Marcin 'Qrczak' Kowalczyk
-
Peter Simons
-
Simon Marlow