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

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)
  • raises IndexError for out-of-bounds indexing
  • raises ValueError for popping from or folding an empty ca
  • raises TypeError if more than 2 arguments are passed to constructor
ca(*dss: 'Iterable[D]')
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
def pushL(self, *ds: 'D') -> None:
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

Push data from the left onto the ca.

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

Push data from the right onto the ca.

def popL(self) -> 'D | Never':
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)

Pop one value off the left side of the ca.

  • raises ValueError when called on an empty ca
def popR(self) -> 'D | Never':
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)

Pop one value off the right side of the ca.

  • raises ValueError when called on an empty ca
def popLD(self, default: 'D', /) -> 'D':
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

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

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

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

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

Rotate ca arguments left n times.

def rotR(self, n: int = 1) -> None:
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

Rotate ca arguments right n times.

def map(self, f: 'Callable[[D], U]', /) -> 'ca[U]':
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))

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

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

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:
402    def capacity(self) -> int:
403        """Returns current capacity of the ca."""
404        return self._cap

Returns current capacity of the ca.

def empty(self) -> None:
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

Empty the ca, keep current capacity.

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

Returns fractional capacity of the ca.

def resize(self, minimum_capacity: int = 2) -> None:
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

Compact ca and resize to min_cap if necessary.

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

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