omni-

thin·ker

2026-05-11

How I wrote the open source Elixir library HolidayEx

I think it will be fun to share the thought process going into writing the HolidayEx library

When writing my time keeping application, Clockd, I was in desperate need for code that could tell me whether a date was a holiday or not. Having picked a niche language like Elixir, the hopes of finding a well supported library, that did such a thing, was slim to none. After having searched for a while I stumbled over what must have been the de facto library for doing this: CoderDenis' Holidays library. With exactly 23 stars on github, it was the highest starred elixir holiday library that I could find. Knowing that it did not have that many users, I gathered that if there were bugs they would not necessarily be reflected in the issues, nor would the library receive much attention from anybody.

I started out my journey with this library by looking at the documentation. Immediately I saw that I had to start a separate elixir application (process) to be able to query dates. This seemed strange to me. Reading some more of the documentation I found out that the rationale for this approach was lazy loading locales. The lazy loading enables the user of the library to choose just how much memory they want to use for the library. Ok, processes are lightweight, but the library did not feel very ergonomic.

Apart from the weird api, it did seem sensible to not load unused locales into memory. So I decided to use the library in my application, being careful to wrap it with a module so that I could easily replace the library.

After a while having used the library I felt that nagging feeling that I could probably do better. Maybe I could fork the library, maybe I could submit and issue, even better I could write a pull request. So now I had no choice but to look at the implementation. What I found was interesting...

What I found was a folder called "definitions", inside was a bunch of yaml files named after their corresponding locales. Looking at the yaml I saw something resembling this:

months:
0:
- name: Fastelavn
regions: ["no"]
function: easter(year)-49
type: informal
- name: Kristi Himmelfartsdag
regions: ["no"]
function: easter(year)+39
- name: 1. pinsedag
regions: ["no"]
function: easter(year)+49
regions: ["no"]
mday: 17
...
12:
- name: Julaften
regions: ["no"]
mday: 24
type: informal
...

(shortened for brevity) So basically just a nested mapping from months to days, with a special 0 month where easter function definitions are defined. A month day combination could easily just be a map, while the easter calculations could possibly be done dynamically if I had the given year. (There were some other fields more relevant to the different locales, but I have left them out of this article.)

I found out that the yaml files were extracted from the Ruby gem Holiday, in other words, the de facto Ruby library for calculating what dates are holidays. The definitions that the library used were based on these files, reduced down to just few locales. The genserver worked by doing a lookup in these definitions. Sensible enough.

Looking at the issues that CoderDennis had written it immediately struck me that he had some of the same thoughts about the library that I had. Especially the issue called "Change to pure functions". In the description of this issue he had written the following:

"Remove the requirements of starting the :holidays application and calling init on each module before it is used." I had come to the same conclusion. I would add another issue, that the number of yaml files that had been translated to elixir was very limited, making the library unusable to most of the world.

I thought about how I would solve the lazy loading issue without a genserver. "Maybe macros would be the answer. Hmm, how could I generate the code and load it into memory, compiling on demand".

Breaking the problem down I thought of two interesting improvements I could make upon the library.

  1. I could convert the yaml files to elixir. Elixir functions could basically just pattern match on the dates and give back the holiday name. This would leverage the highly optimized pattern matching that elixir has. I could write a simple codegen script that would read all the yaml files, and produce the corresponding elixir code.
  2. I could dynamically compile these "locale".ex files on demand by using the __using__/"use" construct in elixir, to get genuine compile time loading of only the necessary locales, taking a minimal amount of memory.

An interesting consequence of the conversion to elixir would be that I could also get rid of any dependency to a yaml parsing library, creating a purely dependency free dependency.

I went forth and implemented my fork of the holidays library. Using the yaml files I could parse the month, day, and name and produce the resulting elixir code like this:

def parse_dates(dates) do
Enum.map(dates, fn {month, day, name} ->
quote do
def holiday(%Date{month: unquote(month), day: unquote(day)}) do
unquote(name)
end
end
end)
end

Where the static dates could trivially be read from the yaml files. For the easter calculations I simply needed the offset that was encoded in the yaml library using a string, for instance ascension day: "easter(year)+39". I could leverage the binary pattern matching construct that elixir provides to read only the offsets.

defp read_offset(<<"easter(year)", "">>), do: 0
defp read_offset(<<"easter(year)", offset::binary>>), do: String.to_integer(offset)

To catch the easter dates, the fallback function, in other words, the function that would match if none of the previous static dates would match, would need to be large cond construct (if, else if, else in other languages). I would calulate the easter date for the given year and produce each cond statement as the easter date with the given offsets.

Enum.map(offset_tuples, fn {name, offset} ->
quote do
Date.add(easter_date, unquote(offset)) == date -> unquote(name)
end
end)

Putting it all together I would create a module with a module name dynamically created from the locale. Within each locale module I would have all the static holiday functions, and a fallback function with the easter date conditions.

quote do
defmodule unquote(module_name) do
@spec holiday(date :: Date.t()) :: binary()
unquote_splicing(date_functions)
def holiday(%Date{year: year} = date) do
easter_date = HolidayEx.Utils.easter(year)
unquote(easter_conditions)
end
end
end

We can see from this simple code gen code, that implementing code that writes code is fairly straight forward in elixir.

With all this implemented I simply wrote a script that produced the code and wrote to files in the priv folder, the uncompiled folder. I could now compile these files dynamically using the use construct in the main module:

defmacro __using__(opts) do
user_locales = Keyword.get(opts, :locales)
locales =
if user_locales == :all do
@supported_locales
else
Enum.each(user_locales, fn locale ->
unless locale in @supported_locales do
raise "Unsupported locale: #{locale}"
end
end)
user_locales
end
quote do
for locale <- unquote(locales) do
locale_name = Atom.to_string(locale)
priv = :code.priv_dir(:holiday_ex) |> List.to_string()
path = Path.join(priv, "locale_modules/#{locale_name}.ex")
@external_resource path
Code.compile_file(path)
end
end
end

I fetch the user specified locales from the options. I do a quick check to see if the locales are in the supported locales, raising an error if the user passes in an unsupported locale (or anything else). Last, I iterate over the locales, fetch the correct .ex file from the priv folder. Final step is to compile the file. The resulting library then solves a few of the issues I had with the existing libraries. I hope this library can be useful to anybody else that needs to know whether a date is a holiday in their locale, or not.