Skip to content

Instantly share code, notes, and snippets.

@pmarreck
Created May 23, 2024 19:13
Show Gist options
  • Save pmarreck/0b60a7732fe50d670bdda320d02563df to your computer and use it in GitHub Desktop.
Save pmarreck/0b60a7732fe50d670bdda320d02563df to your computer and use it in GitHub Desktop.
Computing holidays with a DSL in Elixir
defmodule Addigence.Holidays do
use Timex
@holidays [
{:new_years_day, [:fixed, 1, :January]},
{:new_years_day_observed, [:nearest_weekday_to, 1, :January]},
{:martin_luther_king_jr_day, [:third, :monday, :January]},
{:washingtons_birthday, [:third, :monday, :February]},
{:good_friday, &Addigence.Holidays.compute_good_friday/1},
{:memorial_day, [:last, :monday, :May]},
{:juneteenth, [:fixed, 19, :June]},
{:independence_day, [:fixed, 4, :July]},
{:independence_day_observed, [:nearest_weekday_to, 4, :July]},
{:labor_day, [:first, :monday, :September]},
{:thanksgiving_day, [:fourth, :thursday, :November]},
{:christmas_day, [:fixed, 25, :December]},
{:christmas_day_observed, [:nearest_weekday_to, 25, :December]},
{:easter, &Addigence.Holidays.compute_easter/1}
]
@months %{
January: 1,
February: 2,
March: 3,
April: 4,
May: 5,
June: 6,
July: 7,
August: 8,
September: 9,
October: 10,
November: 11,
December: 12
}
@weeks %{
first: 1,
second: 2,
third: 3,
fourth: 4
}
@weekdays %{
monday: 1,
tuesday: 2,
wednesday: 3,
thursday: 4,
friday: 5,
saturday: 6,
sunday: 7
}
def nyse_holidays(year) do
@holidays
|> Enum.map(fn {holiday, desc} -> {holiday, compute_holiday(year, desc)} end)
|> Enum.filter(fn {_holiday, date} -> date != nil end)
end
def get_nth_weekday(date, week, weekday) when week in 1..4 and weekday in 1..7 do
date
|> Timex.beginning_of_month()
|> Timex.shift(days: days_to_next_weekday(date, weekday))
|> Timex.shift(weeks: week - 1)
end
defp days_to_next_weekday(date, sought_weekday) do # 1 is Monday, 2 is Tuesday etc.
current_weekday = Timex.weekday(date)
if sought_weekday == current_weekday do
0
else
rem(sought_weekday - current_weekday + 7, 7)
end
end
def compute_holiday(year, [:fixed, day, month]) do
Date.new!(year, @months[month], day)
end
def compute_holiday(year, [:nearest_weekday_to, day, month]) do
date = Date.new!(year, @months[month], day)
adjust_for_weekend(date)
end
def compute_holiday(year, [:last, weekday, month]) do
date = Date.new!(year, @months[month], Timex.days_in_month(Date.new!(year, @months[month], 1)))
if Timex.weekday(date) == @weekdays[weekday] do
date
else
start_search_date = Timex.shift(date, days: -6)
Timex.shift(start_search_date, days: days_to_next_weekday(start_search_date, @weekdays[weekday]))
end
end
def compute_holiday(year, [week, weekday, month]) do
date = Date.new!(year, @months[month], 1)
get_nth_weekday(date, @weeks[week], @weekdays[weekday])
end
def compute_holiday(year, special_function) when is_function(special_function) do
special_function.(year)
end
def compute_good_friday(year) do
easter = compute_easter(year)
Timex.shift(easter, days: -2)
end
def compute_easter(year) do
# Using the Anonymous Gregorian algorithm
a = rem(year, 19)
b = div(year, 100)
c = rem(year, 100)
d = div(b, 4)
e = rem(b, 4)
f = div(b + 8, 25)
g = div(b - f + 1, 3)
h = rem(19 * a + b - d - g + 15, 30)
i = div(c, 4)
k = rem(c, 4)
l = rem(32 + 2 * e + 2 * i - h - k, 7)
m = div(a + 11 * h + 22 * l, 451)
month = div(h + l - 7 * m + 114, 31)
day = rem(h + l - 7 * m + 114, 31) + 1
Date.new!(year, month, day)
end
def adjust_for_weekend(date) do
case Timex.weekday(date) do
6 -> Timex.shift(date, days: -1) # If Saturday, shift to Friday
7 -> Timex.shift(date, days: 1) # If Sunday, shift to Monday
_ -> date
end
end
end
defmodule Addigence.HolidaysTest do
use ExUnit.Case, async: true
alias Addigence.Holidays
test "NYSE holidays in 2024" do
holidays = Holidays.nyse_holidays(2024) # |> IO.inspect
assert Enum.any?(holidays, fn date -> date == {:new_years_day, ~D[2024-01-01]} end), "New Year's Day"
assert Enum.any?(holidays, fn date -> date == {:martin_luther_king_jr_day, ~D[2024-01-15]} end), "Martin Luther King Jr. Day"
assert Enum.any?(holidays, fn date -> date == {:washingtons_birthday, ~D[2024-02-19]} end), "Washington's Birthday"
assert Enum.any?(holidays, fn date -> date == {:good_friday, ~D[2024-03-29]} end), "Good Friday"
assert Enum.any?(holidays, fn date -> date == {:memorial_day, ~D[2024-05-27]} end), "Memorial Day"
assert Enum.any?(holidays, fn date -> date == {:juneteenth, ~D[2024-06-19]} end), "Juneteenth"
assert Enum.any?(holidays, fn date -> date == {:independence_day, ~D[2024-07-04]} end), "Independence Day"
assert Enum.any?(holidays, fn date -> date == {:labor_day, ~D[2024-09-02]} end), "Labor Day"
assert Enum.any?(holidays, fn date -> date == {:thanksgiving_day, ~D[2024-11-28]} end), "Thanksgiving Day"
assert Enum.any?(holidays, fn date -> date == {:christmas_day, ~D[2024-12-25]} end), "Christmas Day"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment