Time is Hard (for computers & programmers)

A few weeks ago, I saw some code in a library that purported to get the current date. It looked something like this:

local time = os.time()
local year = math.floor(time / (60*60*24 * 365.25)) + 1970

This sort of simple approximation appears to work. If I run this code, I’ll get 2015 out. However, it’s only an approximation of reality. This post will talk about what time is to a computer and why it’s hard, and some ideas of how to do it right.

For ROBLOX readers, I wrote a date-time formatting module that hopefully correctly addresses most of the problems with time. Let me know if you discover any bug.

What is time to a computer?

While words describe time to humans — “4pm”, “tomorow,” “February 28th”, “July 1980” — computers prefer to work with numbers.

We need time to know when to do things. To schedule an event, you need to be able to describe when it is. This is largely governed by daylight and season.

To accomplish this, most systems represent time as the amount of seconds (or milliseconds or nanoseconds) that have passed since some epoch — a “0 point” in time. This lets you subtract two times and find out how much time is between them, and which one comes first.

Usually, that is the Unix Epoch, which is the beginning of January 1st, 1970. The number of seconds since this date is called Unix time. The current unix time as of beginning this paragraph was 1,437,942,327. It has been 1.4 billion seconds since the beginning of 1970.

One Second per Second

Time is hard. There are a lot of things you’d like to be true that aren’t, and it all boils down to the assumption that time moves forward at one second per second. Clocks can change, they can become un-synced with other clocks, and weird events can happen requiring changes. Internal implementations might not use very accurate methods of keeping track and they might need to be adjusted periodically.

Beyond the technical reasons why clocks are inaccurate, and even excepting timezones, there is a major barrier to the rate of time being simple.

You’ve heard of leap years; there are also leap seconds. Notably, one passed just last month, and there was actually a lot of publicity about this unfortunate detail. At these times, the last second of a day happens twice. Leap seconds are necessary to allow astronomical accuracy between our clocks and the earth’s movement about the sun.

Luckily for schedules using Unix time, this means that for the most part, the leap second can be forgotten — excepting that one confusing second. Unix time will “look” the same whether or not the leap second was inserted.

Another niceness of Unix time is that a day is defined to be exactly 60*60*24 = 86400 seconds.

365

How long is a year? The original Lua snippet was almost right with the definition of a solar year — the precise amount of time needed to do one revolution around the sun. The answer is, on average, 365.242189. This is fairly close to the 365.25 from the snippet.

Thousands of years ago it was recognized that the year is not exactly 365 days. If you assume it is, then your winter will come later and later each year, which is unacceptable to societies which grow food or celebrate seasons.

The modern calendar has a consistent rule to extend some years to 366 days. By alternating between normal years and these leap years, the solar year can be approximated very closely over centuries.

Most readers will know the basic leap-year rule:

A year divisible by 4 is a leap year.

There is more to this rule: A year divisible by 100 is not a leap year, unless it is divisible by 400. In other words, normally the century years are not leap years. This is usually forgotten because the last century, 2000, is divisible by 400, and so had a leap year.

So how do I get the year?

The original algorithm for getting the year is flawed.

Think about the first few hours of January 1st, 1971. Less than 365.24 days have passed since January 1st, 1970 — so the simple algorithm will say the first quarter of January 1st, 1971 actually took place in 1970.

The problem is that while the average year is 365.24 days long, no single year is that long — years are only either 365 or 366 days long.

The simplest solution is to iterate by year, keeping track of how many days have passed accurately by checking if each year is a leap year. This is what I opted for in my date module.

If you care about performance for far future dates, this is a problem. We don’t want to have to spend more time just because the number is larger. This can be fixed, because while the number of days in a year is not constant, the number of days in 400 years is. Then, the process is fairly similar:

local time = os.time()
local lenOf4Centuries = 366 * 97 + 365 * 303
local t1600to1970 = ??? --seconds between 1600 and 1970
local fourCenturies = (time - t1600to1970)/(lenOf4Centuries * 60*60*24)
local year = 1600 + 400 * math.floor(fourCenturies) + yearWithinFourCenturies

etc, though this is somewhat more complicated.

Zoning Out

Another huge complexity of time is time-zones. A time-zone is a political region that has a certain offset (usually in hours, less commonly in half hours) from Universal Time, which is time (sans daylight savings) in Greenwich, England.

To be safe, all times need to remember their timezone. Unix time will usually be returned in GMT (UTC+000), but there’s often an equivalent for local time. In ROBLOX, tick() returns local time and os.time() returns GMT time.

If you have to deal with historical data, things get really messy. Timezones can be created or moved, meaning even if you stay in the same place, time can change. A more modern complication is daylight savings.

Daylight savings adjusts time by an hour. It’s used all over the world, but daylight savings can start at different times. Thus, even when I stay in the same place, 8pm GMT could be either 1pm OR 2pm locally.

It also means that timezones vanish depending on the year or location. For instance, Russia used to use daylight savings. They switched to permanent “summer” time, then to permanent “winter” time. Thus, summer and winter alternated in 2010; only summer happened in 2011, 2012, 2013, and only winter in 2014 and 2015.

This is a subtlety that I haven’t tackled in the date module. Good luck to anyone who wants to amend it!

Weekdays

This is another problem my date module doesn’t tackle. You need to fix some known weekday (1 January 1970 was a Thursday) and figure out how many days it has been since then, and then take the remainder after dividing by 7. Keeping track of this in conjunction with the years since 1970 is not too difficult, but coming up with it just given day, month, and year is fairly difficult.

Other Hard Things

I’m planning on writing blogposts about other human things that are hard for computers. On my mind right now are two things:

  • Color — the complexity of human perception
  • Text — the complexity of encoding and processing written language

BlueTaslem on ROBLOX. Programmer and doer of fun things

BlueTaslem on ROBLOX. Programmer and doer of fun things