holidays 3.3.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/CONTRIBUTING.md +37 -0
  4. data/README.md +16 -25
  5. data/REFERENCES +4 -1
  6. data/Rakefile +38 -8
  7. data/benchmark.rb +8 -0
  8. data/definitions/README.md +187 -8
  9. data/definitions/ar.yaml +2 -1
  10. data/definitions/at.yaml +17 -13
  11. data/definitions/au.yaml +65 -60
  12. data/definitions/be_fr.yaml +14 -10
  13. data/definitions/be_nl.yaml +8 -4
  14. data/definitions/bg.yaml +12 -6
  15. data/definitions/br.yaml +6 -3
  16. data/definitions/ca.yaml +8 -6
  17. data/definitions/ch.yaml +21 -15
  18. data/definitions/cl.yaml +4 -2
  19. data/definitions/cr.yaml +5 -3
  20. data/definitions/cz.yaml +4 -2
  21. data/definitions/de.yaml +25 -14
  22. data/definitions/dk.yaml +26 -17
  23. data/definitions/ecb_target.yaml +4 -2
  24. data/definitions/el.yaml +23 -18
  25. data/definitions/es.yaml +31 -28
  26. data/definitions/federal_reserve.yaml +12 -12
  27. data/definitions/fedex.yaml +6 -6
  28. data/definitions/fi.yaml +26 -25
  29. data/definitions/fr.yaml +8 -4
  30. data/definitions/gb.yaml +9 -7
  31. data/definitions/hr.yaml +8 -6
  32. data/definitions/hu.yaml +8 -6
  33. data/definitions/ie.yaml +17 -16
  34. data/definitions/index.yaml +1 -1
  35. data/definitions/is.yaml +29 -19
  36. data/definitions/it.yaml +10 -9
  37. data/definitions/jp.yaml +92 -44
  38. data/definitions/li.yaml +25 -20
  39. data/definitions/lt.yaml +2 -1
  40. data/definitions/ma.yaml +7 -7
  41. data/definitions/mx.yaml +11 -11
  42. data/definitions/nerc.yaml +6 -6
  43. data/definitions/nl.yaml +22 -18
  44. data/definitions/no.yaml +19 -11
  45. data/definitions/north_america_informal.yaml +6 -6
  46. data/definitions/nyse.yaml +9 -8
  47. data/definitions/nz.yaml +33 -29
  48. data/definitions/ph.yaml +15 -8
  49. data/definitions/pl.yaml +27 -17
  50. data/definitions/pt.yaml +4 -2
  51. data/definitions/ro.yaml +21 -18
  52. data/definitions/se.yaml +24 -18
  53. data/definitions/sg.yaml +10 -9
  54. data/definitions/si.yaml +4 -2
  55. data/definitions/sk.yaml +4 -2
  56. data/definitions/united_nations.yaml +12 -12
  57. data/definitions/ups.yaml +6 -6
  58. data/definitions/us.yaml +12 -11
  59. data/definitions/ve.yaml +8 -4
  60. data/definitions/vi.yaml +6 -6
  61. data/definitions/za.yaml +26 -24
  62. data/holidays.gemspec +3 -1
  63. data/lib/generated_definitions/MANIFEST +1 -1
  64. data/lib/generated_definitions/ar.rb +8 -6
  65. data/lib/generated_definitions/at.rb +11 -9
  66. data/lib/generated_definitions/au.rb +75 -99
  67. data/lib/generated_definitions/be.rb +12 -10
  68. data/lib/generated_definitions/be_fr.rb +12 -10
  69. data/lib/generated_definitions/be_nl.rb +12 -10
  70. data/lib/generated_definitions/bg.rb +14 -13
  71. data/lib/generated_definitions/br.rb +11 -9
  72. data/lib/generated_definitions/ca.rb +20 -20
  73. data/lib/generated_definitions/ch.rb +41 -44
  74. data/lib/generated_definitions/cl.rb +9 -7
  75. data/lib/generated_definitions/cr.rb +9 -7
  76. data/lib/generated_definitions/cz.rb +9 -6
  77. data/lib/generated_definitions/de.rb +25 -25
  78. data/lib/generated_definitions/dk.rb +17 -15
  79. data/lib/generated_definitions/ecb_target.rb +9 -7
  80. data/lib/generated_definitions/el.rb +13 -11
  81. data/lib/generated_definitions/es.rb +35 -33
  82. data/lib/generated_definitions/europe.rb +234 -247
  83. data/lib/generated_definitions/federal_reserve.rb +11 -9
  84. data/lib/generated_definitions/fedex.rb +42 -0
  85. data/lib/generated_definitions/fi.rb +32 -36
  86. data/lib/generated_definitions/fr.rb +12 -10
  87. data/lib/generated_definitions/gb.rb +15 -13
  88. data/lib/generated_definitions/hr.rb +10 -8
  89. data/lib/generated_definitions/hu.rb +9 -7
  90. data/lib/generated_definitions/ie.rb +17 -17
  91. data/lib/generated_definitions/is.rb +26 -26
  92. data/lib/generated_definitions/it.rb +9 -7
  93. data/lib/generated_definitions/jp.rb +112 -89
  94. data/lib/generated_definitions/li.rb +14 -12
  95. data/lib/generated_definitions/lt.rb +9 -7
  96. data/lib/generated_definitions/ma.rb +7 -5
  97. data/lib/generated_definitions/mx.rb +7 -5
  98. data/lib/generated_definitions/nerc.rb +10 -8
  99. data/lib/generated_definitions/nl.rb +15 -13
  100. data/lib/generated_definitions/no.rb +16 -14
  101. data/lib/generated_definitions/north_america.rb +34 -37
  102. data/lib/generated_definitions/nyse.rb +10 -8
  103. data/lib/generated_definitions/nz.rb +40 -40
  104. data/lib/generated_definitions/ph.rb +17 -13
  105. data/lib/generated_definitions/pl.rb +25 -27
  106. data/lib/generated_definitions/pt.rb +10 -8
  107. data/lib/generated_definitions/ro.rb +11 -9
  108. data/lib/generated_definitions/scandinavia.rb +92 -102
  109. data/lib/generated_definitions/se.rb +25 -27
  110. data/lib/generated_definitions/sg.rb +11 -9
  111. data/lib/generated_definitions/si.rb +10 -8
  112. data/lib/generated_definitions/sk.rb +9 -7
  113. data/lib/generated_definitions/united_nations.rb +7 -5
  114. data/lib/generated_definitions/ups.rb +13 -12
  115. data/lib/generated_definitions/us.rb +20 -21
  116. data/lib/generated_definitions/ve.rb +11 -9
  117. data/lib/generated_definitions/vi.rb +7 -5
  118. data/lib/generated_definitions/za.rb +19 -17
  119. data/lib/holidays.rb +20 -83
  120. data/lib/holidays/date_calculator/weekend_modifier.rb +22 -5
  121. data/lib/holidays/definition/context/generator.rb +67 -29
  122. data/lib/holidays/definition/context/merger.rb +8 -8
  123. data/lib/holidays/definition/decorator/custom_method_proc.rb +28 -0
  124. data/lib/holidays/definition/decorator/custom_method_source.rb +30 -0
  125. data/lib/holidays/definition/entity/custom_method.rb +11 -0
  126. data/lib/holidays/definition/parser/custom_method.rb +69 -0
  127. data/lib/holidays/definition/repository/custom_methods.rb +27 -0
  128. data/lib/holidays/definition/repository/holidays_by_month.rb +1 -1
  129. data/lib/holidays/definition/repository/{proc_cache.rb → proc_result_cache.rb} +19 -4
  130. data/lib/holidays/definition/validator/custom_method.rb +31 -0
  131. data/lib/holidays/definition_factory.rb +42 -6
  132. data/lib/holidays/errors.rb +6 -0
  133. data/lib/holidays/load_all_definitions.rb +57 -0
  134. data/lib/holidays/option/context/parse_options.rb +26 -16
  135. data/lib/holidays/option_factory.rb +1 -0
  136. data/lib/holidays/use_case/context/between.rb +41 -14
  137. data/lib/holidays/use_case_factory.rb +2 -1
  138. data/lib/holidays/version.rb +1 -1
  139. data/test/data/test_single_custom_holiday_with_custom_procs.yaml +24 -0
  140. data/test/defs/test_defs_at.rb +1 -1
  141. data/test/defs/test_defs_au.rb +3 -2
  142. data/test/defs/test_defs_cr.rb +1 -0
  143. data/test/defs/test_defs_cz.rb +1 -0
  144. data/test/defs/test_defs_dk.rb +2 -2
  145. data/test/defs/test_defs_el.rb +7 -6
  146. data/test/defs/test_defs_europe.rb +40 -33
  147. data/test/defs/test_defs_fedex.rb +24 -0
  148. data/test/defs/test_defs_fi.rb +4 -3
  149. data/test/defs/test_defs_hr.rb +2 -2
  150. data/test/defs/test_defs_hu.rb +2 -2
  151. data/test/defs/test_defs_is.rb +2 -1
  152. data/test/defs/test_defs_it.rb +2 -1
  153. data/test/defs/test_defs_jp.rb +1 -1
  154. data/test/defs/test_defs_li.rb +1 -1
  155. data/test/defs/test_defs_ma.rb +2 -1
  156. data/test/defs/test_defs_mx.rb +4 -3
  157. data/test/defs/test_defs_nl.rb +7 -6
  158. data/test/defs/test_defs_no.rb +1 -0
  159. data/test/defs/test_defs_north_america.rb +4 -3
  160. data/test/defs/test_defs_nyse.rb +2 -1
  161. data/test/defs/test_defs_pl.rb +1 -0
  162. data/test/defs/test_defs_ro.rb +11 -11
  163. data/test/defs/test_defs_scandinavia.rb +12 -8
  164. data/test/defs/test_defs_se.rb +3 -2
  165. data/test/defs/test_defs_sg.rb +2 -1
  166. data/test/defs/test_defs_vi.rb +1 -1
  167. data/test/defs/test_defs_za.rb +3 -2
  168. data/test/holidays/date_calculator/test_weekend_modifier.rb +11 -0
  169. data/test/holidays/definition/context/test_generator.rb +64 -5
  170. data/test/holidays/definition/context/test_merger.rb +5 -2
  171. data/test/holidays/definition/decorator/test_custom_method_proc.rb +113 -0
  172. data/test/holidays/definition/decorator/test_custom_method_source.rb +96 -0
  173. data/test/holidays/definition/parser/test_custom_method.rb +79 -0
  174. data/test/holidays/definition/repository/test_custom_methods.rb +43 -0
  175. data/test/holidays/definition/repository/test_holidays_by_month.rb +0 -32
  176. data/test/holidays/definition/repository/test_proc_result_cache.rb +84 -0
  177. data/test/holidays/definition/validator/test_custom_method.rb +89 -0
  178. data/test/holidays/option/context/test_parse_options.rb +5 -0
  179. data/test/holidays/test_definition_factory.rb +17 -2
  180. data/test/holidays/use_case/context/test_between.rb +2 -0
  181. data/test/test_all_regions.rb +7 -49
  182. data/test/test_custom_holidays.rb +8 -2
  183. data/test/test_helper.rb +9 -2
  184. data/test/test_holidays.rb +9 -29
  185. metadata +46 -11
  186. data/lib/generated_definitions/fed_ex.rb +0 -41
  187. data/test/holidays/definition/repository/test_proc_cache.rb +0 -29
  188. data/test/test_parse_definitions.rb +0 -30
