RingSeq

July 5, 2026 · View on GitHub

ring-seq Scala version support Maven Central Scala.js CI Scala Steward badge

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

MethodDescription
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

MethodDescription
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

MethodDescription
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

MethodDescription
slidingO(size, step)Circular sliding windows
rotationsAll n rotations
reflectionsOriginal + reflection
reversionsOriginal + reversal
rotationsAndReflectionsAll 2n variants
groupedO(size)Fixed-size circular groups
zipWithIndexO(from)Elements paired with their circular indices

Comparisons

MethodDescription
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

MethodDescription
canonicalIndexIndex of lex-smallest rotation (O(n) time, O(1) space)
canonicalLex-smallest rotation (necklace form)
braceletLex-smallest under rotation and reflection

Symmetry

MethodDescription
rotationalSymmetryOrder of rotational symmetry
symmetryIndicesIndices on each axis of reflectional symmetry
reflectionalSymmetryAxesFull axis geometry as (AxisLocation, AxisLocation) pairs (Vertex / Edge)
symmetryNumber 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 any IndexedSeq) over List.
  • If you start from a List and 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

Other languages

The same library, adapted for the specific idiom, is available also for:

License

Licensed under either of

at your option.