grscheller.circular_array.ca

Module for an indexable circular array data structure.

  1# Copyright 2023-2024 Geoffrey R. Scheller
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#     http://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""#### Module for an indexable circular array data structure."""
 16
 17from __future__ import annotations
 18from typing import Callable, cast, Generic, Iterator, Optional, TypeVar
 19
 20__all__ = ['CA']
 21
 22D = TypeVar('D')
 23T = TypeVar('T')
 24L = TypeVar('L')
 25R = TypeVar('R')
 26
 27class CA(Generic[D]):
 28    """#### Class implementing an indexable circular array data structure.
 29
 30    * generic, stateful data structure
 31    * amortized O(1) pushing and popping from either end
 32    * O(1) random access any element
 33    * will resize itself as needed
 34    * makes defensive copies of contents for the purposes of iteration
 35    * not sliceable
 36    * in boolean context returns true if not empty, false if empty
 37    * in comparisons will compare contents with identity before equality
 38      * like Python builtins like tuples, lists, and dicts do
 39    * raises `IndexError` for out-of-bounds indexing
 40    * raises `ValueError` for popping from or folding an empty CA
 41    """
 42    __slots__ = '_list', '_count', '_capacity', '_front', '_rear'
 43
 44    def __init__(self, *ds: D) -> None:
 45        self._list: list[D|None] = [None] + list(ds) + [None]
 46        self._capacity = capacity = len(self._list)
 47        self._count = capacity - 2
 48        if capacity == 2:
 49            self._front = 0
 50            self._rear = 1
 51        else:
 52            self._front = 1
 53            self._rear = capacity - 2
 54
 55    def __iter__(self) -> Iterator[D]:
 56        if self._count > 0:
 57            capacity,       rear,       position,    currentState = \
 58            self._capacity, self._rear, self._front, self._list.copy()
 59
 60            while position != rear:
 61                yield cast(D, currentState[position])  # will always yield a D
 62                position = (position + 1) % capacity
 63            yield cast(D, currentState[position])  # will always yield a D
 64
 65    def __reversed__(self) -> Iterator[D]:
 66        if self._count > 0:
 67            capacity,       front,       position,   currentState = \
 68            self._capacity, self._front, self._rear, self._list.copy()
 69
 70            while position != front:
 71                yield cast(D, currentState[position])  # will always yield a D
 72                position = (position - 1) % capacity
 73            yield cast(D, currentState[position])  # will always yield a D
 74
 75    def __repr__(self) -> str:
 76        return 'CA(' + ', '.join(map(repr, self)) + ')'
 77
 78    def __str__(self) -> str:
 79        return '(|' + ', '.join(map(str, self)) + '|)'
 80
 81    def __bool__(self) -> bool:
 82        return self._count > 0
 83
 84    def __len__(self) -> int:
 85        return self._count
 86
 87    def __getitem__(self, index: int) -> D:
 88        cnt = self._count
 89        if 0 <= index < cnt:
 90            return cast(D, self._list[(self._front + index)
 91                                       % self._capacity])  # will always return a D
 92        elif -cnt <= index < 0:
 93            return cast(D, self._list[(self._front + cnt + index)
 94                                       % self._capacity])  # will always return a D
 95        else:
 96            if cnt > 0:
 97                msg1 = 'Out of bounds: '
 98                msg2 = f'index = {index} not between {-cnt} and {cnt-1} '
 99                msg3 = 'while getting value from a CA.'