@@ -20,16 +20,31 @@ module Holidays
20
20
  # 0100 0.125000 0.000000 0.125000 ( 0.125000)
21
21
  # 1000 1.234000 0.000000 1.234000 ( 1.234000)
22
22
  # 5000 6.094000 0.031000 6.125000 ( 6.141000)
23
- class ProcCache
23
+ class ProcResultCache
24
24
  def initialize
25
25
  @proc_cache = {}
26
26
  end
27
27
 
28
- def lookup(function, year)
29
- proc_key = Digest::MD5.hexdigest("#{function.to_s}_#{year.to_s}")
30
- @proc_cache[proc_key] = function.call(year) unless @proc_cache[proc_key]
28
+ def lookup(function, *function_arguments)
29
+ validate!(function, function_arguments)
30
+
31
+ proc_key = build_proc_key(function, function_arguments)
32
+ @proc_cache[proc_key] = function.call(*function_arguments) unless @proc_cache[proc_key]
31
33
  @proc_cache[proc_key]
32
34
  end
35
+
36
+ private
37
+
38
+ def validate!(function, function_arguments)
39
+ raise ArgumentError.new("function must be a proc") unless function.is_a?(Proc)
40
+ function_arguments.each do |arg|
41
+ raise ArgumentError.new("function arguments '#{function_arguments}' must contain either integers or dates") unless arg.is_a?(Integer) || arg.is_a?(Date)
42
+ end
43
+ end
44
+
45
+ def build_proc_key(function, function_arguments)
46
+ Digest::MD5.hexdigest("#{function.to_s}_#{function_arguments.join('_')}")
47
+ end
33
48
  end
