dtools.circular_array.ca

Indexable circular array data structure module.

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

Push data from the left onto the ca.

def pushR(self, *ds: 'D') -> None:
220    def pushR(self, *ds: D) -> None:
221        """Push data from the right onto the ca."""
222        for d in ds:
223            if self._cnt == self._cap:
224                self._double_storage_capacity()
225            self._rear = (self._rear + 1) % self._cap
226            self._data[self._rear], self._cnt = d, self._cnt + 1

Push data from the right onto the ca.

def popL(self) -> 'D | Never':
228    def popL(self) -> D|Never:
229        """Pop one value off the left side of the ca.
230
231        * raises `ValueError` when called on an empty ca
232        """
233        if self._cnt > 1:
234            d, self._data[self._front], self._front, self._cnt = \
235                self._data[self._front], None, (self._front+1) % self._cap, self._cnt - 1
236        elif self._cnt < 1:
237            msg = 'Method popL called on an empty ca'
238            raise ValueError(msg)
239        else:
240            d, self._data[self._front], self._cnt, self._front, self._rear = \
241                self._data[self._front], None, 0, 0, self._cap - 1
242        return cast(D, d)

Pop one value off the left side of the ca.

  • raises ValueError when called on an empty ca
def popR(self) -> 'D | Never':
244    def popR(self) -> D|Never:
245        """Pop one value off the right side of the ca.
246
247        * raises `ValueError` when called on an empty ca
248        """
249        if self._cnt > 0:
250            d, self._data[self._rear], self._rear, self._cnt = \
251                self._data[self._rear], None, (self._rear - 1) % self._cap, self._cnt - 1
252        elif self._cnt < 1:
253            msg = 'Method popR called on an empty ca'
254            raise ValueError(msg)
255        else:
256            d, self._data[self._front], self._cnt, self._front, self._rear = \
257                self._data[self._front], None, 0, 0, self._cap - 1
258        return cast(D, d)

Pop one value off the right side of the ca.

  • raises ValueError when called on an empty ca
def popLD(self, default: 'D', /) -> 'D':
260    def popLD(self, default: D, /) -> D:
261        """Pop one value from left, provide a mandatory default value.
262
263        * safe version of popL
264        * returns a default value in the event the `ca` is empty
265        """
266        try:
267            return self.popL()
268        except ValueError:
269            return default

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

  • safe version of popL
  • returns a default value in the event the ca is empty
def popRD(self, default: 'D', /) -> 'D':
271    def popRD(self, default: D, /) -> D:
272        """Pop one value from right, provide a mandatory default value.
273
274        * safe version of popR
275        * returns a default value in the event the `ca` is empty
276        """
277        try:
278            return self.popR()
279        except ValueError:
280            return default

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

  • safe version of popR
  • returns a default value in the event the ca is empty
def popLT(self, max: int) -> 'tuple[D, ...]':
282    def popLT(self, max: int) -> tuple[D, ...]:
283        """Pop multiple values from left side of ca.
284
285        * returns the results in a tuple of type `tuple[~D, ...]`
286        * returns an empty tuple if `ca` is empty
287        * pop no more that `max` values
288        * will pop less if `ca` becomes empty
289        """
290        ds: list[D] = []
291
292        while max > 0:
293            try:
294                ds.append(self.popL())
295            except ValueError:
296                break
297            else:
298                max -= 1
299
300        return tuple(ds)

Pop multiple values from left side of ca.

  • returns the results in a tuple of type tuple[~D, ...]
  • returns an empty tuple if ca is empty
  • pop no more that max values
  • will pop less if ca becomes empty
def popRT(self, max: int) -> 'tuple[D, ...]':
302    def popRT(self, max: int) -> tuple[D, ...]:
303        """Pop multiple values from right side of `ca`.
304
305        * returns the results in a tuple of type `tuple[~D, ...]`
306        * returns an empty tuple if `ca` is empty
307        * pop no more that `max` values
308        * will pop less if `ca` becomes empty
309        """
310        ds: list[D] = []
311        while max > 0:
312            try:
313                ds.append(self.popR())
314            except ValueError:
315                break
316            else:
317                max -= 1
318
319        return tuple(ds)

Pop multiple values from right side of ca.

  • returns the results in a tuple of type tuple[~D, ...]
  • returns an empty tuple if ca is empty
  • pop no more that max values
  • will pop less if ca becomes empty
def rotL(self, n: int = 1) -> None:
321    def rotL(self, n: int=1) -> None:
322        """Rotate ca arguments left n times."""
323        if self._cnt < 2:
324            return
325        while n > 0:
326            self.pushR(self.popL())
327            n -= 1

Rotate ca arguments left n times.

def rotR(self, n: int = 1) -> None:
329    def rotR(self, n: int=1) -> None:
330        """Rotate ca arguments right n times."""
331        if self._cnt < 2:
332            return
333        while n > 0:
334            self.pushL(self.popR())
335            n -= 1

Rotate ca arguments right n times.

