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