module FatCore::Date::ClassMethods

Constants

COMMON_YEAR_DAYS_IN_MONTH

An Array of the number of days in each month indexed by month number, starting with January = 1, etc.

Public Instance Methods

days_in_month(year, month) click to toggle source
# File lib/fat_core/date.rb, line 1680
def days_in_month(year, month)
  raise ArgumentError, 'illegal month number' if month < 1 || month > 12

  days = COMMON_YEAR_DAYS_IN_MONTH[month]
  if month == 2
    ::Date.new(year, month, 1).leap? ? 29 : 28
  else
    days
  end
end
easter(year) click to toggle source

Return the date of Easter for the Western Church in the given year.

@param year [Integer] the year of interest @return [::Date] the date of Easter for year

# File lib/fat_core/date.rb, line 1775
def easter(year)
  y = year
  a = y % 19
  b, c = y.divmod(100)
  d, e = b.divmod(4)
  f = (b + 8) / 25
  g = (b - f + 1) / 3
  h = (19 * a + b - d - g + 15) % 30
  i, k = c.divmod(4)
  l = (32 + 2 * e + 2 * i - h - k) % 7
  m = (a + 11 * h + 22 * l) / 451
  n, p = (h + l - 7 * m + 114).divmod(31)
  ::Date.new(y, n, p + 1)
end
ensure_date(dat) click to toggle source

Ensure that date is of class Date based either on a string or Date object.

@param dat [String, Date, Time] the object to be converted to Date @return [Date, DateTime]

# File lib/fat_core/date.rb, line 1795
def ensure_date(dat)
  case dat
  when String
    ::Date.parse(dat)
  when Date, DateTime
    dat
  when Time
    dat.to_date
  else
    raise ArgumentError, 'requires String, Date, DateTime, or Time'
  end
end
mo_name_to_num(name) click to toggle source

Return the 1-indexed integer that corresponds to a month name.

@param name [String] a name of a month

@return [Integer] the integer integer that corresponds to a month

name, or nil of no month recognized.
# File lib/fat_core/date.rb, line 1697
def mo_name_to_num(name)
  case name.clean
  when /\Ajan/i
    1
  when /\Afeb/i
    2
  when /\Amar/i
    3
  when /\Aapr/i
    4
  when /\Amay/i
    5
  when /\Ajun/i
    6
  when /\Ajul/i
    7
  when /\Aaug/i
    8
  when /\Asep/i
    9
  when /\Aoct/i
    10
  when /\Anov/i
    11
  when /\Adec/i
    12
  else
    nil
  end
end
nth_wday_in_year_month(nth, wday, year, month) click to toggle source

Return the nth weekday in the given month. If n is negative, count from last day of month.

@param nth [Integer] the ordinal number for the weekday @param wday [Integer] the weekday of interest with Monday 0 to Sunday 6 @param year [Integer] the year of interest @param month [Integer] the month of interest with January 1 to December 12

# File lib/fat_core/date.rb, line 1735
def nth_wday_in_year_month(nth, wday, year, month)
  wday = wday.to_i
  raise ArgumentError, 'illegal weekday number' if wday.negative? || wday > 6

  month = month.to_i
  raise ArgumentError, 'illegal month number' if month < 1 || month > 12

  nth = nth.to_i
  if nth.positive?
    # Set d to the 1st wday in month
    d = ::Date.new(year, month, 1)
    d += 1 while d.wday != wday
    # Set d to the nth wday in month
    nd = 1
    while nd != nth
      d += 7
      nd += 1
    end
    d
  elsif nth.negative?
    nth = -nth
    # Set d to the last wday in month
    d = ::Date.new(year, month, 1).end_of_month
    d -= 1 while d.wday != wday
    # Set d to the nth wday in month
    nd = 1
    while nd != nth
      d -= 7
      nd += 1
    end
    d
  else
    raise ArgumentError, 'Argument nth cannot be zero'
  end
end
parse_american(str) click to toggle source

@group Parsing

Convert a string str with an American style date into a ::Date object

