A library that adds new operations to Scala Seq
for when a sequence needs to be considered circular,
its elements forming a ring — the element after the last wraps back to the first.
It works on any immutable or mutable Seq and sub-type (Vector, List, String, ArraySeq, ListBuffer, …),
acting as a decorator via Scala 3 extension
or Scala 2 implicit class.
Available for Scala 3.3.7 and 2.13.18, cross-published for the JVM, Scala.js, and Scala Native.
Zero runtime dependencies.
Add the dependency to your build.sbt:
libraryDependencies += "io.github.scala-tessella" %% "ring-seq" % "0.9.0"
// Use %%% instead of %% for Scala.js or Scala NativeThen import the RingSeq object — every collection under Seq gains the new methods:
import io.github.scala_tessella.ring_seq.RingSeq.*
// Indexing wraps around
Seq(10, 20, 30).applyO(4) // 20
// Rotation produces a new collection of the same type
"RING".rotateRight(1).mkString // "GRIN"
List(0, 1, 2, 3).startAt(2) // List(2, 3, 0, 1)
// Comparison up to rotation
Seq(0, 1, 2).isRotationOf(Seq(2, 0, 1)) // true
// Canonical (necklace) form — for deduplication or hashing
Seq(2, 0, 1).canonical // Seq(0, 1, 2)
// Symmetry detection
Seq(0, 1, 0, 1).rotationalSymmetry // 2| Method | Description |
|---|---|
indexFrom(i) |
Normalize a circular index to [0, n) |
applyO(i) |
Element at circular index |
liftO(i) |
Some(element) at circular index, None if empty |
indexOfO(elem, from) |
Circular index of the first occurrence of elem |
| Method | Description |
|---|---|
rotateRight(step) |
Rotate right by step (negative = left) |
rotateLeft(step) |
Rotate left by step (negative = right) |
startAt(i) |
Rotate so circular index i is first |
reflectAt(i) |
Reflect so circular index i is the axis head |
| Method | Description |
|---|---|
sliceO(from, to) |
Circular interval (can exceed ring length) |
containsSliceO(that) |
Does the ring contain that circularly? |
indexOfSliceO(that) |
First circular position of that |
lastIndexOfSliceO(that) |
Last circular position of that |
segmentLengthO(p, from) |
Length of prefix satisfying p |
takeWhileO(p, from) |
Prefix satisfying p |
dropWhileO(p, from) |
Remainder after that prefix |
spanO(p, from) |
(takeWhileO, dropWhileO) in one call |
| Method | Description |
|---|---|
slidingO(size, step) |
Circular sliding windows |
rotations |
All n rotations |
reflections |
Original + reflection |
reversions |
Original + reversal |
rotationsAndReflections |
All 2n variants |
groupedO(size) |
Fixed-size circular groups |
zipWithIndexO(from) |
Elements paired with their circular indices |
| Method | Description |
|---|---|
isRotationOf(that) |
Same elements, possibly rotated? |
isReflectionOf(that) |
Same elements, possibly reflected? |
isReversionOf(that) |
Same elements, possibly reversed? |
isRotationOrReflectionOf(that) |
Either of the above? |
alignTo(that) |
Some(k) with startAt(k) == that, or None |
hammingDistance(that) |
Positional mismatches (same size required) |
minRotationalHammingDistance(that) |
Minimum distance over all rotations |
| Method | Description |
|---|---|
canonicalIndex |
Index of lex-smallest rotation (O(n) time, O(1) space) |
canonical |
Lex-smallest rotation (necklace form) |
bracelet |
Lex-smallest under rotation and reflection |
| Method | Description |
|---|---|
rotationalSymmetry |
Order of rotational symmetry |
symmetryIndices |
Indices on each axis of reflectional symmetry |
reflectionalSymmetryAxes |
Full axis geometry as (AxisLocation, AxisLocation) pairs (Vertex / Edge) |
symmetry |
Number of reflectional symmetry axes |
For chained transformations, .ring turns any Seq (or String) into a lazy view:
a RingView wraps the elements once
and tracks a rotation offset plus a reflection flag, so rotating and reflecting are O(1)
and never copy — the same design as the Circular view of the Rust port.
val ring = Vector(0, 1, 2, 3).ring // a view — no copy
ring.rotateRight(1).reflectAt() // O(1) transforms, still a view
ring.rotations // Iterator of n views, O(1) each
ring.rotateRight(1).to(List) // materialize at the boundary: List(3, 0, 1, 2)
"RING".ring.reverse.iterator.mkString // "GNIR"On a RingView the circular operations carry their plain names — no O suffix is needed,
since there is no Seq method to clash with: ring(5) wraps, ring.slice(-1, 4),
ring.sliding(2), ring.lift(i), plus all the comparison, necklace and symmetry operations.
The alternative circular versions of methods that already exist on Seq keep the same name
with an appended O suffix (meaning ring) — for example applyO is the circular version of apply.
Operations with no standard-library counterpart use plain names.
On RingView the O suffix is dropped.
Circular operations involve random indexing, which is O(1) on IndexedSeq (e.g. Vector, ArraySeq, String) and O(n) on LinearSeq (e.g. List).
For best performance on large sequences:
- Prefer
Vector(or anyIndexedSeq) overList. - If you start from a
Listand call several circular operations, convert once with.toVector.
- 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
- Website — narrative docs and examples.
- Usage walkthrough.
- Scaladoc API — the Scala 3 API reference is valid for Scala 2.13 as well.
- Changelog.
The same library, adapted for the specific idiom, is available also for:
- Python — ring-seq-py
- Rust — ring-seq-rs
Licensed under either of
at your option.