dtools.circular_array

An indexable circular array data structure.

An indexable, sliceable, auto-resizing circular array data structure with amortized O(1) pushes and pops either end.

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

Indexable circular array data structure

  • generic, stateful, invariant 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 when not empty
    • False when empty
  • in comparisons compare identity before equality, like builtins do
  • raises IndexError for out-of-bounds indexing
  • raises ValueError for popping from or folding an empty CA
CA(*dss: Iterable[~D])
61    def __init__(self, *dss: Iterable[D]) -> None:
62        if len(dss) < 2:
63            self._data: list[D | None] = (
64                [None] + cast(list[D | None], list(*dss)) + [None]
65            )
66        else:
67            msg = f'CA expected at most 1 argument, got {len(dss)}'
68            raise TypeError(msg)
69        self._cap = cap = len(self._data)
70        self._cnt = cap - 2
71        if cap == 2:
72            self._front = 0
73            self._rear = 1
74        else:
75            self._front = 1
76            self._rear = cap - 2
def pushl(self, *ds: ~D) -> None:
271    def pushl(self, *ds: D) -> None:
272        """Push left.
273
274        - push data from the left onto the CA
275
276        """
277        for d in ds:
278            if self._cnt == self._cap:
279                self._double_storage_capacity()
280            self._front = (self._front - 1) % self._cap
281            self._data[self._front], self._cnt = d, self._cnt + 1

Push left.

  • push data from the left onto the CA
def pushr(self, *ds: ~D) -> None:
283    def pushr(self, *ds: D) -> None:
284        """Push right.
285
286        - push data from the right onto the CA
287
288        """
289        for d in ds:
290            if self._cnt == self._cap:
291                self._double_storage_capacity()
292            self._rear = (self._rear + 1) % self._cap
293            self._data[self._rear], self._cnt = d, self._cnt + 1

Push right.

  • push data from the right onto the CA
def popl(self) -> Union[~D, Never]:
295    def popl(self) -> D | Never:
296        """Pop left.
297
298        - pop one value off the left side of the `CA`
299        - raises `ValueError` when called on an empty `CA`
300
301        """
302        if self._cnt > 1:
303            d, self._data[self._front], self._front, self._cnt = (
304                self._data[self._front],
305                None,
306                (self._front + 1) % self._cap,
307                self._cnt - 1,
308            )
309        elif self._cnt == 1:
310            d, self._data[self._front], self._cnt, self._front, self._rear = (
311                self._data[self._front],
312                None,
313                0,
314                0,
315                self._cap - 1,
316            )
317        else:
318            msg = 'Method popl called on an empty CA'
319            raise ValueError(msg)
320        return cast(D, d)

Pop left.

  • pop one value off the left side of the CA
  • raises ValueError when called on an empty CA
def popr(self) -> Union[~D, Never]:
322    def popr(self) -> D | Never:
323        """Pop right
324
325        - pop one value off the right side of the `CA`
326        - raises `ValueError` when called on an empty `CA`
327
328        """
329        if self._cnt > 1:
330            d, self._data[self._rear], self._rear, self._cnt = (
331                self._data[self._rear],
332                None,
333                (self._rear - 1) % self._cap,
334                self._cnt - 1,
335            )
336        elif self._cnt == 1:
337            d, self._data[self._front], self._cnt, self._front, self._rear = (
338                self._data[self._front],
339                None,
340                0,
341                0,
342                self._cap - 1,
343            )
344        else:
345            msg = 'Method popr called on an empty CA'
346            raise ValueError(msg)
347        return cast(D, d)

Pop right

  • pop one value off the right side of the CA
  • raises ValueError when called on an empty CA
def popld(self, default: ~D, /) -> ~D:
349    def popld(self, default: D, /) -> D:
350        """Pop one value from left, provide a mandatory default value.
351
352        - safe version of popl
353        - returns the default value if `CA` is empty
354
355        """
356        try:
357            return self.popl()
358        except ValueError:
359            return default

Pop one value from left, provide a mandatory default value.

  • safe version of popl
  • returns the default value if CA is empty
def poprd(self, default: ~D, /) -> ~D:
361    def poprd(self, default: D, /) -> D:
362        """Pop one value from right, provide a mandatory default value.
363
364        - safe version of popr
365        - returns the default value if `CA` is empty
366
367        """
368        try:
369            return self.popr()
370        except ValueError:
371            return default

Pop one value from right, provide a mandatory default value.

  • safe version of popr
  • returns the default value if CA is empty
def poplt(self, maximum: int, /) -> tuple[~D, ...]:
373    def poplt(self, maximum: int, /) -> tuple[D, ...]:
374        """Pop multiple values from left side of `CA`.
375
376        - returns the results in a tuple of type
377        - pop no more that `maximum` values
378        - will pop less if `CA` becomes empty
379
380        """
381        ds: list[D] = []
382
383        while maximum > 0:
384            try:
385                ds.append(self.popl())
386            except ValueError:
387                break
388            else:
389                maximum -= 1
390
391        return tuple(ds)

