Skip to content

scala-tessella/ring-seq-py

Repository files navigation

RingSeqPy

PyPI - Version CI Python Version from PEP 621 TOML

A Pythonic class for sequences considered circular — where the element after the last wraps back to the first.

RingSeqPy is a small, zero-dependency library exposing a single generic RingSeq[T] class that wraps any Iterable[T], stores it internally as an immutable tuple, and implements the Python Sequence protocol circularly. Instances are hashable, orderable, and interoperable: RingSeq('ABC'), RingSeq(['A','B','C']), and RingSeq(('A','B','C')) all compare equal.

Installation

pip install ring-seq-py

Working for Python 3.10 and above.

Quick start

>>> from ring_seq import RingSeq

>>> # Indexing wraps around
>>> RingSeq([10, 20, 30])[4]
20

>>> # Slicing wraps around too; transformations return a new RingSeq,
>>> # unwrap with .to_list() / .to_tuple() / .to_str()
>>> RingSeq([0, 1, 2]).rotate_right(1).to_list()
[2, 0, 1]

>>> # Comparison up to rotation
>>> RingSeq([0, 1, 2]).is_rotation_of([2, 0, 1])
True

>>> # Canonical (necklace) form for deduplication
>>> RingSeq([2, 0, 1]).canonical().to_list()
[0, 1, 2]

>>> # Symmetry detection
>>> RingSeq([0, 1, 0, 1]).rotational_symmetry()
2

>>> # Strings work naturally; to_str() rejoins
>>> RingSeq('RING').rotate_right(1).to_str()
'GRIN'

Operations

Native sequence protocol (circular)

Method Description
rs[i] Element at circular index (any integer wraps)
rs[i:j], rs[i:j:k] Circular slice (can exceed ring length)
len(rs), iter(rs), x in rs Standard protocol, no surprises
rs == other, hash(rs), min(rings) Positional equality; lexicographic ordering
index(value, start=0, stop=None) Circular first-occurrence lookup

Unwrap

Method Description
to_list() Return a new list
to_tuple() Return the internal tuple
to_str(sep='') Join elements into a str

Indexing helper

Method Description
index_from(i) Normalize a circular index to [0, len)

Transforming

Method Description
rotate_right(step) Rotate right by step (negative = left)
rotate_left(step) Rotate left by step (negative = right)
start_at(i) Rotate so circular index i is first
reflect_at(i=0) Reflect so circular index i is the axis head

Slicing primitives

Method Description
take_while(p, from_=0) Longest prefix from from_ satisfying p
drop_while(p, from_=0) Remainder after that prefix
span(p, from_=0) (take_while, drop_while) in one call

Iterating

Method Description
rotations() All n rotations (lazy)
reflections() Original + reflection (lazy)
reversions() Original + reversal (lazy)
rotations_and_reflections() All 2n variants (lazy)
grouped(size) ceil(n / size) fixed-size blocks, last one wraps the seam
zip_with_index(from_=0) Elements paired with their circular indices

Comparing

Method Description
is_rotation_of(that) Same elements, possibly rotated?
is_reflection_of(that) Same elements, possibly reflected?
is_reversion_of(that) Same elements, possibly reversed?
is_rotation_or_reflection_of(that) Either of the above?
align_to(that) k such that start_at(k) == that, or None
hamming_distance(that) Positional mismatches (same size required)
min_rotational_hamming_distance(that) Minimum distance over all rotations

Necklace

Method Description
canonical_index() Index of lex-smallest rotation (Booth's O(n))
canonical() Lex-smallest rotation (necklace form)
bracelet() Lex-smallest under rotation and reflection

Symmetry

Method Description
rotational_symmetry() Order of rotational symmetry
symmetry_indices() Shifts where the ring equals its reversal rotated left
reflectional_symmetry_axes() Full axis geometry (Vertex / Edge pairs)
symmetry() Number of reflectional symmetry axes

Naming convention

RingSeq subclasses collections.abc.Sequence, so native Python protocols do the work where they map cleanly — rs[i] for indexing, rs[i:j] for slicing, x in rs for containment. A few methods are deliberately renamed from the Scala/Rust counterparts to be Pythonic:

This library Elsewhere
rs[i] apply_o
rs[i:j], rs[i:j:k] slice_o
index index_of_slice
grouped circular_chunks
zip_with_index circular_enumerate

RingSeq.index(value, start=0, stop=None) overrides Sequence.index with circular semantics: without a stop it searches one full revolution and returns an index in [0, len).

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

Other languages

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

License

Licensed under either of

at your option.

About

A Pythonic class for sequences considered circular — rotations, reflections, necklace canonical forms, symmetry detection, and more.

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Contributors

Languages