100                raise IndexError(msg1 + msg2 + msg3)
101            else:
102                msg0 = 'Trying to get a value from an empty CA.'
103                raise IndexError(msg0)
104
105    def __setitem__(self, index: int, value: D) -> None:
106        cnt = self._count
107        if 0 <= index < cnt:
108            self._list[(self._front + index) % self._capacity] = value
109        elif -cnt <= index < 0:
110            self._list[(self._front + cnt + index) % self._capacity] = value
111        else:
112            if cnt > 0:
113                msg1 = 'Out of bounds: '
114                msg2 = f'index = {index} not between {-cnt} and {cnt-1} '
115                msg3 = 'while setting value from a CA.'
116                raise IndexError(msg1 + msg2 + msg3)
117            else:
118                msg0 = 'Trying to set a value from an empty CA.'
119                raise IndexError(msg0)
120
121    def __eq__(self, other: object) -> bool:
122        if self is other:
123            return True
124        if not isinstance(other, type(self)):
125            return False
126
127        frontL,      capacityL,      countL,      frontR,       capacityR,       countR = \
128        self._front, self._capacity, self._count, other._front, other._capacity, other._count
129
130        if countL != countR:
131            return False
132
133        for nn in range(countL):
134            if self._list[(frontL+nn)%capacityL] is other._list[(frontR+nn)%capacityR]:
135                continue
136            if self._list[(frontL+nn)%capacityL] != other._list[(frontR+nn)%capacityR]:
137                return False
138        return True
139
140    def pushL(self, *ds: D) -> None:
141        """##### Push data from the left onto the CircularArray."""
142        for d in ds:
143            if self._count == self._capacity:
144                self.double()
145            self._front = (self._front - 1) % self._capacity
146            self._list[self._front] = d
147            self._count += 1
148
149    def pushR(self, *ds: D) -> None:
150        """##### Push data from the right onto the CircularArray."""
151        for d in ds:
152            if self._count == self._capacity:
153                self.double()
154            self._rear = (self._rear + 1) % self._capacity
155            self._list[self._rear] = d
156            self._count += 1
157
158    def popL(self) -> D:
159        """##### Pop one value off the left side of the CircularArray.
160
161            * raises `ValueError` when called on an empty CA"""
162        if self._count > 0:
163            d, \
164            self._list[self._front], \
165            self._front, \
166            self._count \
167                = \
168            self._list[self._front], \
169            None, \
170            (self._front+1) % self._capacity, \
171            self._count-1
172            return cast(D, d)  # will always yield a D
173        else:
174            msg = 'Method popL called on an empty CA'
175            raise ValueError(msg)
176
177    def popR(self) -> D:
178        """##### Pop one value off the right side of the CircularArray.
179
180            * raises `ValueError` when called on an empty CA
181        """
182        if self._count > 0:
183            d, \
184            self._list[self._rear], \
185            self._rear, \
186            self._count \
187                = \
188            self._list[self._rear], \
189            None, \
190            (self._rear - 1) % self._capacity, \
191            self._count-1
192            return cast(D, d)  # will always yield a D
193        else:
194            msg = 'Method popR called on an empty CA'
195            raise ValueError(msg)
196
197    def popLD(self, default: D) -> D:
198        """##### Pop one value from left, provide a mandatory default value.
199
200            * safe version of popL
201            * returns a default value in the event the `CA` is empty
202        """
203        try:
204            return self.popL()
205        except ValueError:
206            return default
207
208    def popRD(self, default: D) -> D:
209        """##### Pop one value from right, provide a mandatory default value.
210
211            * safe version of popR
212            * returns a default value in the event the `CA` is empty
213        """
214        try:
215            return self.popR()
216        except ValueError:
217            return default
218
219    def popLT(self, max: int=1) -> tuple[D, ...]:
220        """##### Pop multiple values from left side of CircularArray
221
222            * returns the results in a `tuple` of type `tuple[~D, ...]`
223            * returns an empty tuple if `CA` is empty
224            * pop no more that `max` values
225            * will pop less if `CA` becomes empty
226        """
227        ds: list[D] = []
228
229        while max > 0:
230            try:
231                ds.append(self.popL())
232            except ValueError:
233                break
234            else:
235                max -= 1
236
237        return tuple(ds)
238
239    def popRT(self, max: int=1) -> tuple[D, ...]:
240        """##### Pop multiple values from right side of CircularArray
241
242            * returns the results in a `tuple` of type `tuple[~D, ...]`
243            * returns an empty tuple if `CA` is empty
244            * pop no more that `max` values
245            * will pop less if `CA` becomes empty
246        """
247        ds: list[D] = []
248        while max > 0:
249            try:
250                ds.append(self.popR())
251            except ValueError:
252                break
253            else:
254                max -= 1
255
256        return tuple(ds)
257
258    def map(self, f: Callable[[D], T]) -> CA[T]:
259        """##### Apply function f over contents, returns new CircularArray instance.
260
261            * parameter `f` generic function of type `f[~D, ~T] -> CA[~T]`
262            * returns a new instance of type `CA[~T]``
263        """
264        return CA(*map(f, self))
265
266    def foldL(self, f: Callable[[L, D], L], initial: Optional[L]=None) -> L:
267        """##### Left fold CircularArray via function and optional initial value.
268
269            * parameter `f` generic function of type `f[~L, ~D] -> ~L`
270              * the first argument to `f` is for the accumulated value.
271            * parameter `initial` is an optional initial value
272              * note that if not given then it will be the case that `~L = ~D`
273            * returns the reduced value of type `~L`
274              * note that `~L` and `~D` can be the same type
275              * if an initial value is not given then by necessity `~L = ~D` 
276            * raises `ValueError` when called on an empty `CA` and `initial` not given
277        """
278        if self._count == 0:
279            if initial is None:
280                msg = 'Method foldL called on an empty CA without an initial value.'
281                raise ValueError(msg)
282            else:
283                return initial
284        else:
285            if initial is None:
286                acc = cast(L, self[0])  # in this case D = L
287                for idx in range(1, self._count):
288                    acc = f(acc, self[idx])
289                return acc
290            else:
291                acc = initial
292                for d in self:
293                    acc = f(acc, d)
294                return acc
295
296    def foldR(self, f: Callable[[D, R], R], initial: Optional[R]=None) -> R:
297        """##### Right fold CircularArray via function and optional initial value.
298
299            * generic function `f` of type `f[~D, ~R] -> ~R`
300              * the second argument to f is for the accumulated value
301            * parameter `initial` is an optional initial value
302              * note that if not given then it will be the case that `~R = ~D`
303            * returns the reduced value of type `~R`
304              * note that `~R` and `~D` can be the same type
305              * if `initial` is not given then by necessity `~R = ~D`
306            * raises `ValueError` when called on an empty `CA` and `initial` not given
307        """
308        if self._count == 0:
309            if initial is None:
310                msg = 'Method foldR called on an empty CA without an initial value.'
311                raise ValueError(msg)
312            else:
313                return initial
314        else:
315            if initial is None:
316                acc = cast(R, self[-1])  # in this case D = R
317                for idx in range(self._count-2, -1, -1):
318                    acc = f(self[idx], acc)
319                return acc
320            else:
321                acc = initial
322                for d in reversed(self):
323                    acc = f(d, acc)
324                return acc
325
326    def capacity(self) -> int:
327        """##### Returns current capacity of the CircularArray."""
328        return self._capacity
329
330    def compact(self) -> None:
331        """##### Compact the CircularArray."""
332        match self._count:
333            case 0:
334                self._capacity, self._front, self._rear, self._list = \
335                2,              0,           1,          [None, None]
336            case 1:
337                self._capacity, self._front, self._rear, self._list = \
338                3,              1,           1,          [None, self._list[self._front], None]
339            case _:
340                if self._front <= self._rear:
341                    self._capacity, self._front, self._rear,  self._list = \
342                    self._count+2,  1,           self._count, \
343                    [None] + self._list[self._front:self._rear+1] + [None]
344                else:
345                    self._capacity, self._front, self._rear,  self._list = \
346                    self._count+2,  1,           self._count, [None] \
347                        + self._list[self._front:] + self._list[:self._rear+1] \
348                        + [None]
349
350    def double(self) -> None:
351        """##### Double the capacity of the CircularArray."""
352        if self._front <= self._rear:
353            self._list += [None]*self._capacity
354            self._capacity *= 2
355        else:
356            self._list = self._list[:self._front] + [None]*self._capacity + self._list[self._front:]
357            self._front += self._capacity
358            self._capacity *= 2
359
360    def empty(self) -> None:
361        """##### Empty the CircularArray, keep current capacity."""
362        self._list, self._front, self._rear = [None]*self._capacity, 0, self._capacity-1
363
364    def fractionFilled(self) -> float:
365        """##### Returns fractional capacity of the CircularArray."""
366        return self._count/self._capacity
367
368    def resize(self, newSize: int= 0) -> None:
369        """##### Compact CircularArray and resize to newSize if less than newSize."""
370        self.compact()
371        capacity = self._capacity
372        if newSize > capacity:
373            self._list, self._capacity = self._list+[None]*(newSize-capacity), newSize
374            if self._count == 0:
375                self._rear = capacity - 1
class CA(typing.Generic[~D]):
 28class CA(Generic[D]):
 29    """#### Class implementing an indexable circular array data structure.
 30
 31    * generic, stateful data structure
 32    * amortized O(1) pushing and popping from either end
 33    * O(1) random access any element
 34    * will resize itself as needed
 35    * makes defensive copies of contents for the purposes of iteration
 36    * not sliceable
 37    * in boolean context returns true if not empty, false if empty
 38    * in comparisons will compare contents with identity before equality
 39      * like Python builtins like tuples, lists, and dicts do
 40    * raises `IndexError` for out-of-bounds indexing
 41    * raises `ValueError` for popping from or folding an empty CA
 42    """
 43    __slots__ = '_list', '_count', '_capacity', '_front', '_rear'
 44
 45    def __init__(self, *ds: D) -> None:
 46        self._list: list[D|None] = [None] + list(ds) + [None]
 47        self._capacity = capacity = len(self._list)
 48        self._count = capacity - 2
 49        if capacity == 2:
 50            self._front = 0
 51            self._rear = 1
 52        else:
 53            self._front = 1
 54            self._rear = capacity - 2
 55
 56    def __iter__(self) -> Iterator[D]:
 57        if self._count > 0:
 58            capacity,       rear,       position,    currentState = \
 59            self._capacity, self._rear, self._front, self._list.copy()
 60
 61            while position != rear:
 62                yield cast(D, currentState[position])  # will always yield a D
 63                position = (position + 1) % capacity
 64            yield cast(D, currentState[position])  # will always yield a D
 65
 66    def __reversed__(self) -> Iterator[D]:
 67        if self._count > 0:
 68            capacity,       front,       position,   currentState = \
 69            self._capacity, self._front, self._rear, self._list.copy()
 70
 71            while position != front:
 72                yield cast(D, currentState[position])  # will always yield a D
 73                position = (position - 1) % capacity
 74            yield cast(D, currentState[position])  # will always yield a D
 75
 76    def __repr__(self) -> str:
 77        return 'CA(' + ', '.join(map(repr, self)) + ')'
 78
 79    def __str__(self) -> str:
 80        return '(|' + ', '.join(map(str, self)) + '|)'
 81
 82    def __bool__(self) -> bool:
 83        return self._count > 0
 84
 85    def __len__(self) -> int:
 86        return self._count
 87
 88    def __getitem__(self, index: int) -> D:
 89        cnt = self._count
 90        if 0 <= index < cnt:
 91            return cast(D, self._list[(self._front + index)
 92                                       % self._capacity])  # will always return a D
 93        elif -cnt <= index < 0:
 94            return cast(D, self._list[(self._front + cnt + index)
 95                                       % self._capacity])  # will always return a D
 96        else:
 97            if cnt > 0:
 98                msg1 = 'Out of bounds: '
 99                msg2 = f'index = {index} not between {-cnt} and {cnt-1} '