34
49
  end
35
50
  end
@@ -0,0 +1,31 @@
1
+ module Holidays
2
+ module Definition
3
+ module Validator
4
+ class CustomMethod
5
+ VALID_ARGUMENTS = ["date", "year", "month", "day"]
6
+
7
+ def valid?(m)
8
+ valid_name?(m[:name]) &&
9
+ valid_arguments?(m[:arguments]) &&
10
+ valid_source?(m[:source])
11
+ end
12
+
13
+ private
14
+
15
+ def valid_name?(name)
16
+ !name.nil? && !name.empty?
17
+ end
18
+
19
+ def valid_arguments?(arguments)
20
+ arguments.split(",").all? { |arg|
21
+ arg == arg.chomp && VALID_ARGUMENTS.include?(arg.strip)
22
+ }
23
+ end
24
+
25
+ def valid_source?(source)
26
+ !source.nil? && !source.empty?
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,35 +1,67 @@
1
1
  require 'holidays/definition/context/generator'
2
2
  require 'holidays/definition/context/merger'
3
+ require 'holidays/definition/decorator/custom_method_proc'
4
+ require 'holidays/definition/decorator/custom_method_source'
5
+ require 'holidays/definition/parser/custom_method'
3
6
  require 'holidays/definition/repository/holidays_by_month'
