dtools.circular_array.ca

Indexable circular array data structure module.

  1# Copyright 2023-2025 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"""### Indexable circular array data structure module."""
 16from __future__ import annotations
 17from collections.abc import Callable, Iterable, Iterator, Sequence
 18from typing import Any, cast, Never, overload, TypeVar
 19
 20__all__ = [ 'ca', 'CA' ]
 21
 22D = TypeVar('D')
 23L = TypeVar('L')
 24R = TypeVar('R')
 25U = TypeVar('U')
 26
 27class ca[D](Sequence[D]):
 28    """
 29    #### 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    * sliceable
 36    * makes defensive copies of contents for the purposes of iteration
 37    * in boolean context returns true if not empty, false if empty
 38    * in comparisons compare identity before equality (like Python built-ins do)
 39    * lowercase class name choosen to match built-ins like `list` and `tuple`
 40    * raises `IndexError` for out-of-bounds indexing
 41    * raises `ValueError` for popping from or folding an empty `ca`
 42    * raises `TypeError` if 2 or more arguments are passed to constructor
 43
 44    """
 45    __slots__ = '_data', '_cnt', '_cap', '_front', '_rear'
 46
 47    def __init__(self, *dss: Iterable[D]) -> None:
 48        if len(dss) < 2:
 49            self._data: list[D|None] = [None] + cast(list[D|None], list(*dss)) + [None]
 50        else:
 51            msg = f'ca expected at most 1 argument, got {len(dss)}'
 52            raise TypeError(msg)
 53        self._cap = cap = len(self._data)
 54        self._cnt = cap - 2
 55        if cap == 2:
 56            self._front = 0
 57            self._rear = 1
 58        else:
 59            self._front = 1
 60            self._rear = cap - 2
 61
 62    def _double_storage_capacity(self) -> None:
 63        if self._front <= self._rear:
 64            self._data += [None]*self._cap
 65            self._cap *= 2
 66        else:
 67            self._data = self._data[:self._front] + [None]*self._cap + self._data[self._front:]
 68            self._front, self._cap = self._front + self._cap, 2*self._cap
 69
 70    def _compact_storage_capacity(self) -> None:
 71        """Compact the ca."""
 72        match self._cnt:
 73            case 0:
 74                self._cap, self._front, self._rear, self._data = \
 75                    2, 0, 1, [None, None]
 76            case 1:
 77                self._cap, self._front, self._rear, self._data = \
 78                    3, 1, 1, [None, self._data[self._front], None]
 79            case _:
 80                if self._front <= self._rear:
 81                    self._cap, self._front, self._rear, self._data = \
 82                        self._cnt+2, 1, self._cnt, [None] + self._data[self._front:self._rear+1] + [None]
 83                else:
 84                    self._cap, self._front, self._rear, self._data = \
 85                        self._cnt+2, 1, self._cnt, [None] + self._data[self._front:] + self._data[:self._rear+1] + [None]
 86
 87    def __iter__(self) -> Iterator[D]:
 88        if self._cnt > 0:
 89            capacity, rear, position, current_state = \
 90                self._cap, self._rear, self._front, self._data.copy()
 91
 92            while position != rear:
 93                yield cast(D, current_state[position])
 94                position = (position + 1) % capacity
 95            yield cast(D, current_state[position])
 96
 97    def __reversed__(self) -> Iterator[D]:
 98        if self._cnt > 0:
 99            capacity, front, position, current_state = \
