diff --git a/RELEASES.md b/RELEASES.md index 21f875a92..15e59de3a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,10 @@ +Unreleased +========== + +Added +----- +- Add `ArrayBase::rot90` to rotate an array 90 degrees in a plane, equivalent to NumPy's `numpy.rot90` [#866](https://github.com/rust-ndarray/ndarray/issues/866) + Version 0.17.2 (2026-01-10) =========================== Version 0.17.2 is mainly a patch fix to bugs related to the new `ArrayRef` implementation. diff --git a/src/impl_methods.rs b/src/impl_methods.rs index 8046f4d1f..7d6a11cd5 100644 --- a/src/impl_methods.rs +++ b/src/impl_methods.rs @@ -2626,6 +2626,63 @@ where self.parts.dim.slice_mut().reverse(); self.parts.strides.slice_mut().reverse(); } + + /// Rotate the array by 90 degrees, `k` times, in the plane spanned by two axes. + /// + /// The rotation is performed from the axis `axes.0` towards the axis + /// `axes.1`, matching the behavior of NumPy's [`numpy.rot90`]. A positive + /// `k` rotates counterclockwise (in the sense of that axis ordering) and a + /// negative `k` rotates clockwise; `k` is taken modulo 4, so e.g. `k == -1` + /// is equivalent to `k == 3`. + /// + /// This does not move any data, it just adjusts the array's dimensions and + /// strides. Because it consumes `self`, call it on `self.view()` (or + /// `self.clone()`) if you want to keep the original array. + /// + /// **Panics** if the two axes are the same or if either axis is out of + /// bounds. + /// + /// [`numpy.rot90`]: https://numpy.org/doc/stable/reference/generated/numpy.rot90.html + /// + /// # Examples + /// + /// ``` + /// use ndarray::{arr2, Array3}; + /// + /// let a = arr2(&[[1, 2, 3], [4, 5, 6]]); + /// assert_eq!(a.clone().rot90(1, (0, 1)), arr2(&[[3, 6], [2, 5], [1, 4]])); + /// assert_eq!(a.clone().rot90(2, (0, 1)), arr2(&[[6, 5, 4], [3, 2, 1]])); + /// assert_eq!(a.clone().rot90(3, (0, 1)), arr2(&[[4, 1], [5, 2], [6, 3]])); + /// assert_eq!(a.clone().rot90(4, (0, 1)), a); + /// + /// // Rotating in a plane of a higher-dimensional array only affects that plane. + /// let b = Array3::::zeros((1, 2, 3)); + /// assert_eq!(b.rot90(1, (0, 2)).shape(), &[3, 2, 1]); + /// ``` + #[track_caller] + pub fn rot90(mut self, k: i32, axes: (usize, usize)) -> ArrayBase + { + let (a0, a1) = axes; + assert_ne!(a0, a1, "rot90: axes must be different"); + assert!(a0 < self.ndim() && a1 < self.ndim(), "rot90: axis out of bounds"); + match k.rem_euclid(4) { + 0 => {} + 1 => { + self.swap_axes(a0, a1); + self.invert_axis(Axis(a0)); + } + 2 => { + self.invert_axis(Axis(a0)); + self.invert_axis(Axis(a1)); + } + 3 => { + self.swap_axes(a0, a1); + self.invert_axis(Axis(a1)); + } + _ => unreachable!(), + } + self + } } impl ArrayRef diff --git a/tests/array.rs b/tests/array.rs index 391f88b95..ba866b395 100644 --- a/tests/array.rs +++ b/tests/array.rs @@ -947,6 +947,41 @@ fn permuted_axes_oob() a.view().permuted_axes([1, 0, 3]); } +#[test] +fn test_rot90() +{ + let a = arr2(&[[1, 2, 3], [4, 5, 6]]); + assert_eq!(a.clone().rot90(1, (0, 1)), arr2(&[[3, 6], [2, 5], [1, 4]])); + assert_eq!(a.clone().rot90(2, (0, 1)), arr2(&[[6, 5, 4], [3, 2, 1]])); + assert_eq!(a.clone().rot90(3, (0, 1)), arr2(&[[4, 1], [5, 2], [6, 3]])); + assert_eq!(a.clone().rot90(4, (0, 1)), a); + assert_eq!(a.clone().rot90(0, (0, 1)), a); + // negative k rotates clockwise (k == -1 is the same as k == 3) + assert_eq!(a.clone().rot90(-1, (0, 1)), a.clone().rot90(3, (0, 1))); + // rotating in the reversed axis plane goes the opposite direction + assert_eq!(a.clone().rot90(1, (1, 0)), a.clone().rot90(3, (0, 1))); + + // N-dimensional: rotation only affects the chosen plane + let b = Array3::::zeros((1, 2, 3)); + assert_eq!(b.rot90(1, (0, 2)).shape(), &[3, 2, 1]); +} + +#[test] +#[should_panic] +fn test_rot90_same_axis() +{ + let a = arr2(&[[1, 2], [3, 4]]); + a.rot90(1, (0, 0)); +} + +#[test] +#[should_panic] +fn test_rot90_oob() +{ + let a = arr2(&[[1, 2], [3, 4]]); + a.rot90(1, (0, 2)); +} + #[test] fn standard_layout() {