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