diff --git a/Makefile b/Makefile index adf7d14..1e4334d 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ -default: validate +default: test validate: bundle exec ruby lib/validation/run.rb -test: +test: validate bundle exec rspec .PHONY: validate,test diff --git a/au.yaml b/au.yaml index 0bfb458..929a6d2 100644 --- a/au.yaml +++ b/au.yaml @@ -102,12 +102,33 @@ months: - name: ANZAC Day # ANZAC DAY ACTUAL regions: [au_nsw, au_sa, au_tas, au_vic, au_act, au_wa] mday: 25 + year_ranges: + - before: 2025 - name: ANZAC Day # ANZAC DAY OBSERVED - no additional recognised regions: [au_nt, au_qld] mday: 25 observed: to_monday_if_weekend(date) + year_ranges: + - before: 2025 - name: Additional public holiday for ANZAC Day # ADDITIONAL ANZAC DAY - regions: [au_sa, au_act, au_wa] + regions: [au_sa, au_act] + mday: 25 + function: additional_anzac_on_monday_if_on_weekend(date) + year_ranges: + - before: 2025 + - name: ANZAC Day # ANZAC DAY ACTUAL + regions: [au_nsw, au_sa, au_tas, au_vic, au_wa, au_nt] + mday: 25 + year_ranges: + - after: 2026 + - name: ANZAC Day # ANZAC DAY ACTUAL + regions: [au_qld, au_act] + mday: 25 + function: to_monday_if_sunday(date) + year_ranges: + - after: 2026 + - name: Additional public holiday for ANZAC Day # ADDITIONAL ANZAC DAY + regions: [au_wa] mday: 25 function: additional_anzac_on_monday_if_on_weekend(date) 5: @@ -457,20 +478,55 @@ methods: tests: - given: - date: '2020-04-25' - regions: ["au_act"] + date: '2015-04-27' + regions: ["au_qld"] + options: ["observed"] + expect: + name: "ANZAC Day" + - given: + date: ['2020-04-25', '2021-04-25', '2022-04-25', '2023-04-25', '2024-04-25', '2025-04-25'] + regions: ["au_nsw", "au_act", "au_vic", "au_tas", "au_sa", "au_wa"] expect: name: 'ANZAC Day' - given: - date: '2020-04-25' - regions: ["au_qld"] + date: ['2020-04-27', '2021-04-26', '2022-04-25', '2023-04-25', '2024-04-25', '2025-04-25'] + regions: ["au_qld", "au_nt"] + options: ["observed"] + expect: + name: 'ANZAC Day' + - given: + date: ['2026-04-25', '2027-04-25'] + regions: ["au_nsw", "au_vic", "au_tas", "au_sa", "au_wa", "au_nt"] expect: name: 'ANZAC Day' - given: - date: '2021-04-27' + # moves to Monday if falls on Sunday + date: ['2026-04-25', '2027-04-26', '2028-04-25', '2029-04-25', '2030-04-25', '2031-04-25', '2032-04-26', '2033-04-25', '2034-04-25', '2035-04-25'] + regions: ["au_qld", "au_act"] + expect: + name: 'ANZAC Day' + - given: + # additional on Monday if falls on Weekend + date: ['2021-04-26', '2026-04-27', '2027-04-26'] regions: ["au_wa"] expect: name: 'Additional public holiday for ANZAC Day' + - given: + date: '2015-04-25' + regions: ["au_wa"] + expect: + name: 'ANZAC Day' + - given: + date: '2015-04-27' + regions: ["au_wa"] + options: ["observed"] + expect: + name: 'Additional public holiday for ANZAC Day' + - given: + date: '2026-04-27' + regions: ["au_nsw", "au_act", "au_vic", "au_tas", "au_sa", "au_nt", "au_qld"] + expect: + holiday: false - given: date: '2021-12-25' regions: ["au_qld"] @@ -490,7 +546,7 @@ tests: date: '2021-12-28' regions: ["au_qld"] expect: - name: 'Additional public holiday for Boxing Day' + name: 'Additional public holiday Boxing Day' - given: date: '2013-10-07' regions: ["au_qld"] @@ -527,7 +583,7 @@ tests: expect: name: "Queen's Birthday" - given: - date: ['2023-09-25','2024-09-23', '2025-09-30'] + date: ['2023-09-25','2024-09-23', '2025-09-29'] regions: ["au_wa"] expect: name: "King's Birthday" @@ -554,12 +610,12 @@ tests: name: 'ACT Reconciliation Day' - given: date: '2014-01-26' - regions: ["au_qld"] + regions: ["au"] expect: name: 'Australia Day' - given: date: '2014-01-27' - regions: ["au_qld"] + regions: ["au"] options: ["observed"] expect: name: 'Australia Day' @@ -613,21 +669,11 @@ tests: regions: ["au_tas_north"] expect: name: 'Recreation Day' - - given: - date: '2015-12-26' - regions: ["au_tas"] - expect: - holiday: false - given: date: '2015-11-14' regions: ["au_qld_brisbane"] expect: holiday: false - - given: - date: '2015-12-26' - regions: ["au_nt"] - expect: - holiday: false - given: date: '2016-12-27' regions: ["au_sa"] @@ -638,16 +684,6 @@ tests: regions: ["au_nt"] expect: name: 'Christmas Day' - - given: - date: '2014-11-04' - regions: ["au_vic"] - expect: - holiday: false - - given: - date: '2015-11-03' - regions: ["au_vic"] - expect: - holiday: false - given: date: '2016-12-25' regions: ["au_vic"] @@ -663,17 +699,6 @@ tests: regions: ["au_sa"] expect: holiday: false - - given: - date: '2015-12-26' - regions: ["au_sa"] - expect: - holiday: false - - given: - date: '2015-04-27' - regions: ["au_qld"] - options: ["observed"] - expect: - holiday: false - given: date: '2014-11-14' regions: ["au_qld"] @@ -681,17 +706,17 @@ tests: holiday: false - given: date: '2014-11-04' - regions: ["au_vic_melbourne"] + regions: ["au_vic_melbourne", "au_vic"] expect: name: 'Melbourne Cup Day' - given: date: '2015-11-03' - regions: ["au_vic_melbourne"] + regions: ["au_vic_melbourne", "au_vic"] expect: name: 'Melbourne Cup Day' - given: date: '2019-11-05' - regions: ["au_vic"] + regions: ["au_vic", "au_vic_melbourne"] expect: name: 'Melbourne Cup Day' - given: @@ -774,17 +799,6 @@ tests: regions: ["au_sa"] expect: name: 'March Public Holiday' - - given: - date: '2015-04-25' - regions: ["au_qld", "au_wa"] - expect: - name: 'ANZAC Day' - - given: - date: '2015-04-27' - regions: ["au_wa"] - options: ["observed"] - expect: - name: 'ANZAC Day' - given: date: '2015-12-26' regions: ["au_qld"] @@ -795,17 +809,17 @@ tests: regions: ["au_qld"] options: ["observed"] expect: - name: "Boxing Day" + name: "Additional public holiday Boxing Day" - given: date: '2020-12-26' regions: ["au_qld"] expect: - name: "Additional public holiday for Boxing Day" + name: "Boxing Day" - given: date: '2015-12-26' regions: ["au_sa"] expect: - holiday: false + name: "Boxing Day" - given: date: '2015-12-28' regions: ["au_sa"] @@ -814,23 +828,30 @@ tests: - given: date: '2015-12-26' regions: ["au_tas"] + expect: + name: "Boxing Day" + - given: + date: '2015-12-26' + regions: ["au_tas"] + options: ["observed"] expect: holiday: false - given: date: '2015-12-28' regions: ["au_tas"] + options: ["observed"] expect: name: "Boxing Day" - given: date: '2015-12-26' regions: ["au_nt"] expect: - holiday: false + name: "Boxing Day" - given: date: '2015-12-28' regions: ["au_nt"] expect: - name: "Boxing Day" + name: "Additional public holiday Boxing Day" - given: date: '2012-02-13' regions: ["au_tas_south"] @@ -860,9 +881,8 @@ tests: - given: date: '2016-12-27' regions: ["au_qld"] - options: ["observed"] expect: - name: 'Christmas Day' + name: 'Additional public holiday for Christmas Day' - given: date: '2016-12-25' regions: ["au_nsw"] @@ -871,9 +891,8 @@ tests: - given: date: '2016-12-27' regions: ["au_nsw"] - options: ["observed"] expect: - name: 'Christmas Day' + name: 'Additional public holiday for Christmas Day' - given: date: '2016-12-25' regions: ["au_act"] @@ -882,9 +901,8 @@ tests: - given: date: '2016-12-27' regions: ["au_act"] - options: ["observed"] expect: - name: 'Christmas Day' + name: 'Additional public holiday for Christmas Day' - given: date: '2016-12-25' regions: ["au_tas"] @@ -893,9 +911,8 @@ tests: - given: date: '2016-12-27' regions: ["au_tas"] - options: ["observed"] expect: - name: 'Christmas Day' + name: 'Additional public holiday for Christmas Day' - given: date: '2016-12-25' regions: ["au_wa"] @@ -904,9 +921,8 @@ tests: - given: date: '2016-12-27' regions: ["au_wa"] - options: ["observed"] expect: - name: 'Christmas Day' + name: 'Additional public holiday for Christmas Day' - given: date: '2016-12-25' regions: ["au_sa"] @@ -927,7 +943,7 @@ tests: regions: ["au_sa"] options: ["observed"] expect: - name: 'Christmas Day' + name: 'Additional public holiday for Christmas Day' - given: date: '2016-12-25' regions: ["au_nsw"] @@ -936,9 +952,8 @@ tests: - given: date: '2016-12-27' regions: ["au_nsw"] - options: ["observed"] expect: - name: 'Christmas Day' + name: 'Additional public holiday for Christmas Day' - given: date: '2016-12-25' regions: ["au_act"] @@ -947,9 +962,8 @@ tests: - given: date: '2016-12-27' regions: ["au_act"] - options: ["observed"] expect: - name: 'Christmas Day' + name: 'Additional public holiday for Christmas Day' - given: date: '2016-12-25' regions: ["au_tas"] @@ -958,9 +972,8 @@ tests: - given: date: '2016-12-27' regions: ["au_tas"] - options: ["observed"] expect: - name: 'Christmas Day' + name: 'Additional public holiday for Christmas Day' - given: date: '2016-12-25' regions: ["au_wa"] @@ -969,9 +982,8 @@ tests: - given: date: '2016-12-27' regions: ["au_wa"] - options: ["observed"] expect: - name: 'Christmas Day' + name: 'Additional public holiday for Christmas Day' - given: date: '2016-12-25' regions: ["au_sa"] @@ -980,42 +992,28 @@ tests: - given: date: '2016-12-26' regions: ["au_sa"] - options: ["observed"] expect: - name: 'Christmas Day' + name: 'Additional public holiday for Christmas Day' - given: date: '2016-12-27' regions: ["au_vic"] - options: ["observed"] expect: - name: 'Christmas Day' + name: 'Additional public holiday for Christmas Day' - given: - date: '2016-12-27' + date: '2016-12-26' regions: ["au_nt"] - options: ["observed"] expect: - name: 'Christmas Day' + name: 'Additional public holiday for Christmas Day' - given: date: '2017-01-01' - regions: ["au", "au_qld", "au_nsw", "au_act", "au_vic", "au_sa", "au_wa", "au_nt"] + regions: ["au_qld", "au_nsw", "au_act", "au_vic", "au_wa", "au_nt"] expect: name: "New Year's Day" - -#FIXME This commented test is valid but the current code in the ruby repo won't allow it -# to work. We need to discuss potential solutions but in the meantime I'm taking it out. -# See https://github.com/holidays/definitions/issues/37. -# - given: -# date: '2017-01-01' -# regions: ["au_tas"] -# expect: -# holiday: false - - given: date: '2017-01-02' - regions: ["au", "au_qld", "au_nsw", "au_act", "au_vic", "au_sa", "au_wa", "au_nt"] - options: ["observed"] + regions: ["au_qld", "au_nsw", "au_act", "au_vic", "au_sa", "au_wa", "au_nt"] expect: - name: "New Year's Day" + name: "Additional public holiday for New Year's Day" - given: date: '2017-01-02' regions: ["au_tas"] @@ -1023,12 +1021,12 @@ tests: name: "New Year's Day" - given: date: '2022-01-03' - regions: ["au_nsw, au_vic, au_act, au_sa, au_wa, au_nt, au_qld"] + regions: ["au_nsw", "au_vic", "au_act", "au_sa", "au_wa", "au_nt", "au_qld"] expect: name: "Additional public holiday for New Year's Day" - given: date: '2022-01-01' - regions: ["au_nsw, au_vic, au_act, au_sa, au_wa, au_nt, au_qld"] + regions: ["au_nsw", "au_vic", "au_act", "au_wa", "au_nt", "au_qld"] expect: name: "New Year's Day" - given: @@ -1048,14 +1046,9 @@ tests: name: "Easter Sunday" - given: date: '2021-08-02' - regions: ["au-nt"] + regions: ["au_nt"] expect: name: "Picnic Day" - - given: - date: '2022-04-17' - regions: ["au-nt"] - expect: - holiday: false - given: date: '2023-04-09' regions: ["au_nt"] diff --git a/lib/validation/error.rb b/lib/validation/error.rb index d3095be..a8cb117 100644 --- a/lib/validation/error.rb +++ b/lib/validation/error.rb @@ -5,6 +5,7 @@ class NoMonths < Error ; end class InvalidMonth < Error; end class InvalidMethod < Error; end class InvalidRegions < Error; end + class MissingDateSpecification < Error; end class InvalidCustomMethod < Error; end class InvalidTest < Error; end end diff --git a/lib/validation/month_validator.rb b/lib/validation/month_validator.rb index 505751a..86cabc8 100644 --- a/lib/validation/month_validator.rb +++ b/lib/validation/month_validator.rb @@ -16,11 +16,41 @@ def call(months) month_def['regions'].each do |region| raise Errors::InvalidRegions.new("A month must contain at least one region, received: #{months}") if region.nil? || region.empty? end + + # Validate that each holiday has at least one date specification + validate_date_specification!(month_def) end end true end + + private + + def validate_date_specification!(month_def) + has_mday = month_def.key?('mday') && !month_def['mday'].nil? + has_wday = month_def.key?('wday') && !month_def['wday'].nil? + has_week = month_def.key?('week') && !month_def['week'].nil? + has_function = month_def.key?('function') && !month_def['function'].nil? && !month_def['function'].empty? + + holiday_name = month_def['name'] || 'Unknown Holiday' + + # If wday is specified, week must also be specified (and vice versa) + if (has_wday && !has_week) || (!has_wday && has_week) + raise Errors::MissingDateSpecification.new( + "Holiday '#{holiday_name}' with 'wday' must also have 'week' attribute (and vice versa)" + ) + end + + # Must have either mday, function, or both wday AND week + valid_date_spec = has_mday || has_function || (has_wday && has_week) + + unless valid_date_spec + raise Errors::MissingDateSpecification.new( + "Holiday '#{holiday_name}' must have either 'mday', 'function', or both 'wday' and 'week' attributes to specify the date" + ) + end + end end end end diff --git a/spec/validation/month_validator_spec.rb b/spec/validation/month_validator_spec.rb index 53ff06c..ece85c0 100644 --- a/spec/validation/month_validator_spec.rb +++ b/spec/validation/month_validator_spec.rb @@ -68,4 +68,56 @@ } end end + + context 'date specification validation' do + it 'allows holiday with mday' do + months = { 1 => [{"name"=>"Test Holiday", "regions"=>["test"], "mday"=>1}] } + expect(subject.call(months)).to be true + end + + it 'allows holiday with function' do + months = { 1 => [{"name"=>"Test Holiday", "regions"=>["test"], "function"=>"custom_method(year)"}] } + expect(subject.call(months)).to be true + end + + it 'allows holiday with wday and week' do + months = { 1 => [{"name"=>"Test Holiday", "regions"=>["test"], "wday"=>1, "week"=>2}] } + expect(subject.call(months)).to be true + end + + it 'returns error if holiday has no date specification' do + months = { 1 => [{"name"=>"Test Holiday", "regions"=>["test"]}] } + expect { subject.call(months) }.to raise_error(Definitions::Errors::MissingDateSpecification) { |e| + expect(e.message).to eq("Holiday 'Test Holiday' must have either 'mday', 'function', or both 'wday' and 'week' attributes to specify the date") + } + end + + it 'returns error if holiday has wday but no week' do + months = { 1 => [{"name"=>"Test Holiday", "regions"=>["test"], "wday"=>1}] } + expect { subject.call(months) }.to raise_error(Definitions::Errors::MissingDateSpecification) { |e| + expect(e.message).to eq("Holiday 'Test Holiday' with 'wday' must also have 'week' attribute (and vice versa)") + } + end + + it 'returns error if holiday has week but no wday' do + months = { 1 => [{"name"=>"Test Holiday", "regions"=>["test"], "week"=>1}] } + expect { subject.call(months) }.to raise_error(Definitions::Errors::MissingDateSpecification) { |e| + expect(e.message).to eq("Holiday 'Test Holiday' with 'wday' must also have 'week' attribute (and vice versa)") + } + end + + it 'returns error if holiday has empty function' do + months = { 1 => [{"name"=>"Test Holiday", "regions"=>["test"], "function"=>""}] } + expect { subject.call(months) }.to raise_error(Definitions::Errors::MissingDateSpecification) { |e| + expect(e.message).to eq("Holiday 'Test Holiday' must have either 'mday', 'function', or both 'wday' and 'week' attributes to specify the date") + } + end + + it 'returns error if holiday has nil function' do + months = { 1 => [{"name"=>"Test Holiday", "regions"=>["test"], "function"=>nil}] } + expect { subject.call(months) }.to raise_error(Definitions::Errors::MissingDateSpecification) { |e| + expect(e.message).to eq("Holiday 'Test Holiday' must have either 'mday', 'function', or both 'wday' and 'week' attributes to specify the date") + } + end + end end