fat_date 0.1.4 → 0.2.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,685 @@
1
+ - [Introduction](#orgfe24bdd)
2
+ - [Version](#org99a1f7c)
3
+ - [Installation](#org1ab3a2a)
4
+ - [Usage](#orgac97d21)
5
+ - [Constants](#org76fd298)
6
+ - [Ensure](#org7b28e11)
7
+ - [Formatting](#org2d07130)
8
+ - [Chunks](#orgf4df39e)
9
+ - [Parsing American Dates](#org9d15391)
10
+ - [Holidays and Workdays](#org277f99c)
11
+ - [Ordinal Weekdays in Month](#orgd67fd4e)
12
+ - [Easter](#orged12cbf)
13
+ - [Date Specs](#org8cd1a7f)
14
+ - [Contributing](#org0602bdf)
15
+
16
+ [![CI](https://github.com/ddoherty03/fat_date/actions/workflows/ruby.yml/badge.svg?branch=master)](https://github.com/ddoherty03/fat_date/actions/workflows/ruby.yml)
17
+
18
+
19
+ <a id="orgfe24bdd"></a>
20
+
21
+ # Introduction
22
+
23
+ `fat_date` collects core extensions for the Date class to make it more useful in financial applications, including:
24
+
25
+ - determining when a `Date` is a federal or NYSE holiday with Presidential decrees included,
26
+ - determining when a `Date` is part of a larger calendar-related "chunk," such as a year, half, quarter, bimonth, month, semimonth, or week,
27
+ - calculating Easter for a `Date's` year, a date on which some "movable feasts" depend, and
28
+ - parsing so-called "specs" that allow the beginning or ending `Date` of a larger period of time to be returned, a facility put to good use in the [FatPeriod](https://github.com/ddoherty03/fat_period) gem.
29
+
30
+
31
+ <a id="org99a1f7c"></a>
32
+
33
+ # Version
34
+
35
+ ```ruby
36
+ FatDate::VERSION
37
+ ```
38
+
39
+ ```
40
+ 0.1.5
41
+ ```
42
+
43
+
44
+ <a id="org1ab3a2a"></a>
45
+
46
+ # Installation
47
+
48
+ Add this line to your application's Gemfile:
49
+
50
+ ```ruby
51
+ gem 'fat_date', :git => 'https://github.com/ddoherty03/fat_date.git'
52
+ ```
53
+
54
+ And then execute:
55
+
56
+ ```sh
57
+ $ bundle
58
+ ```
59
+
60
+ Or install it yourself as:
61
+
62
+ ```sh
63
+ $ gem install fat_date
64
+ ```
65
+
66
+
67
+ <a id="orgac97d21"></a>
68
+
69
+ # Usage
70
+
71
+ Many of these have little that is of general interest, but there are a few goodies.
72
+
73
+
74
+ <a id="org76fd298"></a>
75
+
76
+ ### Constants
77
+
78
+ `FatDate` adds two date constants to the `Date` class, Date::BOT and Date::EOT. These represent the earliest and latest dates of practical commercial interest. The exact values are rather arbitrary, but they prove useful in date ranges, for example. They are defined as:
79
+
80
+ - **`Date::BOT`:** January 1, 1900
81
+ - **`Date::EOT`:** December 31, 3000
82
+ - **`Date::FEDERAL_DECREED_HOLIDAYS`:** an Array of dates declared as non-work days for federal employees by presidential proclamation
83
+ - **`Date::PRESIDENTIAL_FUNERALS`:** an Array of dates of presidential funerals, which are observed with a closing of most federal agencies
84
+
85
+
86
+ <a id="org7b28e11"></a>
87
+
88
+ ### Ensure
89
+
90
+ The `Date.ensure` class method tries to convert its argument to a `Date` object by (1) applying the `#to_date` method or (2) applying the `Date.parse` method to a String. This is handy when you want to define a method that takes a date argument but want the caller to be able to supply anything that can reasonably be converted to a `Date`:
91
+
92
+ ```ruby
93
+ def tomorow_tomorrow(arg)
94
+ from = Date.ensure(arg) # => ArgumentError: cannot convert class 'Array' to a Date or DateTime
95
+ from + 2.days # => Mon, 03 Jun 2024, Wed, 16 Oct 2024 05:47:30 -0500, Sun, 03 Mar 2024
96
+ end # => :tomorow_tomorrow
97
+
98
+ tomorow_tomorrow('June 1').to_s
99
+ ```
100
+
101
+ ```
102
+ 2025-06-03
103
+ ```
104
+
105
+ If you give it a Time, it will return a `DateTime`
106
+
107
+ ```ruby
108
+ [Time.now, tomorow_tomorrow(Time.now)]
109
+ ```
110
+
111
+ ```
112
+ [2025-12-24 08:06:12.733499363 -0600, Fri, 26 Dec 2025 08:06:12 -0600]
113
+ ```
114
+
115
+ But it's only as good as Date.parse! If all it sees is 'March', it returns March 1 of the current year.
116
+
117
+ ```ruby
118
+ tomorow_tomorrow('Ides of March').to_s
119
+ ```
120
+
121
+ ```
122
+ 2025-03-03
123
+ ```
124
+
125
+
126
+ <a id="org2d07130"></a>
127
+
128
+ ### Formatting
129
+
130
+ `FatDate` provides some concise methods for printing string versions of dates that are often useful:
131
+
132
+ ```ruby
133
+ d = Date.parse('1957-09-22')
134
+ methods = ['iso', 'num', 'tex_quote', 'eng', 'american', 'org']
135
+ tab = []
136
+ tab << ['Description', 'Result']
137
+ tab << nil
138
+ methods.each do |m|
139
+ tab << [m, d.send(m.to_sym)]
140
+ end
141
+ tab << ["org(active: true)", d.org(active: true)]
142
+ ```
143
+
144
+ ```
145
+ | Description | Result |
146
+ |-------------------+--------------------|
147
+ | iso | 1957-09-22 |
148
+ | num | 19570922 |
149
+ | tex_quote | 1957--09--22 |
150
+ | eng | September 22, 1957 |
151
+ | american | 9/22/1957 |
152
+ | org | [1957-09-22 Sun] |
153
+ | org(active: true) | <1957-09-22 Sun> |
154
+ ```
155
+
156
+ Most of these are self-explanatory, but a couple are not. The `Date.org(active: false)` method formats a date as an Emacs org-mode timestamp, by default an inactive timestamp that does not show up in the org agenda, but can be made active with the optional parameter `active:` set to a truthy value. See <https://orgmode.org/manual/Timestamps.html#Timestamps>.
157
+
158
+ The `#tex_quote` method formats the date in iso form but using TeX's convention of using en-dashes to separate the components.
159
+
160
+
161
+ <a id="orgf4df39e"></a>
162
+
163
+ ### Chunks
164
+
165
+ Many of the methods provided by `FatDate` deal with various calendar periods that are less common than those provided by the Ruby Standard Library or gems such as `active_support`. This documentation refers to these calendar periods as "chunks", and they are the following:
166
+
167
+ - year,
168
+ - half,
169
+ - quarter,
170
+ - bimonth,
171
+ - month,
172
+ - semimonth,
173
+ - biweek,
174
+ - week, and
175
+ - day
176
+
177
+ `FatDate` provides methods that query whether the date falls on the beginning or end of each of these chunks:
178
+
179
+ ```ruby
180
+ tab = []
181
+ tab << ['Subject Date', 'Method', 'Result']
182
+ tab << nil
183
+ d = Date.parse('2017-06-30')
184
+ %i[beginning end].each do |side|
185
+ %i(year half quarter bimonth month semimonth biweek week).each do |chunk|
186
+ meth = "#{side}_of_#{chunk}?".to_sym
187
+ tab << [d.iso, meth.to_s, "#{d.send(meth)}"]
188
+ end
189
+ end
190
+ tab
191
+ ```
192
+
193
+ ```
194
+ | Subject Date | Method | Result |
195
+ |--------------+-------------------------+--------|
196
+ | 2017-06-30 | beginning_of_year? | false |
197
+ | 2017-06-30 | beginning_of_half? | false |
198
+ | 2017-06-30 | beginning_of_quarter? | false |
199
+ | 2017-06-30 | beginning_of_bimonth? | false |
200
+ | 2017-06-30 | beginning_of_month? | false |
201
+ | 2017-06-30 | beginning_of_semimonth? | false |
202
+ | 2017-06-30 | beginning_of_biweek? | false |
203
+ | 2017-06-30 | beginning_of_week? | false |
204
+ | 2017-06-30 | end_of_year? | false |
205
+ | 2017-06-30 | end_of_half? | true |
206
+ | 2017-06-30 | end_of_quarter? | true |
207
+ | 2017-06-30 | end_of_bimonth? | true |
208
+ | 2017-06-30 | end_of_month? | true |
209
+ | 2017-06-30 | end_of_semimonth? | true |
210
+ | 2017-06-30 | end_of_biweek? | false |
211
+ | 2017-06-30 | end_of_week? | false |
212
+ ```
213
+
214
+ It also provides corresponding methods that return the date at the beginning or end of the calendar chunk, starting at the given date:
215
+
216
+ ```ruby
217
+ tab = []
218
+ tab << ['Subject Date', 'Method', 'Result']
219
+ tab << nil
220
+ d = Date.parse('2017-04-21')
221
+ %i[beginning end].each do |side|
222
+ %i(year half quarter bimonth month semimonth biweek week ).each do |chunk|
223
+ meth = "#{side}_of_#{chunk}".to_sym
224
+ tab << [d.iso, "d.#{meth}", "#{d.send(meth)}"]
225
+ end
226
+ end
227
+ tab
228
+ ```
229
+
230
+ ```
231
+ | Subject Date | Method | Result |
232
+ |--------------+--------------------------+------------|
233
+ | 2017-04-21 | d.beginning_of_year | 2017-01-01 |
234
+ | 2017-04-21 | d.beginning_of_half | 2017-01-01 |
235
+ | 2017-04-21 | d.beginning_of_quarter | 2017-04-01 |
236
+ | 2017-04-21 | d.beginning_of_bimonth | 2017-03-01 |
237
+ | 2017-04-21 | d.beginning_of_month | 2017-04-01 |
238
+ | 2017-04-21 | d.beginning_of_semimonth | 2017-04-16 |
239
+ | 2017-04-21 | d.beginning_of_biweek | 2017-04-10 |
240
+ | 2017-04-21 | d.beginning_of_week | 2017-04-17 |
241
+ | 2017-04-21 | d.end_of_year | 2017-12-31 |
242
+ | 2017-04-21 | d.end_of_half | 2017-06-30 |
243
+ | 2017-04-21 | d.end_of_quarter | 2017-06-30 |
244
+ | 2017-04-21 | d.end_of_bimonth | 2017-04-30 |
245
+ | 2017-04-21 | d.end_of_month | 2017-04-30 |
246
+ | 2017-04-21 | d.end_of_semimonth | 2017-04-30 |
247
+ | 2017-04-21 | d.end_of_biweek | 2017-04-23 |
248
+ | 2017-04-21 | d.end_of_week | 2017-04-23 |
249
+ ```
250
+
251
+ You can query which numerical half, quarter, etc. that a given date falls in:
252
+
253
+ ```ruby
254
+ tab = []
255
+ tab << ['Subject Date', 'Method', 'Result']
256
+ tab << nil
257
+ %i(year half quarter bimonth month semimonth biweek week ).each do |chunk|
258
+ d = Date.parse('2017-04-21') + rand(100)
259
+ meth = "#{chunk}".to_sym
260
+ tab << [d.iso, "d.#{meth}", "in #{chunk} number #{d.send(meth)}"]
261
+ end
262
+ tab
263
+ ```
264
+
265
+ ```
266
+ | Subject Date | Method | Result |
267
+ |--------------+-------------+------------------------|
268
+ | 2017-05-14 | d.year | in year number 2017 |
269
+ | 2017-07-23 | d.half | in half number 2 |
270
+ | 2017-06-28 | d.quarter | in quarter number 2 |
271
+ | 2017-06-12 | d.bimonth | in bimonth number 3 |
272
+ | 2017-05-24 | d.month | in month number 5 |
273
+ | 2017-06-16 | d.semimonth | in semimonth number 12 |
274
+ | 2017-06-05 | d.biweek | in biweek number 12 |
275
+ | 2017-07-16 | d.week | in week number 28 |
276
+ ```
277
+
278
+
279
+ <a id="org9d15391"></a>
280
+
281
+ ### Parsing American Dates
282
+
283
+ Americans often write dates in the form M/d/Y, and the normal parse method will parse such a string as d/M/Y, often resulting in invalid date errors. `FatDate` adds the specialty parsing method, `Date.parse_american` to handle such strings.
284
+
285
+ ```ruby
286
+ begin
287
+ ss = '9/22/1957'
288
+ Date.parse(ss)
289
+ rescue Date::Error => ex
290
+ puts "Date.parse('#{ss}') raises #{ex.class} (#{ex}), but"
291
+ puts "Date.parse_american('#{ss}') => #{Date.parse_american(ss)}"
292
+ end
293
+ ```
294
+
295
+ ```
296
+ => false
297
+ Date.parse('9/22/1957') raises Date::Error (invalid date), but
298
+ Date.parse_american('9/22/1957') => 1957-09-22
299
+ => nil
300
+ :org_babel_ruby_eoe
301
+ ```
302
+
303
+
304
+ <a id="org277f99c"></a>
305
+
306
+ ### Holidays and Workdays
307
+
308
+ 1. Federal
309
+
310
+ One of the original motivations for this library was to provide an easy way to determine whether a given date is a federal holiday in the United States or, nearly but not quite the same, a non-trading day on the New York Stock Exchange. To that end, `FatDate` provides the following methods:
311
+
312
+ - Date#weekend? &#x2013; is this date on a weekend?
313
+ - Date#weekday? &#x2013; is this date on a week day?
314
+ - Date#easter\_this\_year &#x2013; the date of Easter in the Date's year
315
+
316
+ Methods concerning Federal holidays:
317
+
318
+ - Date#fed\_holiday? &#x2013; is this date a Federal holiday? It knows about obscurities such as holidays decreed by past Presidents, dates of Presidential funerals, and the Federal rule for when holidays fall on a weekend, whether it is moved to the prior Friday or the following Monday.
319
+ - Date#fed\_workday? &#x2013; is it a date when Federal government offices are open?, inverse of Date#fed\_holiday?
320
+ - Date#add\_fed\_workdays(n) &#x2013; n Federal workdays following (or preceding if n negative) this date,
321
+ - Date#next\_fed\_workday &#x2013; the next Federal workday following this date,
322
+ - Date#prior\_fed\_workday &#x2013; the previous Federal workday before this date,
323
+ - Date#next\_until\_fed\_workday &#x2013; starting with this date, move forward until we hit a Federal workday
324
+ - Date#prior\_until\_fed\_workday &#x2013; starting with this date, move back until we hit a Federal workday
325
+
326
+ Whether a particular date is a federal holiday is complicated. Certain holidays are statutory as set forth in [5 U.S.C. §6103](https://www.govinfo.gov/content/pkg/USCODE-2024-title5/pdf/USCODE-2024-title5-partIII-subpartE-chap61-subchapI-sec6103.pdf). But if the holiday falls on a Saturday, the prior Friday is observed; if on a Sunday, the following Monday is observed. Inauguration Day after 1965 is observed by employees in Washington, D.C., and surrounding areas, effectively shutting down most federal agencies.
327
+
328
+ On top of that the days of Presidential funeral are federal holidays. On top of that, each President can decree temporary holidays by Executive Order, often to give employees Christmas Eve and the day after Christmas the day off if they would not otherwise be off. The `fat_date` library attempts to capture all of this, but the days of Presidential decrees are only good for the last decade or so.
329
+
330
+ Here is a sampling:
331
+
332
+ ```ruby
333
+ result = []
334
+ result << ['Date', 'Federal Holiday?', 'Comment']
335
+ result << nil
336
+ result << ['2014-05-16', Date.parse('2014-05-16').fed_holiday?, 'Nuttin special']
337
+ result << ['2014-05-18', Date.parse('2014-05-18').fed_holiday?, 'A weekend']
338
+ result << ['2014-01-01', Date.parse('2014-01-01').fed_holiday?, 'New Year']
339
+ result << ['1963-11-25', Date.parse('1963-11-25').fed_holiday?, 'JFK Funeral']
340
+ result << ['1973-01-25', Date.parse('1973-01-25').fed_holiday?, 'LBJ Funeral']
341
+ result << ['2003-12-25', Date.parse('2003-12-25').fed_holiday?, 'Christmas']
342
+ result << ['1961-01-20', Date.parse('1961-01-20').fed_holiday?, 'JFK Inauguration (before 1965)']
343
+ result << ['1969-01-20', Date.parse('1969-01-20').fed_holiday?, 'RMN Inauguration (after 1965)']
344
+ result << ['2012-12-24', Date.parse('2012-12-24').fed_holiday?, 'Christmas Eve Decreed by Obama']
345
+ result << ['2003-12-26', Date.parse('2003-12-26').fed_holiday?, 'Friday after Christmas']
346
+ ```
347
+
348
+ ```
349
+ | Date | Federal Holiday? | Comment |
350
+ |------------+------------------+--------------------------------|
351
+ | 2014-05-16 | false | Nuttin special |
352
+ | 2014-05-18 | true | A weekend |
353
+ | 2014-01-01 | true | New Year |
354
+ | 1963-11-25 | true | JFK Funeral |
355
+ | 1973-01-25 | true | LBJ Funeral |
356
+ | 2003-12-25 | true | Christmas |
357
+ | 1961-01-20 | false | JFK Inauguration (before 1965) |
358
+ | 1969-01-20 | true | RMN Inauguration (after 1965) |
359
+ | 2012-12-24 | true | Christmas Eve Decreed by Obama |
360
+ | 2003-12-26 | true | Friday after Christmas |
361
+ ```
362
+
363
+ 2. NYSE
364
+
365
+ And we have similar methods for "holidays" or non-trading days on the NYSE:
366
+
367
+ - Date#nyse\_holiday? &#x2013; is this date a NYSE holiday?
368
+ - Date#nyse\_workday? &#x2013; is it a date when the NYSE is open for trading?, inverse of Date#nyse\_holiday?
369
+ - Date#add\_nyse\_workdays(n) &#x2013; n NYSE workdays following (or preceding if n negative) this date,
370
+ - Date#next\_nyse\_workday &#x2013; the next NYSE workday following this date,
371
+ - Date#prior\_nyse\_workday &#x2013; the previous NYSE workday before this date,
372
+ - Date#next\_until\_nyse\_~~workday &#x2013; starting with this date, move forward until we hit a NYSE workday
373
+ - Date#prior\_until\_nyse\_workday &#x2013; starting with this date, move back until we hit a Federal workday
374
+
375
+ Likewise, days on which the NYSE is closed can be gotten with:
376
+
377
+ ```ruby
378
+ Date.parse('2014-04-18').nyse_holiday?
379
+ ```
380
+
381
+ ```
382
+ true
383
+ ```
384
+
385
+ ```ruby
386
+ date_comments = [
387
+ ['2014-04-18', 'Good Friday'],
388
+ ['2014-05-18', 'Weekend'],
389
+ ['2014-05-21', 'Any old day'],
390
+ ['2014-01-01', 'New Year']
391
+ ]
392
+ result = []
393
+ result << ['Date', 'Federal Holiday?', 'NYSE Holiday?', 'Comment']
394
+ result << nil
395
+ date_comments.each do |str, comment|
396
+ d = Date.parse(str)
397
+ result << [d.org, d.fed_holiday?, d.nyse_holiday?, comment]
398
+ end
399
+ result
400
+ ```
401
+
402
+ ```
403
+ | Date | Federal Holiday? | NYSE Holiday? | Comment |
404
+ |------------------+------------------+---------------+-------------|
405
+ | [2014-04-18 Fri] | false | true | Good Friday |
406
+ | [2014-05-18 Sun] | true | true | Weekend |
407
+ | [2014-05-21 Wed] | false | false | Any old day |
408
+ | [2014-01-01 Wed] | true | true | New Year |
409
+ ```
410
+
411
+
412
+ <a id="orgd67fd4e"></a>
413
+
414
+ ### Ordinal Weekdays in Month
415
+
416
+ It is often useful to find the 1st, 2nd, etc, Sunday, Monday, etc. in a given month. `FatDate` provides the class method `Date.nth_wday_in_year_month(nth, wday, year, month)` to return such dates. The first parameter can be negative, which will count from the end of the month.
417
+
418
+ ```ruby
419
+ results = []
420
+ results << ['n', 'Year', 'Month', 'nth Thursday']
421
+ results << nil
422
+ (1..4).each do |n|
423
+ d = Date.nth_wday_in_year_month(n, 4, 2024, 6)
424
+ results << [n, d.year, 'June', d.org]
425
+ end
426
+ (-4..-1).to_a.reverse.each do |n|
427
+ d = Date.nth_wday_in_year_month(n, 4, 2024, 6)
428
+ results << [n, d.year, 'June', d.org]
429
+ end
430
+ results
431
+ ```
432
+
433
+ ```
434
+ | n | Year | Month | nth Thursday |
435
+ |----+------+-------+------------------|
436
+ | 1 | 2024 | June | [2024-06-06 Thu] |
437
+ | 2 | 2024 | June | [2024-06-13 Thu] |
438
+ | 3 | 2024 | June | [2024-06-20 Thu] |
439
+ | 4 | 2024 | June | [2024-06-27 Thu] |
440
+ | -1 | 2024 | June | [2024-06-27 Thu] |
441
+ | -2 | 2024 | June | [2024-06-20 Thu] |
442
+ | -3 | 2024 | June | [2024-06-13 Thu] |
443
+ | -4 | 2024 | June | [2024-06-06 Thu] |
444
+ ```
445
+
446
+
447
+ <a id="orged12cbf"></a>
448
+
449
+ ### Easter
450
+
451
+ Many holidays in the West are determined by the date of Easter, so FatDate provides the class method `Date.easter(year)` to return the date of Easter for the given year, using the Julian calendar date before the year of reform, and using the Gregorian calendar beginning in the year of reform. By default, it uses 1582 for the date of reform, but it can take a named parameter, `reform_year:` to specify a different date. For England, the year of reform was September, 1752. So, to get a historically accurate date of Easter for Anglicans between 1582 and 1752, you should use a reform\_year of 1753, since the reform happened after Easter in 1752.
452
+
453
+ - **`Date.easter(year, reform_year: 1582)`:** return the date of Easter for the given `year`, assuming the given year of calendar reform; return nil for any year before 30AD.
454
+ - **Date#easter\_this\_year:** return the date of Easter for the year in which the subject Date falls.
455
+ - **Date#easter?:** return whether the subject Date is Easter.
456
+
457
+ ```ruby
458
+ yrs = [800, 1000, 1200, 1400, 1500, 1600, 1800, 2000]
459
+ result = []
460
+ result << ['Year', 'Easter Date']
461
+ result << nil
462
+ yrs.each do |y|
463
+ result << [y, Date.easter(y).org ]
464
+ end
465
+ result
466
+ ```
467
+
468
+ ```
469
+ | Year | Easter Date |
470
+ |------+------------------|
471
+ | 800 | [0800-04-19 Wed] |
472
+ | 1000 | [1000-03-31 Mon] |
473
+ | 1200 | [1200-04-09 Sun] |
474
+ | 1400 | [1400-04-18 Fri] |
475
+ | 1500 | [1500-04-19 Thu] |
476
+ | 1600 | [1600-04-02 Sun] |
477
+ | 1800 | [1800-04-13 Sun] |
478
+ | 2000 | [2000-04-23 Sun] |
479
+ ```
480
+
481
+
482
+ <a id="org8cd1a7f"></a>
483
+
484
+ ### Date Specs
485
+
486
+ It is often desirable to get the first or last date of a specified time period. For this `FatDate` provides the `spec` method that takes a string and an optional `spec_type` parameter of either `:from`, indicating that the first date of the period should be returned or `:to`, indicating that the last date of the period should be returned. It assumes the `spec_type` to be `:from` by default.
487
+
488
+ Though many specs, other than those specifying a single day, represent a period of time longer than one date, the `Date.spec` method returns a single date, either the first or last day of the period described by the spec. See the library `FatPeriod` where the `Date.spec` method is put to good use in defining a `Period` type to represent ranges of time.
489
+
490
+ The `spec` method supports a rich set of ways to specify periods of time. The following sections catalog them all.
491
+
492
+ 1. Given Day
493
+
494
+ - **YYYY-MM-DD:** returns a single day given.
495
+ - **MM-DD:** returns the specified day of the specified month in the current year.
496
+
497
+ 2. Day-of-Year
498
+
499
+ - **YYYY-ddd:** returns the ddd'th day of the specified year. Note that exactly three digits are needed: with only two digits it would be interpreted as a month.
500
+ - **ddd:** returns the ddd'th day of the current year. Again, note that exactly three digits are needed: two digits would be interpreted as a month, and four digits as a year.
501
+
502
+ 3. Month
503
+
504
+ The following return the first or last day of the given month.
505
+
506
+ - **YYYY-MM:** returns the first or last day of the specified month in the specified year.
507
+ - **MM:** returns first or last day of the specified month of the current year.
508
+
509
+ 4. Year
510
+
511
+ - **YYYY:** returns the first or last day of the specified year.
512
+
513
+ 5. Commercial Weeks-of-Year
514
+
515
+ - **YYYY-Wnn or YYYY-nnW:** returns the first or last day of the nn'th commercial week of the given year according to the ISO 8601 standard, in which the week containing the first Thursday of the year counts as the first commercial week, even if that week started in the prior calendar year,
516
+ - **Wnn or nnW:** returns the first or last day of the nn'th commercial week of the current year,
517
+
518
+ 6. Halves
519
+
520
+ - **YYYY-1H or YYYY-2H:** returns the first or last day of the specified half year for the given year,
521
+ - **1H or 2H:** returns the first or last day of the specified half year for the current year,
522
+
523
+ 7. Quarters
524
+
525
+ - **YYYY-1Q, YYYY-2Q, etc :** returns the first or last day of the calendar quarter for the given year,
526
+ - **1Q, 2Q, etc :** returns the first or last day of the calendar quarter for the current year,
527
+
528
+ 8. Semi-Months
529
+
530
+ - **YYYY-MM-A or YYYY-MM-B:** returns the first or last day of the semi-month for the given month and year, where the first semi-month always runs from the 1st to the 15th and the second semi-month always runs from the 16th to the last day of the given month, regardless of the number of days in the month.
531
+ - **MM-A or MM-B:** returns the first or last day of the semi-month of the current year.
532
+ - **A or B:** returns the first or last day of the semi-month of the current year and month.
533
+
534
+ 9. Week-of-Month
535
+
536
+ - **YYYY-MM-i or YYYY-MM-ii up to YYYY-MM-vi:** returns the first or last day of the given week within the month, including any partial weeks,
537
+ - **MM-i or MM-ii up to MM-vi:** returns the first or last day of the given week within the month of the current year, including any partial weeks,
538
+ - **i or ii up to vi:** returns the first or last day of the given week within the current month of the current year, including any partial weeks,
539
+
540
+ 10. Day-of-Week
541
+
542
+ - **YYYY-MM-nSu up to YYYY-MM-nSa :** returns the single day that is the n'th Sunday, Monday, etc., in the given month using the first two letters of the English names for the days of the week,
543
+ - **MM-nSu up to MM-nSa or MM-nSun up to MM-nSat:** returns the single date that is the n'th Sunday, Monday, etc., in the given month of the current year using the first two letters of the English names for the days of the week,
544
+ - **nSu up to nSa or nSun up to nSat:** returns the single date that is the n'th Sunday, Monday, etc., in the current month of the current year using the first two letters of the English names for the days of the week,
545
+
546
+ 11. Easter Based
547
+
548
+ - **YYYY-E:** returns the single date of Easter in the Western church for the given year,
549
+ - **E:** returns the single date of Easter in the Western church for the current year,
550
+ - **YYYY-E-n or YYYY-E+n:** returns the single date that falls n days before (-) or after (+) Easter in the Western church for the given year,
551
+ - **E-n or E+n:** returns the single date that falls n days before (-) or after (+) Easter in the Western church for the current year,
552
+
553
+ 12. Relative Dates
554
+
555
+ - **yesterday or yesteryear or lastday or last\_year, etc:** the relative prefixes, 'last' or 'yester' prepended to any chunk name returns the period named by the chunk that precedes today's date.
556
+ - **today or toyear or this-year or thissemimonth, etc:** the relative prefixes, 'to' or 'this' prepended to any chunk name returns the period named by the chunk that contains today's date.
557
+ - **nextday or nextyear or next-year or nextsemimonth, etc:** the relative prefixes, 'next' prepended to any chunk name returns the period named by the chunk that follows today's date. As a special case, 'tomorrow' is treated as equivalent to 'nextday'.
558
+
559
+ 13. Extremes
560
+
561
+ - **forever:** returns Date::BOT for :from, and Date::EOT for :to, which, for financial applications is meant to stand in for eternity.
562
+ - **never:** returns nil, representing no date.
563
+
564
+ 14. Skip Modifiers
565
+
566
+ Appended to any of the above specs (other than 'never'), you may add a 'skip modifier' to change the date to the first day-of-week adjacent to the date that the spec resolves to. This is done by appending one of the following to the spec:
567
+
568
+ - **'<Su', '<Mo', &#x2026; '<Sa':** skip to the first Sunday, Monday, etc., *before* the date the spec resolves to.
569
+ - **'<=Su', '<=Mo', &#x2026; '<=Sa':** skip to the first Sunday, Monday, etc., *on or before* the date the spec resolves to.
570
+ - **'>Su', '>Mo', &#x2026; '>Sa':** skip to the first Sunday, Monday, etc., *after* the date the spec resolves to.
571
+ - **'>=Su', '>=Mo', &#x2026; '>=Sa':** skip to the first Sunday, Monday, etc., *on or after* the date the spec resolves to.
572
+
573
+ For example, `Date.spec('2024<=Tu', :to)` resolves to the last Tuesday of 2024, which happens to be December 31, 2024; `Date.spec('2024<Tu', :to)`, on the other hand would resolve to December 24, 2024, since it looks for the first Tuesday strictly *before* December 31, 2024.
574
+
575
+ 15. Conventions
576
+
577
+ Some things to note with respect to `Date.spec`:
578
+
579
+ 1. The second argument can be either `:from` or `:to`, but it defaults to `:from`. If it is `:from`, `spec` returns the first date of the specified period; if it is `:to`, it returns the last date of the specified period. When the "period" resolves to a single day, both arguments return the same date, so `spec('2024-E', :from)` and `spec('2024-E', :to)` both result in March 31, 2024.
580
+ 2. Where relevant, `spec` accepts letters of either upper or lower case: so 2024-1Q can be written 2024-1q and 'yesteryear' can be written 'YeSterYeaR', and likewise for all components of the spec using letters.
581
+ 3. Date components can be separated with either a hyphen, as in the examples above, or with a '/' as is common. Thus, 2024-11-09 can also be 2024/11/09, or indeed, 2024/11-09 or 2024-11/09.
582
+ 4. The prefixes for relative periods can be separated from the period name by a hyphen, and underscore, or by nothing at all. Thus, yester-day, yester\_day, and yesterday are all acceptable. Neologisms such as 'yestermonth' are quaint, but not harmful.
583
+ 5. Where the names of days of the week are appropriate, any word that starts with 'su' counts as Sunday, regardless of case, any word that starts with 'mo' counts as Monday, and so on.
584
+ 6. 'fortnight' is a synonym for a biweek.
585
+
586
+ 16. Examples
587
+
588
+ The following examples demonstrate all of the date specs available.
589
+
590
+ ```ruby
591
+ strs = ['today', '2024-07-04', '2024-05', '2024', '2024-333',
592
+ '08', '08-12', '2024-W36', '2024-36W', 'W36', '36W',
593
+ '2024-1H', '2024-2H', '1H', '2H',
594
+ '1957-1Q', '1957-2Q', '1957-3Q', '1957-4Q',
595
+ '1Q', '2Q', '3Q', '4Q',
596
+ '2015-06-A', '2015-06-B', '06-A', '06-B', 'A', 'B',
597
+ '2021-09-I', '2021-09-II',
598
+ '2021-09-i', '2021-09-ii', '2021-09-iii', '2021-09-iv', '2021-09-v',
599
+ '10-i', '10-iii',
600
+ '2016-04-3Tu', '2016-11-4Th', '2016-11-2Th',
601
+ '05-3We', '06-3Wed', '3Su', '4Sa',
602
+ '1830-E', 'E', '2012-E+10', '2024-E+40', '2026-E<Fri',
603
+ 'yestermonth', 'lastmonth', 'yesterfortnight', 'thisfortnight', 'nextfortnight',
604
+ '2025-E+50>=Su'
605
+ ]
606
+ tab = []
607
+ tab << ['Spec', 'From', 'To']
608
+ tab << nil
609
+ strs.each do |s|
610
+ tab << ["'#{s}'", Date.spec(s, :from).org, Date.spec(s, :to).org]
611
+ end
612
+ tab
613
+ ```
614
+
615
+ ```
616
+ | Spec | From | To |
617
+ |-------------------+------------------+------------------|
618
+ | 'today' | [2025-12-24 Wed] | [2025-12-24 Wed] |
619
+ | '2024-07-04' | [2024-07-04 Thu] | [2024-07-04 Thu] |
620
+ | '2024-05' | [2024-05-01 Wed] | [2024-05-31 Fri] |
621
+ | '2024' | [2024-01-01 Mon] | [2024-12-31 Tue] |
622
+ | '2024-333' | [2024-11-28 Thu] | [2024-11-28 Thu] |
623
+ | '08' | [2025-08-01 Fri] | [2025-08-31 Sun] |
624
+ | '08-12' | [2025-08-12 Tue] | [2025-08-12 Tue] |
625
+ | '2024-W36' | [2024-09-02 Mon] | [2024-09-08 Sun] |
626
+ | '2024-36W' | [2024-09-02 Mon] | [2024-09-08 Sun] |
627
+ | 'W36' | [2025-09-01 Mon] | [2025-09-07 Sun] |
628
+ | '36W' | [2025-09-01 Mon] | [2025-09-07 Sun] |
629
+ | '2024-1H' | [2024-01-01 Mon] | [2024-06-30 Sun] |
630
+ | '2024-2H' | [2024-07-01 Mon] | [2024-12-31 Tue] |
631
+ | '1H' | [2025-01-01 Wed] | [2025-06-30 Mon] |
632
+ | '2H' | [2025-07-01 Tue] | [2025-12-31 Wed] |
633
+ | '1957-1Q' | [1957-01-01 Tue] | [1957-03-31 Sun] |
634
+ | '1957-2Q' | [1957-04-01 Mon] | [1957-06-30 Sun] |
635
+ | '1957-3Q' | [1957-07-01 Mon] | [1957-09-30 Mon] |
636
+ | '1957-4Q' | [1957-10-01 Tue] | [1957-12-31 Tue] |
637
+ | '1Q' | [2025-01-01 Wed] | [2025-03-31 Mon] |
638
+ | '2Q' | [2025-04-01 Tue] | [2025-06-30 Mon] |
639
+ | '3Q' | [2025-07-01 Tue] | [2025-09-30 Tue] |
640
+ | '4Q' | [2025-10-01 Wed] | [2025-12-31 Wed] |
641
+ | '2015-06-A' | [2015-06-01 Mon] | [2015-06-15 Mon] |
642
+ | '2015-06-B' | [2015-06-16 Tue] | [2015-06-30 Tue] |
643
+ | '06-A' | [2025-06-01 Sun] | [2025-06-15 Sun] |
644
+ | '06-B' | [2025-06-16 Mon] | [2025-06-30 Mon] |
645
+ | 'A' | [2025-12-01 Mon] | [2025-12-15 Mon] |
646
+ | 'B' | [2025-12-16 Tue] | [2025-12-31 Wed] |
647
+ | '2021-09-I' | [2021-09-01 Wed] | [2021-09-05 Sun] |
648
+ | '2021-09-II' | [2021-09-06 Mon] | [2021-09-12 Sun] |
649
+ | '2021-09-i' | [2021-09-01 Wed] | [2021-09-05 Sun] |
650
+ | '2021-09-ii' | [2021-09-06 Mon] | [2021-09-12 Sun] |
651
+ | '2021-09-iii' | [2021-09-13 Mon] | [2021-09-19 Sun] |
652
+ | '2021-09-iv' | [2021-09-20 Mon] | [2021-09-26 Sun] |
653
+ | '2021-09-v' | [2021-09-27 Mon] | [2021-09-30 Thu] |
654
+ | '10-i' | [2025-10-01 Wed] | [2025-10-05 Sun] |
655
+ | '10-iii' | [2025-10-13 Mon] | [2025-10-19 Sun] |
656
+ | '2016-04-3Tu' | [2016-04-19 Tue] | [2016-04-19 Tue] |
657
+ | '2016-11-4Th' | [2016-11-24 Thu] | [2016-11-24 Thu] |
658
+ | '2016-11-2Th' | [2016-11-10 Thu] | [2016-11-10 Thu] |
659
+ | '05-3We' | [2025-05-21 Wed] | [2025-05-21 Wed] |
660
+ | '06-3Wed' | [2025-06-18 Wed] | [2025-06-18 Wed] |
661
+ | '3Su' | [2025-12-21 Sun] | [2025-12-21 Sun] |
662
+ | '4Sa' | [2025-12-27 Sat] | [2025-12-27 Sat] |
663
+ | '1830-E' | [1830-04-11 Sun] | [1830-04-11 Sun] |
664
+ | 'E' | [2025-04-20 Sun] | [2025-04-20 Sun] |
665
+ | '2012-E+10' | [2012-04-18 Wed] | [2012-04-18 Wed] |
666
+ | '2024-E+40' | [2024-05-10 Fri] | [2024-05-10 Fri] |
667
+ | '2026-E<Fri' | [2026-04-03 Fri] | [2026-04-03 Fri] |
668
+ | 'yestermonth' | [2025-11-01 Sat] | [2025-11-30 Sun] |
669
+ | 'lastmonth' | [2025-11-01 Sat] | [2025-11-30 Sun] |
670
+ | 'yesterfortnight' | [2025-12-08 Mon] | [2025-12-21 Sun] |
671
+ | 'thisfortnight' | [2025-12-22 Mon] | [2026-01-04 Sun] |
672
+ | 'nextfortnight' | [2026-01-05 Mon] | [2026-01-18 Sun] |
673
+ | '2025-E+50>=Su' | [2025-06-15 Sun] | [2025-06-15 Sun] |
674
+ ```
675
+
676
+
677
+ <a id="org0602bdf"></a>
678
+
679
+ # Contributing
680
+
681
+ 1. Fork it (<http://github.com/ddoherty03/fat_date/fork> )
682
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
683
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
684
+ 4. Push to the branch (`git push origin my-new-feature`)
685
+ 5. Create new Pull Request