100                self._cap, self._front, self._rear, self._data.copy()
101
102            while position != front:
103                yield cast(D, current_state[position])
104                position = (position - 1) % capacity
105            yield cast(D, current_state[position])
106
107    def __repr__(self) -> str:
108        return 'CA(' + ', '.join(map(repr, self)) + ')'
109
110    def __str__(self) -> str:
111        return '(|' + ', '.join(map(str, self)) + '|)'
112
113    def __bool__(self) -> bool:
114        return self._cnt > 0
115
116    def __len__(self) -> int:
117        return self._cnt
118
119    @overload
120    def __getitem__(self, idx: int, /) -> D: ...
121    @overload
122    def __getitem__(self, idx: slice, /) -> ca[D]: ...
123
124    def __getitem__(self, idx: int|slice, /) -> D|ca[D]:
125        if isinstance(idx, slice):
126            return ca(list(self)[idx])
127
128        cnt = self._cnt
129        if 0 <= idx < cnt:
130            return cast(D, self._data[(self._front + idx) % self._cap])
131        elif -cnt <= idx < 0:
132            return cast(D, self._data[(self._front + cnt + idx) % self._cap])
133        else:
134            if cnt > 0:
135                foo = [1, 2, 3]
136                foo.__setitem__
137                msg1 = 'Out of bounds: '
138                msg2 = f'index = {idx} not between {-cnt} and {cnt-1} '
139                msg3 = 'while getting value from a ca.'
140                raise IndexError(msg1 + msg2 + msg3)
141            else:
142                msg0 = 'Trying to get a value from an empty ca.'
143                raise IndexError(msg0)
144
145    @overload
146    def __setitem__(self, idx: int, vals: D, /) -> None: ...
147    @overload
148    def __setitem__(self, idx: slice, vals: Iterable[D], /) -> None: ...
149
150    def __setitem__(self, idx: int|slice, vals: D|Iterable[D], /) -> None:
151        if isinstance(idx, slice):
152            if isinstance(vals, Iterable):
153                data = list(self)
154                data[idx] = vals
155                _ca = ca(data)
156                self._data, self._cnt, self._cap, self._front, self._rear = \
157                            _ca._data, _ca._cnt, _ca._cap, _ca._front, _ca._rear
158                return
159            else:
160                msg = 'must assign iterable to extended slice'
161                foo = [1,2,3]
162                foo.__delitem__(2)
163                raise TypeError(msg)
164
165        cnt = self._cnt
166        if 0 <= idx < cnt:
167            self._data[(self._front + idx) % self._cap] = cast(D, vals)
168        elif -cnt <= idx < 0:
169            self._data[(self._front + cnt + idx) % self._cap] = cast(D, vals)
170        else:
171            if cnt > 0:
172                msg1 = 'Out of bounds: '
173                msg2 = f'index = {idx} not between {-cnt} and {cnt-1} '
174                msg3 = 'while setting value from a ca.'
175                raise IndexError(msg1 + msg2 + msg3)
176            else:
177                msg0 = 'Trying to set a value from an empty ca.'
178                raise IndexError(msg0)
179
180    @overload
181    def __delitem__(self, idx: int) -> None: ...
182    @overload
183    def __delitem__(self, idx: slice) -> None: ...
184
185    def __delitem__(self, idx: int|slice) -> None:
186        data = list(self)
187        del data[idx]
188        _ca = ca(data)
189        self._data, self._cnt, self._cap, self._front, self._rear = \
190                    _ca._data, _ca._cnt, _ca._cap, _ca._front, _ca._rear
191        return
192
193    def __eq__(self, other: object, /) -> bool:
194        if self is other:
195            return True
196        if not isinstance(other, type(self)):
197            return False
198
199        frontL, frontR, \
200        countL, countR, \
201        capacityL, capacityR = \
202            self._front, other._front, \
203            self._cnt, other._cnt, \
204            self._cap, other._cap
205
206        if countL != countR:
207            return False
208
209        for nn in range(countL):
210            if self._data[(frontL+nn)%capacityL] is other._data[(frontR+nn)%capacityR]:
211                continue
212            if self._data[(frontL+nn)%capacityL] != other._data[(frontR+nn)%capacityR]:
213                return False
214        return True
215
216    def pushL(self, *ds: D) -> None:
217        """Push data from the left onto the ca."""
218        for d in ds:
219            if self._cnt == self._cap:
220                self._double_storage_capacity()
221            self._front = (self._front - 1) % self._cap
222            self._data[self._front], self._cnt = d, self._cnt + 1
223
224    def pushR(self, *ds: D) -> None:
225        """Push data from the right onto the ca."""
226        for d in ds:
227            if self._cnt == self._cap:
228                self._double_storage_capacity()
229            self._rear = (self._rear + 1) % self._cap
230            self._data[self._rear], self._cnt = d, self._cnt + 1
231
232    def popL(self) -> D|Never:
233        """Pop one value off the left side of the ca.
234
235        * raises `ValueError` when called on an empty ca
236        """
237        if self._cnt > 1:
238            d, self._data[self._front], self._front, self._cnt = \
239                self._data[self._front], None, (self._front+1) % self._cap, self._cnt - 1
240        elif self._cnt < 1:
241            msg = 'Method popL called on an empty ca'
242            raise ValueError(msg)
243        else:
244            d, self._data[self._front], self._cnt, self._front, self._rear = \
245                self._data[self._front], None, 0, 0, self._cap - 1
246        return cast(D, d)
247
248    def popR(self) -> D|Never:
249        """Pop one value off the right side of the ca.
250
251        * raises `ValueError` when called on an empty ca
252        """
253        if self._cnt > 0:
254            d, self._data[self._rear], self._rear, self._cnt = \
255                self._data[self._rear], None, (self._rear - 1) % self._cap, self._cnt - 1
256        elif self._cnt < 1:
257            msg = 'Method popR called on an empty ca'
258            raise ValueError(msg)
259        else:
260            d, self._data[self._front], self._cnt, self._front, self._rear = \
261                self._data[self._front], None, 0, 0, self._cap - 1
262        return cast(D, d)
263
264    def popLD(self, default: D, /) -> D:
265        """Pop one value from left, provide a mandatory default value.
266
267        * safe version of popL
268        * returns a default value in the event the `ca` is empty
269        """
270        try:
271            return self.popL()
272        except ValueError:
273            return default
274
275    def popRD(self, default: D, /) -> D:
276        """Pop one value from right, provide a mandatory default value.
277
278        * safe version of popR
279        * returns a default value in the event the `ca` is empty
280        """
281        try:
282            return self.popR()
283        except ValueError:
284            return default
285
286    def popLT(self, max: int) -> tuple[D, ...]:
287        """Pop multiple values from left side of ca.
288
289        * returns the results in a tuple of type `tuple[~D, ...]`
290        * returns an empty tuple if `ca` is empty
291        * pop no more that `max` values
292        * will pop less if `ca` becomes empty
293        """
294        ds: list[D] = []
295
296        while max > 0:
297            try:
298                ds.append(self.popL())
299            except ValueError:
300                break
301            else:
302                max -= 1
303
304        return tuple(ds)
305
306    def popRT(self, max: int) -> tuple[D, ...]:
307        """Pop multiple values from right side of `ca`.
308
309        * returns the results in a tuple of type `tuple[~D, ...]`
310        * returns an empty tuple if `ca` is empty
311        * pop no more that `max` values
312        * will pop less if `ca` becomes empty
313        """
314        ds: list[D] = []
315        while max > 0:
316            try:
317                ds.append(self.popR())
318            except ValueError:
319                break
320            else:
321                max -= 1
322
323        return tuple(ds)
324
325    def rotL(self, n: int=1) -> None:
326        """Rotate ca arguments left n times."""
327        if self._cnt < 2:
328            return
329        while n > 0:
330            self.pushR(self.popL())
331            n -= 1
332
333    def rotR(self, n: int=1) -> None:
334        """Rotate ca arguments right n times."""
335        if self._cnt < 2:
336            return
337        while n > 0:
338            self.pushL(self.popR())
339            n -= 1
340
341    def map[U](self, f: Callable[[D], U], /) -> ca[U]:
342        """Apply function f over contents, returns new `ca` instance.
343
344        * parameter `f` function of type `f[~D, ~U] -> ca[~U]`
345        * returns a new instance of type `ca[~U]`
346        """
347        return ca(map(f, self))
348
349    def foldL[L](self, f: Callable[[L, D], L], /, initial: L|None=None) -> L:
350        """Left fold ca via function and optional initial value.
351
352        * parameter `f` function of type `f[~L, ~D] -> ~L`
353          * the first argument to `f` is for the accumulated value.
354        * parameter `initial` is an optional initial value
355        * returns the reduced value of type `~L`
356          * note that `~L` and `~D` can be the same type
357          * if an initial value is not given then by necessity `~L = ~D`
358        * raises `ValueError` when called on an empty `ca` and `initial` not given
359        """
360        if self._cnt == 0:
361            if initial is None:
362                msg = 'Method foldL called on an empty ca without an initial value.'
363                raise ValueError(msg)
364            else:
365                return initial
366        else:
367            if initial is None:
368                acc = cast(L, self[0])  # in this case D = L
369                for idx in range(1, self._cnt):
370                    acc = f(acc, self[idx])
371                return acc
372            else:
373                acc = initial
374                for d in self:
375                    acc = f(acc, d)
376                return acc
377
378    def foldR[R](self, f: Callable[[D, R], R], /, initial: R|None=None) -> R:
379        """Right fold ca via function and optional initial value.
380
381        * parameter `f` function of type `f[~D, ~R] -> ~R`
382          * the second argument to f is for the accumulated value
383        * parameter `initial` is an optional initial value
384        * returns the reduced value of type `~R`
385          * note that `~R` and `~D` can be the same type
386          * if an initial value is not given then by necessity `~R = ~D`
387        * raises `ValueError` when called on an empty `ca` and `initial` not given
388        """
389        if self._cnt == 0:
390            if initial is None:
391                msg = 'Method foldR called on an empty ca without an initial value.'
392                raise ValueError(msg)
393            else:
394                return initial
395        else:
396            if initial is None:
397                acc = cast(R, self[-1])  # in this case D = R
398                for idx in range(self._cnt-2, -1, -1):
399                    acc = f(self[idx], acc)
400                return acc
401            else:
402                acc = initial
403                for d in reversed(self):
404                    acc = f(d, acc)
405                return acc
406
407    def capacity(self) -> int:
408        """Returns current capacity of the ca."""
409        return self._cap
410
411    def empty(self) -> None:
412        """Empty the ca, keep current capacity."""
413        self._data, self._front, self._rear = [None]*self._cap, 0, self._cap
414
415    def fractionFilled(self) -> float:
416        """Returns fractional capacity of the ca."""
417        return self._cnt/self._cap
418
419    def resize(self, minimum_capacity: int=2) -> None:
420        """Compact `ca` and resize to `min_cap` if necessary.
421
422        * to just compact the `ca`, do not provide a min_cap
423        """
424        self._compact_storage_capacity()
425        if (min_cap := minimum_capacity) > self._cap:
426            self._cap, self._data = \
427                min_cap, self._data + [None]*(min_cap - self._cap)
428            if self._cnt == 0:
429                self._front, self._rear = 0, self._cap - 1
430
431def CA[D](*ds: D) -> ca[D]:
432    """Function to produce a `ca` array from a variable number of arguments."""
433    return ca(ds)
class ca(collections.abc.Sequence[D], typing.Generic[D]):
 28class ca[D](Sequence[D]):
 29    """
 30    #### Indexable circular array data structure
 31
 32    * generic, stateful data structure
 33    * amortized O(1) pushing and popping from either end
 34    * O(1) random access any element
 35    * will resize itself as needed
 36    * sliceable
 37    * makes defensive copies of contents for the purposes of iteration
 38    * in boolean context returns true if not empty, false if empty
 39    * in comparisons compare identity before equality (like Python built-ins do)
 40    * lowercase class name choosen to match built-ins like `list` and `tuple`
 41    * raises `IndexError` for out-of-bounds indexing
 42    * raises `ValueError` for popping from or folding an empty `ca`
 43    * raises `TypeError` if 2 or more arguments are passed to constructor
 44
 45    """
 46    __slots__ = '_data', '_cnt', '_cap', '_front', '_rear'
 47
 48    def __init__(self, *dss: Iterable[D]) -> None:
 49        if len(dss) < 2:
 50            self._data: list[D|None] = [None] + cast(list[D|None], list(*dss)) + [None]
 51        else:
 52            msg = f'ca expected at most 1 argument, got {len(dss)}'
 53            raise TypeError(msg)
 54        self._cap = cap = len(self._data)
 55        self._cnt = cap - 2
 56        if cap == 2:
 57            self._front = 0
 58            self._rear = 1
 59        else:
 60            self._front = 1
 61            self._rear = cap - 2
 62
 63    def _double_storage_capacity(self) -> None:
 64        if self._front <= self._rear:
 65            self._data += [None]*self._cap
 66            self._cap *= 2
 67        else:
 68            self._data = self._data[:self._front] + [None]*self._cap + self._data[self._front:]
 69            self._front, self._cap = self._front + self._cap, 2*self._cap
 70
 71    def _compact_storage_capacity(self) -> None:
 72        """Compact the ca."""
 73        match self._cnt:
 74            case 0:
 75                self._cap, self._front, self._rear, self._data = \
 76                    2, 0, 1, [None, None]
 77            case 1:
 78                self._cap, self._front, self._rear, self._data = \
 79                    3, 1, 1, [None, self._data[self._front], None]
 80            case _:
 81                if self._front <= self._rear:
 82                    self._cap, self._front, self._rear, self._data = \
 83                        self._cnt+2, 1, self._cnt, [None] + self._data[self._front:self._rear+1] + [None]
 84                else:
 85                    self._cap, self._front, self._rear, self._data = \
 86                        self._cnt+2, 1, self._cnt, [None] + self._data[self._front:] + self._data[:self._rear+1] + [None]
 87
 88    def __iter__(self) -> Iterator[D]:
 89        if self._cnt > 0:
 90            capacity, rear, position, current_state = \
 91                self._cap, self._rear, self._front, self._data.copy()
 92
 93            while position != rear:
 94                yield cast(D, current_state[position])
 95                position = (position + 1) % capacity
 96            yield cast(D, current_state[position])
 97
 98    def __reversed__(self) -> Iterator[D]:
 99        if self._cnt > 0:
