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 emptyca
- 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)
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)
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 typef[~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 typef[~L, ~D] -> ~L
- the first argument to
f
is for the accumulated value.
- the first argument to
- 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
- note that
- raises
ValueError
when called on an emptyca
andinitial
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 typef[~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
- note that
- raises
ValueError
when called on an emptyca
andinitial
not given
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
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.