Pop multiple values from left side of CA.

  • returns the results in a tuple of type
  • pop no more that maximum values
  • will pop less if CA becomes empty
def poprt(self, maximum: int, /) -> tuple[~D, ...]:
393    def poprt(self, maximum: int, /) -> tuple[D, ...]:
394        """Pop multiple values from right side of `CA`.
395
396        - returns the results in a tuple
397        - pop no more that `maximum` values
398        - will pop less if `CA` becomes empty
399
400        """
401        ds: list[D] = []
402        while maximum > 0:
403            try:
404                ds.append(self.popr())
405            except ValueError:
406                break
407            else:
408                maximum -= 1
409
410        return tuple(ds)

Pop multiple values from right side of CA.

  • returns the results in a tuple
  • pop no more that maximum values
  • will pop less if CA becomes empty
def rotl(self, n: int = 1, /) -> None:
412    def rotl(self, n: int = 1, /) -> None:
413        """Rotate `CA` components to the left n times."""
414        if self._cnt < 2:
415            return
416        for _ in range(n, 0, -1):
417            self.pushr(self.popl())

Rotate CA components to the left n times.

def rotr(self, n: int = 1, /) -> None:
419    def rotr(self, n: int = 1, /) -> None:
420        """Rotate `CA` components to the right n times."""
421        if self._cnt < 2:
422            return
423        for _ in range(n, 0, -1):
424            self.pushl(self.popr())

Rotate CA components to the right n times.

def map(self, f: Callable[[~D], ~U], /) -> CA[~U]:
426    def map[U](self, f: Callable[[D], U], /) -> CA[U]:
427        """Apply function `f` over the `CA` contents,
428
429        - returns a new `CA` instance
430
431        """
432        return CA(map(f, self))

Apply function f over the CA contents,

  • returns a new CA instance
def foldl(self, f: Callable[[~L, ~D], ~L], initial: Optional[~L] = None, /) -> ~L:
434    def foldl[L](self, f: Callable[[L, D], L], initial: L | None = None, /) -> L:
435        """Left fold `CA` with function `f` and an optional `initial` value.
436
437        - first argument to `f` is for the accumulated value
438        - returns the reduced value of type `~L`
439          - note that `~L` and `~D` can be the same type
440          - if an initial value is not given then by necessity `~L = ~D`
441        - raises `ValueError` when called on an empty `ca` and `initial` not given
442
443        """
444        if self._cnt == 0:
445            if initial is None:
446                msg = 'Method foldl called on an empty `CA` without an initial value.'
447                raise ValueError(msg)
448            return initial
449
450        if initial is None:
451            acc = cast(L, self[0])  # in this case D = L
452            for idx in range(1, self._cnt):
453                acc = f(acc, self[idx])
454            return acc
455
456        acc = initial
457        for d in self:
458            acc = f(acc, d)
459        return acc

Left fold CA with function f and an optional initial value.

  • first argument to f is for the accumulated 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:
461    def foldr[R](self, f: Callable[[D, R], R], initial: R | None = None, /) -> R:
462        """Right fold `CA` with function `f` and an optional `initial` value.
463
464        - second argument to f is for the accumulated value
465        - returns the reduced value of type `~R`
466          - note that `~R` and `~D` can be the same type
467          - if an initial value is not given then by necessity `~R = ~D`
468        - raises `ValueError` when called on an empty `CA` and `initial` not given
469
470        """
471        if self._cnt == 0:
472            if initial is None:
473                msg = 'Method foldr called on empty `CA` without initial value.'
474                raise ValueError(msg)
475            return initial
476
477        if initial is None:
478            acc = cast(R, self[-1])  # in this case D = R
479            for idx in range(self._cnt - 2, -1, -1):
480                acc = f(self[idx], acc)
481            return acc
482
483        acc = initial
484        for d in reversed(self):
485            acc = f(d, acc)
486        return acc

Right fold CA with function f and an optional initial value.

  • second argument to f is for the accumulated 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:
488    def capacity(self) -> int:
489        """Returns current capacity of the `CA`."""
490        return self._cap

Returns current capacity of the CA.

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

Empty the CA, keep current capacity.

def fraction_filled(self) -> float:
496    def fraction_filled(self) -> float:
497        """Returns fractional capacity of the `CA`."""
498        return self._cnt / self._cap

Returns fractional capacity of the CA.

def resize(self, minimum_capacity: int = 2) -> None:
500    def resize(self, minimum_capacity: int = 2) -> None:
501        """Compact `CA` and resize to `minimum_capacity` if necessary.
502
503        * to just compact the `CA`, do not provide a minimum capacity
504
505        """
506        self._compact_storage_capacity()
507        if (min_cap := minimum_capacity) > self._cap:
508            self._cap, self._data = min_cap, self._data + [None] * (min_cap - self._cap)
509            if self._cnt == 0:
510                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: ~D) -> CA[~D]:
513def ca[D](*ds: D) -> CA[D]:
514    """Function to produce a `CA` array from a variable number of arguments."""
515    return CA(ds)

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