Added support for lambdas in holiday definitions
Separated Easter definitions and tests Preliminary support for definition builder
This commit is contained in:
parent
aeee76cadf
commit
d98a443337
8 changed files with 187 additions and 61 deletions
55
create_defs.rb
Normal file
55
create_defs.rb
Normal file
|
@ -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
|
4
data/us.csv
Normal file
4
data/us.csv
Normal file
|
@ -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"
|
|
|
@ -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 <tt>0</tt> 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
|
|||
# [<tt>:regions</tt>] An array of region symbols.
|
||||
# [<tt>:types</tt>] 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]
|
||||
|
||||
|
|
30
lib/holidays/easter.rb
Normal file
30
lib/holidays/easter.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
||||
|
||||
|
|
29
test/test_easter.rb
Normal file
29
test/test_easter.rb
Normal file
|
@ -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
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
@ -59,7 +44,34 @@ class HolidayTests < Test::Unit::TestCase
|
|||
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_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
|
||||
|
|
Loading…
Reference in a new issue