Bizdays
Business day calculations in Elixir, with the Brazilian ANBIMA calendar for 2001–2099 and support for custom calendars.
Pure functions over an immutable calendar struct. ANBIMA holidays are calculated once at compile time. The library has zero runtime dependencies.
Installation
Requires Elixir 1.15 or later. Add bizdays to your dependencies in mix.exs:
def deps do
[
{:bizdays, "~> 0.1"}
]
end
Then run mix deps.get.
Quick start
cal = Bizdays.anbima()
Bizdays.business_day?(cal, ~D[2026-02-16])
# => false (Carnival)
Bizdays.following(cal, ~D[2026-02-16])
# => ~D[2026-02-18]
Bizdays.preceding(cal, ~D[2026-02-16])
# => ~D[2026-02-13]
Bizdays.add(cal, ~D[2026-02-13], 5)
# => ~D[2026-02-24]
Bizdays.count(cal, ~D[2026-02-13], ~D[2026-02-24])
# => 5
All calendar operations take the calendar first and support pipes:
Bizdays.anbima() |> Bizdays.add(~D[2026-02-13], 1)
# => ~D[2026-02-18]
Counting and adding business days
Bizdays.count/3 counts (from, to]: the starting date is excluded and the
ending date is included if it is a business day. Endpoints are not adjusted.
Equal dates return zero. Reversing the dates reverses the sign.
Bizdays.count(cal, ~D[2012-12-31], ~D[2013-01-03])
# => 2 (January 2 and 3)
Bizdays.count(cal, ~D[2013-01-03], ~D[2012-12-31])
# => -2
Bizdays.count(cal, ~D[2026-02-13], ~D[2026-02-17])
# => 0 (weekend and Carnival)
Bizdays.count(cal, ~D[2026-02-16], ~D[2026-02-18])
# => 1
Bizdays.add/3 takes an integer offset. Positive values count business days
strictly after the starting date; negative values count strictly before it.
This also applies when the starting date is a holiday or weekend.
Zero returns Bizdays.following/2.
Bizdays.add(cal, ~D[2026-02-16], 1)
# => ~D[2026-02-18]
Bizdays.add(cal, ~D[2026-02-16], -1)
# => ~D[2026-02-13]
Bizdays.add(cal, ~D[2026-02-16], 0)
# => ~D[2026-02-18]
For a business-day start d, Bizdays.count(cal, d, Bizdays.add(cal, d, n)) == n
when the result stays within the calendar boundaries.
The API draws inspiration from python-bizdays. Zero offsets adjust forward here, and the explicit counting convention can differ when endpoints are not business days.
Date adjustments and lists
| Function | Behavior |
|---|---|
Bizdays.following/2 |
First business day on or after the date. |
Bizdays.preceding/2 |
Last business day on or before the date. |
Bizdays.modified_following/2 |
Following, switching to preceding if the result changes month. |
Bizdays.modified_preceding/2 |
Preceding, switching to following if the result changes month. |
Bizdays.range/3 |
List of business days, including both endpoints when they are business days. |
Bizdays.holidays/2 |
Sorted holiday dates in a year, including holidays on weekends. |
Bizdays.modified_following(cal, ~D[2026-01-31])
# => ~D[2026-01-30]
Bizdays.modified_preceding(cal, ~D[2026-03-01])
# => ~D[2026-03-02]
Bizdays.range(cal, ~D[2026-02-13], ~D[2026-02-19])
# => [~D[2026-02-13], ~D[2026-02-18], ~D[2026-02-19]]
Bizdays.range(cal, ~D[2026-02-19], ~D[2026-02-13])
# => [~D[2026-02-19], ~D[2026-02-18], ~D[2026-02-13]]
Bizdays.holidays(cal, 2026) |> Enum.take(3)
# => [~D[2026-01-01], ~D[2026-02-16], ~D[2026-02-17]]
range/3 includes the starting date when it is a business day; count/3 excludes it.
For equal endpoints, the range contains that date if it is a business day,
otherwise [].
Modified adjustments require each search to succeed within the calendar limits. Reaching a boundary raises an error. If a custom calendar has no business days in a month, the fallback adjustment can also leave that month.
Custom calendars
Bizdays.Calendar.new/1 defaults to no holidays, Saturday/Sunday weekends,
and inclusive boundaries of 2001-01-01 and 2099-12-31. Weekdays use ISO numbers:
Monday is 1 and Sunday is 7. An empty weekend list makes every weekday available.
custom = Bizdays.Calendar.new(
name: "Custom",
holidays: [~D[2026-02-18]],
weekend: [6, 7],
first_date: ~D[2026-01-01],
last_date: ~D[2026-12-31]
)
Bizdays.following(custom, ~D[2026-02-18])
# => ~D[2026-02-19]
To extend ANBIMA, include its holidays explicitly:
custom = Bizdays.Calendar.new(
name: "ANBIMA + local holiday",
holidays: MapSet.put(cal.holidays, ~D[2026-02-18])
)
Bizdays.following(custom, ~D[2026-02-16])
# => ~D[2026-02-19]
Holidays can be a list or MapSet. Duplicate dates are removed. Custom boundaries
must stay within 2001–2099, and all supplied holidays must lie within them.
For a partially covered year, holidays/2 returns only the available holidays.
ANBIMA coverage
The rules follow the official ANBIMA calendar: national holidays affecting bank reserves, plus Carnival Monday and Tuesday, Good Friday and Corpus Christi. November 20 is included from 2024, following Law 14,759/2023.
Maundy Thursday is a business day in the supported interval. Weekend holidays stay on their original dates. Municipal/state holidays, elections and the year-end bank closure are excluded. B3 trading holidays require a separate calendar.
The test suite compares all 1,263 unique holiday dates against a snapshot of ANBIMA's official spreadsheet. The snapshot and its provenance are in test/fixtures. Future legal changes require a library update.
Errors and limits
Operations accept ISO Date values, such as ~D[2026-02-13], and raise
ArgumentError for invalid dates or dates outside the calendar boundaries.
Strings, DateTime and NaiveDateTime are not accepted. A search or offset
that cannot find a business day within the boundaries also raises.
Development
mix deps.get
mix format
mix test
mix credo --strict
mix docs --warnings-as-errors
Open doc/index.html for the generated documentation. The tests run offline,
including the official holiday fixture. CI tests Elixir/OTP 1.15/26, 1.17/27
and 1.20/29, with formatting, Credo and documentation checks on the last pair.
License
MIT — Igor Giamoniano.