4
7
  require 'holidays/definition/repository/regions'
5
8
  require 'holidays/definition/repository/cache'
6
- require 'holidays/definition/repository/proc_cache'
9
+ require 'holidays/definition/repository/proc_result_cache'
10
+ require 'holidays/definition/repository/custom_methods'
11
+ require 'holidays/definition/validator/custom_method'
7
12
  require 'holidays/definition/validator/region'
8
13
 
9
14
  module Holidays
10
15
  module DefinitionFactory
11
16
  class << self
12
17
  def file_parser
13
- Definition::Context::Generator.new
18
+ Definition::Context::Generator.new(
19
+ custom_method_parser,
20
+ custom_method_source_decorator,
21
+ custom_methods_repository,
22
+ )
14
23
  end
15
24
 
16
25
  def source_generator
17
- Definition::Context::Generator.new
26
+ Definition::Context::Generator.new(
27
+ custom_method_parser,
28
+ custom_method_source_decorator,
29
+ custom_methods_repository,
30
+ )
18
31
  end
19
32
 
20
33
  def merger
21
34
  Definition::Context::Merger.new(
22
35
  holidays_by_month_repository,
23
- regions_repository
36
+ regions_repository,
37
+ custom_methods_repository,
24
38
  )
25
39
  end
26
40
 
