lib/holidefs/definition/rule.ex

defmodule Holidefs.Definition.Rule do
  @moduledoc """
  A definition rule has the information about the event and
  when it happens on a year.
  """

  alias Holidefs.Definition.CustomFunctions
  alias Holidefs.Definition.Rule

  defstruct [
    :name,
    :month,
    :day,
    :week,
    :weekday,
    :function,
    :function_modifier,
    :regions,
    :observed,
    :year_ranges,
    informal?: false
  ]

  @type t :: %Rule{
          name: String.t(),
          month: integer,
          day: integer,
          week: integer,
          weekday: integer,
          function: atom | nil,
          function_modifier: integer | nil,
          regions: [String.t()],
          observed: atom | nil,
          year_ranges: map | nil,
          informal?: boolean
        }

  @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,
      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"]),
      regions: load_regions(map, code),
      function: function_from_name(func),
      function_modifier: map["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)
    }
  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)
    }
  end

  defp load_regions(%{"regions" => regions}, code) do
    Enum.map(regions, &String.replace(&1, "#{code}_", ""))
  end

  defp load_regions(_, _) do
    []
  end

  defp observed_from_name(nil), do: nil
  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
    name
    |> String.replace(~r/\(.+\)/, "")
    |> String.to_atom()
    |> function_from_name()
  end

  defp function_from_name(name) when is_atom(name) and name in @custom_functions, do: name
end