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