Module: FatDate::Date

Included in:
Date
Defined in:
lib/fat_date/date.rb

Overview

FatDate Extensions

The FatDate extensions to the Date class add the notion of several additional calendar periods besides years, months, and weeks to those provided for in the Date class and the active_support extensions to Date. In particular, there are several additional calendar subdivisions (called "chunks" in this documentation) supported by FatDate's extension to the Date class:

  • year,
  • half,
  • quarter,
  • bimonth,
  • month,
  • semimonth,
  • biweek,
  • week, and
  • day

For each of those chunks, there are methods for finding the beginning and end of the chunk, for advancing or retreating a Date by the chunk, and for testing whether a Date is at the beginning or end of each of the chunk.

FatDate's Date extension defines a few convenience formatting methods, such as Date#iso and Date#org for formatting Dates as ISO strings and as Emacs org-mode inactive timestamps respectively. It also has a few utility methods for determining the date of Easter, the number of days in any given month, and the Date of the nth workday in a given month (say the third Thursday in October, 2014).

The Date extension defines a couple of class methods for parsing strings into Dates, especially Date.spec, which allows Dates to be specified in a lazy way, either absolutely or relative to the computer's clock.

Finally FatDate's Date extensions provide thorough methods for determining if a Date is a United States federal holiday or workday based on US law, including executive orders. It does the same for the New York Stock Exchange, based on the rules of the New York Stock Exchange, including dates on which the NYSE was closed for special reasons, such as the 9-11 attacks in 2001.

Defined Under Namespace

Modules: ClassMethods

Holidays and Workdays collapse

FED_DECREED_HOLIDAYS =

Holidays decreed by Presidential proclamation

[
  # Obama decree extra day before Christmas See
  # http://www.whitehouse.gov/the-press-office/2012/12/21
  ::Date.parse('2012-12-24'),
  # And Trump
  ::Date.parse('2018-12-24'),
  ::Date.parse('2019-12-24'),
  ::Date.parse('2020-12-24'),
  # Biden
  ::Date.parse('2024-12-24'),
  # Trump
  ::Date.parse('2025-12-24'),
  ::Date.parse('2025-12-26'),
]
PRESIDENTIAL_FUNERALS =

Presidential funeral since JFK

[
  # JKF Funeral
  ::Date.parse('1963-11-25'),
  # DWE Funeral
  ::Date.parse('1969-03-31'),
  # HST Funeral
  ::Date.parse('1972-12-28'),
  # LBJ Funeral
  ::Date.parse('1973-01-25'),
  # RMN Funeral
  ::Date.parse('1994-04-27'),
  # RWR Funeral
  ::Date.parse('2004-06-11'),
  # GTF Funeral
  ::Date.parse('2007-01-02'),
  # GHWBFuneral
  ::Date.parse('2018-12-05'),
  # JEC Funeral
  ::Date.parse('2025-01-09'),
]

Constant Summary collapse

BOT =

Constant for Beginning of Time (BOT) outside the range of what we would ever want to find in commercial situations.

::Date.parse('1900-01-01')
EOT =

Constant for End of Time (EOT) outside the range of what we would ever want to find in commercial situations.

::Date.parse('3000-12-31')
DAYSYMS =

Symbols for each of the days of the week, e.g., :sunday, :monday, etc.

::Date::DAYNAMES.map { |name| name.downcase.to_sym }

Formatting collapse

Queries collapse

Relative ::Dates collapse

Holidays and Workdays collapse

NYSE Holidays and Workdays collapse

Utilities collapse

Class Method Details

.included(base) ⇒ Object

Extend the Date class methods with the FatDate::ClassMethods methods when this module is included.



2057
2058
2059
# File 'lib/fat_date/date.rb', line 2057

def self.included(base)
  base.extend(ClassMethods)
end

Instance Method Details

#add_chunk(chunk, num = 1) ⇒ ::Date

Return the date that is n chunks later than self.

Parameters:

  • chunk (Symbol)

    one of :year, :half, :quarter, :bimonth, :month, :semimonth, :biweek, :week, or :day.

  • num (Integer) (defaults to: 1)

    the number of chunks to add, can be negative

Returns:

  • (::Date)

    the date n chunks from this date



786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
# File 'lib/fat_date/date.rb', line 786

def add_chunk(chunk, num = 1)
  case chunk
  when :year
    next_year(num)
  when :half
    next_month(6 * num)
  when :quarter
    next_month(3 * num)
  when :bimonth
    next_month(2 * num)
  when :month
    next_month(num)
  when :semimonth
    next_semimonth(num)
  when :biweek
    next_biweek(num)
  when :week
    next_week(num)
  when :day
    next_day(num)
  else
    raise ArgumentError, "add_chunk unknown chunk: '#{chunk}'"
  end
end

#add_fed_workdays(num) ⇒ ::Date

Return the date that is n federal workdays after or before (if n < 0) this date.

Parameters:

  • num (Integer)

    number of federal workdays to add to this date

Returns:



1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
# File 'lib/fat_date/date.rb', line 1028

def add_fed_workdays(num)
  d = dup
  return d if num.zero?

  incr = num.negative? ? -1 : 1
  num = num.abs
  while num.positive?
    d += incr
    num -= 1 if d.fed_workday?
  end
  d
end

#add_nyse_workdays(num) ⇒ ::Date Also known as: add_trading_days

Return a new date that is n NYSE trading days after or before (if n < 0) this date.

Parameters:

  • num (Integer)

    number of NYSE trading days to add to this date

Returns:



1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
# File 'lib/fat_date/date.rb', line 1211

def add_nyse_workdays(num)
  d = dup
  return d if num.zero?

  incr = num.negative? ? -1 : 1
  num = num.abs
  while num.positive?
    d += incr
    num -= 1 if d.nyse_workday?
  end
  d
end

#americanString

Format date in MM/DD/YYYY form, as typical for the short American form.

Returns:

  • (String)


105
106
107
# File 'lib/fat_date/date.rb', line 105

def american
  strftime('%-m/%-d/%Y')
