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