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

Push data from the left onto the CA.

def pushr(self, *ds: ~D) -> None:
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

Push data from the right onto the CA.

def popl(self) -> Union[~D, Never]:
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)

Pop one value off the left side of the CA.

Raises ValueError when called on an empty CA.

def popr(self) -> Union[~D, Never]:
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)

Pop one value off the right side of the CA.

Raises ValueError when called on an empty CA.

def popld(self, default: ~D, /) -> ~D:
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

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:
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

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, ...]:
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)

Pop multiple values from left side of CA.

  • returns the results in a tuple of type tuple[~D, ...]
  • returns an empty tuple if CA is empty
  • pop no more that max values
  • will pop less if CA becomes empty
def poprt(self, max: int) -> tuple[~D, ...]:
374    def poprt(self, max: 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 max > 0:
385            try:
386                ds.append(self.popr())
387            except ValueError:
388                break
389            else:
390                max -= 1
391
392        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:
394    def rotl(self, n: int = 1) -> None:
395        """Rotate `CA` arguments left n times."""
396        if self._cnt < 2:
397            return
398        while n > 0:
399            self.pushr(self.popl())
400            n -= 1

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        while n > 0:
407            self.pushl(self.popr())
408            n -= 1

Rotate CA arguments right n times.

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

Returns current capacity of the CA.

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

Empty the CA, keep current capacity.

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

Returns fractional capacity of the CA.

def resize(self, minimum_capacity: int = 2) -> None:
489    def resize(self, minimum_capacity: int = 2) -> None:
490        """Compact `CA` and resize to `minimum_capacity` if necessary.
491
492        To just compact the `CA`, do not provide a minimum capacity.
493
494        """
495        self._compact_storage_capacity()
496        if (min_cap := minimum_capacity) > self._cap:
497            self._cap, self._data = min_cap, self._data + [None] * (min_cap - self._cap)
498            if self._cnt == 0:
499                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]:
502def ca[D](*ds: D) -> CA[D]:
503    """Function to produce a `CA` array from a variable number of arguments."""
504    return CA(ds)

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