100                msg3 = 'while getting value from a CA.'
101                raise IndexError(msg1 + msg2 + msg3)
102            else:
103                msg0 = 'Trying to get a value from an empty CA.'
104                raise IndexError(msg0)
105
106    def __setitem__(self, index: int, value: D) -> None:
107        cnt = self._count
108        if 0 <= index < cnt:
109            self._list[(self._front + index) % self._capacity] = value
110        elif -cnt <= index < 0:
111            self._list[(self._front + cnt + index) % self._capacity] = value
112        else:
113            if cnt > 0:
114                msg1 = 'Out of bounds: '
115                msg2 = f'index = {index} not between {-cnt} and {cnt-1} '
116                msg3 = 'while setting value from a CA.'
117                raise IndexError(msg1 + msg2 + msg3)
118            else:
119                msg0 = 'Trying to set a value from an empty CA.'
120                raise IndexError(msg0)
121
122    def __eq__(self, other: object) -> bool:
123        if self is other:
124            return True
125        if not isinstance(other, type(self)):
126            return False
127
128        frontL,      capacityL,      countL,      frontR,       capacityR,       countR = \
129        self._front, self._capacity, self._count, other._front, other._capacity, other._count
130
131        if countL != countR:
132            return False
133
134        for nn in range(countL):
135            if self._list[(frontL+nn)%capacityL] is other._list[(frontR+nn)%capacityR]:
136                continue
137            if self._list[(frontL+nn)%capacityL] != other._list[(frontR+nn)%capacityR]:
138                return False
139        return True
140
141    def pushL(self, *ds: D) -> None:
142        """##### Push data from the left onto the CircularArray."""
143        for d in ds:
144            if self._count == self._capacity:
145                self.double()
146            self._front = (self._front - 1) % self._capacity
147            self._list[self._front] = d
148            self._count += 1
149
150    def pushR(self, *ds: D) -> None:
151        """##### Push data from the right onto the CircularArray."""
152        for d in ds:
153            if self._count == self._capacity:
154                self.double()
155            self._rear = (self._rear + 1) % self._capacity
156            self._list[self._rear] = d
157            self._count += 1
158
159    def popL(self) -> D:
160        """##### Pop one value off the left side of the CircularArray.
161
162            * raises `ValueError` when called on an empty CA"""
163        if self._count > 0:
164            d, \
165            self._list[self._front], \
166            self._front, \
167            self._count \
168                = \
169            self._list[self._front], \
170            None, \
171            (self._front+1) % self._capacity, \
172            self._count-1
173            return cast(D, d)  # will always yield a D
174        else:
175            msg = 'Method popL called on an empty CA'
176            raise ValueError(msg)
177
178    def popR(self) -> D:
179        """##### Pop one value off the right side of the CircularArray.
180
181            * raises `ValueError` when called on an empty CA
182        """
183        if self._count > 0:
184            d, \
185            self._list[self._rear], \
186            self._rear, \
187            self._count \
188                = \
189            self._list[self._rear], \
190            None, \
191            (self._rear - 1) % self._capacity, \
192            self._count-1
193            return cast(D, d)  # will always yield a D
194        else:
195            msg = 'Method popR called on an empty CA'
196            raise ValueError(msg)
197
198    def popLD(self, default: D) -> D:
199        """##### Pop one value from left, provide a mandatory default value.
200
201            * safe version of popL
202            * returns a default value in the event the `CA` is empty
203        """
204        try:
205            return self.popL()
206        except ValueError:
207            return default
208
209    def popRD(self, default: D) -> D:
210        """##### Pop one value from right, provide a mandatory default value.
211
212            * safe version of popR
213            * returns a default value in the event the `CA` is empty
214        """
215        try:
216            return self.popR()
217        except ValueError:
218            return default
219
220    def popLT(self, max: int=1) -> tuple[D, ...]:
221        """##### Pop multiple values from left side of CircularArray
222
223            * returns the results in a `tuple` of type `tuple[~D, ...]`
224            * returns an empty tuple if `CA` is empty
225            * pop no more that `max` values
226            * will pop less if `CA` becomes empty
227        """
228        ds: list[D] = []
229
230        while max > 0:
231            try:
232                ds.append(self.popL())
233            except ValueError:
234                break
235            else:
236                max -= 1
237
238        return tuple(ds)
239
240    def popRT(self, max: int=1) -> tuple[D, ...]:
241        """##### Pop multiple values from right side of CircularArray
242
243            * returns the results in a `tuple` of type `tuple[~D, ...]`
244            * returns an empty tuple if `CA` is empty
245            * pop no more that `max` values
246            * will pop less if `CA` becomes empty
247        """
248        ds: list[D] = []
249        while max > 0:
250            try:
251                ds.append(self.popR())
252            except ValueError:
253                break
254            else:
255                max -= 1
256
257        return tuple(ds)
258
259    def map(self, f: Callable[[D], T]) -> CA[T]:
260        """##### Apply function f over contents, returns new CircularArray instance.
261
262            * parameter `f` generic function of type `f[~D, ~T] -> CA[~T]`
263            * returns a new instance of type `CA[~T]``
264        """
265        return CA(*map(f, self))
266
267    def foldL(self, f: Callable[[L, D], L], initial: Optional[L]=None) -> L:
268        """##### Left fold CircularArray via function and optional initial value.
269
270            * parameter `f` generic function of type `f[~L, ~D] -> ~L`
271              * the first argument to `f` is for the accumulated value.
272            * parameter `initial` is an optional initial value
273              * note that if not given then it will be the case that `~L = ~D`
274            * returns the reduced value of type `~L`
275              * note that `~L` and `~D` can be the same type
276              * if an initial value is not given then by necessity `~L = ~D` 
277            * raises `ValueError` when called on an empty `CA` and `initial` not given
278        """
279        if self._count == 0:
280            if initial is None:
281                msg = 'Method foldL called on an empty CA without an initial value.'
282                raise ValueError(msg)
283            else:
284                return initial
285        else:
286            if initial is None:
287                acc = cast(L, self[0])  # in this case D = L
288                for idx in range(1, self._count):
289                    acc = f(acc, self[idx])
290                return acc
291            else:
292                acc = initial
293                for d in self:
294                    acc = f(acc, d)
295                return acc
296
297    def foldR(self, f: Callable[[D, R], R], initial: Optional[R]=None) -> R:
298        """##### Right fold CircularArray via function and optional initial value.
299
300            * generic function `f` of type `f[~D, ~R] -> ~R`
301              * the second argument to f is for the accumulated value
302            * parameter `initial` is an optional initial value
303              * note that if not given then it will be the case that `~R = ~D`
304            * returns the reduced value of type `~R`
305              * note that `~R` and `~D` can be the same type
306              * if `initial` is not given then by necessity `~R = ~D`
307            * raises `ValueError` when called on an empty `CA` and `initial` not given
308        """
309        if self._count == 0:
310            if initial is None:
311                msg = 'Method foldR called on an empty CA without an initial value.'
312                raise ValueError(msg)
313            else:
314                return initial
315        else:
316            if initial is None:
317                acc = cast(R, self[-1])  # in this case D = R
318                for idx in range(self._count-2, -1, -1):
319                    acc = f(self[idx], acc)
320                return acc
321            else:
322                acc = initial
323                for d in reversed(self):
324                    acc = f(d, acc)
325                return acc
326
327    def capacity(self) -> int:
328        """##### Returns current capacity of the CircularArray."""
329        return self._capacity
330
331    def compact(self) -> None:
332        """##### Compact the CircularArray."""
333        match self._count:
334            case 0:
335                self._capacity, self._front, self._rear, self._list = \
336                2,              0,           1,          [None, None]
337            case 1:
338                self._capacity, self._front, self._rear, self._list = \
339                3,              1,           1,          [None, self._list[self._front], None]
340            case _:
341                if self._front <= self._rear:
342                    self._capacity, self._front, self._rear,  self._list = \
343                    self._count+2,  1,           self._count, \
344                    [None] + self._list[self._front:self._rear+1] + [None]
345                else:
346                    self._capacity, self._front, self._rear,  self._list = \
347                    self._count+2,  1,           self._count, [None] \
348                        + self._list[self._front:] + self._list[:self._rear+1] \
349                        + [None]
350
351    def double(self) -> None:
352        """##### Double the capacity of the CircularArray."""
353        if self._front <= self._rear:
354            self._list += [None]*self._capacity
355            self._capacity *= 2
356        else:
357            self._list = self._list[:self._front] + [None]*self._capacity + self._list[self._front:]
358            self._front += self._capacity
359            self._capacity *= 2
360
361    def empty(self) -> None:
362        """##### Empty the CircularArray, keep current capacity."""
363        self._list, self._front, self._rear = [None]*self._capacity, 0, self._capacity-1
364
365    def fractionFilled(self) -> float:
366        """##### Returns fractional capacity of the CircularArray."""
367        return self._count/self._capacity
368
369    def resize(self, newSize: int= 0) -> None:
370        """##### Compact CircularArray and resize to newSize if less than newSize."""
371        self.compact()
372        capacity = self._capacity
373        if newSize > capacity:
374            self._list, self._capacity = self._list+[None]*(newSize-capacity), newSize
375            if self._count == 0:
376                self._rear = capacity - 1

