diff --git a/create_defs.rb b/create_defs.rb new file mode 100644 index 0000000..a3eea61 --- /dev/null +++ b/create_defs.rb @@ -0,0 +1,55 @@ +require 'fastercsv' + +# ==== Rows +# 0 -> month +# 1 -> region +# 2 -> mday +# 3 -> wday +# 4 -> week +# 5 -> name + + +rules_by_month = {} + +FasterCSV.foreach('data/us.csv', {:headers => :first_row, :return_headers => false, :force_quotes => true}) do |row| + month = row['month'].to_i + rules_by_month[month] = [] unless rules_by_month[month] + + rule = {} + row.each do |key, val| + rule[key] = val + end + + rules_by_month[month] << rule +end + +out = "# This file is generated by one of the Holiday gem's Rake tasks.\n" +out << "HOLIDAYS_BY_MONTH = {\n" + + +month_strs = [] +rules_by_month.each do |month, rules| + month_str = " #{month.to_s} => [" + rule_strings = [] + rules.each do |rule| + str = '{' + if rule['mday'] + str << ":mday => #{rule['mday']}, " + else + str << ":wday => #{rule['wday']}, :week => #{rule['week']}, " + end + + str << ":name => \"#{rule['name']}\", :regions => [:#{rule['region']}]}" + rule_strings << str + end + month_str << rule_strings.join(",\n ") + "]" + month_strs << month_str +end + +month_strs.join(",\n") + +out << month_strs.join(",\n") + "\n}" + +File.open("test_file.rb","w") do |file| + file.puts out +end \ No newline at end of file diff --git a/data/us.csv b/data/us.csv new file mode 100644 index 0000000..11fc43d --- /dev/null +++ b/data/us.csv @@ -0,0 +1,4 @@ +month,region,mday,wday,week,name +1,us,1,,,"New Year's Day" +1,us,,1,3,"Martin Luther King, Jr. Day" +3,us,,1,3,"George Washington's Birthday" diff --git a/lib/holidays.rb b/lib/holidays.rb index cdba993..d358c03 100644 --- a/lib/holidays.rb +++ b/lib/holidays.rb @@ -1,17 +1,28 @@ +$:.unshift File.dirname(__FILE__) +require 'holidays/easter' + module Holidays # Exception thrown when an unknown region is encountered. class UnkownRegionError < ArgumentError; end + self.extend Easter + VERSION = '0.9.0' - REGIONS = [:ca, :us, :au, :gr, :fr] + REGIONS = [:ca, :us, :au, :gr, :fr, :christian] HOLIDAYS_TYPES = [:bank, :statutory, :religious, :informal] 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) + # :function Can return an integer representing mday or a Date object. + # + # The month 0 is used to store events that require lambda calculations + # and may occur in more than one month. HOLIDAYS_BY_MONTH = { + 0 => [{:function => lambda { |year| easter(year) }, :name => 'Easter Sunday', :regions => [:christian, :ca, :us]}, + {:function => lambda { |year| easter(year)-2 }, :name => 'Good Friday', :regions => [:christian, :ca, :us]}], 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]}, @@ -24,12 +35,11 @@ module Holidays {: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]} - ], + {: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]}], + 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]}, @@ -58,11 +68,9 @@ module Holidays # [:regions] An array of region symbols. # [:types] An array of holiday-type symbols. def self.by_day(date, regions = [:ca, :us]) - #raise(UnkownRegionError, "No holiday information is available for region '#{region}'") unless known_region?(region) + regions = validate_regions(regions) - regions = [regions] unless regions.kind_of?(Array) - - hbm = HOLIDAYS_BY_MONTH[date.mon] + hbm = HOLIDAYS_BY_MONTH.values_at(0,date.mon).flatten holidays = [] @@ -83,9 +91,16 @@ module Holidays if Date.calculate_mday(year, month, h[:week], h[:wday]) == mday holidays << h end + elsif h[:function] + result = h[:function].call(year) + if result.kind_of?(Date) and result.mon == month and result.mday == mday + holidays << h + elsif result == mday + holidays << h + end + end end - holidays end @@ -96,7 +111,7 @@ module Holidays #-- # TODO: do not take full months def self.between(start_date, end_date, regions = [:ca,:us]) - regions = [regions] unless regions.kind_of?(Array) + regions = validate_regions(regions) holidays = [] dates = {} @@ -112,48 +127,26 @@ module Holidays 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]} + unless h[:function] + 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 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 - - private # Check regions against list of supported regions and return an array of # symbols. - def self.check_regions(regions) # :nodoc: + def self.validate_regions(regions) # :nodoc: regions = [regions] unless regions.kind_of?(Array) regions = regions.collect { |r| r.to_sym } + raise UnkownRegionError unless regions.all? { |r| r == :any or REGIONS.include?(r) } + regions end @@ -169,7 +162,7 @@ class Date # => true def is_holiday?(regions = :any) holidays = Holidays.by_day(self, regions) - holidays.empty? + !holidays.empty? end # Calculate day of the month based on the week number and the day of the @@ -197,7 +190,7 @@ class Date #-- # see http://www.irt.org/articles/js050/index.htm def self.calculate_mday(year, month, week, wday) - raise ArgumentError, "Week parameter must be one of Holidays::WEEKS." unless WEEKS.include?(week) + raise ArgumentError, "Week parameter must be one of Holidays::WEEKS (provided #{week})." unless WEEKS.include?(week) week = WEEKS[week] diff --git a/lib/holidays/easter.rb b/lib/holidays/easter.rb new file mode 100644 index 0000000..b3918d4 --- /dev/null +++ b/lib/holidays/easter.rb @@ -0,0 +1,30 @@ +module Holidays + module Easter + # Get the date of Easter in a given year. + # + # +year+ must be a valid Gregorian year. + # + # Returns a Date object. + #-- + # from http://snippets.dzone.com/posts/show/765 + # TODO: check year to ensure Gregorian + def 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 + end +end diff --git a/rakefile.rb b/rakefile.rb index 3f044a6..9968700 100644 --- a/rakefile.rb +++ b/rakefile.rb @@ -4,6 +4,7 @@ require 'rake/rdoctask' require 'rake/gempackagetask' require 'fileutils' require 'lib/holidays' +require 'csv' desc 'Run the unit tests.' Rake::TestTask.new do |t| @@ -13,6 +14,7 @@ Rake::TestTask.new do |t| t.verbose = false end + desc 'Generate documentation.' Rake::RDocTask.new(:rdoc) do |rdoc| rdoc.rdoc_dir = 'doc' @@ -22,7 +24,7 @@ Rake::RDocTask.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('REFERENCES') rdoc.rdoc_files.include('LICENSE') rdoc.rdoc_files.include('lib/*.rb') - #rdoc.rdoc_files.include('lib/holidays/*.rb') + rdoc.rdoc_files.include('lib/holidays/*.rb') end diff --git a/test/test_easter.rb b/test/test_easter.rb new file mode 100644 index 0000000..77189d7 --- /dev/null +++ b/test/test_easter.rb @@ -0,0 +1,29 @@ +require File.dirname(__FILE__) + '/test_helper' + +class HolidaysTests < Test::Unit::TestCase + 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_easter_lambda + [Date.civil(1800,4,13), Date.civil(1899,4,2), Date.civil(1900,4,15), + Date.civil(2008,3,23), Date.civil(2035,3,25)].each do |date| + assert_equal 'Easter Sunday', Holidays.by_day(date, :christian)[0][:name] + end + end + + def test_good_friday_lambda + [Date.civil(1800,4,11), Date.civil(1899,3,31), Date.civil(1900,4,13), + Date.civil(2008,3,21), Date.civil(2035,3,23)].each do |date| + assert_equal 'Good Friday', Holidays.by_day(date, :christian)[0][:name] + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 9cb966b..d5fa452 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,4 +2,5 @@ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__), '../')) $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__), '../lib/')) require 'rubygems' require 'test/unit' +require 'date' require 'holidays' diff --git a/test/test_holidays.rb b/test/test_holidays.rb index 386f475..e4339f5 100644 --- a/test/test_holidays.rb +++ b/test/test_holidays.rb @@ -1,7 +1,6 @@ require File.dirname(__FILE__) + '/test_helper' -require 'date' -# Test cases for reading and generating CSS shorthand properties -class HolidayTests < Test::Unit::TestCase + +class HolidaysTests < Test::Unit::TestCase def setup @date = Date.civil(2008,1,1) @@ -16,22 +15,8 @@ class HolidayTests < Test::Unit::TestCase 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) @@ -58,8 +43,35 @@ class HolidayTests < Test::Unit::TestCase # 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) + assert_equal 2, Date.calculate_mday(2007, 3, :first, 5) + end + def test_requires_valid_week + assert_raises ArgumentError do + Date.calculate_mday(2008, 1, :none, 1) + end + + assert_raises ArgumentError do + Date.calculate_mday(2008, 1, nil, 1) + end + + assert_raises ArgumentError do + Date.calculate_mday(2008, 1, 0, 1) + end + end + + def test_requires_valid_regions + assert_raises Holidays::UnkownRegionError do + Holidays.by_day(Date.civil(2008,1,1), :xx) + end + + assert_raises Holidays::UnkownRegionError do + Holidays.by_day(Date.civil(2008,1,1), [:ca,:xx]) + end + + assert_raises Holidays::UnkownRegionError do + Holidays.between(Date.civil(2008,1,1), Date.civil(2008,12,31), [:ca,:xx]) + end end def test_region_params