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
|
module Holidays
|
||||||
# Exception thrown when an unknown region is encountered.
|
# Exception thrown when an unknown region is encountered.
|
||||||
class UnkownRegionError < ArgumentError; end
|
class UnkownRegionError < ArgumentError; end
|
||||||
|
|
||||||
|
self.extend Easter
|
||||||
|
|
||||||
VERSION = '0.9.0'
|
VERSION = '0.9.0'
|
||||||
|
|
||||||
REGIONS = [:ca, :us, :au, :gr, :fr]
|
REGIONS = [:ca, :us, :au, :gr, :fr, :christian]
|
||||||
HOLIDAYS_TYPES = [:bank, :statutory, :religious, :informal]
|
HOLIDAYS_TYPES = [:bank, :statutory, :religious, :informal]
|
||||||
WEEKS = {:first => 1, :second => 2, :third => 3, :fourth => 4, :fifth => 5, :last => -1}
|
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]
|
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)
|
# :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 = {
|
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]},
|
1 => [{:mday => 1, :name => 'New Year\'s Day', :regions => [:us, :ca, :au]},
|
||||||
{:mday => 1, :name => 'Australia Day', :regions => [:au]},
|
{:mday => 1, :name => 'Australia Day', :regions => [:au]},
|
||||||
{:mday => 6, :name => 'Epiphany', :regions => [:christian]},
|
{:mday => 6, :name => 'Epiphany', :regions => [:christian]},
|
||||||
|
@ -24,8 +35,7 @@ module Holidays
|
||||||
{:wday => 6, :week => :third, :name => 'Armed Forces Day', :regions => [:us]},
|
{:wday => 6, :week => :third, :name => 'Armed Forces Day', :regions => [:us]},
|
||||||
{:wday => 1, :week => :last, :name => 'Memorial Day', :regions => [:us]}],
|
{:wday => 1, :week => :last, :name => 'Memorial Day', :regions => [:us]}],
|
||||||
6 => [{:mday => 14, :name => 'Flag 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]},
|
7 => [{:mday => 1, :name => 'Canada Day', :regions => [:ca]},
|
||||||
{:mday => 4, :name => 'Independence Day', :regions => [:us]},
|
{:mday => 4, :name => 'Independence Day', :regions => [:us]},
|
||||||
{:mday => 14, :name => 'Ascension Day', :regions => [:fr]}],
|
{:mday => 14, :name => 'Ascension Day', :regions => [:fr]}],
|
||||||
|
@ -58,11 +68,9 @@ module Holidays
|
||||||
# [<tt>:regions</tt>] An array of region symbols.
|
# [<tt>:regions</tt>] An array of region symbols.
|
||||||
# [<tt>:types</tt>] An array of holiday-type symbols.
|
# [<tt>:types</tt>] An array of holiday-type symbols.
|
||||||
def self.by_day(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 = validate_regions(regions)
|
||||||
|
|
||||||
regions = [regions] unless regions.kind_of?(Array)
|
hbm = HOLIDAYS_BY_MONTH.values_at(0,date.mon).flatten
|
||||||
|
|
||||||
hbm = HOLIDAYS_BY_MONTH[date.mon]
|
|
||||||
|
|
||||||
holidays = []
|
holidays = []
|
||||||
|
|
||||||
|
@ -83,9 +91,16 @@ module Holidays
|
||||||
if Date.calculate_mday(year, month, h[:week], h[:wday]) == mday
|
if Date.calculate_mday(year, month, h[:week], h[:wday]) == mday
|
||||||
holidays << h
|
holidays << h
|
||||||
end
|
end
|
||||||
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
|
||||||
|
end
|
||||||
holidays
|
holidays
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -96,7 +111,7 @@ module Holidays
|
||||||
#--
|
#--
|
||||||
# TODO: do not take full months
|
# TODO: do not take full months
|
||||||
def self.between(start_date, end_date, regions = [:ca,:us])
|
def self.between(start_date, end_date, regions = [:ca,:us])
|
||||||
regions = [regions] unless regions.kind_of?(Array)
|
regions = validate_regions(regions)
|
||||||
holidays = []
|
holidays = []
|
||||||
|
|
||||||
dates = {}
|
dates = {}
|
||||||
|
@ -112,48 +127,26 @@ module Holidays
|
||||||
hbm.each do |h|
|
hbm.each do |h|
|
||||||
next unless h[:regions].any?{ |reg| regions.include?(reg) }
|
next unless h[:regions].any?{ |reg| regions.include?(reg) }
|
||||||
|
|
||||||
|
unless h[:function]
|
||||||
day = h[:mday] || Date.calculate_mday(year, month, h[:week], h[:wday])
|
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]}
|
holidays << {:month => month, :day => day, :year => year, :name => h[:name], :regions => h[:regions]}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
#puts dates.inspect
|
end
|
||||||
|
|
||||||
holidays
|
holidays
|
||||||
end
|
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
|
private
|
||||||
# Check regions against list of supported regions and return an array of
|
# Check regions against list of supported regions and return an array of
|
||||||
# symbols.
|
# symbols.
|
||||||
def self.check_regions(regions) # :nodoc:
|
def self.validate_regions(regions) # :nodoc:
|
||||||
regions = [regions] unless regions.kind_of?(Array)
|
regions = [regions] unless regions.kind_of?(Array)
|
||||||
regions = regions.collect { |r| r.to_sym }
|
regions = regions.collect { |r| r.to_sym }
|
||||||
|
|
||||||
raise UnkownRegionError unless regions.all? { |r| r == :any or REGIONS.include?(r) }
|
raise UnkownRegionError unless regions.all? { |r| r == :any or REGIONS.include?(r) }
|
||||||
|
|
||||||
regions
|
regions
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -169,7 +162,7 @@ class Date
|
||||||
# => true
|
# => true
|
||||||
def is_holiday?(regions = :any)
|
def is_holiday?(regions = :any)
|
||||||
holidays = Holidays.by_day(self, regions)
|
holidays = Holidays.by_day(self, regions)
|
||||||
holidays.empty?
|
!holidays.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Calculate day of the month based on the week number and the day of the
|
# 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
|
# see http://www.irt.org/articles/js050/index.htm
|
||||||
def self.calculate_mday(year, month, week, wday)
|
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]
|
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 'rake/gempackagetask'
|
||||||
require 'fileutils'
|
require 'fileutils'
|
||||||
require 'lib/holidays'
|
require 'lib/holidays'
|
||||||
|
require 'csv'
|
||||||
|
|
||||||
desc 'Run the unit tests.'
|
desc 'Run the unit tests.'
|
||||||
Rake::TestTask.new do |t|
|
Rake::TestTask.new do |t|
|
||||||
|
@ -13,6 +14,7 @@ Rake::TestTask.new do |t|
|
||||||
t.verbose = false
|
t.verbose = false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
desc 'Generate documentation.'
|
desc 'Generate documentation.'
|
||||||
Rake::RDocTask.new(:rdoc) do |rdoc|
|
Rake::RDocTask.new(:rdoc) do |rdoc|
|
||||||
rdoc.rdoc_dir = 'doc'
|
rdoc.rdoc_dir = 'doc'
|
||||||
|
@ -22,7 +24,7 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
|
||||||
rdoc.rdoc_files.include('REFERENCES')
|
rdoc.rdoc_files.include('REFERENCES')
|
||||||
rdoc.rdoc_files.include('LICENSE')
|
rdoc.rdoc_files.include('LICENSE')
|
||||||
rdoc.rdoc_files.include('lib/*.rb')
|
rdoc.rdoc_files.include('lib/*.rb')
|
||||||
#rdoc.rdoc_files.include('lib/holidays/*.rb')
|
rdoc.rdoc_files.include('lib/holidays/*.rb')
|
||||||
end
|
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/'))
|
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__), '../lib/'))
|
||||||
require 'rubygems'
|
require 'rubygems'
|
||||||
require 'test/unit'
|
require 'test/unit'
|
||||||
|
require 'date'
|
||||||
require 'holidays'
|
require 'holidays'
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
require File.dirname(__FILE__) + '/test_helper'
|
require File.dirname(__FILE__) + '/test_helper'
|
||||||
require 'date'
|
|
||||||
# Test cases for reading and generating CSS shorthand properties
|
class HolidaysTests < Test::Unit::TestCase
|
||||||
class HolidayTests < Test::Unit::TestCase
|
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
@date = Date.civil(2008,1,1)
|
@date = Date.civil(2008,1,1)
|
||||||
|
@ -16,22 +15,8 @@ class HolidayTests < Test::Unit::TestCase
|
||||||
holidays.each do |h|
|
holidays.each do |h|
|
||||||
#puts h.inspect
|
#puts h.inspect
|
||||||
end
|
end
|
||||||
|
|
||||||
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
|
def test_calculating_mdays
|
||||||
# US Memorial day
|
# US Memorial day
|
||||||
assert_equal 29, Date.calculate_mday(2006, 5, :last, 1)
|
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 21, Date.calculate_mday(2008, 1, :third, 1)
|
||||||
assert_equal 1, Date.calculate_mday(2007, 1, :first, 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
|
end
|
||||||
|
|
||||||
def test_region_params
|
def test_region_params
|
||||||
|
|
Loading…
Reference in a new issue