Class implementing an indexable circular array data structure.

  • generic, stateful data structure
  • amortized O(1) pushing and popping from either end
  • O(1) random access any element
  • will resize itself as needed
  • makes defensive copies of contents for the purposes of iteration
  • not sliceable
  • in boolean context returns true if not empty, false if empty
  • in comparisons will compare contents with identity before equality
    • like Python builtins like tuples, lists, and dicts do
  • raises IndexError for out-of-bounds indexing
  • raises ValueError for popping from or folding an empty CA
CA(*ds: ~D)
45    def __init__(self, *ds: D) -> None:
46        self._list: list[D|None] = [None] + list(ds) + [None]
47        self._capacity = capacity = len(self._list)
48        self._count = capacity - 2
49        if capacity == 2:
50            self._front = 0
51            self._rear = 1
52        else:
53            self._front = 1
54            self._rear = capacity - 2
def pushL(self, *ds: ~D) -> None:
141    def pushL(self, *ds: D) -> None:
142        """##### Push data from the left onto the CircularArray."""
143        for d in ds:
144            if self._count == self._capacity:
145                self.double()
146            self._front = (self._front - 1) % self._capacity
147            self._list[self._front] = d
148            self._count += 1
Push data from the left onto the CircularArray.
def pushR(self, *ds: ~D) -> None:
150    def pushR(self, *ds: D) -> None:
151        """##### Push data from the right onto the CircularArray."""
152        for d in ds:
153            if self._count == self._capacity:
154                self.double()
155            self._rear = (self._rear + 1) % self._capacity
156            self._list[self._rear] = d
157            self._count += 1
Push data from the right onto the CircularArray.
def popL(self) -> ~D:
159    def popL(self) -> D:
160        """##### Pop one value off the left side of the CircularArray.
161
162            * raises `ValueError` when called on an empty CA"""
163        if self._count > 0:
164            d, \
165            self._list[self._front], \
166            self._front, \
167            self._count \
168                = \
169            self._list[self._front], \
170            None, \
171            (self._front+1) % self._capacity, \
172            self._count-1
173            return cast(D, d)  # will always yield a D
174        else:
175            msg = 'Method popL called on an empty CA'
176            raise ValueError(msg)
Pop one value off the left side of the CircularArray.
  • raises ValueError when called on an empty CA