100            capacity, front, position, current_state = \
101                self._cap, self._front, self._rear, self._data.copy()
102
103            while position != front:
104                yield cast(D, current_state[position])
105                position = (position - 1) % capacity
106            yield cast(D, current_state[position])
107
108    def __repr__(self) -> str:
109        return 'CA(' + ', '.join(map(repr, self)) + ')'
110
111    def __str__(self) -> str:
112        return '(|' + ', '.join(map(str, self)) + '|)'
113
114    def __bool__(self) -> bool:
115        return self._cnt > 0
116
117    def __len__(self) -> int:
118        return self._cnt
119
120    @overload
121    def __getitem__(self, idx: int, /) -> D: ...
122    @overload
123    def __getitem__(self, idx: slice, /) -> ca[D]: ...
124
125    def __getitem__(self, idx: int|slice, /) -> D|ca[D]:
126        if isinstance(idx, slice):
127            return ca(list(self)[idx])
128
129        cnt = self._cnt
130        if 0 <= idx < cnt:
131            return cast(D, self._data[(self._front + idx) % self._cap])
132        elif -cnt <= idx < 0:
133            return cast(D, self._data[(self._front + cnt + idx) % self._cap])
134        else:
135            if cnt > 0:
136                foo = [1, 2, 3]
137                foo.__setitem__
138                msg1 = 'Out of bounds: '
139                msg2 = f'index = {idx} not between {-cnt} and {cnt-1} '
140                msg3 = 'while getting value from a ca.'
141                raise IndexError(msg1 + msg2 + msg3)
142            else:
143                msg0 = 'Trying to get a value from an empty ca.'
144                raise IndexError(msg0)
145
146    @overload
147    def __setitem__(self, idx: int, vals: D, /) -> None: ...
148    @overload
149    def __setitem__(self, idx: slice, vals: Iterable[D], /) -> None: ...
150
151    def __setitem__(self, idx: int|slice, vals: D|Iterable[D], /) -> None:
152        if isinstance(idx, slice):
153            if isinstance(vals, Iterable):
154                data = list(self)
155                data[idx] = vals
156                _ca = ca(data)
157                self._data, self._cnt, self._cap, self._front, self._rear = \
158                            _ca._data, _ca._cnt, _ca._cap, _ca._front, _ca._rear
159                return
160            else:
161                msg = 'must assign iterable to extended slice'
162                foo = [1,2,3]
163                foo.__delitem__(2)
164                raise TypeError(msg)
165
166        cnt = self._cnt
167        if 0 <= idx < cnt:
168            self._data[(self._front + idx) % self._cap] = cast(D, vals)
169        elif -cnt <= idx < 0:
170            self._data[(self._front + cnt + idx) % self._cap] = cast(D, vals)
171        else:
172            if cnt > 0:
173                msg1 = 'Out of bounds: '
174                msg2 = f'index = {idx} not between {-cnt} and {cnt-1} '
175                msg3 = 'while setting value from a ca.'
176                raise IndexError(msg1 + msg2 + msg3)
177            else:
178                msg0 = 'Trying to set a value from an empty ca.'
179                raise IndexError(msg0)
180
181    @overload
182    def __delitem__(self, idx: int) -> None: ...
183    @overload
184    def __delitem__(self, idx: slice) -> None: ...
185
186    def __delitem__(self, idx: int|slice) -> None:
187        data = list(self)
188        del data[idx]
189        _ca = ca(data)
190        self._data, self._cnt, self._cap, self._front, self._rear = \
191                    _ca._data, _ca._cnt, _ca._cap, _ca._front, _ca._rear
192        return
193
194    def __eq__(self, other: object, /) -> bool:
195        if self is other:
196            return True
197        if not isinstance(other, type(self)):
198            return False
199
200        frontL, frontR, \
201        countL, countR, \
202        capacityL, capacityR = \
203            self._front, other._front, \
204            self._cnt, other._cnt, \
205            self._cap, other._cap
206
207        if countL != countR:
208            return False
209
210        for nn in range(countL):
211            if self._data[(frontL+nn)%capacityL] is other._data[(frontR+nn)%capacityR]:
212                continue
213            if self._data[(frontL+nn)%capacityL] != other._data[(frontR+nn)%capacityR]:
214                return False
215        return True
216
217    def pushL(self, *ds: D) -> None:
218        """Push data from the left onto the ca."""
219        for d in ds:
220            if self._cnt == self._cap:
221                self._double_storage_capacity()
222            self._front = (self._front - 1) % self._cap
223            self._data[self._front], self._cnt = d, self._cnt + 1
224
225    def pushR(self, *ds: D) -> None:
226        """Push data from the right onto the ca."""
227        for d in ds:
228            if self._cnt == self._cap:
229                self._double_storage_capacity()
230            self._rear = (self._rear + 1) % self._cap
231            self._data[self._rear], self._cnt = d, self._cnt + 1
232
233    def popL(self) -> D|Never:
234        """Pop one value off the left side of the ca.
235
236        * raises `ValueError` when called on an empty ca
237        """
238        if self._cnt > 1:
239            d, self._data[self._front], self._front, self._cnt = \
240                self._data[self._front], None, (self._front+1) % self._cap, self._cnt - 1
241        elif self._cnt < 1:
242            msg = 'Method popL called on an empty ca'
243            raise ValueError(msg)
244        else:
245            d, self._data[self._front], self._cnt, self._front, self._rear = \
246                self._data[self._front], None, 0, 0, self._cap - 1
247        return cast(D, d)
248
249    def popR(self) -> D|Never:
250        """Pop one value off the right side of the ca.
251
252        * raises `ValueError` when called on an empty ca
253        """
254        if self._cnt > 0:
255            d, self._data[self._rear], self._rear, self._cnt = \
256                self._data[self._rear], None, (self._rear - 1) % self._cap, self._cnt - 1
257        elif self._cnt < 1:
258            msg = 'Method popR called on an empty ca'
259            raise ValueError(msg)
260        else:
261            d, self._data[self._front], self._cnt, self._front, self._rear = \
262                self._data[self._front], None, 0, 0, self._cap - 1
263        return cast(D, d)
264
265    def popLD(self, default: D, /) -> D:
266        """Pop one value from left, provide a mandatory default value.
267
268        * safe version of popL
269        * returns a default value in the event the `ca` is empty
270        """
271        try:
272            return self.popL()
273        except ValueError:
274            return default
275
276    def popRD(self, default: D, /) -> D:
277        """Pop one value from right, provide a mandatory default value.
278
279        * safe version of popR
280        * returns a default value in the event the `ca` is empty
281        """
282        try:
283            return self.popR()
284        except ValueError:
285            return default
286
287    def popLT(self, max: int) -> tuple[D, ...]:
288        """Pop multiple values from left side of ca.
289
290        * returns the results in a tuple of type `tuple[~D, ...]`
291        * returns an empty tuple if `ca` is empty
292        * pop no more that `max` values
293        * will pop less if `ca` becomes empty
294        """
295        ds: list[D] = []
296
297        while max > 0:
298            try:
299                ds.append(self.popL())
300            except ValueError:
301                break
302            else:
303                max -= 1
304
305        return tuple(ds)
306
307    def popRT(self, max: int) -> tuple[D, ...]:
308        """Pop multiple values from right side of `ca`.
309
310        * returns the results in a tuple of type `tuple[~D, ...]`
311        * returns an empty tuple if `ca` is empty
312        * pop no more that `max` values
313        * will pop less if `ca` becomes empty
314        """
315        ds: list[D] = []
316        while max > 0:
317            try:
318                ds.append(self.popR())
319            except ValueError:
320                break
321            else:
322                max -= 1
323
324        return tuple(ds)
325
326    def rotL(self, n: int=1) -> None:
327        """Rotate ca arguments left n times."""
328        if self._cnt < 2:
329            return
330        while n > 0:
331            self.pushR(self.popL())
332            n -= 1
333
334    def rotR(self, n: int=1) -> None:
335        """Rotate ca arguments right n times."""
336        if self._cnt < 2:
337            return
338        while n > 0:
339            self.pushL(self.popR())
340            n -= 1
341
342    def map[U](self, f: Callable[[D], U], /) -> ca[U]:
343        """Apply function f over contents, returns new `ca` instance.
344
345        * parameter `f` function of type `f[~D, ~U] -> ca[~U]`
346        * returns a new instance of type `ca[~U]`
347        """
348        return ca(map(f, self))
349
350    def foldL[L](self, f: Callable[[L, D], L], /, initial: L|None=None) -> L:
351        """Left fold ca via function and optional initial value.
352
353        * parameter `f` function of type `f[~L, ~D] -> ~L`
354          * the first argument to `f` is for the accumulated value.
355        * parameter `initial` is an optional initial value
356        * returns the reduced value of type `~L`
357          * note that `~L` and `~D` can be the same type
358          * if an initial value is not given then by necessity `~L = ~D`
359        * raises `ValueError` when called on an empty `ca` and `initial` not given
360        """
361        if self._cnt == 0:
362            if initial is None:
363                msg = 'Method foldL called on an empty ca without an initial value.'
364                raise ValueError(msg)
365            else:
366                return initial
367        else:
368            if initial is None:
369                acc = cast(L, self[0])  # in this case D = L
370                for idx in range(1, self._cnt):
371                    acc = f(acc, self[idx])
372                return acc
373            else:
374                acc = initial
375                for d in self:
376                    acc = f(acc, d)
377                return acc
378
379    def foldR[R](self, f: Callable[[D, R], R], /, initial: R|None=None) -> R:
380        """Right fold ca via function and optional initial value.
381
382        * parameter `f` function of type `f[~D, ~R] -> ~R`
383          * the second argument to f is for the accumulated value
384        * parameter `initial` is an optional initial value
385        * returns the reduced value of type `~R`
386          * note that `~R` and `~D` can be the same type
387          * if an initial value is not given then by necessity `~R = ~D`
388        * raises `ValueError` when called on an empty `ca` and `initial` not given
389        """
390        if self._cnt == 0:
391            if initial is None:
392                msg = 'Method foldR called on an empty ca without an initial value.'
393                raise ValueError(msg)
394            else:
395                return initial
396        else:
397            if initial is None:
398                acc = cast(R, self[-1])  # in this case D = R
399                for idx in range(self._cnt-2, -1, -1):
400                    acc = f(self[idx], acc)
401                return acc
402            else:
403                acc = initial
404                for d in reversed(self):
405                    acc = f(d, acc)
406                return acc
407
408    def capacity(self) -> int:
409        """Returns current capacity of the ca."""
410        return self._cap
411
412    def empty(self) -> None:
413        """Empty the ca, keep current capacity."""
414        self._data, self._front, self._rear = [None]*self._cap, 0, self._cap
415
416    def fractionFilled(self) -> float:
417        """Returns fractional capacity of the ca."""
418        return self._cnt/self._cap
419
420    def resize(self, minimum_capacity: int=2) -> None:
421        """Compact `ca` and resize to `min_cap` if necessary.
422
423        * to just compact the `ca`, do not provide a min_cap
424        """
425        self._compact_storage_capacity()
426        if (min_cap := minimum_capacity) > self._cap:
427            self._cap, self._data = \
428                min_cap, self._data + [None]*(min_cap - self._cap)
429            if self._cnt == 0:
430                self._front, self._rear = 0, self._cap - 1

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
  • sliceable
  • makes defensive copies of contents for the purposes of iteration
  • in boolean context returns true if not empty, false if empty
  • in comparisons compare identity before equality (like Python built-ins do)
  • lowercase class name choosen to match built-ins like list and tuple
  • raises IndexError for out-of-bounds indexing
  • raises ValueError for popping from or folding an empty ca
  • raises TypeError if 2 or more arguments are passed to constructor