41
+ def custom_method_parser
42
+ Definition::Parser::CustomMethod.new(
43
+ custom_method_validator,
44
+ )
45
+ end
46
+
47
+ def custom_method_proc_decorator
48
+ Definition::Decorator::CustomMethodProc.new
49
+ end
50
+
51
+ def custom_method_source_decorator
52
+ Definition::Decorator::CustomMethodSource.new
53
+ end
54
+
27
55
  def region_validator
28
56
  Definition::Validator::Region.new(
29
57
  regions_repository
30
58
  )
31
59
  end
32
60
 
61
+ def custom_method_validator
62
+ Definition::Validator::CustomMethod.new
63
+ end
64
+
33
65
  def holidays_by_month_repository
34
66
  @holidays_repo ||= Definition::Repository::HolidaysByMonth.new
35
67
  end
@@ -42,8 +74,12 @@ module Holidays
42
74
  @cache_repo ||= Definition::Repository::Cache.new
43
75
  end
44
76
 
45
- def proc_cache_repository
46
- @proc_cache_repo ||= Definition::Repository::ProcCache.new
77
+ def proc_result_cache_repository
78
+ @proc_result_cache_repo ||= Definition::Repository::ProcResultCache.new
79
+ end
80
+
81
+ def custom_methods_repository
82
+ @custom_methods_repository ||= Definition::Repository::CustomMethods.new
47
83
  end
48
84
  end
49
85
  end
@@ -0,0 +1,6 @@
1
+ module Holidays
2
+ class Error < StandardError; end
3
+
4
+ class FunctionNotFound < Error; end
5
+ class UnknownRegionError < Error ; end
6
+ end
@@ -0,0 +1,57 @@
1
+ module Holidays
2
+ class LoadAllDefinitions
3
+ class << self
4
+ def call
5
+ path = File.expand_path(File.dirname(__FILE__)) + "/../#{Holidays::DEFINITIONS_PATH}/"
6
+
7
+ Dir.foreach(path) do |item|
8
+ next if item == '.' or item == '..'
9
+
10
+ target = path+item
11
+ next if File.extname(target) != '.rb'
12
+
13
+ require target
14
+ end
15
+
16
+ #FIXME I need a better way to do this. I'm thinking of putting these 'common' methods
17
+ # into some kind of definition file so it can be loaded automatically but I'm afraid
18
+ # of making that big of a breaking API change since these are public. For the time
19
+ # being I'll load them manually like this.
20
+ global_methods = {
21
+ "easter(year)" => gregorian_easter.method(:calculate_easter_for).to_proc,
22
+ "orthodox_easter(year)" => gregorian_easter.method(:calculate_orthodox_easter_for).to_proc,
23
+ "orthodox_easter_julian(year)" => julian_easter.method(:calculate_orthodox_easter_for).to_proc,
24
+ "to_monday_if_sunday(date)" => weekend_modifier.method(:to_monday_if_sunday).to_proc,
25
+ "to_monday_if_weekend(date)" => weekend_modifier.method(:to_monday_if_weekend).to_proc,
26
+ "to_weekday_if_boxing_weekend(date)" => weekend_modifier.method(:to_weekday_if_boxing_weekend).to_proc,
27
+ "to_weekday_if_boxing_weekend_from_year(year)" => weekend_modifier.method(:to_weekday_if_boxing_weekend_from_year).to_proc,
28
+ "to_weekday_if_weekend(date)" => weekend_modifier.method(:to_weekday_if_weekend).to_proc,
29
+ "calculate_day_of_month(year, month, day, wday)" => day_of_month_calculator.method(:call).to_proc,
30
+ "to_weekday_if_boxing_weekend_from_year_or_to_tuesday_if_monday(year)" => weekend_modifier.method(:to_weekday_if_boxing_weekend_from_year_or_to_tuesday_if_monday).to_proc,
31
+ "xmas_to_weekday_if_weekend(year)" => weekend_modifier.method(:xmas_to_weekday_if_weekend).to_proc,
32
+ "to_tuesday_if_sunday_or_monday_if_saturday(date)" => weekend_modifier.method(:to_tuesday_if_sunday_or_monday_if_saturday).to_proc,
33
+ }
34
+
35
+ Holidays::DefinitionFactory.custom_methods_repository.add(global_methods)
36
+ end
37
+
38
+ private
39
+
40
+ def gregorian_easter
41
+ DateCalculatorFactory::Easter::Gregorian.easter_calculator
42
+ end
43
+
44
+ def julian_easter
45
+ DateCalculatorFactory::Easter::Julian.easter_calculator
46
+ end
47
+
48
+ def weekend_modifier
49
+ DateCalculatorFactory.weekend_modifier
50
+ end
51
+
52
+ def day_of_month_calculator
53
+ DateCalculatorFactory.day_of_month_calculator
54
+ end
55
+ end
56
+ end
57
+ end
@@ -2,9 +2,10 @@ module Holidays
2
2
  module Option
