]> 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/index.md: Fix internal link
[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 month_length(m, y)
37         if m == 2 then
38                 return is_leap(y) and 29 or 28
39         else
40                 return mon_lengths[m]
41         end
42 end
43
44 local function leap_years_since(year)
45         return idiv(year, 4) - idiv(year, 100) + idiv(year, 400)
46 end
47
48 local function day_of_year(day, month, year)
49         local yday = months_to_days_cumulative[month]
50         if month > 2 and is_leap(year) then
51                 yday = yday + 1
52         end
53         return yday + day
54 end
55
56 local function day_of_week(day, month, year)
57         if month < 3 then
58                 year = year - 1
59         end
60         return(year + leap_years_since(year) + sakamoto[month] + day) % 7 + 1
61 end
62
63 local function borrow(tens, units, base)
64         local frac = tens % 1
65         units = units + frac * base
66         tens = tens - frac
67         return tens, units
68 end
69
70 local function carry(tens, units, base)
71         if units >= base then
72                 tens  = tens + idiv(units, base)
73                 units = units % base
74         elseif units < 0 then
75                 tens  = tens + idiv(units, base)
76                 units = (base + units) % base
77         end
78         return tens, units
79 end
80
81 -- Modify parameters so they all fit within the "normal" range
82 local function normalise(year, month, day, hour, min, sec)
83         -- `month` and `day` start from 1, need -1 and +1 so it works modulo
84         month, day = month - 1, day - 1
85
86         -- Convert everything (except seconds) to an integer
87         -- by propagating fractional components down.
88         year , month = borrow(year , month, 12)
89         -- Carry from month to year first, so we get month length correct in next line around leap years
90         year , month = carry(year, month, 12)
91         month, day   = borrow(month, day  , month_length(floor(month + 1), year))
92         day  , hour  = borrow(day  , hour , 24)
93         hour , min   = borrow(hour , min  , 60)
94         min  , sec   = borrow(min  , sec  , 60)
95
96         -- Propagate out of range values up
97         -- e.g. if `min` is 70, `hour` increments by 1 and `min` becomes 10
98         -- This has to happen for all columns after borrowing, as lower radixes may be pushed out of range
99         min  , sec   = carry(min , sec , 60) -- TODO: consider leap seconds?
100         hour , min   = carry(hour, min , 60)
101         day  , hour  = carry(day , hour, 24)
102         -- Ensure `day` is not underflowed
103         -- Add a whole year of days at a time, this is later resolved by adding months
104         -- TODO[OPTIMIZE]: This could be slow if `day` is far out of range
105         while day < 0 do
106                 month = month - 1
107                 if month < 0 then
108                         year = year - 1
109                         month = 11
110                 end
111                 day = day + month_length(month + 1, 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, fsec = self:unpack()
183         local sec, msec = borrow(fsec, 0, 1000)
184         msec = math.floor(msec)
185         return strformat("%04u-%02u-%02uT%02u:%02u:%02d.%03d", year, month, day, hour, min, sec, msec)
186 end
187
188 function timetable_methods:strftime(format_string)
189         return strftime(format_string, self)
190 end
191
192 local timetable_mt
193
194 local function coerce_arg(t)
195         if getmetatable(t) == timetable_mt then
196                 return t:timestamp()
197         end
198         return t
199 end
200
201 timetable_mt = {
202         __index    = timetable_methods;
203         __tostring = timetable_methods.rfc_3339;
204         __eq = function(a, b)
205                 return a:timestamp() == b:timestamp()
206         end;
207         __lt = function(a, b)
208                 return a:timestamp() < b:timestamp()
209         end;
210         __sub = function(a, b)
211                 return coerce_arg(a) - coerce_arg(b)
212         end;
213 }
214
215 local function cast_timetable(tm)
216         return setmetatable(tm, timetable_mt)
217 end
218
219 local function new_timetable(year, month, day, hour, min, sec, yday, wday)
220         return cast_timetable {
221                 year  = year;
222                 month = month;
223                 day   = day;
224                 hour  = hour;
225                 min   = min;
226                 sec   = sec;
227                 yday  = yday;
228                 wday  = wday;
229         }
230 end
231
232 function timetable_methods:clone()
233         return new_timetable(self:unpack())
234 end
235
236 local function new_from_timestamp(ts)
237         if type(ts) ~= "number" then
238                 error("bad argument #1 to 'new_from_timestamp' (number expected, got " .. type(ts) .. ")", 2)
239         end
240         return new_timetable(1970, 1, 1, 0, 0, ts):normalise()
241 end
242
243 return {
244         is_leap = is_leap;
245         day_of_year = day_of_year;
246         day_of_week = day_of_week;
247         normalise = normalise;
248         timestamp = timestamp;
249
250         new = new_timetable;
251         new_from_timestamp = new_from_timestamp;
252         cast = cast_timetable;
253         timetable_mt = timetable_mt;
254 }