[local-time-devel] encode-timestamp : wrong offset at DST transition

Siebe de Vos Siebe at de-vos.de
Wed May 25 09:31:55 UTC 2011


Hi Local-Time,

Correctly parsing and printing a local-time given a timezone is an essential 
feature for our application and I am happy I found that local-time can offer 
this.

But there are some problems with using timezones. From the list history it is 
clear that this is not a trivial issue. I will point to some obvious 
mismatches between documentation and actual behaviour and then describe a bug 
and a solution for the bug.

On a general level: in my opinion the philosophy of local-time is unclear with 
regard to time-zones. It seems to be the only lisp library around that is 
integrated with zoneinfo information. The date formatting of a timestamp for a 
symbolic timezone works fine. However, parsing and encoding of a timestamp 
expressed in local time historically required an explicit offset. Now 
timezones are added, but not with enough care: not even the documentation 
mentions the API facilities. Without touching the hairy issue of time 
arithmetic and timezones -- using the zoneinfo library when encoding a 
timestamp can and should be handled correctly. And it will make local-time 
really live up to its name!


1. Arguments of timestamp-subtimezone

"Function: timestamp-subtimezone timestamp &optional timezone"

LOCAL-TIME: (timestamp-subtimezone (now))
Error: TIMESTAMP-SUBTIMEZONE got 1 arg, wanted 2 args.

Timezone is not optional in the implementation.

2. Arguments of encode-timestamp

"Function: encode-timestamp nsec sec minute hour day month year &optional 
offset"

LOCAL-TIME: (encode-timestamp 0 0 0 2 26 3 2011 7200)
Error: &key list isn't even.

Offset is a keyword argument in the implementation.

3. Default offset used by encode-timestamp

encode-timestamp : "The offset is the number of seconds offset from UTC of the 
locale. The offset will be set by default to the lisp implementation's default 
offset at the current time."

LOCAL-TIME: (timestamp-subtimezone (now) *default-timezone*)
7200
T
"CEST"

The "implementation's default offset at the current time" is 7200.

LOCAL-TIME: (encode-timestamp 0 0 0 2 26 3 2011)
@2011-03-26T02:00:00.000000+01:00
LOCAL-TIME: (encode-timestamp 0 0 0 2 26 3 2011 :offset 7200)
@2011-03-26T01:00:00.000000+01:00

Contrary to the documentation the two values are not the same. Better "The 
implementation uses the offset of the provided timezone (if not provided, the 
default timezone) valid at the local time being encoded."

4. encode-timestamp fails near DST transitions

When my interpretation of encode-timestamp in point 3 is correct the following 
should not happen:

I'm in a CET (+01:00) locale, currently CEST (+02:00). The last transition was 
on March 27, 2011 when the local time stepped from 01:59:59 to 03:00:00.

CL-USER: (local-time:encode-timestamp 0 0 0 0 27 3 2011)
@2011-03-27T00:00:00.000000+01:00		; Correct
 CL-USER: (local-time:encode-timestamp 0 0 0 1 27 3 2011)
@2011-03-27T00:00:00.000000+01:00		; Wrong, should be T01:00:00
 CL-USER:(local-time:encode-timestamp 0 0 0 2 27 3 2011)
@2011-03-27T01:00:00.000000+01:00		; Wrong nor right: This is the hour
				; lost in the transition. Still more wrong then right, I would say.
CL-USER(32): (local-time:encode-timestamp 0 0 0 3 27 3 2011)
@2011-03-27T03:00:00.000000+02:00		; Correct

The cause of the wrong value is in the function %guess-offset: it takes 
seconds and day and turns them into a unix-time to search the zoneinfo 
transition table. This means that the provided day and seconds are treated as 
having a 0 offset from UTS. However, in the context of encode-timestamp they 
should be treated in the given timezone.

ANALYSIS

The mail archives include several attempts to arrive at a correct offset given 
a timezone and a date and time for that timezone. Thomas Munro (10/05/2009) 
proposed a solution, which also appears in a patch from Antoni Piotr Oleksicki 
(28/07/2009):

(let ((timestamp (or into (make-timestamp))))
        (loop
           for subtimezone across (timezone-subzones timezone) do
             (encode-timestamp nsec sec minute hour day month year
                               :offset (subzone-offset subtimezone)
                               :into timestamp)
             (if (= (timestamp-subtimezone timestamp timezone)
                    (subzone-offset subtimezone))
                 (return timestamp))
           finally
             (error "The requested local time is not valid")))

This has two apparent drawbacks:
- inefficent: the transition table is used after guessing
- inefficient: too much consing, too many timestamp to unix-time conversions 
(involving bignums), ...
- unfriendly: no useful fallback when the requested time is invalid

Then the 1.0.2 code shows %guess-offset. As stated above the unix-time is 
created without offset, whereas the offset of the timezone should be applied 
when converting to unix-time.

I modified %guess-offset, so that it searches for the transition boundary a 
second time after applying the offset, more or less like this:

(if (zerop offset)
    offset
  (let ((transition-position-better
	 (transition-position (- unix-time offset)
			      (timezone-transitions zone))))
    (if (eql transition-position transition-position-better)
	offset
      (subzone-offset
       (elt timezone-subzones
	    (elt timezone-indexes transition-position-better))))))

This works for me. Before making a patch, the following should be considered:
- is this compatible with the spirit?
- are there situations where more iterations are required?
- what to do when both positions are wrong, like T02:30:00 at the forward 
transition from CET to CEST? Suggestion: assume that the requestor forgot to 
change the clock, so use the subzone after the transition.
- what to do if both apply, like for T02:30:00 at the backward transition from 
CEST to CET? Suggestion: assume the new time.
- better use of the ordered transition table structure: you don't have to 
search the second try but only check the transition before or after (depending 
on sign of offset) transition-position
- what is the behaviour in other Olsen based implementations (POSIX, Java)?
- the improvement is based on what zic has done for us and not on a real 
understanding of the zone rules

Thanks for your feedback!

Bye,
Siebe




More information about the local-time-devel mailing list