3
3
  module Context
4
4
  class ParseOptions
5
- def initialize(regions_repo, region_validator)
5
+ def initialize(regions_repo, region_validator, definition_merger)
6
6
  @regions_repo = regions_repo
7
7
  @region_validator = region_validator
8
+ @definition_merger = definition_merger
8
9
  end
9
10
 
10
11
  # Returns [(arr)regions, (bool)observed, (bool)informal]
@@ -23,12 +24,12 @@ module Holidays
23
24
 
24
25
  private
25
26
 
26
- attr_reader :regions_repo, :region_validator
27
+ attr_reader :regions_repo, :region_validator, :definition_merger
27
28
 
28
29
  # Check regions against list of supported regions and return an array of
29
30
  # symbols.
30
31
  #
31
- # If a wildcard region is found (e.g. <tt>:ca_</tt>) it is expanded into all
32
+ # If a wildcard region is found (e.g. :ca_) it is expanded into all
32
33
  # of its available sub regions.
33
34
  def parse_regions!(regions)
34
35
  regions = [regions] unless regions.kind_of?(Array)
@@ -49,20 +50,19 @@ module Holidays
49
50
 
50
51
  regions.flatten!
51
52
 
52
- require "#{DEFINITIONS_PATH}/north_america" if regions.include?(:us) # special case for north_america/US cross-linking
53
+ load_definition_data("north_america") if regions.include?(:us) # special case for north_america/US cross-linking
53
54
 
54
55
  regions.each do |region|
55
- unless region == :any or regions_repo.exists?(region)
56
- region_definition = "#{DEFINITIONS_PATH}/#{region.to_s}"
56
+ unless region == :any || regions_repo.exists?(region)
57
57
  begin
58
- require region_definition #FIXME This is unacceptable, we can't be loading external files while parsing options
59
- rescue LoadError
58
+ load_definition_data(region.to_s)
59
+ rescue NameError => e
60
60
  # This could be a sub region that does not have any holiday
61
61
  # definitions of its own; try to load the containing region instead.
62
62
  if region.to_s =~ /_/
63
63
  load_containing_region(region.to_s)
64
64
  else
65
- raise UnknownRegionError, "Could not load #{region_definition}"
65
+ raise UnknownRegionError.new(e), "Could not load #{region.to_s}"
66
66
  end
67
67
  end
68
68
  end
@@ -81,15 +81,25 @@ module Holidays
81
81
  # and load its definition. (Common code factored out from parse_regions)
