Current section
Files
Jump to
Current section
Files
lib/holidays.ex
defmodule Holidays do
use Holidays.Define
@moduledoc """
Provides the `on` function that gives a list of holidays for a date within
specified regions.
Dates in Erlang (and therefore Elixir) are represented by the tuple
`{year, month, day}`.
This module uses pattern matching as much as possible. So, when a holiday
occurs on a fixed month and day every year, it will match on a clause
that looks something like this:
defp do_on({_year, 1, 1}, :us), do: [%{name: "New Year's Day"}]
A holiday that occurs on a certain week and week day will match like this:
defp do_on(5, :last, :monday, :us), do: [%{name: "Memorial Day"}]
Regions are often country codes, like `:us` or `:ca`, but
may also be entities such as UPS (`:ups`) or the New York Stock Exchange
(`:nyse`).
They can sometimes also be states/provinces, like `:us_ca` or `:us_dc`.
Holidays are defined within modules in the `lib/holidays/definitions`
directory which use the `holiday` macro from `Holidays.Define`.
It's fairly easy to take yaml files from the original Ruby implementation
and translate them into the Elixir definition modules. The yaml files are
included in the root `definitions` folder. They just haven't all been
translated yet.
"""
alias Holidays.DateCalculator.DateMath
@type weekday :: :monday | :tuesday | :wednesday | :thursday | :friday | :saturday | :sunday
@type week :: :first | :second | :third | :fourth | :last
@type region :: atom
@doc """
Returns a list of holidays on the given `date` for the specified `regions`.
## Examples
iex> Holidays.on({2016, 1, 1}, [:us])
[%{name: "New Year's Day"}]
"""
@spec on(:calendar.date, [region]) :: list
def on(date, regions) do
regions
|> Enum.flat_map(&(do_on(date, &1)))
end
for {name, _mod, definition} = holiday <- @holidays do
# IO.inspect name
# IO.inspect mod
month = case Dict.fetch(definition, :month) do
{:ok, m} -> m
_ -> nil
end
regions = Dict.fetch!(definition, :regions)
result = Code.string_to_quoted!(~s/[%{name: #{inspect name}}]/)
cond do
Dict.has_key?(definition, :day) ->
# IO.inspect name
day = Dict.fetch!(definition, :day)
Enum.each(regions, fn region ->
defp do_on({_year,
unquote(month),
unquote(day)},
unquote(region)), do: unquote(result)
end)
Dict.has_key?(definition, :week) ->
week = Dict.fetch!(definition, :week)
weekday = Dict.fetch!(definition, :weekday)
Enum.each(regions, fn region ->
defp do_on(unquote(month),
unquote(week),
unquote(weekday),
unquote(region)), do: unquote(result)
end)
Dict.has_key?(definition, :function) ->
@special_days holiday
end
end
defp do_on({_year, month, _day} = date, region) do
(DateMath.get_week_and_weekday(date)
|> Enum.flat_map(fn {week, weekday} -> do_on(month, week, weekday, region) end))
++ special_days(date, region)
end
defp do_on(_month, _week, _wday, _region), do: []
defp special_days(date, region) do
@special_days
|> Stream.filter(fn {_, _, definition} ->
Enum.any?(Dict.fetch!(definition, :regions), fn r -> r == region end)
end)
|> Stream.filter(fn {_, mod, %{function: fun, regions: regions}} ->
Enum.any?(regions, fn r -> r == region end) &&
special_day(date, mod, fun) == date
end)
|> Enum.map(fn {name, _, _} -> %{name: name} end)
end
defp special_day(date, mod, {fun_name, args, days}) do
special_day(date, mod, {fun_name, args})
|> DateMath.add_days(days)
end
defp special_day({year, _, _}, mod, {fun_name, [:year]}) do
if module_has_function?(mod, fun_name) do
apply(mod, fun_name, [year])
else
apply(__MODULE__, fun_name, [year])
end
end
@doc """
Wrapper for `Holidays.DateCalculator.Easter.gregorian_easter_for(year)`
so Definition modules don't all need their own copy.
For eample, in `Holidays.Definitions.Us`, Easter Sunday is defined like this:
holiday "Easter Sunday",
%{regions: [:us],
function: {:easter, [:year]},
type: :informal}
"""
def easter(year) do
Holidays.DateCalculator.Easter.gregorian_easter_for(year)
end
defp module_has_function?(mod, function_name) do
Keyword.has_key?(mod.__info__(:functions), function_name)
end
end