#+title: Observation of hysteresis in a GNU libc time conversion function #+begin_abstract The ~mktime~ function in GNU libc for specific arguments may possess properties similar to ferromagnetic materials. Dependence of returned value on arguments passed during earler calls gives evidences that it has hidden state. #+end_abstract I was experimenting with a code sample posted to the thread on support of time zones in Org. I noticed rather strange behavior of ~encode-time~ and thus underlying ~mktime~ function. At first I considered it as non-deterministic, but later I realized that time zone offset calculated earlier may affect following calls. I was aware that the function is not pure since it calls ~tzset~ to initialize time zone from the ~TZ~ environment variable and e.g. ~tzname~ global variable. It was a surprise that the function can not be considered even as a stable one. Passing same arguments may lead to different results. I am unsure whether it is intentional or at least known property. #+begin_src elisp :exports nil :results silent (require 'ob-shell) (require 'ob-gnuplot) #+end_src Let's take some backward time transition, so with ambiguous local time, where daylight saving time state is not changed and preferably is not in action. As a result it is impossible to disambiguate whether local time is before or after time jump. Attempt to specify ~dst~ as ~t~ (or set ~t.tm_isdst = 1~ for ~mktime~) will lead to an error. #+begin_src sh :results verbatim :exports both zdump -v Africa/Juba | grep 2021 #+end_src #+RESULTS: : Africa/Juba Sun Jan 31 20:59:59 2021 UT = Sun Jan 31 23:59:59 2021 EAT isdst=0 gmtoff=10800 : Africa/Juba Sun Jan 31 21:00:00 2021 UT = Sun Jan 31 23:00:00 2021 CAT isdst=0 gmtoff=7200 We may convert broken down local time representation around 23:30 local time for a sequence of time moments passing values to ~encode-time~ in increasing and decreasing their order. Under the hood the function calls the =mktime(3)= function. #+name: timestamp #+header: :exports code #+begin_src elisp :var tz="Africa/Juba" :var t0='(0 30 23 31 1 2021) (let* ((f (lambda (minutes) (float-time (encode-time (list (nth 0 t0) (+ (nth 1 t0) minutes) (nth 2 t0) (nth 3 t0) (nth 4 t0) (nth 5 t0) nil -1 tz))))) (dt '(-90 -60 -31 -30 -29 -15 0 15 29 30 31 60 90)) (values (mapcar (lambda (m) (cons m (funcall f m))) dt)) (ts0 (cdr (nth (/ (length values) 2) values)))) (mapcar (lambda (pair) (let ((m (car pair))) (list m (- (cdr pair) ts0) (- (funcall f m) ts0)))) (reverse values))) #+end_src #+RESULTS: timestamp | 90 | 9000.0 | 9000.0 | | 60 | 7200.0 | 7200.0 | | 31 | 5460.0 | 5460.0 | | 30 | 5400.0 | 5400.0 | | 29 | 1740.0 | 5340.0 | | 15 | 900.0 | 4500.0 | | 0 | 0.0 | 3600.0 | | -15 | -900.0 | 2700.0 | | -29 | -1740.0 | 1860.0 | | -30 | -1800.0 | 1800.0 | | -31 | -1860.0 | -1860.0 | | -60 | -3600.0 | -3600.0 | | -90 | -5400.0 | -5400.0 | #+begin_src gnuplot :file mktime-hyst.png :var data=timestamp set key bottom right set xlabel 'minutes' set ylabel 'UNIX epoch difference, seconds' set title 'Hysteresis in GNU libc mktime' plot data using 1:2 with lp title 'increasing', \ '' using 1:3 with lp title 'decreasing' #+end_src #+RESULTS: [[file:mktime-hyst.png]] So result for the same arguments may depend on previous calls. Likely it is due to a [[https://sourceware.org/git/?p=glibc.git;a=blob;f=time/mktime.c;h=94a4320e6ca9d935fc534991f9b57a2f1cc185de;hb=HEAD#l544][static variable]] used in the =mktime.c= file. Such a variable appeared in the commit [[https://sourceware.org/git/?p=glibc.git;a=commit;h=80fd73873b][80fd73873b]] #+begin_example Fri Sep 29 03:43:51 1995 Paul Eggert Rewrite mktime from scratch for performance, and for correctness in the presence of leap seconds. #+end_example Perhaps hidden internal state may be used to disambiguate local time values around backward time steps with unchanged daylight saving time. Unfortunately relying on such implementation details hardly can be considered as a robust approach.