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