holidays/lib/holidays.rb
Alex Dunae 3260cd8be2 Added Easter calculation
Began moving holidays into data/holidays.csv
2007-11-20 04:06:48 +00:00

206 lines
6.8 KiB
Ruby

# === References
# ==== Calculations
# * http://michaelthompson.org/technikos/holidays.php
# ==== World
# * http://en.wikipedia.org/wiki/List_of_holidays_by_country
# ==== US
# * http://www.opm.gov/Operating_Status_Schedules/fedhol/index.asp
# * http://www.smart.net/~mmontes/ushols.html
module Holidays
# Exception class for dealing with unknown regions.
class UnkownRegionError < StandardError; end
VERSION = '1.0.0'
HOLIDAY_REGIONS = {:ca => 'Canada', :us => 'United States'}
WEEKS = {:first => 1, :second => 2, :third => 3, :fourth => 4, :fifth => 5, :last => -1}
MONTH_LENGTHS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
# :wday: Day of the week (0 is Sunday, 6 is Saturday)
HOLIDAYS_BY_MONTH = {
1 => [{:mday => 1, :name => 'New Year\'s Day', :regions => [:us, :ca, :au]},
{:mday => 1, :name => 'Australia Day', :regions => [:au]},
{:mday => 6, :name => 'Epiphany', :regions => [:christian]},
{:wday => 1, :week => :third, :name => 'Martin Luther King, Jr. Day', :regions => [:us]}],
3 => [{:wday => 1, :week => :third, :name => 'George Washington\'s Birthday', :regions => [:us]},
{:mday => 25, :name => 'Independence Day', :regions => [:gr]}],
4 => [{:mday => 25, :name => 'ANZAC Day', :regions => [:au]}],
5 => [{:mday => 1, :name => 'Labour Day', :regions => [:fr,:gr]},
{:mday => 8, :name => 'Victoria 1945', :regions => [:fr]},
{:wday => 6, :week => :third, :name => 'Armed Forces Day', :regions => [:us]},
{:wday => 1, :week => :last, :name => 'Memorial Day', :regions => [:us]}],
6 => [{:mday => 14, :name => 'Flag Day', :regions => [:us]},
{:wday => 1, :week => :second, :name => 'Queen\'s Birthday', :regions => [:au]}
],
7 => [{:mday => 1, :name => 'Canada Day', :regions => [:ca]},
{:mday => 4, :name => 'Independence Day', :regions => [:us]},
{:mday => 14, :name => 'Ascension Day', :regions => [:fr]}],
8 => [{:mday => 15, :name => 'Assumption of Mary', :regions => [:fr, :gr, :christian]}],
9 => [{:wday => 1, :week => :first,:name => 'Labor Day', :regions => [:us]},
{:wday => 1, :week => :first,:name => 'Labour Day', :regions => [:ca]}],
10 => [{:wday => 1, :week => :second, :name => 'Columbus Day', :regions => [:us]},
{:mday => 28, :name => 'National Day', :regions => [:gr]}],
11 => [{:wday => 4, :week => :fourth, :name => 'Thanksgiving Day', :regions => [:us]},
{:mday => 11, :name => 'Rememberance Day', :regions => [:ca,:au]},
{:mday => 11, :name => 'Armistice 1918', :regions => [:fr]},
{:mday => 1, :name => 'Touissant', :regions => [:fr]}],
12 => [{:mday => 25, :name => 'Christmas Day', :regions => [:us,:ca,:christian,:au]},
{:mday => 26, :name => 'Boxing Day', :regions => [:ca,:gr,:au]}]
}
# Get all holidays on a certain date
def self.by_day(date, regions = [:ca, :us])
#raise(UnkownRegionError, "No holiday information is available for region '#{region}'") unless known_region?(region)
regions = [regions] unless regions.kind_of?(Array)
hbm = HOLIDAYS_BY_MONTH[date.mon]
holidays = []
year = date.year
month = date.month
mday = date.mday
wday = date.wday
hbm.each do |h|
# start with the region check
next unless h[:regions].any?{ |reg| regions.include?(reg) }
if h[:mday] and h[:mday] == mday
# fixed day of the month
holidays << h
elsif h[:wday] == wday
# by week calculation
if Date.calculate_mday(year, month, h[:week], h[:wday]) == mday
holidays << h
end
end
end
holidays
end
#--
# TODO: do not take full months
def self.between(start_date, end_date, regions = [:ca,:us])
regions = [regions] unless regions.kind_of?(Array)
holidays = []
dates = {}
(start_date..end_date).each do |date|
dates[date.year] = Array.new unless dates[date.year]
# TODO: test this, maybe should push then flatten
dates[date.year] << date.month unless dates[date.year].include?(date.month)
end
dates.each do |year, months|
months.each do |month|
next unless hbm = HOLIDAYS_BY_MONTH[month]
hbm.each do |h|
next unless h[:regions].any?{ |reg| regions.include?(reg) }
day = h[:mday] || Date.calculate_mday(year, month, h[:week], h[:wday])
holidays << {:month => month, :day => day, :year => year, :name => h[:name], :regions => h[:regions]}
end
end
end
#puts dates.inspect
holidays
end
# Get the date of Easter in a given year.
#
# +year+ must be a valid Gregorian year.
#--
# from http://snippets.dzone.com/posts/show/765
# TODO: check year to ensure Gregorian
def self.easter(year)
y = year
a = y % 19
b = y / 100
c = y % 100
d = b / 4
e = b % 4
f = (b + 8) / 25
g = (b - f + 1) / 3
h = (19 * a + b - d - g + 15) % 30
i = c / 4
k = c % 4
l = (32 + 2 * e + 2 * i - h - k) % 7
m = (a + 11 * h + 22 * l) / 451
month = (h + l - 7 * m + 114) / 31
day = ((h + l - 7 * m + 114) % 31) + 1
Date.civil(year, month, day)
end
#def self.calculate_mday(yr, mo, wday, int)
# earliest = 1 + 7 * (int - 1)
# wd = Date.civil(yr, mo, earliest).wday
# if wday == earliest
# off = 0
# else
# if wday < wd
# off = wday + (7 - wd)
# else
# off = (wday + (7 - wd)) - 7
# end
# end
# earliest + off
# end
end
class Date
include Holidays
# Check if the current date is a holiday.
#
# Date.civil('2008-01-01').is_holiday?(:ca)
# => true
def is_holiday?(regions = [:ca, :us])
holidays = Holidays.by_day(self, regions)
holidays.empty?
end
# Calculate the day of the month based on week and day of the week.
#
# First Monday of Jan, 2008
# calculate_mday(2008, 1, :first, :monday)
#
# Third Thursday of Dec, 2008
# calculate_mday(2008, 12, :third, 4)
#
# Last Monday of Jan, 2008
# calculate_mday(2008, 1, :last, 1)
#--
# see http://www.irt.org/articles/js050/index.htm
def self.calculate_mday(year, month, week, wday)
raise ArgumentError, "Week paramater must be one of Holidays::WEEKS." unless WEEKS.include?(week)
week = WEEKS[week]
# :first, :second, :third, :fourth or :fifth
if week > 0
return ((week - 1) * 7) + 1 + ((7 + wday - Date.civil(year, month,(week-1)*7 + 1).wday) % 7)
end
days = MONTH_LENGTHS[month-1]
if month == 1 and Date.civil(year,1,1).leap?
days = 29
end
return days - ((Date.civil(year, month, days).wday - wday + 7) % 7)
end
end