dtools.circular_array.ca
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"""### An indexable circular array data structure.""" 16 17from __future__ import annotations 18 19from collections.abc import Callable, Iterable, Iterator 20from typing import cast, Never, overload, TypeVar 21 22__all__ = ['CA', 'ca'] 23 24D = TypeVar('D') 25 26 27class CA[D]: 28 """Indexable circular array data structure 29 30 - generic, stateful data structure 31 - amortized O(1) pushing and popping from either end 32 - O(1) random access any element 33 - will resize itself as needed 34 - sliceable 35 - makes defensive copies of contents for the purposes of iteration 36 - in boolean context returns 37 - `True` when not empty 38 - `False` when empty 39 - in comparisons compare identity before equality, like builtins do 40 - raises `IndexError` for out-of-bounds indexing 41 - raises `ValueError` for popping from or folding an empty `CA` 42 43 """ 44 45 __slots__ = '_data', '_cnt', '_cap', '_front', '_rear' 46 47 L = TypeVar('L') 48 R = TypeVar('R') 49 U = TypeVar('U') 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 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 left. 263 264 - push data from the left onto the CA 265 266 """ 267 for d in ds: 268 if self._cnt == self._cap: 269 self._double_storage_capacity() 270 self._front = (self._front - 1) % self._cap 271 self._data[self._front], self._cnt = d, self._cnt + 1 272 273 def pushr(self, *ds: D) -> None: 274 """Push right. 275 276 - push data from the right onto the CA 277 278 """ 279 for d in ds: 280 if self._cnt == self._cap: 281 self._double_storage_capacity() 282 self._rear = (self._rear + 1) % self._cap 283 self._data[self._rear], self._cnt = d, self._cnt + 1 284 285 def popl(self) -> D | Never: 286 """Pop left. 287 288 - pop one value off the left side of the `CA` 289 - raises `ValueError` when called on an empty `CA` 290 291 """ 292 if self._cnt > 1: 293 d, self._data[self._front], self._front, self._cnt = ( 294 self._data[self._front], 295 None, 296 (self._front + 1) % self._cap, 297 self._cnt - 1, 298 ) 299 elif self._cnt == 1: 300 d, self._data[self._front], self._cnt, self._front, self._rear = ( 301 self._data[self._front], 302 None, 303 0, 304 0, 305 self._cap - 1, 306 ) 307 else: 308 msg = 'Method popl called on an empty CA' 309 raise ValueError(msg) 310 return cast(D, d) 311 312 def popr(self) -> D | Never: 313 """Pop right 314 315 - pop one value off the right side of the `CA` 316 - raises `ValueError` when called on an empty `CA` 317 318 """ 319 if self._cnt > 1: 320 d, self._data[self._rear], self._rear, self._cnt = ( 321 self._data[self._rear], 322 None, 323 (self._rear - 1) % self._cap, 324 self._cnt - 1, 325 ) 326 elif self._cnt == 1: 327 d, self._data[self._front], self._cnt, self._front, self._rear = ( 328 self._data[self._front], 329 None, 330 0, 331 0, 332 self._cap - 1, 333 ) 334 else: 335 msg = 'Method popr called on an empty CA' 336 raise ValueError(msg) 337 return cast(D, d) 338 339 def popld(self, default: D, /) -> D: 340 """Pop one value from left, provide a mandatory default value. 341 342 - safe version of popl 343 - returns the default value if `CA` is empty 344 345 """ 346 try: 347 return self.popl() 348 except ValueError: 349 return default 350 351 def poprd(self, default: D, /) -> D: 352 """Pop one value from right, provide a mandatory default value. 353 354 - safe version of popr 355 - returns the default value if `CA` is empty 356 357 """ 358 try: 359 return self.popr() 360 except ValueError: 361 return default 362 363 def poplt(self, maximum: int, /) -> tuple[D, ...]: 364 """Pop multiple values from left side of `CA`. 365 366 - returns the results in a tuple of type 367 - pop no more that `maximum` values 368 - will pop less if `CA` becomes empty 369 370 """ 371 ds: list[D] = [] 372 373 while maximum > 0: 374 try: 375 ds.append(self.popl()) 376 except ValueError: 377 break 378 else: 379 maximum -= 1 380 381 return tuple(ds) 382 383 def poprt(self, maximum: int, /) -> tuple[D, ...]: 384 """Pop multiple values from right side of `CA`. 385 386 - returns the results in a tuple 387 - pop no more that `maximum` values 388 - will pop less if `CA` becomes empty 389 390 """ 391 ds: list[D] = [] 392 while maximum > 0: 393 try: 394 ds.append(self.popr()) 395 except ValueError: 396 break 397 else: 398 maximum -= 1 399 400 return tuple(ds) 401 402 def rotl(self, n: int = 1, /) -> None: 403 """Rotate `CA` components to the left n times.""" 404 if self._cnt < 2: 405 return 406 for _ in range(n, 0, -1): 407 self.pushr(self.popl()) 408 409 def rotr(self, n: int = 1, /) -> None: 410 """Rotate `CA` components to the right n times.""" 411 if self._cnt < 2: 412 return 413 for _ in range(n, 0, -1): 414 self.pushl(self.popr()) 415 416 def map[U](self, f: Callable[[D], U], /) -> CA[U]: 417 """Apply function `f` over the `CA` contents, 418 419 - returns a new `CA` instance 420 421 """ 422 return CA(map(f, self)) 423 424 def foldl[L](self, f: Callable[[L, D], L], initial: L | None = None, /) -> L: 425 """Left fold `CA` with function `f` and an optional `initial` value. 426 427 - first argument to `f` is for the accumulated value 428 - returns the reduced value of type `~L` 429 - note that `~L` and `~D` can be the same type 430 - if an initial value is not given then by necessity `~L = ~D` 431 - raises `ValueError` when called on an empty `ca` and `initial` not given 432 433 """ 434 if self._cnt == 0: 435 if initial is None: 436 msg = 'Method foldl called on an empty `CA` without an initial value.' 437 raise ValueError(msg) 438 return initial 439 440 if initial is None: 441 acc = cast(L, self[0]) # in this case D = L 442 for idx in range(1, self._cnt): 443 acc = f(acc, self[idx]) 444 return acc 445 446 acc = initial 447 for d in self: 448 acc = f(acc, d) 449 return acc 450 451 def foldr[R](self, f: Callable[[D, R], R], initial: R | None = None, /) -> R: 452 """Right fold `CA` with function `f` and an optional `initial` value. 453 454 - second argument to f is for the accumulated value 455 - returns the reduced value of type `~R` 456 - note that `~R` and `~D` can be the same type 457 - if an initial value is not given then by necessity `~R = ~D` 458 - raises `ValueError` when called on an empty `CA` and `initial` not given 459 460 """ 461 if self._cnt == 0: 462 if initial is None: 463 msg = 'Method foldr called on empty `CA` without initial value.' 464 raise ValueError(msg) 465 return initial 466 467 if initial is None: 468 acc = cast(R, self[-1]) # in this case D = R 469 for idx in range(self._cnt - 2, -1, -1): 470 acc = f(self[idx], acc) 471 return acc 472 473 acc = initial 474 for d in reversed(self): 475 acc = f(d, acc) 476 return acc 477 478 def capacity(self) -> int: 479 """Returns current capacity of the `CA`.""" 480 return self._cap 481 482 def empty(self) -> None: 483 """Empty the `CA`, keep current capacity.""" 484 self._data, self._front, self._rear = [None] * self._cap, 0, self._cap 485 486 def fraction_filled(self) -> float: 487 """Returns fractional capacity of the `CA`.""" 488 return self._cnt / self._cap 489 490 def resize(self, minimum_capacity: int = 2) -> None: 491 """Compact `CA` and resize to `minimum_capacity` if necessary. 492 493 * to just compact the `CA`, do not provide a minimum capacity 494 495 """ 496 self._compact_storage_capacity() 497 if (min_cap := minimum_capacity) > self._cap: 498 self._cap, self._data = min_cap, self._data + [None] * (min_cap - self._cap) 499 if self._cnt == 0: 500 self._front, self._rear = 0, self._cap - 1 501 502 503def ca[D](*ds: D) -> CA[D]: 504 """Function to produce a `CA` array from a variable number of arguments.""" 505 return CA(ds)
class
CA(typing.Generic[D]):
28class CA[D]: 29 """Indexable circular array data structure 30 31 - generic, stateful data structure 32 - amortized O(1) pushing and popping from either end 33 - O(1) random access any element 34 - will resize itself as needed 35 - sliceable 36 - makes defensive copies of contents for the purposes of iteration 37 - in boolean context returns 38 - `True` when not empty 39 - `False` when empty 40 - in comparisons compare identity before equality, like builtins do 41 - raises `IndexError` for out-of-bounds indexing 42 - raises `ValueError` for popping from or folding an empty `CA` 43 44 """ 45 46 __slots__ = '_data', '_cnt', '_cap', '_front', '_rear' 47 48 L = TypeVar('L') 49 R = TypeVar('R') 50 U = TypeVar('U') 51 52 def __init__(self, *dss: Iterable[D]) -> None: 53 if len(dss) < 2: 54 self._data: list[D | None] = ( 55 [None] + cast(list[D | None], list(*dss)) + [None] 56 ) 57 else: 58 msg = f'CA expected at most 1 argument, got {len(dss)}' 59 raise TypeError(msg) 60 self._cap = cap = len(self._data) 61 self._cnt = cap - 2 62 if cap == 2: 63 self._front = 0 64 self._rear = 1 65 else: 66 self._front = 1 67 self._rear = cap - 2 68 69 def _double_storage_capacity(self) -> None: 70 if self._front <= self._rear: 71 self._data += [None] * self._cap 72 self._cap *= 2 73 else: 74 self._data = ( 75 self._data[: self._front] 76 + [None] * self._cap 77 + self._data[self._front :] 78 ) 79 self._front, self._cap = self._front + self._cap, 2 * self._cap 80 81 def _compact_storage_capacity(self) -> None: 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 left. 264 265 - push data from the left onto the CA 266 267 """ 268 for d in ds: 269 if self._cnt == self._cap: 270 self._double_storage_capacity() 271 self._front = (self._front - 1) % self._cap 272 self._data[self._front], self._cnt = d, self._cnt + 1 273 274 def pushr(self, *ds: D) -> None: 275 """Push right. 276 277 - push data from the right onto the CA 278 279 """ 280 for d in ds: 281 if self._cnt == self._cap: 282 self._double_storage_capacity() 283 self._rear = (self._rear + 1) % self._cap 284 self._data[self._rear], self._cnt = d, self._cnt + 1 285 286 def popl(self) -> D | Never: 287 """Pop left. 288 289 - pop one value off the left side of the `CA` 290 - raises `ValueError` when called on an empty `CA` 291 292 """ 293 if self._cnt > 1: 294 d, self._data[self._front], self._front, self._cnt = ( 295 self._data[self._front], 296 None, 297 (self._front + 1) % self._cap, 298 self._cnt - 1, 299 ) 300 elif self._cnt == 1: 301 d, self._data[self._front], self._cnt, self._front, self._rear = ( 302 self._data[self._front], 303 None, 304 0, 305 0, 306 self._cap - 1, 307 ) 308 else: 309 msg = 'Method popl called on an empty CA' 310 raise ValueError(msg) 311 return cast(D, d) 312 313 def popr(self) -> D | Never: 314 """Pop right 315 316 - pop one value off the right side of the `CA` 317 - raises `ValueError` when called on an empty `CA` 318 319 """ 320 if self._cnt > 1: 321 d, self._data[self._rear], self._rear, self._cnt = ( 322 self._data[self._rear], 323 None, 324 (self._rear - 1) % self._cap, 325 self._cnt - 1, 326 ) 327 elif self._cnt == 1: 328 d, self._data[self._front], self._cnt, self._front, self._rear = ( 329 self._data[self._front], 330 None, 331 0, 332 0, 333 self._cap - 1, 334 ) 335 else: 336 msg = 'Method popr called on an empty CA' 337 raise ValueError(msg) 338 return cast(D, d) 339 340 def popld(self, default: D, /) -> D: 341 """Pop one value from left, provide a mandatory default value. 342 343 - safe version of popl 344 - returns the default value if `CA` is empty 345 346 """ 347 try: 348 return self.popl() 349 except ValueError: 350 return default 351 352 def poprd(self, default: D, /) -> D: 353 """Pop one value from right, provide a mandatory default value. 354 355 - safe version of popr 356 - returns the default value if `CA` is empty 357 358 """ 359 try: 360 return self.popr() 361 except ValueError: 362 return default 363 364 def poplt(self, maximum: int, /) -> tuple[D, ...]: 365 """Pop multiple values from left side of `CA`. 366 367 - returns the results in a tuple of type 368 - pop no more that `maximum` values 369 - will pop less if `CA` becomes empty 370 371 """ 372 ds: list[D] = [] 373 374 while maximum > 0: 375 try: 376 ds.append(self.popl()) 377 except ValueError: 378 break 379 else: 380 maximum -= 1 381 382 return tuple(ds) 383 384 def poprt(self, maximum: int, /) -> tuple[D, ...]: 385 """Pop multiple values from right side of `CA`. 386 387 - returns the results in a tuple 388 - pop no more that `maximum` values 389 - will pop less if `CA` becomes empty 390 391 """ 392 ds: list[D] = [] 393 while maximum > 0: 394 try: 395 ds.append(self.popr()) 396 except ValueError: 397 break 398 else: 399 maximum -= 1 400 401 return tuple(ds) 402 403 def rotl(self, n: int = 1, /) -> None: 404 """Rotate `CA` components to the left n times.""" 405 if self._cnt < 2: 406 return 407 for _ in range(n, 0, -1): 408 self.pushr(self.popl()) 409 410 def rotr(self, n: int = 1, /) -> None: 411 """Rotate `CA` components to the right n times.""" 412 if self._cnt < 2: 413 return 414 for _ in range(n, 0, -1): 415 self.pushl(self.popr()) 416 417 def map[U](self, f: Callable[[D], U], /) -> CA[U]: 418 """Apply function `f` over the `CA` contents, 419 420 - returns a new `CA` instance 421 422 """ 423 return CA(map(f, self)) 424 425 def foldl[L](self, f: Callable[[L, D], L], initial: L | None = None, /) -> L: 426 """Left fold `CA` with function `f` and an optional `initial` value. 427 428 - first argument to `f` is for the accumulated value 429 - returns the reduced value of type `~L` 430 - note that `~L` and `~D` can be the same type 431 - if an initial value is not given then by necessity `~L = ~D` 432 - raises `ValueError` when called on an empty `ca` and `initial` not given 433 434 """ 435 if self._cnt == 0: 436 if initial is None: 437 msg = 'Method foldl called on an empty `CA` without an initial value.' 438 raise ValueError(msg) 439 return initial 440 441 if initial is None: 442 acc = cast(L, self[0]) # in this case D = L 443 for idx in range(1, self._cnt): 444 acc = f(acc, self[idx]) 445 return acc 446 447 acc = initial 448 for d in self: 449 acc = f(acc, d) 450 return acc 451 452 def foldr[R](self, f: Callable[[D, R], R], initial: R | None = None, /) -> R: 453 """Right fold `CA` with function `f` and an optional `initial` value. 454 455 - second argument to f is for the accumulated value 456 - returns the reduced value of type `~R` 457 - note that `~R` and `~D` can be the same type 458 - if an initial value is not given then by necessity `~R = ~D` 459 - raises `ValueError` when called on an empty `CA` and `initial` not given 460 461 """ 462 if self._cnt == 0: 463 if initial is None: 464 msg = 'Method foldr called on empty `CA` without initial value.' 465 raise ValueError(msg) 466 return initial 467 468 if initial is None: 469 acc = cast(R, self[-1]) # in this case D = R 470 for idx in range(self._cnt - 2, -1, -1): 471 acc = f(self[idx], acc) 472 return acc 473 474 acc = initial 475 for d in reversed(self): 476 acc = f(d, acc) 477 return acc 478 479 def capacity(self) -> int: 480 """Returns current capacity of the `CA`.""" 481 return self._cap 482 483 def empty(self) -> None: 484 """Empty the `CA`, keep current capacity.""" 485 self._data, self._front, self._rear = [None] * self._cap, 0, self._cap 486 487 def fraction_filled(self) -> float: 488 """Returns fractional capacity of the `CA`.""" 489 return self._cnt / self._cap 490 491 def resize(self, minimum_capacity: int = 2) -> None: 492 """Compact `CA` and resize to `minimum_capacity` if necessary. 493 494 * to just compact the `CA`, do not provide a minimum capacity 495 496 """ 497 self._compact_storage_capacity() 498 if (min_cap := minimum_capacity) > self._cap: 499 self._cap, self._data = min_cap, self._data + [None] * (min_cap - self._cap) 500 if self._cnt == 0: 501 self._front, self._rear = 0, self._cap - 1
Indexable circular array data structure
- generic, stateful data structure
- amortized O(1) pushing and popping from either end
- O(1) random access any element
- will resize itself as needed
- sliceable
- makes defensive copies of contents for the purposes of iteration
- in boolean context returns
True
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])
52 def __init__(self, *dss: Iterable[D]) -> None: 53 if len(dss) < 2: 54 self._data: list[D | None] = ( 55 [None] + cast(list[D | None], list(*dss)) + [None] 56 ) 57 else: 58 msg = f'CA expected at most 1 argument, got {len(dss)}' 59 raise TypeError(msg) 60 self._cap = cap = len(self._data) 61 self._cnt = cap - 2 62 if cap == 2: 63 self._front = 0 64 self._rear = 1 65 else: 66 self._front = 1 67 self._rear = cap - 2
def
pushl(self, *ds: ~D) -> None:
262 def pushl(self, *ds: D) -> None: 263 """Push left. 264 265 - push data from the left onto the CA 266 267 """ 268 for d in ds: 269 if self._cnt == self._cap: 270 self._double_storage_capacity() 271 self._front = (self._front - 1) % self._cap 272 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:
274 def pushr(self, *ds: D) -> None: 275 """Push right. 276 277 - push data from the right onto the CA 278 279 """ 280 for d in ds: 281 if self._cnt == self._cap: 282 self._double_storage_capacity() 283 self._rear = (self._rear + 1) % self._cap 284 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]:
286 def popl(self) -> D | Never: 287 """Pop left. 288 289 - pop one value off the left side of the `CA` 290 - raises `ValueError` when called on an empty `CA` 291 292 """ 293 if self._cnt > 1: 294 d, self._data[self._front], self._front, self._cnt = ( 295 self._data[self._front], 296 None, 297 (self._front + 1) % self._cap, 298 self._cnt - 1, 299 ) 300 elif self._cnt == 1: 301 d, self._data[self._front], self._cnt, self._front, self._rear = ( 302 self._data[self._front], 303 None, 304 0, 305 0, 306 self._cap - 1, 307 ) 308 else: 309 msg = 'Method popl called on an empty CA' 310 raise ValueError(msg) 311 return cast(D, d)
def
popr(self) -> Union[~D, Never]:
313 def popr(self) -> D | Never: 314 """Pop right 315 316 - pop one value off the right side of the `CA` 317 - raises `ValueError` when called on an empty `CA` 318 319 """ 320 if self._cnt > 1: 321 d, self._data[self._rear], self._rear, self._cnt = ( 322 self._data[self._rear], 323 None, 324 (self._rear - 1) % self._cap, 325 self._cnt - 1, 326 ) 327 elif self._cnt == 1: 328 d, self._data[self._front], self._cnt, self._front, self._rear = ( 329 self._data[self._front], 330 None, 331 0, 332 0, 333 self._cap - 1, 334 ) 335 else: 336 msg = 'Method popr called on an empty CA' 337 raise ValueError(msg) 338 return cast(D, d)
def
popld(self, default: ~D, /) -> ~D:
340 def popld(self, default: D, /) -> D: 341 """Pop one value from left, provide a mandatory default value. 342 343 - safe version of popl 344 - returns the default value if `CA` is empty 345 346 """ 347 try: 348 return self.popl() 349 except ValueError: 350 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:
352 def poprd(self, default: D, /) -> D: 353 """Pop one value from right, provide a mandatory default value. 354 355 - safe version of popr 356 - returns the default value if `CA` is empty 357 358 """ 359 try: 360 return self.popr() 361 except ValueError: 362 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, ...]:
364 def poplt(self, maximum: int, /) -> tuple[D, ...]: 365 """Pop multiple values from left side of `CA`. 366 367 - returns the results in a tuple of type 368 - pop no more that `maximum` values 369 - will pop less if `CA` becomes empty 370 371 """ 372 ds: list[D] = [] 373 374 while maximum > 0: 375 try: 376 ds.append(self.popl()) 377 except ValueError: 378 break 379 else: 380 maximum -= 1 381 382 return tuple(ds)
def
poprt(self, maximum: int, /) -> tuple[~D, ...]:
384 def poprt(self, maximum: int, /) -> tuple[D, ...]: 385 """Pop multiple values from right side of `CA`. 386 387 - returns the results in a tuple 388 - pop no more that `maximum` values 389 - will pop less if `CA` becomes empty 390 391 """ 392 ds: list[D] = [] 393 while maximum > 0: 394 try: 395 ds.append(self.popr()) 396 except ValueError: 397 break 398 else: 399 maximum -= 1 400 401 return tuple(ds)
def
rotl(self, n: int = 1, /) -> None:
403 def rotl(self, n: int = 1, /) -> None: 404 """Rotate `CA` components to the left n times.""" 405 if self._cnt < 2: 406 return 407 for _ in range(n, 0, -1): 408 self.pushr(self.popl())
Rotate CA
components to the left n times.
def
rotr(self, n: int = 1, /) -> None:
410 def rotr(self, n: int = 1, /) -> None: 411 """Rotate `CA` components to the right n times.""" 412 if self._cnt < 2: 413 return 414 for _ in range(n, 0, -1): 415 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:
425 def foldl[L](self, f: Callable[[L, D], L], initial: L | None = None, /) -> L: 426 """Left fold `CA` with function `f` and an optional `initial` value. 427 428 - first argument to `f` is for the accumulated value 429 - returns the reduced value of type `~L` 430 - note that `~L` and `~D` can be the same type 431 - if an initial value is not given then by necessity `~L = ~D` 432 - raises `ValueError` when called on an empty `ca` and `initial` not given 433 434 """ 435 if self._cnt == 0: 436 if initial is None: 437 msg = 'Method foldl called on an empty `CA` without an initial value.' 438 raise ValueError(msg) 439 return initial 440 441 if initial is None: 442 acc = cast(L, self[0]) # in this case D = L 443 for idx in range(1, self._cnt): 444 acc = f(acc, self[idx]) 445 return acc 446 447 acc = initial 448 for d in self: 449 acc = f(acc, d) 450 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:
452 def foldr[R](self, f: Callable[[D, R], R], initial: R | None = None, /) -> R: 453 """Right fold `CA` with function `f` and an optional `initial` value. 454 455 - second argument to f is for the accumulated value 456 - returns the reduced value of type `~R` 457 - note that `~R` and `~D` can be the same type 458 - if an initial value is not given then by necessity `~R = ~D` 459 - raises `ValueError` when called on an empty `CA` and `initial` not given 460 461 """ 462 if self._cnt == 0: 463 if initial is None: 464 msg = 'Method foldr called on empty `CA` without initial value.' 465 raise ValueError(msg) 466 return initial 467 468 if initial is None: 469 acc = cast(R, self[-1]) # in this case D = R 470 for idx in range(self._cnt - 2, -1, -1): 471 acc = f(self[idx], acc) 472 return acc 473 474 acc = initial 475 for d in reversed(self): 476 acc = f(d, acc) 477 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:
483 def empty(self) -> None: 484 """Empty the `CA`, keep current capacity.""" 485 self._data, self._front, self._rear = [None] * self._cap, 0, self._cap
Empty the CA
, keep current capacity.
def
fraction_filled(self) -> float:
487 def fraction_filled(self) -> float: 488 """Returns fractional capacity of the `CA`.""" 489 return self._cnt / self._cap
Returns fractional capacity of the CA
.
def
resize(self, minimum_capacity: int = 2) -> None:
491 def resize(self, minimum_capacity: int = 2) -> None: 492 """Compact `CA` and resize to `minimum_capacity` if necessary. 493 494 * to just compact the `CA`, do not provide a minimum capacity 495 496 """ 497 self._compact_storage_capacity() 498 if (min_cap := minimum_capacity) > self._cap: 499 self._cap, self._data = min_cap, self._data + [None] * (min_cap - self._cap) 500 if self._cnt == 0: 501 self._front, self._rear = 0, self._cap - 1
504def ca[D](*ds: D) -> CA[D]: 505 """Function to produce a `CA` array from a variable number of arguments.""" 506 return CA(ds)
Function to produce a CA
array from a variable number of arguments.