Skip to content

Commit 2a5fecb

Browse files
committed
Add rot90 method to rotate an array 90 degrees in a plane
Add `ArrayBase::rot90(k, axes)`, the equivalent of NumPy's `numpy.rot90`. It rotates the array `k` times by 90 degrees in the plane spanned by the two given axes, going from `axes.0` toward `axes.1`. Like the existing axis-manipulation methods it moves no data, only adjusting the dimensions and strides via `swap_axes`/`invert_axis`. `k` is reduced modulo 4 with `rem_euclid`, so negative values rotate in the opposite direction. Closes #866.
1 parent bd3ade9 commit 2a5fecb

3 files changed

Lines changed: 99 additions & 0 deletions

File tree

RELEASES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
Unreleased
2+
==========
3+
4+
Added
5+
-----
6+
- 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)
7+
18
Version 0.17.2 (2026-01-10)
29
===========================
310
Version 0.17.2 is mainly a patch fix to bugs related to the new `ArrayRef` implementation.

src/impl_methods.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2626,6 +2626,63 @@ where
26262626
self.parts.dim.slice_mut().reverse();
26272627
self.parts.strides.slice_mut().reverse();
26282628
}
2629+
2630+
/// Rotate the array by 90 degrees, `k` times, in the plane spanned by two axes.
2631+
///
2632+
/// The rotation is performed from the axis `axes.0` towards the axis
2633+
/// `axes.1`, matching the behavior of NumPy's [`numpy.rot90`]. A positive
2634+
/// `k` rotates counterclockwise (in the sense of that axis ordering) and a
2635+
/// negative `k` rotates clockwise; `k` is taken modulo 4, so e.g. `k == -1`
2636+
/// is equivalent to `k == 3`.
2637+
///
2638+
/// This does not move any data, it just adjusts the array's dimensions and
2639+
/// strides. Because it consumes `self`, call it on `self.view()` (or
2640+
/// `self.clone()`) if you want to keep the original array.
2641+
///
2642+
/// **Panics** if the two axes are the same or if either axis is out of
2643+
/// bounds.
2644+
///
2645+
/// [`numpy.rot90`]: https://numpy.org/doc/stable/reference/generated/numpy.rot90.html
2646+
///
2647+
/// # Examples
2648+
///
2649+
/// ```
2650+
/// use ndarray::{arr2, Array3};
2651+
///
2652+
/// let a = arr2(&[[1, 2, 3], [4, 5, 6]]);
2653+
/// assert_eq!(a.clone().rot90(1, (0, 1)), arr2(&[[3, 6], [2, 5], [1, 4]]));
2654+
/// assert_eq!(a.clone().rot90(2, (0, 1)), arr2(&[[6, 5, 4], [3, 2, 1]]));
2655+
/// assert_eq!(a.clone().rot90(3, (0, 1)), arr2(&[[4, 1], [5, 2], [6, 3]]));
2656+
/// assert_eq!(a.clone().rot90(4, (0, 1)), a);
2657+
///
2658+
/// // Rotating in a plane of a higher-dimensional array only affects that plane.
2659+
/// let b = Array3::<u8>::zeros((1, 2, 3));
2660+
/// assert_eq!(b.rot90(1, (0, 2)).shape(), &[3, 2, 1]);
2661+
/// ```
2662+
#[track_caller]
2663+
pub fn rot90(mut self, k: i32, axes: (usize, usize)) -> ArrayBase<S, D>
2664+
{
2665+
let (a0, a1) = axes;
2666+
assert_ne!(a0, a1, "rot90: axes must be different");
2667+
assert!(a0 < self.ndim() && a1 < self.ndim(), "rot90: axis out of bounds");
2668+
match k.rem_euclid(4) {
2669+
0 => {}
2670+
1 => {
2671+
self.swap_axes(a0, a1);
2672+
self.invert_axis(Axis(a0));
2673+
}
2674+
2 => {
2675+
self.invert_axis(Axis(a0));
2676+
self.invert_axis(Axis(a1));
2677+
}
2678+
3 => {
2679+
self.swap_axes(a0, a1);
2680+
self.invert_axis(Axis(a1));
2681+
}
2682+
_ => unreachable!(),
2683+
}
2684+
self
2685+
}
26292686
}
26302687

26312688
impl<A, D: Dimension> ArrayRef<A, D>

tests/array.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,41 @@ fn permuted_axes_oob()
947947
a.view().permuted_axes([1, 0, 3]);
948948
}
949949

950+
#[test]
951+
fn test_rot90()
952+
{
953+
let a = arr2(&[[1, 2, 3], [4, 5, 6]]);
954+
assert_eq!(a.clone().rot90(1, (0, 1)), arr2(&[[3, 6], [2, 5], [1, 4]]));
955+
assert_eq!(a.clone().rot90(2, (0, 1)), arr2(&[[6, 5, 4], [3, 2, 1]]));
956+
assert_eq!(a.clone().rot90(3, (0, 1)), arr2(&[[4, 1], [5, 2], [6, 3]]));
957+
assert_eq!(a.clone().rot90(4, (0, 1)), a);
958+
assert_eq!(a.clone().rot90(0, (0, 1)), a);
959+
// negative k rotates clockwise (k == -1 is the same as k == 3)
960+
assert_eq!(a.clone().rot90(-1, (0, 1)), a.clone().rot90(3, (0, 1)));
961+
// rotating in the reversed axis plane goes the opposite direction
962+
assert_eq!(a.clone().rot90(1, (1, 0)), a.clone().rot90(3, (0, 1)));
963+
964+
// N-dimensional: rotation only affects the chosen plane
965+
let b = Array3::<u8>::zeros((1, 2, 3));
966+
assert_eq!(b.rot90(1, (0, 2)).shape(), &[3, 2, 1]);
967+
}
968+
969+
#[test]
970+
#[should_panic]
971+
fn test_rot90_same_axis()
972+
{
973+
let a = arr2(&[[1, 2], [3, 4]]);
974+
a.rot90(1, (0, 0));
975+
}
976+
977+
#[test]
978+
#[should_panic]
979+
fn test_rot90_oob()
980+
{
981+
let a = arr2(&[[1, 2], [3, 4]]);
982+
a.rot90(1, (0, 2));
983+
}
984+
950985
#[test]
951986
fn standard_layout()
952987
{

0 commit comments

Comments
 (0)