Time: Dates and Periods in Lua

A module for the manipulation of dates and periods according to the Gregorian calendar.

Concepts and features:


local time = require "time"

local d1 = time.date(2012, 4, 30) -- A date at 00:00 AM.

-- Arithmetic operators are supported:
local p1 = time.hours(13) + time.minutes(30) -- A period.
local d2 = d1 + p1 -- The same date above at 1:30 PM.
assert(d2 - d1 == p1)

-- To / from strings, same functionality for periods:
local datestr = "2012-04-30T13:30:00.000000"
local d3 = time.todate(datestr)
assert(d2 == d3)
assert(tostring(d2) == datestr)

-- Comparison operators are supported:
assert(time.minutes(1) == time.seconds(60))
assert(time.minutes(1) >  time.seconds(59))
assert(d2 > d1)

print(time.nowlocal()) -- Now, according to local clock.
print(time.nowutc())   -- Now, according to UTC clock.

time.sleep(time.seconds(1)) -- Sleep for 1 second.

This module is based on Claus T√łndering's calendar algorithms and the corresponding C implementation.

Validity of Dates and Periods

The year component of each date must belong to the range [1582, 9999] and all dates must be valid (for example 2012-02-31 is not). If these conditions are not satisfied an error is thrown.

Attempts to construct periods outside of the supported range [-290000 years, +290000 years] yield undefined results. No attempt is made to detect such mistakes.

Indexing, Operators and Reversibility

All comparisons operator are supported and both sides of a given relation must either be dates or periods (it is an error to compare a date with a period).

The following table list the supported arithmetic operators, the resulting objects and whether the operators are reversible (more on this after the table). By integer we refer to a Lua number with no fractional part and by months and years we refer to the objects obtained by calls to time.months() and time.years().

OperationResultReversibleNote
period ± periodperiodyescommutative
period * integerperiodyescommutative
integer * periodperiodyescommutative
period / integerperiodno
period / periodnumbernofraction
-periodperiodyes
date ± perioddateyes
date ± monthsdateno (yes if day ≤ 28)
date ± yearsdateno (yes if day ≤ 28)

By reversibility we mean being able perform the inverse operation and get back the starting value:


local startp = time.seconds(13)
assert(startp == (startp + time.minutes(12)) - time.minutes(12))
assert(startp == (startp * 3) / 3)
assert(startp == -(-startp))

local startd = time.date(2012, 3, 3)
assert(startd == (startd - time.days(65)) + time.days(65))

This behavior is not guaranteed for divisions and for shifts by months or years. When performing such shifts, if the day exceeds the end of month of the resulting date (which makes the resulting date not valid) a cap is applied equal to the end of month itself. Examples:


local startp = time.microseconds(13)
assert(startp ~= (startp / 2) * 2)
print(time.date(2012, 1, 31) + time.months(1)) -- 2012-02-29

API

In the following we report the interface of the module. For brevity considerations p refers to an instance of a period and d to an instance of a date. We say that hours, minutes, seconds and microseconds are normalized when they belong respectively to the following ranges: ±23, ±59, ±59, ±999999. Normalization is achieved by increasing as much as required bigger count units. For instance 129 seconds is normalized to 2 minutes and 9 seconds.

time = require "time"

Returns the loaded module (no global variable is set).

p = time.weeks|days|hours|minutes|seconds|milliseconds|microseconds(n)

Constructs a period, all the arguments must be integers

p = time.period(hours = 0, minutes = 0, seconds = 0, microseconds = 0)

Equivalent to constructing the period by calling each corresponding constructor function and argument (for example minutes = 3 corresponds to time.minutes(3)) and by taking the sum.

str = tostring(p)

Returns the string representation of a period, which follows the format hours:mm:ss.ffffff where hours represents the number of hours and occupies 1 or more characters. For the remaining part of the string the number of repetitions of the same character specifies the number of characters used by a given normalized field: m for minutes, s for seconds, f for microseconds.

p = time.toperiod(str or int64)

The argument must be a string or a cdata<int64_t>. In the first case it constructs a period from its string representation which must follow the format above. In the second case the period is constructed considering the argument as the total (unnormalized) number of microseconds.

n = p:hours|minutes|seconds|microseconds()

Returns the number of normalized units for each period component.

h, m, s, i = p:parts()

Returns p:hours(), p:minutes(), p:seconds(), p:microseconds().

f = p:tohours|tominutes|toseconds|tomilliseconds|tomicroseconds()

Converts the period to the requested fractional units as Lua numbers. Note that p:tomicroseconds() is not equivalent to p:ticks() as the latter returns a cdata<int64_t>.

int64 = p:ticks()

Returns a cdata<int64_t> representing the total number of microseconds composing the period.

time.sleep(p)

Wait for a period p, which must represent a nonnegative amount of time, before continuing execution.

years = time.years(n)

Returns an object representing n years, where n must be an integer number. It is only used to shift dates via the + and - operators. No other operation is supported for this object.

months = time.months(n)

Returns an object representing n months, where n must be an integer number. It is only used to shift dates via the + and - operators. No other operation is supported for this object.

d = time.date(year, month, day)

Constructs a date at 00:00 AM (i.e. at the beginning of the given day) from the combination year, month, day. Dates corresponding to different times of the day are easily obtained by summing the returned value to appropriate periods.

d = time.nowlocal()

Returns a date representing the local time at the moment of the call: it takes into account the time zone and the eventual daylight saving adjustment.

d = time.nowutc()

Returns a date representing the the UTC time at the moment of the call: it does not take into account the time zone and the eventual daylight saving adjustment. It's potentially more precise and efficient than time.nowlocal().

str = tostring(d)

Returns the string representation of a date, which follows the format YYYY-MM-DDThh:mm:ss.ffffff where the number of repetitions of the same character specifies the number of characters used by a given (eventually) normalized field: Y for year, M for month, D for day (T is a separator like -, : and .), h for hours, m for minutes, s for seconds, f for microseconds.

d = time.todate(str or int64)

The argument must be a string or a cdata<int64_t>. In the first case it constructs a date from its string representation which must follow the format above. In the second case the cdata<int64_t> corresponds to an implementation-defined internal representation of dates and must be obtained by a previous call to d:ticks(). This internal representation is consistent across all architectures and operating systems.

n = d:year|month|day()

Returns the values of the year, month, day part of a date.

Y, M, D = d:ymd()

Returns d:year(), d:month(), d:day()

int64 = d:ticks()

Returns the cdata<int64_t> internal representation of the date which corresponds to the total number of microseconds lapsed since a constant implementation defined date.

p = d:period()

Returns the period corresponding to the time elapsed since the beginning of the date's day at 00:00 AM.

bool = time.isleapyear(year); bool = d:isleapyear()

Returns whether the given year or the year component of a given date, is a leap year.

day = time.endofmonth(year, month); day = d:endofmonth()

Returns the day corresponding to the end of the month (in a given year). The second function uses the year and month components of a given date.

n = time.weekday(year, month, day); n = d:weekday()

Returns the weekday number (from 1 for Monday to 7 for Sunday) corresponding to a given date or to a given year, month, day combination.