Unix Epoch Seconds, revisited

Date:

2017-09-01

Intro

Unix Epoch Seconds are a commonly used time scale with computers. They start at 1970-01-01 00:00:00h UTC and increase eversince. At the time of this writing they have surpassed 1503689470. I decided to use them on the clocks to simplify handling of timezones. A time zone can then be implemented as an offset in seconds. This may not be the most simple method, but once working conversion routines are available, they can be used easily.

Unix Epoch Seconds have no concept of leap seconds, so this time scale is a little distorted occasionally.

Unix Epoch Seconds have traditionally been implemented as signed 32bit integer values. So there will be an instant, where this counter wraps to negative values — this is the cause of the so called year-2038 problem . My implementation uses an unsigned 32bit counter, so the problem moves out to 2106. In Detail:

N

Epoch seconds

Date_Time

2^31-1

2147483647

2038-01-19_03:14:07 UTC

2^32-1

4294967295

2106-02-07_06:28:15 UTC

Unix Epoch Seconds make for geeky displays, either in decimal (the value 1500000000 was reached not so long ago at 2017-07-14 02:40:00 UTC) or in binary: you can see the year 2038 rollover coming!

Design Decisions

  • the conversion routines are stupid, they count days accumulated over the full years since 1970, and add days, hours, minutes, and seconds along the way.

  • 2variables are used to hold the results

  • using them unsigned (which is just a decision of the programmer) gets rid of the year-2038 problem. Whether my hardware will see the year-2106 problem seems rather less likely.

This code has been published before on this site ( Date/Time to unix time and back ) including some test cases.

However, by its very nature a clock is indicating increasing instances in time, so calculating the same time spans over and over again seems like a bit of a waste. Thus I added some shortcuts.

  • the .short variants of ut>s and s>dt use a known starting point stored in _last_esec and _last_epoch. This point can be changed at the beginning of a new year, for example. NB the functions will fail if the point to be converted is before _last_esec.

Code Details

  • s>dt.short  ( d:EpochSeconds -- sec min hour day month year )

  • ut>s.short  ( sec min hour day month year -- d:EpochSeconds )

Putting it all together

The 2variable EsecOffset holds the offset of the current time zone in seconds. The offset is applied to the Esec counter and then converted to HMS counters and displayed:

variable   _last_epoch
2variable  _last_esec
#2017        Evalue   EE_last_epoch
#1483228800. 2Evalue  EE_last_esec

#include epochseconds.fs
2variable Esec
: ++Esec  ( -- )  Esec 2@  1. d+  Esec 2! ;
: .Esec   ( -- )  Esec 2@ ud. ;

...

: local.dt ( -- S M H d m Y )
  Esec 2@  EsecOffset 2@  d+  s>dt.short
;
: cd.localtime
  local.dt          \ -- S M H d m Y
  drop drop drop    \ -- S M H
  rot drop swap     \ -- H M
  >r #10 /mod swap  \ -- H.10 H.1
  r> #10 /mod swap  \ -- H.10 H.1 M.10 M.1
  #4 type.7seg      \ --
;
: job.min  ...
  cd.localtime
;
: init
  ...
  0. Esec    2!
  EE_last_epoch _last_epoch  !
  EE_last_esec  _last_esec  2!
  #3600. EsecOffset 2! \ UTC+1
  ...
;

References