end

#beginning_of_bicweek::Date

Return the date that is the first day of the commercial biweek in which self falls. A biweek is a period of two commercial weeks starting with an odd-numbered week and with each week starting in Monday and ending on Sunday.

Returns:



551
552
553
554
555
556
557
# File 'lib/fat_date/date.rb', line 551

def beginning_of_bicweek
  if cweek.odd?
    beginning_of_week(::Date.beginning_of_week)
  else
    (self - 1.week).beginning_of_week(::Date.beginning_of_week)
  end
end

#beginning_of_bimonth::Date

The date that is the first day of the bimonth in which self falls. A 'bimonth' is a two-month calendar period beginning on the first day of the odd-numbered months. E.g., 2014-01-01 to 2014-02-28 is the first bimonth of 2014.

Returns:



447
448
449
450
451
452
453
# File 'lib/fat_date/date.rb', line 447

def beginning_of_bimonth
  if month.odd?
    beginning_of_month
  else
    (self - 1.month).beginning_of_month
  end
end

#beginning_of_bimonth?Boolean

Return whether the date falls on the first day of a calendar bi-monthly period, i.e., the beginning of an odd-numbered month.

Returns:

  • (Boolean)


253
254
255
# File 'lib/fat_date/date.rb', line 253

def beginning_of_bimonth?
  month.odd? && beginning_of_month == self
end

#beginning_of_biweek::Date

Return the date that is the first day of the biweek in which self falls, or the first day of the year, whichever is later.

Returns:



523
524
525
526
527
528
529
# File 'lib/fat_date/date.rb', line 523

def beginning_of_biweek
  if week_number.odd?
    beginning_of_week
  else
    (self - 1.week).beginning_of_week
  end
end

#beginning_of_biweek?Boolean

Return whether the date falls on the first day of a commercial bi-week, i.e., on /Monday/ in a commercial week that is an odd-numbered week. From ::Date: "The calendar week is a seven day period within a calendar year, starting on a Monday and identified by its ordinal number within the year; the first calendar week of the year is the one that includes the first Thursday of that year. In the Gregorian calendar, this is equivalent to the week which includes January 4."

Returns:

  • (Boolean)


310
311
312
# File 'lib/fat_date/date.rb', line 310

def beginning_of_biweek?
  beginning_of_biweek == self
end

#beginning_of_chunk(chunk) ⇒ ::Date

Return the date that is the beginning of the chunk in which this date falls.

Parameters:

  • chunk (Symbol)

    one of :year, :half, :quarter, :bimonth, :month, :semimonth, :biweek, :week, or :day.

Returns:

  • (::Date)

    the first date in the chunk-sized period in which this date falls



818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
# File 'lib/fat_date/date.rb', line 818

def beginning_of_chunk(chunk)
  case chunk
  when :year
    beginning_of_year
  when :half
    beginning_of_half
  when :quarter
    beginning_of_quarter
  when :bimonth
    beginning_of_bimonth
  when :month
    beginning_of_month
  when :semimonth
    beginning_of_semimonth
  when :biweek
    beginning_of_biweek
  when :week
    beginning_of_week
  when :day
    self
  else
    raise ArgumentError, "unknown chunk sym: '#{chunk}'"
  end
end

#beginning_of_chunk?(chunk) ⇒ ::Boolean

Return whether the date that is the beginning of the chunk

Parameters:

  • chunk (Symbol)

    one of :year, :half, :quarter, :bimonth, :month, :semimonth, :biweek, :week, or :day.

Returns:

  • (::Boolean)

    whether this date begins a chunk



880
881
882
# File 'lib/fat_date/date.rb', line 880

def beginning_of_chunk?(chunk)
  self == beginning_of_chunk(chunk)
end

#beginning_of_half::Date

The date that is the first day of the half-year in which self falls.

Returns:



416
417
418
419
420
421
422
423
424
# File 'lib/fat_date/date.rb', line 416

def beginning_of_half
  if month > 9
    (beginning_of_quarter - 15).beginning_of_quarter
  elsif month > 6
    beginning_of_quarter
  else
    beginning_of_year
  end
end

#beginning_of_half?Boolean

Return whether the date falls on the first day of a half-year.

Returns:

  • (Boolean)


220
221
222
# File 'lib/fat_date/date.rb', line 220

def beginning_of_half?
  beginning_of_half == self
end

#beginning_of_month?Boolean

Return whether the date falls on the first day of a calendar month.

Returns:

  • (Boolean)


270
271
272
# File 'lib/fat_date/date.rb', line 270

def beginning_of_month?
  beginning_of_month == self
end

#beginning_of_quarter?Boolean

Return whether the date falls on the first day of a calendar quarter.

Returns:

  • (Boolean)


236
237
238
# File 'lib/fat_date/date.rb', line 236

def beginning_of_quarter?
  beginning_of_quarter == self
end

#beginning_of_semimonth::Date

The date that is the first day of the semimonth in which self falls. A semimonth is a calendar period beginning on the 1st or 16th of each month and ending on the 15th or last day of the month respectively. So each year has exactly 24 semimonths.

Returns:



477
478
479
480
481
482
483
# File 'lib/fat_date/date.rb', line 477

def beginning_of_semimonth
  if day >= 16
    ::Date.new(year, month, 16)
  else
    beginning_of_month
  end
end

#beginning_of_semimonth?Boolean

Return whether the date falls on the first day of a calendar semi-monthly period, i.e., on the 1st or 15th of a month.

Returns:

  • (Boolean)


287
288
289
# File 'lib/fat_date/date.rb', line 287

def beginning_of_semimonth?
  beginning_of_semimonth == self
end

#beginning_of_week?Boolean

Return whether the date falls on the first day of a commercial week, i.e., on /Monday/ in a commercial week. From ::Date: "The calendar week is a seven day period within a calendar year, starting on a Monday and identified by its ordinal number within the year; the first calendar week of the year is the one that includes the first Thursday of that year. In the Gregorian calendar, this is equivalent to the week which includes January 4."

Returns:

  • (Boolean)