ca(*dss: Iterable[~D])
48    def __init__(self, *dss: Iterable[D]) -> None:
49        if len(dss) < 2:
50            self._data: list[D|None] = [None] + cast(list[D|None], list(*dss)) + [None]
51        else:
52            msg = f'ca expected at most 1 argument, got {len(dss)}'
53            raise TypeError(msg)
54        self._cap = cap = len(self._data)
55        self._cnt = cap - 2
56        if cap == 2:
57            self._front = 0
58            self._rear = 1
59        else:
60            self._front = 1
61            self._rear = cap - 2
def pushL(self, *ds: ~D) -> None:
217    def pushL(self, *ds: D) -> None:
218        """Push data from the left onto the ca."""
219        for d in ds:
220            if self._cnt == self._cap:
221                self._double_storage_capacity()
222            self._front = (self._front - 1) % self._cap
223            self._data[self._front], self._cnt = d, self._cnt + 1

Push data from the left onto the ca.

def pushR(self, *ds: ~D) -> None:
225    def pushR(self, *ds: D) -> None:
226        """Push data from the right onto the ca."""
227        for d in ds:
228            if self._cnt == self._cap:
229                self._double_storage_capacity()
230            self._rear = (self._rear + 1) % self._cap
231            self._data[self._rear], self._cnt = d, self._cnt + 1

