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

luatz/timetable.lua: Fix incorrect normalisation logic for negative day field
[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 = (loadstring or load)([[return function(n,d) return n//d end]], "idiv") -- luacheck: ignore 113
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 + 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                 month = month - 1
111                 if month < 0 then
112                         year = year - 1
113                         month = 11
114                 end
115                 day = day + month_length ( month + 1 , year )
116         end
117         year , month = carry ( year , month , 12 )
118
119         -- TODO[OPTIMIZE]: This could potentially be slow if `day` is very large
120         while true do
121                 local i = month_length ( month + 1 , year )
122                 if day < i then break end
123                 day = day - i
124                 month = month + 1
125                 if month >= 12 then
126                         month = 0
127                         year = year + 1
128                 end
129         end
130
131         -- Now we can place `day` and `month` back in their normal ranges
132         -- e.g. month as 1-12 instead of 0-11
133         month , day = month + 1 , day + 1
134
135         return year , month , day , hour , min , sec
136 end
137
138 local leap_years_since_1970 = leap_years_since ( 1970 )
139 local function timestamp ( year , month , day , hour , min , sec )
140         year , month , day , hour , min , sec = normalise ( year , month , day , hour , min , sec )
141
142         local days_since_epoch = day_of_year ( day , month , year )
143                 + 365 * ( year - 1970 )
144                 -- Each leap year adds one day
145                 + ( leap_years_since ( year - 1 ) - leap_years_since_1970 ) - 1
146
147         return days_since_epoch * (60*60*24)
148                 + hour  * (60*60)
149                 + min   * 60
150                 + sec
151 end
152
153
154 local timetable_methods = { }
155
156 function timetable_methods:unpack ( )
157         return assert ( self.year  , "year required" ) ,
158                 assert ( self.month , "month required" ) ,
159                 assert ( self.day   , "day required" ) ,
160                 self.hour or 12 ,
161                 self.min  or 0 ,
162                 self.sec  or 0 ,
163                 self.yday ,
164                 self.wday
165 end
166
167 function timetable_methods:normalise ( )
168         local year , month , day
169         year , month , day , self.hour , self.min , self.sec = normalise ( self:unpack ( ) )
170
171         self.day   = day
172         self.month = month
173         self.year  = year
174         self.yday  = day_of_year ( day , month , year )
175         self.wday  = day_of_week ( day , month , year )
176
177         return self
178 end
179 timetable_methods.normalize = timetable_methods.normalise -- American English
180
181 function timetable_methods:timestamp ( )
182         return timestamp ( self:unpack ( ) )
183 end
184
185 function timetable_methods:rfc_3339 ( )
186         local year, month, day, hour, min, fsec = self:unpack()
187         local sec, msec = borrow(fsec, 0, 1000)
188         msec = math.floor(msec)
189         return strformat ( "%04u-%02u-%02uT%02u:%02u:%02d.%03d" , year , month , day , hour , min , sec , msec )
190 end
191
192 function timetable_methods:strftime ( format_string )
193         return strftime ( format_string , self )
194 end
195
196 local timetable_mt
197
198 local function coerce_arg ( t )
199         if getmetatable ( t ) == timetable_mt then
200                 return t:timestamp ( )
201         end
202         return t
203 end
204
205 timetable_mt = {
206         __index    = timetable_methods ;
207         __tostring = timetable_methods.rfc_3339 ;
208         __eq = function ( a , b )
209                 return a:timestamp ( ) == b:timestamp ( )
210         end ;
211         __lt = function ( a , b )
212                 return a:timestamp ( ) < b:timestamp ( )
213         end ;
214         __sub = function ( a , b )
215                 return coerce_arg ( a ) - coerce_arg ( b )
216         end ;
217 }
218
219 local function cast_timetable ( tm )
220         return setmetatable ( tm , timetable_mt )
221 end
222
223 local function new_timetable ( year , month , day , hour , min , sec , yday , wday )
224         return cast_timetable {
225                 year  = year ;
226                 month = month ;
227                 day   = day ;
228                 hour  = hour ;
229                 min   = min ;
230                 sec   = sec ;
231                 yday  = yday ;
232                 wday  = wday ;
233         }
234 end
235
236 function timetable_methods:clone ( )
237         return new_timetable ( self:unpack ( ) )
238 end
239
240 local function new_from_timestamp ( ts )
241         if type ( ts ) ~= "number" then
242                 error ( "bad argument #1 to 'new_from_timestamp' (number expected, got " .. type ( ts ) .. ")" , 2 )
243         end
244         return new_timetable ( 1970 , 1 , 1 , 0 , 0 , ts ):normalise ( )
245 end
246
247 return {
248         is_leap = is_leap ;
249         day_of_year = day_of_year ;
250         day_of_week = day_of_week ;
251         normalise = normalise ;
252         timestamp = timestamp ;
253
254         new = new_timetable ;
255         new_from_timestamp = new_from_timestamp ;
256         cast = cast_timetable ;
257         timetable_mt = timetable_mt ;
258 }