fat_core 4.7.0 → 4.7.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.org +377 -0
- data/fat_core.gemspec +3 -3
- data/lib/fat_core/bigdecimal.rb +2 -0
- data/lib/fat_core/date.rb +217 -149
- data/lib/fat_core/enumerable.rb +6 -6
- data/lib/fat_core/hash.rb +9 -10
- data/lib/fat_core/numeric.rb +5 -2
- data/lib/fat_core/patches.rb +2 -0
- data/lib/fat_core/range.rb +6 -1
- data/lib/fat_core/string.rb +25 -28
- data/lib/fat_core/version.rb +1 -1
- data/spec/lib/date_spec.rb +19 -1
- data/spec/lib/string_spec.rb +10 -3
- metadata +9 -9
- data/README.md +0 -165
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66c2dafd995954b7775a200600f938570b7bc5ad0fd5e240fc8fabb1496b69ba
|
4
|
+
data.tar.gz: a3b3ff6b1dfd0a81afd0ed89b24501e7ade68e9b14e40e8a3da54a59e79f39a2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f5ada6fefbf2b7cd2a5adbc7f62bc207ad7b8efb55e52874ec0b6e0a8e46a7bd5bc4d5e35f135fe20ea8a6b619f28e14adecf38ea5a2b01b92dfc5aaaf9b5f31
|
7
|
+
data.tar.gz: 621a7ace7268390f3dd900bc0dc7ac38c2a33381cb54bda460a4f8b0f7d8b273b907007299db2c893518c99f846325312095dd7a24b63783da800909487ade51
|
data/README.org
ADDED
@@ -0,0 +1,377 @@
|
|
1
|
+
[[https://travis-ci.org/ddoherty03/fat_core.svg?branch=master]]
|
2
|
+
|
3
|
+
* FatCore
|
4
|
+
|
5
|
+
fat-core is a simple gem to collect core extensions and a few new classes that
|
6
|
+
I find useful in multiple projects. The emphasis is on extending the Date
|
7
|
+
class to make it more useful in financial applications.
|
8
|
+
|
9
|
+
** Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
#+begin_SRC ruby
|
14
|
+
gem 'fat_core', :git => 'https://github.com/ddoherty03/fat_core.git'
|
15
|
+
#+end_SRC
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
#+begin_src shell
|
20
|
+
$ bundle
|
21
|
+
#+end_src
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
#+begin_src shell
|
26
|
+
$ gem install fat_core
|
27
|
+
#+end_src
|
28
|
+
|
29
|
+
** Usage
|
30
|
+
|
31
|
+
You can extend classes individually by requiring the corresponding file:
|
32
|
+
|
33
|
+
#+begin_SRC ruby
|
34
|
+
require 'fat_core/array'
|
35
|
+
require 'fat_core/bigdecimal'
|
36
|
+
require 'fat_core/date'
|
37
|
+
require 'fat_core/enumerable'
|
38
|
+
require 'fat_core/hash'
|
39
|
+
require 'fat_core/kernel'
|
40
|
+
require 'fat_core/numeric'
|
41
|
+
require 'fat_core/range'
|
42
|
+
require 'fat_core/string'
|
43
|
+
require 'fat_core/symbol'
|
44
|
+
#+end_SRC
|
45
|
+
|
46
|
+
|
47
|
+
Or, you can require them all:
|
48
|
+
|
49
|
+
#+begin_SRC ruby
|
50
|
+
require 'fat_core/all'
|
51
|
+
#+end_SRC
|
52
|
+
|
53
|
+
Many of these have little that is of general interest, but there are a few
|
54
|
+
goodies.
|
55
|
+
|
56
|
+
*** Date
|
57
|
+
|
58
|
+
**** Constants
|
59
|
+
|
60
|
+
~FatCore~ adds two constants to the ~Date~ class, Date::BOT and Date::EOT.
|
61
|
+
These represent the earliest and latest dates of practical commercial
|
62
|
+
interest. The exact values are rather arbitrary, but they prove useful in
|
63
|
+
date ranges, for example. They are defined as:
|
64
|
+
|
65
|
+
- ~Date::BOT~ :: January 1, 1900
|
66
|
+
- ~Date::EOT~ :: December 31, 3000
|
67
|
+
- ~Date::FEDERAL_DECREED_HOLIDAYS~ :: an Array of dates declared as non-work
|
68
|
+
days for federal employees by presidential proclamation
|
69
|
+
- ~Date::PRESIDENTIAL_FUNERALS~ :: an Array of dates of presidential funerals,
|
70
|
+
which are observed with a closing of most federal agencies
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
**** Formatting
|
75
|
+
|
76
|
+
~FatCore~ provides some concise methods for printing string versions of dates
|
77
|
+
that are often useful:
|
78
|
+
|
79
|
+
#+begin_SRC ruby :results output :wrap example :exports both
|
80
|
+
require 'fat_core/date'
|
81
|
+
d = Date.parse('1957-09-22')
|
82
|
+
puts "ISO: #{d.iso}"
|
83
|
+
puts "All Numbers: #{d.num}"
|
84
|
+
puts "Emacs Org Mode Inactive: #{d.org}"
|
85
|
+
puts "Emacs Org Mode Active: #{d.org(true)}"
|
86
|
+
puts "LaTeX: #{d.tex_quote}"
|
87
|
+
puts "English: #{d.eng}"
|
88
|
+
puts "American: #{d.american}"
|
89
|
+
#+end_SRC
|
90
|
+
|
91
|
+
#+RESULTS:
|
92
|
+
#+begin_example
|
93
|
+
ISO: 1957-09-22
|
94
|
+
All Numbers: 19570922
|
95
|
+
Emacs Org Mode Inactive: [1957-09-22 Sun]
|
96
|
+
Emacs Org Mode Active: <1957-09-22 Sun>
|
97
|
+
LaTeX: 1957--09--22
|
98
|
+
English: September 22, 1957
|
99
|
+
American: 9/22/1957
|
100
|
+
#+end_example
|
101
|
+
|
102
|
+
Most of these are self-explanatory, but a couple are not. The ~#org~ method
|
103
|
+
formats a date as an Emacs org-mode timestamp, by default an inactive
|
104
|
+
timestamp that does not show up in the org agenda, but can be made active with
|
105
|
+
the optional parameter set to a truthy value. See
|
106
|
+
[[https://orgmode.org/manual/Timestamps.html#Timestamps]].
|
107
|
+
|
108
|
+
The ~#tex_quote~ method formats the date in iso form but using TeX's
|
109
|
+
convention of using en-dashes to separate the components.
|
110
|
+
|
111
|
+
**** Chunks
|
112
|
+
|
113
|
+
Many of the methods provided by ~FatCore~ deal with various calendar periods
|
114
|
+
that are less common than those provided by the Ruby Standard Library or gems
|
115
|
+
such as ~active_suupor~. This documentation refers to these calendar periods
|
116
|
+
as "chunks", and they are the following:
|
117
|
+
|
118
|
+
- year,
|
119
|
+
- half,
|
120
|
+
- quarter,
|
121
|
+
- bimonth,
|
122
|
+
- month,
|
123
|
+
- semimonth,
|
124
|
+
- biweek,
|
125
|
+
- week, and
|
126
|
+
- day
|
127
|
+
|
128
|
+
~FatCore~ provides methods that query whether the date falls on the beginning
|
129
|
+
or end of each of these chunks:
|
130
|
+
|
131
|
+
#+begin_SRC ruby :wrap example :exports both
|
132
|
+
require 'fat_core/date'
|
133
|
+
|
134
|
+
tab = []
|
135
|
+
d = Date.parse('2017-06-30')
|
136
|
+
%i[beginning end].each do |side|
|
137
|
+
%i(year half quarter bimonth month semimonth biweek week ).each do |chunk|
|
138
|
+
meth = "#{side}_of_#{chunk}?".to_sym
|
139
|
+
tab << [d.iso, meth.to_s, "#{d.send(meth)}"]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
tab
|
143
|
+
#+end_SRC
|
144
|
+
|
145
|
+
#+RESULTS:
|
146
|
+
#+begin_example
|
147
|
+
| 2017-06-30 | beginning_of_year? | false |
|
148
|
+
| 2017-06-30 | beginning_of_half? | false |
|
149
|
+
| 2017-06-30 | beginning_of_quarter? | false |
|
150
|
+
| 2017-06-30 | beginning_of_bimonth? | false |
|
151
|
+
| 2017-06-30 | beginning_of_month? | false |
|
152
|
+
| 2017-06-30 | beginning_of_semimonth? | false |
|
153
|
+
| 2017-06-30 | beginning_of_biweek? | false |
|
154
|
+
| 2017-06-30 | beginning_of_week? | false |
|
155
|
+
| 2017-06-30 | end_of_year? | false |
|
156
|
+
| 2017-06-30 | end_of_half? | true |
|
157
|
+
| 2017-06-30 | end_of_quarter? | true |
|
158
|
+
| 2017-06-30 | end_of_bimonth? | true |
|
159
|
+
| 2017-06-30 | end_of_month? | true |
|
160
|
+
| 2017-06-30 | end_of_semimonth? | true |
|
161
|
+
| 2017-06-30 | end_of_biweek? | false |
|
162
|
+
| 2017-06-30 | end_of_week? | false |
|
163
|
+
#+end_example
|
164
|
+
|
165
|
+
It also provides corresponding methods that return the date at the beginning
|
166
|
+
or end of the calendar chunk, starting at the given date:
|
167
|
+
|
168
|
+
#+begin_SRC ruby :wrap example :exports both
|
169
|
+
require 'fat_core/date'
|
170
|
+
|
171
|
+
tab = []
|
172
|
+
d = Date.parse('2017-04-21')
|
173
|
+
%i[beginning end].each do |side|
|
174
|
+
%i(year half quarter bimonth month semimonth biweek week ).each do |chunk|
|
175
|
+
meth = "#{side}_of_#{chunk}".to_sym
|
176
|
+
tab << [d.iso, "d.#{meth}", "#{d.send(meth)}"]
|
177
|
+
end
|
178
|
+
end
|
179
|
+
tab
|
180
|
+
#+end_SRC
|
181
|
+
|
182
|
+
#+RESULTS:
|
183
|
+
#+begin_example
|
184
|
+
| 2017-04-21 | d.beginning_of_year | 2017-01-01 |
|
185
|
+
| 2017-04-21 | d.beginning_of_half | 2017-01-01 |
|
186
|
+
| 2017-04-21 | d.beginning_of_quarter | 2017-04-01 |
|
187
|
+
| 2017-04-21 | d.beginning_of_bimonth | 2017-03-01 |
|
188
|
+
| 2017-04-21 | d.beginning_of_month | 2017-04-01 |
|
189
|
+
| 2017-04-21 | d.beginning_of_semimonth | 2017-04-16 |
|
190
|
+
| 2017-04-21 | d.beginning_of_biweek | 2017-04-10 |
|
191
|
+
| 2017-04-21 | d.beginning_of_week | 2017-04-17 |
|
192
|
+
| 2017-04-21 | d.end_of_year | 2017-12-31 |
|
193
|
+
| 2017-04-21 | d.end_of_half | 2017-06-30 |
|
194
|
+
| 2017-04-21 | d.end_of_quarter | 2017-06-30 |
|
195
|
+
| 2017-04-21 | d.end_of_bimonth | 2017-04-30 |
|
196
|
+
| 2017-04-21 | d.end_of_month | 2017-04-30 |
|
197
|
+
| 2017-04-21 | d.end_of_semimonth | 2017-04-30 |
|
198
|
+
| 2017-04-21 | d.end_of_biweek | 2017-04-23 |
|
199
|
+
| 2017-04-21 | d.end_of_week | 2017-04-23 |
|
200
|
+
#+end_example
|
201
|
+
|
202
|
+
You can query which numerical half, quarter, etc. that a given date falls in:
|
203
|
+
|
204
|
+
#+begin_SRC ruby :exports both :wrap example
|
205
|
+
require 'fat_core/date'
|
206
|
+
|
207
|
+
tab = []
|
208
|
+
%i(year half quarter bimonth month semimonth biweek week ).each do |chunk|
|
209
|
+
d = Date.parse('2017-04-21') + rand(100)
|
210
|
+
meth = "#{chunk}".to_sym
|
211
|
+
tab << [d.iso, "d.#{meth}", "in #{chunk} number #{d.send(meth)}"]
|
212
|
+
end
|
213
|
+
tab
|
214
|
+
#+end_SRC
|
215
|
+
|
216
|
+
#+RESULTS:
|
217
|
+
#+begin_example
|
218
|
+
| 2017-07-05 | d.year | in year number 2017 |
|
219
|
+
| 2017-06-03 | d.half | in half number 1 |
|
220
|
+
| 2017-05-30 | d.quarter | in quarter number 2 |
|
221
|
+
| 2017-07-08 | d.bimonth | in bimonth number 4 |
|
222
|
+
| 2017-06-28 | d.month | in month number 6 |
|
223
|
+
| 2017-05-14 | d.semimonth | in semimonth number 9 |
|
224
|
+
| 2017-07-25 | d.biweek | in biweek number 15 |
|
225
|
+
| 2017-06-19 | d.week | in week number 25 |
|
226
|
+
#+end_example
|
227
|
+
|
228
|
+
**** Parsing
|
229
|
+
|
230
|
+
~FatCore~ also adds some convenience methods for parsing strings as ~Date~
|
231
|
+
objects.
|
232
|
+
|
233
|
+
***** American Dates
|
234
|
+
|
235
|
+
Americans often write dates in the form M/d/Y, and the normal parse method
|
236
|
+
will parse such a string as d/M/Y, often resulting in invalid date errors.
|
237
|
+
~FatCore~ adds the specialty parsing method, ~Date.parse_american~ to handle
|
238
|
+
such strings.
|
239
|
+
|
240
|
+
#+begin_SRC ruby :results output :exports both :wrap example
|
241
|
+
require 'fat_core/date'
|
242
|
+
|
243
|
+
begin
|
244
|
+
ss = '9/22/1957'
|
245
|
+
Date.parse(ss)
|
246
|
+
rescue Date::Error => ex
|
247
|
+
puts "Date.parse('#{ss}') raises #{ex.class} (#{ex}), but"
|
248
|
+
puts "Date.parse_american('#{ss}') => #{Date.parse_american(ss)}"
|
249
|
+
end
|
250
|
+
#+end_SRC
|
251
|
+
|
252
|
+
#+RESULTS:
|
253
|
+
#+begin_example
|
254
|
+
Date.parse('9/22/1957') raises Date::Error (invalid date), but
|
255
|
+
Date.parse_american('9/22/1957') => 1957-09-22
|
256
|
+
#+end_example
|
257
|
+
|
258
|
+
***** Date Specs
|
259
|
+
|
260
|
+
|
261
|
+
|
262
|
+
**** Holidays and Workdays
|
263
|
+
|
264
|
+
- weekend?
|
265
|
+
- weekday?
|
266
|
+
|
267
|
+
**** Weekdays in Month
|
268
|
+
|
269
|
+
**** Easter
|
270
|
+
|
271
|
+
|
272
|
+
The ~Date~ class extension adds two methods for determining whether a given
|
273
|
+
date is a US federal holiday as defined by federal law, including such things
|
274
|
+
as federal holidays established by executive decree:
|
275
|
+
|
276
|
+
#+begin_SRC ruby
|
277
|
+
require 'fat_core/date'
|
278
|
+
Date.parse('2014-05-18').fed_holiday? => true # It's a weekend
|
279
|
+
Date.parse('2014-01-01').fed_holiday? => true # It's New Years
|
280
|
+
#+end_SRC
|
281
|
+
|
282
|
+
Likewise, days on which the NYSE is closed can be gotten with:
|
283
|
+
|
284
|
+
#+begin_SRC ruby
|
285
|
+
Date.parse('2014-04-18').nyse_holiday? => true # It's Good Friday
|
286
|
+
#+end_SRC
|
287
|
+
|
288
|
+
Conversely, ~Date#fed_workday?~ and ~Date#nyse_workday?~ return true if the
|
289
|
+
federal government and the NYSE respectively are open for business on those
|
290
|
+
days.
|
291
|
+
|
292
|
+
In addition, the Date class, as extended by FatCore, adds ~#next_<chunk>~
|
293
|
+
methods for calendar periods in addition to those provided by the core Date
|
294
|
+
class: ~#next_half~, ~#next_quarter~, ~#next_bimonth~, and ~#next_semimonth~,
|
295
|
+
~#next_biweek~. There are also ~#prior_<chunk>~ variants of these, as well as
|
296
|
+
methods for finding the end and beginning of all these periods (e.g.,
|
297
|
+
~#beginning_of_bimonth~) and for querying whether a Date is at the beginning or
|
298
|
+
end of these periods (e.g., ~#beginning_of_bimonth?~, ~#end_of_bimonth?~, etc.).
|
299
|
+
|
300
|
+
FatCore also provides convenience formatting methods, such as ~Date#iso~ for
|
301
|
+
quickly converting a Date to a string of the form 'YYYY-MM-DD', ~Date#org~ for
|
302
|
+
formatting a Date as an Emacs org-mode timestamp, and several others.
|
303
|
+
|
304
|
+
Finally, it provides a ~#parse_spec~ method for parsing a string, typically
|
305
|
+
provided by a user, allowing all the period chunks to be conveniently and
|
306
|
+
tersely specified by a user. For example, the string '2Q' will be parsed as the
|
307
|
+
second calendar quarter of the current year, while '2014-3Q' will be parsed as
|
308
|
+
the third quarter of the year 2014.
|
309
|
+
|
310
|
+
*** Range
|
311
|
+
|
312
|
+
You can also extend the Range class with several useful methods that emphasize
|
313
|
+
coverage of one range by one or more others (~#spanned_by?~ and ~#gaps~),
|
314
|
+
contiguity of Ranges to one another (~#contiguous?~, ~#left_contiguous?~, and
|
315
|
+
~#right_contiguous?~, ~#join~), and the testing of overlaps between ranges
|
316
|
+
(~#overlaps?~, ~#overlaps_among?~). These are put to good use in the
|
317
|
+
'fat_period' ([[https://github.com/ddoherty03/fat_period]]) gem, which combines
|
318
|
+
fat_core's extended Range class with its extended Date class to make a useful
|
319
|
+
Period class for date ranges, and you may find fat_core's extended Range class
|
320
|
+
likewise useful.
|
321
|
+
|
322
|
+
For example, you can use the ~#gaps~ method to find the gaps left in the
|
323
|
+
coverage on one Range by an Array of other Ranges:
|
324
|
+
|
325
|
+
#+begin_SRC ruby
|
326
|
+
require 'fat_core/range'
|
327
|
+
(0..12).gaps([(0..2), (5..7), (10..12)]) => [(3..4), (8..9)]
|
328
|
+
#+end_SRC
|
329
|
+
|
330
|
+
**
|
331
|
+
* Enumerable
|
332
|
+
|
333
|
+
FatCore::Enumerable extends Enumerable with the ~#each_with_flags~ method that
|
334
|
+
yields the elements of the Enumerable but also yields two booleans, ~first~ and
|
335
|
+
~last~ that are set to true on respectively, the first and last element of the
|
336
|
+
Enumerable. This makes it easy to treat these two cases specially without
|
337
|
+
testing the index as in ~#each_with_index~.
|
338
|
+
|
339
|
+
*** Hash
|
340
|
+
|
341
|
+
FatCore::Hash extends the Hash class with some useful methods for element
|
342
|
+
deletion (~#delete_with_value~) and for manipulating the keys
|
343
|
+
(~#keys_with_value~, ~#remap_keys~ and ~#replace_keys~) of a Hash. It also
|
344
|
+
provides ~#each_pair_with_flags~ as an analog to Enumerable's
|
345
|
+
~#each_with_flags~.
|
346
|
+
|
347
|
+
*** String
|
348
|
+
|
349
|
+
FatCore::String has methods for performing matching of one string with another
|
350
|
+
(~#matches_with~, ~#fuzzy_match~), for converting a string to title-case as
|
351
|
+
might by used in the title of a book (~#entitle~), for converting a String into
|
352
|
+
a useable Symbol (~#as_sym~) and vice-versa (~#as_string~ also
|
353
|
+
~Symbol#as_string~), for wrapping with an optional hanging indent (~#wrap~),
|
354
|
+
cleaning up errant spaces (~#clean~), and computing the Damerau-Levenshtein
|
355
|
+
distance between strings (~#distance~). And several others.
|
356
|
+
|
357
|
+
*** TeX Quoting
|
358
|
+
|
359
|
+
Several of the extension, most notably 'fat_core/string', provides a
|
360
|
+
~#tex_quote~ method for quoting the string version of an object so as to allow
|
361
|
+
its inclusion in a TeX document and quote characters such as '$' or '%' that
|
362
|
+
have a special meaning for TeX.
|
363
|
+
|
364
|
+
*** Numbers
|
365
|
+
|
366
|
+
FatCore::Numeric has methods for inserting grouping commas into a number
|
367
|
+
(~#commas~ and ~#group~), for converting seconds to HH:MM:SS.dd format
|
368
|
+
(~#secs_to_hms~), for testing for integrality (~#whole?~ and ~#int_if_whole~), and
|
369
|
+
testing for sign (~#signum~).
|
370
|
+
|
371
|
+
** Contributing
|
372
|
+
|
373
|
+
1. Fork it ([[http://github.com/ddoherty03/fat_core/fork]] )
|
374
|
+
2. Create your feature branch (~git checkout -b my-new-feature~)
|
375
|
+
3. Commit your changes (~git commit -am 'Add some feature'~)
|
376
|
+
4. Push to the branch (~git push origin my-new-feature~)
|
377
|
+
5. Create new Pull Request
|
data/fat_core.gemspec
CHANGED
@@ -10,8 +10,8 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.authors = ['Daniel E. Doherty']
|
11
11
|
spec.email = ['ded@ddoherty.net']
|
12
12
|
spec.summary = 'fat_core provides some useful core extensions'
|
13
|
-
spec.description = '
|
14
|
-
spec.homepage = ''
|
13
|
+
spec.description = 'Useful extensions to Date, String, Range and other classes'
|
14
|
+
spec.homepage = 'https://github.com/ddoherty03/fat_core.git'
|
15
15
|
spec.license = 'MIT'
|
16
16
|
spec.required_ruby_version = '>= 2.2.2'
|
17
17
|
spec.metadata['yard.run'] = 'yri' # use "yard" to build full HTML docs.
|
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.require_paths = ['lib']
|
24
24
|
|
25
25
|
spec.add_development_dependency 'simplecov'
|
26
|
-
spec.add_development_dependency 'bundler'
|
26
|
+
spec.add_development_dependency 'bundler'
|
27
27
|
spec.add_development_dependency 'rake'
|
28
28
|
spec.add_development_dependency 'rspec'
|
29
29
|
spec.add_development_dependency 'byebug'
|
data/lib/fat_core/bigdecimal.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'bigdecimal'
|
2
2
|
|
3
3
|
module FatCore
|
4
|
+
# Extensions to BigDecimal class
|
4
5
|
module BigDecimal
|
5
6
|
# Provide a human-readable display for BigDecimal. e.g., while debugging.
|
6
7
|
# The inspect method in BigDecimal is unreadable, as it exposes the
|
@@ -13,6 +14,7 @@ module FatCore
|
|
13
14
|
end
|
14
15
|
end
|
15
16
|
|
17
|
+
# Override the core inspect method.
|
16
18
|
class BigDecimal
|
17
19
|
prepend(FatCore::BigDecimal)
|
18
20
|
# @!parse include FatCore::BigDecimal
|
data/lib/fat_core/date.rb
CHANGED
@@ -84,8 +84,12 @@ module FatCore
|
|
84
84
|
# Format as an inactive Org date timestamp of the form `[YYYY-MM-DD <dow>]`
|
85
85
|
# (see Emacs org-mode)
|
86
86
|
# @return [String]
|
87
|
-
def org
|
88
|
-
|
87
|
+
def org(active = false)
|
88
|
+
if active
|
89
|
+
strftime('<%Y-%m-%d %a>')
|
90
|
+
else
|
91
|
+
strftime('[%Y-%m-%d %a]')
|
92
|
+
end
|
89
93
|
end
|
90
94
|
|
91
95
|
# :category: Formatting
|
@@ -108,20 +112,6 @@ module FatCore
|
|
108
112
|
# :category: Queries
|
109
113
|
# @group Queries
|
110
114
|
|
111
|
-
# Does self fall on a weekend?
|
112
|
-
# @return [Boolean]
|
113
|
-
def weekend?
|
114
|
-
saturday? || sunday?
|
115
|
-
end
|
116
|
-
|
117
|
-
# :category: Queries
|
118
|
-
|
119
|
-
# Does self fall on a weekday?
|
120
|
-
# @return [Boolean]
|
121
|
-
def weekday?
|
122
|
-
!weekend?
|
123
|
-
end
|
124
|
-
|
125
115
|
# :category: Queries
|
126
116
|
|
127
117
|
# Self's calendar "half" by analogy to calendar quarters: 1 or 2, depending
|
@@ -155,6 +145,50 @@ module FatCore
|
|
155
145
|
end
|
156
146
|
end
|
157
147
|
|
148
|
+
# Self's calendar bimonth: 1, 2, 3, 4, 5, or 6 depending on which calendar
|
149
|
+
# bimonth the date falls in.
|
150
|
+
# @return [1, 2, 3, 4, 5, 6]
|
151
|
+
def bimonth
|
152
|
+
case month
|
153
|
+
when (1..2)
|
154
|
+
1
|
155
|
+
when (3..4)
|
156
|
+
2
|
157
|
+
when (5..6)
|
158
|
+
3
|
159
|
+
when (7..8)
|
160
|
+
4
|
161
|
+
when (9..10)
|
162
|
+
5
|
163
|
+
when (11..12)
|
164
|
+
6
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Self's calendar semimonth: 1, through 24 depending on which calendar
|
169
|
+
# semimonth the date falls in.
|
170
|
+
# @return [Integer]
|
171
|
+
def semimonth
|
172
|
+
(month - 1) * 2 + (day <= 15 ? 1 : 2)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Self's calendar biweek: 1, through 24 depending on which calendar
|
176
|
+
# semimonth the date falls in.
|
177
|
+
# @return [Integer]
|
178
|
+
def biweek
|
179
|
+
if cweek.odd?
|
180
|
+
(cweek + 1) / 2
|
181
|
+
else
|
182
|
+
cweek / 2
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Self's calendar week: just calls cweek.
|
187
|
+
# @return [Integer]
|
188
|
+
def week
|
189
|
+
cweek
|
190
|
+
end
|
191
|
+
|
158
192
|
# :category: Queries
|
159
193
|
|
160
194
|
# Return whether the date falls on the first day of a year.
|
@@ -315,13 +349,13 @@ module FatCore
|
|
315
349
|
# dates 2 days after date six months before and 2 days before the date six
|
316
350
|
# months after the date `d`.
|
317
351
|
#
|
318
|
-
# @param
|
352
|
+
# @param from_date [::Date] the middle of the six-month range
|
319
353
|
# @return [Boolean]
|
320
|
-
def within_6mos_of?(
|
354
|
+
def within_6mos_of?(from_date)
|
321
355
|
# ::Date 6 calendar months before self
|
322
356
|
start_date = self - 6.months + 2.days
|
323
357
|
end_date = self + 6.months - 2.days
|
324
|
-
(start_date..end_date).cover?(
|
358
|
+
(start_date..end_date).cover?(from_date)
|
325
359
|
end
|
326
360
|
|
327
361
|
# Return whether this date is Easter Sunday for the year in which it falls
|
@@ -337,15 +371,15 @@ module FatCore
|
|
337
371
|
# Return whether this date is the `n`th weekday `wday` of the given `month` in
|
338
372
|
# this date's year.
|
339
373
|
#
|
340
|
-
# @param
|
374
|
+
# @param nth [Integer] number of wday in month, if negative count from end of
|
341
375
|
# the month
|
342
376
|
# @param wday [Integer] day of week, 0 is Sunday, 1 Monday, etc.
|
343
377
|
# @param month [Integer] the month number, 1 is January, 2 is February, etc.
|
344
378
|
# @return [Boolean]
|
345
|
-
def nth_wday_in_month?(
|
379
|
+
def nth_wday_in_month?(nth, wday, month)
|
346
380
|
# Is self the nth weekday in the given month of its year?
|
347
|
-
# If
|
348
|
-
self == ::Date.nth_wday_in_year_month(
|
381
|
+
# If nth is negative, count from last day of month
|
382
|
+
self == ::Date.nth_wday_in_year_month(nth, wday, year, month)
|
349
383
|
end
|
350
384
|
|
351
385
|
# :category: Relative ::Dates
|
@@ -478,61 +512,64 @@ module FatCore
|
|
478
512
|
# Return the date that is +n+ calendar halves after this date, where a
|
479
513
|
# calendar half is a period of 6 months.
|
480
514
|
#
|
481
|
-
# @param
|
515
|
+
# @param num [Integer] number of halves to advance, can be negative
|
482
516
|
# @return [::Date] new date n halves after this date
|
483
|
-
def next_half(
|
484
|
-
|
485
|
-
return self if
|
486
|
-
|
517
|
+
def next_half(num = 1)
|
518
|
+
num = num.floor
|
519
|
+
return self if num.zero?
|
520
|
+
|
521
|
+
next_month(num * 6)
|
487
522
|
end
|
488
523
|
|
489
524
|
# Return the date that is +n+ calendar halves before this date, where a
|
490
525
|
# calendar half is a period of 6 months.
|
491
526
|
#
|
492
|
-
# @param
|
527
|
+
# @param num [Integer] number of halves to retreat, can be negative
|
493
528
|
# @return [::Date] new date n halves before this date
|
494
|
-
def prior_half(
|
495
|
-
next_half(-
|
529
|
+
def prior_half(num = 1)
|
530
|
+
next_half(-num)
|
496
531
|
end
|
497
532
|
|
498
533
|
# Return the date that is +n+ calendar quarters after this date, where a
|
499
534
|
# calendar quarter is a period of 3 months.
|
500
535
|
#
|
501
|
-
# @param
|
536
|
+
# @param num [Integer] number of quarters to advance, can be negative
|
502
537
|
# @return [::Date] new date n quarters after this date
|
503
|
-
def next_quarter(
|
504
|
-
|
505
|
-
return self if
|
506
|
-
|
538
|
+
def next_quarter(num = 1)
|
539
|
+
num = num.floor
|
540
|
+
return self if num.zero?
|
541
|
+
|
542
|
+
next_month(num * 3)
|
507
543
|
end
|
508
544
|
|
509
545
|
# Return the date that is +n+ calendar quarters before this date, where a
|
510
546
|
# calendar quarter is a period of 3 months.
|
511
547
|
#
|
512
|
-
# @param
|
548
|
+
# @param num [Integer] number of quarters to retreat, can be negative
|
513
549
|
# @return [::Date] new date n quarters after this date
|
514
|
-
def prior_quarter(
|
515
|
-
next_quarter(-
|
550
|
+
def prior_quarter(num = 1)
|
551
|
+
next_quarter(-num)
|
516
552
|
end
|
517
553
|
|
518
554
|
# Return the date that is +n+ calendar bimonths after this date, where a
|
519
555
|
# calendar bimonth is a period of 2 months.
|
520
556
|
#
|
521
|
-
# @param
|
557
|
+
# @param num [Integer] number of bimonths to advance, can be negative
|
522
558
|
# @return [::Date] new date n bimonths after this date
|
523
|
-
def next_bimonth(
|
524
|
-
|
525
|
-
return self if
|
526
|
-
|
559
|
+
def next_bimonth(num = 1)
|
560
|
+
num = num.floor
|
561
|
+
return self if num.zero?
|
562
|
+
|
563
|
+
next_month(num * 2)
|
527
564
|
end
|
528
565
|
|
529
566
|
# Return the date that is +n+ calendar bimonths before this date, where a
|
530
567
|
# calendar bimonth is a period of 2 months.
|
531
568
|
#
|
532
|
-
# @param
|
569
|
+
# @param num [Integer] number of bimonths to retreat, can be negative
|
533
570
|
# @return [::Date] new date n bimonths before this date
|
534
|
-
def prior_bimonth(
|
535
|
-
next_bimonth(-
|
571
|
+
def prior_bimonth(num = 1)
|
572
|
+
next_bimonth(-num)
|
536
573
|
end
|
537
574
|
|
538
575
|
# Return the date that is +n+ semimonths after this date. Each semimonth begins
|
@@ -542,15 +579,16 @@ module FatCore
|
|
542
579
|
# as far into the next month past the 1st as the current date is past the
|
543
580
|
# 16th, but never past the 15th of the next month.
|
544
581
|
#
|
545
|
-
# @param
|
582
|
+
# @param num [Integer] number of semimonths to advance, can be negative
|
546
583
|
# @return [::Date] new date n semimonths after this date
|
547
|
-
def next_semimonth(
|
548
|
-
|
549
|
-
return self if
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
584
|
+
def next_semimonth(num = 1)
|
585
|
+
num = num.floor
|
586
|
+
return self if num.zero?
|
587
|
+
|
588
|
+
factor = num.negative? ? -1 : 1
|
589
|
+
num = num.abs
|
590
|
+
if num.even?
|
591
|
+
next_month(num / 2)
|
554
592
|
else
|
555
593
|
# Advance or retreat one semimonth
|
556
594
|
next_sm =
|
@@ -577,23 +615,21 @@ module FatCore
|
|
577
615
|
[prior_month.beginning_of_month + 16.days + (day - 1).days,
|
578
616
|
prior_month.end_of_month].min
|
579
617
|
end
|
580
|
-
|
618
|
+
elsif factor.positive?
|
581
619
|
# In the second half of the month (17th to the 31st), go as many
|
582
620
|
# days into the next month as we are past the 16th. Thus, as many as
|
583
621
|
# 15 days. But we don't want to go past the first half of the next
|
584
622
|
# month, so we only go so far as the 15th of the next month.
|
585
623
|
# ::Date.parse('2015-02-18').next_semimonth should be the 3rd of the
|
586
624
|
# following month.
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
beginning_of_month + [(day - 16), 15].min
|
591
|
-
end
|
625
|
+
next_month.beginning_of_month + [(day - 16), 15].min
|
626
|
+
else
|
627
|
+
beginning_of_month + [(day - 16), 15].min
|
592
628
|
end
|
593
|
-
|
629
|
+
num -= 1
|
594
630
|
# Now that n is even, advance (or retreat) n / 2 months unless we're done.
|
595
|
-
if
|
596
|
-
next_sm.next_month(factor *
|
631
|
+
if num >= 2
|
632
|
+
next_sm.next_month(factor * num / 2)
|
597
633
|
else
|
598
634
|
next_sm
|
599
635
|
end
|
@@ -608,30 +644,31 @@ module FatCore
|
|
608
644
|
# as the current date is past the 15th, but never past the 14th of the the
|
609
645
|
# current month.
|
610
646
|
#
|
611
|
-
# @param
|
647
|
+
# @param num [Integer] number of semimonths to retreat, can be negative
|
612
648
|
# @return [::Date] new date n semimonths before this date
|
613
|
-
def prior_semimonth(
|
614
|
-
next_semimonth(-
|
649
|
+
def prior_semimonth(num = 1)
|
650
|
+
next_semimonth(-num)
|
615
651
|
end
|
616
652
|
|
617
653
|
# Return the date that is +n+ biweeks after this date where each biweek is 14
|
618
654
|
# days.
|
619
655
|
#
|
620
|
-
# @param
|
656
|
+
# @param num [Integer] number of biweeks to advance, can be negative
|
621
657
|
# @return [::Date] new date n biweeks after this date
|
622
|
-
def next_biweek(
|
623
|
-
|
624
|
-
return self if
|
625
|
-
|
658
|
+
def next_biweek(num = 1)
|
659
|
+
num = num.floor
|
660
|
+
return self if num.zero?
|
661
|
+
|
662
|
+
self + (14 * num)
|
626
663
|
end
|
627
664
|
|
628
665
|
# Return the date that is +n+ biweeks before this date where each biweek is 14
|
629
666
|
# days.
|
630
667
|
#
|
631
|
-
# @param
|
668
|
+
# @param num [Integer] number of biweeks to retreat, can be negative
|
632
669
|
# @return [::Date] new date n biweeks before this date
|
633
|
-
def prior_biweek(
|
634
|
-
next_biweek(-
|
670
|
+
def prior_biweek(num = 1)
|
671
|
+
next_biweek(-num)
|
635
672
|
end
|
636
673
|
|
637
674
|
# Return the date that is +n+ weeks after this date where each week is 7 days.
|
@@ -639,21 +676,22 @@ module FatCore
|
|
639
676
|
# goes to the first day of the week in the next week and does not take an
|
640
677
|
# argument +n+ to go multiple weeks.
|
641
678
|
#
|
642
|
-
# @param
|
679
|
+
# @param num [Integer] number of weeks to advance
|
643
680
|
# @return [::Date] new date n weeks after this date
|
644
|
-
def next_week(
|
645
|
-
|
646
|
-
return self if
|
647
|
-
|
681
|
+
def next_week(num = 1)
|
682
|
+
num = num.floor
|
683
|
+
return self if num.zero?
|
684
|
+
|
685
|
+
self + (7 * num)
|
648
686
|
end
|
649
687
|
|
650
688
|
# Return the date that is +n+ weeks before this date where each week is 7
|
651
689
|
# days.
|
652
690
|
#
|
653
|
-
# @param
|
691
|
+
# @param num [Integer] number of weeks to retreat
|
654
692
|
# @return [::Date] new date n weeks from this date
|
655
|
-
def prior_week(
|
656
|
-
next_week(-
|
693
|
+
def prior_week(num)
|
694
|
+
next_week(-num)
|
657
695
|
end
|
658
696
|
|
659
697
|
# NOTE: #next_day is defined in active_support.
|
@@ -661,10 +699,10 @@ module FatCore
|
|
661
699
|
# Return the date that is +n+ weeks before this date where each week is 7
|
662
700
|
# days.
|
663
701
|
#
|
664
|
-
# @param
|
702
|
+
# @param num [Integer] number of days to retreat
|
665
703
|
# @return [::Date] new date n days before this date
|
666
|
-
def prior_day(
|
667
|
-
next_day(-
|
704
|
+
def prior_day(num)
|
705
|
+
next_day(-num)
|
668
706
|
end
|
669
707
|
|
670
708
|
# :category: Relative ::Dates
|
@@ -673,28 +711,28 @@ module FatCore
|
|
673
711
|
#
|
674
712
|
# @param chunk [Symbol] one of +:year+, +:half+, +:quarter+, +:bimonth+,
|
675
713
|
# +:month+, +:semimonth+, +:biweek+, +:week+, or +:day+.
|
676
|
-
# @param
|
714
|
+
# @param num [Integer] the number of chunks to add, can be negative
|
677
715
|
# @return [::Date] the date n chunks from this date
|
678
|
-
def add_chunk(chunk,
|
716
|
+
def add_chunk(chunk, num = 1)
|
679
717
|
case chunk
|
680
718
|
when :year
|
681
|
-
next_year(
|
719
|
+
next_year(num)
|
682
720
|
when :half
|
683
|
-
next_month(6)
|
721
|
+
next_month(6 * num)
|
684
722
|
when :quarter
|
685
|
-
next_month(3)
|
723
|
+
next_month(3 * num)
|
686
724
|
when :bimonth
|
687
|
-
next_month(2)
|
725
|
+
next_month(2 * num)
|
688
726
|
when :month
|
689
|
-
next_month(
|
727
|
+
next_month(num)
|
690
728
|
when :semimonth
|
691
|
-
next_semimonth(
|
729
|
+
next_semimonth(num)
|
692
730
|
when :biweek
|
693
|
-
next_biweek(
|
731
|
+
next_biweek(num)
|
694
732
|
when :week
|
695
|
-
next_week(
|
733
|
+
next_week(num)
|
696
734
|
when :day
|
697
|
-
next_day(
|
735
|
+
next_day(num)
|
698
736
|
else
|
699
737
|
raise ArgumentError, "add_chunk unknown chunk: '#{chunk}'"
|
700
738
|
end
|
@@ -764,6 +802,22 @@ module FatCore
|
|
764
802
|
end
|
765
803
|
end
|
766
804
|
|
805
|
+
# @group Holidays and Workdays
|
806
|
+
|
807
|
+
# Does self fall on a weekend?
|
808
|
+
# @return [Boolean]
|
809
|
+
def weekend?
|
810
|
+
saturday? || sunday?
|
811
|
+
end
|
812
|
+
|
813
|
+
# :category: Queries
|
814
|
+
|
815
|
+
# Does self fall on a weekday?
|
816
|
+
# @return [Boolean]
|
817
|
+
def weekday?
|
818
|
+
!weekend?
|
819
|
+
end
|
820
|
+
|
767
821
|
# Return the date for Easter in the Western Church for the year in which this
|
768
822
|
# date falls.
|
769
823
|
#
|
@@ -773,8 +827,6 @@ module FatCore
|
|
773
827
|
::Date.easter(year)
|
774
828
|
end
|
775
829
|
|
776
|
-
# @group Federal Holidays and Workdays
|
777
|
-
|
778
830
|
# Holidays decreed by Presidential proclamation
|
779
831
|
FED_DECREED_HOLIDAYS =
|
780
832
|
[
|
@@ -782,7 +834,8 @@ module FatCore
|
|
782
834
|
# http://www.whitehouse.gov/the-press-office/2012/12/21
|
783
835
|
::Date.parse('2012-12-24'),
|
784
836
|
# And Trump
|
785
|
-
::Date.parse('2018-12-24')
|
837
|
+
::Date.parse('2018-12-24'),
|
838
|
+
::Date.parse('2019-12-24')
|
786
839
|
].freeze
|
787
840
|
|
788
841
|
# Presidential funeral since JFK
|
@@ -803,7 +856,7 @@ module FatCore
|
|
803
856
|
::Date.parse('2007-01-02'),
|
804
857
|
# GHWBFuneral
|
805
858
|
::Date.parse('2018-12-05')
|
806
|
-
]
|
859
|
+
].freeze
|
807
860
|
|
808
861
|
# Return whether this date is a United States federal holiday.
|
809
862
|
#
|
@@ -865,16 +918,17 @@ module FatCore
|
|
865
918
|
# Return the date that is n federal workdays after or before (if n < 0) this
|
866
919
|
# date.
|
867
920
|
#
|
868
|
-
# @param
|
921
|
+
# @param num [Integer] number of federal workdays to add to this date
|
869
922
|
# @return [::Date]
|
870
|
-
def add_fed_workdays(
|
923
|
+
def add_fed_workdays(num)
|
871
924
|
d = dup
|
872
|
-
return d if
|
873
|
-
|
874
|
-
|
875
|
-
|
925
|
+
return d if num.zero?
|
926
|
+
|
927
|
+
incr = num.negative? ? -1 : 1
|
928
|
+
num = num.abs
|
929
|
+
while num.positive?
|
876
930
|
d += incr
|
877
|
-
|
931
|
+
num -= 1 if d.fed_workday?
|
878
932
|
end
|
879
933
|
d
|
880
934
|
end
|
@@ -975,8 +1029,10 @@ module FatCore
|
|
975
1029
|
# Calculations for NYSE holidays are from Rule 51 and supplementary materials
|
976
1030
|
# for the Rules of the New York Stock Exchange, Inc.
|
977
1031
|
#
|
978
|
-
# * General Rule 1: if a regular holiday falls on Saturday, observe it on
|
979
|
-
#
|
1032
|
+
# * General Rule 1: if a regular holiday falls on Saturday, observe it on
|
1033
|
+
# the preceding Friday.
|
1034
|
+
# * General Rule 2: if a regular holiday falls on Sunday, observe it on
|
1035
|
+
# the following Monday.
|
980
1036
|
#
|
981
1037
|
# These are the regular holidays:
|
982
1038
|
#
|
@@ -1042,16 +1098,17 @@ module FatCore
|
|
1042
1098
|
# Return the date that is n NYSE trading days after or before (if n < 0) this
|
1043
1099
|
# date.
|
1044
1100
|
#
|
1045
|
-
# @param
|
1101
|
+
# @param num [Integer] number of NYSE trading days to add to this date
|
1046
1102
|
# @return [::Date]
|
1047
|
-
def add_nyse_workdays(
|
1103
|
+
def add_nyse_workdays(num)
|
1048
1104
|
d = dup
|
1049
|
-
return d if
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1105
|
+
return d if num.zero?
|
1106
|
+
|
1107
|
+
incr = num.negative? ? -1 : 1
|
1108
|
+
num = num.abs
|
1109
|
+
while num.positive?
|
1053
1110
|
d += incr
|
1054
|
-
|
1111
|
+
num -= 1 if d.nyse_workday?
|
1055
1112
|
end
|
1056
1113
|
d
|
1057
1114
|
end
|
@@ -1095,8 +1152,6 @@ module FatCore
|
|
1095
1152
|
date
|
1096
1153
|
end
|
1097
1154
|
|
1098
|
-
protected
|
1099
|
-
|
1100
1155
|
# Return whether this date is a fixed holiday for the NYSE, that is, a holiday
|
1101
1156
|
# that falls on the same date each year.
|
1102
1157
|
#
|
@@ -1209,7 +1264,9 @@ module FatCore
|
|
1209
1264
|
# @return [Boolean]
|
1210
1265
|
def nyse_special_holiday?
|
1211
1266
|
return false unless self > ::Date.parse('1960-01-01')
|
1267
|
+
|
1212
1268
|
return true if PRESIDENTIAL_FUNERALS.include?(self)
|
1269
|
+
|
1213
1270
|
case self
|
1214
1271
|
when ::Date.parse('1961-05-29')
|
1215
1272
|
# Day before Decoaration Day
|
@@ -1281,9 +1338,11 @@ module FatCore
|
|
1281
1338
|
# @param str [String, #to_s] a stringling of the form MM/DD/YYYY
|
1282
1339
|
# @return [::Date] the date represented by the str paramenter.
|
1283
1340
|
def parse_american(str)
|
1284
|
-
|
1341
|
+
re = %r{\A\s*(\d\d?)\s*[-/]\s*(\d\d?)\s*[-/]\s*((\d\d)?\d\d)\s*\z}
|
1342
|
+
unless str.to_s =~ re
|
1285
1343
|
raise ArgumentError, "date string must be of form 'MM?/DD?/YY(YY)?'"
|
1286
1344
|
end
|
1345
|
+
|
1287
1346
|
year = $3.to_i
|
1288
1347
|
month = $1.to_i
|
1289
1348
|
day = $2.to_i
|
@@ -1345,13 +1404,13 @@ module FatCore
|
|
1345
1404
|
# designated by spec
|
1346
1405
|
def parse_spec(spec, spec_type = :from)
|
1347
1406
|
spec = spec.to_s.strip
|
1348
|
-
unless [
|
1407
|
+
unless %i[from to].include?(spec_type)
|
1349
1408
|
raise ArgumentError, "invalid date spec type: '#{spec_type}'"
|
1350
1409
|
end
|
1351
1410
|
|
1352
1411
|
today = ::Date.current
|
1353
1412
|
case spec.clean
|
1354
|
-
when
|
1413
|
+
when %r{\A(\d\d\d\d)[-/](\d\d?)[-/](\d\d?)\z}
|
1355
1414
|
# A specified date
|
1356
1415
|
::Date.new($1.to_i, $2.to_i, $3.to_i)
|
1357
1416
|
when /\AW(\d\d?)\z/, /\A(\d\d?)W\z/
|
@@ -1359,29 +1418,32 @@ module FatCore
|
|
1359
1418
|
if week_num < 1 || week_num > 53
|
1360
1419
|
raise ArgumentError, "invalid week number (1-53): '#{spec}'"
|
1361
1420
|
end
|
1421
|
+
|
1362
1422
|
if spec_type == :from
|
1363
1423
|
::Date.commercial(today.year, week_num).beginning_of_week
|
1364
1424
|
else
|
1365
1425
|
::Date.commercial(today.year, week_num).end_of_week
|
1366
1426
|
end
|
1367
|
-
when
|
1427
|
+
when %r{\A(\d\d\d\d)[-/]W(\d\d?)\z}, %r{\A(\d\d\d\d)[-/](\d\d?)W\z}
|
1368
1428
|
year = $1.to_i
|
1369
1429
|
week_num = $2.to_i
|
1370
1430
|
if week_num < 1 || week_num > 53
|
1371
1431
|
raise ArgumentError, "invalid week number (1-53): '#{spec}'"
|
1372
1432
|
end
|
1433
|
+
|
1373
1434
|
if spec_type == :from
|
1374
1435
|
::Date.commercial(year, week_num).beginning_of_week
|
1375
1436
|
else
|
1376
1437
|
::Date.commercial(year, week_num).end_of_week
|
1377
1438
|
end
|
1378
|
-
when
|
1439
|
+
when %r{^(\d\d\d\d)[-/](\d)[Qq]$}, %r{^(\d\d\d\d)[-/][Qq](\d)$}
|
1379
1440
|
# Year-Quarter
|
1380
1441
|
year = $1.to_i
|
1381
1442
|
quarter = $2.to_i
|
1382
1443
|
unless [1, 2, 3, 4].include?(quarter)
|
1383
1444
|
raise ArgumentError, "invalid quarter number (1-4): '#{spec}'"
|
1384
1445
|
end
|
1446
|
+
|
1385
1447
|
month = quarter * 3
|
1386
1448
|
if spec_type == :from
|
1387
1449
|
::Date.new(year, month, 1).beginning_of_quarter
|
@@ -1395,19 +1457,20 @@ module FatCore
|
|
1395
1457
|
unless [1, 2, 3, 4].include?(quarter)
|
1396
1458
|
raise ArgumentError, "invalid quarter number (1-4): '#{spec}'"
|
1397
1459
|
end
|
1460
|
+
|
1398
1461
|
date = ::Date.new(this_year, quarter * 3, 15)
|
1399
1462
|
if spec_type == :from
|
1400
1463
|
date.beginning_of_quarter
|
1401
1464
|
else
|
1402
1465
|
date.end_of_quarter
|
1403
1466
|
end
|
1404
|
-
when
|
1467
|
+
when %r{^(\d\d\d\d)[-/](\d)[Hh]$}, %r{^(\d\d\d\d)[-/][Hh](\d)$}
|
1405
1468
|
# Year-Half
|
1406
1469
|
year = $1.to_i
|
1407
1470
|
half = $2.to_i
|
1408
|
-
|
1409
|
-
|
1410
|
-
|
1471
|
+
msg = "invalid half number: '#{spec}'"
|
1472
|
+
raise ArgumentError, msg unless [1, 2].include?(half)
|
1473
|
+
|
1411
1474
|
month = half * 6
|
1412
1475
|
if spec_type == :from
|
1413
1476
|
::Date.new(year, month, 15).beginning_of_half
|
@@ -1418,34 +1481,36 @@ module FatCore
|
|
1418
1481
|
# Half only
|
1419
1482
|
this_year = today.year
|
1420
1483
|
half = $1.to_i
|
1421
|
-
|
1422
|
-
|
1423
|
-
|
1484
|
+
msg = "invalid half number: '#{spec}'"
|
1485
|
+
raise ArgumentError, msg unless [1, 2].include?(half)
|
1486
|
+
|
1424
1487
|
date = ::Date.new(this_year, half * 6, 15)
|
1425
1488
|
if spec_type == :from
|
1426
1489
|
date.beginning_of_half
|
1427
1490
|
else
|
1428
1491
|
date.end_of_half
|
1429
1492
|
end
|
1430
|
-
when
|
1493
|
+
when %r{^(\d\d\d\d)[-/](\d\d?)*$}
|
1431
1494
|
# Year-Month only
|
1432
1495
|
year = $1.to_i
|
1433
1496
|
month = $2.to_i
|
1434
1497
|
unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
|
1435
1498
|
raise ArgumentError, "invalid month number (1-12): '#{spec}'"
|
1436
1499
|
end
|
1500
|
+
|
1437
1501
|
if spec_type == :from
|
1438
1502
|
::Date.new(year, month, 1)
|
1439
1503
|
else
|
1440
1504
|
::Date.new(year, month, 1).end_of_month
|
1441
1505
|
end
|
1442
|
-
when
|
1506
|
+
when %r{^(\d\d?)[-/](\d\d?)*$}
|
1443
1507
|
# Month-Day only
|
1444
1508
|
month = $1.to_i
|
1445
1509
|
day = $2.to_i
|
1446
1510
|
unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
|
1447
1511
|
raise ArgumentError, "invalid month number (1-12): '#{spec}'"
|
1448
1512
|
end
|
1513
|
+
|
1449
1514
|
if spec_type == :from
|
1450
1515
|
::Date.new(today.year, month, day)
|
1451
1516
|
else
|
@@ -1457,6 +1522,7 @@ module FatCore
|
|
1457
1522
|
unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
|
1458
1523
|
raise ArgumentError, "invalid month number (1-12): '#{spec}'"
|
1459
1524
|
end
|
1525
|
+
|
1460
1526
|
if spec_type == :from
|
1461
1527
|
::Date.new(today.year, month, 1)
|
1462
1528
|
else
|
@@ -1580,11 +1646,12 @@ module FatCore
|
|
1580
1646
|
# starting with January = 1, etc.
|
1581
1647
|
COMMON_YEAR_DAYS_IN_MONTH = [31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31,
|
1582
1648
|
30, 31].freeze
|
1583
|
-
def days_in_month(
|
1584
|
-
raise ArgumentError, 'illegal month number' if
|
1585
|
-
|
1586
|
-
|
1587
|
-
|
1649
|
+
def days_in_month(year, month)
|
1650
|
+
raise ArgumentError, 'illegal month number' if month < 1 || month > 12
|
1651
|
+
|
1652
|
+
days = COMMON_YEAR_DAYS_IN_MONTH[month]
|
1653
|
+
if month == 2
|
1654
|
+
::Date.new(year, month, 1).leap? ? 29 : 28
|
1588
1655
|
else
|
1589
1656
|
days
|
1590
1657
|
end
|
@@ -1593,41 +1660,43 @@ module FatCore
|
|
1593
1660
|
# Return the nth weekday in the given month. If n is negative, count from
|
1594
1661
|
# last day of month.
|
1595
1662
|
#
|
1596
|
-
# @param
|
1663
|
+
# @param nth [Integer] the ordinal number for the weekday
|
1597
1664
|
# @param wday [Integer] the weekday of interest with Monday 0 to Sunday 6
|
1598
1665
|
# @param year [Integer] the year of interest
|
1599
1666
|
# @param month [Integer] the month of interest with January 1 to December 12
|
1600
|
-
def nth_wday_in_year_month(
|
1667
|
+
def nth_wday_in_year_month(nth, wday, year, month)
|
1601
1668
|
wday = wday.to_i
|
1602
|
-
raise ArgumentError, 'illegal weekday number' if wday
|
1669
|
+
raise ArgumentError, 'illegal weekday number' if wday.negative? || wday > 6
|
1670
|
+
|
1603
1671
|
month = month.to_i
|
1604
1672
|
raise ArgumentError, 'illegal month number' if month < 1 || month > 12
|
1605
|
-
|
1606
|
-
|
1673
|
+
|
1674
|
+
nth = nth.to_i
|
1675
|
+
if nth.positive?
|
1607
1676
|
# Set d to the 1st wday in month
|
1608
1677
|
d = ::Date.new(year, month, 1)
|
1609
1678
|
d += 1 while d.wday != wday
|
1610
1679
|
# Set d to the nth wday in month
|
1611
1680
|
nd = 1
|
1612
|
-
while nd !=
|
1681
|
+
while nd != nth
|
1613
1682
|
d += 7
|
1614
1683
|
nd += 1
|
1615
1684
|
end
|
1616
1685
|
d
|
1617
|
-
elsif
|
1618
|
-
|
1686
|
+
elsif nth.negative?
|
1687
|
+
nth = -nth
|
1619
1688
|
# Set d to the last wday in month
|
1620
1689
|
d = ::Date.new(year, month, 1).end_of_month
|
1621
1690
|
d -= 1 while d.wday != wday
|
1622
1691
|
# Set d to the nth wday in month
|
1623
1692
|
nd = 1
|
1624
|
-
while nd !=
|
1693
|
+
while nd != nth
|
1625
1694
|
d -= 7
|
1626
1695
|
nd += 1
|
1627
1696
|
end
|
1628
1697
|
d
|
1629
1698
|
else
|
1630
|
-
raise ArgumentError, 'Argument
|
1699
|
+
raise ArgumentError, 'Argument nth cannot be zero'
|
1631
1700
|
end
|
1632
1701
|
end
|
1633
1702
|
|
@@ -1669,7 +1738,6 @@ module FatCore
|
|
1669
1738
|
end
|
1670
1739
|
end
|
1671
1740
|
|
1672
|
-
# @private
|
1673
1741
|
def self.included(base)
|
1674
1742
|
base.extend(ClassMethods)
|
1675
1743
|
end
|