Push data from the right onto the ca.

def popL(self) -> Union[~D, Never]:
233    def popL(self) -> D|Never:
234        """Pop one value off the left side of the ca.
235
236        * raises `ValueError` when called on an empty ca
237        """
238        if self._cnt > 1:
239            d, self._data[self._front], self._front, self._cnt = \
240                self._data[self._front], None, (self._front+1) % self._cap, self._cnt - 1
241        elif self._cnt < 1:
242            msg = 'Method popL called on an empty ca'
243            raise ValueError(msg)
244        else:
245            d, self._data[self._front], self._cnt, self._front, self._rear = \
246                self._data[self._front], None, 0, 0, self._cap - 1
247        return cast(D, d)

Pop one value off the left side of the ca.

  • raises ValueError when called on an empty ca
def popR(self) -> Union[~D, Never]:
249    def popR(self) -> D|Never:
250        """Pop one value off the right side of the ca.
251
252        * raises `ValueError` when called on an empty ca
253        """
254        if self._cnt > 0:
255            d, self._data[self._rear], self._rear, self._cnt = \
256                self._data[self._rear], None, (self._rear - 1) % self._cap, self._cnt - 1
257        elif self._cnt < 1:
258            msg = 'Method popR called on an empty ca'
259            raise ValueError(msg)
260        else:
261            d, self._data[self._front], self._cnt, self._front, self._rear = \
262                self._data[self._front], None, 0, 0, self._cap - 1
263        return cast(D, d)

