diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index c6c4dadaf8c9d..b686bcf690e8f 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -782,6 +782,7 @@ Strings - Bug in the conversion from ``pyarrow.ChunkedArray`` to :class:`~arrays.StringArray` when the original had zero chunks (:issue:`41040`) - Bug in :meth:`Series.replace` and :meth:`DataFrame.replace` ignoring replacements with ``regex=True`` for ``StringDType`` data (:issue:`41333`, :issue:`35977`) - Bug in :meth:`Series.str.extract` with :class:`~arrays.StringArray` returning object dtype for empty :class:`DataFrame` (:issue:`41441`) +- Bug in :meth:`Series.str.replace` where the ``case`` argument was ignored when ``regex=False`` (:issue:`41602`) Interval ^^^^^^^^ diff --git a/pandas/core/strings/accessor.py b/pandas/core/strings/accessor.py index 8e6f4bfd3b320..ca0067b77ee23 100644 --- a/pandas/core/strings/accessor.py +++ b/pandas/core/strings/accessor.py @@ -1358,14 +1358,13 @@ def replace( "*not* be treated as literal strings when regex=True." ) warnings.warn(msg, FutureWarning, stacklevel=3) - regex = True # Check whether repl is valid (GH 13438, GH 15055) if not (isinstance(repl, str) or callable(repl)): raise TypeError("repl must be a string or callable") is_compiled_re = is_re(pat) - if regex: + if regex or regex is None: if is_compiled_re and (case is not None or flags != 0): raise ValueError( "case and flags cannot be set when pat is a compiled regex" @@ -1378,6 +1377,14 @@ def replace( elif callable(repl): raise ValueError("Cannot use a callable replacement when regex=False") + # The current behavior is to treat single character patterns as literal strings, + # even when ``regex`` is set to ``True``. + if isinstance(pat, str) and len(pat) == 1: + regex = False + + if regex is None: + regex = True + if case is None: case = True diff --git a/pandas/core/strings/object_array.py b/pandas/core/strings/object_array.py index fb9fd77d21732..c214ada9c1ada 100644 --- a/pandas/core/strings/object_array.py +++ b/pandas/core/strings/object_array.py @@ -145,10 +145,10 @@ def _str_replace( # add case flag, if provided flags |= re.IGNORECASE - if regex and ( - isinstance(pat, re.Pattern) or len(pat) > 1 or flags or callable(repl) - ): + if regex or flags or callable(repl): if not isinstance(pat, re.Pattern): + if regex is False: + pat = re.escape(pat) pat = re.compile(pat, flags=flags) n = n if n >= 0 else 0 diff --git a/pandas/tests/strings/test_find_replace.py b/pandas/tests/strings/test_find_replace.py index 2104b50c5121a..391c71e57399a 100644 --- a/pandas/tests/strings/test_find_replace.py +++ b/pandas/tests/strings/test_find_replace.py @@ -555,6 +555,19 @@ def test_replace_moar(any_string_dtype): tm.assert_series_equal(result, expected) +def test_replace_not_case_sensitive_not_regex(any_string_dtype): + # https://github.com/pandas-dev/pandas/issues/41602 + ser = Series(["A.", "a.", "Ab", "ab", np.nan], dtype=any_string_dtype) + + result = ser.str.replace("a", "c", case=False, regex=False) + expected = Series(["c.", "c.", "cb", "cb", np.nan], dtype=any_string_dtype) + tm.assert_series_equal(result, expected) + + result = ser.str.replace("a.", "c.", case=False, regex=False) + expected = Series(["c.", "c.", "Ab", "ab", np.nan], dtype=any_string_dtype) + tm.assert_series_equal(result, expected) + + def test_replace_regex_default_warning(any_string_dtype): # https://github.com/pandas-dev/pandas/pull/24809 s = Series(["a", "b", "ac", np.nan, ""], dtype=any_string_dtype)