grscheller.circular_array.ca

Indexable circular array data structure module.

  1# Copyright 2023-2024 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 typing import Callable, cast, final, Iterable, Iterator
 18from typing import Sequence, TypeVar, Never
 19
 20__all__ = ['ca', 'CA']
 21
 22class ca[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    * makes defensive copies of contents for the purposes of iteration
 31    * not sliceable
 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      * as Python tuples, lists, and dicts 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', '_count', '_capacity', '_front', '_rear'
 41
 42    def __init__(self, *dss: Iterable[D]) -> None:
 43        if len(dss) < 2:
 44            self._data: list[D|None] = [None] + \
 45                                       cast(list[D|None], list(*dss)) + \
 46                                       [None]
 47        else:
 48            msg = f'ca expected at most 1 argument, got {len(dss)}'
 49            raise TypeError(msg)
 50        self._capacity = capacity = len(self._data)
 51        self._count = capacity - 2
 52        if capacity == 2:
 53            self._front = 0
 54            self._rear = 1
 55        else:
 56            self._front = 1
 57            self._rear = capacity - 2
 58
 59    def _double_storage_capacity(self) -> None:
 60        if self._front <= self._rear:
 61            self._data += [None]*self._capacity
 62            self._capacity *= 2
 63        else:
 64            self._data = self._data[:self._front] + [None]*self._capacity + self._data[self._front:]
 65            self._front, self._capacity = self._front + self._capacity, 2*self._capacity
 66
 67    def _compact_storage_capacity(self) -> None:
 68        """Compact the ca."""
 69        match self._count:
 70            case 0:
 71                self._capacity, self._front, self._rear, self._data = 2, 0, 1, [None, None]
 72            case 1:
 73                self._capacity, self._front, self._rear, self._data = 3, 1, 1, [None, self._data[self._front], None]
 74            case _:
 75                if self._front <= self._rear:
 76                    self._capacity, self._front, self._rear, self._data = \
 77                        self._count+2, 1, self._count, [None] + self._data[self._front:self._rear+1] + [None]
 78                else:
 79                    self._capacity, self._front, self._rear, self._data = \
 80                        self._count+2, 1, self._count, [None] + self._data[self._front:] + self._data[:self._rear+1] + [None]
 81
 82    def __iter__(self) -> Iterator[D]:
 83        if self._count > 0:
 84            capacity, rear, position, current_state = \
 85                self._capacity, 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._count > 0:
 94            capacity, front, position, current_state = \
 95                self._capacity, 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._count > 0
110
111    def __len__(self) -> int:
112        return self._count
113
114    def __getitem__(self, index: int, /) -> D:
115        cnt = self._count
116        if 0 <= index < cnt:
117            return cast(D, self._data[(self._front + index) % self._capacity])
118        elif -cnt <= index < 0:
119            return cast(D, self._data[(self._front + cnt + index) % self._capacity])
120        else:
121            if cnt > 0:
122                msg1 = 'Out of bounds: '
123                msg2 = f'index = {index} not between {-cnt} and {cnt-1} '
124                msg3 = 'while getting value from a ca.'
125                raise IndexError(msg1 + msg2 + msg3)
126            else:
127                msg0 = 'Trying to get a value from an empty ca.'
128                raise IndexError(msg0)
129
130    def __setitem__(self, index: int, value: D, /) -> None:
131        cnt = self._count
132        if 0 <= index < cnt:
133            self._data[(self._front + index) % self._capacity] = value
134        elif -cnt <= index < 0:
135            self._data[(self._front + cnt + index) % self._capacity] = value
136        else:
137            if cnt > 0:
138                msg1 = 'Out of bounds: '
139                msg2 = f'index = {index} not between {-cnt} and {cnt-1} '
140                msg3 = 'while setting value from a ca.'
141                raise IndexError(msg1 + msg2 + msg3)
142            else:
143                msg0 = 'Trying to set a value from an empty ca.'
144                raise IndexError(msg0)
145
146    def __eq__(self, other: object, /) -> bool:
147        if self is other:
148            return True
149        if not isinstance(other, type(self)):
150            return False
151
152        frontL, frontR, \
153        countL, countR, \
154        capacityL, capacityR = \
155            self._front, other._front, \
156            self._count, other._count, \
157            self._capacity, other._capacity
158
159        if countL != countR:
160            return False
161
162        for nn in range(countL):
163            if self._data[(frontL+nn)%capacityL] is other._data[(frontR+nn)%capacityR]:
164                continue
165            if self._data[(frontL+nn)%capacityL] != other._data[(frontR+nn)%capacityR]:
166                return False
167        return True
168
169    def pushL(self, *ds: D) -> None:
170        """Push data from the left onto the ca."""
171        for d in ds:
172            if self._count == self._capacity:
173                self._double_storage_capacity()
174            self._front = (self._front - 1) % self._capacity
175            self._data[self._front], self._count = d, self._count + 1
176
177    def pushR(self, *ds: D) -> None:
178        """Push data from the right onto the ca."""
179        for d in ds:
180            if self._count == self._capacity:
181                self._double_storage_capacity()
182            self._rear = (self._rear + 1) % self._capacity
183            self._data[self._rear], self._count = d, self._count + 1
184
185    def popL(self) -> D|Never:
186        """Pop one value off the left side of the ca.
187
188        * raises `ValueError` when called on an empty ca
189        """
190        if self._count > 1:
191            d, self._data[self._front], self._front, self._count = \
192                self._data[self._front], None, (self._front+1) % self._capacity, self._count - 1
193        elif self._count < 1:
194            msg = 'Method popL called on an empty ca'
195            raise ValueError(msg)
196        else:
197            d, self._data[self._front], self._count, self._front, self._rear = \
198                self._data[self._front], None, 0, 0, self._capacity - 1
199        return cast(D, d)
200
201    def popR(self) -> D|Never:
202        """Pop one value off the right side of the ca.
203
204        * raises `ValueError` when called on an empty ca
205        """
206        if self._count > 0:
207            d, self._data[self._rear], self._rear, self._count = \
208                self._data[self._rear], None, (self._rear - 1) % self._capacity, self._count - 1
209        elif self._count < 1:
210            msg = 'Method popR called on an empty ca'
211            raise ValueError(msg)
212        else:
213            d, self._data[self._front], self._count, self._front, self._rear = \
214                self._data[self._front], None, 0, 0, self._capacity - 1
215        return cast(D, d)
216
217    def popLD(self, default: D, /) -> D:
218        """Pop one value from left, provide a mandatory default value.
219
220        * safe version of popL
221        * returns a default value in the event the `ca` is empty
222        """
223        try:
224            return self.popL()
225        except ValueError:
226            return default
227
228    def popRD(self, default: D, /) -> D:
229        """Pop one value from right, provide a mandatory default value.
230
231        * safe version of popR
232        * returns a default value in the event the `ca` is empty
233        """
234        try:
235            return self.popR()
236        except ValueError:
237            return default
238
239    def popLT(self, max: int) -> tuple[D, ...]:
240        """Pop multiple values from left side of ca.
241
242        * returns the results in a tuple of type `tuple[~D, ...]`
243        * returns an empty tuple if `ca` is empty
244        * pop no more that `max` values
245        * will pop less if `ca` becomes empty
246        """
247        ds: list[D] = []
248
249        while max > 0:
250            try:
251                ds.append(self.popL())
252            except ValueError:
253                break
254            else:
255                max -= 1
256
257        return tuple(ds)
258
259    def popRT(self, max: int) -> tuple[D, ...]:
260        """Pop multiple values from right side of `ca`.
261
262        * returns the results in a tuple of type `tuple[~D, ...]`
263        * returns an empty tuple if `ca` is empty
264        * pop no more that `max` values
265        * will pop less if `ca` becomes empty
266        """
267        ds: list[D] = []
268        while max > 0:
269            try:
270                ds.append(self.popR())
271            except ValueError:
272                break
273            else:
274                max -= 1
275
276        return tuple(ds)
277
278    def map[U](self, f: Callable[[D], U], /) -> ca[U]:
279        """Apply function f over contents, returns new `ca` instance.
280
281        * parameter `f` function of type `f[~D, ~U] -> ca[~U]`
282        * returns a new instance of type `ca[~U]`
283        """
284        return ca(map(f, self))
285
286    def foldL[L](self, f: Callable[[L, D], L], /, initial: L|None=None) -> L:
287        """Left fold ca via function and optional initial value.
288
289        * parameter `f` function of type `f[~L, ~D] -> ~L`
290          * the first argument to `f` is for the accumulated value.
291        * parameter `initial` is an optional initial value
292        * returns the reduced value of type `~L`
293          * note that `~L` and `~D` can be the same type
294          * if an initial value is not given then by necessity `~L = ~D` 
295        * raises `ValueError` when called on an empty `ca` and `initial` not given
296        """
297        if self._count == 0:
298            if initial is None:
299                msg = 'Method foldL called on an empty ca without an initial value.'
300                raise ValueError(msg)
301            else:
302                return initial
303        else:
304            if initial is None:
305                acc = cast(L, self[0])  # in this case D = L
306                for idx in range(1, self._count):
307                    acc = f(acc, self[idx])
308                return acc
309            else:
310                acc = initial
311                for d in self:
312                    acc = f(acc, d)
313                return acc
314
315    def foldR[R](self, f: Callable[[D, R], R], /, initial: R|None=None) -> R:
316        """Right fold ca via function and optional initial value.
317
318        * parameter `f` function of type `f[~D, ~R] -> ~R`
319          * the second argument to f is for the accumulated value
320        * parameter `initial` is an optional initial value
321        * returns the reduced value of type `~R`
322          * note that `~R` and `~D` can be the same type
323          * if an initial value is not given then by necessity `~R = ~D`
324        * raises `ValueError` when called on an empty `ca` and `initial` not given
325        """
326        if self._count == 0:
327            if initial is None:
328                msg = 'Method foldR called on an empty ca without an initial value.'
329                raise ValueError(msg)
330            else:
331                return initial
332        else:
333            if initial is None:
334                acc = cast(R, self[-1])  # in this case D = R
335                for idx in range(self._count-2, -1, -1):
336                    acc = f(self[idx], acc)
337                return acc
338            else:
339                acc = initial
340                for d in reversed(self):
341                    acc = f(d, acc)
342                return acc
343
344    def capacity(self) -> int:
345        """Returns current capacity of the ca."""
346        return self._capacity
347
348    def empty(self) -> None:
349        """Empty the ca, keep current capacity."""
350        self._data, self._front, self._rear = [None]*self._capacity, 0, self._capacity-1
351
352    def fractionFilled(self) -> float:
353        """Returns fractional capacity of the ca."""
354        return self._count/self._capacity
355
356    def resize(self, min_capacity: int=2) -> None:
357        """Compact `ca` and resize to `min_capacity` if necessary.
358
359        * to just compact the `ca`, do not provide a min_capacity
360        """
361        self._compact_storage_capacity()
362        if min_capacity > self._capacity:
363            self._capacity, self._data = \
364                min_capacity, self._data + [None]*(min_capacity-self._capacity)
365            if self._count == 0:
366                self._front, self._rear = 0, self._capacity - 1
367
368def CA[U](*ds: U) -> ca[U]:
369    """Function to produce a `ca` array from a variable number of arguments."""
370    return ca(ds)
class ca(typing.Generic[D]):
 23class ca[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    * makes defensive copies of contents for the purposes of iteration
 32    * not sliceable
 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      * as Python tuples, lists, and dicts do
 36    * raises `IndexError` for out-of-bounds indexing
 37    * raises `ValueError` for popping from or folding an empty `ca`
 38    * raises `TypeError` if more than 2 arguments are passed to constructor
 39
 40    """
 41    __slots__ = '_data', '_count', '_capacity', '_front', '_rear'
 42
 43    def __init__(self, *dss: Iterable[D]) -> None:
 44        if len(dss) < 2:
 45            self._data: list[D|None] = [None] + \
 46                                       cast(list[D|None], list(*dss)) + \
 47                                       [None]
 48        else:
 49            msg = f'ca expected at most 1 argument, got {len(dss)}'
 50            raise TypeError(msg)
 51        self._capacity = capacity = len(self._data)
 52        self._count = capacity - 2
 53        if capacity == 2:
 54            self._front = 0
 55            self._rear = 1
 56        else:
 57            self._front = 1
 58            self._rear = capacity - 2
 59
 60    def _double_storage_capacity(self) -> None:
 61        if self._front <= self._rear:
 62            self._data += [None]*self._capacity
 63            self._capacity *= 2
 64        else:
 65            self._data = self._data[:self._front] + [None]*self._capacity + self._data[self._front:]
 66            self._front, self._capacity = self._front + self._capacity, 2*self._capacity
 67
 68    def _compact_storage_capacity(self) -> None:
 69        """Compact the ca."""
 70        match self._count:
 71            case 0:
 72                self._capacity, self._front, self._rear, self._data = 2, 0, 1, [None, None]
 73            case 1:
 74                self._capacity, self._front, self._rear, self._data = 3, 1, 1, [None, self._data[self._front], None]
 75            case _:
 76                if self._front <= self._rear:
 77                    self._capacity, self._front, self._rear, self._data = \
 78                        self._count+2, 1, self._count, [None] + self._data[self._front:self._rear+1] + [None]
 79                else:
 80                    self._capacity, self._front, self._rear, self._data = \
 81                        self._count+2, 1, self._count, [None] + self._data[self._front:] + self._data[:self._rear+1] + [None]
 82
 83    def __iter__(self) -> Iterator[D]:
 84        if self._count > 0:
 85            capacity, rear, position, current_state = \
 86                self._capacity, 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._count > 0:
 95            capacity, front, position, current_state = \
 96                self._capacity, 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._count > 0
111
112    def __len__(self) -> int:
113        return self._count
114
115    def __getitem__(self, index: int, /) -> D:
116        cnt = self._count
117        if 0 <= index < cnt:
118            return cast(D, self._data[(self._front + index) % self._capacity])
119        elif -cnt <= index < 0:
120            return cast(D, self._data[(self._front + cnt + index) % self._capacity])
121        else:
122            if cnt > 0:
123                msg1 = 'Out of bounds: '
124                msg2 = f'index = {index} not between {-cnt} and {cnt-1} '
125                msg3 = 'while getting value from a ca.'
126                raise IndexError(msg1 + msg2 + msg3)
127            else:
128                msg0 = 'Trying to get a value from an empty ca.'
129                raise IndexError(msg0)
130
131    def __setitem__(self, index: int, value: D, /) -> None:
132        cnt = self._count
133        if 0 <= index < cnt:
134            self._data[(self._front + index) % self._capacity] = value
135        elif -cnt <= index < 0:
136            self._data[(self._front + cnt + index) % self._capacity] = value
137        else:
138            if cnt > 0:
139                msg1 = 'Out of bounds: '
140                msg2 = f'index = {index} not between {-cnt} and {cnt-1} '
141                msg3 = 'while setting value from a ca.'
142                raise IndexError(msg1 + msg2 + msg3)
143            else:
144                msg0 = 'Trying to set a value from an empty ca.'
145                raise IndexError(msg0)
146
147    def __eq__(self, other: object, /) -> bool:
148        if self is other:
149            return True
150        if not isinstance(other, type(self)):
151            return False
152
153        frontL, frontR, \
154        countL, countR, \
155        capacityL, capacityR = \
156            self._front, other._front, \
157            self._count, other._count, \
158            self._capacity, other._capacity
159
160        if countL != countR:
161            return False
162
163        for nn in range(countL):
164            if self._data[(frontL+nn)%capacityL] is other._data[(frontR+nn)%capacityR]:
165                continue
166            if self._data[(frontL+nn)%capacityL] != other._data[(frontR+nn)%capacityR]:
167                return False
168        return True
169
170    def pushL(self, *ds: D) -> None:
171        """Push data from the left onto the ca."""
172        for d in ds:
173            if self._count == self._capacity:
174                self._double_storage_capacity()
175            self._front = (self._front - 1) % self._capacity
176            self._data[self._front], self._count = d, self._count + 1
177
178    def pushR(self, *ds: D) -> None:
179        """Push data from the right onto the ca."""
180        for d in ds:
181            if self._count == self._capacity:
182                self._double_storage_capacity()
183            self._rear = (self._rear + 1) % self._capacity
184            self._data[self._rear], self._count = d, self._count + 1
185
186    def popL(self) -> D|Never:
187        """Pop one value off the left side of the ca.
188
189        * raises `ValueError` when called on an empty ca
190        """
191        if self._count > 1:
192            d, self._data[self._front], self._front, self._count = \
193                self._data[self._front], None, (self._front+1) % self._capacity, self._count - 1
194        elif self._count < 1:
195            msg = 'Method popL called on an empty ca'
196            raise ValueError(msg)
197        else:
198            d, self._data[self._front], self._count, self._front, self._rear = \
199                self._data[self._front], None, 0, 0, self._capacity - 1
200        return cast(D, d)
201
202    def popR(self) -> D|Never:
203        """Pop one value off the right side of the ca.
204
205        * raises `ValueError` when called on an empty ca
206        """
207        if self._count > 0:
208            d, self._data[self._rear], self._rear, self._count = \
209                self._data[self._rear], None, (self._rear - 1) % self._capacity, self._count - 1
210        elif self._count < 1:
211            msg = 'Method popR called on an empty ca'
212            raise ValueError(msg)
213        else:
214            d, self._data[self._front], self._count, self._front, self._rear = \
215                self._data[self._front], None, 0, 0, self._capacity - 1
216        return cast(D, d)
217
218    def popLD(self, default: D, /) -> D:
219        """Pop one value from left, provide a mandatory default value.
220
221        * safe version of popL
222        * returns a default value in the event the `ca` is empty
223        """
224        try:
225            return self.popL()
226        except ValueError:
227            return default
228
229    def popRD(self, default: D, /) -> D:
230        """Pop one value from right, provide a mandatory default value.
231
232        * safe version of popR
233        * returns a default value in the event the `ca` is empty
234        """
235        try:
236            return self.popR()
237        except ValueError:
238            return default
239
240    def popLT(self, max: int) -> tuple[D, ...]:
241        """Pop multiple values from left side of ca.
242
243        * returns the results in a tuple of type `tuple[~D, ...]`
244        * returns an empty tuple if `ca` is empty
245        * pop no more that `max` values
246        * will pop less if `ca` becomes empty
247        """
248        ds: list[D] = []
249
250        while max > 0:
251            try:
252                ds.append(self.popL())
253            except ValueError:
254                break
255            else:
256                max -= 1
257
258        return tuple(ds)
259
260    def popRT(self, max: int) -> tuple[D, ...]:
261        """Pop multiple values from right side of `ca`.
262
263        * returns the results in a tuple of type `tuple[~D, ...]`
264        * returns an empty tuple if `ca` is empty
265        * pop no more that `max` values
266        * will pop less if `ca` becomes empty
267        """
268        ds: list[D] = []
269        while max > 0:
270            try:
271                ds.append(self.popR())
272            except ValueError:
273                break
274            else:
275                max -= 1
276
277        return tuple(ds)
278
279    def map[U](self, f: Callable[[D], U], /) -> ca[U]:
280        """Apply function f over contents, returns new `ca` instance.
281
282        * parameter `f` function of type `f[~D, ~U] -> ca[~U]`
283        * returns a new instance of type `ca[~U]`
284        """
285        return ca(map(f, self))
286
287    def foldL[L](self, f: Callable[[L, D], L], /, initial: L|None=None) -> L:
288        """Left fold ca via function and optional initial value.
289
290        * parameter `f` function of type `f[~L, ~D] -> ~L`
291          * the first argument to `f` is for the accumulated value.
292        * parameter `initial` is an optional initial value
293        * returns the reduced value of type `~L`
294          * note that `~L` and `~D` can be the same type
295          * if an initial value is not given then by necessity `~L = ~D` 
296        * raises `ValueError` when called on an empty `ca` and `initial` not given
297        """
298        if self._count == 0:
299            if initial is None:
300                msg = 'Method foldL called on an empty ca without an initial value.'
301                raise ValueError(msg)
302            else:
303                return initial
304        else:
305            if initial is None:
306                acc = cast(L, self[0])  # in this case D = L
307                for idx in range(1, self._count):
308                    acc = f(acc, self[idx])
309                return acc
310            else:
311                acc = initial
312                for d in self:
313                    acc = f(acc, d)
314                return acc
315
316    def foldR[R](self, f: Callable[[D, R], R], /, initial: R|None=None) -> R:
317        """Right fold ca via function and optional initial value.
318
319        * parameter `f` function of type `f[~D, ~R] -> ~R`
320          * the second argument to f is for the accumulated value
321        * parameter `initial` is an optional initial value
322        * returns the reduced value of type `~R`
323          * note that `~R` and `~D` can be the same type
324          * if an initial value is not given then by necessity `~R = ~D`
325        * raises `ValueError` when called on an empty `ca` and `initial` not given
326        """
327        if self._count == 0:
328            if initial is None:
329                msg = 'Method foldR called on an empty ca without an initial value.'
330                raise ValueError(msg)
331            else:
332                return initial
333        else:
334            if initial is None:
335                acc = cast(R, self[-1])  # in this case D = R
336                for idx in range(self._count-2, -1, -1):
337                    acc = f(self[idx], acc)
338                return acc
339            else:
340                acc = initial
341                for d in reversed(self):
342                    acc = f(d, acc)
343                return acc
344
345    def capacity(self) -> int:
346        """Returns current capacity of the ca."""
347        return self._capacity
348
349    def empty(self) -> None:
350        """Empty the ca, keep current capacity."""
351        self._data, self._front, self._rear = [None]*self._capacity, 0, self._capacity-1
352
353    def fractionFilled(self) -> float:
354        """Returns fractional capacity of the ca."""
355        return self._count/self._capacity
356
357    def resize(self, min_capacity: int=2) -> None:
358        """Compact `ca` and resize to `min_capacity` if necessary.
359
360        * to just compact the `ca`, do not provide a min_capacity
361        """
362        self._compact_storage_capacity()
363        if min_capacity > self._capacity:
364            self._capacity, self._data = \
365                min_capacity, self._data + [None]*(min_capacity-self._capacity)
366            if self._count == 0:
367                self._front, self._rear = 0, self._capacity - 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
  • makes defensive copies of contents for the purposes of iteration
  • not sliceable
  • in boolean context returns true if not empty, false if empty
  • in comparisons compare identity before equality (like Python built-ins do)
    • as Python tuples, lists, and dicts 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]')
43    def __init__(self, *dss: Iterable[D]) -> None:
44        if len(dss) < 2:
45            self._data: list[D|None] = [None] + \
46                                       cast(list[D|None], list(*dss)) + \
47                                       [None]
48        else:
49            msg = f'ca expected at most 1 argument, got {len(dss)}'
50            raise TypeError(msg)
51        self._capacity = capacity = len(self._data)
52        self._count = capacity - 2
53        if capacity == 2:
54            self._front = 0
55            self._rear = 1
56        else:
57            self._front = 1
58            self._rear = capacity - 2
def pushL(self, *ds: 'D') -> None:
170    def pushL(self, *ds: D) -> None:
171        """Push data from the left onto the ca."""
172        for d in ds:
173            if self._count == self._capacity:
174                self._double_storage_capacity()
175            self._front = (self._front - 1) % self._capacity
176            self._data[self._front], self._count = d, self._count + 1

Push data from the left onto the ca.

def pushR(self, *ds: 'D') -> None:
178    def pushR(self, *ds: D) -> None:
179        """Push data from the right onto the ca."""
180        for d in ds:
181            if self._count == self._capacity:
182                self._double_storage_capacity()
183            self._rear = (self._rear + 1) % self._capacity
184            self._data[self._rear], self._count = d, self._count + 1

Push data from the right onto the ca.

def popL(self) -> 'D | Never':
186    def popL(self) -> D|Never:
187        """Pop one value off the left side of the ca.
188
189        * raises `ValueError` when called on an empty ca
190        """
191        if self._count > 1:
192            d, self._data[self._front], self._front, self._count = \
193                self._data[self._front], None, (self._front+1) % self._capacity, self._count - 1
194        elif self._count < 1:
195            msg = 'Method popL called on an empty ca'
196            raise ValueError(msg)
197        else:
198            d, self._data[self._front], self._count, self._front, self._rear = \
199                self._data[self._front], None, 0, 0, self._capacity - 1
200        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':
202    def popR(self) -> D|Never:
203        """Pop one value off the right side of the ca.
204
205        * raises `ValueError` when called on an empty ca
206        """
207        if self._count > 0:
208            d, self._data[self._rear], self._rear, self._count = \
209                self._data[self._rear], None, (self._rear - 1) % self._capacity, self._count - 1
210        elif self._count < 1:
211            msg = 'Method popR called on an empty ca'
212            raise ValueError(msg)
213        else:
214            d, self._data[self._front], self._count, self._front, self._rear = \
215                self._data[self._front], None, 0, 0, self._capacity - 1
216        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':
218    def popLD(self, default: D, /) -> D:
219        """Pop one value from left, provide a mandatory default value.
220
221        * safe version of popL
222        * returns a default value in the event the `ca` is empty
223        """
224        try:
225            return self.popL()
226        except ValueError:
227            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':
229    def popRD(self, default: D, /) -> D:
230        """Pop one value from right, provide a mandatory default value.
231
232        * safe version of popR
233        * returns a default value in the event the `ca` is empty
234        """
235        try:
236            return self.popR()
237        except ValueError:
238            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, ...]':
240    def popLT(self, max: int) -> tuple[D, ...]:
241        """Pop multiple values from left side of ca.
242
243        * returns the results in a tuple of type `tuple[~D, ...]`
244        * returns an empty tuple if `ca` is empty
245        * pop no more that `max` values
246        * will pop less if `ca` becomes empty
247        """
248        ds: list[D] = []
249
250        while max > 0:
251            try:
252                ds.append(self.popL())
253            except ValueError:
254                break
255            else:
256                max -= 1
257
258        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, ...]':
260    def popRT(self, max: int) -> tuple[D, ...]:
261        """Pop multiple values from right side of `ca`.
262
263        * returns the results in a tuple of type `tuple[~D, ...]`
264        * returns an empty tuple if `ca` is empty
265        * pop no more that `max` values
266        * will pop less if `ca` becomes empty
267        """
268        ds: list[D] = []
269        while max > 0:
270            try:
271                ds.append(self.popR())
272            except ValueError:
273                break
274            else:
275                max -= 1
276
277        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 map(self, f: 'Callable[[D], U]', /) -> 'ca[U]':
279    def map[U](self, f: Callable[[D], U], /) -> ca[U]:
280        """Apply function f over contents, returns new `ca` instance.
281
282        * parameter `f` function of type `f[~D, ~U] -> ca[~U]`
283        * returns a new instance of type `ca[~U]`
284        """
285        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':
287    def foldL[L](self, f: Callable[[L, D], L], /, initial: L|None=None) -> L:
288        """Left fold ca via function and optional initial value.
289
290        * parameter `f` function of type `f[~L, ~D] -> ~L`
291          * the first argument to `f` is for the accumulated value.
292        * parameter `initial` is an optional initial value
293        * returns the reduced value of type `~L`
294          * note that `~L` and `~D` can be the same type
295          * if an initial value is not given then by necessity `~L = ~D` 
296        * raises `ValueError` when called on an empty `ca` and `initial` not given
297        """
298        if self._count == 0:
299            if initial is None:
300                msg = 'Method foldL called on an empty ca without an initial value.'
301                raise ValueError(msg)
302            else:
303                return initial
304        else:
305            if initial is None:
306                acc = cast(L, self[0])  # in this case D = L
307                for idx in range(1, self._count):
308                    acc = f(acc, self[idx])
309                return acc
310            else:
311                acc = initial
312                for d in self:
313                    acc = f(acc, d)
314                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':
316    def foldR[R](self, f: Callable[[D, R], R], /, initial: R|None=None) -> R:
317        """Right fold ca via function and optional initial value.
318
319        * parameter `f` function of type `f[~D, ~R] -> ~R`
320          * the second argument to f is for the accumulated value
321        * parameter `initial` is an optional initial value
322        * returns the reduced value of type `~R`
323          * note that `~R` and `~D` can be the same type
324          * if an initial value is not given then by necessity `~R = ~D`
325        * raises `ValueError` when called on an empty `ca` and `initial` not given
326        """
327        if self._count == 0:
328            if initial is None:
329                msg = 'Method foldR called on an empty ca without an initial value.'
330                raise ValueError(msg)
331            else:
332                return initial
333        else:
334            if initial is None:
335                acc = cast(R, self[-1])  # in this case D = R
336                for idx in range(self._count-2, -1, -1):
337                    acc = f(self[idx], acc)
338                return acc
339            else:
340                acc = initial
341                for d in reversed(self):
342                    acc = f(d, acc)
343                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:
345    def capacity(self) -> int:
346        """Returns current capacity of the ca."""
347        return self._capacity

Returns current capacity of the ca.

def empty(self) -> None:
349    def empty(self) -> None:
350        """Empty the ca, keep current capacity."""
351        self._data, self._front, self._rear = [None]*self._capacity, 0, self._capacity-1

Empty the ca, keep current capacity.

def fractionFilled(self) -> float:
353    def fractionFilled(self) -> float:
354        """Returns fractional capacity of the ca."""
355        return self._count/self._capacity

Returns fractional capacity of the ca.

def resize(self, min_capacity: int = 2) -> None:
357    def resize(self, min_capacity: int=2) -> None:
358        """Compact `ca` and resize to `min_capacity` if necessary.
359
360        * to just compact the `ca`, do not provide a min_capacity
361        """
362        self._compact_storage_capacity()
363        if min_capacity > self._capacity:
364            self._capacity, self._data = \
365                min_capacity, self._data + [None]*(min_capacity-self._capacity)
366            if self._count == 0:
367                self._front, self._rear = 0, self._capacity - 1

Compact ca and resize to min_capacity if necessary.

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

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