dtools.circular_array

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

Indexable circular array 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])
68    def __init__(self, *dss: Iterable[D]) -> None:
69        if len(dss) < 2:
70            self._data: list[D | None] = (
71                [None] + cast(list[D | None], list(*dss)) + [None]
72            )
73        else:
74            msg = f'CA expected at most 1 argument, got {len(dss)}'
75            raise TypeError(msg)
76        self._cap = cap = len(self._data)
77        self._cnt = cap - 2
78        if cap == 2:
79            self._front = 0
80            self._rear = 1
81        else:
82            self._front = 1
83            self._rear = cap - 2
def pushl(self, *ds: ~D) -> None:
278    def pushl(self, *ds: D) -> None:
279        """Push left.
280
281        - push data from the left onto the CA
282
283        """
284        for d in ds:
285            if self._cnt == self._cap:
286                self._double_storage_capacity()
287            self._front = (self._front - 1) % self._cap
288            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:
290    def pushr(self, *ds: D) -> None:
291        """Push right.
292
293        - push data from the right onto the CA
294
295        """
296        for d in ds:
297            if self._cnt == self._cap:
298                self._double_storage_capacity()
299            self._rear = (self._rear + 1) % self._cap
300            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]:
302    def popl(self) -> D | Never:
303        """Pop left.
304
305        - pop one value off the left side of the `CA`
306        - raises `ValueError` when called on an empty `CA`
307
308        """
309        if self._cnt > 1:
310            d, self._data[self._front], self._front, self._cnt = (
311                self._data[self._front],
312                None,
313                (self._front + 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 popl called on an empty CA'
326            raise ValueError(msg)
327        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]:
329    def popr(self) -> D | Never:
330        """Pop right
331
332        - pop one value off the right side of the `CA`
333        - raises `ValueError` when called on an empty `CA`
334
335        """
336        if self._cnt > 1:
337            d, self._data[self._rear], self._rear, self._cnt = (
338                self._data[self._rear],
339                None,
340                (self._rear - 1) % self._cap,
341                self._cnt - 1,
342            )
343        elif self._cnt == 1:
344            d, self._data[self._front], self._cnt, self._front, self._rear = (
345                self._data[self._front],
346                None,
347                0,
348                0,
349                self._cap - 1,
350            )
351        else:
352            msg = 'Method popr called on an empty CA'
353            raise ValueError(msg)
354        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:
356    def popld(self, default: D, /) -> D:
357        """Pop one value from left, provide a mandatory default value.
358
359        - safe version of popl
360        - returns the default value if `CA` is empty
361
362        """
363        try:
364            return self.popl()
365        except ValueError:
366            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:
368    def poprd(self, default: D, /) -> D:
369        """Pop one value from right, provide a mandatory default value.
370
371        - safe version of popr
372        - returns the default value if `CA` is empty
373
374        """
375        try:
376            return self.popr()
377        except ValueError:
378            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, ...]:
380    def poplt(self, maximum: int, /) -> tuple[D, ...]:
381        """Pop multiple values from left side of `CA`.
382
383        - returns the results in a tuple of type
384        - pop no more that `maximum` values
385        - will pop less if `CA` becomes empty
386
387        """
388        ds: list[D] = []
389
390        while maximum > 0:
391            try:
392                ds.append(self.popl())
393            except ValueError:
394                break
395            else:
396                maximum -= 1
397
398        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, ...]:
400    def poprt(self, maximum: int, /) -> tuple[D, ...]:
401        """Pop multiple values from right side of `CA`.
402
403        - returns the results in a tuple
404        - pop no more that `maximum` values
405        - will pop less if `CA` becomes empty
406
407        """
408        ds: list[D] = []
409        while maximum > 0:
410            try:
411                ds.append(self.popr())
412            except ValueError:
413                break
414            else:
415                maximum -= 1
416
417        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:
419    def rotl(self, n: int = 1, /) -> None:
420        """Rotate `CA` components to the left n times."""
421        if self._cnt < 2:
422            return
423        for _ in range(n, 0, -1):
424            self.pushr(self.popl())

Rotate CA components to the left n times.

def rotr(self, n: int = 1, /) -> None:
426    def rotr(self, n: int = 1, /) -> None:
427        """Rotate `CA` components to the right n times."""
428        if self._cnt < 2:
429            return
430        for _ in range(n, 0, -1):
431            self.pushl(self.popr())

Rotate CA components to the right n times.

def map(self, f: Callable[[~D], ~U], /) -> CA[~U]:
433    def map[U](self, f: Callable[[D], U], /) -> CA[U]:
434        """Apply function `f` over the `CA` contents,
435
436        - returns a new `CA` instance
437
438        """
439        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:
441    def foldl[L](self, f: Callable[[L, D], L], initial: L | None = None, /) -> L:
442        """Left fold `CA` with function `f` and an optional `initial` value.
443
444        - first argument to `f` is for the accumulated value
445        - returns the reduced value of type `~L`
446          - note that `~L` and `~D` can be the same type
447          - if an initial value is not given then by necessity `~L = ~D`
448        - raises `ValueError` when called on an empty `ca` and `initial` not given
449
450        """
451        if self._cnt == 0:
452            if initial is None:
453                msg = 'Method foldl called on an empty `CA` without an initial value.'
454                raise ValueError(msg)
455            return initial
456
457        if initial is None:
458            acc = cast(L, self[0])  # in this case D = L
459            for idx in range(1, self._cnt):
460                acc = f(acc, self[idx])
461            return acc
462
463        acc = initial
464        for d in self:
465            acc = f(acc, d)
466        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:
468    def foldr[R](self, f: Callable[[D, R], R], initial: R | None = None, /) -> R:
469        """Right fold `CA` with function `f` and an optional `initial` value.
470
471        - second argument to f is for the accumulated value
472        - returns the reduced value of type `~R`
473          - note that `~R` and `~D` can be the same type
474          - if an initial value is not given then by necessity `~R = ~D`
475        - raises `ValueError` when called on an empty `CA` and `initial` not given
476
477        """
478        if self._cnt == 0:
479            if initial is None:
480                msg = 'Method foldr called on empty `CA` without initial value.'
481                raise ValueError(msg)
482            return initial
483
484        if initial is None:
485            acc = cast(R, self[-1])  # in this case D = R
486            for idx in range(self._cnt - 2, -1, -1):
487                acc = f(self[idx], acc)
488            return acc
489
490        acc = initial
491        for d in reversed(self):
492            acc = f(d, acc)
493        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:
495    def capacity(self) -> int:
496        """Returns current capacity of the `CA`."""
497        return self._cap

Returns current capacity of the CA.

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

Empty the CA, keep current capacity.

def fraction_filled(self) -> float:
503    def fraction_filled(self) -> float:
504        """Returns fractional capacity of the `CA`."""
505        return self._cnt / self._cap

Returns fractional capacity of the CA.

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

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