Module grscheller.circular_array.ca

Module for an indexable circular array data structure.

Classes

class CA (*ds: _D, sentinel: _S)

Class implementing an indexable circular array

  • stateful generic data structure with a data type and a "fallback/sentinel" type
  • amortized O(1) pushing and popping from either end
  • O(1) random access any element
  • will resize itself as needed
  • not sliceable
  • in a boolean context returned False if empty, True otherwise
  • intended to implement other data structures, so
  • does not make defensive copies of data for the purposes of iteration
  • raises: IndexError
Expand source code
class CA(Generic[_D, _S]):
    """Class implementing an indexable circular array

    * stateful generic data structure with a data type and a "fallback/sentinel" type
    * amortized O(1) pushing and popping from either end
    * O(1) random access any element
    * will resize itself as needed
    * not sliceable
    * in a boolean context returned False if empty, True otherwise
    * intended to implement other data structures, so
    * does not make defensive copies of data for the purposes of iteration
    * raises: IndexError

    """
    __slots__ = '_count', '_capacity', '_front', '_rear', '_list', '_s'

    def __init__(self, *ds: _D, sentinel: _S):
        self._s = sentinel
        match len(ds):
            case 0:
                self._list: list[_D|_S] = [sentinel, sentinel]
                self._count = 0
                self._capacity = 2
                self._front = 0
                self._rear = 1
            case count:
                self._list = list(ds)
                self._count = count
                self._capacity = count
                self._front = 0
                self._rear = count - 1

    def __iter__(self) -> Iterator[_D]:
        if self._count > 0:
            capacity,       rear,       position,    currentState = \
            self._capacity, self._rear, self._front, self._list.copy()

            while position != rear:
                yield currentState[position]          # type: ignore # will always yield _D
                position = (position + 1) % capacity
            yield currentState[position]              # type: ignore # will always yield _D

    def __reversed__(self) -> Iterator[_D]:
        if self._count > 0:
            capacity,       front,       position,   currentState = \
            self._capacity, self._front, self._rear, self._list.copy()

            while position != front:
                yield currentState[position]         # type: ignore # will always yield _D
                position = (position - 1) % capacity
            yield currentState[position]             # type: ignore # will always yield _D

    def __repr__(self) -> str:
        return 'CA(' + ', '.join(map(repr, self)) + ', sentinel = ' + repr(self._s) + ')'

    def __str__(self) -> str:
        return "(|" + ", ".join(map(str, self)) + "|)"

    def __bool__(self) -> bool:
        return self._count > 0

    def __len__(self) -> int:
        return self._count

    def __getitem__(self, index: int) -> _D:
        cnt = self._count
        if 0 <= index < cnt:
            return self._list[(self._front + index) % self._capacity]        # type: ignore # will always return a _D
        elif -cnt <= index < 0:
            return self._list[(self._front + cnt + index) % self._capacity]  # type: ignore # will always return a _D
        else:
            if cnt > 0:
                msg1 = 'Out of bounds: '
                msg2 = f'index = {index} not between {-cnt} and {cnt-1} '
                msg3 = 'while getting value from a CircularArray.'
                raise IndexError(msg1 + msg2 + msg3)
            else:
                msg0 = 'Trying to get value from an empty CircularArray.'
                raise IndexError(msg0)

    def __setitem__(self, index: int, value: _D) -> None:
        cnt = self._count
        if 0 <= index < cnt:
            self._list[(self._front + index) % self._capacity] = value
        elif -cnt <= index < 0:
            self._list[(self._front + cnt + index) % self._capacity] = value
        else:
            if cnt > 0:
                msg1 = 'Out of bounds: '
                msg2 = f'index = {index} not between {-cnt} and {cnt-1} '
                msg3 = 'while setting value from a CircularArray.'
                raise IndexError(msg1 + msg2 + msg3)
            else:
                msg0 = 'Trying to set value from an empty CircularArray.'
                raise IndexError(msg0)

    def __eq__(self, other: object) -> bool:
        """Returns True if all the data stored in both compare as equal.

        * worst case is O(n) behavior for the true case

        """
        if not isinstance(other, type(self)):
            return False
        if self._s != other._s:
            return False

        frontL,      capacityL,      countL,      frontR,       capacityR,       countR = \
        self._front, self._capacity, self._count, other._front, other._capacity, other._count

        if countL != countR:
            return False

        for nn in range(countL):
            if self._list[(frontL+nn)%capacityL] != other._list[(frontR+nn)%capacityR]:
                return False
        return True

    def copy(self) -> CA[_D, _S]:
        """Return a shallow copy of the CircularArray."""
        return CA(*self, sentinel=self._s)

    def pushR(self, *ds: _D) -> None:
        """Push data onto the rear of the CircularArray."""
        for d in ds:
            if self._count == self._capacity:
                self.double()
            self._rear = (self._rear + 1) % self._capacity
            self._list[self._rear] = d
            self._count += 1

    def pushL(self, *ds: _D) -> None:
        """Push data onto the front of the CircularArray."""
        for d in ds:
            if self._count == self._capacity:
                self.double()
            self._front = (self._front - 1) % self._capacity
            self._list[self._front] = d
            self._count += 1

    def popR(self) -> _D|_S:
        """Pop data off the rear of the CircularArray.

        * returns None if empty
        * use in a boolean context to determine if empty

        """
        if self._count == 0:
            return self._s
        else:
            d, self._count, self._list[self._rear], self._rear = \
                self._list[self._rear], self._count-1, self._s, (self._rear - 1) % self._capacity
            return d

    def popL(self) -> _D|_S:
        """Pop data off the front of the CircularArray.

        * returns None if empty
        * use in a boolean context to determine if empty

        """
        if self._count == 0:
            return self._s
        else:
            d, self._count, self._list[self._front], self._front = \
                self._list[self._front], self._count-1, self._s, (self._front+1) % self._capacity
            return d

    def map(self, f: Callable[[_D], _U]) -> CA[_U, _S]:
        """Apply function f over the CircularArray's contents.

        * return the results in a new CircularArray

        """
        return CA(*map(f, self), sentinel=self._s)

    def foldL(self, f: Callable[[_D, _D], _D]) -> _D|_S:
        """Fold left with an initial value.

        * first argument of function f is for the accumulated value
        * if empty, return the sentinel value of type _S

        """
        if self:
            it = iter(self)
            acc = next(it)
            for v in it:
                acc = f(acc, v)
            return acc
        else:
            return self._s

    def foldL1(self, f: Callable[[_L, _D], _L], init: _L) -> _L:
        """Fold left with an initial value.

        * first argument of function f is for the accumulated value

        """
        acc = init
        for v in iter(self):
            acc = f(acc, v)
        return acc

    def foldR(self, f: Callable[[_D, _D], _D]) -> _D|_S:
        """Fold right with an initial value.

        * second argument of function f is for the accumulated value
        * if empty, return the sentinel value of type _S

        """
        if self:
            it = reversed(self)
            acc = next(it)
            for v in it:
                acc = f(v, acc)
            return acc
        else:
            return self._s

    def foldR1(self, f: Callable[[_D, _R], _R], init: _R) -> _R:
        """Fold right with an initial value.

        * second argument of function f is for the accumulated value

        """
        acc = init
        for v in reversed(self):
            acc = f(v, acc)
        return acc

    def capacity(self) -> int:
        """Returns current capacity of the CircularArray."""
        return self._capacity

    def compact(self) -> None:
        """Compact the CircularArray as much as possible."""
        match self._count:
            case 0:
                self._capacity, self._front, self._rear, self._list = 2, 0, 1, [self._s]*2
            case 1:
                self._capacity, self._front, self._rear, self._list = 1, 0, 0, [self._list[self._front]]
            case _:
                if self._front <= self._rear:
                    self._capacity, self._front, self._rear,    self._list = \
                    self._count,    0,           self._count-1, self._list[self._front:self._rear+1]
                else:
                    self._capacity, self._front, self._rear,    self._list = \
                    self._count,    0,           self._count-1, self._list[self._front:] + self._list[:self._rear+1]

    def double(self) -> None:
        """Double the capacity of the CircularArray."""
        if self._front <= self._rear:
            self._list += [self._s]*self._capacity
            self._capacity *= 2
        else:
            self._list = self._list[:self._front] + [self._s]*self._capacity + self._list[self._front:]
            self._front += self._capacity
            self._capacity *= 2

    def empty(self) -> None:
        """Empty the CircularArray, keep current capacity."""
        self._list, self._front, self._rear = [self._s]*self._capacity, 0, self._capacity-1

    def fractionFilled(self) -> float:
        """Returns fractional capacity of the CircularArray."""
        return self._count/self._capacity

    def resize(self, newSize: int= 0) -> None:
        """Compact CircularArray and resize to newSize if less than newSize."""
        self.compact()
        capacity = self._capacity
        if newSize > capacity:
            self._list, self._capacity = self._list+[self._s]*(newSize-capacity), newSize
            if self._count == 0:
                self._rear = capacity - 1

