]> git.madduck.net Git - etc/awesome.git/blob - src/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:

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