diff --git a/xarray/indexes/range_index.py b/xarray/indexes/range_index.py index 34e6c2f7c00..ab619c6f722 100644 --- a/xarray/indexes/range_index.py +++ b/xarray/indexes/range_index.py @@ -20,8 +20,9 @@ class RangeCoordinateTransform(CoordinateTransform): start: float stop: float + _step: float | None - __slots__ = ("start", "stop") + __slots__ = ("_step", "start", "stop") def __init__( self, @@ -39,6 +40,7 @@ def __init__( self.start = start self.stop = stop + self._step = None # Will be calculated by property @property def coord_name(self) -> Hashable: @@ -54,7 +56,13 @@ def size(self) -> int: @property def step(self) -> float: - return (self.stop - self.start) / self.size + if self._step is not None: + return self._step + if self.size > 0: + return (self.stop - self.start) / self.size + else: + # For empty arrays, default to 1.0 + return 1.0 def forward(self, dim_positions: dict[str, Any]) -> dict[Hashable, Any]: positions = dim_positions[self.dim] @@ -81,12 +89,22 @@ def equals( def slice(self, sl: slice) -> "RangeCoordinateTransform": new_range = range(self.size)[sl] new_size = len(new_range) + new_start = self.start + new_range.start * self.step new_stop = self.start + new_range.stop * self.step - return type(self)( - new_start, new_stop, new_size, self.coord_name, self.dim, dtype=self.dtype + result = type(self)( + new_start, + new_stop, + new_size, + self.coord_name, + self.dim, + dtype=self.dtype, ) + if new_size == 0: + # For empty slices, preserve step from parent + result._step = self.step + return result class RangeIndex(CoordinateTransformIndex): diff --git a/xarray/tests/test_range_index.py b/xarray/tests/test_range_index.py index d0644ba73a2..a2412b75634 100644 --- a/xarray/tests/test_range_index.py +++ b/xarray/tests/test_range_index.py @@ -166,6 +166,62 @@ def test_range_index_isel() -> None: assert_identical(actual, expected) +def test_range_index_empty_slice() -> None: + """Test that empty slices of RangeIndex are printable and preserve step. + + Regression test for https://github.com/pydata/xarray/issues/10547 + """ + # Test with linspace + n = 30 + step = 1 + da = xr.DataArray(np.zeros(n), dims=["x"]) + da = da.assign_coords( + xr.Coordinates.from_xindex(RangeIndex.linspace(0, (n - 1) * step, n, dim="x")) + ) + + # This should not raise ZeroDivisionError + sub = da.isel(x=slice(0)) + assert sub.sizes["x"] == 0 + + # Test that it's printable + repr_str = repr(sub) + assert "RangeIndex" in repr_str + assert "step=1" in repr_str + + # Test with different step values + index = RangeIndex.arange(0, 10, 2.5, dim="y") + da2 = xr.DataArray(np.zeros(4), dims=["y"]) + da2 = da2.assign_coords(xr.Coordinates.from_xindex(index)) + empty = da2.isel(y=slice(0)) + + # Should preserve step + assert empty.sizes["y"] == 0 + range_index_y = empty._indexes["y"] + assert isinstance(range_index_y, RangeIndex) + assert range_index_y.step == 2.5 + + # Test that it's printable + repr_str2 = repr(empty) + assert "RangeIndex" in repr_str2 + assert "step=2.5" in repr_str2 + + # Test negative step + index3 = RangeIndex.arange(10, 0, -1, dim="z") + da3 = xr.DataArray(np.zeros(10), dims=["z"]) + da3 = da3.assign_coords(xr.Coordinates.from_xindex(index3)) + empty3 = da3.isel(z=slice(0)) + + assert empty3.sizes["z"] == 0 + range_index_z = empty3._indexes["z"] + assert isinstance(range_index_z, RangeIndex) + assert range_index_z.step == -1.0 + + # Test that it's printable + repr_str3 = repr(empty3) + assert "RangeIndex" in repr_str3 + assert "step=-1" in repr_str3 + + def test_range_index_sel() -> None: ds = create_dataset_arange(0.0, 1.0, 0.1)