Ancestors

  • typing.Generic

Methods

def capacity(self) ‑> int

Returns current capacity of the CircularArray.

def compact(self) ‑> None

Compact the CircularArray as much as possible.

def copy(self) ‑> CA[_D, _S]

Return a shallow copy of the CircularArray.

def double(self) ‑> None

Double the capacity of the CircularArray.

def empty(self) ‑> None

Empty the CircularArray, keep current capacity.

def foldL(self, f: Callable[[_D, _D], _D]) ‑> Union[_D, _S]

Fold left with an initial value.

  • first argument of function f is for the accumulated value
  • if empty, return the sentinel value of type _S
def foldL1(self, f: Callable[[_L, _D], _L], init: _L) ‑> _L

Fold left with an initial value.

  • first argument of function f is for the accumulated value
def foldR(self, f: Callable[[_D, _D], _D]) ‑> Union[_D, _S]

Fold right with an initial value.

  • second argument of function f is for the accumulated value
  • if empty, return the sentinel value of type _S
def foldR1(self, f: Callable[[_D, _R], _R], init: _R) ‑> _R

Fold right with an initial value.

  • second argument of function f is for the accumulated value
def fractionFilled(self) ‑> float

Returns fractional capacity of the CircularArray.

def map(self, f: Callable[[_D], _U]) ‑> CA[_U, _S]

Apply function f over the CircularArray's contents.

  • return the results in a new CircularArray
def popL(self) ‑> Union[_D, _S]

Pop data off the front of the CircularArray.

  • returns None if empty
  • use in a boolean context to determine if empty
def popR(self) ‑> Union[_D, _S]

Pop data off the rear of the CircularArray.

  • returns None if empty
  • use in a boolean context to determine if empty
def pushL(self, *ds: _D) ‑> None

Push data onto the front of the CircularArray.

def pushR(self, *ds: _D) ‑> None

Push data onto the rear of the CircularArray.

def resize(self, newSize: int = 0) ‑> None

Compact CircularArray and resize to newSize if less than newSize.