Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,7 @@ Categorical
^^^^^^^^^^^
- Bug in :class:`Categorical` where constructing from a pandas :class:`Series` or :class:`Index` with ``dtype='object'`` did not preserve the categories' dtype as ``object``; now the ``categories.dtype`` is preserved as ``object`` for these cases, while numpy arrays and Python sequences with ``dtype='object'`` continue to infer the most specific dtype (for example, ``str`` if all elements are strings) (:issue:`61778`)
- Bug in :func:`Series.apply` where ``nan`` was ignored for :class:`CategoricalDtype` (:issue:`59938`)
- Bug in :func:`bdate_range` raising ``ValueError`` with frequency ``freq="cbh"`` (:issue:`62849`)
- Bug in :func:`testing.assert_index_equal` raising ``TypeError`` instead of ``AssertionError`` for incomparable ``CategoricalIndex`` when ``check_categorical=True`` and ``exact=False`` (:issue:`61935`)
- Bug in :meth:`Categorical.astype` where ``copy=False`` would still trigger a copy of the codes (:issue:`62000`)
- Bug in :meth:`DataFrame.pivot` and :meth:`DataFrame.set_index` raising an ``ArrowNotImplementedError`` for columns with pyarrow dictionary dtype (:issue:`53051`)
Expand Down
6 changes: 4 additions & 2 deletions pandas/core/indexes/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1133,12 +1133,14 @@ def bdate_range(
msg = "freq must be specified for bdate_range; use date_range instead"
raise TypeError(msg)

if isinstance(freq, str) and freq.startswith("C"):
if isinstance(freq, str) and freq.upper().startswith("C"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to allow strings that start with uppercase "C"? or just lowercase?

Copy link
Contributor Author

@natmokval natmokval Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbrockmendel, thanks for reviewing this PR.
I think we want to cover both cases, strings start with "C" and with "c", because bdate_range should work for all custom frequencies.

I would prefer to do an explicit check on line 1136, like freq in ["CBMS", "CBME", "C", "cbh"]. In this case, there’s no reason to keep the exception block below.

except (KeyError, TypeError) as err:
msg = f"invalid custom frequency string: {freq}"
raise ValueError(msg) from err

We should also update the error message in the test_all_custom_freq.
Do you think, it is better to check explicit insted of freq.upper().startswith("C")?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess if they pass an upper-case the ideal thing would be an exception message telling them specifically to do lower case? im happy to defer to your judgement on this

Copy link
Contributor Author

@natmokval natmokval Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess if they pass an upper-case the ideal thing would be an exception message telling them specifically to do lower case? im happy to defer to your judgement on this

I agree, for incorrect casing we could raise an error message and suggest correct casing. It seems we only have one incorrect casing for custom frequencies: "CBH". Other cases like "cbme", "c" or "Cbh" we consider as invalid frequencies.

I will make changes and add the specific exception message with suggestion to do lower case for "CBH".

msg = f"invalid custom frequency string: {freq}"
if freq == "CBH":
raise ValueError(f"{msg}, did you mean cbh?")
try:
weekmask = weekmask or "Mon Tue Wed Thu Fri"
freq = prefix_mapping[freq](holidays=holidays, weekmask=weekmask)
except (KeyError, TypeError) as err:
msg = f"invalid custom frequency string: {freq}"
raise ValueError(msg) from err
elif holidays or weekmask:
msg = (
Expand Down
35 changes: 34 additions & 1 deletion pandas/tests/indexes/datetimes/test_date_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -1216,7 +1216,7 @@ def test_cdaterange_holidays_weekmask_requires_freqstr(self):
)

@pytest.mark.parametrize(
"freq", [freq for freq in prefix_mapping if freq.startswith("C")]
"freq", [freq for freq in prefix_mapping if freq.upper().startswith("C")]
)
def test_all_custom_freq(self, freq):
# should not raise
Expand Down Expand Up @@ -1280,6 +1280,39 @@ def test_data_range_custombusinessday_partial_time(self, unit):
)
tm.assert_index_equal(result, expected)

def test_cdaterange_cbh(self):
# GH#62849
result = bdate_range(
"2009-03-13",
"2009-03-15",
freq="cbh",
weekmask="Mon Wed Fri",
holidays=["2009-03-14"],
)
expected = DatetimeIndex(
[
"2009-03-13 09:00:00",
"2009-03-13 10:00:00",
"2009-03-13 11:00:00",
"2009-03-13 12:00:00",
"2009-03-13 13:00:00",
"2009-03-13 14:00:00",
"2009-03-13 15:00:00",
"2009-03-13 16:00:00",
],
dtype="datetime64[ns]",
freq="cbh",
)
tm.assert_index_equal(result, expected)

def test_cdaterange_deprecated_error_CBH(self):
# GH#62849
msg = "invalid custom frequency string: CBH, did you mean cbh?"
with pytest.raises(ValueError, match=msg):
bdate_range(
START, END, freq="CBH", weekmask="Mon Wed Fri", holidays=["2009-03-14"]
)


class TestDateRangeNonNano:
def test_date_range_reso_validation(self):
Expand Down
Loading