def popR(self) -> ~D:
178    def popR(self) -> D:
179        """##### Pop one value off the right side of the CircularArray.
180
181            * raises `ValueError` when called on an empty CA
182        """
183        if self._count > 0:
184            d, \
185            self._list[self._rear], \
186            self._rear, \
187            self._count \
188                = \
189            self._list[self._rear], \
190            None, \
191            (self._rear - 1) % self._capacity, \
192            self._count-1
193            return cast(D, d)  # will always yield a D
194        else:
195            msg = 'Method popR called on an empty CA'
196            raise ValueError(msg)
Pop one value off the right side of the CircularArray.
  • raises ValueError when called on an empty CA
def popLD(self, default: ~D) -> ~D:
198    def popLD(self, default: D) -> D:
199        """##### Pop one value from left, provide a mandatory default value.
200
201            * safe version of popL
202            * returns a default value in the event the `CA` is empty
203        """
204        try:
205            return self.popL()
206        except ValueError:
207            return default
Pop one value from left, provide a mandatory default value.
  • safe version of popL
  • returns a default value in the event the CA is empty
def popRD(self, default: ~D) -> ~D:
209    def popRD(self, default: D) -> D:
210        """##### Pop one value from right, provide a mandatory default value.
211
212            * safe version of popR
213            * returns a default value in the event the `CA` is empty
214        """
215        try:
216            return self.popR()
217        except ValueError:
218            return default
Pop one value from right, provide a mandatory default value.
  • safe version of popR
  • returns a default value in the event the CA is empty