337
338
339
# File 'lib/fat_date/date.rb', line 337

def beginning_of_week?
  beginning_of_week == self
end

#beginning_of_year?Boolean

Return whether the date falls on the first day of a year.

Returns:

  • (Boolean)


204
205
206
# File 'lib/fat_date/date.rb', line 204

def beginning_of_year?
  beginning_of_year == self
end

#bimonthInteger

Self's calendar bimonth: 1, 2, 3, 4, 5, or 6 depending on which calendar bimonth the date falls in.

Returns:

  • (Integer)


159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/fat_date/date.rb', line 159

def bimonth
  case month
  when (1..2)
    1
  when (3..4)
    2
  when (5..6)
    3
  when (7..8)
    4
  when (9..10)
    5
  when (11..12)
    6
  end
end

#biweekInteger

Self's calendar biweek: 1, through 24 depending on which calendar semimonth the date falls in.

Returns:

  • (Integer)


186
187
188
189
190
191
192
# File 'lib/fat_date/date.rb', line 186

def biweek
  if cweek.odd?
    (cweek + 1) / 2
  else
    cweek / 2
  end
end

#days_in_monthInteger

Number of days in self's month

Returns:

  • (Integer)


116
117
118
# File 'lib/fat_date/date.rb', line 116

def days_in_month
  self.class.days_in_month(year, month)
end

#easter?(reform_year: 1582) ⇒ Boolean

Return whether this date is Easter Sunday for the year in which it falls according to the Western Church.

Returns:

  • (Boolean)


922
923
924
925
# File 'lib/fat_date/date.rb', line 922

def easter?(reform_year: 1582)
  # Am I Easter?
  self == easter_this_year(reform_year: reform_year)
end

#easter_this_year(reform_year: 1582) ⇒ ::Date

Return the date for Easter in the Western Church for the year in which this date falls. A few holidays key off this date as "moveable feasts."

Returns:



913
914
915
916
# File 'lib/fat_date/date.rb', line 913

def easter_this_year(reform_year: 1582)
  # Return the date of Easter in self's year
  ::Date.easter(year, reform_year: reform_year)
end

#end_of_bicweekObject

