diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 712971192a4..4df2ffd1db7 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -168,6 +168,11 @@ Bug Fixes the array. Previously, e.g. ``da.cumulative("time").argmax()`` returned indices shifted by the number of missing values in front of the array (:issue:`11336`). By `Michael Niklas `_. +- :py:meth:`DataArray.str.replace` now respects ``case=False`` when ``regex=False``; + the option was silently ignored. :py:meth:`DataArray.str.contains` with + ``case=False, regex=False`` no longer raises ``AttributeError`` on object-dtype + or ``StringDType`` arrays (:pull:`11747`). + By `Novain Shibu `_. - Fix :py:func:`tutorial.open_dataset` and :py:func:`tutorial.open_datatree` with ``cache=False`` failing on Windows, because the downloaded file was removed while still open (:pull:`11703`). diff --git a/xarray/core/accessor_str.py b/xarray/core/accessor_str.py index 103de9e20c1..fb13f53d70d 100644 --- a/xarray/core/accessor_str.py +++ b/xarray/core/accessor_str.py @@ -1520,14 +1520,10 @@ def func(x, ipat): pat = self._stringify(pat) if case or case is None: func = lambda x, ipat: ipat in x - elif self._obj.dtype.char == "U": - uppered = self.casefold() - uppat = StringAccessor(pat).casefold() # type: ignore[type-var] # hack? - return uppered.str.contains(uppat, regex=False) # type: ignore[return-value] + elif self._obj.dtype.kind == "S": + func = lambda x, ipat: ipat.upper() in x.upper() else: - uppered = self.upper() - uppat = StringAccessor(pat).upper() # type: ignore[type-var] # hack? - return uppered.str.contains(uppat, regex=False) # type: ignore[return-value] + func = lambda x, ipat: ipat.casefold() in x.casefold() return self._apply(func=func, func_args=(pat,), dtype=bool) @@ -1962,7 +1958,22 @@ def replace( ) else: pat = self._stringify(pat) - func = lambda x, ipat, irepl, i_n: x.replace(ipat, irepl, i_n) + if case is False: + # Literal match, but case-insensitive: escape the pattern and + # return the replacement as-is so it is not parsed as a template. + func = lambda x, ipat, irepl, i_n: ( + x + if i_n == 0 + else re.sub( + re.escape(ipat), + lambda _: irepl, + x, + count=max(i_n, 0), + flags=re.IGNORECASE, + ) + ) + else: + func = lambda x, ipat, irepl, i_n: x.replace(ipat, irepl, i_n) return self._apply(func=func, func_args=(pat, repl, n)) def extract( diff --git a/xarray/tests/test_accessor_str.py b/xarray/tests/test_accessor_str.py index 55ba2b55f5e..e71558aaca1 100644 --- a/xarray/tests/test_accessor_str.py +++ b/xarray/tests/test_accessor_str.py @@ -403,6 +403,52 @@ def test_replace_n_zero(dtype) -> None: assert_equal(result, expected) +def test_replace_literal_case_insensitive(dtype) -> None: + values = xr.DataArray(["fooBAD.bad__barbAd"], dims=["x"]).astype(dtype) + + result = values.str.replace("bad", "X", case=False, regex=False) + expected = xr.DataArray(["fooX.X__barX"], dims=["x"]).astype(dtype) + assert result.dtype == expected.dtype + assert_equal(result, expected) + + # the pattern is literal, and the replacement is not a regex template + result = values.str.replace("D.B", r"", n=1, case=False, regex=False) + expected = xr.DataArray([r"fooBAad__barbAd"], dims=["x"]).astype(dtype) + assert result.dtype == expected.dtype + assert_equal(result, expected) + + result = values.str.replace("bad", "X", n=2, case=False, regex=False) + expected = xr.DataArray(["fooX.X__barbAd"], dims=["x"]).astype(dtype) + assert_equal(result, expected) + + result = values.str.replace("bad", "X", n=0, case=False, regex=False) + assert_equal(result, values) + + +@pytest.mark.parametrize( + "values", + [ + pytest.param(np.array(["Foo", "xYz", "fOO.oo"], dtype=object), id="object"), + pytest.param( + np.array(["Foo", "xYz", "fOO.oo"], dtype=np.dtypes.StringDType()), + id="StringDType", + ), + ], +) +def test_case_insensitive_literal_non_unicode_dtype(values) -> None: + da = xr.DataArray(values, dims=["x"]) + + result = da.str.contains("o.", case=False, regex=False) + assert_equal(result, xr.DataArray([False, False, True], dims=["x"])) + + result = da.str.replace("o", "_", case=False, regex=False) + expected = xr.DataArray( + np.array(["F__", "xYz", "f__.__"], dtype=values.dtype), dims=["x"] + ) + assert result.dtype == expected.dtype + assert_equal(result, expected) + + def test_replace_callable() -> None: values = xr.DataArray(["fooBAD__barBAD"])