From bc0e4f16806507667518254af92b144f9db67bde Mon Sep 17 00:00:00 2001 From: Douglas Vought Date: Thu, 11 Jul 2024 01:59:12 -0400 Subject: [PATCH 1/8] Wrap year ranges in_year_ranges?/2 was failing because it expects a list for the first argument, but some rules produce a map instead of a list. --- lib/holidefs/holiday.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/holidefs/holiday.ex b/lib/holidefs/holiday.ex index f474919..4ee270e 100644 --- a/lib/holidefs/holiday.ex +++ b/lib/holidefs/holiday.ex @@ -25,14 +25,14 @@ defmodule Holidefs.Holiday do """ @spec from_rule(atom, Holidefs.Definition.Rule.t(), integer, Holidefs.Options.t()) :: [t] def from_rule(code, %Rule{year_ranges: year_ranges} = rule, year, opts \\ %Options{}) do - if in_year_ranges?(year_ranges, year) do + if in_year_ranges?(List.wrap(year_ranges), year) do build_from_rule(code, rule, year, opts) else [] end end - defp in_year_ranges?(nil, _), do: true + defp in_year_ranges?([nil], _), do: true defp in_year_ranges?(list, year) when is_list(list), do: Enum.all?(list, &in_year_range?(&1, year)) From 12b7cea46b8bdf6efeaf785ca9ab1a0e3d28e35e Mon Sep 17 00:00:00 2001 From: Douglas Vought Date: Thu, 11 Jul 2024 02:00:27 -0400 Subject: [PATCH 2/8] Add missing in_year_range/2 clauses --- lib/holidefs/holiday.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/holidefs/holiday.ex b/lib/holidefs/holiday.ex index 4ee270e..cef1fb2 100644 --- a/lib/holidefs/holiday.ex +++ b/lib/holidefs/holiday.ex @@ -40,7 +40,11 @@ defmodule Holidefs.Holiday do defp in_year_range?(%{"before" => before_year}, year), do: year <= before_year defp in_year_range?(%{"after" => after_year}, year), do: year >= after_year defp in_year_range?(%{"limited" => years}, year), do: year in years - defp in_year_range?(%{"between" => years}, year), do: year in years + defp in_year_range?(%{"between" => %{"start" => year_start, "end" => year_end}}, year) do + year in year_start..year_end + end + defp in_year_range?(%{"from" => from_year}, year), do: year >= from_year + defp in_year_range?(%{"until" => until_year}, year), do: year <= until_year defp build_from_rule(code, %Rule{function: fun} = rule, year, opts) when fun != nil do name = translate_name(code, rule.name) From 183571d9ad9f170319db56ac75c22473d10a0ab2 Mon Sep 17 00:00:00 2001 From: Douglas Vought Date: Thu, 11 Jul 2024 02:57:27 -0400 Subject: [PATCH 3/8] Use deps to get holiday definition files --- mix.exs | 8 +++++++- mix.lock | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 875a86c..d5e3b5a 100644 --- a/mix.exs +++ b/mix.exs @@ -96,7 +96,13 @@ defmodule Holidefs.Mixfile do {:gettext, "~> 0.23"}, {:inch_ex, only: :docs}, {:yaml_elixir, "~> 2.0"}, - {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false} + {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}, + {:holidays, + github: "holidays/definitions", + ref: "b2c3d6dda526245082a6bd93a6b438f1990f845b", + app: false, + compile: false, + depth: 1}, ] end end diff --git a/mix.lock b/mix.lock index 04ae4ff..384cedf 100644 --- a/mix.lock +++ b/mix.lock @@ -16,6 +16,7 @@ "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "gettext": {:hex, :gettext, "0.23.1", "821e619a240e6000db2fc16a574ef68b3bd7fe0167ccc264a81563cc93e67a31", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "19d744a36b809d810d610b57c27b934425859d158ebd56561bc41f7eeb8795db"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "holidays": {:git, "https://github.com/holidays/definitions.git", "b2c3d6dda526245082a6bd93a6b438f1990f845b", [ref: "b2c3d6dda526245082a6bd93a6b438f1990f845b"]}, "httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "inch_ex": {:hex, :inch_ex, "2.1.0-rc.1", "7642a8902c0d2ed5d9b5754b2fc88fedf630500d630fc03db7caca2e92dedb36", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4ceee988760f9382d1c1d0b93ea5875727f6071693e89a0a3c49c456ef1be75d"}, From e710953d3e3b630d2c6f194585bbfe4a2fb01777 Mon Sep 17 00:00:00 2001 From: Douglas Vought Date: Thu, 11 Jul 2024 02:57:45 -0400 Subject: [PATCH 4/8] Add missing date functions For Holidefs.Definition.CustomFunctions: - Add orthodox_easter_julian/2 as an alias - Add Rosh Hashanah dates up to 2052 - Add Yom Kippur dates up to 2052 - Add Matariki dates up to 2052 - Add epiphany/2 - Add to_near_monday_after/2 - Add qld_brisbane_ekka_holiday/2 - Add qld_kings_bday_october/2 - Add ch_be_zibelemaerit/2 - Add saint_josephs_day/2 - Add saint_peter_and_saint_paul/2 - Add assumption_of_mary/2 - Add columbus_day/2 - Add all_saints_day/2 - Add independence_of_cartagena/2 - Add co_shift_date/3 to assist other functions For Holidefs.DateCalculator: - Add to_nearest_monday_after/1 --- lib/holidefs/date_calculator.ex | 14 ++ lib/holidefs/definition/custom_functions.ex | 218 +++++++++++++++++--- 2 files changed, 204 insertions(+), 28 deletions(-) diff --git a/lib/holidefs/date_calculator.ex b/lib/holidefs/date_calculator.ex index c09c300..071632d 100644 --- a/lib/holidefs/date_calculator.ex +++ b/lib/holidefs/date_calculator.ex @@ -172,4 +172,18 @@ defmodule Holidefs.DateCalculator do |> next_beginning_of_month(month) |> Date.add(-1) end + + def to_nearest_monday_after(date) do + day = + case Date.day_of_week(date) do + 1 -> 0 + 2 -> 6 + 3 -> 5 + 4 -> 4 + 5 -> 3 + 6 -> 2 + 7 -> 1 + end + Date.add(date, day) + end end diff --git a/lib/holidefs/definition/custom_functions.ex b/lib/holidefs/definition/custom_functions.ex index 9bf9d1a..bc16039 100644 --- a/lib/holidefs/definition/custom_functions.ex +++ b/lib/holidefs/definition/custom_functions.ex @@ -30,6 +30,11 @@ defmodule Holidefs.Definition.CustomFunctions do DateCalculator.julian_orthodox_easter(year) end + @doc false + def orthodox_easter_julian(year, _) do + DateCalculator.julian_orthodox_easter(year) + end + @doc false def us_inauguration_day(year, rule) when rem(year, 4) == 1 do {:ok, date} = Date.new(year, rule.month, 20) @@ -54,34 +59,122 @@ defmodule Holidefs.Definition.CustomFunctions do end @doc false - def rosh_hashanah(year, _) do - map = %{ - 2014 => ~D[2014-09-25], - 2015 => ~D[2015-09-14], - 2016 => ~D[2016-10-03], - 2017 => ~D[2017-09-21], - 2018 => ~D[2018-09-10], - 2019 => ~D[2019-09-30], - 2020 => ~D[2020-09-19] - } - - map[year] - end - - @doc false - def yom_kippur(year, _) do - map = %{ - 2014 => ~D[2014-10-04], - 2015 => ~D[2015-09-23], - 2016 => ~D[2016-10-12], - 2017 => ~D[2017-09-30], - 2018 => ~D[2018-09-19], - 2019 => ~D[2019-10-09], - 2020 => ~D[2020-09-28] - } - - map[year] - end + def rosh_hashanah(2014, _), do: ~D[2014-09-25] + def rosh_hashanah(2015, _), do: ~D[2015-09-14] + def rosh_hashanah(2016, _), do: ~D[2016-10-03] + def rosh_hashanah(2017, _), do: ~D[2017-09-21] + def rosh_hashanah(2018, _), do: ~D[2018-09-10] + def rosh_hashanah(2019, _), do: ~D[2019-09-30] + def rosh_hashanah(2020, _), do: ~D[2020-09-19] + def rosh_hashanah(2021, _), do: ~D[2021-09-07] + def rosh_hashanah(2022, _), do: ~D[2022-09-26] + def rosh_hashanah(2023, _), do: ~D[2023-09-16] + def rosh_hashanah(2024, _), do: ~D[2024-10-03] + def rosh_hashanah(2025, _), do: ~D[2025-09-23] + def rosh_hashanah(2026, _), do: ~D[2026-09-12] + def rosh_hashanah(2027, _), do: ~D[2027-10-02] + def rosh_hashanah(2028, _), do: ~D[2028-09-21] + def rosh_hashanah(2029, _), do: ~D[2029-09-10] + def rosh_hashanah(2030, _), do: ~D[2030-09-28] + def rosh_hashanah(2031, _), do: ~D[2031-09-18] + def rosh_hashanah(2032, _), do: ~D[2032-09-06] + def rosh_hashanah(2033, _), do: ~D[2033-09-24] + def rosh_hashanah(2034, _), do: ~D[2034-09-14] + def rosh_hashanah(2035, _), do: ~D[2035-10-04] + def rosh_hashanah(2036, _), do: ~D[2036-09-22] + def rosh_hashanah(2037, _), do: ~D[2037-09-09] + def rosh_hashanah(2038, _), do: ~D[2038-09-30] + def rosh_hashanah(2039, _), do: ~D[2039-09-19] + def rosh_hashanah(2040, _), do: ~D[2040-09-08] + def rosh_hashanah(2041, _), do: ~D[2041-09-26] + def rosh_hashanah(2042, _), do: ~D[2042-09-15] + def rosh_hashanah(2043, _), do: ~D[2043-10-05] + def rosh_hashanah(2044, _), do: ~D[2044-09-22] + def rosh_hashanah(2045, _), do: ~D[2045-09-12] + def rosh_hashanah(2046, _), do: ~D[2046-10-01] + def rosh_hashanah(2047, _), do: ~D[2047-09-21] + def rosh_hashanah(2048, _), do: ~D[2048-09-08] + def rosh_hashanah(2049, _), do: ~D[2049-09-27] + def rosh_hashanah(2050, _), do: ~D[2050-09-17] + def rosh_hashanah(2051, _), do: ~D[2051-09-07] + def rosh_hashanah(2052, _), do: ~D[2052-09-24] + def rosh_hashanah(_, _), do: nil + + @doc false + def yom_kippur(2014, _), do: ~D[2014-10-04] + def yom_kippur(2015, _), do: ~D[2015-09-23] + def yom_kippur(2016, _), do: ~D[2016-10-12] + def yom_kippur(2017, _), do: ~D[2017-09-30] + def yom_kippur(2018, _), do: ~D[2018-09-19] + def yom_kippur(2019, _), do: ~D[2019-10-09] + def yom_kippur(2020, _), do: ~D[2020-09-28] + def yom_kippur(2021, _), do: ~D[2021-09-16] + def yom_kippur(2022, _), do: ~D[2022-10-05] + def yom_kippur(2023, _), do: ~D[2023-09-25] + def yom_kippur(2024, _), do: ~D[2024-10-12] + def yom_kippur(2025, _), do: ~D[2025-10-02] + def yom_kippur(2026, _), do: ~D[2026-09-21] + def yom_kippur(2027, _), do: ~D[2027-10-11] + def yom_kippur(2028, _), do: ~D[2028-09-30] + def yom_kippur(2029, _), do: ~D[2029-09-19] + def yom_kippur(2030, _), do: ~D[2030-10-07] + def yom_kippur(2031, _), do: ~D[2031-09-27] + def yom_kippur(2032, _), do: ~D[2032-09-15] + def yom_kippur(2033, _), do: ~D[2033-10-03] + def yom_kippur(2034, _), do: ~D[2034-09-23] + def yom_kippur(2035, _), do: ~D[2035-10-13] + def yom_kippur(2036, _), do: ~D[2036-10-01] + def yom_kippur(2037, _), do: ~D[2037-09-19] + def yom_kippur(2038, _), do: ~D[2038-10-09] + def yom_kippur(2039, _), do: ~D[2039-09-28] + def yom_kippur(2040, _), do: ~D[2040-09-17] + def yom_kippur(2041, _), do: ~D[2041-10-05] + def yom_kippur(2042, _), do: ~D[2042-09-24] + def yom_kippur(2043, _), do: ~D[2043-10-14] + def yom_kippur(2044, _), do: ~D[2044-10-01] + def yom_kippur(2045, _), do: ~D[2045-09-21] + def yom_kippur(2046, _), do: ~D[2046-10-10] + def yom_kippur(2047, _), do: ~D[2047-09-30] + def yom_kippur(2048, _), do: ~D[2048-09-17] + def yom_kippur(2049, _), do: ~D[2049-10-06] + def yom_kippur(2050, _), do: ~D[2050-09-26] + def yom_kippur(2051, _), do: ~D[2051-09-16] + def yom_kippur(2052, _), do: ~D[2052-10-03] + def yom_kippur(_, _), do: nil + + @doc false + def matariki(2022, _), do: ~D[2022-06-24] + def matariki(2023, _), do: ~D[2023-07-14] + def matariki(2024, _), do: ~D[2024-06-28] + def matariki(2025, _), do: ~D[2025-06-20] + def matariki(2026, _), do: ~D[2026-07-10] + def matariki(2027, _), do: ~D[2027-06-25] + def matariki(2028, _), do: ~D[2028-07-14] + def matariki(2029, _), do: ~D[2029-07-06] + def matariki(2030, _), do: ~D[2030-06-21] + def matariki(2031, _), do: ~D[2031-07-11] + def matariki(2032, _), do: ~D[2032-07-02] + def matariki(2033, _), do: ~D[2033-06-24] + def matariki(2034, _), do: ~D[2034-07-07] + def matariki(2035, _), do: ~D[2035-06-29] + def matariki(2036, _), do: ~D[2036-07-18] + def matariki(2037, _), do: ~D[2037-07-10] + def matariki(2038, _), do: ~D[2038-06-25] + def matariki(2039, _), do: ~D[2039-07-15] + def matariki(2040, _), do: ~D[2040-07-06] + def matariki(2041, _), do: ~D[2041-07-19] + def matariki(2042, _), do: ~D[2042-07-11] + def matariki(2043, _), do: ~D[2043-07-03] + def matariki(2044, _), do: ~D[2044-06-24] + def matariki(2045, _), do: ~D[2045-07-07] + def matariki(2046, _), do: ~D[2046-06-29] + def matariki(2047, _), do: ~D[2047-07-19] + def matariki(2048, _), do: ~D[2048-07-03] + def matariki(2049, _), do: ~D[2049-06-25] + def matariki(2050, _), do: ~D[2050-07-15] + def matariki(2051, _), do: ~D[2051-06-30] + def matariki(2052, _), do: ~D[2052-06-21] + def matariki(_, _), do: nil @doc false def election_day(year, _) do @@ -387,4 +480,73 @@ defmodule Holidefs.Definition.CustomFunctions do {:ok, date} = Date.new(year, 10, 31) DateCalculator.next_day_of_week(date, 6) end + + @doc false + def epiphany(year, _) do + {:ok, date} = Date.new(year, 1, 6) + day_of_week = Date.day_of_week(date) + + cond do + day_of_week > 1 && day_of_week < 7 -> + Date.add(date, 8 - day_of_week) + day_of_week == 7 -> + Date.add(date, 1) + true -> + date + end + end + + @doc false + def to_nearest_monday_after(year, rule) do + {:ok, date} = Date.new(year, rule.month, rule.day) + DateCalculator.to_nearest_monday_after(date) + end + + @doc false + def qld_brisbane_ekka_holiday(year, rule) do + first_friday = DateCalculator.nth_day_of_week(year, rule.month, 1, 5) + + date = if first_friday.day < 5 do + DateCalculator.nth_day_of_week(year, rule.month, 2, 5) + else + first_friday + end + + Date.add(date, 5) + end + + @doc false + def qld_kings_bday_october(year, rule) do + if year >= 2023 do + DateCalculator.nth_day_of_week(year, rule.month, 1, 1) + end + end + + @doc false + def ch_be_zibelemaerit(year, rule) do + DateCalculator.nth_day_of_week(year, rule.month, 4, 1) + end + + @doc false + def saint_josephs_day(year, rule), do: co_shift_date(year, rule, 19) + @doc false + def saint_peter_and_saint_paul(year, rule), do: co_shift_date(year, rule, 29) + @doc false + def assumption_of_mary(year, rule), do: co_shift_date(year, rule, 15) + @doc false + def columbus_day(year, rule), do: co_shift_date(year, rule, 12) + @doc false + def all_saints_day(year, rule), do: co_shift_date(year, rule, 1) + @doc false + def independence_of_cartagena(year, rule), do: co_shift_date(year, rule, 11) + + defp co_shift_date(year, rule, day) do + {:ok, date} = Date.new(year, rule.month, day) + + if (day_of_week = Date.day_of_week(date)) > 1 do + Date.add(date, 8 - day_of_week) + else + date + end + end end From cf137c870a011db4d1b217033a75b5a65f1a6d10 Mon Sep 17 00:00:00 2001 From: Douglas Vought Date: Thu, 11 Jul 2024 02:59:03 -0400 Subject: [PATCH 5/8] Add custom exceptions --- lib/holidefs/exceptions.ex | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 lib/holidefs/exceptions.ex diff --git a/lib/holidefs/exceptions.ex b/lib/holidefs/exceptions.ex new file mode 100644 index 0000000..0e97fcb --- /dev/null +++ b/lib/holidefs/exceptions.ex @@ -0,0 +1,26 @@ +defmodule Holidefs.Exceptions do + defmodule FunctionNotDefinedError do + defexception [:name] + + @impl Exception + def message(exception) do + "Function `#{exception.name}` is not defined." + end + end + defmodule InvalidRuleMapError do + defexception [:map] + + @impl Exception + def message(exception) do + """ + Expected a rule map with one of these sets of keys: + - `week` and `wday` + - `mday` + - `function` + + but got + #{inspect exception.map} + """ + end + end +end \ No newline at end of file From e6b0d448be8463d8e002a0493aed9e9b8358b869 Mon Sep 17 00:00:00 2001 From: Douglas Vought Date: Thu, 11 Jul 2024 03:22:52 -0400 Subject: [PATCH 6/8] Refactor build/3 and supporting functions Some rules failed to build because their maps didn't match the clauses in build/3. - Added week!/1 for validating weeks - Added weekday!/1 for validating weekdays as well as converting Ruby's Sunday value (0) to Elixir's Sunday value (7) so that manual edits of the definition files are no longer necessary - Changed function_from_name/1 to function_from_name!/1 and raise a FunctionNotDefinedError when the function isn't available - Added fall-through cases for function_from_name!/1 - Tests check for the new exceptions and verify the Ruby week day gets converted properly --- lib/holidefs/definition/rule.ex | 92 ++++++++++++++------------ test/holidefs/definition/rule_test.exs | 10 +-- 2 files changed, 55 insertions(+), 47 deletions(-) diff --git a/lib/holidefs/definition/rule.ex b/lib/holidefs/definition/rule.ex index d9f5ace..8eddbd8 100644 --- a/lib/holidefs/definition/rule.ex +++ b/lib/holidefs/definition/rule.ex @@ -4,8 +4,11 @@ defmodule Holidefs.Definition.Rule do when it happens on a year. """ + require Logger + alias Holidefs.Definition.CustomFunctions alias Holidefs.Definition.Rule + alias Holidefs.Exceptions.{FunctionNotDefinedError, InvalidRuleMapError} defstruct [ :name, @@ -38,50 +41,41 @@ defmodule Holidefs.Definition.Rule do @valid_weeks [-5, -4, -3, -2, -1, 1, 2, 3, 4, 5] @valid_weekdays 1..7 - @doc """ - Builds a new rule from its month and definition map - """ - @spec build(atom, integer, map) :: t - def build(code, month, %{"name" => name, "function" => func} = map) do - %Rule{ - name: name, + def build(code, month, %{"name" => name} = map) do + %{"year_ranges" => year_ranges, + "type" => type, + "observed" => observed, + "function" => function, + "function_modifier" => function_modifier} = fill_missing(map) + + rule = %Rule{ month: month, - day: map["mday"], - week: map["week"], - weekday: map["wday"], - year_ranges: map["year_ranges"], - informal?: map["type"] == "informal", - observed: observed_from_name(map["observed"]), + name: name, + year_ranges: year_ranges, + informal?: type == "informal", + observed: observed_from_name(observed), regions: load_regions(map, code), - function: function_from_name(func), - function_modifier: map["function_modifier"] + function: function_from_name!(function), + function_modifier: function_modifier } - end - def build(code, month, %{"name" => name, "week" => week, "wday" => wday} = map) - when week in @valid_weeks and wday in @valid_weekdays do - %Rule{ - name: name, - month: month, - week: week, - weekday: wday, - year_ranges: map["year_ranges"], - informal?: map["type"] == "informal", - observed: observed_from_name(map["observed"]), - regions: load_regions(map, code) - } + case map do + %{"week" => week, "wday" => wday} -> + %{rule | week: week!(week), weekday: weekday!(wday)} + %{"mday" => mday} -> + %{rule | day: mday} + %{"function" => function} when not is_nil(function) -> + rule + _ -> + raise InvalidRuleMapError, map: map + end end - def build(code, month, %{"name" => name, "mday" => day} = map) do - %Rule{ - name: name, - month: month, - day: day, - year_ranges: map["year_ranges"], - informal?: map["type"] == "informal", - observed: observed_from_name(map["observed"]), - regions: load_regions(map, code) - } + defp fill_missing(map) do + keys = ~w(year_ranges type observed function function_modifier) + Enum.reduce(keys, map, fn key, map -> + Map.put_new(map, key, nil) + end) end defp load_regions(%{"regions" => regions}, code) do @@ -93,18 +87,32 @@ defmodule Holidefs.Definition.Rule do end defp observed_from_name(nil), do: nil - defp observed_from_name(name), do: function_from_name(name) + defp observed_from_name(name), do: function_from_name!(name) @custom_functions :exports |> CustomFunctions.module_info() |> Keyword.keys() - defp function_from_name(name) when is_binary(name) do + defp function_from_name!(name) when is_binary(name) do name |> String.replace(~r/\(.+\)/, "") |> String.to_atom() - |> function_from_name() + |> function_from_name!() + end + + defp function_from_name!(:""), do: nil + defp function_from_name!(nil), do: nil + defp function_from_name!(name) when is_atom(name) and name in @custom_functions do + name + end + defp function_from_name!(name) when is_atom(name) do + raise FunctionNotDefinedError, name: name end - defp function_from_name(name) when is_atom(name) and name in @custom_functions, do: name + def week!(week) when week in @valid_weeks, do: week + def week!(nil), do: nil + # Ruby uses 0 for Sunday, but Elixir uses 7 + def weekday!(0), do: weekday!(7) + def weekday!(wday) when wday in @valid_weekdays, do: wday + def weekday!(nil), do: nil end diff --git a/test/holidefs/definition/rule_test.exs b/test/holidefs/definition/rule_test.exs index a042151..74c8aca 100644 --- a/test/holidefs/definition/rule_test.exs +++ b/test/holidefs/definition/rule_test.exs @@ -1,6 +1,7 @@ defmodule Holidefs.Definition.RuleTest do use ExUnit.Case alias Holidefs.Definition.Rule + alias Holidefs.Exceptions.{FunctionNotDefinedError, InvalidRuleMapError} doctest Rule @@ -32,11 +33,11 @@ defmodule Holidefs.Definition.RuleTest do Rule.build(:us, 0, %{"no_name" => "Bad"}) end - assert_raise FunctionClauseError, fn -> + assert_raise InvalidRuleMapError, fn -> Rule.build(:us, 0, %{"name" => "Bad"}) end - assert_raise FunctionClauseError, fn -> + assert_raise FunctionNotDefinedError, fn -> Rule.build(:us, 0, %{"name" => "Bad", "function" => "no_function()"}) end end @@ -46,9 +47,8 @@ defmodule Holidefs.Definition.RuleTest do assert %Rule{} = Rule.build(:us, 1, %{"name" => "Weekday test", "week" => 1, "wday" => i}) end - assert_raise FunctionClauseError, fn -> - Rule.build(:us, 1, %{"name" => "Weekday test", "week" => 1, "wday" => 0}) - end + assert %Rule{weekday: weekday} = Rule.build(:us, 1, %{"name" => "Weekday test", "week" => 1, "wday" => 0}) + assert weekday == 7 assert_raise FunctionClauseError, fn -> Rule.build(:us, 1, %{"name" => "Weekday test", "week" => 1, "wday" => 8}) From d772d35cbc3eaf321a97220dfa42fc195b0e053d Mon Sep 17 00:00:00 2001 From: Douglas Vought Date: Thu, 11 Jul 2024 03:29:52 -0400 Subject: [PATCH 7/8] Add the ability supply a :deps argument to load!/3 When :deps is supplied in the third argument, we use the holiday definitions from the deps path instead of the priv directory. --- lib/holidefs/definition.ex | 28 ++++++++++++++++++---------- test/holidefs/definition_test.exs | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/lib/holidefs/definition.ex b/lib/holidefs/definition.ex index 1ffcb73..ffb5513 100644 --- a/lib/holidefs/definition.ex +++ b/lib/holidefs/definition.ex @@ -4,6 +4,8 @@ defmodule Holidefs.Definition do the year. """ + @holidays_deps_path Path.join(Mix.Project.deps_path(), "holidays") + alias Holidefs.Definition alias Holidefs.Definition.Rule @@ -20,17 +22,23 @@ defmodule Holidefs.Definition do @doc """ Returns the path for the given locale definition file. """ - @spec file_path(atom, Path.t()) :: binary - def file_path(code, path \\ path()), do: Path.join(path, "#{code}.yaml") + @spec file_path(atom, atom) :: binary + def file_path(code, path_type \\ :default), do: Path.join(path(path_type), "#{code}.yaml") @doc """ Returns the path where all the locale definitions are saved. """ - @spec path() :: Path.t() - def path() do + @spec path(atom) :: Path.t() + def path(type \\ :default) + + def path(:default) do Path.join(:code.priv_dir(:holidefs), "/calendars/definitions") end + def path(:deps) do + @holidays_deps_path + end + @doc """ Loads the definition for a locale code and name. @@ -38,9 +46,9 @@ defmodule Holidefs.Definition do If any definition rule is invalid, a `RuntimeError` will be raised """ - @spec load!(atom, String.t()) :: t - def load!(code, name) do - case read_file(code) do + @spec load!(atom, String.t(), atom) :: t | nil + def load!(code, name, path_type \\ :default) do + case read_file(code, path_type) do {:ok, file_data} -> rules = file_data @@ -56,14 +64,14 @@ defmodule Holidefs.Definition do } {:error, _} -> - Logger.warn("Definition file for #{code} not found.") + Logger.warning("Definition file for #{code} not found.") nil end end - defp read_file(code) do + defp read_file(code, path_type \\ :default) do code - |> file_path() + |> file_path(path_type) |> to_charlist() |> YamlElixir.read_from_file() end diff --git a/test/holidefs/definition_test.exs b/test/holidefs/definition_test.exs index 6923677..35e8060 100644 --- a/test/holidefs/definition_test.exs +++ b/test/holidefs/definition_test.exs @@ -28,4 +28,29 @@ defmodule Holidefs.DefinitionTest do {"Natal", 12, 25, nil} ] end + + test "load!/2 loads the given locale from the deps path" do + assert %Definition{} = definition = Definition.load!(:br, "Brazil", :deps) + assert definition.code == :br + assert definition.name == "Brazil" + + assert Enum.map(definition.rules, fn rule -> + fun_res = if rule.function, do: CustomFunctions.call(rule.function, 2017, rule) + {rule.name, rule.month, rule.day, fun_res} + end) == [ + {"Carnaval", 0, nil, ~D[2017-02-28]}, + {"Sexta-feira Santa", 0, nil, ~D[2017-04-14]}, + {"Páscoa", 0, nil, ~D[2017-04-16]}, + {"Corpus Christi", 0, nil, ~D[2017-06-15]}, + {"Dia da Confraternização Universal", 1, 1, nil}, + {"Dia de Tiradentes", 4, 21, nil}, + {"Dia do Trabalho", 5, 1, nil}, + {"Proclamação da Independência", 9, 7, nil}, + {"Dia de Nossa Senhora Aparecida", 10, 12, nil}, + {"Dia de Finados", 11, 2, nil}, + {"Proclamação da República", 11, 15, nil}, + {"Dia Nacional de Zumbi e da Consciência Negra", 11, 20, nil}, + {"Natal", 12, 25, nil} + ] + end end From d83db15810b7b6aad85ef91c5db0219bec715b47 Mon Sep 17 00:00:00 2001 From: Douglas Vought Date: Thu, 11 Jul 2024 03:40:35 -0400 Subject: [PATCH 8/8] Format and apply credo suggestions --- config/config.exs | 2 +- lib/holidefs/date_calculator.ex | 1 + lib/holidefs/definition.ex | 4 ++-- lib/holidefs/definition/custom_functions.ex | 22 ++++++++++++--------- lib/holidefs/definition/rule.ex | 16 +++++++++++---- lib/holidefs/definition/store.ex | 5 ++++- lib/holidefs/exceptions.ex | 11 ++++++++--- lib/holidefs/holiday.ex | 4 +++- mix.exs | 10 +++++----- test/holidefs/definition/rule_test.exs | 4 +++- 10 files changed, 52 insertions(+), 27 deletions(-) diff --git a/config/config.exs b/config/config.exs index 3d474da..d0a6f0c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -21,4 +21,4 @@ import Config # config :logger, level: :info # -import_config "#{config_env()}.exs" \ No newline at end of file +import_config "#{config_env()}.exs" diff --git a/lib/holidefs/date_calculator.ex b/lib/holidefs/date_calculator.ex index 071632d..6221464 100644 --- a/lib/holidefs/date_calculator.ex +++ b/lib/holidefs/date_calculator.ex @@ -184,6 +184,7 @@ defmodule Holidefs.DateCalculator do 6 -> 2 7 -> 1 end + Date.add(date, day) end end diff --git a/lib/holidefs/definition.ex b/lib/holidefs/definition.ex index ffb5513..2ba2c22 100644 --- a/lib/holidefs/definition.ex +++ b/lib/holidefs/definition.ex @@ -29,8 +29,8 @@ defmodule Holidefs.Definition do Returns the path where all the locale definitions are saved. """ @spec path(atom) :: Path.t() - def path(type \\ :default) - + def path(type \\ :default) + def path(:default) do Path.join(:code.priv_dir(:holidefs), "/calendars/definitions") end diff --git a/lib/holidefs/definition/custom_functions.ex b/lib/holidefs/definition/custom_functions.ex index bc16039..69404b5 100644 --- a/lib/holidefs/definition/custom_functions.ex +++ b/lib/holidefs/definition/custom_functions.ex @@ -94,7 +94,7 @@ defmodule Holidefs.Definition.CustomFunctions do def rosh_hashanah(2046, _), do: ~D[2046-10-01] def rosh_hashanah(2047, _), do: ~D[2047-09-21] def rosh_hashanah(2048, _), do: ~D[2048-09-08] - def rosh_hashanah(2049, _), do: ~D[2049-09-27] + def rosh_hashanah(2049, _), do: ~D[2049-09-27] def rosh_hashanah(2050, _), do: ~D[2050-09-17] def rosh_hashanah(2051, _), do: ~D[2051-09-07] def rosh_hashanah(2052, _), do: ~D[2052-09-24] @@ -485,12 +485,14 @@ defmodule Holidefs.Definition.CustomFunctions do def epiphany(year, _) do {:ok, date} = Date.new(year, 1, 6) day_of_week = Date.day_of_week(date) - + cond do day_of_week > 1 && day_of_week < 7 -> Date.add(date, 8 - day_of_week) + day_of_week == 7 -> Date.add(date, 1) + true -> date end @@ -505,12 +507,13 @@ defmodule Holidefs.Definition.CustomFunctions do @doc false def qld_brisbane_ekka_holiday(year, rule) do first_friday = DateCalculator.nth_day_of_week(year, rule.month, 1, 5) - - date = if first_friday.day < 5 do - DateCalculator.nth_day_of_week(year, rule.month, 2, 5) - else - first_friday - end + + date = + if first_friday.day < 5 do + DateCalculator.nth_day_of_week(year, rule.month, 2, 5) + else + first_friday + end Date.add(date, 5) end @@ -542,8 +545,9 @@ defmodule Holidefs.Definition.CustomFunctions do defp co_shift_date(year, rule, day) do {:ok, date} = Date.new(year, rule.month, day) + day_of_week = Date.day_of_week(date) - if (day_of_week = Date.day_of_week(date)) > 1 do + if day_of_week > 1 do Date.add(date, 8 - day_of_week) else date diff --git a/lib/holidefs/definition/rule.ex b/lib/holidefs/definition/rule.ex index 8eddbd8..6636526 100644 --- a/lib/holidefs/definition/rule.ex +++ b/lib/holidefs/definition/rule.ex @@ -42,12 +42,14 @@ defmodule Holidefs.Definition.Rule do @valid_weekdays 1..7 def build(code, month, %{"name" => name} = map) do - %{"year_ranges" => year_ranges, + %{ + "year_ranges" => year_ranges, "type" => type, "observed" => observed, "function" => function, - "function_modifier" => function_modifier} = fill_missing(map) - + "function_modifier" => function_modifier + } = fill_missing(map) + rule = %Rule{ month: month, name: name, @@ -62,10 +64,13 @@ defmodule Holidefs.Definition.Rule do case map do %{"week" => week, "wday" => wday} -> %{rule | week: week!(week), weekday: weekday!(wday)} + %{"mday" => mday} -> %{rule | day: mday} + %{"function" => function} when not is_nil(function) -> rule + _ -> raise InvalidRuleMapError, map: map end @@ -73,6 +78,7 @@ defmodule Holidefs.Definition.Rule do defp fill_missing(map) do keys = ~w(year_ranges type observed function function_modifier) + Enum.reduce(keys, map, fn key, map -> Map.put_new(map, key, nil) end) @@ -102,9 +108,11 @@ defmodule Holidefs.Definition.Rule do defp function_from_name!(:""), do: nil defp function_from_name!(nil), do: nil - defp function_from_name!(name) when is_atom(name) and name in @custom_functions do + + defp function_from_name!(name) when is_atom(name) and name in @custom_functions do name end + defp function_from_name!(name) when is_atom(name) do raise FunctionNotDefinedError, name: name end diff --git a/lib/holidefs/definition/store.ex b/lib/holidefs/definition/store.ex index 3550503..0cc538b 100644 --- a/lib/holidefs/definition/store.ex +++ b/lib/holidefs/definition/store.ex @@ -5,7 +5,10 @@ defmodule Holidefs.Definition.Store do alias Holidefs.Definition - definitions = Holidefs.locales() |> Enum.map(fn {c, n} -> Definition.load!(c, n) end) |> Enum.reject(&is_nil/1) + definitions = + Holidefs.locales() + |> Enum.map(fn {c, n} -> Definition.load!(c, n) end) + |> Enum.reject(&is_nil/1) @doc """ Returns all the loaded definitions with their rules. diff --git a/lib/holidefs/exceptions.ex b/lib/holidefs/exceptions.ex index 0e97fcb..9539431 100644 --- a/lib/holidefs/exceptions.ex +++ b/lib/holidefs/exceptions.ex @@ -1,5 +1,8 @@ defmodule Holidefs.Exceptions do + @moduledoc false + defmodule FunctionNotDefinedError do + @moduledoc false defexception [:name] @impl Exception @@ -7,7 +10,9 @@ defmodule Holidefs.Exceptions do "Function `#{exception.name}` is not defined." end end + defmodule InvalidRuleMapError do + @moduledoc false defexception [:map] @impl Exception @@ -17,10 +22,10 @@ defmodule Holidefs.Exceptions do - `week` and `wday` - `mday` - `function` - + but got - #{inspect exception.map} + #{inspect(exception.map)} """ end end -end \ No newline at end of file +end diff --git a/lib/holidefs/holiday.ex b/lib/holidefs/holiday.ex index cef1fb2..c5ee00a 100644 --- a/lib/holidefs/holiday.ex +++ b/lib/holidefs/holiday.ex @@ -40,9 +40,11 @@ defmodule Holidefs.Holiday do defp in_year_range?(%{"before" => before_year}, year), do: year <= before_year defp in_year_range?(%{"after" => after_year}, year), do: year >= after_year defp in_year_range?(%{"limited" => years}, year), do: year in years - defp in_year_range?(%{"between" => %{"start" => year_start, "end" => year_end}}, year) do + + defp in_year_range?(%{"between" => %{"start" => year_start, "end" => year_end}}, year) do year in year_start..year_end end + defp in_year_range?(%{"from" => from_year}, year), do: year >= from_year defp in_year_range?(%{"until" => until_year}, year), do: year <= until_year diff --git a/mix.exs b/mix.exs index d5e3b5a..0a118de 100644 --- a/mix.exs +++ b/mix.exs @@ -98,11 +98,11 @@ defmodule Holidefs.Mixfile do {:yaml_elixir, "~> 2.0"}, {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}, {:holidays, - github: "holidays/definitions", - ref: "b2c3d6dda526245082a6bd93a6b438f1990f845b", - app: false, - compile: false, - depth: 1}, + github: "holidays/definitions", + ref: "b2c3d6dda526245082a6bd93a6b438f1990f845b", + app: false, + compile: false, + depth: 1} ] end end diff --git a/test/holidefs/definition/rule_test.exs b/test/holidefs/definition/rule_test.exs index 74c8aca..a587da4 100644 --- a/test/holidefs/definition/rule_test.exs +++ b/test/holidefs/definition/rule_test.exs @@ -47,7 +47,9 @@ defmodule Holidefs.Definition.RuleTest do assert %Rule{} = Rule.build(:us, 1, %{"name" => "Weekday test", "week" => 1, "wday" => i}) end - assert %Rule{weekday: weekday} = Rule.build(:us, 1, %{"name" => "Weekday test", "week" => 1, "wday" => 0}) + assert %Rule{weekday: weekday} = + Rule.build(:us, 1, %{"name" => "Weekday test", "week" => 1, "wday" => 0}) + assert weekday == 7 assert_raise FunctionClauseError, fn ->