lib/holidefs/holiday.ex

defmodule Holidefs.Holiday do
  @moduledoc """
  A holiday itself.
  """

  alias Holidefs.DateCalculator
  alias Holidefs.Definition.CustomFunctions
  alias Holidefs.Definition.Rule
  alias Holidefs.Holiday
  alias Holidefs.Options

  defstruct [:name, :raw_date, :observed_date, :date, :uid, informal?: false]

  @type t :: %Holidefs.Holiday{
          name: String.t(),
          raw_date: Date.t(),
          observed_date: Date.t(),
          date: Date.t(),
          informal?: boolean,
          uid: String.t()
        }

  @doc """
  Returns a list of holidays for the definition rule on the given year
  """
  @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
      build_from_rule(code, rule, year, opts)
    else
      []
    end
  end

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

  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 build_from_rule(code, %Rule{function: fun} = rule, year, opts) when fun != nil do
    name = translate_name(code, rule.name)

    case CustomFunctions.call(fun, year, rule) do
      list when is_list(list) ->
        for date <- list do
          %Holiday{
            name: name,
            raw_date: date,
            observed_date: load_observed(rule, date),
            date: load_date(rule, date, opts),
            informal?: rule.informal?,
            uid: generate_uid(code, year, rule.name)
          }
        end

      %Date{} = date ->
        [
          %Holiday{
            name: name,
            raw_date: date,
            observed_date: load_observed(rule, date),
            date: load_date(rule, date, opts),
            informal?: rule.informal?,
            uid: generate_uid(code, year, rule.name)
          }
        ]

      nil ->
        []
    end
  end

  defp build_from_rule(code, %Rule{month: month, day: day} = rule, year, opts)
       when nil not in [month, day] do
    {:ok, date} = Date.new(year, month, day)

    [
      %Holiday{
        name: translate_name(code, rule.name),
        raw_date: date,
        observed_date: load_observed(rule, date),
        date: load_date(rule, date, opts),
        informal?: rule.informal?,
        uid: generate_uid(code, year, rule.name)
      }
    ]
  end

  defp build_from_rule(code, %Rule{} = rule, year, opts) do
    date = DateCalculator.nth_day_of_week(year, rule.month, rule.week, rule.weekday)

    [
      %Holiday{
        name: translate_name(code, rule.name),
        raw_date: date,
        observed_date: load_observed(rule, date),
        date: load_date(rule, date, opts),
        informal?: rule.informal?,
        uid: generate_uid(code, year, rule.name)
      }
    ]
  end

  defp load_date(rule, date, %Options{observed?: observed?}) do
    load_date(rule, date, observed?)
  end

  defp load_date(rule, date, true) do
    load_observed(rule, date)
  end

  defp load_date(_rule, date, false) do
    date
  end

  defp load_observed(%Rule{observed: nil}, date), do: date
  defp load_observed(%Rule{observed: fun} = rule, date), do: CustomFunctions.call(fun, date, rule)

  defp generate_uid(code, year, name) do
    <<sha1::128, _::32>> = :crypto.hash(:sha, name)

    hash =
      <<sha1::128>>
      |> Base.encode16()
      |> String.downcase()

    "#{code}-#{year}-#{hash}"
  end

  @doc """
  Returns the translated name of the given holiday
  """
  @spec translate_name(atom, String.t()) :: String.t()
  def translate_name(code, name) do
    Gettext.dgettext(Holidefs.Gettext, Atom.to_string(code), name)
  end
end