fat_period 2.1.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.envrc +0 -1
- data/.github/workflows/ruby.yml +57 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG.org +43 -0
- data/Gemfile +13 -11
- data/README.md +1291 -0
- data/README.org +1229 -19
- data/Rakefile +25 -5
- data/bin/console +1 -0
- data/fat_period.gemspec +4 -3
- data/lib/fat_period/date.rb +4 -6
- data/lib/fat_period/period.rb +248 -33
- data/lib/fat_period/version.rb +3 -1
- data/lib/fat_period.rb +25 -5
- metadata +23 -11
- data/.github/workflows/gem-push.yml +0 -48
- data/.travis.yml +0 -6
data/README.org
CHANGED
|
@@ -1,22 +1,40 @@
|
|
|
1
|
-
#+
|
|
1
|
+
#+TITLE: FatPeriod User Guide
|
|
2
|
+
#+OPTIONS: toc:5
|
|
2
3
|
#+LATEX_HEADER: \usepackage[margin=0.75in]{geometry}
|
|
4
|
+
#+PROPERTY: header-args:ruby :results value :colnames no :hlines yes :exports both :dir "./"
|
|
5
|
+
#+PROPERTY: header-args:ruby+ :wrap example :session fat_period_session :eval yes
|
|
6
|
+
#+PROPERTY: header-args:ruby+ :prologue "$:.unshift('./lib') unless $:.first == './lib'; require 'fat_period'"
|
|
7
|
+
#+PROPERTY: header-args:sh :exports code :eval no
|
|
8
|
+
#+PROPERTY: header-args:bash :exports code :eval no
|
|
3
9
|
|
|
4
|
-
#+
|
|
5
|
-
|
|
10
|
+
#+BEGIN_EXPORT markdown
|
|
11
|
+
[](https://github.com/ddoherty03/fat_period/actions/workflows/ruby.yml)
|
|
12
|
+
#+END_EXPORT
|
|
6
13
|
|
|
7
|
-
The following is for org.
|
|
8
|
-
#+END_COMMENT
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
* Version
|
|
16
|
+
#+begin_src ruby :wrap EXAMPLE
|
|
17
|
+
"Current version is: #{FatPeriod::VERSION}"
|
|
18
|
+
#+end_src
|
|
19
|
+
|
|
20
|
+
#+RESULTS:
|
|
21
|
+
#+begin_EXAMPLE
|
|
22
|
+
Current version is: 3.1.0
|
|
23
|
+
#+end_EXAMPLE
|
|
11
24
|
|
|
12
25
|
* Introduction
|
|
26
|
+
~FatPeriod~ provides a Ruby ~Period~ class for dealing with time periods of
|
|
27
|
+
days, that is ranges whose endpoints are of class ~Date~. It's target is
|
|
28
|
+
financial applications, but it serves well for any application where periods
|
|
29
|
+
of time are useful. It builds on the [[https://github.com/ddoherty03/fat_date][fat_date]] gem, which provides
|
|
30
|
+
enhancements to the ~Date~ class, especially its class method ~Date.spec~ for
|
|
31
|
+
interpreting a rich set of "specs" as the beginning or end of a variety of
|
|
32
|
+
calendar-related periods.
|
|
13
33
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
various 'chunk' sizes that correspond to calendar-related periods such as
|
|
19
|
-
days, weeks, months, and so forth.'
|
|
34
|
+
In addition, set operations are provided for Period, as well as methods for
|
|
35
|
+
breaking a larger periods into an array of smaller periods of various 'chunk'
|
|
36
|
+
sizes that correspond to calendar-related periods such as days, weeks, months,
|
|
37
|
+
and so forth.
|
|
20
38
|
|
|
21
39
|
* Installation
|
|
22
40
|
|
|
@@ -28,22 +46,39 @@ Add this line to your application's Gemfile:
|
|
|
28
46
|
gem 'fat_period'
|
|
29
47
|
#+END_SRC
|
|
30
48
|
|
|
49
|
+
#+RESULTS:
|
|
50
|
+
#+begin_example
|
|
51
|
+
true
|
|
52
|
+
#+end_example
|
|
53
|
+
|
|
31
54
|
And then execute:
|
|
32
55
|
|
|
33
56
|
#+BEGIN_SRC sh
|
|
34
57
|
$ bundle
|
|
35
58
|
#+END_SRC
|
|
36
59
|
|
|
60
|
+
#+RESULTS:
|
|
61
|
+
|
|
37
62
|
Or install it yourself as:
|
|
38
63
|
|
|
39
64
|
#+BEGIN_SRC sh
|
|
40
65
|
$ gem install fat_period
|
|
41
66
|
#+END_SRC
|
|
42
67
|
|
|
68
|
+
#+RESULTS:
|
|
69
|
+
|
|
43
70
|
* Usage
|
|
71
|
+
** Constant ~Period::FOREVER~
|
|
44
72
|
|
|
45
|
-
|
|
73
|
+
The ~Period~ class depends on the extensions to ~Date~ made by the ~fat_core~
|
|
74
|
+
gem, which you can read about [[https://github.com/ddoherty03/fat_core][here]]. It defines a constant, ~Period::FOREVER~,
|
|
75
|
+
which is defined as extending from ~Date::BOT~ to ~Date::EOT~, which are
|
|
76
|
+
defined in ~fat_date~ as 1900-01-01 and 3000-12-31, respectively and define
|
|
77
|
+
the beginning of time and end of time for practical commercial purposes. The
|
|
78
|
+
constant is not frozen, so you can re-define it to your liking.
|
|
46
79
|
|
|
80
|
+
** Construction of Periods
|
|
81
|
+
*** ~Period.new~
|
|
47
82
|
A Period is constructed with two arguments for the begin and end date. The
|
|
48
83
|
begin date must be on or before the end date. Each argument can be (1) a
|
|
49
84
|
Date, (2) a string parseable as a Date by the Date.parse method, or (3) an
|
|
@@ -53,15 +88,1190 @@ object that responds to ~#to_s~ and can be parsed as a Date by Date.parse:
|
|
|
53
88
|
p1 = Period.new(Date.today, Date.today + 30)
|
|
54
89
|
p2 = Period.new('Nov 22, 1963', Date.today)
|
|
55
90
|
p3 = Period.new('1961-01-21', '1963-11-22')
|
|
56
|
-
|
|
91
|
+
[[p1.to_s], [p2.to_s], [p3.to_s]]
|
|
57
92
|
#+end_SRC
|
|
58
93
|
|
|
59
|
-
|
|
94
|
+
#+RESULTS:
|
|
95
|
+
#+begin_example
|
|
96
|
+
| 2026-11-05 to 2026-12-05 |
|
|
97
|
+
| 1963-11-22 to 2026-11-05 |
|
|
98
|
+
| 1961-01-21 to 1963-11-22 |
|
|
99
|
+
#+end_example
|
|
100
|
+
|
|
101
|
+
#+begin_src ruby
|
|
102
|
+
["Camelot lasted #{p3.length} days"]
|
|
103
|
+
#+end_src
|
|
104
|
+
|
|
105
|
+
#+RESULTS:
|
|
106
|
+
#+begin_example
|
|
107
|
+
| Camelot lasted 1036 days |
|
|
108
|
+
#+end_example
|
|
109
|
+
|
|
110
|
+
*** ~Period.parse~
|
|
111
|
+
A more convenient way to construct a period is provided by ~Period.parse~. It
|
|
112
|
+
takes two strings as its arguments, a mandatory "from-spec" and an optional
|
|
113
|
+
"to-spec":
|
|
114
|
+
|
|
115
|
+
A "spec" is a string designating some period of time. There are many ways of
|
|
116
|
+
specifying a period, which are detailed below.
|
|
117
|
+
|
|
118
|
+
**** With Only a From-Spec
|
|
119
|
+
|
|
120
|
+
If only a from-spec is given, it defines both the beginning and end of the
|
|
121
|
+
overall period:
|
|
122
|
+
|
|
123
|
+
#+begin_src ruby
|
|
124
|
+
tab = []
|
|
125
|
+
tab << ['From Spec', 'Result']
|
|
126
|
+
tab << nil
|
|
127
|
+
froms = ['2020', '2020-2Q', '2020-W15', '2020-09', '2020-09-A', '2020-09-iii']
|
|
128
|
+
froms.each do |f|
|
|
129
|
+
tab << [f, Period.parse(f).inspect]
|
|
130
|
+
end
|
|
131
|
+
tab
|
|
132
|
+
#+end_src
|
|
133
|
+
|
|
134
|
+
#+RESULTS:
|
|
135
|
+
#+begin_example
|
|
136
|
+
| From Spec | Result |
|
|
137
|
+
|-------------+--------------------------------|
|
|
138
|
+
| 2020 | Period(2020-01-01..2020-12-31) |
|
|
139
|
+
| 2020-2Q | Period(2020-04-01..2020-06-30) |
|
|
140
|
+
| 2020-W15 | Period(2020-04-06..2020-04-12) |
|
|
141
|
+
| 2020-09 | Period(2020-09-01..2020-09-30) |
|
|
142
|
+
| 2020-09-A | Period(2020-09-01..2020-09-15) |
|
|
143
|
+
| 2020-09-iii | Period(2020-09-14..2020-09-20) |
|
|
144
|
+
#+end_example
|
|
145
|
+
|
|
146
|
+
#+begin_example
|
|
147
|
+
| From Spec | Result |
|
|
148
|
+
|-------------+--------------------------------|
|
|
149
|
+
| 2020 | Period(2020-01-01..2020-12-31) |
|
|
150
|
+
| 2020-2Q | Period(2020-04-01..2020-06-30) |
|
|
151
|
+
| 2020-W15 | Period(2020-04-06..2020-04-12) |
|
|
152
|
+
| 2020-09 | Period(2020-09-01..2020-09-30) |
|
|
153
|
+
| 2020-09-A | Period(2020-09-01..2020-09-15) |
|
|
154
|
+
| 2020-09-iii | Period(2020-09-14..2020-09-20) |
|
|
155
|
+
#+end_example
|
|
156
|
+
|
|
157
|
+
**** With Both a From-Spec and To-Spec
|
|
158
|
+
But, if a to-spec is also given, the from-spec defines the beginning of the
|
|
159
|
+
period and the to-spec defines the end of the period. In particular, the
|
|
160
|
+
beginning of the period is the first day of the from-spec and the end of the
|
|
161
|
+
period is the last day of the to-spec:
|
|
162
|
+
|
|
163
|
+
#+begin_src ruby
|
|
164
|
+
tab = []
|
|
165
|
+
tab << ['From Spec', 'To Spec', 'Result']
|
|
166
|
+
tab << nil
|
|
167
|
+
from_tos = [['2020', '2020-2Q'], ['2020-2Q', '2020-W15'], ['2020-W15', '2020-09'], ['2020-09', '2020-09-A'], ['2020-09-A', '2020-09-iii']]
|
|
168
|
+
from_tos.each do |f, t|
|
|
169
|
+
tab << [f, t, Period.parse(f, t).inspect]
|
|
170
|
+
end
|
|
171
|
+
tab
|
|
172
|
+
#+end_src
|
|
173
|
+
|
|
174
|
+
#+RESULTS:
|
|
175
|
+
#+begin_example
|
|
176
|
+
| From Spec | To Spec | Result |
|
|
177
|
+
|-----------+-------------+--------------------------------|
|
|
178
|
+
| 2020 | 2020-2Q | Period(2020-01-01..2020-06-30) |
|
|
179
|
+
| 2020-2Q | 2020-W15 | Period(2020-04-01..2020-04-12) |
|
|
180
|
+
| 2020-W15 | 2020-09 | Period(2020-04-06..2020-09-30) |
|
|
181
|
+
| 2020-09 | 2020-09-A | Period(2020-09-01..2020-09-15) |
|
|
182
|
+
| 2020-09-A | 2020-09-iii | Period(2020-09-01..2020-09-20) |
|
|
183
|
+
#+end_example
|
|
184
|
+
|
|
185
|
+
#+begin_example
|
|
186
|
+
| From Spec | To Spec | Result |
|
|
187
|
+
|-----------+-------------+--------------------------------|
|
|
188
|
+
| 2020 | 2020-2Q | Period(2020-01-01..2020-06-30) |
|
|
189
|
+
| 2020-2Q | 2020-W15 | Period(2020-04-01..2020-04-12) |
|
|
190
|
+
| 2020-W15 | 2020-09 | Period(2020-04-06..2020-09-30) |
|
|
191
|
+
| 2020-09 | 2020-09-A | Period(2020-09-01..2020-09-15) |
|
|
192
|
+
| 2020-09-A | 2020-09-iii | Period(2020-09-01..2020-09-20) |
|
|
193
|
+
#+end_example
|
|
194
|
+
|
|
195
|
+
**** Using Skip Modifiers
|
|
196
|
+
One new feature of FatDate is the ability to add a "skip modifier" to the end
|
|
197
|
+
of a date spec to skip forward or backward to the first day-of-week either on
|
|
198
|
+
or before/after the date given by the spec. For example, the following
|
|
199
|
+
demonstrates that one can set the 'to' spec to the /last/ Wednesday of 2025 or
|
|
200
|
+
the last Wednesday /before/ the end of 2025. Using '>' or '>=' specified
|
|
201
|
+
skipping forward instead.
|
|
202
|
+
|
|
203
|
+
#+begin_src ruby
|
|
204
|
+
tab = []
|
|
205
|
+
tab << ['From Spec', 'To Spec', 'Result', 'Description']
|
|
206
|
+
tab << nil
|
|
207
|
+
from_to_descs = [['2025-2Q', '2025<=Wed', 'From 2q to last Wednesday of 2025'],
|
|
208
|
+
['2025-2Q', '2025<Wed', 'From 2q to last Wednesday /before/ the end of 2025'],
|
|
209
|
+
['2012-11', '2012-11<=Thur', 'November 2012 through last Thursday'],
|
|
210
|
+
['2012-11', '2012-11-4Thur', 'And through Thanksgiving (not always the /last/ Thursday!)']
|
|
211
|
+
]
|
|
212
|
+
from_to_descs.each do |f, t, d|
|
|
213
|
+
tab << [f, t, Period.parse(f, t).inspect, d]
|
|
214
|
+
end
|
|
215
|
+
tab
|
|
216
|
+
#+end_src
|
|
217
|
+
|
|
218
|
+
#+RESULTS:
|
|
219
|
+
#+begin_example
|
|
220
|
+
| From Spec | To Spec | Result | Description |
|
|
221
|
+
|-----------+---------------+--------------------------------+------------------------------------------------------------|
|
|
222
|
+
| 2025-2Q | 2025<=Wed | Period(2025-04-01..2025-12-31) | From 2q to last Wednesday of 2025 |
|
|
223
|
+
| 2025-2Q | 2025<Wed | Period(2025-04-01..2025-12-31) | From 2q to last Wednesday /before/ the end of 2025 |
|
|
224
|
+
| 2012-11 | 2012-11<=Thur | Period(2012-11-01..2012-11-29) | November 2012 through last Thursday |
|
|
225
|
+
| 2012-11 | 2012-11-4Thur | Period(2012-11-01..2012-11-22) | And through Thanksgiving (not always the /last/ Thursday!) |
|
|
226
|
+
#+end_example
|
|
227
|
+
|
|
228
|
+
#+begin_example
|
|
229
|
+
| From Spec | To Spec | Result | Description |
|
|
230
|
+
|-----------+---------------+--------------------------------+------------------------------------------------------------|
|
|
231
|
+
| 2025-2Q | 2025<=Wed | Period(2025-04-01..2025-12-31) | From 2q to last Wednesday of 2025 |
|
|
232
|
+
| 2025-2Q | 2025<Wed | Period(2025-04-01..2025-12-31) | From 2q to last Wednesday /before/ the end of 2025 |
|
|
233
|
+
| 2012-11 | 2012-11<=Thur | Period(2012-11-01..2012-11-29) | November 2012 through last Thursday |
|
|
234
|
+
| 2012-11 | 2012-11-4Thur | Period(2012-11-01..2012-11-22) | And through Thanksgiving (not always the /last/ Thursday!) |
|
|
235
|
+
#+end_example
|
|
236
|
+
|
|
237
|
+
*** ~Period.parse_phrase~
|
|
238
|
+
For example:
|
|
239
|
+
|
|
240
|
+
The ~Period.parse_phrase~ method will take a string having a 'from', 'to', and
|
|
241
|
+
'per' clause and return an Array of Periods encompassing the same period as
|
|
242
|
+
~Period.parse~, but optionally broken into sub-periods each having the length
|
|
243
|
+
specified by the 'per' clause. ~Period.parse_phrase~ always returns an Array
|
|
244
|
+
of Periods even if there is no 'per' clause and the Array has only one
|
|
245
|
+
member. If there is no 'to' clause, the returned period is from the start of
|
|
246
|
+
the 'from' period to its end. If there is neither a 'from' or a 'to' clause,
|
|
247
|
+
it tries to interpret the beginning of the phrase as a valid spec and uses it
|
|
248
|
+
as a 'from' clause.
|
|
249
|
+
|
|
250
|
+
#+begin_src ruby
|
|
251
|
+
tab = []
|
|
252
|
+
tab << ['k', 'Sub Period']
|
|
253
|
+
tab << nil
|
|
254
|
+
pds = Period.parse_phrase('from 2025 to 2025-3Q per month')
|
|
255
|
+
pds.each_with_index do |pd, k|
|
|
256
|
+
tab << [k, pd.inspect]
|
|
257
|
+
end
|
|
258
|
+
tab
|
|
259
|
+
#+end_src
|
|
260
|
+
|
|
261
|
+
#+RESULTS:
|
|
262
|
+
#+begin_example
|
|
263
|
+
| k | Sub Period |
|
|
264
|
+
|---+--------------------------------|
|
|
265
|
+
| 0 | Period(2025-01-01..2025-01-31) |
|
|
266
|
+
| 1 | Period(2025-02-01..2025-02-28) |
|
|
267
|
+
| 2 | Period(2025-03-01..2025-03-31) |
|
|
268
|
+
| 3 | Period(2025-04-01..2025-04-30) |
|
|
269
|
+
| 4 | Period(2025-05-01..2025-05-31) |
|
|
270
|
+
| 5 | Period(2025-06-01..2025-06-30) |
|
|
271
|
+
| 6 | Period(2025-07-01..2025-07-31) |
|
|
272
|
+
| 7 | Period(2025-08-01..2025-08-31) |
|
|
273
|
+
| 8 | Period(2025-09-01..2025-09-30) |
|
|
274
|
+
#+end_example
|
|
275
|
+
|
|
276
|
+
The period named in the 'per' clause is called a 'chunk' and there are several
|
|
277
|
+
valid chunk names in ~FatPeriod~:
|
|
278
|
+
|
|
279
|
+
| Chunk Name |
|
|
280
|
+
|------------|
|
|
281
|
+
| year |
|
|
282
|
+
| half |
|
|
283
|
+
| quarter |
|
|
284
|
+
| bimonth |
|
|
285
|
+
| month |
|
|
286
|
+
| semimonth |
|
|
287
|
+
| biweek |
|
|
288
|
+
| week |
|
|
289
|
+
| day |
|
|
290
|
+
|
|
291
|
+
Here is the same period broken into weeks. Notice that the first and last
|
|
292
|
+
"weeks" are not whole weeks because parts of them fall outside the boundaries
|
|
293
|
+
of the overall period.
|
|
294
|
+
|
|
295
|
+
#+begin_src ruby
|
|
296
|
+
tab = []
|
|
297
|
+
tab << ['k', 'Sub Period']
|
|
298
|
+
tab << nil
|
|
299
|
+
pds = Period.parse_phrase('from 2025 to 2025-3Q per week')
|
|
300
|
+
pds.each_with_index do |pd, k|
|
|
301
|
+
tab << [k, pd.inspect]
|
|
302
|
+
end
|
|
303
|
+
tab
|
|
304
|
+
#+end_src
|
|
305
|
+
|
|
306
|
+
#+RESULTS:
|
|
307
|
+
#+begin_example
|
|
308
|
+
| k | Sub Period |
|
|
309
|
+
|----+--------------------------------|
|
|
310
|
+
| 0 | Period(2025-01-01..2025-01-05) |
|
|
311
|
+
| 1 | Period(2025-01-06..2025-01-12) |
|
|
312
|
+
| 2 | Period(2025-01-13..2025-01-19) |
|
|
313
|
+
| 3 | Period(2025-01-20..2025-01-26) |
|
|
314
|
+
| 4 | Period(2025-01-27..2025-02-02) |
|
|
315
|
+
| 5 | Period(2025-02-03..2025-02-09) |
|
|
316
|
+
| 6 | Period(2025-02-10..2025-02-16) |
|
|
317
|
+
| 7 | Period(2025-02-17..2025-02-23) |
|
|
318
|
+
| 8 | Period(2025-02-24..2025-03-02) |
|
|
319
|
+
| 9 | Period(2025-03-03..2025-03-09) |
|
|
320
|
+
| 10 | Period(2025-03-10..2025-03-16) |
|
|
321
|
+
| 11 | Period(2025-03-17..2025-03-23) |
|
|
322
|
+
| 12 | Period(2025-03-24..2025-03-30) |
|
|
323
|
+
| 13 | Period(2025-03-31..2025-04-06) |
|
|
324
|
+
| 14 | Period(2025-04-07..2025-04-13) |
|
|
325
|
+
| 15 | Period(2025-04-14..2025-04-20) |
|
|
326
|
+
| 16 | Period(2025-04-21..2025-04-27) |
|
|
327
|
+
| 17 | Period(2025-04-28..2025-05-04) |
|
|
328
|
+
| 18 | Period(2025-05-05..2025-05-11) |
|
|
329
|
+
| 19 | Period(2025-05-12..2025-05-18) |
|
|
330
|
+
| 20 | Period(2025-05-19..2025-05-25) |
|
|
331
|
+
| 21 | Period(2025-05-26..2025-06-01) |
|
|
332
|
+
| 22 | Period(2025-06-02..2025-06-08) |
|
|
333
|
+
| 23 | Period(2025-06-09..2025-06-15) |
|
|
334
|
+
| 24 | Period(2025-06-16..2025-06-22) |
|
|
335
|
+
| 25 | Period(2025-06-23..2025-06-29) |
|
|
336
|
+
| 26 | Period(2025-06-30..2025-07-06) |
|
|
337
|
+
| 27 | Period(2025-07-07..2025-07-13) |
|
|
338
|
+
| 28 | Period(2025-07-14..2025-07-20) |
|
|
339
|
+
| 29 | Period(2025-07-21..2025-07-27) |
|
|
340
|
+
| 30 | Period(2025-07-28..2025-08-03) |
|
|
341
|
+
| 31 | Period(2025-08-04..2025-08-10) |
|
|
342
|
+
| 32 | Period(2025-08-11..2025-08-17) |
|
|
343
|
+
| 33 | Period(2025-08-18..2025-08-24) |
|
|
344
|
+
| 34 | Period(2025-08-25..2025-08-31) |
|
|
345
|
+
| 35 | Period(2025-09-01..2025-09-07) |
|
|
346
|
+
| 36 | Period(2025-09-08..2025-09-14) |
|
|
347
|
+
| 37 | Period(2025-09-15..2025-09-21) |
|
|
348
|
+
| 38 | Period(2025-09-22..2025-09-28) |
|
|
349
|
+
| 39 | Period(2025-09-29..2025-09-30) |
|
|
350
|
+
#+end_example
|
|
351
|
+
|
|
352
|
+
*** ~Period.ensure~
|
|
353
|
+
~Period.ensure~ tries to interpret its argument as a ~Period~ and returns it;
|
|
354
|
+
otherwise it throws an ~ArgumentError~ exception:
|
|
355
|
+
|
|
356
|
+
- if the argument responds to the ~to_period~ method, it invokes that on the
|
|
357
|
+
argument and returns it;
|
|
358
|
+
- if the argument is a ~String~, it uses ~Period.parse_phrase~ to try to
|
|
359
|
+
interepret it as a ~Period~;
|
|
360
|
+
- if it is already a ~Period~, it just returns the argument;
|
|
361
|
+
- otherwise, it throws an ~ArgumentError~ exception.
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
#+begin_src ruby
|
|
365
|
+
class ContainingMonth
|
|
366
|
+
def initialize(dat)
|
|
367
|
+
@dat = Date.ensure(dat)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def to_period
|
|
371
|
+
Period.month_containing(@dat)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
cm = ContainingMonth.new('2025-09-22')
|
|
376
|
+
Period.ensure(cm)
|
|
377
|
+
#+end_src
|
|
378
|
+
|
|
379
|
+
#+RESULTS:
|
|
380
|
+
#+begin_example
|
|
381
|
+
Period(2025-09-01..2025-09-30)
|
|
382
|
+
#+end_example
|
|
383
|
+
|
|
384
|
+
#+begin_src ruby
|
|
385
|
+
Period.ensure('from 2016 to 2018-3Q')
|
|
386
|
+
#+end_src
|
|
387
|
+
|
|
388
|
+
#+RESULTS:
|
|
389
|
+
#+begin_example
|
|
390
|
+
Period(2016-01-01..2018-09-30)
|
|
391
|
+
#+end_example
|
|
392
|
+
|
|
393
|
+
#+begin_src ruby
|
|
394
|
+
Period.ensure(Period.new('2016-01-02', '2017-09-29'))
|
|
395
|
+
#+end_src
|
|
396
|
+
|
|
397
|
+
#+RESULTS:
|
|
398
|
+
#+begin_example
|
|
399
|
+
Period(2016-01-02..2017-09-29)
|
|
400
|
+
#+end_example
|
|
401
|
+
|
|
402
|
+
** Conversion
|
|
403
|
+
*** ~#to_range~
|
|
404
|
+
~Period#to_range~ converts the ~Period~ to a ~Range~ with limits
|
|
405
|
+
~first..last~.
|
|
406
|
+
|
|
407
|
+
#+begin_src ruby
|
|
408
|
+
[Period.this_year.to_range.class, Period.this_year.to_range.to_s]
|
|
409
|
+
#+end_src
|
|
410
|
+
|
|
411
|
+
#+RESULTS:
|
|
412
|
+
#+begin_example
|
|
413
|
+
| Range | 2026-01-01..2026-12-31 |
|
|
414
|
+
#+end_example
|
|
415
|
+
|
|
416
|
+
*** ~#to_s~
|
|
417
|
+
~Period#to_s~ does ~String~ conversion but using compact notation for
|
|
418
|
+
years, halves, quarters, and months if possible; otherwise, it gives
|
|
419
|
+
~first~ and ~last~ explicitly:
|
|
420
|
+
|
|
421
|
+
#+begin_src ruby
|
|
422
|
+
specs = [
|
|
423
|
+
'from this_year',
|
|
424
|
+
'from 2026-2H',
|
|
425
|
+
'from 2026-3Q',
|
|
426
|
+
'from 2026-10',
|
|
427
|
+
'from 2026-10 to 2026-10-30'
|
|
428
|
+
]
|
|
429
|
+
tab = []
|
|
430
|
+
tab << ['Spec', '#to_s']
|
|
431
|
+
tab << nil
|
|
432
|
+
specs.each do |s|
|
|
433
|
+
pd = Period.parse_phrase(s).first
|
|
434
|
+
tab << [s, pd.to_s]
|
|
435
|
+
end
|
|
436
|
+
tab
|
|
437
|
+
#+end_src
|
|
438
|
+
|
|
439
|
+
#+RESULTS:
|
|
440
|
+
#+begin_example
|
|
441
|
+
| Spec | #to_s |
|
|
442
|
+
|----------------------------+--------------------------|
|
|
443
|
+
| from this_year | 2026 |
|
|
444
|
+
| from 2026-2H | 2026-2H |
|
|
445
|
+
| from 2026-3Q | 2026-3Q |
|
|
446
|
+
| from 2026-10 | 2026-10 |
|
|
447
|
+
| from 2026-10 to 2026-10-30 | 2026-10-01 to 2026-10-30 |
|
|
448
|
+
#+end_example
|
|
449
|
+
|
|
450
|
+
*** ~#tex_quote~
|
|
451
|
+
~Period#tex_quote~ formats a period in a way suitable for TeX or LaTeX.
|
|
452
|
+
Mainly, this causes it to use an "en-dash" to separate the end points
|
|
453
|
+
and hyphens to separate date components.
|
|
454
|
+
|
|
455
|
+
#+begin_src ruby
|
|
456
|
+
specs = [
|
|
457
|
+
'from this_year',
|
|
458
|
+
'from 2026-2H',
|
|
459
|
+
'from 2026-3Q',
|
|
460
|
+
'from 2026-10',
|
|
461
|
+
'from 2026-10 to 2026-10-30'
|
|
462
|
+
]
|
|
463
|
+
tab = []
|
|
464
|
+
tab << ['Spec', '#to_s']
|
|
465
|
+
tab << nil
|
|
466
|
+
specs.each do |s|
|
|
467
|
+
pd = Period.parse_phrase(s).first
|
|
468
|
+
tab << [s, pd.tex_quote]
|
|
469
|
+
end
|
|
470
|
+
tab
|
|
471
|
+
#+end_src
|
|
472
|
+
|
|
473
|
+
#+RESULTS:
|
|
474
|
+
#+begin_example
|
|
475
|
+
| Spec | #to_s |
|
|
476
|
+
|----------------------------+------------------------|
|
|
477
|
+
| from this_year | 2026-01-01--2026-12-31 |
|
|
478
|
+
| from 2026-2H | 2026-07-01--2026-12-31 |
|
|
479
|
+
| from 2026-3Q | 2026-07-01--2026-09-30 |
|
|
480
|
+
| from 2026-10 | 2026-10-01--2026-10-31 |
|
|
481
|
+
| from 2026-10 to 2026-10-30 | 2026-10-01--2026-10-30 |
|
|
482
|
+
#+end_example
|
|
483
|
+
|
|
484
|
+
** Comparison
|
|
485
|
+
The ~Period~ class includes ~Comparable~ so periods can be compared
|
|
486
|
+
directly. Period are compared by ~first~, and if those are the same,
|
|
487
|
+
then by ~last~, and those using normal ~Date~ comparison.
|
|
488
|
+
|
|
489
|
+
#+begin_src ruby
|
|
490
|
+
spec_pairs = [
|
|
491
|
+
['from 2026', 'from 2026-1H'],
|
|
492
|
+
['from 2026', 'from 2026-2H'],
|
|
493
|
+
['from 2016-05', 'from 2016-05 to 2016-06'],
|
|
494
|
+
['from 2026', 'from 2026-2H'],
|
|
495
|
+
['from 2026', 'from 2025'],
|
|
496
|
+
['from 2010-09', 'from 2010-09'],
|
|
497
|
+
]
|
|
498
|
+
tab = []
|
|
499
|
+
tab << ['Period1', 'Period2', 'Period1 <=> Period2']
|
|
500
|
+
tab << nil
|
|
501
|
+
spec_pairs.each do |s1, s2|
|
|
502
|
+
p1 = Period.parse_phrase(s1).first
|
|
503
|
+
p2 = Period.parse_phrase(s2).first
|
|
504
|
+
tab << [p1.to_s, p2.to_s, p1 <=> p2]
|
|
505
|
+
end
|
|
506
|
+
tab
|
|
507
|
+
#+end_src
|
|
508
|
+
|
|
509
|
+
#+RESULTS:
|
|
510
|
+
#+begin_example
|
|
511
|
+
| Period1 | Period2 | Period1 <=> Period2 |
|
|
512
|
+
|---------+--------------------------+---------------------|
|
|
513
|
+
| 2026 | 2026-1H | 1 |
|
|
514
|
+
| 2026 | 2026-2H | -1 |
|
|
515
|
+
| 2016-05 | 2016-05-01 to 2016-06-30 | -1 |
|
|
516
|
+
| 2026 | 2026-2H | -1 |
|
|
517
|
+
| 2026 | 2025 | 1 |
|
|
518
|
+
| 2010-09 | 2010-09 | 0 |
|
|
519
|
+
#+end_example
|
|
520
|
+
|
|
521
|
+
** Enumeration
|
|
522
|
+
*** ~#each~
|
|
523
|
+
~Period~ includes ~Enumerable~ and defines an ~#each~ method that
|
|
524
|
+
iterates through each day in the period:
|
|
525
|
+
|
|
526
|
+
#+begin_src ruby
|
|
527
|
+
pd = Period.parse('2024-02')
|
|
528
|
+
n = 0
|
|
529
|
+
tab = []
|
|
530
|
+
tab << ['N', 'Day']
|
|
531
|
+
tab << nil
|
|
532
|
+
pd.each do |day|
|
|
533
|
+
tab << [n += 1, day.iso]
|
|
534
|
+
end
|
|
535
|
+
tab
|
|
536
|
+
#+end_src
|
|
537
|
+
|
|
538
|
+
#+RESULTS:
|
|
539
|
+
#+begin_example
|
|
540
|
+
| N | Day |
|
|
541
|
+
|----+------------|
|
|
542
|
+
| 1 | 2024-02-01 |
|
|
543
|
+
| 2 | 2024-02-02 |
|
|
544
|
+
| 3 | 2024-02-03 |
|
|
545
|
+
| 4 | 2024-02-04 |
|
|
546
|
+
| 5 | 2024-02-05 |
|
|
547
|
+
| 6 | 2024-02-06 |
|
|
548
|
+
| 7 | 2024-02-07 |
|
|
549
|
+
| 8 | 2024-02-08 |
|
|
550
|
+
| 9 | 2024-02-09 |
|
|
551
|
+
| 10 | 2024-02-10 |
|
|
552
|
+
| 11 | 2024-02-11 |
|
|
553
|
+
| 12 | 2024-02-12 |
|
|
554
|
+
| 13 | 2024-02-13 |
|
|
555
|
+
| 14 | 2024-02-14 |
|
|
556
|
+
| 15 | 2024-02-15 |
|
|
557
|
+
| 16 | 2024-02-16 |
|
|
558
|
+
| 17 | 2024-02-17 |
|
|
559
|
+
| 18 | 2024-02-18 |
|
|
560
|
+
| 19 | 2024-02-19 |
|
|
561
|
+
| 20 | 2024-02-20 |
|
|
562
|
+
| 21 | 2024-02-21 |
|
|
563
|
+
| 22 | 2024-02-22 |
|
|
564
|
+
| 23 | 2024-02-23 |
|
|
565
|
+
| 24 | 2024-02-24 |
|
|
566
|
+
| 25 | 2024-02-25 |
|
|
567
|
+
| 26 | 2024-02-26 |
|
|
568
|
+
| 27 | 2024-02-27 |
|
|
569
|
+
| 28 | 2024-02-28 |
|
|
570
|
+
| 29 | 2024-02-29 |
|
|
571
|
+
#+end_example
|
|
572
|
+
|
|
573
|
+
*** ~#size~
|
|
574
|
+
~Period#size~ returns the number of days in the period.
|
|
575
|
+
|
|
576
|
+
#+begin_src ruby
|
|
577
|
+
Period.parse('2024-02').size
|
|
578
|
+
#+end_src
|
|
579
|
+
|
|
580
|
+
#+RESULTS:
|
|
581
|
+
#+begin_example
|
|
582
|
+
29
|
|
583
|
+
#+end_example
|
|
584
|
+
|
|
585
|
+
** Chunking
|
|
586
|
+
~FatPeriod~ recognizes several calendar sub-period types than the
|
|
587
|
+
standard libraries do:
|
|
588
|
+
|
|
589
|
+
- day
|
|
590
|
+
- week
|
|
591
|
+
- biweek
|
|
592
|
+
- semimonth
|
|
593
|
+
- month
|
|
594
|
+
- bimonth
|
|
595
|
+
- quarter
|
|
596
|
+
- half
|
|
597
|
+
- year
|
|
598
|
+
|
|
599
|
+
~FatPeriod~ refers to these special kinds of periods as "chunks", and
|
|
600
|
+
they are often useful in financial applications.
|
|
601
|
+
|
|
602
|
+
*** ~Period.<chunk>_containing(date)~
|
|
603
|
+
For each chunk, ~FatPeriod~ provides a method for returning the ~Period~
|
|
604
|
+
of the size of the given chunk containing a given date. For example, to
|
|
605
|
+
find the calendar quarter containing '2016-08-04', you can call
|
|
606
|
+
~Period.quarter_containing('2016-08-04')~ and get the period ~2016-3Q~.
|
|
607
|
+
|
|
608
|
+
#+begin_src ruby
|
|
609
|
+
chunks = %w(
|
|
610
|
+
day
|
|
611
|
+
week
|
|
612
|
+
biweek
|
|
613
|
+
semimonth
|
|
614
|
+
month
|
|
615
|
+
bimonth
|
|
616
|
+
quarter
|
|
617
|
+
half
|
|
618
|
+
year
|
|
619
|
+
)
|
|
620
|
+
tab = []
|
|
621
|
+
tab << ['Chunk Size', 'Containing Period']
|
|
622
|
+
tab << nil
|
|
623
|
+
chunks.each do |ck|
|
|
624
|
+
meth = "#{ck}_containing".to_sym
|
|
625
|
+
pd = Period.send(meth, '2016-08-04').to_s
|
|
626
|
+
tab << [ck, pd.to_s]
|
|
627
|
+
end
|
|
628
|
+
tab
|
|
629
|
+
#+end_src
|
|
630
|
+
|
|
631
|
+
#+RESULTS:
|
|
632
|
+
#+begin_example
|
|
633
|
+
| Chunk Size | Containing Period |
|
|
634
|
+
|------------+--------------------------|
|
|
635
|
+
| day | 2016-08-04 to 2016-08-04 |
|
|
636
|
+
| week | 2016-08-01 to 2016-08-07 |
|
|
637
|
+
| biweek | 2016-08-01 to 2016-08-14 |
|
|
638
|
+
| semimonth | 2016-08-01 to 2016-08-15 |
|
|
639
|
+
| month | 2016-08 |
|
|
640
|
+
| bimonth | 2016-07-01 to 2016-08-31 |
|
|
641
|
+
| quarter | 2016-3Q |
|
|
642
|
+
| half | 2016-2H |
|
|
643
|
+
| year | 2016 |
|
|
644
|
+
#+end_example
|
|
645
|
+
|
|
646
|
+
~Period~ also provides several related methods that return the
|
|
647
|
+
chunk-sized period containing today's date:
|
|
648
|
+
|
|
649
|
+
#+begin_src ruby
|
|
650
|
+
chunks = %w(
|
|
651
|
+
day
|
|
652
|
+
week
|
|
653
|
+
biweek
|
|
654
|
+
semimonth
|
|
655
|
+
month
|
|
656
|
+
bimonth
|
|
657
|
+
quarter
|
|
658
|
+
half
|
|
659
|
+
year
|
|
660
|
+
)
|
|
661
|
+
tab = []
|
|
662
|
+
tab << ['Chunk Size', 'Current Period']
|
|
663
|
+
tab << nil
|
|
664
|
+
chunks.each do |ck|
|
|
665
|
+
meth = "this_#{ck}".to_sym
|
|
666
|
+
pd = Period.send(meth).to_s
|
|
667
|
+
tab << [ck, pd.to_s]
|
|
668
|
+
end
|
|
669
|
+
tab
|
|
670
|
+
#+end_src
|
|
671
|
+
|
|
672
|
+
#+RESULTS:
|
|
673
|
+
#+begin_example
|
|
674
|
+
| Chunk Size | Current Period |
|
|
675
|
+
|------------+--------------------------|
|
|
676
|
+
| day | 2026-11-05 to 2026-11-05 |
|
|
677
|
+
| week | 2026-11-02 to 2026-11-08 |
|
|
678
|
+
| biweek | 2026-10-26 to 2026-11-08 |
|
|
679
|
+
| semimonth | 2026-11-01 to 2026-11-15 |
|
|
680
|
+
| month | 2026-11 |
|
|
681
|
+
| bimonth | 2026-11-01 to 2026-12-31 |
|
|
682
|
+
| quarter | 2026-4Q |
|
|
683
|
+
| half | 2026-2H |
|
|
684
|
+
| year | 2026 |
|
|
685
|
+
#+end_example
|
|
686
|
+
|
|
687
|
+
*** ~#chunk_sym~ and ~#chunk_name~
|
|
688
|
+
A ~Period~ can determine if it is a chunk with the ~#chunk_sym~ method.
|
|
689
|
+
It's ~first~ and ~last~ dates correspond to a chunk boundary, it returns
|
|
690
|
+
a ~Symbol~ for its chunks size, or the symbol :irregular otherwise. A
|
|
691
|
+
similar method, ~#chunk_name~, returns a ~String~ name of the chunk:
|
|
692
|
+
|
|
693
|
+
#+begin_src ruby
|
|
694
|
+
Date.beginning_of_week = :sunday
|
|
695
|
+
pds = [
|
|
696
|
+
Period.new('2016-02-01', '2016-02-29'),
|
|
697
|
+
Period.new('2016-02-01', '2016-02-28'),
|
|
698
|
+
Period.new('2017-02-01', '2017-02-28'),
|
|
699
|
+
Period.new('2017-01-01', '2017-02-28'),
|
|
700
|
+
Period.new('2016-01-01', '2016-03-31'),
|
|
701
|
+
Period.new('2016-03-20', '2016-03-26'),
|
|
702
|
+
Period.new('2016-01-02', '2016-04-01'),
|
|
703
|
+
]
|
|
704
|
+
tab = []
|
|
705
|
+
tab << ['First', 'Last', 'Chunk']
|
|
706
|
+
tab << nil
|
|
707
|
+
pds.each do |pd|
|
|
708
|
+
tab << [pd.first.iso, pd.last.iso, pd.chunk_sym]
|
|
709
|
+
end
|
|
710
|
+
tab
|
|
711
|
+
#+end_src
|
|
712
|
+
|
|
713
|
+
#+RESULTS:
|
|
714
|
+
#+begin_example
|
|
715
|
+
| First | Last | Chunk |
|
|
716
|
+
|------------+------------+------------|
|
|
717
|
+
| 2016-02-01 | 2016-02-29 | :month |
|
|
718
|
+
| 2016-02-01 | 2016-02-28 | :irregular |
|
|
719
|
+
| 2017-02-01 | 2017-02-28 | :month |
|
|
720
|
+
| 2017-01-01 | 2017-02-28 | :bimonth |
|
|
721
|
+
| 2016-01-01 | 2016-03-31 | :quarter |
|
|
722
|
+
| 2016-03-20 | 2016-03-26 | :week |
|
|
723
|
+
| 2016-01-02 | 2016-04-01 | :irregular |
|
|
724
|
+
#+end_example
|
|
725
|
+
|
|
726
|
+
*** ~#chunks(size: :month, partial_first: true, partial_last: true, round_up_last: false)~
|
|
727
|
+
You may want to divide a year into months, or a quarter into
|
|
728
|
+
semi-months. For this ~FatTable~ provides ~Period#chunks~. By default,
|
|
729
|
+
the first and last chunks will be partial, the first starting at
|
|
730
|
+
~Period#first~ regardless of whether it is a full chunk and the last
|
|
731
|
+
ending at ~Period#last~ regardless of whether it is a full chunk. But
|
|
732
|
+
this can be altered at the cost of not fully spanning the subject
|
|
733
|
+
~Period~.
|
|
734
|
+
|
|
735
|
+
- ~partial_first~ :: if set ~false~, the first chunk will be a
|
|
736
|
+
full-sized chunk at the possible cost of starting later than
|
|
737
|
+
~Period#first~; it is ~true~ by default.
|
|
738
|
+
- ~partial_last~ :: if set ~false~, the last chunk will be a
|
|
739
|
+
full-sized chunk at the possible cost of ending earlier than
|
|
740
|
+
~Period#last~; it is ~true~ by default.
|
|
741
|
+
- ~round_up_last~ :: if set ~true~, the last chunk returned may extend
|
|
742
|
+
beyond ~Period#last~ if it needs to in order to make it whole; it is
|
|
743
|
+
~false~ by default.
|
|
744
|
+
|
|
745
|
+
Since weeks do not necessarily align with calendar quarters, we get a
|
|
746
|
+
partial first and last week by default:
|
|
747
|
+
|
|
748
|
+
#+begin_src ruby
|
|
749
|
+
Period.parse('2026-3Q').chunks(size: :week).map {|m| [m]}
|
|
750
|
+
#+end_src
|
|
751
|
+
|
|
752
|
+
#+RESULTS:
|
|
753
|
+
#+begin_example
|
|
754
|
+
| Period | (2026-07-01..2026-07-04) |
|
|
755
|
+
| Period | (2026-07-05..2026-07-11) |
|
|
756
|
+
| Period | (2026-07-12..2026-07-18) |
|
|
757
|
+
| Period | (2026-07-19..2026-07-25) |
|
|
758
|
+
| Period | (2026-07-26..2026-08-01) |
|
|
759
|
+
| Period | (2026-08-02..2026-08-08) |
|
|
760
|
+
| Period | (2026-08-09..2026-08-15) |
|
|
761
|
+
| Period | (2026-08-16..2026-08-22) |
|
|
762
|
+
| Period | (2026-08-23..2026-08-29) |
|
|
763
|
+
| Period | (2026-08-30..2026-09-05) |
|
|
764
|
+
| Period | (2026-09-06..2026-09-12) |
|
|
765
|
+
| Period | (2026-09-13..2026-09-19) |
|
|
766
|
+
| Period | (2026-09-20..2026-09-26) |
|
|
767
|
+
| Period | (2026-09-27..2026-09-30) |
|
|
768
|
+
#+end_example
|
|
769
|
+
|
|
770
|
+
But we can suppress the partials at the cost of not spanning the whole
|
|
771
|
+
quarter:
|
|
772
|
+
|
|
773
|
+
#+begin_src ruby
|
|
774
|
+
Period.parse('2026-3Q').chunks(size: :week, partial_first: false, partial_last: false).map {|m| [m]}
|
|
775
|
+
#+end_src
|
|
776
|
+
|
|
777
|
+
#+RESULTS:
|
|
778
|
+
#+begin_example
|
|
779
|
+
| Period | (2026-07-05..2026-07-11) |
|
|
780
|
+
| Period | (2026-07-12..2026-07-18) |
|
|
781
|
+
| Period | (2026-07-19..2026-07-25) |
|
|
782
|
+
| Period | (2026-07-26..2026-08-01) |
|
|
783
|
+
| Period | (2026-08-02..2026-08-08) |
|
|
784
|
+
| Period | (2026-08-09..2026-08-15) |
|
|
785
|
+
| Period | (2026-08-16..2026-08-22) |
|
|
786
|
+
| Period | (2026-08-23..2026-08-29) |
|
|
787
|
+
| Period | (2026-08-30..2026-09-05) |
|
|
788
|
+
| Period | (2026-09-06..2026-09-12) |
|
|
789
|
+
| Period | (2026-09-13..2026-09-19) |
|
|
790
|
+
| Period | (2026-09-20..2026-09-26) |
|
|
791
|
+
#+end_example
|
|
792
|
+
|
|
793
|
+
With ~round_up_last~ set ~true~, the last chunk overflows the subject
|
|
794
|
+
Period's bounds if needed to make it a whole week:
|
|
795
|
+
|
|
796
|
+
#+begin_src ruby
|
|
797
|
+
Period.parse('2026-3Q').chunks(size: :week, round_up_last: true).map {|m| [m]}
|
|
798
|
+
#+end_src
|
|
799
|
+
|
|
800
|
+
#+RESULTS:
|
|
801
|
+
#+begin_example
|
|
802
|
+
| Period | (2026-07-01..2026-07-04) |
|
|
803
|
+
| Period | (2026-07-05..2026-07-11) |
|
|
804
|
+
| Period | (2026-07-12..2026-07-18) |
|
|
805
|
+
| Period | (2026-07-19..2026-07-25) |
|
|
806
|
+
| Period | (2026-07-26..2026-08-01) |
|
|
807
|
+
| Period | (2026-08-02..2026-08-08) |
|
|
808
|
+
| Period | (2026-08-09..2026-08-15) |
|
|
809
|
+
| Period | (2026-08-16..2026-08-22) |
|
|
810
|
+
| Period | (2026-08-23..2026-08-29) |
|
|
811
|
+
| Period | (2026-08-30..2026-09-05) |
|
|
812
|
+
| Period | (2026-09-06..2026-09-12) |
|
|
813
|
+
| Period | (2026-09-13..2026-09-19) |
|
|
814
|
+
| Period | (2026-09-20..2026-09-26) |
|
|
815
|
+
| Period | (2026-09-27..2026-10-03) |
|
|
816
|
+
#+end_example
|
|
817
|
+
|
|
818
|
+
** Set Operations
|
|
819
|
+
Another question one might want to ask of a ~Period~ is how it relates
|
|
820
|
+
to another ~Period~. Does it fully contain another? Is it a sub-period
|
|
821
|
+
of another, and so-forth. Since these resemble the same sorts of
|
|
822
|
+
questions one might ask of sets of numbers, these are called "set
|
|
823
|
+
operations" on ~Period~s.
|
|
824
|
+
|
|
825
|
+
*** ~#subset_of?(other)~
|
|
826
|
+
Is this ~Period~ wholly contained within the ~other~ ~Period~?
|
|
827
|
+
|
|
828
|
+
#+begin_src ruby
|
|
829
|
+
Period.parse('2026-1Q').subset_of?(Period.parse('2026'))
|
|
830
|
+
#+end_src
|
|
831
|
+
|
|
832
|
+
#+RESULTS:
|
|
833
|
+
#+begin_example
|
|
834
|
+
true
|
|
835
|
+
#+end_example
|
|
836
|
+
|
|
837
|
+
Note that every ~Period~ is it's own subset:
|
|
838
|
+
|
|
839
|
+
#+begin_src ruby
|
|
840
|
+
Period.parse('2026').subset_of?(Period.parse('2026'))
|
|
841
|
+
#+end_src
|
|
842
|
+
|
|
843
|
+
#+RESULTS:
|
|
844
|
+
#+begin_example
|
|
845
|
+
true
|
|
846
|
+
#+end_example
|
|
847
|
+
|
|
848
|
+
#+begin_src ruby
|
|
849
|
+
Period.parse('2026').subset_of?(Period.parse('2026-2Q'))
|
|
850
|
+
#+end_src
|
|
851
|
+
|
|
852
|
+
#+RESULTS:
|
|
853
|
+
#+begin_example
|
|
854
|
+
false
|
|
855
|
+
#+end_example
|
|
856
|
+
|
|
857
|
+
*** ~#proper_subset_of?(other)~
|
|
858
|
+
Is this ~Period~ wholly contained within the ~other~ ~Period~ but not
|
|
859
|
+
identical to it?
|
|
860
|
+
|
|
861
|
+
#+begin_src ruby
|
|
862
|
+
Period.parse('2026-1Q').proper_subset_of?(Period.parse('2026'))
|
|
863
|
+
#+end_src
|
|
864
|
+
|
|
865
|
+
#+RESULTS:
|
|
866
|
+
#+begin_example
|
|
867
|
+
true
|
|
868
|
+
#+end_example
|
|
869
|
+
|
|
870
|
+
Note that every ~Period~ is /not/ it's own proper subset:
|
|
871
|
+
|
|
872
|
+
#+begin_src ruby
|
|
873
|
+
Period.parse('2026').proper_subset_of?(Period.parse('2026'))
|
|
874
|
+
#+end_src
|
|
875
|
+
|
|
876
|
+
#+RESULTS:
|
|
877
|
+
#+begin_example
|
|
878
|
+
false
|
|
879
|
+
#+end_example
|
|
880
|
+
|
|
881
|
+
*** ~#superset_of(other)~
|
|
882
|
+
Does this ~Period~ wholly contain ~other~ ~Period~?
|
|
883
|
+
|
|
884
|
+
#+begin_src ruby
|
|
885
|
+
Period.parse('2026-1Q').superset_of?(Period.parse('2026'))
|
|
886
|
+
#+end_src
|
|
887
|
+
|
|
888
|
+
#+RESULTS:
|
|
889
|
+
#+begin_example
|
|
890
|
+
false
|
|
891
|
+
#+end_example
|
|
892
|
+
|
|
893
|
+
#+begin_src ruby
|
|
894
|
+
Period.parse('2026').superset_of?(Period.parse('2026-2H'))
|
|
895
|
+
#+end_src
|
|
896
|
+
|
|
897
|
+
#+RESULTS:
|
|
898
|
+
#+begin_example
|
|
899
|
+
true
|
|
900
|
+
#+end_example
|
|
901
|
+
|
|
902
|
+
Note that every ~Period~ is it's own superset:
|
|
903
|
+
|
|
904
|
+
#+begin_src ruby
|
|
905
|
+
Period.parse('2026').superset_of?(Period.parse('2026'))
|
|
906
|
+
#+end_src
|
|
907
|
+
|
|
908
|
+
#+RESULTS:
|
|
909
|
+
#+begin_example
|
|
910
|
+
true
|
|
911
|
+
#+end_example
|
|
912
|
+
|
|
913
|
+
*** ~#proper_superset_of(other)~
|
|
914
|
+
Does this ~Period~ wholly contain ~other~ ~Period~ without being
|
|
915
|
+
identical to it?
|
|
916
|
+
|
|
917
|
+
#+begin_src ruby
|
|
918
|
+
Period.parse('2026').proper_superset_of?(Period.parse('2026-2Q'))
|
|
919
|
+
#+end_src
|
|
920
|
+
|
|
921
|
+
#+RESULTS:
|
|
922
|
+
#+begin_example
|
|
923
|
+
true
|
|
924
|
+
#+end_example
|
|
925
|
+
|
|
926
|
+
Note that every ~Period~ is /not/ it's own proper superset:
|
|
927
|
+
|
|
928
|
+
#+begin_src ruby
|
|
929
|
+
Period.parse('2026').proper_superset_of?(Period.parse('2026'))
|
|
930
|
+
#+end_src
|
|
931
|
+
|
|
932
|
+
#+RESULTS:
|
|
933
|
+
#+begin_example
|
|
934
|
+
false
|
|
935
|
+
#+end_example
|
|
936
|
+
|
|
937
|
+
*** ~#intersection(other)~
|
|
938
|
+
Return a new ~Period~ that is contained in both the subject ~Period~ and
|
|
939
|
+
~other~.
|
|
940
|
+
|
|
941
|
+
#+begin_src ruby
|
|
942
|
+
Period.parse('2026').intersection(Period.parse_phrase('from 2025-33W to 2026-23W').first)
|
|
943
|
+
#+end_src
|
|
944
|
+
|
|
945
|
+
#+RESULTS:
|
|
946
|
+
#+begin_example
|
|
947
|
+
Period(2026-01-01..2026-06-07)
|
|
948
|
+
#+end_example
|
|
949
|
+
|
|
950
|
+
The ~&~ operator is equivalent.
|
|
951
|
+
#+begin_src ruby
|
|
952
|
+
Period.parse('2026') & Period.parse_phrase('from 2025-33W to 2026-23W').first
|
|
953
|
+
#+end_src
|
|
954
|
+
|
|
955
|
+
#+RESULTS:
|
|
956
|
+
#+begin_example
|
|
957
|
+
Period(2026-01-01..2026-06-07)
|
|
958
|
+
#+end_example
|
|
959
|
+
|
|
960
|
+
As is the method ~narrow_to~.
|
|
961
|
+
#+begin_src ruby
|
|
962
|
+
Period.parse('2026').narrow_to(Period.parse_phrase('from 2025-33W to 2026-23W').first)
|
|
963
|
+
#+end_src
|
|
964
|
+
|
|
965
|
+
#+RESULTS:
|
|
966
|
+
#+begin_example
|
|
967
|
+
Period(2026-01-01..2026-06-07)
|
|
968
|
+
#+end_example
|
|
969
|
+
|
|
970
|
+
Return ~nil~ if there is no intersection:
|
|
971
|
+
#+begin_src ruby
|
|
972
|
+
Period.parse('2026').intersection(Period.parse('2024-3Q'))
|
|
973
|
+
#+end_src
|
|
974
|
+
|
|
975
|
+
#+RESULTS:
|
|
976
|
+
#+begin_example
|
|
977
|
+
nil
|
|
978
|
+
#+end_example
|
|
979
|
+
|
|
980
|
+
*** ~#union(other)~
|
|
981
|
+
If two ~Periods~ either overlap or are contiguous, you can return a
|
|
982
|
+
~Period~ that spans both:
|
|
983
|
+
|
|
984
|
+
#+begin_src ruby
|
|
985
|
+
Period.parse('2026').union(Period.parse_phrase('from 2025-33W to 2026-23W').first)
|
|
986
|
+
#+end_src
|
|
987
|
+
|
|
988
|
+
#+RESULTS:
|
|
989
|
+
#+begin_example
|
|
990
|
+
Period(2025-08-11..2026-12-31)
|
|
991
|
+
#+end_example
|
|
992
|
+
|
|
993
|
+
The ~+~ operator is equivalent.
|
|
994
|
+
#+begin_src ruby
|
|
995
|
+
Period.parse('2026') + Period.parse_phrase('from 2025-33W to 2026-23W').first
|
|
996
|
+
#+end_src
|
|
997
|
+
|
|
998
|
+
#+RESULTS:
|
|
999
|
+
#+begin_example
|
|
1000
|
+
Period(2025-08-11..2026-12-31)
|
|
1001
|
+
#+end_example
|
|
1002
|
+
|
|
1003
|
+
Return ~nil~ if there is no overlap or contiguity:
|
|
1004
|
+
#+begin_src ruby
|
|
1005
|
+
Period.parse('2026').union(Period.parse('2024-3Q'))
|
|
1006
|
+
#+end_src
|
|
1007
|
+
|
|
1008
|
+
#+RESULTS:
|
|
1009
|
+
#+begin_example
|
|
1010
|
+
nil
|
|
1011
|
+
#+end_example
|
|
1012
|
+
|
|
1013
|
+
*** ~#difference(other)~
|
|
1014
|
+
Return an ~Array~ of ~Periods~ representing the subject ~Period~ with
|
|
1015
|
+
any overlap with ~other~ excluded. If there is no overlap, return the
|
|
1016
|
+
subject ~Period~.
|
|
1017
|
+
|
|
1018
|
+
#+begin_src ruby
|
|
1019
|
+
Period.parse('2015-1Q').difference(Period.parse('2015-02')).map {|p| [p.to_s]}
|
|
1020
|
+
#+end_src
|
|
1021
|
+
|
|
1022
|
+
#+RESULTS:
|
|
1023
|
+
#+begin_example
|
|
1024
|
+
| 2015-01 |
|
|
1025
|
+
| 2015-03 |
|
|
1026
|
+
#+end_example
|
|
1027
|
+
|
|
1028
|
+
#+begin_src ruby
|
|
1029
|
+
Period.parse('2015-1Q').difference(Period.parse('2015-08')).map {|p| [p.to_s]}
|
|
1030
|
+
#+end_src
|
|
1031
|
+
|
|
1032
|
+
#+RESULTS:
|
|
1033
|
+
#+begin_example
|
|
1034
|
+
| 2015-1Q |
|
|
1035
|
+
#+end_example
|
|
1036
|
+
|
|
1037
|
+
The operator ~-~ can be used as well:
|
|
1038
|
+
#+begin_src ruby
|
|
1039
|
+
(Period.parse('2015-1Q') - Period.parse('2015-02')).map {|p| [p.to_s]}
|
|
1040
|
+
#+end_src
|
|
1041
|
+
|
|
1042
|
+
#+RESULTS:
|
|
1043
|
+
#+begin_example
|
|
1044
|
+
| 2015-01 |
|
|
1045
|
+
| 2015-03 |
|
|
1046
|
+
#+end_example
|
|
1047
|
+
|
|
1048
|
+
** Coverage
|
|
1049
|
+
*** ~#contains?(date)~
|
|
1050
|
+
Return whether the subject ~Period~ contains the given ~Date~.
|
|
1051
|
+
|
|
1052
|
+
#+begin_src ruby
|
|
1053
|
+
Period.parse('2026-2Q').contains?('2026-04-15')
|
|
1054
|
+
#+end_src
|
|
1055
|
+
|
|
1056
|
+
#+RESULTS:
|
|
1057
|
+
#+begin_example
|
|
1058
|
+
true
|
|
1059
|
+
#+end_example
|
|
1060
|
+
|
|
1061
|
+
#+begin_src ruby
|
|
1062
|
+
Period.parse('2026-2Q').contains?('2026-08-15')
|
|
1063
|
+
#+end_src
|
|
1064
|
+
|
|
1065
|
+
#+RESULTS:
|
|
1066
|
+
#+begin_example
|
|
1067
|
+
false
|
|
1068
|
+
#+end_example
|
|
1069
|
+
|
|
1070
|
+
*** ~Period.overlaps_among?(periods)~
|
|
1071
|
+
This class method takes an ~Array~ of ~Periods~ and returns whether the
|
|
1072
|
+
given periods have any overlaps with one another.
|
|
1073
|
+
|
|
1074
|
+
*** ~#overlaps_among?~
|
|
1075
|
+
Similar to ~Period.overlaps_among~ except that it returns true only if
|
|
1076
|
+
any of the overlaps occur within the subject ~Period~.
|
|
1077
|
+
|
|
1078
|
+
*** ~#spanned_by(periods)~
|
|
1079
|
+
Returns whether the given ~Array~ of ~Periods~ "span" the subject
|
|
1080
|
+
~Period~, that is, whether they collectively cover the subject ~Period~
|
|
1081
|
+
with no overlaps and no gaps.
|
|
1082
|
+
|
|
1083
|
+
*** ~#gaps~
|
|
1084
|
+
Returns an ~Array~ of ~Period~s representing the gaps within the subject
|
|
1085
|
+
~Period~ not covered by the given ~Array~ of ~Periods~. Overlaps among
|
|
1086
|
+
the periods do not affect the result nor do gaps outside the range of
|
|
1087
|
+
the subject ~Period~. Ordering among the given ~periods~ does not
|
|
1088
|
+
matter.
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
** Interest Day-count fraction (~#year_fraction~)
|
|
1092
|
+
For interest-related calculations, the methods by which accrued interest
|
|
1093
|
+
is allocated to a partial ~Period~ are numerous and subtle. Rather than
|
|
1094
|
+
solve this problem over-and-over, ~FatPeriod~ aims to implement a single
|
|
1095
|
+
method that solves this problem once-and-for-all.
|
|
1096
|
+
|
|
1097
|
+
The dates that correspond to the notation in Wikipedia's [[https://en.wikipedia.org/wiki/Day_count_convention][Day count
|
|
1098
|
+
convention]] article, which was used as a reference for ~FatPeriod~'s
|
|
1099
|
+
implementation, are as follows:
|
|
1100
|
+
|
|
1101
|
+
- ~first~ is Date1, the start of accrual.
|
|
1102
|
+
- ~date2~ is Date2, the payment or settlement date, supplied as a ~Date~ or a
|
|
1103
|
+
date string. It must fall within the Period, including its endpoints.
|
|
1104
|
+
- ~last~ is Date3, the next coupon payment date. For ICMA, construct the
|
|
1105
|
+
Period with consecutive coupon boundaries.
|
|
1106
|
+
|
|
1107
|
+
~Period#year_fraction~ returns a ~BigDecimal~ factor for calculating
|
|
1108
|
+
interest from ~first~ to a given payment or settlement date. Usually
|
|
1109
|
+
this is a fraction of a year: multiply the principal by the annual rate
|
|
1110
|
+
and this factor to calculate accrued interest. The default convention
|
|
1111
|
+
is *30/360 US* (~convention: 1~).
|
|
1112
|
+
|
|
1113
|
+
#+begin_example
|
|
1114
|
+
period.year_fraction(date2, convention: 1, eom: true, maturity_date: nil)
|
|
1115
|
+
#+end_example
|
|
1116
|
+
|
|
1117
|
+
Day counts use elapsed days, without adding one as ~Period#size~ does. For
|
|
1118
|
+
example, January 1 to July 1 is the reference period for a coupon paid on
|
|
1119
|
+
July 1; use July 1 as ~last~, rather than June 30. Passing ~last~ as the
|
|
1120
|
+
settlement date calculates the full coupon's factor.
|
|
1121
|
+
|
|
1122
|
+
#+begin_src ruby
|
|
1123
|
+
period = Period.new('2024-01-01', '2024-07-01')
|
|
1124
|
+
pay_date = '2024-04-01'
|
|
1125
|
+
principal = BigDecimal('1000')
|
|
1126
|
+
rate = BigDecimal('0.05')
|
|
1127
|
+
|
|
1128
|
+
tab =
|
|
1129
|
+
[['Principal', principal.to_s('F')],
|
|
1130
|
+
['Rate', rate.to_s('F')],
|
|
1131
|
+
['Period', period.to_s],
|
|
1132
|
+
['Payment Date', pay_date]]
|
|
1133
|
+
tab
|
|
1134
|
+
#+end_src
|
|
1135
|
+
|
|
1136
|
+
#+RESULTS:
|
|
1137
|
+
#+begin_example
|
|
1138
|
+
| Principal | 1000.0 |
|
|
1139
|
+
| Rate | 0.05 |
|
|
1140
|
+
| Period | 2024-01-01 to 2024-07-01 |
|
|
1141
|
+
| Payment Date | 2024-04-01 |
|
|
1142
|
+
#+end_example
|
|
1143
|
+
|
|
1144
|
+
#+begin_src ruby :results value table
|
|
1145
|
+
tab2 = []
|
|
1146
|
+
tab2 << ['Convention', 'Factor', 'Interest']
|
|
1147
|
+
tab2 << nil
|
|
1148
|
+
0.upto(11) do |conv|
|
|
1149
|
+
factor = period.year_fraction(pay_date, convention: conv)
|
|
1150
|
+
interest = principal * rate * factor
|
|
1151
|
+
tab2 << [conv, factor.round(4).to_s('F'), interest.round(2).to_s('F')]
|
|
1152
|
+
end
|
|
1153
|
+
tab2
|
|
1154
|
+
#+end_src
|
|
1155
|
+
|
|
1156
|
+
#+RESULTS:
|
|
1157
|
+
#+begin_example
|
|
1158
|
+
| Convention | Factor | Interest |
|
|
1159
|
+
|------------+--------+----------|
|
|
1160
|
+
| 0 | 0.25 | 12.5 |
|
|
1161
|
+
| 1 | 0.25 | 12.5 |
|
|
1162
|
+
| 2 | 0.25 | 12.5 |
|
|
1163
|
+
| 3 | 0.25 | 12.5 |
|
|
1164
|
+
| 4 | 0.25 | 12.5 |
|
|
1165
|
+
| 5 | 0.2486 | 12.43 |
|
|
1166
|
+
| 6 | 0.2493 | 12.47 |
|
|
1167
|
+
| 7 | 0.2528 | 12.64 |
|
|
1168
|
+
| 8 | 0.25 | 12.5 |
|
|
1169
|
+
| 9 | 0.2486 | 12.43 |
|
|
1170
|
+
| 10 | 0.2486 | 12.43 |
|
|
1171
|
+
| 11 | 1.0 | 50.0 |
|
|
1172
|
+
#+end_example
|
|
1173
|
+
|
|
1174
|
+
*** Choosing a convention
|
|
1175
|
+
The convention numbers are specific to ~FatPeriod~; they are not Excel's
|
|
1176
|
+
~YEARFRAC~ basis numbers. Supply ~convention: 0~ explicitly for Bond Basis.
|
|
1177
|
+
The ISDA references below are to Section 4.16 of the /2006 ISDA Definitions/.
|
|
1178
|
+
|
|
1179
|
+
| Convention | Name | Reference |
|
|
1180
|
+
|------------+---------------------+---------------------------------|
|
|
1181
|
+
| 0 | 30/360 Bond Basis | ISDA 4.16(f) |
|
|
1182
|
+
| 1 | 30/360 US (default) | 4.16(f), plus US February rules |
|
|
1183
|
+
| 2 | 30E/360 | ISDA 4.16(g) |
|
|
1184
|
+
| 3 | 30E/360 ISDA | ISDA 4.16(h) |
|
|
1185
|
+
| 4 | Actual/Actual ICMA | ISDA 4.16(c); ICMA Rule 251 |
|
|
1186
|
+
| 5 | Actual/Actual ISDA | ISDA 4.16(b) |
|
|
1187
|
+
| 6 | Actual/365 Fixed | ISDA 4.16(d) |
|
|
1188
|
+
| 7 | Actual/360 | ISDA 4.16(e) |
|
|
1189
|
+
| 8 | Actual/364 | Wikipedia's Actual/364 section |
|
|
1190
|
+
| 9 | Actual/365L | Wikipedia's Actual/365L section |
|
|
1191
|
+
| 10 | Actual/Actual AFB | Wikipedia's AFB discussion |
|
|
1192
|
+
| 11 | 1/1 | ISDA 4.16(a) |
|
|
1193
|
+
|
|
1194
|
+
For convention 1, ~eom: true~ (the default) applies the February
|
|
1195
|
+
end-of-month rules for investments that pay at month end. Set ~eom:
|
|
1196
|
+
false~ to omit those February adjustments. The remaining 30/360
|
|
1197
|
+
adjustments still apply.
|
|
1198
|
+
|
|
1199
|
+
For convention 3, ~maturity_date:~ accepts a ~Date~ or date string and
|
|
1200
|
+
identifies the investment's final maturity. It is irrelevant for the
|
|
1201
|
+
other conventions and is ~nil~ by default. Under convention 3, when
|
|
1202
|
+
settlement (i.e., ~date2~) is that date and is the last day of February,
|
|
1203
|
+
its day number is left unchanged. Otherwise the usual month-end
|
|
1204
|
+
adjustment applies.
|
|
1205
|
+
|
|
1206
|
+
#+begin_src ruby
|
|
1207
|
+
final_coupon = Period.new('2023-08-31', '2024-02-29')
|
|
1208
|
+
ordinary = final_coupon.year_fraction('2024-02-29', convention: 3)
|
|
1209
|
+
at_maturity = final_coupon.year_fraction(
|
|
1210
|
+
'2024-02-29', convention: 3, maturity_date: '2024-02-29'
|
|
1211
|
+
)
|
|
1212
|
+
[ordinary.to_s('F'), at_maturity.round(10).to_s('F')]
|
|
1213
|
+
#+end_src
|
|
1214
|
+
|
|
1215
|
+
#+RESULTS:
|
|
1216
|
+
#+begin_example
|
|
1217
|
+
| 0.5 | 0.4972222222 |
|
|
1218
|
+
#+end_example
|
|
1219
|
+
|
|
1220
|
+
*** Actual-day conventions and boundaries
|
|
1221
|
+
ICMA (4) divides elapsed days by the product of payment frequency and elapsed
|
|
1222
|
+
days in the full coupon period. Frequency is inferred from the full period's
|
|
1223
|
+
length. For example, both a full 181-day half-year coupon and a full 184-day
|
|
1224
|
+
half-year coupon have a factor of 0.5, but their daily accruals differ.
|
|
1225
|
+
|
|
1226
|
+
#+begin_src ruby
|
|
1227
|
+
coupon = Period.new('2024-01-01', '2024-07-01')
|
|
1228
|
+
partial = coupon.year_fraction('2024-04-01', convention: 4)
|
|
1229
|
+
full = coupon.year_fraction(coupon.last, convention: 4)
|
|
1230
|
+
[partial.to_s('F'), full.to_s('F')]
|
|
1231
|
+
#+end_src
|
|
1232
|
+
|
|
1233
|
+
#+RESULTS:
|
|
1234
|
+
#+begin_example
|
|
1235
|
+
| 0.25 | 0.5 |
|
|
1236
|
+
#+end_example
|
|
1237
|
+
|
|
1238
|
+
ICMA supports regular coupon periods only; it does not construct notional
|
|
1239
|
+
coupon schedules for irregular first or final coupons. Frequency inference
|
|
1240
|
+
allows the same 10% length tolerance as ~chunk_name~ for monthly and longer
|
|
1241
|
+
periods. An unrecognizable length raises ~ArgumentError~ for conventions
|
|
1242
|
+
that require frequency (4 and 9).
|
|
1243
|
+
|
|
1244
|
+
ISDA Actual/Actual (5) splits the elapsed interval at calendar-year boundaries,
|
|
1245
|
+
using 365 or 366 for each portion. Conventions 6, 7, and 8 use fixed
|
|
1246
|
+
denominators of 365, 360, and 364 respectively.
|
|
1247
|
+
|
|
1248
|
+
Actual/365L (9) follows the Wikipedia article's settlement-date rule: for
|
|
1249
|
+
annual payments, it uses 366 if February 29 falls after ~first~ and on or
|
|
1250
|
+
before settlement, otherwise 365. For other frequencies, it uses the number
|
|
1251
|
+
of days in the settlement year. The full period determines frequency, not
|
|
1252
|
+
the leap-day test interval.
|
|
1253
|
+
|
|
1254
|
+
AFB (10) counts complete years backwards from settlement, then calculates the
|
|
1255
|
+
remaining initial fraction. It uses the ISDA backward-counting February
|
|
1256
|
+
adjustment described in the article: a February 28 anniversary moves to
|
|
1257
|
+
February 29 when the preceding year is a leap year. The remaining fraction
|
|
1258
|
+
uses 366 only when it includes February 29, counting its start but excluding
|
|
1259
|
+
its end.
|
|
1260
|
+
|
|
1261
|
+
Equal start and settlement dates return zero for conventions 0 through 10.
|
|
1262
|
+
Convention 11 (1/1) always returns one, even for equal dates: it applies the
|
|
1263
|
+
full contractual rate without prorating for time. Invalid convention numbers
|
|
1264
|
+
or settlement dates outside the Period raise ~ArgumentError~.
|
|
1265
|
+
|
|
1266
|
+
*** Sources
|
|
1267
|
+
- [[https://en.wikipedia.org/wiki/Day_count_convention][Wikipedia: Day count convention]] gives the formulas, naming variants, and
|
|
1268
|
+
discussion of the conventions implemented here.
|
|
1269
|
+
- [[https://www.isda.org/book/2006-isda-definitions/][2006 ISDA Definitions]], Section 4.16, /Day Count Fraction/, is the primary
|
|
1270
|
+
source for most of these rules. A [[https://www.sc.com/en/uploads/sites/66/content/docs/2006-ISDA-Definitions.pdf][PDF copy hosted by Standard Chartered]]
|
|
1271
|
+
contains the section on printed pages 11-13. Section 4.16(f) defines Bond
|
|
1272
|
+
Basis; the additional US February rules are described in the Wikipedia
|
|
1273
|
+
article rather than that clause.
|
|
60
1274
|
|
|
61
|
-
The ~Period~ class depends on the extensions to ~Date~ made by the ~fat_core~
|
|
62
|
-
gem, which you can read about [[https://github.com/ddoherty03/fat_core][here]]. It defines two constants, ~Date::BOT~ and
|
|
63
|
-
~Date::EOT~, which define beginning of time and end of time for practical
|
|
64
|
-
commercial purposes.
|
|
65
1275
|
|
|
66
1276
|
* Development
|
|
67
1277
|
|