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