Added support for lambdas in holiday definitions

Separated Easter definitions and tests
Preliminary support for definition builder
This commit is contained in:
Alex Dunae 2007-11-21 00:13:18 +00:00
parent aeee76cadf
commit d98a443337
8 changed files with 187 additions and 61 deletions

55
create_defs.rb Normal file
View 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
View 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 month region mday wday week name
2 1 us 1 New Year's Day
3 1 us 1 3 Martin Luther King, Jr. Day
4 3 us 1 3 George Washington's Birthday

View file

@ -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,8 +35,7 @@ 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]}],
@ -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
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) }
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
#puts dates.inspect
end
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
View 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

View file

@ -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
View 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

View file

@ -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'

View file

@ -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