DeepCover
v0.1.16
1 | # frozen_string_literal: true |
2 | |
3 | require "active_support/core_ext/object/try" |
4 | |
5 | module DateAndTime |
6 | module Calculations |
7 | DAYS_INTO_WEEK = { |
8 | monday: 0, |
9 | tuesday: 1, |
10 | wednesday: 2, |
11 | thursday: 3, |
12 | friday: 4, |
13 | saturday: 5, |
14 | sunday: 6 |
15 | } |
16 | WEEKEND_DAYS = [ 6, 0 ] |
17 | |
18 | # Returns a new date/time representing yesterday. |
19 | def yesterday |
20 | advance(days: -1) |
21 | end |
22 | |
23 | # Returns a new date/time the specified number of days ago. |
24 | def prev_day(days = 1) |
25 | advance(days: -days) |
26 | end |
27 | |
28 | # Returns a new date/time representing tomorrow. |
29 | def tomorrow |
30 | advance(days: 1) |
31 | end |
32 | |
33 | # Returns a new date/time the specified number of days in the future. |
34 | def next_day(days = 1) |
35 | advance(days: days) |
36 | end |
37 | |
38 | # Returns true if the date/time is today. |
39 | def today? |
40 | to_date == ::Date.current |
41 | end |
42 | |
43 | # Returns true if the date/time is in the past. |
44 | def past? |
45 | self < self.class.current |
46 | end |
47 | |
48 | # Returns true if the date/time is in the future. |
49 | def future? |
50 | self > self.class.current |
51 | end |
52 | |
53 | # Returns true if the date/time falls on a Saturday or Sunday. |
54 | def on_weekend? |
55 | WEEKEND_DAYS.include?(wday) |
56 | end |
57 | |
58 | # Returns true if the date/time does not fall on a Saturday or Sunday. |
59 | def on_weekday? |
60 | !WEEKEND_DAYS.include?(wday) |
61 | end |
62 | |
63 | # Returns a new date/time the specified number of days ago. |
64 | def days_ago(days) |
65 | advance(days: -days) |
66 | end |
67 | |
68 | # Returns a new date/time the specified number of days in the future. |
69 | def days_since(days) |
70 | advance(days: days) |
71 | end |
72 | |
73 | # Returns a new date/time the specified number of weeks ago. |
74 | def weeks_ago(weeks) |
75 | advance(weeks: -weeks) |
76 | end |
77 | |
78 | # Returns a new date/time the specified number of weeks in the future. |
79 | def weeks_since(weeks) |
80 | advance(weeks: weeks) |
81 | end |
82 | |
83 | # Returns a new date/time the specified number of months ago. |
84 | def months_ago(months) |
85 | advance(months: -months) |
86 | end |
87 | |
88 | # Returns a new date/time the specified number of months in the future. |
89 | def months_since(months) |
90 | advance(months: months) |
91 | end |
92 | |
93 | # Returns a new date/time the specified number of years ago. |
94 | def years_ago(years) |
95 | advance(years: -years) |
96 | end |
97 | |
98 | # Returns a new date/time the specified number of years in the future. |
99 | def years_since(years) |
100 | advance(years: years) |
101 | end |
102 | |
103 | # Returns a new date/time at the start of the month. |
104 | # |
105 | # today = Date.today # => Thu, 18 Jun 2015 |
106 | # today.beginning_of_month # => Mon, 01 Jun 2015 |
107 | # |
108 | # +DateTime+ objects will have a time set to 0:00. |
109 | # |
110 | # now = DateTime.current # => Thu, 18 Jun 2015 15:23:13 +0000 |
111 | # now.beginning_of_month # => Mon, 01 Jun 2015 00:00:00 +0000 |
112 | def beginning_of_month |
113 | first_hour(change(day: 1)) |
114 | end |
115 | alias :at_beginning_of_month :beginning_of_month |
116 | |
117 | # Returns a new date/time at the start of the quarter. |
118 | # |
119 | # today = Date.today # => Fri, 10 Jul 2015 |
120 | # today.beginning_of_quarter # => Wed, 01 Jul 2015 |
121 | # |
122 | # +DateTime+ objects will have a time set to 0:00. |
123 | # |
124 | # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 |
125 | # now.beginning_of_quarter # => Wed, 01 Jul 2015 00:00:00 +0000 |
126 | def beginning_of_quarter |
127 | first_quarter_month = [10, 7, 4, 1].detect { |m| m <= month } |
128 | beginning_of_month.change(month: first_quarter_month) |
129 | end |
130 | alias :at_beginning_of_quarter :beginning_of_quarter |
131 | |
132 | # Returns a new date/time at the end of the quarter. |
133 | # |
134 | # today = Date.today # => Fri, 10 Jul 2015 |
135 | # today.end_of_quarter # => Wed, 30 Sep 2015 |
136 | # |
137 | # +DateTime+ objects will have a time set to 23:59:59. |
138 | # |
139 | # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 |
140 | # now.end_of_quarter # => Wed, 30 Sep 2015 23:59:59 +0000 |
141 | def end_of_quarter |
142 | last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month } |
143 | beginning_of_month.change(month: last_quarter_month).end_of_month |
144 | end |
145 | alias :at_end_of_quarter :end_of_quarter |
146 | |
147 | # Returns a new date/time at the beginning of the year. |
148 | # |
149 | # today = Date.today # => Fri, 10 Jul 2015 |
150 | # today.beginning_of_year # => Thu, 01 Jan 2015 |
151 | # |
152 | # +DateTime+ objects will have a time set to 0:00. |
153 | # |
154 | # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 |
155 | # now.beginning_of_year # => Thu, 01 Jan 2015 00:00:00 +0000 |
156 | def beginning_of_year |
157 | change(month: 1).beginning_of_month |
158 | end |
159 | alias :at_beginning_of_year :beginning_of_year |
160 | |
161 | # Returns a new date/time representing the given day in the next week. |
162 | # |
163 | # today = Date.today # => Thu, 07 May 2015 |
164 | # today.next_week # => Mon, 11 May 2015 |
165 | # |
166 | # The +given_day_in_next_week+ defaults to the beginning of the week |
167 | # which is determined by +Date.beginning_of_week+ or +config.beginning_of_week+ |
168 | # when set. |
169 | # |
170 | # today = Date.today # => Thu, 07 May 2015 |
171 | # today.next_week(:friday) # => Fri, 15 May 2015 |
172 | # |
173 | # +DateTime+ objects have their time set to 0:00 unless +same_time+ is true. |
174 | # |
175 | # now = DateTime.current # => Thu, 07 May 2015 13:31:16 +0000 |
176 | # now.next_week # => Mon, 11 May 2015 00:00:00 +0000 |
177 | def next_week(given_day_in_next_week = Date.beginning_of_week, same_time: false) |
178 | result = first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week))) |
179 | same_time ? copy_time_to(result) : result |
180 | end |
181 | |
182 | # Returns a new date/time representing the next weekday. |
183 | def next_weekday |
184 | if next_day.on_weekend? |
185 | next_week(:monday, same_time: true) |
186 | else |
187 | next_day |
188 | end |
189 | end |
190 | |
191 | # Returns a new date/time the specified number of months in the future. |
192 | def next_month(months = 1) |
193 | advance(months: months) |
194 | end |
195 | |
196 | # Short-hand for months_since(3) |
197 | def next_quarter |
198 | months_since(3) |
199 | end |
200 | |
201 | # Returns a new date/time the specified number of years in the future. |
202 | def next_year(years = 1) |
203 | advance(years: years) |
204 | end |
205 | |
206 | # Returns a new date/time representing the given day in the previous week. |
207 | # Week is assumed to start on +start_day+, default is |
208 | # +Date.beginning_of_week+ or +config.beginning_of_week+ when set. |
209 | # DateTime objects have their time set to 0:00 unless +same_time+ is true. |
210 | def prev_week(start_day = Date.beginning_of_week, same_time: false) |
211 | result = first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day))) |
212 | same_time ? copy_time_to(result) : result |
213 | end |
214 | alias_method :last_week, :prev_week |
215 | |
216 | # Returns a new date/time representing the previous weekday. |
217 | def prev_weekday |
218 | if prev_day.on_weekend? |
219 | copy_time_to(beginning_of_week(:friday)) |
220 | else |
221 | prev_day |
222 | end |
223 | end |
224 | alias_method :last_weekday, :prev_weekday |
225 | |
226 | # Returns a new date/time the specified number of months ago. |
227 | def prev_month(months = 1) |
228 | advance(months: -months) |
229 | end |
230 | |
231 | # Short-hand for months_ago(1). |
232 | def last_month |
233 | months_ago(1) |
234 | end |
235 | |
236 | # Short-hand for months_ago(3). |
237 | def prev_quarter |
238 | months_ago(3) |
239 | end |
240 | alias_method :last_quarter, :prev_quarter |
241 | |
242 | # Returns a new date/time the specified number of years ago. |
243 | def prev_year(years = 1) |
244 | advance(years: -years) |
245 | end |
246 | |
247 | # Short-hand for years_ago(1). |
248 | def last_year |
249 | years_ago(1) |
250 | end |
251 | |
252 | # Returns the number of days to the start of the week on the given day. |
253 | # Week is assumed to start on +start_day+, default is |
254 | # +Date.beginning_of_week+ or +config.beginning_of_week+ when set. |
255 | def days_to_week_start(start_day = Date.beginning_of_week) |
256 | start_day_number = DAYS_INTO_WEEK[start_day] |
257 | current_day_number = wday != 0 ? wday - 1 : 6 |
258 | (current_day_number - start_day_number) % 7 |
259 | end |
260 | |
261 | # Returns a new date/time representing the start of this week on the given day. |
262 | # Week is assumed to start on +start_day+, default is |
263 | # +Date.beginning_of_week+ or +config.beginning_of_week+ when set. |
264 | # +DateTime+ objects have their time set to 0:00. |
265 | def beginning_of_week(start_day = Date.beginning_of_week) |
266 | result = days_ago(days_to_week_start(start_day)) |
267 | acts_like?(:time) ? result.midnight : result |
268 | end |
269 | alias :at_beginning_of_week :beginning_of_week |
270 | |
271 | # Returns Monday of this week assuming that week starts on Monday. |
272 | # +DateTime+ objects have their time set to 0:00. |
273 | def monday |
274 | beginning_of_week(:monday) |
275 | end |
276 | |
277 | # Returns a new date/time representing the end of this week on the given day. |
278 | # Week is assumed to start on +start_day+, default is |
279 | # +Date.beginning_of_week+ or +config.beginning_of_week+ when set. |
280 | # DateTime objects have their time set to 23:59:59. |
281 | def end_of_week(start_day = Date.beginning_of_week) |
282 | last_hour(days_since(6 - days_to_week_start(start_day))) |
283 | end |
284 | alias :at_end_of_week :end_of_week |
285 | |
286 | # Returns Sunday of this week assuming that week starts on Monday. |
287 | # +DateTime+ objects have their time set to 23:59:59. |
288 | def sunday |
289 | end_of_week(:monday) |
290 | end |
291 | |
292 | # Returns a new date/time representing the end of the month. |
293 | # DateTime objects will have a time set to 23:59:59. |
294 | def end_of_month |
295 | last_day = ::Time.days_in_month(month, year) |
296 | last_hour(days_since(last_day - day)) |
297 | end |
298 | alias :at_end_of_month :end_of_month |
299 | |
300 | # Returns a new date/time representing the end of the year. |
301 | # DateTime objects will have a time set to 23:59:59. |
302 | def end_of_year |
303 | change(month: 12).end_of_month |
304 | end |
305 | alias :at_end_of_year :end_of_year |
306 | |
307 | # Returns a Range representing the whole day of the current date/time. |
308 | def all_day |
309 | beginning_of_day..end_of_day |
310 | end |
311 | |
312 | # Returns a Range representing the whole week of the current date/time. |
313 | # Week starts on start_day, default is <tt>Date.beginning_of_week</tt> or <tt>config.beginning_of_week</tt> when set. |
314 | def all_week(start_day = Date.beginning_of_week) |
315 | beginning_of_week(start_day)..end_of_week(start_day) |
316 | end |
317 | |
318 | # Returns a Range representing the whole month of the current date/time. |
319 | def all_month |
320 | beginning_of_month..end_of_month |
321 | end |
322 | |
323 | # Returns a Range representing the whole quarter of the current date/time. |
324 | def all_quarter |
325 | beginning_of_quarter..end_of_quarter |
326 | end |
327 | |
328 | # Returns a Range representing the whole year of the current date/time. |
329 | def all_year |
330 | beginning_of_year..end_of_year |
331 | end |
332 | |
333 | # Returns a new date/time representing the next occurrence of the specified day of week. |
334 | # |
335 | # today = Date.today # => Thu, 14 Dec 2017 |
336 | # today.next_occurring(:monday) # => Mon, 18 Dec 2017 |
337 | # today.next_occurring(:thursday) # => Thu, 21 Dec 2017 |
338 | def next_occurring(day_of_week) |
339 | current_day_number = wday != 0 ? wday - 1 : 6 |
340 | from_now = DAYS_INTO_WEEK.fetch(day_of_week) - current_day_number |
341 | from_now += 7 unless from_now > 0 |
342 | advance(days: from_now) |
343 | end |
344 | |
345 | # Returns a new date/time representing the previous occurrence of the specified day of week. |
346 | # |
347 | # today = Date.today # => Thu, 14 Dec 2017 |
348 | # today.prev_occurring(:monday) # => Mon, 11 Dec 2017 |
349 | # today.prev_occurring(:thursday) # => Thu, 07 Dec 2017 |
350 | def prev_occurring(day_of_week) |
351 | current_day_number = wday != 0 ? wday - 1 : 6 |
352 | ago = current_day_number - DAYS_INTO_WEEK.fetch(day_of_week) |
353 | ago += 7 unless ago > 0 |
354 | advance(days: -ago) |
355 | end |
356 | |
357 | private |
358 | def first_hour(date_or_time) |
359 | date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time |
360 | end |
361 | |
362 | def last_hour(date_or_time) |
363 | date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time |
364 | end |
365 | |
366 | def days_span(day) |
367 | (DAYS_INTO_WEEK[day] - DAYS_INTO_WEEK[Date.beginning_of_week]) % 7 |
368 | end |
369 | |
370 | def copy_time_to(other) |
371 | other.change(hour: hour, min: min, sec: sec, nsec: try(:nsec)) |
372 | end |
373 | end |
374 | end |