Migration guide¶
This page lists every change you may need to handle when you move from bizdays 1.0.20 (the last 1.x release on PyPI) to bizdays 2.0.
If you use 1.0.19, the guide applies too: 1.0.19 has the same API as 1.0.20. The only difference is the B3 data, see Calendar data.
Each entry has the same parts:
v1 says what 1.0.20 did, and v2 what 2.0 does.
Why gives the reason for the change.
Before (1.0.20) shows old code. After (2.0) shows the new code and its output. The “After” examples are tested with every release.
Migration says how much work the change needs:
mechanical: a search and replace, or a small fixed edit;
manual: you must read the code around the call and decide;
no direct replacement: the feature is gone; the entry says what to do instead;
nothing to change: your code still runs, but some results are different, so check stored results.
The “After” examples use these imports and this calendar:
>>> import datetime
>>> import io
>>> import json
>>> import os
>>> import tempfile
>>> import warnings
>>> import numpy as np
>>> import pandas as pd
>>> from bizdays import Calendar, DateOutOfRange
>>> cal = Calendar.load(name="ANBIMA")
>>> tmp = tempfile.mkdtemp()
Inventory¶
Change |
Migration |
Entry |
|---|---|---|
Python 3.10 or newer; pandas 3 is supported |
mechanical |
|
|
mechanical |
|
Only |
no direct replacement |
|
|
manual |
|
|
mechanical |
|
|
mechanical |
|
|
mechanical |
|
|
mechanical |
|
Text |
manual |
|
Load errors have specific types |
mechanical |
|
Date strings must be |
manual |
|
Sets, generators, |
mechanical |
|
Counts must be whole numbers |
mechanical |
|
Results are NumPy values, not Python values |
manual |
|
Missing values are |
manual |
|
Vector lengths must match, or one must be 1 |
mechanical |
|
Dates outside the calendar range raise |
manual |
|
|
mechanical |
|
|
manual |
|
|
nothing to change |
|
|
manual |
|
Actual has a wider range (B3 already covered 2099 in 1.0.20) |
nothing to change |
Install and packaging¶
Python and dependency versions¶
v1: 1.0.20 needs Python 3.9 or newer, pandas>=2,<3 and
pandas-market-calendars>=4,<5. NumPy comes in through pandas.
v2: 2.0 needs Python 3.10 or newer (tested on 3.10 to 3.14),
numpy>=1.24 and pandas>=2 (pandas 2 and 3 both work).
pandas-market-calendars is no longer required (see
Optional calendar providers).
Why: Python 3.9 is no longer supported upstream, and users asked for pandas 3. bizdays uses NumPy directly, so it names it.
Before (1.0.20):
pip install "bizdays==1.0.20" # Python 3.9+, pandas 2 only
After (2.0):
pip install "bizdays>=2" # Python 3.10+, pandas 2 or 3
Migration: mechanical. Move to Python 3.10 or newer. If you pinned
pandas<3 only for bizdays, you can drop the pin.
Optional calendar providers¶
v1: pandas_market_calendars was a required dependency, so
Calendar.load("PMC/B3") worked after a plain install. There was no
missing-provider error; a bad PMC/ name raised the provider’s own
RuntimeError.
v2: each external provider is an optional extra: pmc for
PMC/<name> (pandas_market_calendars), xcal for XCAL/<name>
(exchange_calendars, new) and work for WORK/<code>
(workalendar, new). Without the extra, loading such a calendar raises
ImportError, and the message names the extra to install.
Why: most users only need the packaged calendars. Big optional packages should not be installed for everyone.
Before (1.0.20):
# pip install bizdays
from bizdays import Calendar
cal = Calendar.load("PMC/B3")
After (2.0):
# pip install "bizdays[pmc]" (or bizdays[xcal], bizdays[work])
from bizdays import Calendar
cal = Calendar.load(name="PMC/B3")
# Without the extra: ImportError ... pip install "bizdays[pmc]"
Migration: mechanical. Add the extra to your requirements, for example
bizdays[pmc].
Public names¶
Removed top-level names¶
v1: bizdays exported many helpers at the top level, such as
load_holidays, Date, DateIndex, VectorizedOps, isseq,
isstr and options.
v2: the public names are only Calendar, list_calendars and
DateOutOfRange (the __all__ list). Submodules such as
bizdays.utils or bizdays.dateindex are internal: they can change in
any release.
Why: a small public surface can be kept stable. The helpers were internal tools, not part of a designed API.
Before (1.0.20):
from bizdays import Date, load_holidays
holidays = load_holidays("ANBIMA.cal")
d = Date("2024-01-02")
After (2.0):
>>> import bizdays
>>> print(bizdays.__all__)
['Calendar', 'list_calendars', 'DateOutOfRange']
>>> from bizdays import Date
Traceback (most recent call last):
...
ImportError: cannot import name 'Date' from 'bizdays'
Migration: no direct replacement. Pass dates as strings, Python dates, NumPy or pandas values. To read a holiday list from a file, see Text .cal files.
Options and modes¶
v1: get_option and set_option changed the results of every
calendar. set_option("mode", "pandas") made seq return a pandas
DatetimeIndex and made counts object arrays with pd.NA.
set_option("mode.datetype", "iso") made dates come back as strings such
as '2015-12-28'.
v2: both functions are gone. Results are always NumPy values (see Return types). pandas inputs are still accepted.
Why: a global switch made the same call return different types in different parts of a program.
Before (1.0.20):
from bizdays import set_option
set_option("mode", "pandas")
cal.seq("2014-01-02", "2014-01-07") # DatetimeIndex
set_option("mode.datetype", "iso")
cal.following("2015-12-25") # '2015-12-28'
After (2.0):
>>> from bizdays import set_option
Traceback (most recent call last):
...
ImportError: cannot import name 'set_option' from 'bizdays'
>>> s = pd.Series(["2024-01-02", "2024-03-01"], index=["a", "b"])
>>> counts = pd.Series(cal.bizdays(s, "2024-06-14"), index=s.index)
>>> print(counts.to_dict())
{'a': 113.0, 'b': 72.0}
>>> print(np.datetime_as_string(cal.following("2015-12-25")))
2015-12-28
>>> print(cal.following(["2015-12-25"]).astype(str))
['2015-12-28']
>>> idx = pd.DatetimeIndex(cal.seq("2014-01-02", "2014-01-07"))
>>> print(list(idx.strftime("%Y-%m-%d")))
['2014-01-02', '2014-01-03', '2014-01-06', '2014-01-07']
Migration: manual. Remove the set_option calls. Where you need
pandas objects, wrap the result yourself and pass index= to keep your
index. Where you need ISO strings, use np.datetime_as_string(result) or
result.astype(str).
cal.vec is gone¶
v1: vector calls went through cal.vec, for example
cal.vec.adjust_next(dates).
v2: cal.vec is gone. Every method takes a single value or a vector.
Why: two ways to make the same call were not needed.
Before (1.0.20):
list(cal.vec.adjust_next(["2015-12-25"]))
After (2.0):
>>> print(cal.following(["2015-12-25"]))
['2015-12-28']
>>> print(cal.adjust_next(["2015-12-25"]))
['2015-12-28']
Migration: mechanical. Replace .vec. with ..
DateOutOfRange is a ValueError¶
v1: from bizdays import DateOutOfRange worked, and the class was a
direct subclass of Exception.
v2: the class is a subclass of ValueError, and it is still exported
at the top level.
Why: a date outside the calendar range is a bad input value, so
ValueError is the expected base class.
Before (1.0.20):
from bizdays import DateOutOfRange
try:
cal.bizdays("1999-01-01", "2024-01-02")
except ValueError:
... # not reached in 1.0.20
except DateOutOfRange:
... # reached
After (2.0):
>>> issubclass(DateOutOfRange, ValueError)
True
>>> try:
... cal.bizdays("1999-01-01", "2024-01-02")
... except DateOutOfRange:
... print("out of range")
... except ValueError:
... print("bad input")
out of range
Migration: mechanical. If you catch both, put except DateOutOfRange before except ValueError; otherwise the
ValueError branch also catches range errors.
Building and loading calendars¶
Calendar(...) defaults and checks¶
v1:
Calendar().namewasNone, sostr(Calendar())showedNone.Calendar(holidays=[...])withoutweekdaysgave no warning.A calendar built from one holiday and no range worked, with the range 2024-01-01..2024-01-01.
startdateafterenddateraisedKeyError.Unknown weekday names raised a
ValueErrorabout a list index; numbers such as[5, 6]raisedTypeError.A calendar with every weekday off was built, and its counts were 0.
Duplicate weekdays were kept.
Holidays outside an explicit
startdate..enddatewere kept.
v2:
Calendar().nameis"". Passingname=Nonestill works and also gives"".Calendar(holidays=[...])withoutweekdaysemits aUserWarning(as R bizdays does). Passweekdays=[]for a calendar without weekend.A calendar without any business day in its range raises
ValueError, for example one holiday and no range, or every weekday off.startdateafterenddateraisesValueError.Weekday names must be English, full (
"Saturday") or 3-letter ("sat"), in any case. Other words and numbers raiseValueError.Duplicate weekdays are removed.
Holidays outside an explicit
startdate..enddateare dropped.
Why: these setups were mistakes that gave wrong counts later, far from the cause. Failing early shows the cause.
Before (1.0.20):
Calendar().name # None
Calendar(holidays=["2024-01-01"]) # range 2024-01-01..2024-01-01
Calendar(startdate="2024-01-10", enddate="2024-01-01") # KeyError
Calendar(weekdays=["Sabado"]) # ValueError: ... not in list
Calendar(weekdays=[5, 6]) # TypeError
Calendar(weekdays=["Saturday", "saturday"]).weekdays # both kept
After (2.0):
>>> print(repr(Calendar().name))
''
>>> with warnings.catch_warnings(record=True) as caught:
... warnings.simplefilter("always")
... two = Calendar(holidays=["2024-01-01", "2024-12-25"])
>>> print(caught[0].message)
You provided holidays without set weekdays.
That setup leads to inconsistencies!
>>> with warnings.catch_warnings(record=True) as caught:
... warnings.simplefilter("always")
... two = Calendar(holidays=["2024-01-01", "2024-12-25"], weekdays=[])
>>> len(caught)
0
>>> Calendar(holidays=["2024-01-01"], weekdays=[])
Traceback (most recent call last):
...
ValueError: The calendar has no business days in 2024-01-01..2024-01-01
>>> Calendar(startdate="2024-01-10", enddate="2024-01-01")
Traceback (most recent call last):
...
ValueError: startdate 2024-01-10 is after enddate 2024-01-01
>>> Calendar(weekdays=["Sabado"])
Traceback (most recent call last):
...
ValueError: Invalid weekday 'Sabado'; use English names such as 'Saturday' or 'sat'
>>> Calendar(weekdays=[5, 6])
Traceback (most recent call last):
...
ValueError: Invalid weekday 5; use English names such as 'Saturday' or 'sat'
>>> week = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
>>> Calendar(weekdays=week, startdate="2024-01-01", enddate="2024-01-10")
Traceback (most recent call last):
...
ValueError: The calendar has no business days in 2024-01-01..2024-01-10
>>> print(Calendar(weekdays=["Saturday", "saturday"]).weekdays)
('Saturday',)
>>> inside = Calendar(
... holidays=["2023-01-01", "2024-01-01"], weekdays=[],
... startdate="2023-06-01", enddate="2024-12-31",
... )
>>> len(inside.holidays)
1
Migration: mechanical. Use English weekday names, pass weekdays=[]
when you mean “no weekend”, and check that startdate comes before
enddate. Code that tests cal.name is None should test
not cal.name.
Calendar.load takes keywords only¶
v1: Calendar.load("ANBIMA") worked by position, because the first
parameter was name. A file needed filename=: a file path passed by
position raised Exception: Invalid calendar. If both name and
filename were given, filename won.
v2: use Calendar.load(name=...) for packaged and provider calendars,
and Calendar.load(filename=...) for your own JSON files. A positional
argument raises TypeError. Exactly one of the two keywords must be
given; both, or neither, raise ValueError.
Why: keywords make the call clear, and each call names exactly one source, so no argument is silently ignored.
Before (1.0.20):
Calendar.load("ANBIMA")
Calendar.load(filename="custom-calendar.json")
Calendar.load(name="B3", filename="x.json") # loads x.json
After (2.0):
>>> print(Calendar.load(name="ANBIMA").name)
ANBIMA
>>> Calendar.load("ANBIMA")
Traceback (most recent call last):
...
TypeError: Calendar.load() takes 1 positional argument but 2 were given
>>> Calendar.load(name="B3", filename="x.json")
Traceback (most recent call last):
...
ValueError: Provide exactly one of 'name' or 'filename'
Migration: mechanical. Write name= before a calendar name and
filename= before a file path.
Text .cal files¶
v1: Calendar.load(filename="my.cal") read a text file with one
holiday (YYYY-MM-DD) or one English weekday name per line. Blank lines
were skipped.
v2: only JSON calendar files are read. A .cal file raises
ValueError with the text “a JSON calendar is expected” and a link to
this page.
Why: one file format, the same as R bizdays, is easier to keep correct.
JSON can also hold the name, the range and the financial flag.
Before (1.0.20):
# my.cal:
# 2001-01-01
# 2001-02-26
# Saturday
# Sunday
cal = Calendar.load(filename="my.cal")
After (2.0): a .cal file raises an error.
>>> path = os.path.join(tmp, "old.cal")
>>> with open(path, "w") as f:
... _ = f.write("2001-01-01\nSaturday\n")
>>> Calendar.load(filename=path)
Traceback (most recent call last):
...
ValueError: Invalid calendar file ...: a JSON calendar is expected. ...
Read the .cal lines yourself: lines that start with a letter are
weekdays, the other lines are holidays.
>>> lines = io.StringIO("2001-01-01\n2001-02-26\nSaturday\nSunday\n")
>>> rows = [line.strip() for line in lines if line.strip()]
>>> old_weekdays = [row for row in rows if row[0].isalpha()]
>>> old_holidays = [row for row in rows if not row[0].isalpha()]
>>> old = Calendar(holidays=old_holidays, weekdays=old_weekdays, name="old")
>>> print(old.weekdays)
('Saturday', 'Sunday')
>>> len(old.holidays)
2
To keep a file, write the same lists to a JSON file once, and load it with
filename=:
>>> data = {"name": "old", "weekdays": old_weekdays, "holidays": old_holidays}
>>> json_path = os.path.join(tmp, "old.json")
>>> with open(json_path, "w") as f:
... json.dump(data, f)
>>> print(Calendar.load(filename=json_path).weekdays)
('Saturday', 'Sunday')
JSON calendar files use these top-level fields:
name: stringweekdays: list of English weekday namesholidays: list ofYYYY-MM-DDstringsfinancial: boolean (defaulttrue)start.dateandend.date: optionalYYYY-MM-DDstrings for the rangeadjust.fromandadjust.to: optional, accepted for compatibility with R bizdays (see Load errors)
Minimal example:
{
"name": "Custom",
"weekdays": ["saturday", "sunday"],
"holidays": ["2024-01-01", "2024-12-25"],
"financial": true
}
Migration: manual. Convert each .cal file to JSON once, with the
recipe above, or build the calendar in Python from your own data.
Load errors¶
v1: an unknown calendar name and a missing file both raised a plain
Exception: Invalid calendar: ....
v2:
an unknown name raises
ValueError, and the message lists the packaged calendars;a missing file raises
FileNotFoundError;a JSON file with
adjust.fromother than"following", oradjust.toother than"preceding", raisesValueError, because bizdays always uses these two rules.
Why: specific error types can be caught on their own.
Before (1.0.20):
try:
Calendar.load("B1")
except Exception:
...
After (2.0):
>>> Calendar.load(name="B1")
Traceback (most recent call last):
...
ValueError: Unknown calendar: 'B1'; packaged calendars are ANBIMA, B3, Actual
>>> Calendar.load(filename=os.path.join(tmp, "missing.json"))
Traceback (most recent call last):
...
FileNotFoundError: Calendar file not found: ...
>>> adjust_path = os.path.join(tmp, "adjust.json")
>>> with open(adjust_path, "w") as f:
... json.dump({"name": "X", "weekdays": [], "holidays": [],
... "adjust.to": "following"}, f)
>>> Calendar.load(filename=adjust_path)
Traceback (most recent call last):
...
ValueError: Invalid calendar JSON: 'adjust.to' must be 'preceding', got 'following'
Migration: mechanical. Catch ValueError or FileNotFoundError
instead of Exception.
Inputs¶
Strict date strings¶
v1: date strings without zero padding, such as "2024-1-2", worked.
v2: a date string must be YYYY-MM-DD. Other strings raise
ValueError, including "" and "NaT". A numpy.datetime64 with
unit Y, M or W is not a date and raises ValueError. A
string such as "2024-01" was already an error for dates; now it is only
accepted as a month ref in getdate.
Why: a loose parser can read a wrong value as a valid date without any error.
Before (1.0.20):
cal.isbizday("2024-1-2") # True
After (2.0):
>>> cal.isbizday("2024-1-2")
Traceback (most recent call last):
...
ValueError: dt: invalid date string np.str_('2024-1-2'); expected YYYY-MM-DD
>>> cal.isbizday("")
Traceback (most recent call last):
...
ValueError: dt: invalid date string np.str_(''); expected YYYY-MM-DD
>>> cal.isbizday(np.datetime64("2024-01"))
Traceback (most recent call last):
...
ValueError: dt: numpy.datetime64 with unit 'M' is not a date; use unit 'D' or finer
>>> values = ["2024-1-2", "2024-1-6"]
>>> print(cal.isbizday(pd.to_datetime(values)))
[True False]
>>> print(cal.isbizday(pd.to_datetime(values).strftime("%Y-%m-%d")))
[True False]
Migration: manual. Find where your date strings come from. Parse loose
strings with pd.to_datetime and pass the result directly, or format them
with .strftime("%Y-%m-%d").
Input containers¶
v1: any iterable worked as a vector: cal.isbizday({"2024-01-02"})
gave [True], and generators worked too.
v2: vectors are lists, tuples, NumPy arrays and pandas Series or
Index objects. Sets, generators, range and dicts raise
ValueError. 2-D input, such as a DataFrame or a list of lists, also
raises ValueError.
Why: sets and dicts have no fixed order, and a generator can be read only once, so results could not be matched to inputs.
Before (1.0.20):
cal.isbizday({"2024-01-02"}) # [True]
cal.isbizday(d for d in ["2024-01-02"]) # [True]
After (2.0):
>>> cal.isbizday({"2024-01-02"})
Traceback (most recent call last):
...
ValueError: dt: set is not a supported sequence; use a list, tuple, NumPy array or pandas Series/Index
>>> cal.isbizday(d for d in ["2024-01-02"])
Traceback (most recent call last):
...
ValueError: dt: generator is not a supported sequence; ...
>>> df = pd.DataFrame({"day": ["2024-01-02", "2024-01-06"]})
>>> cal.isbizday(df)
Traceback (most recent call last):
...
ValueError: dt must be 1-D, got a DataFrame
>>> print(cal.isbizday(list(d for d in ["2024-01-02"])))
[True]
>>> print(cal.isbizday(df["day"]))
[True False]
Migration: mechanical. Wrap the input in list(...), or pass one
column (df["col"]) instead of a DataFrame.
Whole-number counts¶
v1: cal.offset("2013-01-02", True) was read as offset(..., 1)
and gave 2013-01-03. cal.offset("2013-01-02", 1.5) raised
TypeError.
v2: the count must be a whole number. True, False and 1.5
raise ValueError. Whole floats such as 3.0 now work.
Why: a bool count is almost always a bug, and whole floats are
common when counts come from pandas.
Before (1.0.20):
cal.offset("2013-01-02", True) # datetime.date(2013, 1, 3)
cal.offset("2013-01-02", 1.5) # TypeError
After (2.0):
>>> cal.offset("2013-01-02", True)
Traceback (most recent call last):
...
ValueError: n must be whole numbers or missing (got True)
>>> cal.offset("2013-01-02", 1.5)
Traceback (most recent call last):
...
ValueError: n must be whole numbers or missing (got 1.5)
>>> print(cal.offset("2013-01-02", 3.0))
2013-01-07
Migration: mechanical. Pass int(n).
Outputs and missing values¶
Return types¶
v1: results were Python values: datetime.date for dates, int
for counts, bool for flags, and lists for vectors.
v2: results are NumPy values:
dates are
numpy.datetime64(unitD);counts are
numpy.float64, even when the value is whole;flags are
numpy.bool_, socal.isbizday(d) is TrueisFalse;vectors are
numpy.ndarray(and masked arrays forisbizday, see Missing values);a
numpy.datetime64is not found in asetordictofdatetime.datevalues;a pandas
Seriesinput does not keep its index in the result.
Why: NumPy values make vector calls fast, and the same call returns the same type for one value and for many.
Before (1.0.20):
cal.following("2015-12-25") # datetime.date(2015, 12, 28)
cal.bizdays("2024-01-02", "2024-06-14") # 113
cal.isbizday("2024-01-02") is True # True
cal.seq("2014-01-02", "2014-01-07") # list of datetime.date
After (2.0):
>>> type(cal.following("2015-12-25")).__name__
'datetime64'
>>> print(cal.bizdays("2024-01-02", "2024-06-14"))
113.0
>>> cal.isbizday("2024-01-02") is True
False
>>> bool(cal.isbizday("2024-01-02"))
True
>>> cal.following("2015-12-25") in {datetime.date(2015, 12, 28)}
False
>>> cal.following("2015-12-25").item()
datetime.date(2015, 12, 28)
>>> cal.seq("2014-01-02", "2014-01-07").tolist()
[datetime.date(2014, 1, 2), datetime.date(2014, 1, 3), datetime.date(2014, 1, 6), datetime.date(2014, 1, 7)]
>>> df = pd.DataFrame({"day": ["2024-01-02", "2024-03-01"]}, index=[10, 20])
>>> df["count"] = cal.bizdays(df["day"], "2024-06-14")
>>> print(df["count"].to_dict())
{10: 113.0, 20: 72.0}
Migration: manual. Use bool(...) or == instead of is True.
Use .item() for one Python value and .tolist() for a list. Use
int(...) or .astype(int) where you need integers (drop NaN
first). Assign results to a DataFrame column, or build a Series with
index=; never pd.Series(result) alone, which gets a new index.
Missing values¶
v1: a missing input (None) gave None inside the result list,
for example cal.bizdays(["2024-01-02", None], "2024-06-14") gave
[113, None]. In pandas mode it gave pd.NA. cal.seq(None, ...)
raised TypeError.
v2:
counts use
NaN;dates use
NaT(not a time);isbizdaywith a vector always returns anumpy.ma.MaskedArray, with missing entries masked; a missing scalar givesnumpy.ma.masked;diffgivesNaNfor each step that touches a missing date;seqwith a missing end raisesValueError.
Why: None inside a list forces Python objects; NaN, NaT and
masks keep NumPy arrays fast and typed.
Before (1.0.20):
cal.bizdays(["2024-01-02", None], "2024-06-14") # [113, None]
cal.isbizday(["2024-01-02", None]) # [True, None]
cal.seq(None, "2014-01-07") # TypeError
After (2.0):
>>> counts = cal.bizdays(["2024-01-02", None], "2024-06-14")
>>> print(counts)
[113. nan]
>>> print(np.isnan(counts))
[False True]
>>> print(counts[~np.isnan(counts)].astype(int))
[113]
>>> flags = cal.isbizday(["2024-01-02", None])
>>> print(flags)
[True --]
>>> type(flags).__name__
'MaskedArray'
>>> print(flags.mask)
[False True]
>>> print(flags.filled(False))
[ True False]
>>> print(np.isnat(cal.following(["2015-12-25", None])))
[False True]
>>> print(cal.diff(["2014-01-02", None, "2014-01-10"]))
[nan nan]
>>> cal.seq(None, "2014-01-07")
Traceback (most recent call last):
...
ValueError: seq endpoints cannot be missing
Migration: manual. Replace x is None checks with np.isnan for
counts, np.isnat for dates and .mask for flags. Use
.filled(False) to turn a masked flag array into a plain bool array.
Drop NaN before .astype(int).
Vectors and calendar range¶
Vector lengths¶
v1: bizdays recycled a shorter vector when its length divided the
longer one (lengths 2 and 4 gave 4 results), and raised a plain
Exception otherwise (lengths 2 and 3). offset, getdate and
getbizdays recycled any lengths silently (lengths 3 and 2 gave 3
results). An empty vector raised ZeroDivisionError.
v2: two vector arguments must have equal lengths, or one of them must
have length 1. Any other combination raises ValueError. An empty
argument gives an empty array when the other argument is empty or has
length 1.
Why: silent recycling hid bugs where two columns were not aligned.
Before (1.0.20):
cal.bizdays(["2024-01-02", "2024-01-03"], ["2024-02-01"] * 4) # [22, 21, 22, 21]
cal.bizdays(["2024-01-02", "2024-01-03"], ["2024-02-01"] * 3) # Exception
cal.bizdays([], "2024-01-02") # ZeroDivisionError
After (2.0):
>>> cal.bizdays(["2024-01-02", "2024-01-03"], ["2024-02-01"] * 4)
Traceback (most recent call last):
...
ValueError: date_from has length 2 and date_to has length 4; ...
>>> print(cal.bizdays(np.tile(["2024-01-02", "2024-01-03"], 2), ["2024-02-01"] * 4))
[22. 21. 22. 21.]
>>> print(cal.bizdays([], "2024-01-02"))
[]
Migration: mechanical. Pass equal lengths, or repeat the shorter input
with np.tile or np.repeat.
Calendar range¶
v1: some calls past the calendar ends wrapped around. On ANBIMA (range
2000-01-01..2099-12-25), offset("2000-01-03", -3) returned 2099-12-22.
v2: any input or result outside startdate..enddate raises
bizdays.DateOutOfRange. There are no partial results: one bad
element fails the whole call, and the error’s positions attribute lists
every bad element. For getdate and getbizdays the whole period
(month or year) must be inside the range, so
getdate("last bizday", "2099-12") on ANBIMA raises. Use
bizdays.Calendar.inrange() to check input dates before a call.
Why: a wrapped date is a wrong answer that looks right.
Before (1.0.20):
cal.offset("2000-01-03", -3) # datetime.date(2099, 12, 22)
After (2.0):
>>> cal.offset("2000-01-03", -3)
Traceback (most recent call last):
...
DateOutOfRange: 1 of 1 elements out of calendar range 2000-01-01..2099-12-25; ...
>>> try:
... cal.offset(["2024-01-02", "2099-12-24"], 5)
... except DateOutOfRange as err:
... print(err.positions)
[1]
>>> print(cal.inrange(["1999-01-01", "2024-01-02"]))
[False True]
>>> cal.getdate("last bizday", "2099-12")
Traceback (most recent call last):
...
DateOutOfRange: 1 of 1 elements out of calendar range 2000-01-01..2099-12-25; ...
Migration: manual. Filter inputs with inrange before the call, or
catch DateOutOfRange and use positions. Code that needs partial
results has no direct replacement yet; errors="coerce" is planned for
2.1.
getdate¶
getdate(expr, ref)¶
v1: getdate(expr, year, month=None), for example
cal.getdate("last bizday", 2024, 2) gave 2024-02-29.
v2: getdate(expr, ref). The ref is a year (2024), a month
("2024-02" or a numpy.datetime64 with unit M) or a date. The
old call with three arguments raises TypeError.
Why: one ref argument, as in R bizdays, also allows date refs and vectors of refs.
Day refs are not month refs. A pd.Timestamp, a datetime.date or
a "YYYY-MM-DD" string is a date ref, even on the first day of a month.
"last bizday" with a date ref is the business day before that date. To
get the last business day of a month from dates, convert them to months
with .astype("datetime64[M]").
Before (1.0.20):
cal.getdate("last bizday", 2024, 2) # datetime.date(2024, 2, 29)
After (2.0):
>>> year, month = 2024, 2
>>> print(cal.getdate("last bizday", f"{year}-{month:02d}"))
2024-02-29
>>> cal.getdate("last bizday", 2024, 2)
Traceback (most recent call last):
...
TypeError: Calendar.getdate() takes 3 positional arguments but 4 were given
>>> print(cal.getdate("last bizday", pd.Timestamp("2024-02-01")))
2024-01-31
>>> print(cal.getdate("last bizday", np.datetime64("2024-02")))
2024-02-29
>>> days = pd.to_datetime(["2024-02-01", "2024-03-15"]).values
>>> print(cal.getdate("last bizday", days.astype("datetime64[M]")))
['2024-02-29' '2024-03-28']
Migration: mechanical. Replace year, month with
f"{year}-{month:02d}", and year alone with year. If your refs are
dates, convert them with .astype("datetime64[M]") when you mean the
month.
getdate expressions¶
v1:
ordinals with a wrong suffix worked:
"2st bizday"was the 2nd business day;previouswas not supported: everyprevious ...expression raisedValueError;next <weekday>with a year or month ref raisedValueError;only year and month refs existed;
all refs in one vector call could mix any kind.
v2:
ordinals need the right English suffix:
1st,2nd,3rd,4th,11th,21st;2straisesValueError;previous <weekday>is new and needs a date ref;previous bizdayandprevious daystill raiseValueError;next <weekday>(andnext day,next bizday) with a year or month ref is the first one in the period;date refs accept the forms of R bizdays:
<n> dayis the date plusndays,last daythe day before,<n> bizdayisoffset(date, n),last bizdayisoffset(date, -1), andnext/last <weekday>the next or previous one;expressions with
beforeorafterneed a year or month ref;all refs in one call must be of one kind (year, month or date).
weekday words must be full or 3-letter English names (
tuesraisesValueError, as in 1.0.20).
Why: the grammar now follows R bizdays, the reference implementation.
Before (1.0.20):
cal.getdate("2st bizday", 2024, 1) # datetime.date(2024, 1, 3)
cal.getdate("next wed", 2002, 1) # ValueError
After (2.0):
>>> cal.getdate("2st bizday", "2024-01")
Traceback (most recent call last):
...
ValueError: Invalid ordinal in getdate expression: 2st (use 2nd)
>>> print(cal.getdate("2nd bizday", "2024-01"))
2024-01-03
>>> cal.getdate("first tues", "2024-01")
Traceback (most recent call last):
...
ValueError: Invalid day token in getdate expression: tues
>>> cal.getdate("previous bizday", "2024-01")
Traceback (most recent call last):
...
ValueError: previous expressions require a weekday target
>>> print(cal.getdate("next wed", "2002-01"))
2002-01-02
>>> print(cal.getdate("next bizday", "2022-06-18"))
2022-06-20
>>> cal.getdate("first bizday before first day", "2024-01-10")
Traceback (most recent call last):
...
ValueError: Composite getdate expressions need a year or month ref, ...
>>> cal.getdate(["first day", "next wed"], ["2002-01", "2021-02-10"])
Traceback (most recent call last):
...
ValueError: getdate refs must all be of one kind (year, month or date); found: date, month
>>> print(cal.getdate(["first bizday", "last bizday"], ["2024-01", "2024-02"]))
['2024-01-02' '2024-02-29']
Migration: manual. Fix ordinal suffixes and split calls that mix ref kinds.
getdate with a year ref¶
v1: cal.getdate("first bizday before first day", 2024) gave
2024-01-02. This was a bug: the business day before 1 January is in the
year before.
v2: the same call gives 2023-12-29, the business day before 1 January.
Why: the old result was wrong.
Before (1.0.20):
cal.getdate("first bizday before first day", 2024) # datetime.date(2024, 1, 2)
After (2.0):
>>> print(cal.getdate("first bizday before first day", 2024))
2023-12-29
Migration: nothing to change. The call still runs, but check results that you stored with 1.0.20.
getbizdays¶
getbizdays¶
v1: getbizdays(year, month=None) returned an int (or a list of
int). A period that went past the calendar end gave a partial count: on
ANBIMA, getbizdays(2099) gave 245. getbizdays(year, 0) meant the
whole year.
v2:
results are
float64(NaNwhereyearormonthis missing);the whole period must lie inside the calendar range, or
DateOutOfRangeis raised;monthmust be between 1 and 12;0raisesValueError(leavemonthout, or passmonth=None, for the whole year);vectors follow the length rules of Vector lengths.
Why: a partial count looks like a full one, and 0 is not a month.
Before (1.0.20):
cal.getbizdays(2024) # 253
cal.getbizdays(2099) # 245 (partial)
cal.getbizdays(2024, 0) # 253
cal.getbizdays([2023, 2024]) # [249, 253]
After (2.0):
>>> print(cal.getbizdays(2024))
253.0
>>> print(cal.getbizdays(2024, 12))
21.0
>>> cal.getbizdays(2099)
Traceback (most recent call last):
...
DateOutOfRange: 1 of 1 elements out of calendar range 2000-01-01..2099-12-25; ...
>>> cal.getbizdays(2024, 0)
Traceback (most recent call last):
...
ValueError: month must be between 1 and 12 (got 0)
>>> print(cal.getbizdays([2023, 2024]))
[249. 253.]
Migration: manual. Replace month=0 with no month, use int(...)
where you need integers, and check periods near the calendar ends.
Calendar data¶
Calendar data¶
v1: in 1.0.20 the B3 calendar runs to 2099-12-31: official up to 2026,
projected from 2027. In 1.0.19 it ended on 2026-12-31, so
Calendar.load("B3").bizdays("2026-12-01", "2027-01-10") raised
DateOutOfRange. The Actual calendar covered 1970-01-01..2071-01-01.
v2: B3 has the same data as in 1.0.20: official for 2000-2026, and
projected for 2027-2099 (ANBIMA holidays, plus December 24, plus the last
weekday of the year). Actual
covers 1900-01-01..2199-12-31. The range of Calendar() without holidays
or dates does not change (1970-01-01..2071-01-01). See Calendar data
for every range and the update policy.
Why: a calendar that ends soon breaks long-dated contracts.
Before (1.0.20):
b3 = Calendar.load("B3")
b3.enddate # datetime.date(2099, 12, 31); 2026-12-31 in 1.0.19
b3.bizdays("2026-12-01", "2027-01-10") # 24; DateOutOfRange in 1.0.19
Calendar.load("Actual").enddate # datetime.date(2071, 1, 1)
After (2.0):
>>> b3 = Calendar.load(name="B3")
>>> print(b3.enddate)
2099-12-31
>>> print(b3.bizdays("2026-12-01", "2027-01-10"))
24.0
>>> actual = Calendar.load(name="Actual")
>>> print(actual.startdate, actual.enddate)
1900-01-01 2199-12-31
>>> print(Calendar().startdate, Calendar().enddate)
1970-01-01 2071-01-01
Migration: nothing to change. Calls that raised DateOutOfRange in
1.0.19, or on the Actual calendar after 2071, now work. Projected B3
years can change when B3 publishes its official calendar.
New in 2.0¶
These are new features, not breaking changes. See API reference for details.
bizdays.list_calendars()lists the packaged and provider calendars.bizdays.Calendar.inrange()tells which dates are inside the calendar range.bizdays.DateOutOfRangehas apositionsattribute.numpy.datetime64values and pandasTimestamp,SeriesandDatetimeIndexinputs work everywhere (1.0.20 rejectednumpy.datetime64with aValueError).XCAL/<name>andWORK/<code>calendar providers.getdatedate refs and vectors of expressions and refs.getdateprevious <weekday>with a date ref.Empty inputs give empty results (1.0.20 raised
ZeroDivisionError).
What did not change¶
Method names, and both alias pairs:
following/adjust_nextandpreceding/adjust_previous.Property types:
holidays,startdateandenddateare still Pythondatetime.datevalues.Counting rules, including ends that are not business days (
bizdays(a, b) == bizdays(following(a), preceding(b))) andfinancial=False.offset(d, 0)returnsdunchanged, even whendis a holiday.seqwith the start after the end gives a descending sequence.Time-zone-aware values use the wall-clock date: 2024-01-06 23:00 at -03:00 is 2024-01-06.
seqwith a vector raisesValueError.getdatewith an unknown weekday word, such astues,wednormonkey, raisesValueError.ANBIMA and B3 holidays are the same as in 1.0.20 (and, up to 2026, the same as in 1.0.19).
>>> print(cal.bizdays("2024-01-02", "2024-06-15"),
... cal.bizdays(cal.following("2024-01-02"), cal.preceding("2024-06-15")))
113.0 113.0