Module grscheller.circular_array
Module for an indexable circular array data structure
- stateful data structure
- O(1) random access any element
- amortized O(1) pushing and popping from either end
- data structure will resize itself as needed
Expand source code
# Copyright 2023-2024 Geoffrey R. Scheller
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module for an indexable circular array data structure
* stateful data structure
* O(1) random access any element
* amortized O(1) pushing and popping from either end
* data structure will resize itself as needed
"""
from __future__ import annotations
__version__ = "2.0.0"
__all__ = ['CircularArray']
__author__ = "Geoffrey R. Scheller"
__copyright__ = "Copyright (c) 2023-2024 Geoffrey R. Scheller"
__license__ = "Apache License 2.0"
from typing import Any, Callable
from itertools import chain
class CircularArray:
"""Class implementing an indexible circular array
* indexing, pushing & popping and length determination all O(1) operations
* popping from an empty CircularArray returns None
* in a boolean context returnd False if empty, True otherwise
* iterators caches current content
* a CircularArray instance will resize itself as needed
* circularArrays are not sliceable
* raises: IndexError
"""
__slots__ = '_count', '_capacity', '_front', '_rear', '_list'
def __init__(self, *data):
match len(data):
case 0:
self._list = [None, None]
self._count = 0
self._capacity = 2
self._front = 0
self._rear = 1
case count:
self._list = list(data)
self._count = count
self._capacity = count
self._front = 0
self._rear = count - 1
def __iter__(self):
if self._count > 0:
capacity, rear, position, currentState = \
self._capacity, self._rear, self._front, self._list.copy()
while position != rear:
yield currentState[position]
position = (position + 1) % capacity
yield currentState[position]
def __reversed__(self):
if self._count > 0:
capacity, front, position, currentState = \
self._capacity, self._front, self._rear, self._list.copy()
while position != front:
yield currentState[position]
position = (position - 1) % capacity
yield currentState[position]
def __repr__(self):
return f'{self.__class__.__name__}(' + ', '.join(map(repr, self)) + ')'
def __str__(self):
return "(|" + ", ".join(map(repr, self)) + "|)"
def __bool__(self):
return self._count > 0
def __len__(self):
return self._count
def __getitem__(self, index: int) -> Any:
cnt = self._count
if 0 <= index < cnt:
return self._list[(self._front + index) % self._capacity]
elif -cnt <= index < 0:
return self._list[(self._front + cnt + index) % self._capacity]
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: Any) -> Any:
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):
"""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
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) -> CircularArray:
"""Return a shallow copy of the CircularArray."""
return CircularArray(*self)
def reverse(self) -> CircularArray:
return CircularArray(*reversed(self))
def pushR(self, value: Any) -> None:
"""Push data onto the rear of the CircularArray."""
if self._count == self._capacity:
self.double()
self._rear = (self._rear + 1) % self._capacity
self._list[self._rear] = value
self._count += 1
def pushL(self, value: Any) -> None:
"""Push data onto the front of the CircularArray."""
if self._count == self._capacity:
self.double()
self._front = (self._front - 1) % self._capacity
self._list[self._front] = value
self._count += 1
def popR(self) -> Any:
"""Pop data off the rear of the CirclularArray, returns None if empty."""
if self._count == 0:
return None
else:
value, self._count, self._list[self._rear], self._rear = \
self._list[self._rear], self._count-1, None, (self._rear - 1) % self._capacity
return value
def popL(self) -> Any:
"""Pop data off the front of the CirclularArray, returns None if empty."""
if self._count == 0:
return None
else:
value, self._count, self._list[self._front], self._front = \
self._list[self._front], self._count-1, None, (self._front+1) % self._capacity
return value
def map(self, f: Callable[[Any], Any]) -> CircularArray:
"""Apply function f over the CircularArray's contents and return
the results in a new CircularArray.
"""
return CircularArray(*map(f, self))
def mapSelf(self, f: Callable[[Any], Any]) -> None:
"""Apply function f over the CircularArray's contents mutating the
CircularArray, does not return anything.
"""
ca = CircularArray(*map(f, self))
self._count, self._capacity, self._front, self._rear, self._list = \
ca._count, ca._capacity, ca._front, ca._rear, ca._list
def foldL(self, f: Callable[[Any, Any], Any], initial: Any=None) -> Any:
"""Fold left with optional initial value. The first argument of `f` is
the accumulated value. If CircularArray is empty and no initial value
given, return `None`.
"""
if self._count == 0:
return initial
if initial is None:
vs = iter(self)
else:
vs = chain((initial,), self)
value = next(vs)
for v in vs:
value = f(value, v)
return value
def foldR(self, f: Callable[[Any, Any], Any], initial: Any=None) -> Any:
"""Fold right with optional initial value. The second argument of `f` is
the accumulated value. If CircularArray is empty and no initial
value given, return `None`.
"""
if self._count == 0:
return initial
if initial is None:
vs = reversed(self)
else:
vs = chain((initial,), reversed(self))
value = next(vs)
for v in vs:
value = f(v, value)
return value
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, [None]*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 += [None]*self._capacity
self._capacity *= 2
else:
self._list = self._list[:self._front] + [None]*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 = [None]*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+[None]*(newSize-capacity), newSize
if self._count == 0:
self._rear = capacity - 1
if __name__ == "__main__":
pass
Classes
class CircularArray (*data)
-
Class implementing an indexible circular array
- indexing, pushing & popping and length determination all O(1) operations
- popping from an empty CircularArray returns None
- in a boolean context returnd False if empty, True otherwise
- iterators caches current content
- a CircularArray instance will resize itself as needed
- circularArrays are not sliceable
- raises: IndexError
Expand source code
class CircularArray: """Class implementing an indexible circular array * indexing, pushing & popping and length determination all O(1) operations * popping from an empty CircularArray returns None * in a boolean context returnd False if empty, True otherwise * iterators caches current content * a CircularArray instance will resize itself as needed * circularArrays are not sliceable * raises: IndexError """ __slots__ = '_count', '_capacity', '_front', '_rear', '_list' def __init__(self, *data): match len(data): case 0: self._list = [None, None] self._count = 0 self._capacity = 2 self._front = 0 self._rear = 1 case count: self._list = list(data) self._count = count self._capacity = count self._front = 0 self._rear = count - 1 def __iter__(self): if self._count > 0: capacity, rear, position, currentState = \ self._capacity, self._rear, self._front, self._list.copy() while position != rear: yield currentState[position] position = (position + 1) % capacity yield currentState[position] def __reversed__(self): if self._count > 0: capacity, front, position, currentState = \ self._capacity, self._front, self._rear, self._list.copy() while position != front: yield currentState[position] position = (position - 1) % capacity yield currentState[position] def __repr__(self): return f'{self.__class__.__name__}(' + ', '.join(map(repr, self)) + ')' def __str__(self): return "(|" + ", ".join(map(repr, self)) + "|)" def __bool__(self): return self._count > 0 def __len__(self): return self._count def __getitem__(self, index: int) -> Any: cnt = self._count if 0 <= index < cnt: return self._list[(self._front + index) % self._capacity] elif -cnt <= index < 0: return self._list[(self._front + cnt + index) % self._capacity] 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: Any) -> Any: 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): """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 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) -> CircularArray: """Return a shallow copy of the CircularArray.""" return CircularArray(*self) def reverse(self) -> CircularArray: return CircularArray(*reversed(self)) def pushR(self, value: Any) -> None: """Push data onto the rear of the CircularArray.""" if self._count == self._capacity: self.double() self._rear = (self._rear + 1) % self._capacity self._list[self._rear] = value self._count += 1 def pushL(self, value: Any) -> None: """Push data onto the front of the CircularArray.""" if self._count == self._capacity: self.double() self._front = (self._front - 1) % self._capacity self._list[self._front] = value self._count += 1 def popR(self) -> Any: """Pop data off the rear of the CirclularArray, returns None if empty.""" if self._count == 0: return None else: value, self._count, self._list[self._rear], self._rear = \ self._list[self._rear], self._count-1, None, (self._rear - 1) % self._capacity return value def popL(self) -> Any: """Pop data off the front of the CirclularArray, returns None if empty.""" if self._count == 0: return None else: value, self._count, self._list[self._front], self._front = \ self._list[self._front], self._count-1, None, (self._front+1) % self._capacity return value def map(self, f: Callable[[Any], Any]) -> CircularArray: """Apply function f over the CircularArray's contents and return the results in a new CircularArray. """ return CircularArray(*map(f, self)) def mapSelf(self, f: Callable[[Any], Any]) -> None: """Apply function f over the CircularArray's contents mutating the CircularArray, does not return anything. """ ca = CircularArray(*map(f, self)) self._count, self._capacity, self._front, self._rear, self._list = \ ca._count, ca._capacity, ca._front, ca._rear, ca._list def foldL(self, f: Callable[[Any, Any], Any], initial: Any=None) -> Any: """Fold left with optional initial value. The first argument of `f` is the accumulated value. If CircularArray is empty and no initial value given, return `None`. """ if self._count == 0: return initial if initial is None: vs = iter(self) else: vs = chain((initial,), self) value = next(vs) for v in vs: value = f(value, v) return value def foldR(self, f: Callable[[Any, Any], Any], initial: Any=None) -> Any: """Fold right with optional initial value. The second argument of `f` is the accumulated value. If CircularArray is empty and no initial value given, return `None`. """ if self._count == 0: return initial if initial is None: vs = reversed(self) else: vs = chain((initial,), reversed(self)) value = next(vs) for v in vs: value = f(v, value) return value 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, [None]*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 += [None]*self._capacity self._capacity *= 2 else: self._list = self._list[:self._front] + [None]*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 = [None]*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+[None]*(newSize-capacity), newSize if self._count == 0: self._rear = capacity - 1
Methods
def capacity(self) ‑> int
-
Returns current capacity of the CircularArray.
Expand source code
def capacity(self) -> int: """Returns current capacity of the CircularArray.""" return self._capacity
def compact(self) ‑> None
-
Compact the CircularArray as much as possible.
Expand source code
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, [None]*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 copy(self) ‑> CircularArray
-
Return a shallow copy of the CircularArray.
Expand source code
def copy(self) -> CircularArray: """Return a shallow copy of the CircularArray.""" return CircularArray(*self)
def double(self) ‑> None
-
Double the capacity of the CircularArray.
Expand source code
def double(self) -> None: """Double the capacity of the CircularArray.""" if self._front <= self._rear: self._list += [None]*self._capacity self._capacity *= 2 else: self._list = self._list[:self._front] + [None]*self._capacity + self._list[self._front:] self._front += self._capacity self._capacity *= 2
def empty(self) ‑> None
-
Empty the CircularArray, keep current capacity.
Expand source code
def empty(self) -> None: """Empty the CircularArray, keep current capacity.""" self._list, self._front, self._rear = [None]*self._capacity, 0, self._capacity-1
def foldL(self, f: Callable[[Any, Any], Any], initial: Any = None) ‑> Any
-
Fold left with optional initial value. The first argument of
f
is the accumulated value. If CircularArray is empty and no initial value given, returnNone
.Expand source code
def foldL(self, f: Callable[[Any, Any], Any], initial: Any=None) -> Any: """Fold left with optional initial value. The first argument of `f` is the accumulated value. If CircularArray is empty and no initial value given, return `None`. """ if self._count == 0: return initial if initial is None: vs = iter(self) else: vs = chain((initial,), self) value = next(vs) for v in vs: value = f(value, v) return value
def foldR(self, f: Callable[[Any, Any], Any], initial: Any = None) ‑> Any
-
Fold right with optional initial value. The second argument of
f
is the accumulated value. If CircularArray is empty and no initial value given, returnNone
.Expand source code
def foldR(self, f: Callable[[Any, Any], Any], initial: Any=None) -> Any: """Fold right with optional initial value. The second argument of `f` is the accumulated value. If CircularArray is empty and no initial value given, return `None`. """ if self._count == 0: return initial if initial is None: vs = reversed(self) else: vs = chain((initial,), reversed(self)) value = next(vs) for v in vs: value = f(v, value) return value
def fractionFilled(self) ‑> float
-
Returns fractional capacity of the CircularArray.
Expand source code
def fractionFilled(self) -> float: """Returns fractional capacity of the CircularArray.""" return self._count/self._capacity
def map(self, f: Callable[[Any], Any]) ‑> CircularArray
-
Apply function f over the CircularArray's contents and return the results in a new CircularArray.
Expand source code
def map(self, f: Callable[[Any], Any]) -> CircularArray: """Apply function f over the CircularArray's contents and return the results in a new CircularArray. """ return CircularArray(*map(f, self))
def mapSelf(self, f: Callable[[Any], Any]) ‑> None
-
Apply function f over the CircularArray's contents mutating the CircularArray, does not return anything.
Expand source code
def mapSelf(self, f: Callable[[Any], Any]) -> None: """Apply function f over the CircularArray's contents mutating the CircularArray, does not return anything. """ ca = CircularArray(*map(f, self)) self._count, self._capacity, self._front, self._rear, self._list = \ ca._count, ca._capacity, ca._front, ca._rear, ca._list
def popL(self) ‑> Any
-
Pop data off the front of the CirclularArray, returns None if empty.
Expand source code
def popL(self) -> Any: """Pop data off the front of the CirclularArray, returns None if empty.""" if self._count == 0: return None else: value, self._count, self._list[self._front], self._front = \ self._list[self._front], self._count-1, None, (self._front+1) % self._capacity return value
def popR(self) ‑> Any
-
Pop data off the rear of the CirclularArray, returns None if empty.
Expand source code
def popR(self) -> Any: """Pop data off the rear of the CirclularArray, returns None if empty.""" if self._count == 0: return None else: value, self._count, self._list[self._rear], self._rear = \ self._list[self._rear], self._count-1, None, (self._rear - 1) % self._capacity return value
def pushL(self, value: Any) ‑> None
-
Push data onto the front of the CircularArray.
Expand source code
def pushL(self, value: Any) -> None: """Push data onto the front of the CircularArray.""" if self._count == self._capacity: self.double() self._front = (self._front - 1) % self._capacity self._list[self._front] = value self._count += 1
def pushR(self, value: Any) ‑> None
-
Push data onto the rear of the CircularArray.
Expand source code
def pushR(self, value: Any) -> None: """Push data onto the rear of the CircularArray.""" if self._count == self._capacity: self.double() self._rear = (self._rear + 1) % self._capacity self._list[self._rear] = value self._count += 1
def resize(self, newSize: int = 0) ‑> None
-
Compact CircularArray and resize to newSize if less than newSize.
Expand source code
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+[None]*(newSize-capacity), newSize if self._count == 0: self._rear = capacity - 1
def reverse(self) ‑> CircularArray
-
Expand source code
def reverse(self) -> CircularArray: return CircularArray(*reversed(self))