Circular (ring) sequence operations for Rust slices.
Treat any [T], Vec<T>, array, or Box<[T]> as a circular sequence
— the element after the last wraps back to the first.
- Zero dependencies. Zero
unsafe.#![no_std]with a defaultallocfeature. - All transforms return lazy views or iterators; allocation is opt-in.
- Single entry point:
slice.circular()gives you aCircular<T>wrapper that hosts every operation.
[dependencies]
ring-seq = "0.3"use ring_seq::AsCircular;
let r = [10, 20, 30].circular();
// Indexing wraps in both directions
assert_eq!(*r.apply(4), 20);
assert_eq!(*r.apply(-1), 30);
// Reindexed views — no allocation
let rotated: Vec<_> = r.rotate_right(1).iter().copied().collect();
assert_eq!(rotated, [30, 10, 20]);
// Comparison up to rotation / reflection
assert!(r.is_rotation_of(&[20, 30, 10]));
assert!(r.is_reflection_of(&[10, 30, 20]));
// Canonical (necklace) form — O(n) minimal rotation
assert_eq!(r.canonical(), [10, 20, 30]);
// Lazy iterators of views compose naturally
let firsts: Vec<i32> = r.rotations().map(|v| *v.apply(0)).collect();
assert_eq!(firsts, [10, 20, 30]);
// Symmetry detection
assert_eq!([0, 1, 0, 1].circular().rotational_symmetry(), 2);| Method | Returns | Description |
|---|---|---|
apply(i) / r[i] |
&T |
Element at circular index (panics if empty) |
get(i) |
Option<&T> |
Non-panicking apply |
index_from(i) |
usize |
Normalize a circular index to [0, len) |
iter() |
CircularIter |
Walk the view's elements (lazy, double-ended) |
Circular also implements IntoIterator, so for x in ring.circular()
walks the view directly.
| Method | Returns | Description |
|---|---|---|
start_at(i) |
Circular |
View starts at circular index i |
rotate_left(step) |
Circular |
Shift left by step (negative = right) |
rotate_right(step) |
Circular |
Shift right by step (negative = left) |
reflect_at(i) |
Circular |
Reflect around index i |
| Method | Returns | Description |
|---|---|---|
slice(from, to) |
CircularIter |
max(to - from, 0) elements, wrapping |
take_while(pred, from) |
impl Iterator |
Prefix satisfying pred (≤ one lap) |
drop_while(pred, from) |
impl Iterator |
Remainder after the prefix |
enumerate(from) |
Enumerate |
(&T, ring_index) pairs |
| Method | Yields | Description |
|---|---|---|
rotations() |
Circular × n |
Every rotation |
reflections() |
Circular × 2 |
Original + reflect_at(0) |
reversions() |
Circular × 2 |
Original + reverse |
rotations_and_reflections() |
Circular × 2n |
All dihedral variants |
windows(size) |
CircularIter × n |
Sliding windows, step 1 |
chunks(size) |
CircularIter × ceil(n/size) |
Non-overlapping chunks |
| Method | Description |
|---|---|
is_rotation_of(other) |
Same elements, possibly rotated? |
is_reflection_of(other) |
Equals self or self.reflect_at(0) |
is_reversion_of(other) |
Equals self or its reverse |
is_rotation_or_reflection_of(other) |
Any dihedral variant |
rotation_offset(other) |
Some(k) where self.start_at(k) == other |
hamming_distance(other) |
Positional mismatches |
min_rotational_hamming_distance(other) |
Minimum over all rotations |
contains_slice(needle) |
Does needle appear circularly? |
index_of_slice(needle, from) |
First view position where needle matches |
The rotation/needle searches and symmetry counts are intentionally
simple quadratic scans (O(n·m) / O(n²)); canonical_index is O(n).
| Method | Returns | Notes |
|---|---|---|
canonical_index() |
usize |
Index of lex-smallest rotation (O(n) time, O(1) space) |
canonical() |
Vec<T> |
Lex-smallest rotation (alloc) |
bracelet() |
Vec<T> |
Lex-smallest under rotation + reflection (alloc) |
rotational_symmetry() |
usize |
Order of rotational symmetry |
symmetry() |
usize |
Number of reflectional axes |
symmetry_indices() |
Vec<usize> |
Shifts where the view equals its reverse (alloc) |
reflectional_symmetry_axes() |
Vec<(AxisLocation, AxisLocation)> |
Full axis geometry (alloc) |
| Method | Returns | Notes |
|---|---|---|
to_vec() |
Vec<T> |
Materialize the view (alloc, T: Clone) |
[dependencies]
ring-seq = { version = "0.3", default-features = false }Disabling the default alloc feature drops only the methods that return
owned collections (canonical, bracelet, symmetry_indices,
reflectional_symmetry_axes, to_vec). Everything else — the Circular
wrapper, every reindexed-view method, every iterator, and every scalar
query including canonical_index — depends only on core.
CI verifies the crate compiles for wasm32-unknown-unknown both with and
without alloc.
- Bioinformatics — circular DNA/RNA sequence alignment and comparison
- Graphics — polygon vertex manipulation, closed curve operations
- Procedural generation — tile rings, symmetry-aware pattern generation
- Music theory — pitch-class sets, chord inversions
- Combinatorics — necklace/bracelet enumeration, Burnside's lemma
- Embedded / robotics — circular sensor arrays, rotary encoder positions
1.63
The same library, adapted for the specific idiom, is available also for:
- Python — ring-seq-py
- Scala — ring-seq
Licensed under either of
at your option.