RingSeq
July 5, 2026 · View on GitHub
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.
Setup
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 Native
Then 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
Operations
Indexing
| 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 |
Rotation and reflection
| 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 |
Slicing
| 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 |
Iterators
| 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 |
Comparisons
| 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 |
Canonical forms
| 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 |
Symmetry
| 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 |
The RingView layer
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.
Naming convention
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.
Performance notes
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.
Use cases
- 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
Documentation
- Website — narrative docs and examples.
- Usage walkthrough.
- Scaladoc API — the Scala 3 API reference is valid for Scala 2.13 as well.
- Changelog.
Other languages
The same library, adapted for the specific idiom, is available also for:
- Python — ring-seq-py
- Rust — ring-seq-rs
License
Licensed under either of
at your option.