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