Skip to main content

ring_seq/
circular.rs

1//! The [`Circular`] wrapper and its element iterator.
2//!
3//! Reach the wrapper through [`AsCircular::circular`] on any slice (or
4//! anything that derefs to one — [`Vec<T>`](alloc::vec::Vec), arrays,
5//! [`Box<[T]>`](alloc::boxed::Box)). The wrapper is the single home for
6//! every circular operation; all transforms it offers are lazy and
7//! allocation-free.
8
9use core::iter::FusedIterator;
10
11#[cfg(feature = "alloc")]
12use alloc::vec::Vec;
13
14#[cfg(feature = "alloc")]
15use crate::AxisLocation;
16
17/// A borrowed view of a slice as a circular sequence.
18///
19/// Carries a `(slice, offset, reflected)` triple. Every operation routes
20/// through a single index-mapping function, so the algebra stays consistent
21/// however the view was constructed.
22///
23/// The type is `Copy` and trivially cheap to pass by value.
24///
25/// # Examples
26///
27/// ```
28/// use ring_seq::AsCircular;
29///
30/// let ring = [10, 20, 30];
31/// let c = ring.circular();
32/// assert_eq!(c.len(), 3);
33/// assert_eq!(*c.apply(4), 20); // wraps
34/// ```
35pub struct Circular<'a, T> {
36    ring: &'a [T],
37    offset: usize,
38    reflected: bool,
39}
40
41// Hand-implemented so they don't pick up a phantom `T: Clone` bound from
42// the derive macro. The wrapper is value-typed and trivially copyable for
43// every `T`.
44impl<T> Clone for Circular<'_, T> {
45    fn clone(&self) -> Self {
46        *self
47    }
48}
49impl<T> Copy for Circular<'_, T> {}
50
51impl<T: core::fmt::Debug> core::fmt::Debug for Circular<'_, T> {
52    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
53        f.debug_struct("Circular")
54            .field("ring", &self.ring)
55            .field("offset", &self.offset)
56            .field("reflected", &self.reflected)
57            .finish()
58    }
59}
60
61impl<'a, T> Circular<'a, T> {
62    /// Constructs a fresh view over `ring` with zero offset and no reflection.
63    #[inline]
64    pub(crate) fn new(ring: &'a [T]) -> Self {
65        Circular {
66            ring,
67            offset: 0,
68            reflected: false,
69        }
70    }
71
72    /// Number of elements in the underlying ring.
73    #[inline]
74    #[must_use]
75    pub fn len(self) -> usize {
76        self.ring.len()
77    }
78
79    /// Returns `true` if the ring has no elements.
80    #[inline]
81    #[must_use]
82    pub fn is_empty(self) -> bool {
83        self.ring.is_empty()
84    }
85
86    /// Single source of truth: map a position within this view to an index
87    /// into the underlying slice.
88    ///
89    /// Caller must ensure `len() > 0`.
90    #[inline]
91    fn map_index(self, pos: usize) -> usize {
92        let n = self.ring.len();
93        debug_assert!(n > 0);
94        let p = pos % n;
95        if self.reflected {
96            (self.offset + n - p) % n
97        } else {
98            (self.offset + p) % n
99        }
100    }
101
102    /// Returns the element at the (possibly negative, possibly out-of-bounds)
103    /// circular index `i`.
104    ///
105    /// # Panics
106    ///
107    /// Panics if the ring is empty.
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use ring_seq::AsCircular;
113    ///
114    /// let r = [10, 20, 30].circular();
115    /// assert_eq!(*r.apply(0), 10);
116    /// assert_eq!(*r.apply(3), 10);    // wraps forward
117    /// assert_eq!(*r.apply(-1), 30);   // wraps backward
118    /// ```
119    #[inline]
120    #[must_use]
121    pub fn apply(self, i: isize) -> &'a T {
122        let n = self.ring.len();
123        assert!(n > 0, "cannot index into an empty ring");
124        let pos = i.rem_euclid(n as isize) as usize;
125        &self.ring[self.map_index(pos)]
126    }
127
128    /// Returns an iterator yielding the elements of this view in order.
129    ///
130    /// The iterator is `ExactSizeIterator` and `FusedIterator`.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use ring_seq::AsCircular;
136    ///
137    /// let r = [1, 2, 3].circular();
138    /// let collected: Vec<_> = r.iter().copied().collect();
139    /// assert_eq!(collected, vec![1, 2, 3]);
140    /// ```
141    #[inline]
142    #[must_use]
143    pub fn iter(self) -> CircularIter<'a, T> {
144        CircularIter {
145            view: self,
146            pos: 0,
147            remaining: self.ring.len(),
148        }
149    }
150
151    // -----------------------------------------------------------------------
152    // Reindexed views — return a new Circular, do not allocate
153    // -----------------------------------------------------------------------
154
155    /// Returns a view whose position 0 is the element at circular index `i`.
156    ///
157    /// Equivalent to [`rotate_left(i)`](Circular::rotate_left). All other
158    /// reindexed-view methods are defined in terms of this one.
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use ring_seq::AsCircular;
164    ///
165    /// let r = [10, 20, 30, 40].circular();
166    /// let v: Vec<_> = r.start_at(2).iter().copied().collect();
167    /// assert_eq!(v, vec![30, 40, 10, 20]);
168    /// ```
169    #[must_use]
170    pub fn start_at(self, i: isize) -> Circular<'a, T> {
171        let n = self.ring.len();
172        if n == 0 {
173            return self;
174        }
175        let i_pos = i.rem_euclid(n as isize) as usize;
176        let new_offset = if self.reflected {
177            (self.offset + n - i_pos) % n
178        } else {
179            (self.offset + i_pos) % n
180        };
181        Circular {
182            ring: self.ring,
183            offset: new_offset,
184            reflected: self.reflected,
185        }
186    }
187
188    /// Shifts the view left by `step` positions. Position 0 of the result
189    /// is what was at position `step` of `self`.
190    ///
191    /// `step` may be negative (delegates to [`rotate_right`](Self::rotate_right))
192    /// and may exceed `len` (wraps).
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// use ring_seq::AsCircular;
198    ///
199    /// let r = [1, 2, 3, 4].circular();
200    /// let v: Vec<_> = r.rotate_left(1).iter().copied().collect();
201    /// assert_eq!(v, vec![2, 3, 4, 1]);
202    /// ```
203    #[inline]
204    #[must_use]
205    pub fn rotate_left(self, step: isize) -> Circular<'a, T> {
206        self.start_at(step)
207    }
208
209    /// Shifts the view right by `step` positions. Position 0 of the result
210    /// is what was at position `-step` of `self`.
211    ///
212    /// `step` may be negative (delegates to [`rotate_left`](Self::rotate_left))
213    /// and may exceed `len` (wraps).
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// use ring_seq::AsCircular;
219    ///
220    /// let r = [1, 2, 3, 4].circular();
221    /// let v: Vec<_> = r.rotate_right(1).iter().copied().collect();
222    /// assert_eq!(v, vec![4, 1, 2, 3]);
223    /// ```
224    #[inline]
225    #[must_use]
226    pub fn rotate_right(self, step: isize) -> Circular<'a, T> {
227        self.start_at(-step)
228    }
229
230    /// Reflects the view around position `i`.
231    ///
232    /// Position 0 of the result is the element at position `i`; subsequent
233    /// positions walk backward from there. Flips the `reflected` bit of the
234    /// view (a second reflection at the same index recovers the original).
235    ///
236    /// # Examples
237    ///
238    /// ```
239    /// use ring_seq::AsCircular;
240    ///
241    /// let r = ['a', 'b', 'c', 'd'].circular();
242    /// let v: Vec<_> = r.reflect_at(0).iter().copied().collect();
243    /// assert_eq!(v, vec!['a', 'd', 'c', 'b']);
244    /// ```
245    #[must_use]
246    pub fn reflect_at(self, i: isize) -> Circular<'a, T> {
247        let n = self.ring.len();
248        if n == 0 {
249            return self;
250        }
251        let i_pos = i.rem_euclid(n as isize) as usize;
252        let new_offset = if self.reflected {
253            (self.offset + n - i_pos) % n
254        } else {
255            (self.offset + i_pos) % n
256        };
257        Circular {
258            ring: self.ring,
259            offset: new_offset,
260            reflected: !self.reflected,
261        }
262    }
263
264    // -----------------------------------------------------------------------
265    // Bounded iteration — return an iterator, do not allocate
266    // -----------------------------------------------------------------------
267
268    /// Returns an iterator over the circular range `[from, to)`.
269    ///
270    /// The range is taken in the view's forward direction starting at the
271    /// element with circular index `from`. Yields `max(to - from, 0)`
272    /// elements, wrapping around the ring as many times as needed; an
273    /// empty range produces an empty iterator.
274    ///
275    /// # Examples
276    ///
277    /// ```
278    /// use ring_seq::AsCircular;
279    ///
280    /// let r = [0, 1, 2, 3, 4].circular();
281    /// let v: Vec<_> = r.slice(2, 7).copied().collect();
282    /// assert_eq!(v, vec![2, 3, 4, 0, 1]);
283    /// ```
284    #[must_use]
285    pub fn slice(self, from: isize, to: isize) -> CircularIter<'a, T> {
286        if self.ring.is_empty() || to <= from {
287            return CircularIter {
288                view: self,
289                pos: 0,
290                remaining: 0,
291            };
292        }
293        let count = (to - from) as usize;
294        let started = self.start_at(from);
295        CircularIter {
296            view: started,
297            pos: 0,
298            remaining: count,
299        }
300    }
301
302    /// Returns an iterator yielding elements from circular index `from`
303    /// while `pred` holds, stopping at the first element that fails (or
304    /// after one full lap).
305    ///
306    /// # Examples
307    ///
308    /// ```
309    /// use ring_seq::AsCircular;
310    ///
311    /// let r = [1, 2, 3, 4, 5].circular();
312    /// let v: Vec<_> = r.take_while(|&x| x < 4, 0).copied().collect();
313    /// assert_eq!(v, vec![1, 2, 3]);
314    /// ```
315    pub fn take_while<P>(self, mut pred: P, from: isize) -> impl Iterator<Item = &'a T>
316    where
317        P: FnMut(&T) -> bool,
318    {
319        self.start_at(from).iter().take_while(move |x| pred(*x))
320    }
321
322    /// Returns an iterator that skips elements from circular index `from`
323    /// while `pred` holds, then yields the rest of the lap (one full
324    /// rotation maximum).
325    ///
326    /// # Examples
327    ///
328    /// ```
329    /// use ring_seq::AsCircular;
330    ///
331    /// let r = [1, 2, 3, 4, 5].circular();
332    /// let v: Vec<_> = r.drop_while(|&x| x < 4, 0).copied().collect();
333    /// assert_eq!(v, vec![4, 5]);
334    /// ```
335    pub fn drop_while<P>(self, mut pred: P, from: isize) -> impl Iterator<Item = &'a T>
336    where
337        P: FnMut(&T) -> bool,
338    {
339        self.start_at(from).iter().skip_while(move |x| pred(*x))
340    }
341
342    /// Returns an iterator yielding `(element, ring_index)` pairs starting
343    /// at circular index `from` and going around once.
344    ///
345    /// Unlike [`Iterator::enumerate`], the second element of each pair is
346    /// the element's index in the underlying ring (already wrapped to
347    /// `[0, len)`), not the iteration count.
348    ///
349    /// # Examples
350    ///
351    /// ```
352    /// use ring_seq::AsCircular;
353    ///
354    /// let r = ['a', 'b', 'c'].circular();
355    /// let v: Vec<_> = r.enumerate(1).map(|(&c, i)| (c, i)).collect();
356    /// assert_eq!(v, vec![('b', 1), ('c', 2), ('a', 0)]);
357    /// ```
358    #[must_use]
359    pub fn enumerate(self, from: isize) -> Enumerate<'a, T> {
360        let started = if self.ring.is_empty() {
361            self
362        } else {
363            self.start_at(from)
364        };
365        Enumerate {
366            view: started,
367            pos: 0,
368            remaining: started.ring.len(),
369        }
370    }
371
372    // -----------------------------------------------------------------------
373    // Iterators of views — yield Circular<'a, T>
374    // -----------------------------------------------------------------------
375
376    /// Returns an iterator yielding each rotation of this view as a fresh
377    /// [`Circular`].
378    ///
379    /// For a non-empty ring of length `n` the iterator yields `n` items;
380    /// for an empty ring it yields one (empty) item. The `reflected` bit
381    /// of `self` is preserved by every yielded rotation.
382    ///
383    /// # Examples
384    ///
385    /// ```
386    /// use ring_seq::AsCircular;
387    ///
388    /// let r = [1, 2, 3].circular();
389    /// let starts: Vec<i32> = r.rotations().map(|v| *v.apply(0)).collect();
390    /// assert_eq!(starts, vec![1, 2, 3]);
391    /// ```
392    #[must_use]
393    pub fn rotations(self) -> Rotations<'a, T> {
394        let total = if self.ring.is_empty() {
395            1
396        } else {
397            self.ring.len()
398        };
399        Rotations {
400            base: self,
401            index: 0,
402            total,
403        }
404    }
405
406    /// Returns an iterator yielding the view and its reflection at
407    /// position 0.
408    ///
409    /// For a non-empty ring the iterator yields two items; for an empty
410    /// ring it yields one. The first item is always `self`.
411    ///
412    /// # Examples
413    ///
414    /// ```
415    /// use ring_seq::AsCircular;
416    ///
417    /// let r = [1, 2, 3, 4].circular();
418    /// let count = r.reflections().count();
419    /// assert_eq!(count, 2);
420    /// ```
421    #[must_use]
422    pub fn reflections(self) -> Reflections<'a, T> {
423        Reflections {
424            base: self,
425            state: 0,
426        }
427    }
428
429    /// Returns an iterator yielding the view and its reverse.
430    ///
431    /// "Reverse" here means walking the ring in the opposite direction
432    /// starting from position `len - 1` — equivalent to
433    /// `reflect_at(-1)`.
434    ///
435    /// For a non-empty ring the iterator yields two items; for an empty
436    /// ring it yields one.
437    ///
438    /// # Examples
439    ///
440    /// ```
441    /// use ring_seq::AsCircular;
442    ///
443    /// let r = [1, 2, 3].circular();
444    /// let last: Vec<_> = r.reversions()
445    ///     .map(|v| v.iter().copied().collect::<Vec<_>>())
446    ///     .collect();
447    /// assert_eq!(last, vec![vec![1, 2, 3], vec![3, 2, 1]]);
448    /// ```
449    #[must_use]
450    pub fn reversions(self) -> Reversions<'a, T> {
451        Reversions {
452            base: self,
453            state: 0,
454        }
455    }
456
457    /// Returns an iterator yielding every rotation of this view followed
458    /// by every rotation of its reflection at position 0.
459    ///
460    /// For a non-empty ring of length `n` the iterator yields `2n` items;
461    /// for an empty ring it yields one.
462    ///
463    /// # Examples
464    ///
465    /// ```
466    /// use ring_seq::AsCircular;
467    ///
468    /// let r = [1, 2, 3].circular();
469    /// assert_eq!(r.rotations_and_reflections().count(), 6);
470    /// ```
471    #[must_use]
472    pub fn rotations_and_reflections(self) -> RotationsAndReflections<'a, T> {
473        let n = self.ring.len();
474        let total = if n == 0 { 1 } else { 2 * n };
475        let reflected = if n == 0 { self } else { self.reflect_at(0) };
476        RotationsAndReflections {
477            base: self,
478            reflected,
479            index: 0,
480            total,
481            n,
482        }
483    }
484
485    /// Returns an iterator yielding every circular window of length `size`
486    /// (step 1) as a [`CircularIter`].
487    ///
488    /// For a non-empty ring of length `n` the iterator yields `n` windows;
489    /// for an empty ring it yields no windows.
490    ///
491    /// # Panics
492    ///
493    /// Panics if `size` is zero.
494    ///
495    /// # Examples
496    ///
497    /// ```
498    /// use ring_seq::AsCircular;
499    ///
500    /// let r = [1, 2, 3, 4].circular();
501    /// let windows: Vec<Vec<i32>> = r
502    ///     .windows(2)
503    ///     .map(|w| w.copied().collect())
504    ///     .collect();
505    /// assert_eq!(windows, vec![vec![1, 2], vec![2, 3], vec![3, 4], vec![4, 1]]);
506    /// ```
507    #[must_use]
508    pub fn windows(self, size: usize) -> Windows<'a, T> {
509        assert!(size > 0, "window size must be positive");
510        let total = if self.ring.is_empty() {
511            0
512        } else {
513            self.ring.len()
514        };
515        Windows {
516            base: self,
517            size,
518            step: 1,
519            index: 0,
520            total,
521        }
522    }
523
524    // -----------------------------------------------------------------------
525    // Indexing
526    // -----------------------------------------------------------------------
527
528    /// Normalizes a circular index to `[0, len)`.
529    ///
530    /// Uses Euclidean remainder so that negative indices wrap correctly.
531    ///
532    /// # Panics
533    ///
534    /// Panics if the ring is empty.
535    ///
536    /// # Examples
537    ///
538    /// ```
539    /// use ring_seq::AsCircular;
540    ///
541    /// let r = [10, 20, 30].circular();
542    /// assert_eq!(r.index_from(0), 0);
543    /// assert_eq!(r.index_from(4), 1);
544    /// assert_eq!(r.index_from(-1), 2);
545    /// ```
546    #[must_use]
547    pub fn index_from(self, i: isize) -> usize {
548        let n = self.ring.len();
549        assert!(n > 0, "cannot normalize against an empty ring");
550        let pos = i.rem_euclid(n as isize) as usize;
551        self.map_index(pos)
552    }
553
554    // -----------------------------------------------------------------------
555    // Comparison — alloc-free
556    // -----------------------------------------------------------------------
557
558    /// Returns `true` if `other` is some rotation of this view.
559    #[must_use]
560    pub fn is_rotation_of(self, other: &[T]) -> bool
561    where
562        T: PartialEq,
563    {
564        if self.len() != other.len() {
565            return false;
566        }
567        if self.ring.is_empty() {
568            return true;
569        }
570        self.rotations().any(|rot| rot.iter().eq(other.iter()))
571    }
572
573    /// Returns `true` if `other` equals this view or its reflection at
574    /// position 0.
575    #[must_use]
576    pub fn is_reflection_of(self, other: &[T]) -> bool
577    where
578        T: PartialEq,
579    {
580        if self.len() != other.len() {
581            return false;
582        }
583        self.iter().eq(other.iter()) || self.reflect_at(0).iter().eq(other.iter())
584    }
585
586    /// Returns `true` if `other` equals this view or its reverse.
587    #[must_use]
588    pub fn is_reversion_of(self, other: &[T]) -> bool
589    where
590        T: PartialEq,
591    {
592        if self.len() != other.len() {
593            return false;
594        }
595        self.iter().eq(other.iter()) || self.reflect_at(-1).iter().eq(other.iter())
596    }
597
598    /// Returns `true` if `other` is any rotation of this view or of its
599    /// reflection at position 0.
600    #[must_use]
601    pub fn is_rotation_or_reflection_of(self, other: &[T]) -> bool
602    where
603        T: PartialEq,
604    {
605        if self.len() != other.len() {
606            return false;
607        }
608        self.rotations_and_reflections()
609            .any(|v| v.iter().eq(other.iter()))
610    }
611
612    /// Returns the starting offset at which `other` matches this view as a
613    /// rotation, or `None` if no such offset exists.
614    #[must_use]
615    pub fn rotation_offset(self, other: &[T]) -> Option<usize>
616    where
617        T: PartialEq,
618    {
619        if self.len() != other.len() {
620            return None;
621        }
622        if self.ring.is_empty() {
623            return Some(0);
624        }
625        let n = self.ring.len();
626        (0..n).find(|&i| self.start_at(i as isize).iter().eq(other.iter()))
627    }
628
629    /// Counts positions where this view and `other` differ.
630    ///
631    /// # Panics
632    ///
633    /// Panics if the lengths differ.
634    #[must_use]
635    pub fn hamming_distance(self, other: &[T]) -> usize
636    where
637        T: PartialEq,
638    {
639        assert_eq!(self.len(), other.len(), "sequences must have the same size");
640        self.iter()
641            .zip(other.iter())
642            .filter(|(a, b)| a != b)
643            .count()
644    }
645
646    /// Minimum Hamming distance over all rotations of this view against
647    /// `other`.
648    ///
649    /// # Panics
650    ///
651    /// Panics if the lengths differ.
652    #[must_use]
653    pub fn min_rotational_hamming_distance(self, other: &[T]) -> usize
654    where
655        T: PartialEq,
656    {
657        assert_eq!(self.len(), other.len(), "sequences must have the same size");
658        let n = self.ring.len();
659        if n == 0 {
660            return 0;
661        }
662        let mut best = usize::MAX;
663        for rot in self.rotations() {
664            let count = rot.iter().zip(other.iter()).filter(|(a, b)| a != b).count();
665            if count < best {
666                best = count;
667            }
668            if best == 0 {
669                break;
670            }
671        }
672        best
673    }
674
675    /// Returns `true` if `needle` appears as a contiguous (possibly
676    /// wrapping) substring of this view.
677    ///
678    /// An empty `needle` is always contained.
679    #[must_use]
680    pub fn contains_slice(self, needle: &[T]) -> bool
681    where
682        T: PartialEq,
683    {
684        if needle.is_empty() {
685            return true;
686        }
687        if self.ring.is_empty() {
688            return false;
689        }
690        let n = self.ring.len();
691        let m = needle.len();
692        (0..n).any(|start| (0..m).all(|j| *self.apply((start + j) as isize) == needle[j]))
693    }
694
695    /// Returns the starting circular index at which `needle` appears, or
696    /// `None` if it does not. Searches forward from `from`.
697    #[must_use]
698    pub fn index_of_slice(self, needle: &[T], from: isize) -> Option<usize>
699    where
700        T: PartialEq,
701    {
702        if needle.is_empty() {
703            return if self.ring.is_empty() {
704                Some(0)
705            } else {
706                Some(self.index_from(from))
707            };
708        }
709        if self.ring.is_empty() {
710            return None;
711        }
712        let n = self.ring.len();
713        let m = needle.len();
714        let start = self.index_from(from);
715        (0..n)
716            .map(|k| (start + k) % n)
717            .find(|&i| (0..m).all(|j| *self.apply((i + j) as isize) == needle[j]))
718    }
719
720    // -----------------------------------------------------------------------
721    // Symmetry — alloc-free counts
722    // -----------------------------------------------------------------------
723
724    /// Returns the rotational symmetry order: `n / smallest_period`, where
725    /// the smallest period is the smallest divisor `d` of `n` such that
726    /// `apply(i) == apply(i + d)` for all `i`. Empty and single-element
727    /// rings have rotational symmetry 1.
728    #[must_use]
729    pub fn rotational_symmetry(self) -> usize
730    where
731        T: PartialEq,
732    {
733        let n = self.ring.len();
734        if n < 2 {
735            return 1;
736        }
737        let smallest = (1..=n).find(|&d| {
738            n % d == 0
739                && (0..n - d).all(|i| *self.apply(i as isize) == *self.apply((i + d) as isize))
740        });
741        n / smallest.unwrap_or(n)
742    }
743
744    /// Returns an iterator over the shifts at which this view equals its
745    /// own reverse rotated by that shift — i.e., the count of reflectional
746    /// symmetry axes (without allocating the indices themselves).
747    ///
748    /// Use [`symmetry_indices`](Self::symmetry_indices) (alloc-gated) if
749    /// you need the actual indices.
750    #[must_use]
751    pub fn symmetry(self) -> usize
752    where
753        T: PartialEq,
754    {
755        let n = self.ring.len();
756        if n == 0 {
757            return 0;
758        }
759        let reversed = self.reflect_at(-1);
760        (0..n)
761            .filter(|&shift| {
762                (0..n).all(|i| *self.apply(i as isize) == *reversed.apply((i + shift) as isize))
763            })
764            .count()
765    }
766
767    /// Returns an iterator yielding `ceil(len / size)` non-overlapping
768    /// circular chunks of length `size` as [`CircularIter`]s.
769    ///
770    /// When `size` does not divide `len`, the final chunk wraps across
771    /// the seam so every chunk has exactly `size` elements. An empty ring
772    /// yields no chunks.
773    ///
774    /// # Panics
775    ///
776    /// Panics if `size` is zero.
777    ///
778    /// # Examples
779    ///
780    /// ```
781    /// use ring_seq::AsCircular;
782    ///
783    /// let r = [1, 2, 3, 4, 5].circular();
784    /// let chunks: Vec<Vec<i32>> = r
785    ///     .chunks(2)
786    ///     .map(|c| c.copied().collect())
787    ///     .collect();
788    /// // ceil(5/2) = 3 chunks; the last one wraps
789    /// assert_eq!(chunks, vec![vec![1, 2], vec![3, 4], vec![5, 1]]);
790    /// ```
791    #[must_use]
792    pub fn chunks(self, size: usize) -> Windows<'a, T> {
793        assert!(size > 0, "chunk size must be positive");
794        let n = self.ring.len();
795        let total = if n == 0 { 0 } else { (n + size - 1) / size };
796        Windows {
797            base: self,
798            size,
799            step: size,
800            index: 0,
801            total,
802        }
803    }
804}
805
806// ---------------------------------------------------------------------------
807// CircularIter
808// ---------------------------------------------------------------------------
809
810/// Iterator over the elements of a [`Circular`] view.
811///
812/// Created by [`Circular::iter`]. Yields `&'a T` and is
813/// [`ExactSizeIterator`] + [`FusedIterator`].
814#[derive(Debug)]
815pub struct CircularIter<'a, T> {
816    view: Circular<'a, T>,
817    pos: usize,
818    remaining: usize,
819}
820
821impl<T> Clone for CircularIter<'_, T> {
822    fn clone(&self) -> Self {
823        CircularIter {
824            view: self.view,
825            pos: self.pos,
826            remaining: self.remaining,
827        }
828    }
829}
830
831impl<'a, T> Iterator for CircularIter<'a, T> {
832    type Item = &'a T;
833
834    #[inline]
835    fn next(&mut self) -> Option<&'a T> {
836        if self.remaining == 0 {
837            return None;
838        }
839        let item = &self.view.ring[self.view.map_index(self.pos)];
840        self.pos += 1;
841        self.remaining -= 1;
842        Some(item)
843    }
844
845    #[inline]
846    fn size_hint(&self) -> (usize, Option<usize>) {
847        (self.remaining, Some(self.remaining))
848    }
849}
850
851impl<T> ExactSizeIterator for CircularIter<'_, T> {}
852impl<T> FusedIterator for CircularIter<'_, T> {}
853
854// ---------------------------------------------------------------------------
855// Enumerate — yields (&T, ring_index)
856// ---------------------------------------------------------------------------
857
858/// Iterator over `(element, ring_index)` pairs of a [`Circular`] view.
859///
860/// Created by [`Circular::enumerate`]. Yields `(&'a T, usize)` where the
861/// `usize` is the position in the underlying ring (already wrapped to
862/// `[0, len)`).
863#[derive(Debug)]
864pub struct Enumerate<'a, T> {
865    view: Circular<'a, T>,
866    pos: usize,
867    remaining: usize,
868}
869
870impl<T> Clone for Enumerate<'_, T> {
871    fn clone(&self) -> Self {
872        Enumerate {
873            view: self.view,
874            pos: self.pos,
875            remaining: self.remaining,
876        }
877    }
878}
879
880impl<'a, T> Iterator for Enumerate<'a, T> {
881    type Item = (&'a T, usize);
882
883    #[inline]
884    fn next(&mut self) -> Option<Self::Item> {
885        if self.remaining == 0 {
886            return None;
887        }
888        let idx = self.view.map_index(self.pos);
889        let item = &self.view.ring[idx];
890        self.pos += 1;
891        self.remaining -= 1;
892        Some((item, idx))
893    }
894
895    #[inline]
896    fn size_hint(&self) -> (usize, Option<usize>) {
897        (self.remaining, Some(self.remaining))
898    }
899}
900
901impl<T> ExactSizeIterator for Enumerate<'_, T> {}
902impl<T> FusedIterator for Enumerate<'_, T> {}
903
904// ---------------------------------------------------------------------------
905// Rotations — yields each rotation as a Circular view
906// ---------------------------------------------------------------------------
907
908/// Iterator over the rotations of a [`Circular`] view.
909///
910/// Created by [`Circular::rotations`]. Yields one [`Circular`] per
911/// starting position; preserves the `reflected` bit of the source view.
912#[derive(Debug)]
913pub struct Rotations<'a, T> {
914    base: Circular<'a, T>,
915    index: usize,
916    total: usize,
917}
918
919impl<T> Clone for Rotations<'_, T> {
920    fn clone(&self) -> Self {
921        Rotations {
922            base: self.base,
923            index: self.index,
924            total: self.total,
925        }
926    }
927}
928
929impl<'a, T> Iterator for Rotations<'a, T> {
930    type Item = Circular<'a, T>;
931
932    fn next(&mut self) -> Option<Self::Item> {
933        if self.index >= self.total {
934            return None;
935        }
936        let item = if self.base.ring.is_empty() {
937            self.base
938        } else {
939            self.base.start_at(self.index as isize)
940        };
941        self.index += 1;
942        Some(item)
943    }
944
945    fn size_hint(&self) -> (usize, Option<usize>) {
946        let r = self.total - self.index;
947        (r, Some(r))
948    }
949}
950
951impl<T> ExactSizeIterator for Rotations<'_, T> {}
952impl<T> FusedIterator for Rotations<'_, T> {}
953
954// ---------------------------------------------------------------------------
955// Reflections — yields the view and its reflection at position 0
956// ---------------------------------------------------------------------------
957
958/// Iterator yielding the source [`Circular`] and its [`reflect_at(0)`].
959///
960/// Created by [`Circular::reflections`].
961///
962/// [`reflect_at(0)`]: Circular::reflect_at
963#[derive(Debug)]
964pub struct Reflections<'a, T> {
965    base: Circular<'a, T>,
966    state: u8,
967}
968
969impl<T> Clone for Reflections<'_, T> {
970    fn clone(&self) -> Self {
971        Reflections {
972            base: self.base,
973            state: self.state,
974        }
975    }
976}
977
978impl<'a, T> Iterator for Reflections<'a, T> {
979    type Item = Circular<'a, T>;
980
981    fn next(&mut self) -> Option<Self::Item> {
982        match self.state {
983            0 => {
984                self.state = if self.base.ring.is_empty() { 2 } else { 1 };
985                Some(self.base)
986            }
987            1 => {
988                self.state = 2;
989                Some(self.base.reflect_at(0))
990            }
991            _ => None,
992        }
993    }
994
995    fn size_hint(&self) -> (usize, Option<usize>) {
996        let r = match self.state {
997            0 => {
998                if self.base.ring.is_empty() {
999                    1
1000                } else {
1001                    2
1002                }
1003            }
1004            1 => 1,
1005            _ => 0,
1006        };
1007        (r, Some(r))
1008    }
1009}
1010
1011impl<T> ExactSizeIterator for Reflections<'_, T> {}
1012impl<T> FusedIterator for Reflections<'_, T> {}
1013
1014// ---------------------------------------------------------------------------
1015// Reversions — yields the view and its reverse
1016// ---------------------------------------------------------------------------
1017
1018/// Iterator yielding the source [`Circular`] and its reverse
1019/// (`reflect_at(-1)`).
1020///
1021/// Created by [`Circular::reversions`].
1022#[derive(Debug)]
1023pub struct Reversions<'a, T> {
1024    base: Circular<'a, T>,
1025    state: u8,
1026}
1027
1028impl<T> Clone for Reversions<'_, T> {
1029    fn clone(&self) -> Self {
1030        Reversions {
1031            base: self.base,
1032            state: self.state,
1033        }
1034    }
1035}
1036
1037impl<'a, T> Iterator for Reversions<'a, T> {
1038    type Item = Circular<'a, T>;
1039
1040    fn next(&mut self) -> Option<Self::Item> {
1041        match self.state {
1042            0 => {
1043                self.state = if self.base.ring.is_empty() { 2 } else { 1 };
1044                Some(self.base)
1045            }
1046            1 => {
1047                self.state = 2;
1048                Some(self.base.reflect_at(-1))
1049            }
1050            _ => None,
1051        }
1052    }
1053
1054    fn size_hint(&self) -> (usize, Option<usize>) {
1055        let r = match self.state {
1056            0 => {
1057                if self.base.ring.is_empty() {
1058                    1
1059                } else {
1060                    2
1061                }
1062            }
1063            1 => 1,
1064            _ => 0,
1065        };
1066        (r, Some(r))
1067    }
1068}
1069
1070impl<T> ExactSizeIterator for Reversions<'_, T> {}
1071impl<T> FusedIterator for Reversions<'_, T> {}
1072
1073// ---------------------------------------------------------------------------
1074// RotationsAndReflections — yields 2n views
1075// ---------------------------------------------------------------------------
1076
1077/// Iterator yielding every rotation of the source followed by every
1078/// rotation of its reflection at position 0.
1079///
1080/// Created by [`Circular::rotations_and_reflections`].
1081#[derive(Debug)]
1082pub struct RotationsAndReflections<'a, T> {
1083    base: Circular<'a, T>,
1084    reflected: Circular<'a, T>,
1085    index: usize,
1086    total: usize,
1087    n: usize,
1088}
1089
1090impl<T> Clone for RotationsAndReflections<'_, T> {
1091    fn clone(&self) -> Self {
1092        RotationsAndReflections {
1093            base: self.base,
1094            reflected: self.reflected,
1095            index: self.index,
1096            total: self.total,
1097            n: self.n,
1098        }
1099    }
1100}
1101
1102impl<'a, T> Iterator for RotationsAndReflections<'a, T> {
1103    type Item = Circular<'a, T>;
1104
1105    fn next(&mut self) -> Option<Self::Item> {
1106        if self.index >= self.total {
1107            return None;
1108        }
1109        if self.n == 0 {
1110            self.index += 1;
1111            return Some(self.base);
1112        }
1113        let item = if self.index < self.n {
1114            self.base.start_at(self.index as isize)
1115        } else {
1116            self.reflected.start_at((self.index - self.n) as isize)
1117        };
1118        self.index += 1;
1119        Some(item)
1120    }
1121
1122    fn size_hint(&self) -> (usize, Option<usize>) {
1123        let r = self.total - self.index;
1124        (r, Some(r))
1125    }
1126}
1127
1128impl<T> ExactSizeIterator for RotationsAndReflections<'_, T> {}
1129impl<T> FusedIterator for RotationsAndReflections<'_, T> {}
1130
1131// ---------------------------------------------------------------------------
1132// Windows / Chunks — yield CircularIter<'a, T>
1133// ---------------------------------------------------------------------------
1134
1135/// Iterator yielding circular windows (or chunks) of a [`Circular`] view
1136/// as [`CircularIter`]s.
1137///
1138/// Created by [`Circular::windows`] (step 1) and [`Circular::chunks`]
1139/// (step equal to the chunk size).
1140#[derive(Debug)]
1141pub struct Windows<'a, T> {
1142    base: Circular<'a, T>,
1143    size: usize,
1144    step: usize,
1145    index: usize,
1146    total: usize,
1147}
1148
1149impl<T> Clone for Windows<'_, T> {
1150    fn clone(&self) -> Self {
1151        Windows {
1152            base: self.base,
1153            size: self.size,
1154            step: self.step,
1155            index: self.index,
1156            total: self.total,
1157        }
1158    }
1159}
1160
1161impl<'a, T> Iterator for Windows<'a, T> {
1162    type Item = CircularIter<'a, T>;
1163
1164    fn next(&mut self) -> Option<Self::Item> {
1165        if self.index >= self.total {
1166            return None;
1167        }
1168        let start = (self.index * self.step) as isize;
1169        let view = self.base.start_at(start);
1170        let iter = CircularIter {
1171            view,
1172            pos: 0,
1173            remaining: self.size,
1174        };
1175        self.index += 1;
1176        Some(iter)
1177    }
1178
1179    fn size_hint(&self) -> (usize, Option<usize>) {
1180        let r = self.total - self.index;
1181        (r, Some(r))
1182    }
1183}
1184
1185impl<T> ExactSizeIterator for Windows<'_, T> {}
1186impl<T> FusedIterator for Windows<'_, T> {}
1187
1188// ---------------------------------------------------------------------------
1189// AsCircular
1190// ---------------------------------------------------------------------------
1191
1192/// Extension trait providing [`circular`](AsCircular::circular) on slices.
1193///
1194/// Implemented for `[T]`; reaches `Vec<T>`, arrays, and `Box<[T]>` via
1195/// deref coercion.
1196///
1197/// # Examples
1198///
1199/// ```
1200/// use ring_seq::AsCircular;
1201///
1202/// let v = vec![1, 2, 3];
1203/// let r = v.circular();
1204/// assert_eq!(*r.apply(5), 3);
1205/// ```
1206pub trait AsCircular<T> {
1207    /// Returns a [`Circular`] view of this slice.
1208    fn circular(&self) -> Circular<'_, T>;
1209}
1210
1211impl<T> AsCircular<T> for [T] {
1212    #[inline]
1213    fn circular(&self) -> Circular<'_, T> {
1214        Circular::new(self)
1215    }
1216}
1217
1218// ---------------------------------------------------------------------------
1219// Alloc-gated section: methods returning owned collections, plus Booth's
1220// algorithm for canonical-index search.
1221// ---------------------------------------------------------------------------
1222
1223#[cfg(feature = "alloc")]
1224impl<'a, T> Circular<'a, T> {
1225    /// Materializes this view as a `Vec<T>`. Requires `T: Clone`.
1226    #[must_use]
1227    pub fn to_vec(self) -> Vec<T>
1228    where
1229        T: Clone,
1230    {
1231        self.iter().cloned().collect()
1232    }
1233
1234    /// Returns the starting offset of the lexicographically smallest
1235    /// rotation of this view (Booth's O(n) algorithm).
1236    ///
1237    /// Single-element and empty views return `0`.
1238    #[must_use]
1239    pub fn canonical_index(self) -> usize
1240    where
1241        T: Ord,
1242    {
1243        if self.ring.len() <= 1 {
1244            0
1245        } else {
1246            booth_least_rotation(self)
1247        }
1248    }
1249
1250    /// Returns the canonical (lexicographically smallest) rotation of
1251    /// this view as a `Vec<T>`. Requires `T: Clone + Ord`.
1252    #[must_use]
1253    pub fn canonical(self) -> Vec<T>
1254    where
1255        T: Clone + Ord,
1256    {
1257        let idx = self.canonical_index();
1258        self.start_at(idx as isize).to_vec()
1259    }
1260
1261    /// Returns the bracelet form: the lexicographically smallest among
1262    /// `canonical()` and `reflect_at(0).canonical()`. Requires
1263    /// `T: Clone + Ord`.
1264    #[must_use]
1265    pub fn bracelet(self) -> Vec<T>
1266    where
1267        T: Clone + Ord,
1268    {
1269        let a = self.canonical();
1270        let b = self.reflect_at(0).canonical();
1271        if a <= b {
1272            a
1273        } else {
1274            b
1275        }
1276    }
1277
1278    /// Returns the shifts `k` in `[0, n)` for which this view equals its
1279    /// own reverse rotated by `k`. Each shift corresponds to one axis of
1280    /// reflectional symmetry.
1281    #[must_use]
1282    pub fn symmetry_indices(self) -> Vec<usize>
1283    where
1284        T: PartialEq,
1285    {
1286        let n = self.ring.len();
1287        if n == 0 {
1288            return Vec::new();
1289        }
1290        let reversed = self.reflect_at(-1);
1291        (0..n)
1292            .filter(|&shift| {
1293                (0..n).all(|i| *self.apply(i as isize) == *reversed.apply((i + shift) as isize))
1294            })
1295            .collect()
1296    }
1297
1298    /// Returns the axes of reflectional symmetry as
1299    /// `(AxisLocation, AxisLocation)` pairs (each axis hits the ring in
1300    /// two locations).
1301    #[must_use]
1302    pub fn reflectional_symmetry_axes(self) -> Vec<(AxisLocation, AxisLocation)>
1303    where
1304        T: PartialEq,
1305    {
1306        let n = self.ring.len();
1307        if n == 0 {
1308            return Vec::new();
1309        }
1310
1311        #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
1312        self.symmetry_indices()
1313            .into_iter()
1314            .map(|shift| {
1315                let raw_k = (n as isize - 1 - shift as isize) % n as isize;
1316                let k = if raw_k < 0 {
1317                    (raw_k + n as isize) as usize
1318                } else {
1319                    raw_k as usize
1320                };
1321
1322                if n % 2 != 0 {
1323                    let v = (k * (n + 1) / 2) % n;
1324                    let opp = (v + n / 2) % n;
1325                    (AxisLocation::Vertex(v), AxisLocation::edge(opp, n))
1326                } else if k % 2 == 0 {
1327                    let v1 = k / 2;
1328                    let v2 = (v1 + n / 2) % n;
1329                    (AxisLocation::Vertex(v1), AxisLocation::Vertex(v2))
1330                } else {
1331                    let e1 = (k - 1) / 2;
1332                    let e2 = (e1 + n / 2) % n;
1333                    (AxisLocation::edge(e1, n), AxisLocation::edge(e2, n))
1334                }
1335            })
1336            .collect()
1337    }
1338}
1339
1340/// Booth's O(n) algorithm for finding the starting offset of the
1341/// lexicographically smallest rotation of a `Circular` view.
1342#[cfg(feature = "alloc")]
1343#[allow(
1344    clippy::cast_sign_loss,
1345    clippy::cast_possible_wrap,
1346    clippy::many_single_char_names
1347)]
1348fn booth_least_rotation<T: Ord>(c: Circular<'_, T>) -> usize {
1349    let n = c.ring.len();
1350    let len = 2 * n;
1351    let mut f: Vec<isize> = alloc::vec![-1; len];
1352    let mut k: usize = 0;
1353    let at = |idx: usize| &c.ring[c.map_index(idx % n)];
1354
1355    for j in 1..len {
1356        let sj = at(j);
1357        let mut i = f[j - k - 1];
1358        while i != -1 && at(k + i as usize + 1) != sj {
1359            if sj < at(k + i as usize + 1) {
1360                k = j - i as usize - 1;
1361            }
1362            i = f[i as usize];
1363        }
1364        if i == -1 && at(k) != sj {
1365            if sj < at(k) {
1366                k = j;
1367            }
1368            f[j - k] = -1;
1369        } else {
1370            f[j - k] = i + 1;
1371        }
1372    }
1373    k
1374}
1375
1376// ---------------------------------------------------------------------------
1377// Tests — pure core, no alloc needed
1378// ---------------------------------------------------------------------------
1379
1380#[cfg(test)]
1381mod tests {
1382    use super::*;
1383
1384    #[test]
1385    fn empty_ring_is_empty() {
1386        let empty: [i32; 0] = [];
1387        let c = empty.circular();
1388        assert_eq!(c.len(), 0);
1389        assert!(c.is_empty());
1390        assert_eq!(c.iter().count(), 0);
1391    }
1392
1393    #[test]
1394    fn len_and_is_empty() {
1395        let r = [1, 2, 3].circular();
1396        assert_eq!(r.len(), 3);
1397        assert!(!r.is_empty());
1398    }
1399
1400    #[test]
1401    fn apply_positive() {
1402        let r = [10, 20, 30].circular();
1403        assert_eq!(*r.apply(0), 10);
1404        assert_eq!(*r.apply(1), 20);
1405        assert_eq!(*r.apply(2), 30);
1406    }
1407
1408    #[test]
1409    fn apply_wraps_forward() {
1410        let r = [10, 20, 30].circular();
1411        assert_eq!(*r.apply(3), 10);
1412        assert_eq!(*r.apply(7), 20);
1413    }
1414
1415    #[test]
1416    fn apply_wraps_backward() {
1417        let r = [10, 20, 30].circular();
1418        assert_eq!(*r.apply(-1), 30);
1419        assert_eq!(*r.apply(-3), 10);
1420        assert_eq!(*r.apply(-4), 30);
1421    }
1422
1423    #[test]
1424    #[should_panic]
1425    fn apply_panics_on_empty() {
1426        let empty: [i32; 0] = [];
1427        let _ = empty.circular().apply(0);
1428    }
1429
1430    #[test]
1431    fn iter_yields_elements_in_order() {
1432        let r = [1, 2, 3].circular();
1433        let mut it = r.iter();
1434        assert_eq!(it.next(), Some(&1));
1435        assert_eq!(it.next(), Some(&2));
1436        assert_eq!(it.next(), Some(&3));
1437        assert_eq!(it.next(), None);
1438    }
1439
1440    #[test]
1441    fn iter_size_hint_and_exact_size() {
1442        let r = [1, 2, 3, 4].circular();
1443        let mut it = r.iter();
1444        assert_eq!(it.len(), 4);
1445        assert_eq!(it.size_hint(), (4, Some(4)));
1446        it.next();
1447        assert_eq!(it.len(), 3);
1448    }
1449
1450    #[test]
1451    fn iter_is_fused() {
1452        let r = [1, 2].circular();
1453        let mut it = r.iter();
1454        it.next();
1455        it.next();
1456        assert_eq!(it.next(), None);
1457        assert_eq!(it.next(), None); // stays None
1458    }
1459
1460    #[test]
1461    fn copy_semantics() {
1462        let r = [1, 2, 3].circular();
1463        let r2 = r; // copy
1464        assert_eq!(r.len(), 3);
1465        assert_eq!(r2.len(), 3);
1466    }
1467
1468    // T does not need to be Clone for Circular to be Copy.
1469    #[test]
1470    fn copy_works_without_t_clone() {
1471        #[allow(dead_code)]
1472        struct NoClone(i32);
1473        let arr = [NoClone(1), NoClone(2)];
1474        let r = arr.circular();
1475        let r2 = r; // would fail to compile if Copy carried a T: Clone bound
1476        assert_eq!(r.len(), 2);
1477        assert_eq!(r2.len(), 2);
1478    }
1479
1480    // ── Lazy view operations ────────────────────────────────────────────
1481
1482    fn into_array<const N: usize>(c: Circular<'_, i32>) -> [i32; N] {
1483        let mut out = [0; N];
1484        for (slot, &x) in out.iter_mut().zip(c.iter()) {
1485            *slot = x;
1486        }
1487        out
1488    }
1489
1490    #[test]
1491    fn start_at_basic() {
1492        let r = [10, 20, 30, 40].circular();
1493        assert_eq!(into_array::<4>(r.start_at(0)), [10, 20, 30, 40]);
1494        assert_eq!(into_array::<4>(r.start_at(2)), [30, 40, 10, 20]);
1495        assert_eq!(into_array::<4>(r.start_at(-1)), [40, 10, 20, 30]);
1496        assert_eq!(into_array::<4>(r.start_at(5)), [20, 30, 40, 10]);
1497    }
1498
1499    #[test]
1500    fn start_at_empty() {
1501        let empty: [i32; 0] = [];
1502        assert_eq!(empty.circular().start_at(3).len(), 0);
1503    }
1504
1505    #[test]
1506    fn rotate_right_basic() {
1507        let r = [1, 2, 3, 4].circular();
1508        assert_eq!(into_array::<4>(r.rotate_right(0)), [1, 2, 3, 4]);
1509        assert_eq!(into_array::<4>(r.rotate_right(1)), [4, 1, 2, 3]);
1510        assert_eq!(into_array::<4>(r.rotate_right(-1)), [2, 3, 4, 1]);
1511        assert_eq!(into_array::<4>(r.rotate_right(5)), [4, 1, 2, 3]);
1512    }
1513
1514    #[test]
1515    fn rotate_left_basic() {
1516        let r = [1, 2, 3, 4].circular();
1517        assert_eq!(into_array::<4>(r.rotate_left(1)), [2, 3, 4, 1]);
1518        assert_eq!(into_array::<4>(r.rotate_left(-1)), [4, 1, 2, 3]);
1519    }
1520
1521    #[test]
1522    fn rotate_right_then_left_is_identity() {
1523        let ring = [1, 2, 3, 4, 5, 6, 7];
1524        let r = ring.circular();
1525        for step in -10..=10 {
1526            assert_eq!(
1527                into_array::<7>(r.rotate_right(step).rotate_left(step)),
1528                ring,
1529                "failed for step {}",
1530                step,
1531            );
1532        }
1533    }
1534
1535    #[test]
1536    fn reflect_at_basic() {
1537        let r = [10, 20, 30, 40].circular();
1538        // reflect_at(0): pos 0 = ring[0]=10, pos 1 = ring[3]=40, pos 2 = ring[2]=30, pos 3 = ring[1]=20
1539        assert_eq!(into_array::<4>(r.reflect_at(0)), [10, 40, 30, 20]);
1540        // reflect_at(2): pos 0 = ring[2]=30, pos 1 = ring[1]=20, pos 2 = ring[0]=10, pos 3 = ring[3]=40
1541        assert_eq!(into_array::<4>(r.reflect_at(2)), [30, 20, 10, 40]);
1542    }
1543
1544    #[test]
1545    fn reflect_at_is_involution() {
1546        let ring = [1, 2, 3, 4, 5, 6, 7];
1547        let r = ring.circular();
1548        for i in -3..10 {
1549            assert_eq!(
1550                into_array::<7>(r.reflect_at(i).reflect_at(i)),
1551                ring,
1552                "failed for i {}",
1553                i
1554            );
1555        }
1556    }
1557
1558    #[test]
1559    fn rotate_then_reflect_commutes() {
1560        // rotate_right(k).reflect_at(0) == reflect_at(0).rotate_left(k)
1561        let ring = [1, 2, 3, 4, 5];
1562        let r = ring.circular();
1563        for k in -7..=7 {
1564            assert_eq!(
1565                into_array::<5>(r.rotate_right(k).reflect_at(0)),
1566                into_array::<5>(r.reflect_at(0).rotate_left(k)),
1567                "failed for k {}",
1568                k,
1569            );
1570        }
1571    }
1572
1573    // ── slice ──────────────────────────────────────────────────────────
1574
1575    #[test]
1576    fn slice_within_one_lap() {
1577        let r = [0, 1, 2, 3, 4].circular();
1578        let v: [i32; 3] = {
1579            let mut out = [0; 3];
1580            for (s, &x) in out.iter_mut().zip(r.slice(1, 4)) {
1581                *s = x;
1582            }
1583            out
1584        };
1585        assert_eq!(v, [1, 2, 3]);
1586    }
1587
1588    #[test]
1589    fn slice_wraps() {
1590        let r = [0, 1, 2, 3, 4].circular();
1591        let mut out = [0; 5];
1592        for (s, &x) in out.iter_mut().zip(r.slice(2, 7)) {
1593            *s = x;
1594        }
1595        assert_eq!(out, [2, 3, 4, 0, 1]);
1596    }
1597
1598    #[test]
1599    fn slice_negative_from() {
1600        let r = [0, 1, 2, 3, 4].circular();
1601        let mut out = [0; 3];
1602        for (s, &x) in out.iter_mut().zip(r.slice(-2, 1)) {
1603            *s = x;
1604        }
1605        assert_eq!(out, [3, 4, 0]);
1606    }
1607
1608    #[test]
1609    fn slice_empty_range() {
1610        let r = [0, 1, 2, 3, 4].circular();
1611        assert_eq!(r.slice(3, 3).count(), 0);
1612        assert_eq!(r.slice(5, 2).count(), 0);
1613    }
1614
1615    #[test]
1616    fn slice_empty_ring() {
1617        let empty: [i32; 0] = [];
1618        assert_eq!(empty.circular().slice(0, 4).count(), 0);
1619    }
1620
1621    // ── take_while / drop_while ────────────────────────────────────────
1622
1623    #[test]
1624    fn take_while_basic() {
1625        let r = [1, 2, 3, 4, 5].circular();
1626        let mut out = [0; 3];
1627        for (s, &x) in out.iter_mut().zip(r.take_while(|&x| x < 4, 0)) {
1628            *s = x;
1629        }
1630        assert_eq!(out, [1, 2, 3]);
1631    }
1632
1633    #[test]
1634    fn take_while_stops_after_one_lap() {
1635        // If the predicate is always true, take_while still terminates at n elements.
1636        let r = [1, 1, 1].circular();
1637        assert_eq!(r.take_while(|_| true, 0).count(), 3);
1638    }
1639
1640    #[test]
1641    fn drop_while_basic() {
1642        let r = [1, 2, 3, 4, 5].circular();
1643        let mut out = [0; 2];
1644        for (s, &x) in out.iter_mut().zip(r.drop_while(|&x| x < 4, 0)) {
1645            *s = x;
1646        }
1647        assert_eq!(out, [4, 5]);
1648    }
1649
1650    #[test]
1651    fn drop_while_all_dropped() {
1652        let r = [1, 2, 3].circular();
1653        assert_eq!(r.drop_while(|_| true, 0).count(), 0);
1654    }
1655
1656    // ── enumerate ──────────────────────────────────────────────────────
1657
1658    #[test]
1659    fn enumerate_yields_ring_indices() {
1660        let r = [10, 20, 30].circular();
1661        let mut out: [(i32, usize); 3] = [(0, 0); 3];
1662        for (s, (&x, i)) in out.iter_mut().zip(r.enumerate(1)) {
1663            *s = (x, i);
1664        }
1665        assert_eq!(out, [(20, 1), (30, 2), (10, 0)]);
1666    }
1667
1668    #[test]
1669    fn enumerate_empty() {
1670        let empty: [i32; 0] = [];
1671        assert_eq!(empty.circular().enumerate(0).count(), 0);
1672    }
1673
1674    #[test]
1675    fn enumerate_is_exact_size() {
1676        let r = [10, 20, 30, 40].circular();
1677        let it = r.enumerate(2);
1678        assert_eq!(it.len(), 4);
1679    }
1680
1681    // ── Iterators of views ─────────────────────────────────────────────
1682
1683    #[test]
1684    fn rotations_yields_n_views() {
1685        let ring = [1, 2, 3, 4];
1686        let r = ring.circular();
1687        let firsts: [i32; 4] = {
1688            let mut out = [0; 4];
1689            for (s, v) in out.iter_mut().zip(r.rotations()) {
1690                *s = *v.apply(0);
1691            }
1692            out
1693        };
1694        assert_eq!(firsts, [1, 2, 3, 4]);
1695    }
1696
1697    #[test]
1698    fn rotations_empty_yields_one() {
1699        let empty: [i32; 0] = [];
1700        assert_eq!(empty.circular().rotations().count(), 1);
1701    }
1702
1703    #[test]
1704    fn rotations_exact_size() {
1705        let r = [1, 2, 3, 4, 5].circular();
1706        let it = r.rotations();
1707        assert_eq!(it.len(), 5);
1708    }
1709
1710    #[test]
1711    fn rotations_preserves_reflected() {
1712        // Rotations of the reflected view should each be reflected too.
1713        let r = [1, 2, 3].circular().reflect_at(0);
1714        let firsts: [i32; 3] = {
1715            let mut out = [0; 3];
1716            for (s, v) in out.iter_mut().zip(r.rotations()) {
1717                *s = *v.apply(0);
1718            }
1719            out
1720        };
1721        // Reflected of [1,2,3] at 0 = [1, 3, 2]. Its rotations start at 1, 3, 2.
1722        assert_eq!(firsts, [1, 3, 2]);
1723    }
1724
1725    #[test]
1726    fn reflections_yields_two() {
1727        let r = [1, 2, 3, 4].circular();
1728        let count = r.reflections().count();
1729        assert_eq!(count, 2);
1730    }
1731
1732    #[test]
1733    fn reflections_second_is_reflect_at_zero() {
1734        let r = [1, 2, 3, 4].circular();
1735        let mut it = r.reflections();
1736        let first = it.next().unwrap();
1737        let second = it.next().unwrap();
1738        let first_v: [i32; 4] = into_array(first);
1739        let second_v: [i32; 4] = into_array(second);
1740        assert_eq!(first_v, [1, 2, 3, 4]);
1741        assert_eq!(second_v, [1, 4, 3, 2]);
1742    }
1743
1744    #[test]
1745    fn reflections_empty_yields_one() {
1746        let empty: [i32; 0] = [];
1747        assert_eq!(empty.circular().reflections().count(), 1);
1748    }
1749
1750    #[test]
1751    fn reversions_yields_two() {
1752        let r = [1, 2, 3].circular();
1753        assert_eq!(r.reversions().count(), 2);
1754    }
1755
1756    #[test]
1757    fn reversions_second_is_reverse() {
1758        let r = [1, 2, 3, 4].circular();
1759        let mut it = r.reversions();
1760        let first = it.next().unwrap();
1761        let second = it.next().unwrap();
1762        let first_v: [i32; 4] = into_array(first);
1763        let second_v: [i32; 4] = into_array(second);
1764        assert_eq!(first_v, [1, 2, 3, 4]);
1765        assert_eq!(second_v, [4, 3, 2, 1]);
1766    }
1767
1768    #[test]
1769    fn rotations_and_reflections_yields_2n() {
1770        let r = [1, 2, 3, 4].circular();
1771        assert_eq!(r.rotations_and_reflections().count(), 8);
1772    }
1773
1774    #[test]
1775    fn rotations_and_reflections_distinct_views() {
1776        // Distinct shapes for an aperiodic ring.
1777        let r = [1, 2, 3, 4].circular();
1778        let all: [[i32; 4]; 8] = {
1779            let mut out = [[0; 4]; 8];
1780            for (s, v) in out.iter_mut().zip(r.rotations_and_reflections()) {
1781                *s = into_array(v);
1782            }
1783            out
1784        };
1785        assert_eq!(all[0], [1, 2, 3, 4]);
1786        assert_eq!(all[1], [2, 3, 4, 1]);
1787        assert_eq!(all[2], [3, 4, 1, 2]);
1788        assert_eq!(all[3], [4, 1, 2, 3]);
1789        // Reflected half: rotations of [1, 4, 3, 2] (reflect_at(0) of [1,2,3,4])
1790        assert_eq!(all[4], [1, 4, 3, 2]);
1791        assert_eq!(all[5], [4, 3, 2, 1]);
1792        assert_eq!(all[6], [3, 2, 1, 4]);
1793        assert_eq!(all[7], [2, 1, 4, 3]);
1794    }
1795
1796    #[test]
1797    fn rotations_and_reflections_empty_yields_one() {
1798        let empty: [i32; 0] = [];
1799        assert_eq!(empty.circular().rotations_and_reflections().count(), 1);
1800    }
1801
1802    // ── Windows / Chunks ───────────────────────────────────────────────
1803
1804    fn collect_iter<const N: usize>(mut iter: CircularIter<'_, i32>) -> [i32; N] {
1805        let mut out = [0; N];
1806        for s in out.iter_mut() {
1807            *s = *iter.next().unwrap();
1808        }
1809        out
1810    }
1811
1812    #[test]
1813    fn windows_basic() {
1814        let r = [1, 2, 3, 4].circular();
1815        let mut it = r.windows(2);
1816        assert_eq!(collect_iter::<2>(it.next().unwrap()), [1, 2]);
1817        assert_eq!(collect_iter::<2>(it.next().unwrap()), [2, 3]);
1818        assert_eq!(collect_iter::<2>(it.next().unwrap()), [3, 4]);
1819        assert_eq!(collect_iter::<2>(it.next().unwrap()), [4, 1]); // wraps
1820        assert!(it.next().is_none());
1821    }
1822
1823    #[test]
1824    fn windows_count_equals_ring_len() {
1825        let r = [1, 2, 3, 4, 5].circular();
1826        assert_eq!(r.windows(2).count(), 5);
1827        assert_eq!(r.windows(3).count(), 5);
1828        assert_eq!(r.windows(5).count(), 5);
1829    }
1830
1831    #[test]
1832    fn windows_size_greater_than_ring() {
1833        let r = [1, 2, 3].circular();
1834        // Each window of size 5 wraps multiple times.
1835        let mut it = r.windows(5);
1836        assert_eq!(collect_iter::<5>(it.next().unwrap()), [1, 2, 3, 1, 2]);
1837        assert_eq!(collect_iter::<5>(it.next().unwrap()), [2, 3, 1, 2, 3]);
1838        assert_eq!(collect_iter::<5>(it.next().unwrap()), [3, 1, 2, 3, 1]);
1839        assert!(it.next().is_none());
1840    }
1841
1842    #[test]
1843    fn windows_empty_ring() {
1844        let empty: [i32; 0] = [];
1845        assert_eq!(empty.circular().windows(3).count(), 0);
1846    }
1847
1848    #[test]
1849    #[should_panic]
1850    fn windows_zero_size_panics() {
1851        let _ = [1, 2, 3].circular().windows(0);
1852    }
1853
1854    #[test]
1855    fn chunks_evenly_divisible() {
1856        let r = [1, 2, 3, 4].circular();
1857        let mut it = r.chunks(2);
1858        assert_eq!(collect_iter::<2>(it.next().unwrap()), [1, 2]);
1859        assert_eq!(collect_iter::<2>(it.next().unwrap()), [3, 4]);
1860        assert!(it.next().is_none());
1861    }
1862
1863    #[test]
1864    fn chunks_wrap_at_seam() {
1865        // ceil(5/2) = 3 chunks; last one wraps
1866        let r = [1, 2, 3, 4, 5].circular();
1867        let mut it = r.chunks(2);
1868        assert_eq!(collect_iter::<2>(it.next().unwrap()), [1, 2]);
1869        assert_eq!(collect_iter::<2>(it.next().unwrap()), [3, 4]);
1870        assert_eq!(collect_iter::<2>(it.next().unwrap()), [5, 1]);
1871        assert!(it.next().is_none());
1872    }
1873
1874    #[test]
1875    fn chunks_size_one() {
1876        let r = [1, 2, 3].circular();
1877        assert_eq!(r.chunks(1).count(), 3);
1878    }
1879
1880    #[test]
1881    fn chunks_size_greater_than_ring() {
1882        let r = [1, 2, 3].circular();
1883        // ceil(3/5) = 1 chunk
1884        let mut it = r.chunks(5);
1885        assert_eq!(collect_iter::<5>(it.next().unwrap()), [1, 2, 3, 1, 2]);
1886        assert!(it.next().is_none());
1887    }
1888
1889    #[test]
1890    fn chunks_empty_ring() {
1891        let empty: [i32; 0] = [];
1892        assert_eq!(empty.circular().chunks(2).count(), 0);
1893    }
1894
1895    #[test]
1896    #[should_panic]
1897    fn chunks_zero_size_panics() {
1898        let _ = [1, 2, 3].circular().chunks(0);
1899    }
1900
1901    // ── Chained operations ─────────────────────────────────────────────
1902
1903    #[test]
1904    fn chained_rotations_first_element() {
1905        let r = [3, 1, 2].circular();
1906        let firsts: [i32; 3] = {
1907            let mut out = [0; 3];
1908            for (s, v) in out.iter_mut().zip(r.rotations()) {
1909                *s = *v.apply(0);
1910            }
1911            out
1912        };
1913        assert_eq!(firsts, [3, 1, 2]);
1914    }
1915
1916    #[test]
1917    fn chained_windows_count_satisfying() {
1918        let r = [1, 2, 3, 4, 5].circular();
1919        // Count windows whose first element is even
1920        let n = r
1921            .windows(2)
1922            .filter(|w| {
1923                let mut clone = w.clone();
1924                clone.next().map_or(false, |x| x % 2 == 0)
1925            })
1926            .count();
1927        assert_eq!(n, 2); // windows starting at 2 and 4
1928    }
1929
1930    // ── Index normalization ────────────────────────────────────────────
1931
1932    #[test]
1933    fn index_from_basic() {
1934        let r = [10, 20, 30].circular();
1935        assert_eq!(r.index_from(0), 0);
1936        assert_eq!(r.index_from(2), 2);
1937        assert_eq!(r.index_from(3), 0);
1938        assert_eq!(r.index_from(7), 1);
1939        assert_eq!(r.index_from(-1), 2);
1940        assert_eq!(r.index_from(-4), 2);
1941    }
1942
1943    #[test]
1944    #[should_panic]
1945    fn index_from_empty_panics() {
1946        let empty: [i32; 0] = [];
1947        let _ = empty.circular().index_from(0);
1948    }
1949
1950    // ── Comparison predicates ──────────────────────────────────────────
1951
1952    #[test]
1953    fn is_rotation_of_true() {
1954        let a = [1, 2, 3, 4];
1955        let b = [3, 4, 1, 2];
1956        assert!(a.circular().is_rotation_of(&b));
1957    }
1958
1959    #[test]
1960    fn is_rotation_of_false() {
1961        let a = [1, 2, 3, 4];
1962        let b = [4, 3, 2, 1];
1963        assert!(!a.circular().is_rotation_of(&b));
1964    }
1965
1966    #[test]
1967    fn is_rotation_of_empty() {
1968        let a: [i32; 0] = [];
1969        let b: [i32; 0] = [];
1970        assert!(a.circular().is_rotation_of(&b));
1971    }
1972
1973    #[test]
1974    fn is_rotation_of_length_mismatch() {
1975        assert!(![1, 2].circular().is_rotation_of(&[1, 2, 3]));
1976    }
1977
1978    #[test]
1979    fn is_reflection_of_basic() {
1980        let a = [1, 2, 3, 4];
1981        assert!(a.circular().is_reflection_of(&[1, 4, 3, 2]));
1982        assert!(a.circular().is_reflection_of(&[1, 2, 3, 4]));
1983        assert!(!a.circular().is_reflection_of(&[2, 1, 4, 3]));
1984    }
1985
1986    #[test]
1987    fn is_reversion_of_basic() {
1988        let a = [1, 2, 3, 4];
1989        assert!(a.circular().is_reversion_of(&[1, 2, 3, 4]));
1990        assert!(a.circular().is_reversion_of(&[4, 3, 2, 1]));
1991        assert!(!a.circular().is_reversion_of(&[3, 2, 1, 4]));
1992    }
1993
1994    #[test]
1995    fn is_rotation_or_reflection_of_basic() {
1996        let a = [1, 2, 3, 4];
1997        assert!(a.circular().is_rotation_or_reflection_of(&[3, 4, 1, 2])); // rotation
1998        assert!(a.circular().is_rotation_or_reflection_of(&[2, 1, 4, 3])); // rotation of reflection
1999        assert!(!a.circular().is_rotation_or_reflection_of(&[1, 3, 2, 4])); // neither
2000    }
2001
2002    #[test]
2003    fn rotation_offset_basic() {
2004        let a = [10, 20, 30, 40];
2005        assert_eq!(a.circular().rotation_offset(&[30, 40, 10, 20]), Some(2));
2006        assert_eq!(a.circular().rotation_offset(&[10, 20, 30, 40]), Some(0));
2007        assert_eq!(a.circular().rotation_offset(&[1, 2, 3, 4]), None);
2008    }
2009
2010    #[test]
2011    fn rotation_offset_length_mismatch() {
2012        assert_eq!([1, 2, 3].circular().rotation_offset(&[1, 2]), None);
2013    }
2014
2015    // ── Distance ───────────────────────────────────────────────────────
2016
2017    #[test]
2018    fn hamming_distance_basic() {
2019        let a = [1, 2, 3, 4];
2020        let b = [1, 2, 0, 4];
2021        assert_eq!(a.circular().hamming_distance(&b), 1);
2022        assert_eq!(a.circular().hamming_distance(&a), 0);
2023    }
2024
2025    #[test]
2026    #[should_panic]
2027    fn hamming_distance_length_mismatch_panics() {
2028        let _ = [1, 2].circular().hamming_distance(&[1, 2, 3]);
2029    }
2030
2031    #[test]
2032    fn min_rotational_hamming_distance_basic() {
2033        let a = [1, 2, 3, 4];
2034        let b = [3, 4, 1, 2]; // rotation of a
2035        assert_eq!(a.circular().min_rotational_hamming_distance(&b), 0);
2036        assert_eq!(a.circular().min_rotational_hamming_distance(&a), 0);
2037        let c = [3, 4, 1, 5]; // 1 off from a rotation
2038        assert_eq!(a.circular().min_rotational_hamming_distance(&c), 1);
2039    }
2040
2041    // ── contains_slice / index_of_slice ────────────────────────────────
2042
2043    #[test]
2044    fn contains_slice_basic() {
2045        let r = [1, 2, 3, 4, 5].circular();
2046        assert!(r.contains_slice(&[2, 3, 4]));
2047        assert!(r.contains_slice(&[5, 1, 2])); // wraps
2048        assert!(!r.contains_slice(&[2, 4]));
2049    }
2050
2051    #[test]
2052    fn contains_slice_empty() {
2053        assert!([1, 2, 3].circular().contains_slice(&[]));
2054        let empty: [i32; 0] = [];
2055        assert!(!empty.circular().contains_slice(&[1]));
2056        assert!(empty.circular().contains_slice(&[]));
2057    }
2058
2059    #[test]
2060    fn index_of_slice_basic() {
2061        let r = [1, 2, 3, 4, 5].circular();
2062        assert_eq!(r.index_of_slice(&[3, 4], 0), Some(2));
2063        assert_eq!(r.index_of_slice(&[5, 1], 0), Some(4)); // wraps
2064        assert_eq!(r.index_of_slice(&[9], 0), None);
2065    }
2066
2067    // ── Symmetry counts ────────────────────────────────────────────────
2068
2069    #[test]
2070    fn rotational_symmetry_basic() {
2071        assert_eq!([1, 2, 3, 4].circular().rotational_symmetry(), 1);
2072        assert_eq!([1, 2, 1, 2].circular().rotational_symmetry(), 2);
2073        assert_eq!([1, 1, 1, 1].circular().rotational_symmetry(), 4);
2074    }
2075
2076    #[test]
2077    fn rotational_symmetry_short() {
2078        let empty: [i32; 0] = [];
2079        assert_eq!(empty.circular().rotational_symmetry(), 1);
2080        assert_eq!([5].circular().rotational_symmetry(), 1);
2081    }
2082
2083    #[test]
2084    fn symmetry_palindrome() {
2085        // Palindrome has at least one reflectional symmetry axis.
2086        assert!([1, 2, 3, 2, 1].circular().symmetry() >= 1);
2087    }
2088
2089    #[test]
2090    fn symmetry_empty() {
2091        let empty: [i32; 0] = [];
2092        assert_eq!(empty.circular().symmetry(), 0);
2093    }
2094}
2095
2096#[cfg(all(test, feature = "alloc"))]
2097mod alloc_tests {
2098    use super::*;
2099    use alloc::vec;
2100    use alloc::vec::Vec;
2101
2102    // ── Booth / canonical ──────────────────────────────────────────────
2103
2104    #[test]
2105    fn canonical_index_basic() {
2106        assert_eq!([3, 1, 2].circular().canonical_index(), 1);
2107        assert_eq!([1, 2, 3].circular().canonical_index(), 0);
2108        assert_eq!([2, 3, 0, 1].circular().canonical_index(), 2);
2109    }
2110
2111    #[test]
2112    fn canonical_index_short() {
2113        let empty: [i32; 0] = [];
2114        assert_eq!(empty.circular().canonical_index(), 0);
2115        assert_eq!([7].circular().canonical_index(), 0);
2116    }
2117
2118    #[test]
2119    fn canonical_basic() {
2120        assert_eq!([3, 1, 2].circular().canonical(), vec![1, 2, 3]);
2121        assert_eq!([1, 2, 3].circular().canonical(), vec![1, 2, 3]);
2122    }
2123
2124    #[test]
2125    fn canonical_all_equal() {
2126        assert_eq!([5, 5, 5].circular().canonical(), vec![5, 5, 5]);
2127    }
2128
2129    #[test]
2130    fn canonical_rotations_share_canonical() {
2131        let a = [3, 1, 2];
2132        let b = [1, 2, 3];
2133        let c = [2, 3, 1];
2134        assert_eq!(a.circular().canonical(), b.circular().canonical());
2135        assert_eq!(b.circular().canonical(), c.circular().canonical());
2136    }
2137
2138    #[test]
2139    fn bracelet_basic() {
2140        // Bracelet treats sequence and its reflection as equivalent.
2141        let a = [3, 1, 2];
2142        let b = [3, 2, 1]; // reflection of a at index 0 = [3, 2, 1]
2143        assert_eq!(a.circular().bracelet(), b.circular().bracelet());
2144    }
2145
2146    // ── Symmetry indices / axes ────────────────────────────────────────
2147
2148    #[test]
2149    fn symmetry_indices_palindrome() {
2150        let r = [1, 2, 3, 2, 1].circular();
2151        assert!(!r.symmetry_indices().is_empty());
2152    }
2153
2154    #[test]
2155    fn symmetry_indices_none() {
2156        let r = [1, 2, 3, 4].circular();
2157        // Asymmetric ring has no reflectional symmetries
2158        assert_eq!(r.symmetry_indices(), Vec::<usize>::new());
2159    }
2160
2161    #[test]
2162    fn symmetry_indices_empty() {
2163        let empty: [i32; 0] = [];
2164        assert_eq!(empty.circular().symmetry_indices(), Vec::<usize>::new());
2165    }
2166
2167    #[test]
2168    fn reflectional_symmetry_axes_count_matches_symmetry() {
2169        let r = [1, 2, 1, 2].circular();
2170        let axes = r.reflectional_symmetry_axes();
2171        assert_eq!(axes.len(), r.symmetry());
2172    }
2173
2174    // ── to_vec ─────────────────────────────────────────────────────────
2175
2176    #[test]
2177    fn to_vec_basic() {
2178        let r = [10, 20, 30].circular();
2179        assert_eq!(r.to_vec(), vec![10, 20, 30]);
2180        assert_eq!(r.rotate_right(1).to_vec(), vec![30, 10, 20]);
2181        assert_eq!(r.reflect_at(0).to_vec(), vec![10, 30, 20]);
2182    }
2183
2184    #[test]
2185    fn to_vec_empty() {
2186        let empty: [i32; 0] = [];
2187        assert_eq!(empty.circular().to_vec(), Vec::<i32>::new());
2188    }
2189
2190    // ── Chained: prototype's headline example ──────────────────────────
2191
2192    #[test]
2193    fn rotations_map_canonical_index() {
2194        let r = [3, 1, 2, 3, 1, 2].circular();
2195        let indices: Vec<usize> = r.rotations().map(|v| v.canonical_index()).collect();
2196        assert_eq!(indices, vec![1, 0, 2, 1, 0, 2]);
2197    }
2198}