Pop one value off the right side of the ca.

  • raises ValueError when called on an empty ca
def popLD(self, default: ~D, /) -> ~D:
265    def popLD(self, default: D, /) -> D:
266        """Pop one value from left, provide a mandatory default value.
267
268        * safe version of popL
269        * returns a default value in the event the `ca` is empty
270        """
271        try:
272            return self.popL()
273        except ValueError:
274            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:
276    def popRD(self, default: D, /) -> D:
277        """Pop one value from right, provide a mandatory default value.
278
279        * safe version of popR
280        * returns a default value in the event the `ca` is empty
281        """
282        try:
283            return self.popR()
284        except ValueError:
285            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) -> tuple[~D, ...]:
287    def popLT(self, max: int) -> tuple[D, ...]:
288        """Pop multiple values from left side of ca.
289
290        * returns the results in a tuple of type `tuple[~D, ...]`
291        * returns an empty tuple if `ca` is empty
292        * pop no more that `max` values
293        * will pop less if `ca` becomes empty
294        """
295        ds: list[D] = []
296
297        while max > 0:
298            try:
299                ds.append(self.popL())
300            except ValueError:
301                break
302            else:
303                max -= 1
304
305        return tuple(ds)

Pop multiple values from left side of ca.

  • 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) -> tuple[~D, ...]:
