12 Date and Time

The standard libraries offer few functions to manipulate date and time in Lua. As usual, all it offers is what is available in the standard C libraries. Nevertheless, despite its apparent simplicity, we can make quite a lot with this basic support.

Lua uses two representations for date and time. The first one is through a single number, usually an integer. Although not required by ISO C, on most systems this number is the number of seconds since some fixed date, called the epoch. In particular, both in POSIX and Windows systems the epoch is Jan 01, 1970, 0:00 UTC.

The second representation that Lua uses for dates and times is a table. Such date tables have the following significant fields: year, month, day, hour, min, sec, wday, yday, and isdst. All fields except isdst have integer values. The first six fields have obvious meanings. The wday field is the day of the week (one is Sunday); the yday field is the day of the year (one is January 1st). The isdst field is a Boolean, true if daylight saving is in effect. As an example, Sep 16, 1998, 23:48:10 (a Wednesday) corresponds to the following table:

      {year = 1998, month = 9, day = 16, yday = 259, wday = 4,
       hour = 23, min = 48, sec = 10, isdst = false}

Date tables do not encode a time zone. It is up to the program to interpret them correctly with respect to time zones.

The function os.time, when called without arguments, returns the current date and time, coded as a number:

      > os.time()          --> 1439653520

This date corresponds to Aug 15, 2015, 12:45:20.[13] In a POSIX system, we can use some basic arithmetic to decompose that number:

      local date = 1439653520
      local day2year = 365.242                 -- days in a year
      local sec2hour = 60 * 60                 -- seconds in an hour
      local sec2day = sec2hour * 24            -- seconds in a day
      local sec2year = sec2day * day2year      -- seconds in a year
      
      -- year
      print(date // sec2year + 1970)          --> 2015.0
      
      -- hour (in UTC)
      print(date % sec2day // sec2hour)       --> 15
      
      -- minutes
      print(date % sec2hour // 60)            --> 45
      
      -- seconds
      print(date % 60)                        --> 20

We can also call os.time with a date table, to convert the table representation to a number. The year, month, and day fields are mandatory. The hour, min, and sec fields default to noon (12:00:00) when not provided. Other fields (including wday and yday) are ignored.

      > os.time({year=2015, month=8, day=15, hour=12, min=45, sec=20})
        --> 1439653520
      > os.time({year=1970, month=1, day=1, hour=0})    --> 10800
      > os.time({year=1970, month=1, day=1, hour=0, sec=1})
        --> 10801
      > os.time({year=1970, month=1, day=1})            --> 54000

Note that 10800 is three hours (the time zone) in seconds and 54000 is 10800 plus 12 hours in seconds.

The function os.date, despite its name, is a kind of reverse of os.time: it converts a number representing the date and time to some higher-level representation, either a date table or a string. Its first parameter is a format string, describing the representation we want. The second parameter is the numeric date–time; it defaults to the current date and time if not provided.

To produce a date table, we use the format string "*t". For instance, the call os.date("*t", 906000490) returns the following table:

      {year = 1998, month = 9, day = 16, yday = 259, wday = 4,
       hour = 23, min = 48, sec = 10, isdst = false}

In general, we have that os.time(os.date("*t", t)) == t, for any valid time t.

Except for isdst, the resulting fields are integers in the following ranges:

year

a full year

month

1–12

day

1–31

hour

0–23

min

0–59

sec

0–60

wday

1–7

yday

1–366

(Seconds can go up to 60 to allow for leap seconds.)

For other format strings, os.date returns a copy of the string with specific directives replaced by information about the given time and date. A directive consists of a percent sign followed by a letter, as in the next example:

      print(os.date("a %A in %B"))            --> a Tuesday in May
      print(os.date("%d/%m/%Y", 906000490))   --> 16/09/1998

When relevant, representations follow the current locale. For instance, in a locale for Brazil–Portuguese, %A would result in "terça-feira" and %B in "maio".

Figure 12.1, “Directives for function os.date shows the main directives. For each directive, it presents its meaning and its value for September 16, 1998 (a Wednesday), at 23:48:10.

For numerical values, the table shows also their range of possible values. Here are some examples, showing how to create some ISO 8601 formats:

      t = 906000490
      -- ISO 8601 date
      print(os.date("%Y-%m-%d", t))           --> 1998-09-16
      -- ISO 8601 combined date and time
      print(os.date("%Y-%m-%dT%H:%M:%S", t))  --> 1998-09-16T23:48:10
      -- ISO 8601 ordinal date
      print(os.date("%Y-%j", t))              --> 1998-259

If the format string starts with an exclamation mark, then os.date interprets the time in UTC:

      -- the Epoch
      print(os.date("!%c", 0))     --> Thu Jan  1 00:00:00 1970

If we call os.date without any arguments, it uses the %c format, that is, date and time information in a reasonable format. Note that the representations for %x, %X, and %c change according to the locale and the system. If you want a fixed representation, such as dd/mm/yyyy, use an explicit format string, such as "%d/%m/%Y".

When os.date creates a date table, its fields are all in the proper ranges. However, when we give a date table to os.time, its fields do not need to be normalized. This feature is an important tool to manipulate dates and times.

As a simple example, suppose we want to know the date 40 days from now. We can compute that date as follows:

      t = os.date("*t")        -- get current time
      print(os.date("%Y/%m/%d", os.time(t)))     --> 2015/08/18
      t.day = t.day + 40
      print(os.date("%Y/%m/%d", os.time(t)))     --> 2015/09/27

If we convert the numeric time back to a table, we get a normalized version of that date–time:

      t = os.date("*t")
      print(t.day, t.month)             -->  26    2
      t.day = t.day - 40
      print(t.day, t.month)             --> -14    2
      t = os.date("*t", os.time(t))
      print(t.day, t.month)             -->  17    1

In this example, Feb -14 has been normalized to Jan 17, which is 40 days before Feb 26.

In most systems, we could also add or subtract 3456000 (40 days in seconds) to the numeric time. However, the C standard does not guarantee the correctness of this operation, because it does not require numeric times to denote seconds from some epoch. Moreover, if we want to add some months instead of days, the direct manipulation of seconds becomes problematic, as different months have different durations. The normalization method, on the other hand, has none of these problems:

      t = os.date("*t")        -- get current time
      print(os.date("%Y/%m/%d", os.time(t)))     --> 2015/08/18
      t.month = t.month + 6    -- six months from now
      print(os.date("%Y/%m/%d", os.time(t)))     --> 2016/02/18

We have to be careful when manipulating dates. Normalization works in a somewhat obvious way, but it may have some non-obvious consequences. For instance, if we compute one month after March 31, that would give April 31, which is normalized to May 1 (one day after April 30). That sounds quite natural. However, if we take one month back from that result (May 1), we arrive on April 1, not the original March 31. Note that this mismatch is a consequence of the way our calendar works; it has nothing to do with Lua.

To compute the difference between two times, there is the function os.difftime. It returns the difference, in seconds, between two given numeric times. For most systems, this difference is exactly the result of subtracting on time from the other. Unlike the subtraction, however, the behavior of os.difftime is guaranteed in any system. The next example computes the number of days passed between the release of Lua 5.2 and Lua 5.3:

      local t5_3 = os.time({year=2015, month=1, day=12})
      local t5_2 = os.time({year=2011, month=12, day=16})
      local d = os.difftime(t5_3, t5_2)
      print(d // (24 * 3600))         --> 1123.0

With difftime, we can express dates as number of seconds since any arbitrary epoch:

      > myepoch = os.time{year = 2000, month = 1, day = 1, hour = 0}
      > now = os.time{year = 2015, month = 11, day = 20}
      > os.difftime(now, myepoch)     --> 501336000.0

Using normalization, it is easy to convert that number of seconds back to a legitimate numeric time: we create a table with the epoch and set its seconds as the number we want to convert, as in the next example.

      > T = {year = 2000, month = 1, day = 1, hour = 0}
      > T.sec = 501336000
      > os.date("%d/%m/%Y", os.time(T))   --> 20/11/2015

We can also use os.difftime to compute the running time of a piece of code. For this task, however, it is better to use os.clock. The function os.clock returns the number of seconds of CPU time used by the program. Its typical use is to benchmark a piece of code:

      local x = os.clock()
      local s = 0
      for i = 1, 100000 do s = s + i end
      print(string.format("elapsed time: %.2f\n", os.clock() - x))

Unlike os.time, os.clock usually has sub-second precision, so its result is a float. The exact precision depends on the platform; in POSIX systems, it is typically one microsecond.

Exercise 12.1: Write a function that returns the date–time exactly one month after a given date–time. (Assume the numeric coding of date–time.)

Exercise 12.2: Write a function that returns the day of the week (coded as an integer, one is Sunday) of a given date.

Exercise 12.3: Write a function that takes a date–time (coded as a number) and returns the number of seconds passed since the beginning of its respective day.

Exercise 12.4: Write a function that takes a year and returns the day of its first Friday.

Exercise 12.5: Write a function that computes the number of complete days between two given dates.

Exercise 12.6: Write a function that computes the number of complete months between two given dates.

Exercise 12.7: Does adding one month and then one day to a given date give the same result as adding one day and then one month?

Exercise 12.8: Write a function that produces the system’s time zone.



[13] Unless otherwise stated, my dates are from a POSIX system running in Rio de Janeiro.

Personal copy of Eric Taylor <jdslkgjf.iapgjflksfg@yandex.com>