+local strformat = string.format
local floor = math.floor
local function idiv ( n , d )
return floor ( n / d )
end
local function month_length ( m , y )
+ m = ( m - 1 ) % 12 + 1
if m == 2 then
return is_leap ( y ) and 29 or 28
else
end
end
+local function leap_years_since ( year )
+ return idiv ( year , 4 ) - idiv ( year , 100 ) + idiv ( year , 400 )
+end
+
local function doomsday ( year )
return ( 3 -- Tuesday
- - 1 + year + idiv ( year , 4 ) - idiv ( year , 100 ) + idiv ( year , 400 ) )
+ - 1 + year + leap_years_since ( year ) )
% 7 + 1
end
local doomsday_cache = setmetatable ( { } , {
return tens , units
end
-local function normalise ( tm )
- local sec = tm.sec or 0
- local min = tm.min or 0
- local hour = tm.hour or 12
- local day = assert ( tm.day , "day required" )
- local month = assert ( tm.month , "month required" )
- local year = assert ( tm.year , "year required" )
+local function unpack_tm ( tm )
+ return assert ( tm.year , "year required" ) ,
+ assert ( tm.month , "month required" ) ,
+ assert ( tm.day , "day required" ) ,
+ tm.hour or 12 ,
+ tm.min or 0 ,
+ tm.sec or 0 ,
+ tm.yday ,
+ tm.wday
+end
+-- Modify parameters so they all fit within the "normal" range
+local function normalise ( year , month , day , hour , min , sec )
min , sec = increment ( min , sec , 60 ) -- TODO: consider leap seconds?
hour , min = increment ( hour , min , 60 )
day , hour = increment ( day , hour , 24 )
year , month = increment ( year , month - 1 , 12 )
month = month + 1
- tm.sec = sec
- tm.min = min
- tm.hour = hour
- tm.day = day
- tm.month = month
- tm.year = year
+ return year , month , day , hour , min , sec
+end
+
+local leap_years_since_1970 = leap_years_since ( 1970 )
+local function timestamp ( year , month , day , hour , min , sec )
+ year , month , day , hour , min , sec = normalise ( year , month , day , hour , min , sec )
+
+ local days_since_epoch = day_of_year ( day , month , year )
+ + 365 * ( year - 1970 )
+ -- Each leap year adds one day
+ + ( leap_years_since ( year - 1 ) - leap_years_since_1970 ) - 1
+
+ return days_since_epoch * (60*60*24)
+ + hour * (60*60)
+ + min * 60
+ + sec
+end
+
+
+local timetable_methods = { }
+
+function timetable_methods:normalise ( )
+ local year , month , day
+ year , month , day , self.hour , self.min , self.sec = normalise ( unpack_tm ( self ) )
+
+ self.day = day
+ self.month = month
+ self.year = year
local yday = day_of_year ( day , month , year )
local wday = day_of_week ( yday , year )
- tm.yday = yday
- tm.wday = wday
+ self.yday = yday
+ self.wday = wday
+
+ return self
+end
+timetable_methods.normalize = timetable_methods.normalise -- American English
+
+function timetable_methods:timestamp ( )
+ return timestamp ( unpack_tm ( self ) )
+end
- return tm
+function timetable_methods:rfc_3339 ( )
+ -- %06.4g gives 3 (=6-4+1) digits after decimal
+ return strformat ( "%04u-%02u-%02uT%02u:%02u:%06.4g" , unpack_tm ( self ) )
+end
+
+local timetable_mt
+
+local function coerce_arg ( t )
+ if getmetatable ( t ) == timetable_mt then
+ return t:timestamp ( )
+ end
+ return t
+end
+
+timetable_mt = {
+ __index = timetable_methods ;
+ __tostring = timetable_methods.rfc_3339 ;
+ __eq = function ( a , b )
+ return coerce_arg ( a ) == coerce_arg ( b )
+ end ;
+ __lt = function ( a , b )
+ return coerce_arg ( a ) < coerce_arg ( b )
+ end ;
+}
+
+local function cast_timetable ( tm )
+ return setmetatable ( tm , timetable_mt )
+end
+
+local function new_timetable ( year , month , day , hour , min , sec , yday , wday )
+ return cast_timetable {
+ year = year ;
+ month = month ;
+ day = day ;
+ hour = hour ;
+ min = min ;
+ sec = sec ;
+ yday = yday ;
+ wday = wday ;
+ }
+end
+
+function timetable_methods:clone ( )
+ return new_timetable ( unpack_tm ( self ) )
+end
+
+local function new_from_timestamp ( ts )
+ return new_timetable ( 1970 , 1 , 1 , 0 , 0 , ts )
end
return {
doomsday = doomsday ;
normalise = normalise ;
+ timestamp = timestamp ;
+
+ new = new_timetable ;
+ new_from_timestamp = new_from_timestamp ;
+ cast = cast_timetable ;
+ timetable_mt = timetable_mt ;
}