sira
sira

Reputation: 291

Circular range in Python

How can I implement a circular range object in Python?

e.g.

Let S is a circular space modulo 2^3 (range [0, 2^3)). I want to generate a range object like this:

crange(3, 7, 2 ** 3)  # => a range object [3, 4, 5, 6]
crange(7, 3, 2 ** 3)  # => a range object [7, 0, 1, 2]

I tried this:

def crange(start, stop, modulo):
    if start > stop:
        return range(start, modulo) or range(stop)
    else:
        return range(start, stop)

But I can't input bigint to crange e.g. crange(8, 2, 2 ** 160).

OverflowError: Python int too large to convert to C ssize_t

Upvotes: 3

Views: 6459

Answers (7)

Karl Knechtel
Karl Knechtel

Reputation: 61597

Python 3.x's range objects provides more functionality than just iteration and membership testing: subscripting (indexing), reverse iteration, length testing (which implies boolean coercion), the .count and .index methods (all of these collectively being the Sequence protocol), plus __str__ and __repr__.

To emulate these, we can wrap a range object, delegating to its methods with modified arguments. We can also leverage collections.abc in order to avoid the need to delegate everything, relying on mixins from the Sequence ABC instead.

There's also an issue here in that the input specification is tricky to interpret in corner cases, and inflexible (for example, we can't represent an iterator over [3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6]). The simple way around this is to redefine: a crange(start, stop, step, modulo) iterates over the values start % modulo, (start + step) % modulo ... (start + n * step) % modulo, such that (start + n * step) is limited to stop. (That is: the same values as would be in the range, but applying the specified modulo value to each of them.)

This is fairly straightforward to implement, as we need only __getitem__ and __len__:

from collections.abc import Sequence
class crange(Sequence):
    def __init__(self, start, stop, *, step=1, modulo=None):
        self._modulo, self._impl = modulo, range(start, stop, step)
    def __getitem__(self, index):
        result = self._impl[index]
        if self._modulo is not None:
            result %= self._modulo
        return result
    def __len__(self):
        return len(self._impl)
    def __repr__(self):
        return f'c{self._impl}'

Now we can do fun things like:

>>> list(reversed(crange(7, 19, step=3, modulo=8)))
[0, 5, 2, 7]

Upvotes: 0

sira
sira

Reputation: 291

I implemented crange which I want (in reference to @Ni and @J.F.Sebastian).

import math


class crange:
    def __init__(self, start, stop, step=None, modulo=None):
        if step == 0:
            raise ValueError('crange() arg 3 must not be zero')

        if step is None and modulo is None:
            self.start = 0
            self.stop = start
            self.step = 1
            self.modulo = stop
        else:
            self.start = start
            self.stop = stop
            if modulo is None:
                self.step = 1
                self.modulo = step
            else:
                self.step = step
                self.modulo = modulo

    def __iter__(self):
        n = self.start
        if n > self.stop:
            while n < self.modulo:
                yield n
                n += 1
            n = 0
        while n < self.stop:
            yield n
            n += 1

    def __contains__(self, n):
        if self.start >= self.stop:
            return self.start <= n < self.modulo or 0 <= n < self.stop
        else:
            return self.start <= n < self.stop

I got the following output:

>>> print(list(crange(start=7, stop=3, modulo=2 ** 4)))
[7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2]
>>> print(3 in crange(start=7, stop=3, modulo=2 ** 4))
False
>>> print(7 in crange(start=7, stop=3, modulo=2 ** 4))
True
>>> print(list(crange(start=3, stop=7, modulo=2 ** 4)))
[3, 4, 5, 6]
>>> print(3 in crange(start=3, stop=7, modulo=2 ** 4))
True
>>> print(7 in crange(start=3, stop=7, modulo=2 ** 4))
False

Upvotes: 3

J.Doe
J.Doe

Reputation: 444

I made my own version that can slice forwards and backwards. What it can't do in this state is use negative integers and different step sizes than one though, so that would need to be implemented to cover the whole gamut of slicing options in python. I bet that's an easy addition though.

l = list(range(10))

def crange(l,a,b):
  
  assert a >= 0
  assert b >= 0
  
  d = b-a 
  
  if d == 0:
    return
  elif d > 0:
    increment = 1
  elif d < 0:
    increment = -1
  
  start = a%len(l)
  counter = 0
  
  while counter <= abs(d):
    index = abs(start + counter*increment)%len(l)
    yield l[index]
    counter += 1
    
print(list(l))
print(list(crange(l,0,6)))
print(list(crange(l,6,10)))
print(list(crange(l,10,25)))
print(list(crange(l,9,0)))
print(list(crange(l,10,5)))

Upvotes: 0

Sadern Alwis
Sadern Alwis

Reputation: 114

Added no reset to start and subscriptability. original copied from answer by @sira

class Crange:
    def __init__(self, start, stop, step=None, modulo=None, no_reset=True):
        if step == 0:
            raise ValueError('crange() arg 3 must not be zero')
        self.no_reset = no_reset
        if step is None and modulo is None:
            self.start = 0
            self.stop = start
            self.step = 1
            self.modulo = stop
        else:
            self.start = start
            self.stop = stop
            if modulo is None:
                self.step = 1
                self.modulo = step
            else:
                self.step = step
                self.modulo = modulo

    def __iter__(self):
        n = self.start
        if n > self.stop:
            while n < self.modulo:
                yield n
                n += self.step
            if n > self.modulo: 
                n = n-self.modulo if self.no_reset else 0
            else: n=0
        while n < self.stop:
            yield n
            n += self.step
            
    def __getitem__(self, __name):
        return [*self][__name]
        
    def __contains__(self, n):
        if self.start >= self.stop:
            return self.start <= n < self.modulo or 0 <= n < self.stop
        else:
            return self.start <= n < self.stop
    

Upvotes: 0

Pepe Mandioca
Pepe Mandioca

Reputation: 344

An equivalent solution to that of @Fomalhaut but simpler:

def crange(start, end, modulo):
    for i in range(start,end):
        yield i % modulo

Upvotes: 2

kmaork
kmaork

Reputation: 6012

You can avoid the use of range and the storage of a huge list in memory by creating your own generator:

def crange(start, end, modulo):
    if start > end:
        while start < modulo:
            yield start
            start += 1
        start = 0

    while start < end:
        yield start
        start += 1

print list(crange(3, 7, 2 ** 3))
print list(crange(7, 3, 2 ** 3))
print next(crange(8, 2, 2 ** 160))

This code outputs:

[3, 4, 5, 6]
[7, 0, 1, 2]
8

Upvotes: 2

Fomalhaut
Fomalhaut

Reputation: 9765

Try this:

def crange(start, stop, modulo):
    result = []
    index = start
    while index != stop:
        result.append(index)
        index = (index + 1) % modulo
    return result

If you know that your list can be too long, you can use a generator instead that generates the necessage sequence:

def crange(start, stop, modulo):
    index = start
    while index != stop:
        yield index
        index = (index + 1) % modulo

Upvotes: 3

Related Questions