An American style date is of the form `MM/DD/YYYY`, that is it places the month first, then the day of the month, and finally the year. The European convention is typically to place the day of the month first, `DD/MM/YYYY`. A date found in the wild can be ambiguous, e.g. 3/5/2014, but a date string known to be using the American convention can be parsed using this method. Both the month and the day can be a single digit. The year can be either 2 or 4 digits, and if given as 2 digits, it adds 2000 to it to give the year.

@example

::Date.parse_american('9/11/2001') #=> ::Date(2011, 9, 11)
::Date.parse_american('9/11/01')   #=> ::Date(2011, 9, 11)
::Date.parse_american('9/11/1')    #=> ArgumentError

@param str [String, to_s] a stringling of the form MM/DD/YYYY @return [::Date] the date represented by the str paramenter.

# File lib/fat_core/date.rb, line 1369
def parse_american(str)
  re = %r{\A\s*(\d\d?)\s*[-/]\s*(\d\d?)\s*[-/]\s*((\d\d)?\d\d)\s*\z}
  unless str.to_s =~ re
    raise ArgumentError, "date string must be of form 'MM?/DD?/YY(YY)?'"
  end

  year = $3.to_i
  month = $1.to_i
  day = $2.to_i
  year += 2000 if year < 100
  ::Date.new(year, month, day)
end
parse_spec(spec, spec_type = :from) click to toggle source

Convert a 'period spec' `spec` to a ::Date. A date spec is a short-hand way of specifying a calendar period either absolutely or relative to the computer clock. This method returns the first date of that period, when `spec_type` is set to `:from`, the default, and returns the last date of the period when `spec_type` is `:to`.

There are a number of forms the `spec` can take. In each case, `::Date.parse_spec` returns the first date in the period if `spec_type` is `:from` and the last date in the period if `spec_type` is `:to`:

  • `YYYY` is the whole year `YYYY`,

  • `YYYY-1H` or `YYYY-H1` is the first calendar half in year `YYYY`,

  • `H2` or `2H` is the second calendar half of the current year,

  • `YYYY-3Q` or `YYYY-Q3` is the third calendar quarter of year YYYY,

  • `Q3` or `3Q` is the third calendar quarter in the current year,

  • `YYYY-04` or `YYYY-4` is April, the fourth month of year `YYYY`,

  • `4-12` or `04-12` is the 12th of April in the current year,

  • `4` or `04` is April in the current year,

  • `YYYY-W32` or `YYYY-32W` is the 32nd week in year YYYY,

  • `W32` or `32W` is the 32nd week in the current year,

  • `YYYY-MM-DD` a particular date, so `:from` and `:to` return the same date,

  • `this_<chunk>` where `<chunk>` is one of `year`, `half`, `quarter`, `bimonth`, `month`, `semimonth`, `biweek`, `week`, or `day`, the corresponding calendar period in which the current date falls,

  • `last_<chunk>` where `<chunk>` is one of `year`, `half`, `quarter`, `bimonth`, `month`, `semimonth`, `biweek`, `week`, or `day`, the corresponding calendar period immediately before the one in which the current date falls,

  • `today` is the same as `this_day`,

  • `yesterday` is the same as `last_day`,

  • `forever` is the period from ::Date::BOT to ::Date::EOT, essentially all dates of commercial interest, and

  • `never` causes the method to return nil.

In all of the above example specs, letter used for calendar chunks, `W`, `Q`, and `H` can be written in lower case as well. Also, you can use `/` to separate date components instead of `-`.

@example

::Date.parse_spec('2012-W32').iso      # => "2012-08-06"
::Date.parse_spec('2012-W32', :to).iso # => "2012-08-12"
::Date.parse_spec('W32').iso           # => "2012-08-06" if executed in 2012
::Date.parse_spec('W32').iso           # => "2012-08-04" if executed in 2014

@param spec [String, to_s] the spec to be interpreted as a calendar period

@param spec_type [Symbol, :from, :to] return the first (:from) or last (:to)

date in the spec's period respectively

