dtools.circular_array.ca

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

Indexable circular array data structure

  • generic, stateful data structure
  • amortized O(1) pushing and popping from either end
  • O(1) random access any element
  • will resize itself as needed
  • sliceable
  • makes defensive copies of contents for the purposes of iteration
  • in boolean context returns
    • True 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])
52    def __init__(self, *dss: Iterable[D]) -> None:
53        if len(dss) < 2:
54            self._data: list[D | None] = (
55                [None] + cast(list[D | None], list(*dss)) + [None]
56            )
57        else:
58            msg = f'CA expected at most 1 argument, got {len(dss)}'
59            raise TypeError(msg)
60        self._cap = cap = len(self._data)
61        self._cnt = cap - 2
62        if cap == 2:
63            self._front = 0
64            self._rear = 1
65        else:
66            self._front = 1
67            self._rear = cap - 2
def pushl(self, *ds: ~D) -> None:
262    def pushl(self, *ds: D) -> None:
263        """Push left.
264
265        - push data from the left onto the CA
266
267        """
268        for d in ds:
269            if self._cnt == self._cap:
270                self._double_storage_capacity()
271            self._front = (self._front - 1) % self._cap
272            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:
274    def pushr(self, *ds: D) -> None:
275        """Push right.
276
277        - push data from the right onto the CA
278
279        """
280        for d in ds:
281            if self._cnt == self._cap:
282                self._double_storage_capacity()
283            self._rear = (self._rear + 1) % self._cap
284            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]:
286    def popl(self) -> D | Never:
287        """Pop left.
288
289        - pop one value off the left side of the `CA`
290        - raises `ValueError` when called on an empty `CA`
291
292        """
293        if self._cnt > 1:
294            d, self._data[self._front], self._front, self._cnt = (
295                self._data[self._front],
296                None,
297                (self._front + 1) % self._cap,
298                self._cnt - 1,
299            )
300        elif self._cnt == 1:
301            d, self._data[self._front], self._cnt, self._front, self._rear = (
302                self._data[self._front],
303                None,
304                0,
305                0,
306                self._cap - 1,
307            )
308        else:
309            msg = 'Method popl called on an empty CA'
310            raise ValueError(msg)
311        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]:
313    def popr(self) -> D | Never:
314        """Pop right
315
316        - pop one value off the right side of the `CA`
317        - raises `ValueError` when called on an empty `CA`
318
319        """
320        if self._cnt > 1:
321            d, self._data[self._rear], self._rear, self._cnt = (
322                self._data[self._rear],
323                None,
324                (self._rear - 1) % self._cap,
325                self._cnt - 1,
326            )
327        elif self._cnt == 1:
328            d, self._data[self._front], self._cnt, self._front, self._rear = (
329                self._data[self._front],
330                None,
331                0,
332                0,
333                self._cap - 1,
334            )
335        else:
336            msg = 'Method popr called on an empty CA'
337            raise ValueError(msg)
338        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:
340    def popld(self, default: D, /) -> D:
341        """Pop one value from left, provide a mandatory default value.
342
343        - safe version of popl
344        - returns the default value if `CA` is empty
345
346        """
347        try:
348            return self.popl()
349        except ValueError:
350            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:
352    def poprd(self, default: D, /) -> D:
353        """Pop one value from right, provide a mandatory default value.
354
355        - safe version of popr
356        - returns the default value if `CA` is empty
357
358        """
359        try:
360            return self.popr()
361        except ValueError:
362            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, ...]:
364    def poplt(self, maximum: int, /) -> tuple[D, ...]:
365        """Pop multiple values from left side of `CA`.
366
367        - returns the results in a tuple of type
368        - pop no more that `maximum` values
369        - will pop less if `CA` becomes empty
370
371        """
372        ds: list[D] = []
373
374        while maximum > 0:
375            try:
376                ds.append(self.popl())
377            except ValueError:
378                break
379            else:
380                maximum -= 1
381
382        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, ...]:
384    def poprt(self, maximum: int, /) -> tuple[D, ...]:
385        """Pop multiple values from right side of `CA`.
386
387        - returns the results in a tuple
388        - pop no more that `maximum` values
389        - will pop less if `CA` becomes empty
390
391        """
392        ds: list[D] = []
393        while maximum > 0:
394            try:
395                ds.append(self.popr())
396            except ValueError:
397                break
398            else:
399                maximum -= 1
400
401        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:
403    def rotl(self, n: int = 1, /) -> None:
404        """Rotate `CA` components to the left n times."""
405        if self._cnt < 2:
406            return
407        for _ in range(n, 0, -1):
408            self.pushr(self.popl())

Rotate CA components to the left n times.

def rotr(self, n: int = 1, /) -> None:
410    def rotr(self, n: int = 1, /) -> None:
411        """Rotate `CA` components to the right n times."""
412        if self._cnt < 2:
413            return
414        for _ in range(n, 0, -1):
415            self.pushl(self.popr())

Rotate CA components to the right n times.

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

Returns current capacity of the CA.

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

Empty the CA, keep current capacity.

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

Returns fractional capacity of the CA.

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

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