def popLT(self, max: int = 1) -> tuple[~D, ...]:
220    def popLT(self, max: int=1) -> tuple[D, ...]:
221        """##### Pop multiple values from left side of CircularArray
222
223            * returns the results in a `tuple` of type `tuple[~D, ...]`
224            * returns an empty tuple if `CA` is empty
225            * pop no more that `max` values
226            * will pop less if `CA` becomes empty
227        """
228        ds: list[D] = []
229
230        while max > 0:
231            try:
232                ds.append(self.popL())
233            except ValueError:
234                break
235            else:
236                max -= 1
237
238        return tuple(ds)
Pop multiple values from left side of CircularArray
  • returns the results in a tuple of type tuple[~D, ...]
  • returns an empty tuple if CA is empty
  • pop no more that max values
  • will pop less if CA becomes empty
def popRT(self, max: int = 1) -> tuple[~D, ...]:
240    def popRT(self, max: int=1) -> tuple[D, ...]:
241        """##### Pop multiple values from right side of CircularArray
242
243            * returns the results in a `tuple` of type `tuple[~D, ...]`
244            * returns an empty tuple if `CA` is empty
245            * pop no more that `max` values
246            * will pop less if `CA` becomes empty
247        """
248        ds: list[D] = []
249        while max > 0:
250            try:
251                ds.append(self.popR())
252            except ValueError:
253                break
254            else:
255                max -= 1
256
257        return tuple(ds)
Pop multiple values from right side of CircularArray
  • returns the results in a tuple of type tuple[~D, ...]
  • returns an empty tuple if CA is empty
  • pop no more that max values
  • will pop less if CA becomes empty
