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