On time
What every programmer should know about time
Joeri Sebrechts / @joeri_s
Every day has 24 hours.
Australia, Lord Howe's Island, April 2nd 2017
24.5 hours
Every non-DST day is 24 hours.
Russia, Kamchatka, March 28th 2010
Putin kills 2 time zones.
https://www.thrillist.com/travel/nation/world-time-zones-insane-facts-you-didn-t-know-about-time-zones
In GMT a day is 24 hours.
Greenwich observes British Summer Time.
In UTC a day is 24 hours.
Except if there is a leap second.
DST transitions twice a year.
Morocco, 2014
March 30 +1h
June 28 -1h
August 2 +1h
October 26 -1h
In majority muslim countries like Morocco and Egypt,
they like to suspend DST during Ramadan so the day ends sooner.
If you know the local time, you know the UTC time.
With DST the same hour occurs twice.
This is why ISO local time strings have an offset, not a time zone identifier.
Every time between 0:00 and 24:00 is valid.
With DST, some hours don't exist.
Every date exists (in recent times).
Samoa, december 30th 2011
But at least we know what the rules are.
In 2014 the IANA TZ db changed 10 times.
Governments update the rules of time zones all the time,
often for political reasons, and not always in sensible ways.
The IANA time zone database changes get rolled into OS and application updates,
so they can take a while to hit your system.
We get proper notice of changes.
Turkey, October 2015 DST rules change with 3 weeks notice.
The date libraries handle all of that.
I
What is time
The universe is old: 13.77 billion years ± 59 million.
For aeons time passed without anyone noticing,
and then mankind started tracking time, which they did by observing
the movements of the heavenly bodies, the stars, the sun, the moon.
sidereal year
as compared to the fixed stars: 365 d 6 h 9 min 9.76 s
tropical year = common year
time between vernal (spring) equinoxes: 365 days, 5 hours, 48 minutes
Tropical 20 min. shorter due to precession of earth (wobble).
Length varies a little bit every year due to gravity of other planets.
Average long-term length is 365.24219 days.
The length of the year at the end of the 19th century was 365.242196 days,
while today it is 365.242190 days.
13.0.4.17.19
6th of Kislev, 5778
III Id. Nov. MMDCCLXX
May 31st 2017 in the Mayan ,
Hebrew , Julian calendars.
The 3rd day of the Ides of November in the year 2770,
because the Romans counted from the founding of Rome, 753 BC .
Actually, this date is november 11th in the Julian calendar, but we'll get to that.
Martius 31
Aprilis 30
Maius 31
Junius 30
Quintilis 31
Sextilis 30
September 30
October 31
November 30
December 30
Intercalary 51
The early roman calendar as claimed to come from Romulus,
but in actuality came from the Etruscans.
It descended from calendars that tracked the lunar cycle,
29.5 day per month (alternating 29 and 30),
but the days were made longer for unclear reasons.
The intercalary was meant to realign to the lunar and solar cycles,
but it was 10 days too short, which eventually put summer in winter.
Ianuarius 29
Februarius 28
(Intercalary) 23
Martius 31
Aprilis 29
Maius 31
Iunius 29
Quintilis 31
Sextilis 29
September 29
October 31
November 29
December 29
50 years after Rome was founded, King Pompilius Numa attempted to realign the year with the calendar,
by making the months add up to a year. However, the year only added up to 355 days,
so periodically an intercalary period was needed to realign the calendar.
The pontiff of the roman faith decided when and how long an intercalary should be,
which eventually got abused to keep allies in office longer.
This variability meant that in the provinces the exact date was often not known.
Ianuarius 31
Februarius 28/29
Martius 31
Aprilis 30
Maius 31
Iunius 30
Iulius 31
Augustus 31
September 30
October 31
November 30
December 31
The abuse by the pontiffs led Julius Caesar didn't like this system due to its vagueness,
so he changed it to one we recognize now, except there was a leap year every 4 years,
which caused the calendar to drift against the solar year.
After caesar's death Mark Antony renamed Quintilis to Iulius to honor him.
Later Sextilis would be renamed to honor emperor Augustus.
AD
The AD calendar wasn't conceived until 525 AD,
and popularized by Charlemagne around 800 AD.
Dionysius Exiguus, the monk who conceived it, made some errors
which cause Jesus to actually be born in 5 BC.
It was the dark ages after all.
The AD system does not have a year zero, because it used roman numerals.
Astronomers do use a year zero, putting it at 1 BC in the AD system.
Gregorian Calendar
Thu, 4 October 1582 → Fri, 15 October 1582
Year Adopted Days
1582 France (mostly), Italy, Poland, Portugal, Spain 10
1583 Austria, Germany (Catholic states) 10
...
1923 Greece 13
1927 Turkey 13
Pope Gregory's calendar introduced the leap year rule we now know:
Every year that is exactly divisible by four is a leap year, except for years that are exactly divisible by 100, but these centurial years are leap years if they are exactly divisible by 400.
This calendar took a long time to be adopted by countries.
As a consequence, historical dates don't always mean what you think they do .
This calendar drifts one day per 7700 years, meaning in 1.4 million years winter falls in summer.
Herschel's calendar fixes it by making multiples of 4000 not be leap years,
which drifts only one day per 30.000 years.
dies daeg day
Solii Sunnan Sun
Lunae Monan Mon
Martis Tiwes Tue
Mercurii Wodan Wed
Iovis Thor Thu
Veneris Freya Fri
Saturnii Sætern Sat
Many cultures used weeks to track the phases of the moon,
alternating 7 and 8 day weeks. The romans originally had an 8 day week,
inherited from the Etruscans.
However, for reasons which are not clear, the romans gradually
moved to a 7 day week, with the days named according to the
seven heavenly bodies visible with the naked eye.
The germanic tribes didn't particularly care for those names,
so they renamed most of them.
gotcha
Twitter, December 29, 2014
ISO week from until
2014-52 22 dec. 2014 28 dec. 2014
2015-01 29 dec. 2014 4 jan. 2015
strftime value
%G 4 digit ISO week year
%Y 4 digit calendar year
On this date twitter's login went down for 5.5 hours.
Traditionally weeks started on sunday and were not numbered.
ISO standardized weeks as starting on monday and having numbers,
with the first week of a year containing the first thursday inside that year.
Due to this, the first week of 2015 started on december 29th.
Twitter's login code used %G instead of %Y to obtain the year,
and thought it was already 2015 when it was not.
MCS got burned by a similar issue once too, be careful which
year flag you use when converting dates to strings.
10 + 2
5 × 12
Hours don't come from the romans but from the ancient egyptians.
They divided the day into 10 hours and 2 twilight hours,
then later the night hours were divided similarly.
Also later (~200 BC) the Greek astronomer Eratosthenes would divide
each hour into 60 minutes and each minute into 60 seconds.
The duodecimal (base 12) system is a pre-arabic system of counting,
easy to do because you count finger joints on your hand with your thumb.
The sexagesimal (base 60) system is convenient for expressing fractions.
9 192 631 770
TAI
The system of dividing a day into seconds produced a variable length second,
since days are variable length, and are slowly getting longer because
the tidal effect of the moon transfers momentum from the earth to the moon,
causing the earth to slow and the moon to speed up, and recede by 4 cm a year.
Thus, the SI system uses a fixed length second.
1 SI second = the duration of 9 192 631 770 periods of the radiation corresponding to the transition between the two hyperfine levels of the ground state of the cesium 133 atom.
This corresponds to the estimated average length of a real second in 1820, a year for which they had good estimates.
The only truly accurate clocks are therefore the cesium-133-based atomic clocks.
About 400 of them form a weighted average called International Atomic Time,
which is the baseline for all our timekeeping.
Leap seconds
CUT TUC UTC
However, since the length of a second is fixed, and the day is slowly growing longer,
this means that the SI day is a bit too short. For this reason,
the International Earth Rotation and Reference Systems Service (IERS)
decides about 6 months in advance when to add a leap second,
which adds one second to the latest minute of the day,
creating a 61 second minute.
The result of adding leap seconds to TAI is UTC, coordinated universal time.
The british wanted CUT, the french wanted TUC, and UTC is the compromise
(actual reason for why that is the acronym). The leap second is added at midnight UTC,
so it falls in different times in different time zones.
Gotcha: it's not possible to predict time to the second more than a year in advance,
because it is not known exactly when leap seconds will be needed.
UTC is currently TAI + 37 seconds.
Time zones
$$t_{local} = tz(t_{utc})$$
local date and time - offset = UTC
2017-11-24 T 12:00:00 +01:00 2017-11-24 T 11:00:00 Z
A time zone identifier (e.g. "Europe/Brussels") determines a function or calculation rule.
This function takes a time in UTC and outputs a time in the local time for that time zone.
More accurately, it calculates the offset between UTC and local time at that instant in time,
which can then be added to UTC to get the local time. The process is not reversible,
you cannot derive UTC from local time without knowing the offset (the result of the prior calculation).
Regions not near the tropics tend to observe DST because it produces longer evenings.
DST was first coined by Ben Franklin and adopted in Germany in 1916,
mostly abandoned after WW2 due to cheap electrical lighting, and reintroduced during the energy crisis.
II
keeping time
(in computers)
Now that we have a firm grasp on the absurdity of what time is,
we are ready to move on to how computers keep the time.
Gotcha
August 2013, Deep Impact spacecraft lost
January 2010, Bank of Queensland terminals break
August 1999, GPS navigation devices broke
These three bugs all are caused by date representation issues.
Deep Impact failed because it tracked time as tenths of a second in 32-bit signed,
which overflowed on August 13.
BoQ terminals interpreted 2-digit BCD decimal as hexadecimal,
with 09 being the same in both, but 10 being interpreted as the year 2016.
GPS has a 10-bit week counter , which counted from january 6th 1980 and rolled over in 1999.
It rolls over again on April 6th 2019.
So the way we represent time in computers really matters.
C time_t
Nov. 1971 Unix 1st ed. 32-bit signed counting 60ths of a second
Nov. 1973 Unix 4th ed. 32-bit signed counting seconds
Ken Thompson and Dennis Ritchie working on the C port of Unix in 1972.
1st edition unix was designed for the PDP-11/20, which had 24 KB of RAM,
so it needed an extremely compact way of representing time.
The initial time representation of 1st ed. unix was linked to the rate of the system clock on the PDP-11.
It had epoch at january 1st 1971 and could represent 2.5 years of time. The epoch was moved several times.
4th edition unix, ported to C, moved the epoch to 1st Jan. 1970 and counted seconds.
Y2K38
January 19 2038, 03:14:07 UTC
The 32-bit time_t value has a range from december 13 1901 to january 19 2038.
On this date time_t will roll over on this date.
This will cause software to think it is 1901.
Much software today still bakes in 32-bit time_t values, like the Ext3 filesystem, or the MySQL database.
There is on-going work in the linux kernel to fix this issue.
The mac FHS+ filesystem has a date representation that rolls over in 2040,
which is one of the reasons Apple FS is replacing it in new OS updates.
The C time_t is not a particularly bad time representation, but it has a limited range,
limited precision (whole seconds), and it does not handle time zones.
So modern systems should generally avoid it when possible.
System time
RTC / IRQ 8 - seconds
HPET - nanoseconds
The Real Time Clock is kept in hardware, battery-backed,
and allows the OS to get time with a resolution of seconds.
IRQ8 is triggered several times a second with the latest value.
The RTC is backed by a quartz crystal, which osscilates at a predictable frequency when voltage is applied to it.
The frequency varies with the temperature, which means this is not a precise or long-term reliable time source.
The high precision event timer counts nanoseconds since CPU reset, highly precise and monotonic.
It is however not a clock, since you cannot obtain time from it.
Gotcha
VM clock drift
The RTC is semi-reliably in hardware, but becomes less so in virtualization .
It can slow down a lot of the host is overburdened and cannot execute the guest OS at full speed.
If you deploy NTP on both the host and guest, they can enter into duels, which gets ugly.
It is generally considered best practice to install the guest tools to synchronize the guest clock with the host,
and to run NTP only on the host.
Network time
NTP
GPS
The NTP algorithm continually adjusts the local clock to match the reference clock obtained from the time server.
If the delta is > 128 ms it sets the local clock, otherwise it slews it, speeding it up or down, by 0.5 ms per second.
The consequence is that you can't really know whether delta's you obtain from the clock are in any way reliable.
Essentially the only reliable time source is an atomic clock. GPS satellites carry multiple rubidium atomic clocks.
If you use a GPS receiver you can get the time accurate to ~100 ns.
Fun fact: GPS satellites are designed to run 38 micro-seconds per day too slow when on earth
to compensate for the relativistic effects of the increased distance from the gravity mass of the earth (+45ms)
and the relative speed (-7ms) of being in orbit.
Aside from having your own atomic clock NTP and GPS are the only ways of keeping a clock reliable long-term.
NTP allows you to get an accuracy of ~150 ms in practice. Google wanted to do better, so they use TrueTime to get an accuracy of 7 ms.
time representation
wall clock time !== coordinated time
range !== precision
When it comes to a storage format for representing time,
we want multiple things. We want to be able to store coordinated time,
relative to UTC, but also wall clock time. In theory they are the same,
in practice due to delayed TZ db updated, misconfigured NTP, etc... they are not.
We also want to have a large range so we can represent all possible dates,
but not at the cost of precision, to store subsecond resolution.
All of these constraints make it hard to define a good representation.
java.util.Date
The good: 292 million BC - 292 million CE
The ugly:
not a date
not a value
not TZ aware
no leap seconds
imprecise
months to 11
In the rush to deliver Java 1.0 the java.util.Date class was thrown together by James Gosling.
It is an abomination .
In essence it is a wrapper around a 64-bit long that counts milliseconds since the unix epoch.
It can't be used for correct date arithmetic, dealing with time zones, or string parsing / formatting.
For that you need the almost equally horrible java.util.Calendar.
java date api
Date();
@Deprecated
Date(int year, int month, int date, int hrs, int min, int sec)
long getTime();
void setTime(long time);
@Deprecated long parse(String s);
@Deprecated int getDate();
@Deprecated int getDay();
// ...
Pretty much immediately after shipping Java 1.0
Sun realized that this was a terrible API,
so they deprecated almost all of it in JAva 1.1
and made it a glorified number container.
java.time
does not suck
Local and zoned time
ZonedDateTime z =
ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]")
.withZoneSameInstant(ZoneId.of("IST"));
Parse and display
System.out.println(
z.format(new DateTimeFormatter(ISO_OFFSET_DATE_TIME))
);
// 2007-12-03T14:45:30+05:30
JavaScript Date
Date(year, month, date, hours, minutes, seconds, milliseconds)
getTime() // number
setTime(timeValue)
parse(dateString) // number
getDay()
getUTCDay()
// ...
While Java 1.0 was in development Sun partnered with Netscape,
and part of the deal was that JavaScript was a stepping stone to Java,
so during the 10 days that Brendan Eich had to create JavaScript
he liberally stole from the Java code, including the Date type and its flaws .
It is subtly better. It can do date arithmetic (setDate(getDate+1)),
and it has the UTC methods which allow you to work with UTC time.
This can't be used to work with time zones, since you don't know the current time zone (identifier),
you can't be sure DST rules are correctly applied for dates in history,
and you can't work in any other time zone than the local one (of the browser) and UTC.
PHP
legacy date is bad (32-bit)
DateTime is ok
The legacy date functions work with a 32-bit signed int,
which means they suffer from the Y2K38 problem and cannot represent a date
prior to 1901.
The DateTime class is however pretty much ok.
PHP DateTime
does not suck
Local and zoned time
$dt = new DateTimeImmutable("2017-05-31 20:00:00.000123",
new DateTimeZone("Europe/Brussels"));
echo $dt->format("Y-m-d H:i:s.u P"); // 2017-05-31 20:00:00.000123 +02:00
UTC time
$dt = (new DateTimeImmutable("20:00Z"))
->setDate(1000000, 5, 31);
echo $dt->format("Y-m-d H:i:s P"); // 1000000-05-31 20:00:00 +00:00
PHP DateTime is a surprisingly good API.
It has a range 292 billion years in either direction,
can store microseconds, supports any time zone,
can parse any date format and produce any date format,
supports immutable values, and has an easy to use API.
ANSI SQL92
does not suck
type stores shows
TIMESTAMP UTC session TZ
TS WITH TIME ZONE local date/time + offset
DATE local date
TIME local time
Aside from the missing DATETIME type for storing local time, the SQL92 standard is sane.
It has types for conveniently storing UTC and zoned date+time, as well as local time.
MySQL
sucks
type stores shows
TIMESTAMP UTC (32-bit) session TZ
DATETIME local date+time (no TZ)
DATE local date (0001 - 9999)
TIME local time (+ microsecs.)
MySQL can store local date/time, but not with an offset.
It can store UTC time, but not beyond 2038 .
It can not store zoned time in any way.
Yes, MySQL really sucks.
Postgres
almost doesn't suck
type stores shows
TIMESTAMP UTC (64-bit) session TZ
TS WITH TIME ZONE UTC (64-bit) session TZ
DATE local date
TIME local time
Postgres implements the SQL92 standard, with a twist.
While it has even greater range than MySQL's datetime type (4713 BC to 294276 AD),
with microsecond precision, it has an odd behavior of the timestamp with time zone type.
Timestamp without time zone always interprets input strings in session time,
stores in UTC, and outputs back in session time. Timestamp with time zone
adds the twist of interpreting tz identifiers in input strings to allow inputting time
in a different time zone than the session time zone.
Oracle
doesn't suck?
type stores shows
TIMESTAMP local date & time (11 byte)
TS WITH TIME ZONE TS TZ TS TZ
TS WITH LOCAL TZ UTC session TZ
DATE local date & time
TIME
Oracle supports a very wide range of dates, and the timestamp types support fractional seconds as well.
Timestamp stores local date and time and does not adjust it.
Timestamp with local time zone converts session TZ to UTC and vice versa,
so it is very convenient for dealing with moments in time that you want
to represent in the time zone of the current user (= session time zone).
Timestamp with time zone stores the offset along with the UTC time, so both
local time when the time was inserted and UTC time are known, which covers all needs.
Finally the date field stores local date and time. There is no support for the ANSI SQL TIME type,
but that's mostly ok because the DATE field can be used and its date component ignored.
Surprisingly, despite not implementing the standard properly, Oracle covers all needs pretty efficiently.
Gotcha
Burundi passport birth date
the letter "x" is sometimes used instead of the day and month of birth
It turns out that some people only have a birth year, not a birth date.
You find this in Asia and Central Africa, and you do run across this in practice.
Storing birth dates is therefore notoriously tricky, and best not done with a date type.
Strategies
Where possible always work in coordinated time,
either UTC or zoned time convertible to UTC.
If you need to keep wall clock time, then use local time + an offset to store it,
so that it remains convertible to UTC.
Try to use only one source of time, to avoid synchronization issues
between the different time sources (e.g. trying to edit a future item).
Try to centralize your time handling code in a way that is easy to test,
generally it is best to put time manipulation and calculation code in separate
classes or functions, to keep that pure (not mixed with side effects or other concerns),
and to use immutable date/time values.
Use the ISO 8601 date/time format instead of the unix timestamp.
It can store time with a higher range, and can store both UTC
and zoned time keeping track of the offset relative to UTC.
Don't worry too much about edge cases like leap seconds and DST.
In practice it doesn't matter whether you support them correctly.
SQL Strategies
TIMESTAMP
TS w/ (LOCAL) TZ or VARCHAR (ISO 8601)
CURRENT_TIMESTAMP()
set session time zone
By default always use the TIMESTAMP type, since it stores as UTC
but displays as local time, which is usually what you want.
When you have a need to store actual local time,
use a format that is convertible to ISO 8601 (has an offset),
TS w/ TZ when it is available (TS w/ LOCAL TZ in oracle),
and VARCHAR otherwise (storing an ISO timestamp).
Use the database as your time source, and use the SQL92 CURRENT_TIMESTAMP() function
to do so. It is available in all databases and returns a TIMESTAMP WITH TIME ZONE
for databases that support that type. It is also replication-safe, so it will
give consistent time in clustered databases.
All of this magic only works if you set the DB session time zone when you create
the database connection.
PHP Strategies
DateTimeImmutable
Now from the DB
date_default_timezone_set()
You want to use the DateTime class instead of the legacy date functions,
and preferably the DateTimeImmutable class to avoid having values modified
that you didn't expect to be modified.
It is always best to have a single source of time, and the database is the best candidate.
Therefore, it is a good idea to always obtain "now" from the database
using the CURRENT_TIMESTAMP()
SQL function.
Finally, it is best practice to set your default PHP timezone (for new DateTime values)
on every request. The best approach for multi time zone software is storing the time zone
in the user's profile (where they can change it), and then using that as the PHP time zone,
setting it on every request, and as the DB session time zone, setting it on every connection.
JavaScript strategies
Avoid when possible
moment-timezone
instead of Date
Now from the server
window.performance.now()
for profiling
When possible, avoid doing date arithmetic in JavaScript. Date is a terrible API,
and it has no alternative (everything else wraps it). If you must do so,
then use the momentjs library with the moment-timezone extension.
Obtain the current time from the server (ultimately the database) to ensure consistent time
across clients, since you never know whether a client's clock is correct.
Finally use the window.performance.now() api when measuring elapsed time for profiling purposes,
since it has microsecond resolution and is guaranteed to be linearly increasing at a rate of one second per second.
PHP has no alternative for window.performance.now(), so you're stuck with querying the clock via DateTime,
which can behave unpredictably.
Testing strategies
Inject now
Test the future in the past
Use interesting dates
You want to cover your date/time handling code with tests,
but you don't want those tests to be brittle. Having a random element like now
in those tests can cause brittle behavior, so it is best to design the code
so that you can inject a different value of now for test purposes.
Then, with that ability, you can put now at some point in the past,
so that you can write tests that deal with past, present and future entirely in the past.
The advantage is that calendar rules never change in the past,
so you can reliably bake the desired behavior into your tests.
Finally, try to not just test the golden path, but test interesting boundary conditions,
like around the new year, or on a leap day. Don't sweat the small stuff like leap seconds
and DST, they rarely lead to significant bugs anyway.
Thank you
for your time
questions?