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

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