fat_period 3.0.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/.github/workflows/ruby.yml +5 -2
- data/.gitignore +1 -0
- data/CHANGELOG.org +14 -0
- data/Gemfile +1 -0
- data/README.md +1291 -0
- data/README.org +937 -96
- data/Rakefile +3 -0
- data/fat_period.gemspec +1 -1
- data/lib/fat_period/date.rb +2 -6
- data/lib/fat_period/period.rb +223 -10
- data/lib/fat_period/version.rb +1 -1
- data/lib/fat_period.rb +21 -6
- metadata +6 -5
data/README.org
CHANGED
|
@@ -1,54 +1,28 @@
|
|
|
1
1
|
#+TITLE: FatPeriod User Guide
|
|
2
2
|
#+OPTIONS: toc:5
|
|
3
|
-
#+PROPERTY: header-args:ruby :colnames no :session fat_period :hlines yes :exports both :wrap example :ruby "bundle exec irb --prompt=simple"
|
|
4
|
-
#+PROPERTY: header-args:sh :exports code
|
|
5
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
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
#+BEGIN_EXPORT markdown
|
|
11
|
+
[](https://github.com/ddoherty03/fat_period/actions/workflows/ruby.yml)
|
|
12
|
+
#+END_EXPORT
|
|
8
13
|
|
|
9
|
-
* Setup for Code Blocks :noexport:
|
|
10
|
-
Run this block before all others to ensure that we are reading the libraries
|
|
11
|
-
from the source directory.
|
|
12
|
-
|
|
13
|
-
#+begin_src ruby :results output :export no
|
|
14
|
-
Dir.chdir("/home/ded/src/fat_period")
|
|
15
|
-
puts "Current directory: #{Dir.pwd}"
|
|
16
|
-
puts "Ruby LOADPATH:"
|
|
17
|
-
$LOAD_PATH.unshift(File.expand_path("lib", Dir.pwd)) unless $:[0].match?(%r{src/fat_period/lib})
|
|
18
|
-
$:[0..10].each { |d| puts d }
|
|
19
|
-
puts "..."
|
|
20
|
-
require 'fat_period'
|
|
21
|
-
#+end_src
|
|
22
|
-
|
|
23
|
-
#+RESULTS:
|
|
24
|
-
#+begin_example
|
|
25
|
-
Current directory: /home/ded/src/fat_period
|
|
26
|
-
Ruby LOADPATH:
|
|
27
|
-
/home/ded/src/fat_period/lib
|
|
28
|
-
/home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/bundler-2.6.7/lib
|
|
29
|
-
/home/ded/.rbenv/rbenv.d/exec/gem-rehash
|
|
30
|
-
/home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/bundler/gems/rubocop-ddoherty-6b28e9614f18/lib
|
|
31
|
-
/home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rubocop-shopify-2.17.1/lib
|
|
32
|
-
/home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rubocop-rspec-3.7.0/lib
|
|
33
|
-
/home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rubocop-rake-0.7.1/lib
|
|
34
|
-
/home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rubocop-performance-1.26.1/lib
|
|
35
|
-
/home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rubocop-1.81.6/lib
|
|
36
|
-
/home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/unicode-display_width-3.2.0/lib
|
|
37
|
-
/home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/unicode-emoji-4.1.0/lib
|
|
38
|
-
...
|
|
39
|
-
#+end_example
|
|
40
14
|
|
|
41
15
|
* Version
|
|
42
16
|
#+begin_src ruby :wrap EXAMPLE
|
|
43
17
|
"Current version is: #{FatPeriod::VERSION}"
|
|
44
18
|
#+end_src
|
|
45
19
|
|
|
20
|
+
#+RESULTS:
|
|
46
21
|
#+begin_EXAMPLE
|
|
47
|
-
Current version is: 3.
|
|
22
|
+
Current version is: 3.1.0
|
|
48
23
|
#+end_EXAMPLE
|
|
49
24
|
|
|
50
25
|
* Introduction
|
|
51
|
-
|
|
52
26
|
~FatPeriod~ provides a Ruby ~Period~ class for dealing with time periods of
|
|
53
27
|
days, that is ranges whose endpoints are of class ~Date~. It's target is
|
|
54
28
|
financial applications, but it serves well for any application where periods
|
|
@@ -62,43 +36,6 @@ breaking a larger periods into an array of smaller periods of various 'chunk'
|
|
|
62
36
|
sizes that correspond to calendar-related periods such as days, weeks, months,
|
|
63
37
|
and so forth.
|
|
64
38
|
|
|
65
|
-
* Table of Contents :toc:noexport:
|
|
66
|
-
- [[#version][Version]]
|
|
67
|
-
- [[#introduction][Introduction]]
|
|
68
|
-
- [[#installation][Installation]]
|
|
69
|
-
- [[#installing-the-gem][Installing the gem]]
|
|
70
|
-
- [[#usage][Usage]]
|
|
71
|
-
- [[#constant-periodforever][Constant Period::FOREVER]]
|
|
72
|
-
- [[#construction-of-periods][Construction of Periods]]
|
|
73
|
-
- [[#periodnew][Period.new]]
|
|
74
|
-
- [[#periodparse][Period.parse]]
|
|
75
|
-
- [[#with-only-a-from-spec][With Only a From-Spec]]
|
|
76
|
-
- [[#with-both-a-from-spec-and-to-spec][With Both a From-Spec and To-Spec]]
|
|
77
|
-
- [[#using-skip-modifiers][Using Skip Modifiers]]
|
|
78
|
-
- [[#periodparse_phrase][Period.parse_phrase]]
|
|
79
|
-
- [[#periodensure][Period.ensure]]
|
|
80
|
-
- [[#conversion][Conversion]]
|
|
81
|
-
- [[#to-range][To Range]]
|
|
82
|
-
- [[#to-string][To String]]
|
|
83
|
-
- [[#tex-form][TeX Form]]
|
|
84
|
-
- [[#comparison][Comparison]]
|
|
85
|
-
- [[#enumeration][Enumeration]]
|
|
86
|
-
- [[#size][Size]]
|
|
87
|
-
- [[#chunking][Chunking]]
|
|
88
|
-
- [[#set-operations][Set Operations]]
|
|
89
|
-
- [[#subset-determination][Subset Determination]]
|
|
90
|
-
- [[#superset-determination][Superset Determination]]
|
|
91
|
-
- [[#intersection][Intersection]]
|
|
92
|
-
- [[#difference][Difference]]
|
|
93
|
-
- [[#union][Union]]
|
|
94
|
-
- [[#coverage][Coverage]]
|
|
95
|
-
- [[#contains][Contains?]]
|
|
96
|
-
- [[#overlapping][Overlapping]]
|
|
97
|
-
- [[#spanning][Spanning]]
|
|
98
|
-
- [[#gaps][Gaps]]
|
|
99
|
-
- [[#development][Development]]
|
|
100
|
-
- [[#contributing][Contributing]]
|
|
101
|
-
|
|
102
39
|
* Installation
|
|
103
40
|
|
|
104
41
|
** Installing the gem
|
|
@@ -109,20 +46,29 @@ Add this line to your application's Gemfile:
|
|
|
109
46
|
gem 'fat_period'
|
|
110
47
|
#+END_SRC
|
|
111
48
|
|
|
49
|
+
#+RESULTS:
|
|
50
|
+
#+begin_example
|
|
51
|
+
true
|
|
52
|
+
#+end_example
|
|
53
|
+
|
|
112
54
|
And then execute:
|
|
113
55
|
|
|
114
56
|
#+BEGIN_SRC sh
|
|
115
57
|
$ bundle
|
|
116
58
|
#+END_SRC
|
|
117
59
|
|
|
60
|
+
#+RESULTS:
|
|
61
|
+
|
|
118
62
|
Or install it yourself as:
|
|
119
63
|
|
|
120
64
|
#+BEGIN_SRC sh
|
|
121
65
|
$ gem install fat_period
|
|
122
66
|
#+END_SRC
|
|
123
67
|
|
|
68
|
+
#+RESULTS:
|
|
69
|
+
|
|
124
70
|
* Usage
|
|
125
|
-
** Constant Period::FOREVER
|
|
71
|
+
** Constant ~Period::FOREVER~
|
|
126
72
|
|
|
127
73
|
The ~Period~ class depends on the extensions to ~Date~ made by the ~fat_core~
|
|
128
74
|
gem, which you can read about [[https://github.com/ddoherty03/fat_core][here]]. It defines a constant, ~Period::FOREVER~,
|
|
@@ -132,7 +78,7 @@ the beginning of time and end of time for practical commercial purposes. The
|
|
|
132
78
|
constant is not frozen, so you can re-define it to your liking.
|
|
133
79
|
|
|
134
80
|
** Construction of Periods
|
|
135
|
-
*** Period.new
|
|
81
|
+
*** ~Period.new~
|
|
136
82
|
A Period is constructed with two arguments for the begin and end date. The
|
|
137
83
|
begin date must be on or before the end date. Each argument can be (1) a
|
|
138
84
|
Date, (2) a string parseable as a Date by the Date.parse method, or (3) an
|
|
@@ -146,18 +92,22 @@ object that responds to ~#to_s~ and can be parsed as a Date by Date.parse:
|
|
|
146
92
|
#+end_SRC
|
|
147
93
|
|
|
148
94
|
#+RESULTS:
|
|
149
|
-
|
|
150
|
-
|
|
|
95
|
+
#+begin_example
|
|
96
|
+
| 2026-11-05 to 2026-12-05 |
|
|
97
|
+
| 1963-11-22 to 2026-11-05 |
|
|
151
98
|
| 1961-01-21 to 1963-11-22 |
|
|
99
|
+
#+end_example
|
|
152
100
|
|
|
153
101
|
#+begin_src ruby
|
|
154
102
|
["Camelot lasted #{p3.length} days"]
|
|
155
103
|
#+end_src
|
|
156
104
|
|
|
157
105
|
#+RESULTS:
|
|
106
|
+
#+begin_example
|
|
158
107
|
| Camelot lasted 1036 days |
|
|
108
|
+
#+end_example
|
|
159
109
|
|
|
160
|
-
*** Period.parse
|
|
110
|
+
*** ~Period.parse~
|
|
161
111
|
A more convenient way to construct a period is provided by ~Period.parse~. It
|
|
162
112
|
takes two strings as its arguments, a mandatory "from-spec" and an optional
|
|
163
113
|
"to-spec":
|
|
@@ -181,6 +131,18 @@ overall period:
|
|
|
181
131
|
tab
|
|
182
132
|
#+end_src
|
|
183
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
|
+
|
|
184
146
|
#+begin_example
|
|
185
147
|
| From Spec | Result |
|
|
186
148
|
|-------------+--------------------------------|
|
|
@@ -209,6 +171,17 @@ period is the last day of the to-spec:
|
|
|
209
171
|
tab
|
|
210
172
|
#+end_src
|
|
211
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
|
+
|
|
212
185
|
#+begin_example
|
|
213
186
|
| From Spec | To Spec | Result |
|
|
214
187
|
|-----------+-------------+--------------------------------|
|
|
@@ -242,6 +215,7 @@ skipping forward instead.
|
|
|
242
215
|
tab
|
|
243
216
|
#+end_src
|
|
244
217
|
|
|
218
|
+
#+RESULTS:
|
|
245
219
|
#+begin_example
|
|
246
220
|
| From Spec | To Spec | Result | Description |
|
|
247
221
|
|-----------+---------------+--------------------------------+------------------------------------------------------------|
|
|
@@ -251,7 +225,16 @@ skipping forward instead.
|
|
|
251
225
|
| 2012-11 | 2012-11-4Thur | Period(2012-11-01..2012-11-22) | And through Thanksgiving (not always the /last/ Thursday!) |
|
|
252
226
|
#+end_example
|
|
253
227
|
|
|
254
|
-
|
|
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~
|
|
255
238
|
For example:
|
|
256
239
|
|
|
257
240
|
The ~Period.parse_phrase~ method will take a string having a 'from', 'to', and
|
|
@@ -275,6 +258,7 @@ as a 'from' clause.
|
|
|
275
258
|
tab
|
|
276
259
|
#+end_src
|
|
277
260
|
|
|
261
|
+
#+RESULTS:
|
|
278
262
|
#+begin_example
|
|
279
263
|
| k | Sub Period |
|
|
280
264
|
|---+--------------------------------|
|
|
@@ -319,6 +303,7 @@ of the overall period.
|
|
|
319
303
|
tab
|
|
320
304
|
#+end_src
|
|
321
305
|
|
|
306
|
+
#+RESULTS:
|
|
322
307
|
#+begin_example
|
|
323
308
|
| k | Sub Period |
|
|
324
309
|
|----+--------------------------------|
|
|
@@ -364,8 +349,7 @@ of the overall period.
|
|
|
364
349
|
| 39 | Period(2025-09-29..2025-09-30) |
|
|
365
350
|
#+end_example
|
|
366
351
|
|
|
367
|
-
|
|
368
|
-
*** Period.ensure
|
|
352
|
+
*** ~Period.ensure~
|
|
369
353
|
~Period.ensure~ tries to interpret its argument as a ~Period~ and returns it;
|
|
370
354
|
otherwise it throws an ~ArgumentError~ exception:
|
|
371
355
|
|
|
@@ -392,14 +376,16 @@ otherwise it throws an ~ArgumentError~ exception:
|
|
|
392
376
|
Period.ensure(cm)
|
|
393
377
|
#+end_src
|
|
394
378
|
|
|
395
|
-
#+
|
|
396
|
-
|
|
379
|
+
#+RESULTS:
|
|
380
|
+
#+begin_example
|
|
381
|
+
Period(2025-09-01..2025-09-30)
|
|
397
382
|
#+end_example
|
|
398
383
|
|
|
399
384
|
#+begin_src ruby
|
|
400
385
|
Period.ensure('from 2016 to 2018-3Q')
|
|
401
386
|
#+end_src
|
|
402
387
|
|
|
388
|
+
#+RESULTS:
|
|
403
389
|
#+begin_example
|
|
404
390
|
Period(2016-01-01..2018-09-30)
|
|
405
391
|
#+end_example
|
|
@@ -408,29 +394,884 @@ Period(2016-01-01..2018-09-30)
|
|
|
408
394
|
Period.ensure(Period.new('2016-01-02', '2017-09-29'))
|
|
409
395
|
#+end_src
|
|
410
396
|
|
|
397
|
+
#+RESULTS:
|
|
411
398
|
#+begin_example
|
|
412
399
|
Period(2016-01-02..2017-09-29)
|
|
413
400
|
#+end_example
|
|
414
401
|
|
|
415
402
|
** Conversion
|
|
416
|
-
***
|
|
417
|
-
|
|
418
|
-
|
|
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
|
+
|
|
419
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
|
+
|
|
420
521
|
** Enumeration
|
|
421
|
-
|
|
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
|
+
|
|
422
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
|
+
|
|
423
818
|
** Set Operations
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
+
|
|
429
1048
|
** Coverage
|
|
430
|
-
***
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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.
|
|
1274
|
+
|
|
434
1275
|
|
|
435
1276
|
* Development
|
|
436
1277
|
|