def map(self, f: 'Callable[[D], U]', /) -> 'ca[U]':
337    def map[U](self, f: Callable[[D], U], /) -> ca[U]:
338        """Apply function f over contents, returns new `ca` instance.
339
340        * parameter `f` function of type `f[~D, ~U] -> ca[~U]`
341        * returns a new instance of type `ca[~U]`
342        """
343        return ca(map(f, self))

Apply function f over contents, returns new ca instance.

  • parameter f function of type f[~D, ~U] -> ca[~U]
  • returns a new instance of type ca[~U]
def foldL(self, f: 'Callable[[L, D], L]', /, initial: 'L | None' = None) -> 'L':
345    def foldL[L](self, f: Callable[[L, D], L], /, initial: L|None=None) -> L:
346        """Left fold ca via function and optional initial value.
347
348        * parameter `f` function of type `f[~L, ~D] -> ~L`
349          * the first argument to `f` is for the accumulated value.
350        * parameter `initial` is an optional initial value
351        * returns the reduced value of type `~L`
352          * note that `~L` and `~D` can be the same type
353          * if an initial value is not given then by necessity `~L = ~D`
354        * raises `ValueError` when called on an empty `ca` and `initial` not given
355        """
356        if self._cnt == 0:
357            if initial is None:
358                msg = 'Method foldL called on an empty ca without an initial value.'
359                raise ValueError(msg)
360            else:
361                return initial
362        else:
363            if initial is None:
364                acc = cast(L, self[0])  # in this case D = L
365                for idx in range(1, self._cnt):
366                    acc = f(acc, self[idx])
367                return acc
368            else:
369                acc = initial
370                for d in self:
371                    acc = f(acc, d)
372                return acc

Left fold ca via function and optional initial value.

  • parameter f function of type f[~L, ~D] -> ~L
    • the first argument to f is for the accumulated value.
  • parameter initial is an optional initial value
  • returns the reduced value of type ~L
    • note that ~L and ~D can be the same type
    • if an initial value is not given then by necessity ~L = ~D
  • raises ValueError when called on an empty ca and initial not given
def foldR(self, f: 'Callable[[D, R], R]', /, initial: 'R | None' = None) -> 'R':
374    def foldR[R](self, f: Callable[[D, R], R], /, initial: R|None=None) -> R:
375        """Right fold ca via function and optional initial value.
376
377        * parameter `f` function of type `f[~D, ~R] -> ~R`
378          * the second argument to f is for the accumulated value
379        * parameter `initial` is an optional initial value
380        * returns the reduced value of type `~R`
381          * note that `~R` and `~D` can be the same type
382          * if an initial value is not given then by necessity `~R = ~D`
383        * raises `ValueError` when called on an empty `ca` and `initial` not given
384        """
385        if self._cnt == 0:
386            if initial is None:
387                msg = 'Method foldR called on an empty ca without an initial value.'
388                raise ValueError(msg)
389            else:
390                return initial
391        else:
392            if initial is None:
393                acc = cast(R, self[-1])  # in this case D = R
394                for idx in range(self._cnt-2, -1, -1):
395                    acc = f(self[idx], acc)
396                return acc
397            else:
398                acc = initial
399                for d in reversed(self):
400                    acc = f(d, acc)
401                return acc

Right fold ca via function and optional initial value.

  • parameter f function of type f[~D, ~R] -> ~R
    • the second argument to f is for the accumulated value
  • parameter initial is an optional initial value
  • returns the reduced value of type ~R
    • note that ~R and ~D can be the same type
    • if an initial value is not given then by necessity ~R = ~D
  • raises ValueError when called on an empty ca and initial not given
def capacity(self) -> int:
403    def capacity(self) -> int:
404        """Returns current capacity of the ca."""
405        return self._cap

Returns current capacity of the ca.

def empty(self) -> None:
407    def empty(self) -> None:
408        """Empty the ca, keep current capacity."""
409        self._data, self._front, self._rear = [None]*self._cap, 0, self._cap

Empty the ca, keep current capacity.

def fractionFilled(self) -> float:
411    def fractionFilled(self) -> float:
412        """Returns fractional capacity of the ca."""
413        return self._cnt/self._cap

Returns fractional capacity of the ca.

def resize(self, minimum_capacity: int = 2) -> None:
415    def resize(self, minimum_capacity: int=2) -> None:
416        """Compact `ca` and resize to `min_cap` if necessary.
417
418        * to just compact the `ca`, do not provide a min_cap
419        """
420        self._compact_storage_capacity()
421        if (min_cap := minimum_capacity) > self._cap:
422            self._cap, self._data = \
423                min_cap, self._data + [None]*(min_cap - self._cap)
424            if self._cnt == 0:
425                self._front, self._rear = 0, self._cap - 1

Compact ca and resize to min_cap if necessary.

  • to just compact the ca, do not provide a min_cap
def CA(*ds: 'D') -> 'ca[D]':
427def CA[D](*ds: D) -> ca[D]:
428    """Function to produce a `ca` array from a variable number of arguments."""
429    return ca(ds)

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