307    def popRT(self, max: int) -> tuple[D, ...]:
308        """Pop multiple values from right side of `ca`.
309
310        * returns the results in a tuple of type `tuple[~D, ...]`
311        * returns an empty tuple if `ca` is empty
312        * pop no more that `max` values
313        * will pop less if `ca` becomes empty
314        """
315        ds: list[D] = []
316        while max > 0:
317            try:
318                ds.append(self.popR())
319            except ValueError:
320                break
321            else:
322                max -= 1
323
324        return tuple(ds)

Pop multiple values from right side of ca.

  • 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 rotL(self, n: int = 1) -> None:
326    def rotL(self, n: int=1) -> None:
327        """Rotate ca arguments left n times."""
328        if self._cnt < 2:
329            return
330        while n > 0:
331            self.pushR(self.popL())
332            n -= 1

Rotate ca arguments left n times.

def rotR(self, n: int = 1) -> None:
334    def rotR(self, n: int=1) -> None:
335        """Rotate ca arguments right n times."""
336        if self._cnt < 2:
337            return
338        while n > 0:
339            self.pushL(self.popR())
340            n -= 1

Rotate ca arguments right n times.

def map(self, f: Callable[[~D], ~U], /) -> ca[~U]:
342    def map[U](self, f: Callable[[D], U], /) -> ca[U]:
343        """Apply function f over contents, returns new `ca` instance.
344
345        * parameter `f` function of type `f[~D, ~U] -> ca[~U]`
346        * returns a new instance of type `ca[~U]`
347        """
348        return ca(map(f, self))