82
82
  def load_containing_region(sub_reg)
83
83
  prefix = sub_reg.split('_').first
84
- region_definition = "#{DEFINITIONS_PATH}/#{prefix}"
85
- unless regions_repo.exists?(prefix.to_sym)
86
- begin
87
- require region_definition #FIXME This is not acceptable, we can't be loading external things while parsing options.
88
- rescue LoadError
89
- raise UnknownRegionError, "Could not load #{region_definition}"
90
- end
84
+
85
+ return if regions_repo.exists?(prefix.to_sym)
86
+
87
+ begin
88
+ load_definition_data(prefix)
89
+ rescue NameError => e
90
+ raise UnknownRegionError.new(e), "Could not load region prefix: #{prefix.to_s}, original subregion: #{sub_reg.to_s}"
91
91
  end
92
92
  end
93
+
94
+ def load_definition_data(region)
95
+ target_region_module = Module.const_get("Holidays").const_get(region.upcase)
96
+
97
+ definition_merger.call(
98
+ target_region_module.defined_regions,
99
+ target_region_module.holidays_by_month,
100
+ target_region_module.custom_methods,
101
+ )
102
+ end
93
103
  end
94
104
  end
95
105
  end
@@ -7,6 +7,7 @@ module Holidays
7
7
  Option::Context::ParseOptions.new(
8
8
  DefinitionFactory.regions_repository,
9
9
  DefinitionFactory.region_validator,
10
+ DefinitionFactory.merger,
10
11
  )
11
12
  end
12
13
  end
@@ -2,10 +2,11 @@ module Holidays
2
2
  module UseCase
3
3
  module Context
4
4
  class Between
5
- def initialize(holidays_by_month_repo, day_of_month_calculator, proc_cache_repo)
5
+ def initialize(holidays_by_month_repo, day_of_month_calculator, custom_methods_repo, proc_result_cache_repo)
6
6
  @holidays_by_month_repo = holidays_by_month_repo
7
7
  @day_of_month_calculator = day_of_month_calculator
8
- @proc_cache_repo = proc_cache_repo
8
+ @custom_methods_repo = custom_methods_repo
9
+ @proc_result_cache_repo = proc_result_cache_repo
9
10
  end
10
11
 
11
12
  def call(start_date, end_date, dates_driver, regions, observed, informal)
@@ -18,9 +19,7 @@ module Holidays
18
19
  next unless hbm = holidays_by_month_repo.find_by_month(month)
19
20
  hbm.each do |h|
20
21
  next unless in_region?(regions, h[:regions])
21
-
22
- # Skip informal holidays unless they have been requested
23
- next if h[:type] == :informal and not informal
22
+ next if h[:type] == :informal && !informal
24
23
 
25
24
  # range check feature.
26
25
  if h[:year_ranges]
@@ -45,18 +44,42 @@ module Holidays
45
44
  next unless valid_range_year
46
45
  end
47
46
 
47
+ #FIXME I don't like this entire if/else. If it's a function, do something, else do some
48
+ # weird mday logic? Bollocks. I think this should be a refactor target.
48
49
  if h[:function]
49
- # Holiday definition requires a calculation
50
- result = call_proc(h[:function], year)
50
+ function_arguments = []
51
+
52
+ #FIXME This is a refactor target. We should also allow 'date'. Right now these are the only
53
+ # three things that we allow in. I think having a more testable, robust approach here is vital.
54
+ if h[:function_arguments].include?(:year)
55
+ function_arguments << year
56
+ end
57
+
58
+ if h[:function_arguments].include?(:month)
59
+ function_arguments << month
60
+ end
61
+
62
+ if h[:function_arguments].include?(:day)
63
+ function_arguments << h[:mday]
64
+ end
65
+
66
+ result = call_proc(h[:function], *function_arguments)
67
+
68
+ #FIXME This is a dangerous assumption. We should raise an error or something
69
+ # if these procs return something unexpected.
70
+ #
51
71
  # Procs may return either Date or an integer representing mday
