]> git.madduck.net Git - etc/awesome.git/blob - luatz/timetable.lua

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

spec/: Better test descriptions.
[etc/awesome.git] / luatz / timetable.lua
1 local strftime = require "luatz.strftime".strftime
2 local strformat = string.format
3 local floor = math.floor
4 local function idiv ( n , d )
5         return floor ( n / d )
6 end
7
8
9 local mon_lengths = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
10 -- Number of days in year until start of month; not corrected for leap years
11 local months_to_days_cumulative = { 0 }
12 for i = 2, 12 do
13         months_to_days_cumulative [ i ] = months_to_days_cumulative [ i-1 ] + mon_lengths [ i-1 ]
14 end
15 -- For Sakamoto's Algorithm (day of week)
16 local sakamoto = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
17
18 local function is_leap ( y )
19         return (y % 4) == 0 and (y % 100) ~= 0 or (y % 400) == 0
20 end
21
22 local function year_length ( y )
23         return is_leap ( y ) and 366 or 365
24 end
25
26 local function month_length ( m , y )
27         if m == 2 then
28                 return is_leap ( y ) and 29 or 28
29         else
30                 return mon_lengths [ m ]
31         end
32 end
33
34 local function leap_years_since ( year )
35         return idiv ( year , 4 ) - idiv ( year , 100 ) + idiv ( year , 400 )
36 end
37
38 local function day_of_year ( day , month , year )
39         local yday = months_to_days_cumulative [ month ]
40         if month > 2 and is_leap ( year ) then
41                 yday = yday + 1
42         end
43         return yday + day
44 end
45
46 local function day_of_week ( day , month , year )
47         if month < 3 then
48                 year = year - 1
49         end
50         return ( year + leap_years_since ( year ) + sakamoto[month] + day ) % 7 + 1
51 end
52
53 local function increment ( tens , units , base )
54         if units >= base then
55                 tens  = tens + idiv ( units , base )
56                 units = units % base
57         elseif units < 0 then
58                 tens  = tens - 1 + idiv ( -units , base )
59                 units = base - ( -units % base )
60         end
61         return tens , units
62 end
63
64 -- Modify parameters so they all fit within the "normal" range
65 local function normalise ( year , month , day , hour , min , sec )
66         min  , sec  = increment ( min  , sec  , 60 ) -- TODO: consider leap seconds?
67         hour , min  = increment ( hour , min  , 60 )
68         day  , hour = increment ( day  , hour , 24 )
69
70         while day <= 0 do
71                 year = year - 1
72                 day  = day + year_length ( year )
73         end
74
75         -- Lua months start from 1, need -1 and +1 around this increment
76         month = month - 1
77         year , month = increment ( year , month , 12 )
78         month = month + 1
79
80         -- This could potentially be slow if `day` is very large
81         while true do
82                 local i = month_length ( month , year )
83                 if day <= i then break end
84                 day = day - i
85                 month = month + 1
86                 if month > 12 then
87                         month = 1
88                         year = year + 1
89                 end
90         end
91
92         return year , month , day , hour , min , sec
93 end
94
95 local leap_years_since_1970 = leap_years_since ( 1970 )
96 local function timestamp ( year , month , day , hour , min , sec )
97         year , month , day , hour , min , sec = normalise ( year , month , day , hour , min , sec )
98
99         local days_since_epoch = day_of_year ( day , month , year )
100                 + 365 * ( year - 1970 )
101                 -- Each leap year adds one day
102                 + ( leap_years_since ( year - 1 ) - leap_years_since_1970 ) - 1
103
104         return days_since_epoch * (60*60*24)
105                 + hour  * (60*60)
106                 + min   * 60
107                 + sec
108 end
109
110
111 local timetable_methods = { }
112
113 function timetable_methods:unpack ( )
114         return assert ( self.year  , "year required" ) ,
115                 assert ( self.month , "month required" ) ,
116                 assert ( self.day   , "day required" ) ,
117                 self.hour or 12 ,
118                 self.min  or 0 ,
119                 self.sec  or 0 ,
120                 self.yday ,
121                 self.wday
122 end
123
124 function timetable_methods:normalise ( )
125         local year , month , day
126         year , month , day , self.hour , self.min , self.sec = normalise ( self:unpack ( ) )
127
128         self.day   = day
129         self.month = month
130         self.year  = year
131         self.yday  = day_of_year ( day , month , year )
132         self.wday  = day_of_week ( day , month , year )
133
134         return self
135 end
136 timetable_methods.normalize = timetable_methods.normalise -- American English
137
138 function timetable_methods:timestamp ( )
139         return timestamp ( self:unpack ( ) )
140 end
141
142 function timetable_methods:rfc_3339 ( )
143         -- %06.3f gives 3 (=6-3) digits after decimal
144         return strformat ( "%04u-%02u-%02uT%02u:%02u:%06.3f" , self:unpack ( ) )
145 end
146
147 function timetable_methods:strftime ( format_string )
148         return strftime ( format_string , self )
149 end
150
151 local timetable_mt
152
153 local function coerce_arg ( t )
154         if getmetatable ( t ) == timetable_mt then
155                 return t:timestamp ( )
156         end
157         return t
158 end
159
160 timetable_mt = {
161         __index    = timetable_methods ;
162         __tostring = timetable_methods.rfc_3339 ;
163         __eq = function ( a , b )
164                 return a:timestamp ( ) == b:timestamp ( )
165         end ;
166         __lt = function ( a , b )
167                 return a:timestamp ( ) < b:timestamp ( )
168         end ;
169         __sub = function ( a , b )
170                 return coerce_arg ( a ) - coerce_arg ( b )
171         end ;
172 }
173
174 local function cast_timetable ( tm )
175         return setmetatable ( tm , timetable_mt )
176 end
177
178 local function new_timetable ( year , month , day , hour , min , sec , yday , wday )
179         return cast_timetable {
180                 year  = year ;
181                 month = month ;
182                 day   = day ;
183                 hour  = hour ;
184                 min   = min ;
185                 sec   = sec ;
186                 yday  = yday ;
187                 wday  = wday ;
188         }
189 end
190
191 function timetable_methods:clone ( )
192         return new_timetable ( self:unpack ( ) )
193 end
194
195 local function new_from_timestamp ( ts )
196         if type ( ts ) ~= "number" then
197                 error ( "bad argument #1 to 'new_from_timestamp' (number expected, got " .. type ( ts ) .. ")" , 2 )
198         end
199         return new_timetable ( 1970 , 1 , 1 , 0 , 0 , ts )
200 end
201
202 return {
203         day_of_year = day_of_year ;
204         day_of_week = day_of_week ;
205         normalise = normalise ;
206         timestamp = timestamp ;
207
208         new = new_timetable ;
209         new_from_timestamp = new_from_timestamp ;
210         cast = cast_timetable ;
211         timetable_mt = timetable_mt ;
212 }