Apply function f over contents, returns new ca instance.

  • parameter f function of type f[~D, ~U] -> ca[~U]
  • returns a new instance of type ca[~U]
def foldL(self, f: Callable[[~L, ~D], ~L], /, initial: Optional[~L] = None) -> ~L:
350    def foldL[L](self, f: Callable[[L, D], L], /, initial: L|None=None) -> L:
351        """Left fold ca via function and optional initial value.
352
353        * parameter `f` function of type `f[~L, ~D] -> ~L`
354          * the first argument to `f` is for the accumulated value.
355        * parameter `initial` is an optional initial value
356        * returns the reduced value of type `~L`
357          * note that `~L` and `~D` can be the same type
358          * if an initial value is not given then by necessity `~L = ~D`
359        * raises `ValueError` when called on an empty `ca` and `initial` not given
360        """
361        if self._cnt == 0:
362            if initial is None:
363                msg = 'Method foldL called on an empty ca without an initial value.'
364                raise ValueError(msg)
365            else:
366                return initial
367        else:
368            if initial is None:
369                acc = cast(L, self[0])  # in this case D = L
370                for idx in range(1, self._cnt):
371                    acc = f(acc, self[idx])
372                return acc
373            else:
374                acc = initial
375                for d in self:
376                    acc = f(acc, d)
377                return acc

Left fold ca via function and optional initial value.

  • parameter f function of type f[~L, ~D] -> ~L
    • the first argument to f is for the accumulated value.
  • parameter initial is an optional initial value
  • 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:
379    def foldR[R](self, f: Callable[[D, R], R], /, initial: R|None=None) -> R:
380        """Right fold ca via function and optional initial value.
381
382        * parameter `f` function of type `f[~D, ~R] -> ~R`
383          * the second argument to f is for the accumulated value
384        * parameter `initial` is an optional initial value
385        * returns the reduced value of type `~R`
386          * note that `~R` and `~D` can be the same type
387          * if an initial value is not given then by necessity `~R = ~D`
388        * raises `ValueError` when called on an empty `ca` and `initial` not given
389        """
390        if self._cnt == 0:
391            if initial is None:
392                msg = 'Method foldR called on an empty ca without an initial value.'
393                raise ValueError(msg)
394            else:
395                return initial
396        else:
397            if initial is None:
398                acc = cast(R, self[-1])  # in this case D = R
399                for idx in range(self._cnt-2, -1, -1):
400                    acc = f(self[idx], acc)
401                return acc
402            else:
403                acc = initial
404                for d in reversed(self):
405                    acc = f(d, acc)
406                return acc

Right fold ca via function and optional initial value.

  • parameter f function of type f[~D, ~R] -> ~R
    • the second argument to f is for the accumulated value
  • parameter initial is an optional initial value
  • returns the reduced value of type ~R
    • note that ~R and ~D can be the same type
    • if an initial value is not given then by necessity ~R = ~D
  • raises ValueError when called on an empty ca and initial not given
def capacity(self) -> int:
408    def capacity(self) -> int:
409        """Returns current capacity of the ca."""
410        return self._cap

Returns current capacity of the ca.

def empty(self) -> None:
412    def empty(self) -> None:
413        """Empty the ca, keep current capacity."""
414        self._data, self._front, self._rear = [None]*self._cap, 0, self._cap

Empty the ca, keep current capacity.

def fractionFilled(self) -> float:
416    def fractionFilled(self) -> float:
417        """Returns fractional capacity of the ca."""
418        return self._cnt/self._cap

Returns fractional capacity of the ca.

def resize(self, minimum_capacity: int = 2) -> None:
420    def resize(self, minimum_capacity: int=2) -> None:
421        """Compact `ca` and resize to `min_cap` if necessary.
422
423        * to just compact the `ca`, do not provide a min_cap
424        """
425        self._compact_storage_capacity()
426        if (min_cap := minimum_capacity) > self._cap:
427            self._cap, self._data = \
428                min_cap, self._data + [None]*(min_cap - self._cap)
429            if self._cnt == 0:
430                self._front, self._rear = 0, self._cap - 1

Compact ca and resize to min_cap if necessary.

  • to just compact the ca, do not provide a min_cap
def CA(*ds: ~D) -> ca[~D]:
432def CA[D](*ds: D) -> ca[D]:
433    """Function to produce a `ca` array from a variable number of arguments."""
434    return ca(ds)

Function to produce a ca array from a variable number of arguments.