]> 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:

rockspec: Remove < 5.3 requirement, add supported environments to README
[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 idiv do
5         -- Try and use actual integer division when available (Lua 5.3+)
6         local idiv_loader, err = (loadstring or load)([[return function(n,d) return n//d end]], "idiv")
7         if idiv_loader then
8                 idiv = idiv_loader()
9         else
10                 idiv = function(n, d)
11                         return floor(n/d)
12                 end
13         end
14 end
15
16
17 local mon_lengths = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
18 -- Number of days in year until start of month; not corrected for leap years
19 local months_to_days_cumulative = { 0 }
20 for i = 2, 12 do
21         months_to_days_cumulative [ i ] = months_to_days_cumulative [ i-1 ] + mon_lengths [ i-1 ]
22 end
23 -- For Sakamoto's Algorithm (day of week)
24 local sakamoto = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
25
26 local function is_leap ( y )
27         if (y % 4) ~= 0 then
28                 return false
29         elseif (y % 100) ~= 0 then
30                 return true
31         else
32                 return (y % 400) == 0
33         end
34 end
35
36 local function year_length ( y )
37         return is_leap ( y ) and 366 or 365
38 end
39
40 local function month_length ( m , y )
41         if m == 2 then
42                 return is_leap ( y ) and 29 or 28
43         else
44                 return mon_lengths [ m ]
45         end
46 end
47
48 local function leap_years_since ( year )
49         return idiv ( year , 4 ) - idiv ( year , 100 ) + idiv ( year , 400 )
50 end
51
52 local function day_of_year ( day , month , year )
53         local yday = months_to_days_cumulative [ month ]
54         if month > 2 and is_leap ( year ) then
55                 yday = yday + 1
56         end
57         return yday + day
58 end
59
60 local function day_of_week ( day , month , year )
61         if month < 3 then
62                 year = year - 1
63         end
64         return ( year + leap_years_since ( year ) + sakamoto[month] + day ) % 7 + 1
65 end
66
67 local function borrow ( tens , units , base )
68         local frac = tens % 1
69         units = units + frac * base
70         tens = tens - frac
71         return tens , units
72 end
73
74 local function carry ( tens , units , base )
75         if units >= base then
76                 tens  = tens + idiv ( units , base )
77                 units = units % base
78         elseif units < 0 then
79                 tens  = tens - 1 + idiv ( -units , base )
80                 units = base - ( -units % base )
81         end
82         return tens , units
83 end
84
85 -- Modify parameters so they all fit within the "normal" range
86 local function normalise ( year , month , day , hour , min , sec )
87         -- `month` and `day` start from 1, need -1 and +1 so it works modulo
88         month , day = month - 1 , day - 1
89
90         -- Convert everything (except seconds) to an integer
91         -- by propagating fractional components down.
92         year  , month = borrow ( year  , month , 12 )
93         -- Carry from month to year first, so we get month length correct in next line around leap years
94         year  , month = carry ( year , month , 12 )
95         month , day   = borrow ( month , day   , month_length ( floor ( month + 1 ) , year ) )
96         day   , hour  = borrow ( day   , hour  , 24 )
97         hour  , min   = borrow ( hour  , min   , 60 )
98         min   , sec   = borrow ( min   , sec   , 60 )
99
100         -- Propagate out of range values up
101         -- e.g. if `min` is 70, `hour` increments by 1 and `min` becomes 10
102         -- This has to happen for all columns after borrowing, as lower radixes may be pushed out of range
103         min   , sec   = carry ( min   , sec   , 60 ) -- TODO: consider leap seconds?
104         hour  , min   = carry ( hour  , min   , 60 )
105         day   , hour  = carry ( day   , hour  , 24 )
106         -- Ensure `day` is not underflowed
107         -- Add a whole year of days at a time, this is later resolved by adding months
108         -- TODO[OPTIMIZE]: This could be slow if `day` is far out of range
109         while day < 0 do
110                 year = year - 1
111                 day  = day + year_length ( year )
112         end
113         year , month = carry ( year , month , 12 )
114
115         -- TODO[OPTIMIZE]: This could potentially be slow if `day` is very large
116         while true do
117                 local i = month_length ( month + 1 , year )
118                 if day < i then break end
119                 day = day - i
120                 month = month + 1
121                 if month >= 12 then
122                         month = 0
123                         year = year + 1
124                 end
125         end
126
127         -- Now we can place `day` and `month` back in their normal ranges
128         -- e.g. month as 1-12 instead of 0-11
129         month , day = month + 1 , day + 1
130
131         return year , month , day , hour , min , sec
132 end
133
134 local leap_years_since_1970 = leap_years_since ( 1970 )
135 local function timestamp ( year , month , day , hour , min , sec )
136         year , month , day , hour , min , sec = normalise ( year , month , day , hour , min , sec )
137
138         local days_since_epoch = day_of_year ( day , month , year )
139                 + 365 * ( year - 1970 )
140                 -- Each leap year adds one day
141                 + ( leap_years_since ( year - 1 ) - leap_years_since_1970 ) - 1
142
143         return days_since_epoch * (60*60*24)
144                 + hour  * (60*60)
145                 + min   * 60
146                 + sec
147 end
148
149
150 local timetable_methods = { }
151
152 function timetable_methods:unpack ( )
153         return assert ( self.year  , "year required" ) ,
154                 assert ( self.month , "month required" ) ,
155                 assert ( self.day   , "day required" ) ,
156                 self.hour or 12 ,
157                 self.min  or 0 ,
158                 self.sec  or 0 ,
159                 self.yday ,
160                 self.wday
161 end
162
163 function timetable_methods:normalise ( )
164         local year , month , day
165         year , month , day , self.hour , self.min , self.sec = normalise ( self:unpack ( ) )
166
167         self.day   = day
168         self.month = month
169         self.year  = year
170         self.yday  = day_of_year ( day , month , year )
171         self.wday  = day_of_week ( day , month , year )
172
173         return self
174 end
175 timetable_methods.normalize = timetable_methods.normalise -- American English
176
177 function timetable_methods:timestamp ( )
178         return timestamp ( self:unpack ( ) )
179 end
180
181 function timetable_methods:rfc_3339 ( )
182         local year , month , day , hour , min , sec = self:unpack ( )
183         local sec , msec = borrow ( sec , 0 , 1000 )
184         return strformat ( "%04u-%02u-%02uT%02u:%02u:%02d.%03d" , year , month , day , hour , min , sec , msec )
185 end
186
187 function timetable_methods:strftime ( format_string )
188         return strftime ( format_string , self )
189 end
190
191 local timetable_mt
192
193 local function coerce_arg ( t )
194         if getmetatable ( t ) == timetable_mt then
195                 return t:timestamp ( )
196         end
197         return t
198 end
199
200 timetable_mt = {
201         __index    = timetable_methods ;
202         __tostring = timetable_methods.rfc_3339 ;
203         __eq = function ( a , b )
204                 return a:timestamp ( ) == b:timestamp ( )
205         end ;
206         __lt = function ( a , b )
207                 return a:timestamp ( ) < b:timestamp ( )
208         end ;
209         __sub = function ( a , b )
210                 return coerce_arg ( a ) - coerce_arg ( b )
211         end ;
212 }
213
214 local function cast_timetable ( tm )
215         return setmetatable ( tm , timetable_mt )
216 end
217
218 local function new_timetable ( year , month , day , hour , min , sec , yday , wday )
219         return cast_timetable {
220                 year  = year ;
221                 month = month ;
222                 day   = day ;
223                 hour  = hour ;
224                 min   = min ;
225                 sec   = sec ;
226                 yday  = yday ;
227                 wday  = wday ;
228         }
229 end
230
231 function timetable_methods:clone ( )
232         return new_timetable ( self:unpack ( ) )
233 end
234
235 local function new_from_timestamp ( ts )
236         if type ( ts ) ~= "number" then
237                 error ( "bad argument #1 to 'new_from_timestamp' (number expected, got " .. type ( ts ) .. ")" , 2 )
238         end
239         return new_timetable ( 1970 , 1 , 1 , 0 , 0 , ts ):normalise ( )
240 end
241
242 return {
243         is_leap = is_leap ;
244         day_of_year = day_of_year ;
245         day_of_week = day_of_week ;
246         normalise = normalise ;
247         timestamp = timestamp ;
248
249         new = new_timetable ;
250         new_from_timestamp = new_from_timestamp ;
251         cast = cast_timetable ;
252         timetable_mt = timetable_mt ;
253 }