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