diff --git a/data/holidays.csv b/data/holidays.csv new file mode 100644 index 0000000..89aba3d --- /dev/null +++ b/data/holidays.csv @@ -0,0 +1,9 @@ +month, region, mday, wday, week, name +1, us, 1, , , 'New Year\'s Day' +1, ca, 1, , , 'New Year\'s Day' +1, au, 1, , , 'New Year\'s Day' +1, christian, 6, , , 'Epiphany' +1, us, , 1, 3, 'Martin Luther King, Jr. Day' +3, us, , 1, 3, 'George Washington\'s Birthday' +3, gr, 25, , , 'Independence Day' +4, au, 25, , , 'ANZAC Day' diff --git a/lib/holidays.rb b/lib/holidays.rb index c0e86c9..50f0f26 100644 --- a/lib/holidays.rb +++ b/lib/holidays.rb @@ -20,36 +20,42 @@ module Holidays # :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]}, - {:mday => 6, :name => 'Epiphany Day', :regions => [:gr]}, + 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]}], - 7 => [{:mday => 4, :name => 'Independence 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, :christ]}], + 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]}, + {: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,:christ]}, - {:mday => 26, :name => 'Boxing Day', :regions => [:ca,:gr]}] + 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.lookup_holidays(date, regions = [:ca, :us]) + 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 = [] @@ -68,7 +74,7 @@ module Holidays holidays << h elsif h[:wday] == wday # by week calculation - if calculate_mday(year, month, h[:week], h[:wday]) == mday + if Date.calculate_mday(year, month, h[:week], h[:wday]) == mday holidays << h end end @@ -77,6 +83,62 @@ module Holidays 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) @@ -94,6 +156,21 @@ module Holidays # 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 @@ -107,43 +184,23 @@ module Holidays #-- # 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) + raise ArgumentError, "Week paramater must be one of Holidays::WEEKS." unless WEEKS.include?(week) - nth = WEEKS[week] + week = WEEKS[week] - if nth > 0 - return (nth-1)*7 + 1 + (7 + wday - Date.civil(year, month,(nth-1)*7 + 1).wday)%7 + # :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] - if month == 2 and Date.civil(year,1,1).leap? + 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 + 8)%7; + return days - ((Date.civil(year, month, days).wday - wday + 7) % 7) end - def self.holidays_in_range(from, to, regions = [:ca,:us]) - regions = [regions] unless regions.kind_of?(Array) - holidays = [] - - end -end - -class Date - include Holidays - - # Date.civil('2008-01-01').is_holiday?(:us) - def is_holiday?(region = 'us') - region = region.to_sym - - holidays = Holidays.lookup_holidays(self, region) - if holidays - return true - else - return false - end - end end \ No newline at end of file diff --git a/rakefile.rb b/rakefile.rb index 30bbd2b..a12d0fe 100644 --- a/rakefile.rb +++ b/rakefile.rb @@ -13,3 +13,37 @@ Rake::TestTask.new do |t| t.verbose = false end +desc 'Generate documentation.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.title = 'Ruby Holidays Gem' + rdoc.options << '--all' << '--inline-source' << '--line-numbers' + rdoc.rdoc_files.include('README') + #rdoc.rdoc_files.include('LICENSE') + rdoc.rdoc_files.include('lib/*.rb') + #rdoc.rdoc_files.include('lib/holidays/*.rb') +end + + +spec = Gem::Specification.new do |s| + s.name = "holidays" + s.version = "0.9.0" + s.author = "Alex Dunae" + s.homepage = "http://code.dunae.ca/holidays" + s.platform = Gem::Platform::RUBY + s.description = <<-EOF + A collection of Ruby methods to deal with statutory and other holidays. You deserve a holiday! + EOF + s.summary = "A collection of Ruby methods to deal with statutory and other holidays. You deserve a holiday!" + s.files = FileList["{lib}/**/*"].to_a + s.test_files = Dir.glob('test/test_*.rb') + s.has_rdoc = true + s.extra_rdoc_files = ["README", "LICENSE"] + s.rdoc_options << '--all' << '--inline-source' << '--line-numbers' +end + +desc 'Build the gem.' +Rake::GemPackageTask.new(spec) do |pkg| + pkg.need_zip = true + pkg.need_tar = true +end \ No newline at end of file diff --git a/test/test_holidays.rb b/test/test_holidays.rb index 335fa77..386f475 100644 --- a/test/test_holidays.rb +++ b/test/test_holidays.rb @@ -11,57 +11,85 @@ class HolidayTests < Test::Unit::TestCase assert @date.respond_to?('is_holiday?') end - def test_calculating_mdays - assert_equal 21, Holidays.calculate_mday(2008, 1, :third, 1) - assert_equal 1, Holidays.calculate_mday(2007, 1, :first, 1) - assert_equal 2, Holidays.calculate_mday(2007, 3, :first, 5) - assert_equal 25, Holidays.calculate_mday(2008, 5, :last, 1) + def test_date_ranges + holidays = Holidays.between(Date.civil(2008,1,1), Date.civil(2008,12,31), :au) + holidays.each do |h| + #puts h.inspect + end + end + + def test_easter_dates + assert_equal '1800-04-13', Holidays.easter(1800).to_s + assert_equal '1899-04-02', Holidays.easter(1899).to_s + assert_equal '1900-04-15', Holidays.easter(1900).to_s + assert_equal '1999-04-04', Holidays.easter(1999).to_s + assert_equal '2000-04-23', Holidays.easter(2000).to_s + assert_equal '2025-04-20', Holidays.easter(2025).to_s + assert_equal '2035-03-25', Holidays.easter(2035).to_s + assert_equal '2067-04-03', Holidays.easter(2067).to_s + assert_equal '2099-04-12', Holidays.easter(2099).to_s + end + + + def test_calculating_mdays + # US Memorial day + assert_equal 29, Date.calculate_mday(2006, 5, :last, 1) + assert_equal 28, Date.calculate_mday(2007, 5, :last, 1) + assert_equal 26, Date.calculate_mday(2008, 5, :last, 1) + assert_equal 25, Date.calculate_mday(2009, 5, :last, 1) + assert_equal 31, Date.calculate_mday(2010, 5, :last, 1) + assert_equal 30, Date.calculate_mday(2011, 5, :last, 1) # Labour day - assert_equal 3, Holidays.calculate_mday(2007, 9, :first, 1) - assert_equal 1, Holidays.calculate_mday(2008, 9, :first, 1) - assert_equal 7, Holidays.calculate_mday(2009, 9, :first, 1) - assert_equal 5, Holidays.calculate_mday(2011, 9, :first, 1) - assert_equal 5, Holidays.calculate_mday(2050, 9, :first, 1) - assert_equal 4, Holidays.calculate_mday(2051, 9, :first, 1) + assert_equal 3, Date.calculate_mday(2007, 9, :first, 1) + assert_equal 1, Date.calculate_mday(2008, 9, :first, 1) + assert_equal 7, Date.calculate_mday(2009, 9, :first, 1) + assert_equal 5, Date.calculate_mday(2011, 9, :first, 1) + assert_equal 5, Date.calculate_mday(2050, 9, :first, 1) + assert_equal 4, Date.calculate_mday(2051, 9, :first, 1) # Canadian thanksgiving - assert_equal 8, Holidays.calculate_mday(2007, 10, :second, 1) - assert_equal 13, Holidays.calculate_mday(2008, 10, :second, 1) - assert_equal 12, Holidays.calculate_mday(2009, 10, :second, 1) - assert_equal 11, Holidays.calculate_mday(2010, 10, :second, 1) - + assert_equal 8, Date.calculate_mday(2007, 10, :second, 1) + assert_equal 13, Date.calculate_mday(2008, 10, :second, 1) + assert_equal 12, Date.calculate_mday(2009, 10, :second, 1) + assert_equal 11, Date.calculate_mday(2010, 10, :second, 1) + + # Misc + assert_equal 21, Date.calculate_mday(2008, 1, :third, 1) + assert_equal 1, Date.calculate_mday(2007, 1, :first, 1) + assert_equal 2, Date.calculate_mday(2007, 3, :first, 5) + end def test_region_params - holidays = Holidays.lookup_holidays(@date, :us) + holidays = Holidays.by_day(@date, :us) assert_equal 1, holidays.length - holidays = Holidays.lookup_holidays(@date, [:us,:ca]) + holidays = Holidays.by_day(@date, [:us,:ca]) assert_equal 1, holidays.length end - def test_lookup_holidays_spot_checks - h = Holidays.lookup_holidays(Date.civil(2008,5,1), :gr) + def test_by_day_spot_checks + h = Holidays.by_day(Date.civil(2008,5,1), :gr) assert_equal 'Labour Day', h[0][:name] - h = Holidays.lookup_holidays(Date.civil(2045,11,1), :fr) + h = Holidays.by_day(Date.civil(2045,11,1), :fr) assert_equal 'Touissant', h[0][:name] end - def test_lookup_holidays_and_iterate - holidays = Holidays.lookup_holidays(@date, :ca) + def test_by_day_and_iterate + holidays = Holidays.by_day(@date, :ca) holidays.each do |h| - puts h[:name] + #puts h[:name] end end def test_lookup_holiday - holidays = Holidays.lookup_holidays(Date.civil(2008,1,21), :ca) + holidays = Holidays.by_day(Date.civil(2008,1,21), :ca) assert_equal 0, holidays.length - holidays = Holidays.lookup_holidays(Date.civil(2008,1,21), :us) + holidays = Holidays.by_day(Date.civil(2008,1,21), :us) assert_equal 1, holidays.length end