dtools.circular_array.ca

Module for an indexable circular array data structure.

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

Indexable circular array data structure

  • generic, stateful data structure
  • lowercase class name chosen to match nomenclature for builtins
    • like list and tuple
  • 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 builtins)
  • raises IndexError for out-of-bounds indexing
  • raises ValueError for popping from or folding an empty ca
CA(*dss: Iterable[~D])
51    def __init__(self, *dss: Iterable[D]) -> None:
52        if len(dss) < 2:
53            self._data: list[D | None] = (
54                [None] + cast(list[D | None], list(*dss)) + [None]
55            )
56        else:
57            msg = f'CA expected at most 1 argument, got {len(dss)}'
58            raise TypeError(msg)
59        self._cap = cap = len(self._data)
60        self._cnt = cap - 2
61        if cap == 2:
62            self._front = 0
63            self._rear = 1
64        else:
65            self._front = 1
66            self._rear = cap - 2
def pushl(self, *ds: ~D) -> None:
262    def pushl(self, *ds: D) -> None:
263        """Push data from the left onto the CA."""
264        for d in ds:
265            if self._cnt == self._cap:
266                self._double_storage_capacity()
267            self._front = (self._front - 1) % self._cap
268            self._data[self._front], self._cnt = d, self._cnt + 1

Push data from the left onto the CA.

def pushr(self, *ds: ~D) -> None:
270    def pushr(self, *ds: D) -> None:
271        """Push data from the right onto the CA."""
272        for d in ds:
273            if self._cnt == self._cap:
274                self._double_storage_capacity()
275            self._rear = (self._rear + 1) % self._cap
276            self._data[self._rear], self._cnt = d, self._cnt + 1

Push data from the right onto the CA.

def popl(self) -> Union[~D, Never]:
278    def popl(self) -> D | Never:
279        """Pop one value off the left side of the CA.
280
281        Raises `ValueError` when called on an empty CA.
282
283        """
284        if self._cnt > 1:
285            d, self._data[self._front], self._front, self._cnt = (
286                self._data[self._front],
287                None,
288                (self._front + 1) % self._cap,
289                self._cnt - 1,
290            )
291        elif self._cnt == 1:
292            d, self._data[self._front], self._cnt, self._front, self._rear = (
293                self._data[self._front],
294                None,
295                0,
296                0,
297                self._cap - 1,
298            )
299        else:
300            msg = 'Method popl called on an empty CA'
301            raise ValueError(msg)
302        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]:
304    def popr(self) -> D | Never:
305        """Pop one value off the right side of the CA.
306
307        Raises `ValueError` when called on an empty CA.
308
309        """
310        if self._cnt > 1:
311            d, self._data[self._rear], self._rear, self._cnt = (
312                self._data[self._rear],
313                None,
314                (self._rear - 1) % self._cap,
315                self._cnt - 1,
316            )
317        elif self._cnt == 1:
318            d, self._data[self._front], self._cnt, self._front, self._rear = (
319                self._data[self._front],
320                None,
321                0,
322                0,
323                self._cap - 1,
324            )
325        else:
326            msg = 'Method popr called on an empty CA'
327            raise ValueError(msg)
328        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:
330    def popld(self, default: D, /) -> D:
331        """Pop one value from left, provide a mandatory default value.
332
333        - safe version of popl
334        - returns a default value in the event the `CA` is empty
335
336        """
337        try:
338            return self.popl()
339        except ValueError:
340            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:
342    def poprd(self, default: D, /) -> D:
343        """Pop one value from right, provide a mandatory default value.
344
345        - safe version of popr
346        - returns a default value in the event the `CA` is empty
347
348        """
349        try:
350            return self.popr()
351        except ValueError:
352            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, maximum: int, /) -> tuple[~D, ...]:
354    def poplt(self, maximum: int, /) -> tuple[D, ...]:
355        """Pop multiple values from left side of `CA`.
356
357        - returns the results in a tuple of type `tuple[~D, ...]`
358        - returns an empty tuple if `CA` is empty
359        - pop no more that `max` values
360        - will pop less if `CA` becomes empty
361
362        """
363        ds: list[D] = []
364
365        while maximum > 0:
366            try:
367                ds.append(self.popl())
368            except ValueError:
369                break
370            else:
371                maximum -= 1
372
373        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, maximum: int, /) -> tuple[~D, ...]:
375    def poprt(self, maximum: int, /) -> tuple[D, ...]:
376        """Pop multiple values from right side of `CA`.
377
378        - returns the results in a tuple of type `tuple[~D, ...]`
379        - returns an empty tuple if `CA` is empty
380        - pop no more that `max` values
381        - will pop less if `CA` becomes empty
382
383        """
384        ds: list[D] = []
385        while maximum > 0:
386            try:
387                ds.append(self.popr())
388            except ValueError:
389                break
390            else:
391                maximum -= 1
392
393        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:
395    def rotl(self, n: int = 1, /) -> None:
396        """Rotate `CA` arguments left n times."""
397        if self._cnt < 2:
398            return
399        for _ in range(n, 0, -1):
400            self.pushr(self.popl())