Return the date that is the last day of the commercial biweek in which self falls. A biweek is a period of two commercial weeks starting with an odd-numbered week and with each week starting on Monday and ending on Sunday. So this will always return a Sunday in an even-numbered week. In the last week of the year (if it is not part of next year's first week) the end of the biweek will not extend beyond self's week, so that week 1 of the following year will start a new biweek. @return [::Date]



568
569
570
571
572
573
574
575
576
# File 'lib/fat_date/date.rb', line 568

def end_of_bicweek
  if cweek >= 52 && end_of_week(::Date.beginning_of_week).year > year
    end_of_week(::Date.beginning_of_week)
  elsif cweek.odd?
    (self + 1.week).end_of_week(::Date.beginning_of_week)
  else
    end_of_week(::Date.beginning_of_week)
  end
end

#end_of_bimonth::Date

The date that is the last day of the bimonth in which self falls. A 'bimonth' is a two-month calendar period beginning on the first day of the odd-numbered months. E.g., 2014-01-01 to 2014-02-28 is the first bimonth of 2014.

Returns:



462
463
464
465
466
467
468
# File 'lib/fat_date/date.rb', line 462

def end_of_bimonth
  if month.odd?
    (self + 1.month).end_of_month
  else
    end_of_month
  end
end

#end_of_bimonth?Boolean

Return whether the date falls on the last day of a calendar bi-monthly period, i.e., the end of an even-numbered month.

Returns:

  • (Boolean)


262
263
264
# File 'lib/fat_date/date.rb', line 262

def end_of_bimonth?
  month.even? && end_of_month == self
end

#end_of_biweek::Date

Return the date that is the last day of the biweek in which self falls, or the last day of the year, whichever if earlier.

Returns:



536
537
538
539
540
541
542
# File 'lib/fat_date/date.rb', line 536

def end_of_biweek
  if week_number.even?
    end_of_week
  else
    (self + 1.week).end_of_week
  end
end

#end_of_biweek?Boolean

Return whether the date falls on the last day of a commercial bi-week, i.e., on /Sunday/ in a commercial week that is an even-numbered week. From ::Date: "The calendar week is a seven day period within a calendar year, starting on a Monday and identified by its ordinal number within the year; the first calendar week of the year is the one that includes the first Thursday of that year. In the Gregorian calendar, this is equivalent to the week which includes January 4."

Returns:

  • (Boolean)


324
325
326
# File 'lib/fat_date/date.rb', line 324

def end_of_biweek?
  end_of_biweek == self
end

#end_of_chunk(chunk) ⇒ ::Date

Return the date that is the end of the chunk in which this date falls.

Parameters:

  • chunk (Symbol)

    one of :year, :half, :quarter, :bimonth, :month, :semimonth, :biweek, :week, or :day.

Returns:

  • (::Date)

    the first date in the chunk-sized period in which this date falls



850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
# File 'lib/fat_date/date.rb', line 850

def end_of_chunk(chunk)
  case chunk
  when :year
    end_of_year
  when :half
    end_of_half
  when :quarter
    end_of_quarter
  when :bimonth
    end_of_bimonth
  when :month
    end_of_month
  when :semimonth
    end_of_semimonth
  when :biweek
    end_of_biweek
  when :week
    end_of_week
  when :day
    self
  else
    raise ArgumentError, "unknown chunk: '#{chunk}'"
  end
end

#end_of_chunk?(chunk) ⇒ ::Boolean

Return whether the date that is the end of the chunk

Parameters:

  • chunk (Symbol)

    one of :year, :half, :quarter, :bimonth, :month, :semimonth, :biweek, :week, or :day.

Returns:

  • (::Boolean)

    whether this date ends a chunk



889
890
891
# File 'lib/fat_date/date.rb', line 889

def end_of_chunk?(chunk)
  self == end_of_chunk(chunk)
end

#end_of_half::Date

The date that is the last day of the half-year in which self falls.

Returns:



430
431
432
433
434
435
436
437
438
# File 'lib/fat_date/date.rb', line 430

def end_of_half
  if month < 4
    (end_of_quarter + 15).end_of_quarter
  elsif month < 7
    end_of_quarter
  else
    end_of_year
  end
end

#end_of_half?Boolean

Return whether the date falls on the last day of a half-year.

Returns:

  • (Boolean)


228
229
230
# File 'lib/fat_date/date.rb', line 228

def end_of_half?
  end_of_half == self
end

#end_of_month?Boolean

Return whether the date falls on the last day of a calendar month.

Returns:

  • (Boolean)


278
279
280
# File 'lib/fat_date/date.rb', line 278

def end_of_month?
  end_of_month == self
end

#end_of_quarter?Boolean

Return whether the date falls on the last day of a calendar quarter.

Returns:

  • (Boolean)


244
245
246
# File 'lib/fat_date/date.rb', line 244

def end_of_quarter?
  end_of_quarter == self
end

#end_of_semimonth::Date

The date that is the last day of the semimonth in which self falls. A semimonth is a calendar period beginning on the 1st or 16th of each month and ending on the 15th or last day of the month respectively. So each year has exactly 24 semimonths.

Returns:



492
493
494
495
496
497
498
# File 'lib/fat_date/date.rb', line 492

def end_of_semimonth
  if day <= 15
    ::Date.new(year, month, 15)
  else
    end_of_month
  end
end

#end_of_semimonth?Boolean

Return whether the date falls on the last day of a calendar semi-monthly period, i.e., on the 14th or the last day of a month.

Returns:

  • (Boolean)


296
297
298
# File 'lib/fat_date/date.rb', line 296

def end_of_semimonth?
  end_of_semimonth == self
end

#end_of_week?Boolean

Return whether the date falls on the first day of a commercial week, i.e., on /Sunday/ in a commercial week. From ::Date: "The calendar week is a seven day period within a calendar year, starting on a Monday and identified by its ordinal number within the year; the first calendar week of the year is the one that includes the first Thursday of that year. In the Gregorian calendar, this is equivalent to the week which includes January 4."

Returns:

  • (Boolean)


350
351
352
# File 'lib/fat_date/date.rb', line 350

def end_of_week?
  end_of_week == self
end

#end_of_year?Boolean

Return whether the date falls on the last day of a year.

Returns:

  • (Boolean)


212
213
214
# File 'lib/fat_date/date.rb', line 212

def end_of_year?
  end_of_year == self
end

#engString

Format as an English string, like 'January 12, 2016'

Returns:

  • (String)


96
97
98
# File 'lib/fat_date/date.rb', line 96

def eng
  strftime('%B %-d, %Y')
end

#fed_holiday?Boolean

Return whether this date is a United States federal holiday.

Calculations for Federal holidays are based on 5 USC 6103, include all weekends, Presidential funerals, and holidays decreed executive orders.

Returns:

  • (Boolean)


972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
# File 'lib/fat_date/date.rb', line 972

def fed_holiday?
  # All Saturdays and Sundays are "holidays"
  return true if weekend?

  # Some days are holidays by executive decree
  return true if FED_DECREED_HOLIDAYS.include?(self)

  # Presidential funerals
  return true if PRESIDENTIAL_FUNERALS.include?(self)

  # Is self a fixed holiday
  return true if fed_fixed_holiday? || fed_moveable_feast?

  if friday? && month == 12 && day == 26
    # If Christmas falls on a Thursday, apparently, the Friday after is
    # treated as a holiday as well.  See 2003, 2008, for example.
    true
  elsif friday?
    # A Friday is a holiday if a fixed-date holiday
    # would fall on the following Saturday
    (self + 1).fed_fixed_holiday? || (self + 1).fed_moveable_feast?
  elsif monday?
    # A Monday is a holiday if a fixed-date holiday
    # would fall on the preceding Sunday
    (self - 1).fed_fixed_holiday? || (self - 1).fed_moveable_feast?
  elsif (year % 4 == 1) && year > 1965 && mon == 1 && mday == 20
    # Inauguration Day after 1965 is a federal holiday, but if it falls on a
    # Sunday, the following Monday is observed, but if it falls on a
    # Saturday, the prior Friday is /not/ observed. So, we can't just count
    # this as a regular fixed holiday.
    true
  elsif monday? && (year % 4 == 1) && year > 1965 && mon == 1 && mday == 21
    # Inauguration Day after 1965 is a federal holiday, but if it falls on a
    # Sunday, the following Monday is observed, but if it falls on a
    # Saturday, the prior Friday is /not/ observed.
    true
  else
    false
  end
end

#fed_workday?Boolean

Return whether this date is a date on which the US federal government is open for business. It is the opposite of #fed_holiday?

Returns:

  • (Boolean)


1017
1018
1019
# File 'lib/fat_date/date.rb', line 1017

def fed_workday?
  !fed_holiday?
end

#halfInteger

Self's calendar "half" by analogy to calendar quarters: 1 or 2, depending on whether the date falls in the first or second half of the calendar year.

Returns:

  • (Integer)


129
130
131
132
133
134
135
136
# File 'lib/fat_date/date.rb', line 129

def half
  case month
  when (1..6)
    1
  when (7..12)
    2
  end
end

#isoString

Format as an ISO string of the form YYYY-MM-DD.

Returns:

  • (String)


59
60
61
# File 'lib/fat_date/date.rb', line 59

def iso
  strftime('%Y-%m-%d')
end

#next_bimonth(num = 1) ⇒ ::Date

Return the date that is n calendar bimonths after this date, where a calendar bimonth is a period of 2 months.

Parameters:

  • num (Integer) (defaults to: 1)

    number of bimonths to advance, can be negative

Returns:

  • (::Date)

    new date n bimonths after this date



627
628
629
630
631
632
# File 'lib/fat_date/date.rb', line 627

def next_bimonth(num = 1)
  num = num.floor
  return self if num.zero?

  next_month(num * 2)
end

#next_biweek(num = 1) ⇒ ::Date

Return the date that is n biweeks after this date where each biweek is 14 days.

Parameters:

  • num (Integer) (defaults to: 1)

    number of biweeks to advance, can be negative

Returns:

  • (::Date)

    new date n biweeks after this date



728
729
730
731
732
733
# File 'lib/fat_date/date.rb', line 728

def next_biweek(num = 1)
  num = num.floor
  return self if num.zero?

  self + (14 * num)
end

#next_fed_workday::Date

Return the next federal workday after this date. The date returned is always a date at least one day after this date, never this date.

Returns:



1045
1046
1047
# File 'lib/fat_date/date.rb', line 1045

def next_fed_workday
  add_fed_workdays(1)
end

#next_half(num = 1) ⇒ ::Date

Return the date that is n calendar halves after this date, where a calendar half is a period of 6 months.

Parameters:

  • num (Integer) (defaults to: 1)

    number of halves to advance, can be negative

Returns:

  • (::Date)

    new date n halves after this date



585
586
587
588
589
590
# File 'lib/fat_date/date.rb', line 585

def next_half(num = 1)
  num = num.floor
  return self if num.zero?

  next_month(num * 6)
end

#next_nyse_workday::Date Also known as: next_trading_day

Return the next NYSE trading day after this date. The date returned is always a date at least one day after this date, never this date.

Returns:



1229
1230
1231
# File 'lib/fat_date/date.rb', line 1229

def next_nyse_workday
  add_nyse_workdays(1)
end

#next_quarter(num = 1) ⇒ ::Date

Return the date that is n calendar quarters after this date, where a calendar quarter is a period of 3 months.

Parameters:

  • num (Integer) (defaults to: 1)

    number of quarters to advance, can be negative

Returns:

  • (::Date)

    new date n quarters after this date



606
607
608
609
610
611
# File 'lib/fat_date/date.rb', line 606

def next_quarter(num = 1)
  num = num.floor
  return self if num.zero?

  next_month(num * 3)
end

#next_semimonth(num = 1) ⇒ ::Date

Return the date that is n semimonths after this date. Each semimonth begins on the 1st or 16th of the month, and advancing one semimonth from the first half of a month means to go as far past the 16th as the current date is past the 1st; advancing one semimonth from the second half of a month means to go as far into the next month past the 1st as the current date is past the 16th, but never past the 15th of the next month.

Parameters:

  • num (Integer) (defaults to: 1)

    number of semimonths to advance, can be negative

Returns:

  • (::Date)

    new date n semimonths after this date



652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
# File 'lib/fat_date/date.rb', line 652

def next_semimonth(num = 1)
  num = num.floor
  return self if num.zero?

  factor = num.negative? ? -1 : 1
  num = num.abs
  if num.even?
    next_month(num / 2)
  else
    # Advance or retreat one semimonth
    next_sm =
      if day == 1
        if factor.positive?
          beginning_of_month + 16.days
        else
          prior_month.beginning_of_month + 16.days
        end
      elsif day == 16
        if factor.positive?
          next_month.beginning_of_month
        else
          beginning_of_month
        end
      elsif day < 16
        # In the first half of the month (the 2nd to the 15th), go as far past
        # the 16th as the date is past the 1st. Thus, as many as 14 days past
        # the 16th, so at most to the 30th of the month unless there are less
        # than 30 days in the month, then go to the end of the month.
        if factor.positive?
          [beginning_of_month + 16.days + (day - 1).days, end_of_month].min
        else
          [
            prior_month.beginning_of_month + 16.days + (day - 1).days,
            prior_month.end_of_month
          ].min
        end
      elsif factor.positive?
        # In the second half of the month (17th to the 31st), go as many
        # days into the next month as we are past the 16th. Thus, as many as
        # 15 days.  But we don't want to go past the first half of the next
        # month, so we only go so far as the 15th of the next month.
        # ::Date.parse('2015-02-18').next_semimonth should be the 3rd of the
        # following month.
        next_month.beginning_of_month + [(day - 16), 15].min
      else
        beginning_of_month + [(day - 16), 15].min
      end
    num -= 1
    # Now that n is even, advance (or retreat) n / 2 months unless we're done.
    if num >= 2
      next_sm.next_month(factor * num / 2)
    else
      next_sm
    end
  end
end

#next_until_fed_workday::Date

Return this date if its a federal workday, otherwise skip forward to the first later federal workday.

Returns:



1061
1062
1063
1064
1065
# File 'lib/fat_date/date.rb', line 1061

def next_until_fed_workday
  date = dup
  date += 1 until date.fed_workday?
  date
end

#next_until_nyse_workday::Date Also known as: next_until_trading_day

Return this date if its a trading day, otherwise skip forward to the first later trading day.

Returns:



1247
1248
1249
1250
1251
# File 'lib/fat_date/date.rb', line 1247

def next_until_nyse_workday
  date = dup
  date += 1 until date.trading_day?
  date
end

#next_week(num = 1) ⇒ ::Date

Return the date that is n weeks after this date where each week is 7 days. This is different from the #next_week method in active_support, which goes to the first day of the week in the next week and does not take an argument n to go multiple weeks.

Parameters:

  • num (Integer) (defaults to: 1)

    number of weeks to advance

Returns:

  • (::Date)

    new date n weeks after this date



751
752
753
754
755
756
# File 'lib/fat_date/date.rb', line 751

def next_week(num = 1)
  num = num.floor
  return self if num.zero?

  self + (7 * num)
end

#nth_wday_in_month?(nth, wday, month) ⇒ Boolean

Return whether this date is the nth weekday wday of the given month in this date's year.

Parameters:

  • nth (Integer)

    number of wday in month, if negative count from end of the month

  • wday (Integer)

    day of week, 0 is Sunday, 1 Monday, etc.

  • month (Integer)

    the month number, 1 is January, 2 is February, etc.

Returns:

  • (Boolean)


397
398
399
400
401
# File 'lib/fat_date/date.rb', line 397

def nth_wday_in_month?(nth, wday, month)
  # Is self the nth weekday in the given month of its year?
  # If nth is negative, count from last day of month
  self == ::Date.nth_wday_in_year_month(nth, wday, year, month)
end

#numString

Format as an all-numeric string of the form YYYYMMDD

Returns:

  • (String)


75
76
77
# File 'lib/fat_date/date.rb', line 75

def num
  strftime('%Y%m%d')
end

#nyse_fixed_holiday?Boolean

Return whether this date is a fixed holiday for the NYSE, that is, a holiday that falls on the same date each year.

Returns:

  • (Boolean)


1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
# File 'lib/fat_date/date.rb', line 1268

def nyse_fixed_holiday?
  # Fixed-date holidays
  if mon == 1 && mday == 1
    # New Years (January 1),
    true
  elsif mon == 7 && mday == 4
    # Independence Day (July 4),
    true
  elsif mon == 12 && mday == 25
    # Christmas (December 25), and
    true
  else
    false
  end
end

#nyse_holiday?Boolean

Returns whether this date is one on which the NYSE was or is expected to be closed for business.

Calculations for NYSE holidays are from Rule 51 and supplementary materials for the Rules of the New York Stock Exchange, Inc.

  • General Rule 1: if a regular holiday falls on Saturday, observe it on the preceding Friday.
  • General Rule 2: if a regular holiday falls on Sunday, observe it on the following Monday.

These are the regular holidays:

  • New Year's Day, January 1.
  • Birthday of Martin Luther King, Jr., the third Monday in January.
  • Washington's Birthday, the third Monday in February.
  • Good Friday Friday before Easter Sunday. NOTE: this is not a fed holiday
  • Memorial Day, the last Monday in May.
  • Independence Day, July 4.
  • Labor Day, the first Monday in September.
  • Thanksgiving Day, the fourth Thursday in November.
  • Christmas Day, December 25.

Columbus and Veterans days not observed.

In addition, there have been several days on which the exchange has been closed for special events such as Presidential funerals, the 9-11 attacks, the paper-work crisis in the 1960's, hurricanes, etc. All of these are considered holidays for purposes of this method.

In addition, every weekend is considered a holiday.

Returns:

  • (Boolean)


1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
# File 'lib/fat_date/date.rb', line 1167

def nyse_holiday?
  # All Saturdays and Sundays are "holidays"
  return true if weekend?

  # Presidential funerals, observed by NYSE as well.
  return true if PRESIDENTIAL_FUNERALS.include?(self)

  # Is self a fixed holiday
  return true if nyse_fixed_holiday? || nyse_moveable_feast?

  return true if nyse_special_holiday?

  if friday? && (self >= ::Date.parse('1959-07-03'))
    # A Friday is a holiday if a holiday would fall on the following
    # Saturday.  The rule does not apply if the Friday "ends a monthly or
    # yearly accounting period." Adopted July 3, 1959. E.g, December 31,
    # 2010, fell on a Friday, so New Years was on Saturday, but the NYSE
    # opened because it ended a yearly accounting period.  I believe 12/31
    # is the only date to which the exception can apply since only New
    # Year's can fall on the first of the month.
    !end_of_quarter? &&
      ((self + 1).nyse_fixed_holiday? || (self + 1).nyse_moveable_feast?)
  elsif monday?
    # A Monday is a holiday if a holiday would fall on the
    # preceding Sunday.  This has apparently always been the rule.
    (self - 1).nyse_fixed_holiday? || (self - 1).nyse_moveable_feast?
  else
    false
  end
end

#nyse_moveable_feast?Boolean

Return whether this date is a non-fixed holiday for the NYSE, that is, a holiday that can fall on different dates each year, a so-called "moveable feast".

Returns:

  • (Boolean)


1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
# File 'lib/fat_date/date.rb', line 1290

def nyse_moveable_feast?
  # See if today is a "movable feast," all of which are
  # rigged to fall on Monday except Thanksgiving

  # No moveable feasts in certain months
  return false if [7, 8, 10, 12].include?(month)

  case month
  when 1
    # MLK's Birthday (Third Monday in Jan) since 1998
    year >= 1998 && nth_wday_in_month?(3, 1, 1)
  when 2
    # Washington's Birthday was celebrated on February 22 until 1970. In
    # 1971 and later, it was moved to the third Monday in February.  Note:
    # Lincoln's birthday is not an official holiday, but is sometimes
    # included with Washington's and called "Presidents' Day."
    if year <= 1970
      month == 2 && day == 22
    else
      nth_wday_in_month?(3, 1, 2)
    end
  when 3, 4
    # Good Friday
    if !friday?
      false
    elsif [1898, 1906, 1907].include?(year)
      # Good Friday, the Friday before Easter, except certain years
      false
    else
      (self + 2).easter?
    end
  when 5
    # Memorial Day (Last Monday in May)
    year <= 1970 ? (month == 5 && day == 30) : nth_wday_in_month?(-1, 1, 5)
  when 6
    # Juneteenth, starting 2022.  On June 19, but moved to prior Friday if
    # it falls on Saturday and moved to Monday if it fall on Sunday.
    if year < 2022
      false
    else
      jun19 = ::Date.new(year, 6, 19)
      if jun19.saturday?
        day == 18
      elsif jun19.sunday?
        day == 20
      else
        day == 19
      end
    end
  when 9
    # Labor Day (First Monday in Sep)
    nth_wday_in_month?(1, 1, 9)
  when 10
    # Columbus Day (Oct 12) 1909--1953
    year.between?(1909, 1953) && day == 12
  when 11
    if tuesday?
      # Election Day. Until 1968 all Election Days.  From 1972 to 1980
      # Election Day in presidential years only.  Election Day is the first
      # Tuesday after the first Monday in November.
      first_tuesday = ::Date.nth_wday_in_year_month(1, 1, year, 11) + 1
      is_election_day = (self == first_tuesday)
      if year <= 1968
        is_election_day
      elsif year <= 1980
        is_election_day && (year % 4).zero?
      else
        false
      end
    elsif thursday?
      # Historically Thanksgiving (NYSE closed all day) had been declared to
      #   be the last Thursday in November until 1938; the next-to-last
      #   Thursday in November from 1939 to 1941 (therefore the 3rd Thursday
      #   in 1940 and 1941); the last Thursday in November in 1942; the fourth
      #   Thursday in November since 1943;
      if year < 1938
        nth_wday_in_month?(-1, 4, 11)
      elsif year <= 1941
        nth_wday_in_month?(3, 4, 11)
      elsif year == 1942
        nth_wday_in_month?(-1, 4, 11)
      else
        nth_wday_in_month?(4, 4, 11)
      end
    elsif day == 11
      # Armistice or Veterans Day.  1918--1921; 1934--1953.
      year.between?(1918, 1921) || year.between?(1934, 1953)
    else
      false
    end
  else
    false
  end
end

#nyse_special_holiday?Boolean

They NYSE has closed on several occasions outside its normal holiday rules. This detects those dates beginning in 1960. Closing for part of a day is not counted. See http://www1.nyse.com/pdfs/closings.pdf. Return whether this date is one of those special closings.

Returns:

  • (Boolean)


1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
# File 'lib/fat_date/date.rb', line 1393

def nyse_special_holiday?
  return false if self <= ::Date.parse('1960-01-01')

  return true if PRESIDENTIAL_FUNERALS.include?(self)

  case self
  when ::Date.parse('1961-05-29')
    # Day before Decoaration Day
    true
  when ::Date.parse('1963-11-25')
    # President Kennedy's funeral
    true
  when ::Date.parse('1965-12-24')
    # Christmas eve unscheduled for normal holiday
    true
  when ::Date.parse('1968-02-12')
    # Lincoln birthday
    true
  when ::Date.parse('1968-04-09')
    # Mourning MLK
    true
  when ::Date.parse('1968-07-05')
    # Day after Independence Day
    true
  when (::Date.parse('1968-06-12')..::Date.parse('1968-12-31'))
    # Paperwork crisis (closed on Wednesdays if no other holiday in week)
    wednesday? && (self - 2).nyse_workday? && (self - 1).nyse_workday? &&
      (self + 1).nyse_workday? && (self + 2).nyse_workday?
  when ::Date.parse('1969-02-10')
    # Heavy snow
    true
  when ::Date.parse('1969-07-21')
    # Moon landing
    true
  when ::Date.parse('1977-07-14')
    # Electrical blackout NYC
    true
  when ::Date.parse('1985-09-27')
    # Hurricane Gloria
    true
  when (::Date.parse('2001-09-11')..::Date.parse('2001-09-14'))
    # 9-11 Attacks
    true
  when ::Date.parse('2007-01-02')
    # Observance death of President Ford
    true
  when ::Date.parse('2012-10-29'), ::Date.parse('2012-10-30')
    # Hurricane Sandy
    true
  else
    false
  end
end

#nyse_workday?Boolean Also known as: trading_day?

Return whether the NYSE is open for trading on this date.

Returns:

  • (Boolean)


1201
1202
1203
# File 'lib/fat_date/date.rb', line 1201

def nyse_workday?
  !nyse_holiday?
end

#org(active: false) ⇒ String

Format as an inactive Org date timestamp of the form [YYYY-MM-DD <dow>] (see Emacs org-mode)

Returns:

  • (String)


84
85
86
87
88
89
90
# File 'lib/fat_date/date.rb', line 84

def org(active: false)
  if active
    strftime('<%Y-%m-%d %a>')
  else
    strftime('[%Y-%m-%d %a]')
  end
end

#pred::Date

Predecessor of self, opposite of #succ.

Returns:



408
409
410
# File 'lib/fat_date/date.rb', line 408

def pred
  self - 1.day
end

#prior_bimonth(num = 1) ⇒ ::Date

Return the date that is n calendar bimonths before this date, where a calendar bimonth is a period of 2 months.

Parameters:

  • num (Integer) (defaults to: 1)

    number of bimonths to retreat, can be negative

Returns:

  • (::Date)

    new date n bimonths before this date



639
640
641
# File 'lib/fat_date/date.rb', line 639

def prior_bimonth(num = 1)
  next_bimonth(-num)
end

#prior_biweek(num = 1) ⇒ ::Date

Return the date that is n biweeks before this date where each biweek is 14 days.

Parameters:

  • num (Integer) (defaults to: 1)

    number of biweeks to retreat, can be negative

Returns:

  • (::Date)

    new date n biweeks before this date



740
741
742
# File 'lib/fat_date/date.rb', line 740

def prior_biweek(num = 1)
  next_biweek(-num)
end

#prior_day(num) ⇒ ::Date

Return the date that is n weeks before this date where each week is 7 days.

Parameters:

  • num (Integer)

    number of days to retreat

Returns:

  • (::Date)

    new date n days before this date



774
775
776
# File 'lib/fat_date/date.rb', line 774

def prior_day(num)
  next_day(-num)
end

#prior_fed_workday::Date

Return the last federal workday before this date. The date returned is always a date at least one day before this date, never this date.

Returns:



1053
1054
1055
# File 'lib/fat_date/date.rb', line 1053

def prior_fed_workday
  add_fed_workdays(-1)
end

#prior_half(num = 1) ⇒ ::Date

Return the date that is n calendar halves before this date, where a calendar half is a period of 6 months.

Parameters:

  • num (Integer) (defaults to: 1)

    number of halves to retreat, can be negative

Returns:

  • (::Date)

    new date n halves before this date



597
598
599
# File 'lib/fat_date/date.rb', line 597

def prior_half(num = 1)
  next_half(-num)
end

#prior_nyse_workday::Date Also known as: prior_trading_day

Return the last NYSE trading day before this date. The date returned is always a date at least one day before this date, never this date.

Returns:



1238
1239
1240
# File 'lib/fat_date/date.rb', line 1238

def prior_nyse_workday
  add_nyse_workdays(-1)
end

#prior_quarter(num = 1) ⇒ ::Date

Return the date that is n calendar quarters before this date, where a calendar quarter is a period of 3 months.

Parameters:

  • num (Integer) (defaults to: 1)

    number of quarters to retreat, can be negative

Returns:

  • (::Date)

    new date n quarters after this date



618
619
620
# File 'lib/fat_date/date.rb', line 618

def prior_quarter(num = 1)
  next_quarter(-num)
end

#prior_semimonth(num = 1) ⇒ ::Date

Return the date that is n semimonths before this date. Each semimonth begins on the 1st or 15th of the month, and retreating one semimonth from the first half of a month means to go as far past the 15th of the prior month as the current date is past the 1st; retreating one semimonth from the second half of a month means to go as far past the 1st of the current month as the current date is past the 15th, but never past the 14th of the the current month.

Parameters:

  • num (Integer) (defaults to: 1)

    number of semimonths to retreat, can be negative

Returns:

  • (::Date)

    new date n semimonths before this date



719
720
721
# File 'lib/fat_date/date.rb', line 719

def prior_semimonth(num = 1)
  next_semimonth(-num)
end

#prior_until_fed_workdayObject

Return this if its a federal workday, otherwise skip back to the first prior federal workday.



1069
1070
1071
1072
1073
# File 'lib/fat_date/date.rb', line 1069

def prior_until_fed_workday
  date = dup
  date -= 1 until date.fed_workday?
  date
end

#prior_until_trading_day::Date

Return this date if its a trading day, otherwise skip back to the first prior trading day.

Returns:



1258
1259
1260
1261
1262
# File 'lib/fat_date/date.rb', line 1258

def prior_until_trading_day
  date = dup
  date -= 1 until date.trading_day?
  date
end

#prior_week(num) ⇒ ::Date

Return the date that is n weeks before this date where each week is 7 days.

Parameters:

  • num (Integer)

    number of weeks to retreat

Returns:

  • (::Date)

    new date n weeks from this date



763
764
765
# File 'lib/fat_date/date.rb', line 763

def prior_week(num)
  next_week(-num)
end

#quarterInteger

Self's calendar quarter: 1, 2, 3, or 4, depending on which calendar quarter the date falls in.

Returns:

  • (Integer)


143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/fat_date/date.rb', line 143

def quarter
  case month
  when (1..3)
    1
  when (4..6)
    2
  when (7..9)
    3
  when (10..12)
    4
  end
end

#semimonthInteger

Self's calendar semimonth: 1, through 24 depending on which calendar semimonth the date falls in.

Returns:

  • (Integer)


179
180
181
# File 'lib/fat_date/date.rb', line 179

def semimonth
  ((month - 1) * 2) + (day <= 15 ? 1 : 2)
end

#tex_quoteString

Format date to TeX documents as ISO strings but with en-dashes

Returns:

  • (String)


67
68
69
# File 'lib/fat_date/date.rb', line 67

def tex_quote
  strftime('%Y--%m--%d').tex_quote
end

#weekInteger

Self's calendar week: just calls cweek.

Returns:

  • (Integer)


196
197
198
# File 'lib/fat_date/date.rb', line 196

def week
  cweek
end

#week_numberObject

In order to accomodate biweeks, we adopt the convention that weeks are numbered so that whatever week contains the year's first Date.beginning_of_week (usually Sunday or Monday) is week number 1. Biweeks are then pairs of weeks: an odd numbered week first, followed by an even numbered week last.



507
508
509
510
511
512
513
514
515
516
517
518
# File 'lib/fat_date/date.rb', line 507

def week_number
  start_of_weeks = ::Date.new(year, 1, 1)
  bow_wday = DAYSYMS.index(::Date.beginning_of_week)
  start_of_weeks += 1 until start_of_weeks.wday == bow_wday
  if yday >= start_of_weeks.yday
    ((yday - start_of_weeks.yday) / 7) + 1
  else
    # One of the days before the start of the year's first week, so it
    # belongs to the last week of the prior year.
    ::Date.new(year - 1, 12, 31).week_number
  end
end

#weekday?Boolean

Does self fall on a weekday?

Returns:

  • (Boolean)


905
906
907
# File 'lib/fat_date/date.rb', line 905

def weekday?
  !weekend?
end

#weekend?Boolean

Does self fall on a weekend?

Returns:

  • (Boolean)


897
898
899
# File 'lib/fat_date/date.rb', line 897

def weekend?
  saturday? || sunday?
end

#within_6mos_of?(from_date) ⇒ Boolean

Return whether this date falls within a period of less than six months from the date d using the Stella v. Graham Page Motors convention that "less" than six months is true only if this date falls within the range of dates 2 days after date six months before and 2 days before the date six months after the date d.

Parameters:

  • from_date (::Date)

    the middle of the six-month range

Returns:

  • (Boolean)


362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/fat_date/date.rb', line 362

def within_6mos_of?(from_date)
  from_date = ::Date.parse(from_date) unless from_date.is_a?(Date)
  from_day = from_date.day
  if [28, 29, 30, 31].include?(from_day)
    # Near the end of the month, we need to make sure that when we go
    # forward or backwards 6 months, we do not go past the end of the
    # destination month when finding the "corresponding" day in that
    # month, per Stella v. Graham Page Motors.  This refinement was
    # endorsed in the Jammies International case.  After we find the
    # corresponding day in the target month, then add two days (for the
    # month six months before the from_date) or subtract two days (for the
    # month six months after the from_date) to get the first and last days
    # of the "within a period of less than six months" date range.
    start_month = from_date.beginning_of_month - 6.months
    start_days = start_month.days_in_month
    start_date = ::Date.new(start_month.year, start_month.month, [start_days, from_day].min) + 2.days
    end_month = from_date.beginning_of_month + 6.months
    end_days = end_month.days_in_month
    end_date = ::Date.new(end_month.year, end_month.month, [end_days, from_day].min) - 2.days
  else
    # ::Date 6 calendar months before self
    start_date = from_date - 6.months + 2.days
    end_date = from_date + 6.months - 2.days
  end
  (start_date..end_date).cover?(self)
end