@return [::Date] date that is the first (:from) or last (:to) in the period

designated by spec
# File lib/fat_core/date.rb, line 1434
def parse_spec(spec, spec_type = :from)
  spec = spec.to_s.strip
  unless %i[from to].include?(spec_type)
    raise ArgumentError, "invalid date spec type: '#{spec_type}'"
  end

  today = ::Date.current
  case spec.clean
  when %r{\A(?<yr>\d\d\d\d)[-/](?<mo>\d\d?)[-/](?<dy>\d\d?)\z}
    # A specified date
    ::Date.new(Regexp.last_match[:yr].to_i, Regexp.last_match[:mo].to_i,
               Regexp.last_match[:dy].to_i)
  when /\AW(?<wk>\d\d?)\z/, /\A(?<wk>\d\d?)W\z/
    week_num = Regexp.last_match[:wk].to_i
    if week_num < 1 || week_num > 53
      raise ArgumentError, "invalid week number (1-53): '#{spec}'"
    end

    if spec_type == :from
      ::Date.commercial(today.year, week_num).beginning_of_week
    else
      ::Date.commercial(today.year, week_num).end_of_week
    end
  when %r{\A(?<yr>\d\d\d\d)[-/]W(?<wk>\d\d?)\z}, %r{\A(?<yr>\d\d\d\d)[-/](?<wk>\d\d?)W\z}
    year = Regexp.last_match[:yr].to_i
    week_num = Regexp.last_match[:wk].to_i
    if week_num < 1 || week_num > 53
      raise ArgumentError, "invalid week number (1-53): '#{spec}'"
    end

    if spec_type == :from
      ::Date.commercial(year, week_num).beginning_of_week
    else
      ::Date.commercial(year, week_num).end_of_week
    end
  when %r{^(?<yr>\d\d\d\d)[-/](?<qt>\d)[Qq]$}, %r{^(?<yr>\d\d\d\d)[-/][Qq](?<qt>\d)$}
    # Year-Quarter
    year = Regexp.last_match[:yr].to_i
    quarter = Regexp.last_match[:qt].to_i
    unless [1, 2, 3, 4].include?(quarter)
      raise ArgumentError, "invalid quarter number (1-4): '#{spec}'"
    end

    month = quarter * 3
    if spec_type == :from
      ::Date.new(year, month, 1).beginning_of_quarter
    else
      ::Date.new(year, month, 1).end_of_quarter
    end
  when /^(?<qt>[1234])[qQ]$/, /^[qQ](?<qt>[1234])$/
    # Quarter only
    this_year = today.year
    quarter = Regexp.last_match[:qt].to_i
    unless [1, 2, 3, 4].include?(quarter)
      raise ArgumentError, "invalid quarter number (1-4): '#{spec}'"
    end

    date = ::Date.new(this_year, quarter * 3, 15)
    if spec_type == :from
      date.beginning_of_quarter
    else
      date.end_of_quarter
    end
  when %r{^(?<yr>\d\d\d\d)[-/](?<hf>\d)[Hh]$}, %r{^(?<yr>\d\d\d\d)[-/][Hh](?<hf>\d)$}
    # Year-Half
    year = Regexp.last_match[:yr].to_i
    half = Regexp.last_match[:hf].to_i
    msg = "invalid half number: '#{spec}'"
    raise ArgumentError,  msg unless [1, 2].include?(half)

    month = half * 6
    if spec_type == :from
      ::Date.new(year, month, 15).beginning_of_half
    else
      ::Date.new(year, month, 1).end_of_half
    end
  when /^(?<hf>[12])[hH]$/, /^[hH](?<hf>[12])$/
    # Half only
    this_year = today.year
    half = Regexp.last_match[:hf].to_i
    msg = "invalid half number: '#{spec}'"
    raise ArgumentError, msg unless [1, 2].include?(half)

    date = ::Date.new(this_year, half * 6, 15)
    if spec_type == :from
      date.beginning_of_half
    else
      date.end_of_half
    end
  when %r{^(?<yr>\d\d\d\d)[-/](?<mo>\d\d?)*$}
    # Year-Month only
    year = Regexp.last_match[:yr].to_i
    month = Regexp.last_match[:mo].to_i
    unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
      raise ArgumentError, "invalid month number (1-12): '#{spec}'"
    end

    if spec_type == :from
      ::Date.new(year, month, 1)
    else
      ::Date.new(year, month, 1).end_of_month
    end
  when %r{^(?<mo>\d\d?)[-/](?<dy>\d\d?)*$}
    # Month-Day only
    month = Regexp.last_match[:mo].to_i
    day = Regexp.last_match[:dy].to_i
    unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
      raise ArgumentError, "invalid month number (1-12): '#{spec}'"
    end

    if spec_type == :from
      ::Date.new(today.year, month, day)
    else
      ::Date.new(today.year, month, day).end_of_month
    end
  when /\A(?<mo>\d\d?)\z/
    # Month only
    month = Regexp.last_match[:mo].to_i
    unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
      raise ArgumentError, "invalid month number (1-12): '#{spec}'"
    end

    if spec_type == :from
      ::Date.new(today.year, month, 1)
    else
      ::Date.new(today.year, month, 1).end_of_month
    end
  when /^(?<yr>\d\d\d\d)$/
    # Year only
    year = Regexp.last_match[:yr].to_i
    if spec_type == :from
      ::Date.new(year, 1, 1)
    else
      ::Date.new(year, 12, 31)
    end
  when /^(to|this_?)?day/
    today
  when /^(yester|last_?)?day/
    today - 1.day
  when /^(this_?)?week/
    spec_type == :from ? today.beginning_of_week : today.end_of_week
  when /last_?week/
    if spec_type == :from
      (today - 1.week).beginning_of_week
    else
      (today - 1.week).end_of_week
    end
  when /^(this_?)?biweek/
    if spec_type == :from
      today.beginning_of_biweek
    else
      today.end_of_biweek
    end
  when /last_?biweek/
    if spec_type == :from
      (today - 2.week).beginning_of_biweek
    else
      (today - 2.week).end_of_biweek
    end
  when /^(this_?)?semimonth/
    spec_type == :from ? today.beginning_of_semimonth : today.end_of_semimonth
  when /^last_?semimonth/
    if spec_type == :from
      (today - 15.days).beginning_of_semimonth
    else
      (today - 15.days).end_of_semimonth
    end
  when /^(this_?)?month/
    if spec_type == :from
      today.beginning_of_month
    else
      today.end_of_month
    end
  when /^last_?month/
    if spec_type == :from
      (today - 1.month).beginning_of_month
    else
      (today - 1.month).end_of_month
    end
  when /^(this_?)?bimonth/
    if spec_type == :from
      today.beginning_of_bimonth
    else
      today.end_of_bimonth
    end
  when /^last_?bimonth/
    if spec_type == :from
      (today - 2.month).beginning_of_bimonth
    else
      (today - 2.month).end_of_bimonth
    end
  when /^(this_?)?quarter/
    if spec_type == :from
      today.beginning_of_quarter
    else
      today.end_of_quarter
    end
  when /^last_?quarter/
    if spec_type == :from
      (today - 3.months).beginning_of_quarter
    else
      (today - 3.months).end_of_quarter
    end
  when /^(this_?)?half/
    if spec_type == :from
      today.beginning_of_half
    else
      today.end_of_half
    end
  when /^last_?half/
    if spec_type == :from
      (today - 6.months).beginning_of_half
    else
      (today - 6.months).end_of_half
    end
  when /^(this_?)?year/
    if spec_type == :from
      today.beginning_of_year
    else
      today.end_of_year
    end
  when /^last_?year/
    if spec_type == :from
      (today - 1.year).beginning_of_year
    else
      (today - 1.year).end_of_year
    end
  when /^forever/
    if spec_type == :from
      ::Date::BOT
    else
      ::Date::EOT
    end
  when /^never/
    nil
  else
    raise ArgumentError, "bad date spec: '#{spec}''"
  end
end