def map(self, f: Callable[[~D], ~T]) -> CA[~T]:
259    def map(self, f: Callable[[D], T]) -> CA[T]:
260        """##### Apply function f over contents, returns new CircularArray instance.
261
262            * parameter `f` generic function of type `f[~D, ~T] -> CA[~T]`
263            * returns a new instance of type `CA[~T]``
264        """
265        return CA(*map(f, self))
Apply function f over contents, returns new CircularArray instance.
  • parameter f generic function of type f[~D, ~T] -> CA[~T]
  • returns a new instance of type `CA[~T]``
def foldL(self, f: Callable[[~L, ~D], ~L], initial: Optional[~L] = None) -> ~L:
267    def foldL(self, f: Callable[[L, D], L], initial: Optional[L]=None) -> L:
268        """##### Left fold CircularArray via function and optional initial value.
269
270            * parameter `f` generic function of type `f[~L, ~D] -> ~L`
271              * the first argument to `f` is for the accumulated value.
272            * parameter `initial` is an optional initial value
273              * note that if not given then it will be the case that `~L = ~D`
274            * returns the reduced value of type `~L`
275              * note that `~L` and `~D` can be the same type
276              * if an initial value is not given then by necessity `~L = ~D` 
277            * raises `ValueError` when called on an empty `CA` and `initial` not given
278        """
279        if self._count == 0:
280            if initial is None:
281                msg = 'Method foldL called on an empty CA without an initial value.'
282                raise ValueError(msg)
283            else:
284                return initial
285        else:
286            if initial is None:
287                acc = cast(L, self[0])  # in this case D = L
288                for idx in range(1, self._count):
289                    acc = f(acc, self[idx])
290                return acc
291            else:
292                acc = initial
293                for d in self:
294                    acc = f(acc, d)
295                return acc
Left fold CircularArray via function and optional initial value.
  • parameter f generic function of type f[~L, ~D] -> ~L
    • the first argument to f is for the accumulated value.
  • parameter initial is an optional initial value
    • note that if not given then it will be the case that ~L = ~D
  • returns the reduced value of type ~L
    • note that ~L and ~D can be the same type
    • if an initial value is not given then by necessity ~L = ~D
  • raises ValueError when called on an empty CA and initial not given
def foldR(self, f: Callable[[~D, ~R], ~R], initial: Optional[~R] = None) -> ~R:
297    def foldR(self, f: Callable[[D, R], R], initial: Optional[R]=None) -> R:
298        """##### Right fold CircularArray via function and optional initial value.
299
300            * generic function `f` of type `f[~D, ~R] -> ~R`
301              * the second argument to f is for the accumulated value
302            * parameter `initial` is an optional initial value
303              * note that if not given then it will be the case that `~R = ~D`
304            * returns the reduced value of type `~R`
305              * note that `~R` and `~D` can be the same type
306              * if `initial` is not given then by necessity `~R = ~D`
307            * raises `ValueError` when called on an empty `CA` and `initial` not given
308        """
309        if self._count == 0:
310            if initial is None:
311                msg = 'Method foldR called on an empty CA without an initial value.'
312                raise ValueError(msg)
313            else:
314                return initial
315        else:
316            if initial is None:
317                acc = cast(R, self[-1])  # in this case D = R
318                for idx in range(self._count-2, -1, -1):
319                    acc = f(self[idx], acc)
320                return acc
321            else:
322                acc = initial
323                for d in reversed(self):
324                    acc = f(d, acc)
325                return acc
Right fold CircularArray via function and optional initial value.
  • generic function f of type f[~D, ~R] -> ~R
    • the second argument to f is for the accumulated value
  • parameter initial is an optional initial value
    • note that if not given then it will be the case that ~R = ~D
  • returns the reduced value of type ~R
    • note that ~R and ~D can be the same type
    • if initial is not given then by necessity ~R = ~D
  • raises ValueError when called on an empty CA and initial not given
def capacity(self) -> int:
327    def capacity(self) -> int:
328        """##### Returns current capacity of the CircularArray."""
329        return self._capacity
Returns current capacity of the CircularArray.
def compact(self) -> None:
331    def compact(self) -> None:
332        """##### Compact the CircularArray."""
333        match self._count:
334            case 0:
335                self._capacity, self._front, self._rear, self._list = \
336                2,              0,           1,          [None, None]
337            case 1:
338                self._capacity, self._front, self._rear, self._list = \
339                3,              1,           1,          [None, self._list[self._front], None]
340            case _:
341                if self._front <= self._rear:
342                    self._capacity, self._front, self._rear,  self._list = \
343                    self._count+2,  1,           self._count, \
344                    [None] + self._list[self._front:self._rear+1] + [None]
345                else:
346                    self._capacity, self._front, self._rear,  self._list = \
347                    self._count+2,  1,           self._count, [None] \
348                        + self._list[self._front:] + self._list[:self._rear+1] \
349                        + [None]
Compact the CircularArray.
def double(self) -> None:
351    def double(self) -> None:
352        """##### Double the capacity of the CircularArray."""
353        if self._front <= self._rear:
354            self._list += [None]*self._capacity
355            self._capacity *= 2
356        else:
357            self._list = self._list[:self._front] + [None]*self._capacity + self._list[self._front:]
358            self._front += self._capacity
359            self._capacity *= 2
Double the capacity of the CircularArray.
def empty(self) -> None:
361    def empty(self) -> None:
362        """##### Empty the CircularArray, keep current capacity."""
363        self._list, self._front, self._rear = [None]*self._capacity, 0, self._capacity-1
Empty the CircularArray, keep current capacity.
def fractionFilled(self) -> float:
365    def fractionFilled(self) -> float:
366        """##### Returns fractional capacity of the CircularArray."""
367        return self._count/self._capacity
Returns fractional capacity of the CircularArray.
def resize(self, newSize: int = 0) -> None:
369    def resize(self, newSize: int= 0) -> None:
370        """##### Compact CircularArray and resize to newSize if less than newSize."""
371        self.compact()
372        capacity = self._capacity
373        if newSize > capacity:
374            self._list, self._capacity = self._list+[None]*(newSize-capacity), newSize
375            if self._count == 0:
376                self._rear = capacity - 1
Compact CircularArray and resize to newSize if less than newSize.