52
72
  if result.kind_of?(Date)
73
+ if h[:function_modifier]
74
+ result = result + h[:function_modifier] # NOTE: This could be a positive OR negative number.
75
+ end
76
+
53
77
  month = result.month
54
78
  mday = result.mday
55
79
  else
56
80
  mday = result
57
81
  end
58
82
  else
59
- # Calculate the mday
60
83
  mday = h[:mday] || day_of_month_calculator.call(year, month, h[:week], h[:wday])
61
84
  end
62
85
 
@@ -65,9 +88,10 @@ module Holidays
65
88
  date = Date.civil(year, month, mday)
66
89
  rescue; next; end
67
90
 
68
- # If the :observed option is set, calculate the date when the holiday
69
- # is observed.
70
- if observed and h[:observed]
91
+ #FIXME We should be checking the function arguments and passing in what is specified.
92
+ # Right now all 'observed' functions require 'date' but that is by convention. Nothing
93
+ # is requiring that. We should be more explicit.
94
+ if observed && h[:observed]
71
95
  date = call_proc(h[:observed], date)
72
96
  end
73
97
 
@@ -83,7 +107,7 @@ module Holidays
83
107
 
84
108
  private
85
109
 
86
- attr_reader :holidays_by_month_repo, :day_of_month_calculator, :proc_cache_repo
110
+ attr_reader :holidays_by_month_repo, :day_of_month_calculator, :custom_methods_repo, :proc_result_cache_repo
87
111
 
88
112
  def validate!(start_date, end_date, dates_driver, regions)
89
113
  raise ArgumentError unless start_date
@@ -98,8 +122,11 @@ module Holidays
98
122
  raise ArgumentError if regions.nil? || regions.empty?
99
123
  end
100
124
 
101
- def call_proc(function, year)
102
- proc_cache_repo.lookup(function, year)
125
+ def call_proc(function_id, *arguments)
126
+ function = custom_methods_repo.find(function_id)
127
+ raise Holidays::FunctionNotFound.new("Unable to find function with id '#{function_id}'") if function.nil?
128
+
129
+ proc_result_cache_repo.lookup(function, *arguments)
103
130
  end
104
131
 
105
132
  # Check sub regions.
@@ -8,7 +8,8 @@ module Holidays
8
8
  UseCase::Context::Between.new(
9
9
  DefinitionFactory.holidays_by_month_repository,
10
10
  DateCalculatorFactory.day_of_month_calculator,
11
- DefinitionFactory.proc_cache_repository,
11
+ DefinitionFactory.custom_methods_repository,
12
+ DefinitionFactory.proc_result_cache_repository,
12
13
  )
13
14
  end
14
15
 
@@ -1,3 +1,3 @@
1
1
  module Holidays
2
- VERSION = '3.3.0'
2
+ VERSION = '4.0.0'
3
3
  end
@@ -0,0 +1,24 @@
1
+ months:
2
+ 0:
3
+ - name: Custom Holiday
4
+ regions: [custom_single_file]
5
+ function: custom_method(year, month)
6
+ function_modifier: 5
7
+ 6:
8
+ - name: Company Founding
9
+ regions: [custom_single_file]
10
+ mday: 20
11
+ methods:
12
+ custom_method:
13
+ arguments: year, month
14
+ source: |
15
+ d = Date.civil(year, month, 1)
16
+ d + 2
17
+ tests: |
18
+ {Date.civil(2013,6,20) => 'Company Founding'}.each do |date, name|
19
+ assert_equal name, (Holidays.on(date, :custom_single_file)[0] || {})[:name]
20
+ end
21
+
22
+ {Date.civil(2015, 1, 1) => 'Custom Holiday'}.each do |date, name|
23
+ assert_equal name, (Holidays.on(date, :custom_single_file)[0] || {}[:name]
24
+ end