Rotate CA arguments left n times.

def rotr(self, n: int = 1, /) -> None:
402    def rotr(self, n: int = 1, /) -> None:
403        """Rotate `CA` arguments right n times."""
404        if self._cnt < 2:
405            return
406        for _ in range(n, 0, -1):
407            self.pushl(self.popr())

Rotate CA arguments right n times.

def map(self, f: Callable[[~D], ~U], /) -> CA[~U]:
409    def map[U](self, f: Callable[[D], U], /) -> CA[U]:
410        """Apply function f over contents, returns new `CA` instance.
411
412        - parameter `f` function of type `f[~D, ~U] -> CA[~U]`
413        - returns a new instance of type `CA[~U]`
414
415        """
416        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:
418    def foldl[L](self, f: Callable[[L, D], L], /, initial: L | None = None) -> L:
419        """Left fold `CA` via function and optional initial value.
420
421        - parameter `f` function of type `f[~L, ~D] -> ~L`
422          - the first argument to `f` is for the accumulated value.
423        - parameter `initial` is an optional initial value
424        - returns the reduced value of type `~L`
425          - note that `~L` and `~D` can be the same type
426          - if an initial value is not given then by necessity `~L = ~D`
427        - raises `ValueError` when called on an empty `ca` and `initial` not given
428
429        """
430        if self._cnt == 0:
431            if initial is None:
432                msg = 'Method foldl called on an empty `CA` without an initial value.'
433                raise ValueError(msg)
434            return initial
435
436        if initial is None:
437            acc = cast(L, self[0])  # in this case D = L
438            for idx in range(1, self._cnt):
439                acc = f(acc, self[idx])
440            return acc
441
442        acc = initial
443        for d in self:
444            acc = f(acc, d)
445        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:
447    def foldr[R](self, f: Callable[[D, R], R], /, initial: R | None = None) -> R:
448        """Right fold `CA` via function and optional initial value.
449
450        - parameter `f` function of type `f[~D, ~R] -> ~R`
451          - the second argument to f is for the accumulated value
452        - parameter `initial` is an optional initial value
453        - returns the reduced value of type `~R`
454          - note that `~R` and `~D` can be the same type
455          - if an initial value is not given then by necessity `~R = ~D`
456        - raises `ValueError` when called on an empty `CA` and `initial` not given
457
458        """
459        if self._cnt == 0:
460            if initial is None:
461                msg = 'Method foldr called on empty `CA` without initial value.'
462                raise ValueError(msg)
463            return initial
464
465        if initial is None:
466            acc = cast(R, self[-1])  # in this case D = R
467            for idx in range(self._cnt - 2, -1, -1):
468                acc = f(self[idx], acc)
469            return acc
470
471        acc = initial
472        for d in reversed(self):
473            acc = f(d, acc)
474        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:
476    def capacity(self) -> int:
477        """Returns current capacity of the `CA`."""
478        return self._cap

Returns current capacity of the CA.

def empty(self) -> None:
480    def empty(self) -> None:
481        """Empty the `CA`, keep current capacity."""
482        self._data, self._front, self._rear = [None] * self._cap, 0, self._cap

Empty the CA, keep current capacity.

def fraction_filled(self) -> float:
484    def fraction_filled(self) -> float:
485        """Returns fractional capacity of the `CA`."""
486        return self._cnt / self._cap

Returns fractional capacity of the CA.

def resize(self, minimum_capacity: int = 2) -> None:
488    def resize(self, minimum_capacity: int = 2) -> None:
489        """Compact `CA` and resize to `minimum_capacity` if necessary.
490
491        * to just compact the `CA`, do not provide a minimum capacity
492
493        """
494        self._compact_storage_capacity()
495        if (min_cap := minimum_capacity) > self._cap:
496            self._cap, self._data = min_cap, self._data + [None] * (min_cap - self._cap)
497            if self._cnt == 0:
498                self._front, self._rear = 0, self._cap - 1

Compact CA and resize to minimum_capacity if necessary.

  • to just compact the CA, do not provide a minimum capacity
def ca(*ds: ~T) -> CA[~T]:
501def ca[T](*ds: T) -> CA[T]:
502    """Function to produce a `CA` array from a variable number of arguments."""
503    return CA(ds)

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