The Code

  1\ 2014-10-13  ew
  2\
  3\ Written in 2014-2017 by Erich Wälde <erich.waelde@forth-ev.de>
  4\
  5\ To the extent possible under law, the author(s) have dedicated
  6\ all copyright and related and neighboring rights to this software
  7\ to the public domain worldwide. This software is distributed
  8\ without any warranty.
  9\
 10\ You should have received a copy of the CC0 Public Domain
 11\ Dedication along with this software. If not, see
 12\ <http://creativecommons.org/publicdomain/zero/1.0/>.
 13\
 14\ words
 15\     leapyear?   FIXME: there is an implementation in ewlib/timeup.fs as well
 16\     __Epoch     1970, constant
 17\     s>dt        ( d:EpochSeconds -- sec min hour day month year )
 18\     s>dt.short  ( d:EpochSeconds -- sec min hour day month year )
 19\     ut>s        ( sec min hour day month year -- T/sec )
 20\     ut>s.short  ( sec min hour day month year -- T/sec )
 21\
 22\ internal use only
 23\     365+1 ( year -- 365|366 )
 24\     years/mod
 25\     years/mod.short
 26\     __acc_days  -- accumulated days of year at 1st of each month (0..11)
 27\     months/mod
 28
 29\ #include m-star-slash.frt
 30\ #include leap_year_q.fs
 31
 32&1970 constant __Epoch
 33: 365+1 ( year -- 365|366 )
 34    &365 swap leap_year? if 1+ then
 35;
 36: years/mod.short ( T/day -- years T/day' )
 37    dup &365 u>= if         \ -- T
 38        _last_epoch @ swap  \ -- year T
 39        begin
 40            over 365+1
 41            -
 42            swap 1+ swap    \ -- T-365/6 year+1
 43            over 365+1      \ -- year' T' 365
 44            over swap       \ -- year' T' T' 365
 45        u>= 0= until
 46    else
 47        _last_epoch @ swap
 48    then
 49
 50;
 51: years/mod ( T/day -- years T/day' )
 52    dup &365 u>= if         \ -- T
 53        __Epoch swap        \ -- year T
 54        begin
 55            over 365+1
 56            -
 57            swap 1+ swap    \ -- T-365/6 year+1
 58            over 365+1      \ -- year' T' 365
 59            over swap       \ -- year' T' T' 365
 60        u>= 0= until
 61    else
 62        __Epoch swap
 63    then
 64;
 65create __acc_days 0 , &31 ,  &59 ,  &90 , &120 , &151 , &181 ,
 66                     &212 , &243 , &273 , &304 , &334 , &365 ,
 67: months/mod ( year T/day -- year month T/day' )
 68    dup 0= if
 69        drop 1 1
 70    else
 71        &12 swap            \ -- year month T
 72        begin
 73            over __acc_days + @i
 74                            \ -- year month T acc_days[month]
 75            \ correct acc_days for leap year and months > 1 (January)
 76            3 pick leap_year? 3 pick 1 > and if 1+ then
 77            over over swap   \ -- year month T acc_days[month] acc_days[month] T
 78            u>
 79        while               \ -- year month T
 80                drop swap 1- swap
 81                            \ -- year month-1 T
 82        repeat              \ -- year month' T acc_days[month']
 83        -                   \ -- year month' T-acc_days[month']
 84        swap 1+
 85        swap 1+
 86    then
 87;
 88
 89: s>dt.short  ( d:EpochSeconds -- sec min hour day month year )
 90    _last_esec 2@ d-
 91    &60 ud/mod          \ -- sec T/min
 92    &60 ud/mod          \ -- sec min T/hour
 93    &24 ud/mod          \ -- sec min hour T/day
 94    d>s
 95    years/mod.short     \ -- sec min hour year T/day
 96    months/mod          \ -- sec min hour year month day
 97    swap                \ -- sec min hour year day month
 98    rot                 \ -- sec min hour day month year
 99;
100
101: s>dt  ( d:EpochSeconds -- sec min hour day month year )
102    &60 ud/mod          \ -- sec T/min
103    &60 ud/mod          \ -- sec min T/hour
104    &24 ud/mod          \ -- sec min hour T/day
105    d>s
106    years/mod           \ -- sec min hour year T/day
107    months/mod          \ -- sec min hour year month day
108    swap                \ -- sec min hour year day month
109    rot                 \ -- sec min hour day month year
110;
111
112: ut>s.short ( sec min hour day month year -- T/sec )
113    \ add start value T=0
114    0 over              \ -- sec min hour day month year T=0 year
115    _last_epoch @       \ -- sec min hour day month year T year Epoch
116    ?do
117        i 365+1 +
118    loop                \ -- sec min hour day month year T/days
119    2 pick 1-           \ -- sec min hour day month year T/days month-1
120    __acc_days + @i     \ -- sec min hour day month year T/days acc_days[month]
121    +                   \ -- sec min hour day month year T/days
122    swap                \ -- sec min hour day month T/days year
123    leap_year? rot 2 > and if 1+ then
124    \                   \ -- sec min hour day T/days
125    swap 1- +           \ -- sec min hour T/days
126    s>d
127    24 1 m*/ rot s>d d+ \ -- sec min T/hours
128    60 1 m*/ rot s>d d+ \ -- sec T/minutes
129    60 1 m*/ rot s>d d+ \ -- T/sec
130    _last_esec 2@ d+    \ -- T/sec
131;
132
133: ut>s ( sec min hour day month year -- T/sec )
134    \ add start value T=0
135    0 over              \ -- sec min hour day month year T=0 year
136    __Epoch             \ -- sec min hour day month year T year Epoch
137    ?do
138        i 365+1 +
139    loop                \ -- sec min hour day month year T/days
140    2 pick 1-           \ -- sec min hour day month year T/days month-1
141    __acc_days + @i     \ -- sec min hour day month year T/days acc_days[month]
142    +                   \ -- sec min hour day month year T/days
143    swap                \ -- sec min hour day month T/days year
144    leap_year? rot 2 > and if 1+ then
145    \                   \ -- sec min hour day T/days
146    swap 1- +           \ -- sec min hour T/days
147    s>d
148    24 1 m*/ rot s>d d+ \ -- sec min T/hours
149    60 1 m*/ rot s>d d+ \ -- sec T/minutes
150    60 1 m*/ rot s>d d+ \ -- T/sec
151;