grscheller.circular_array.ca
Indexable circular array data structure module.
1# Copyright 2023-2024 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 typing import Callable, cast, final, Iterable, Iterator 18from typing import Sequence, TypeVar, Never 19 20__all__ = ['ca', 'CA'] 21 22class ca[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 * makes defensive copies of contents for the purposes of iteration 31 * not sliceable 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 * as Python tuples, lists, and dicts do 35 * raises `IndexError` for out-of-bounds indexing 36 * raises `ValueError` for popping from or folding an empty `ca` 37 * raises `TypeError` if more than 2 arguments are passed to constructor 38 39 """ 40 __slots__ = '_data', '_count', '_capacity', '_front', '_rear' 41 42 def __init__(self, *dss: Iterable[D]) -> None: 43 if len(dss) < 2: 44 self._data: list[D|None] = [None] + \ 45 cast(list[D|None], list(*dss)) + \ 46 [None] 47 else: 48 msg = f'ca expected at most 1 argument, got {len(dss)}' 49 raise TypeError(msg) 50 self._capacity = capacity = len(self._data) 51 self._count = capacity - 2 52 if capacity == 2: 53 self._front = 0 54 self._rear = 1 55 else: 56 self._front = 1 57 self._rear = capacity - 2 58 59 def _double_storage_capacity(self) -> None: 60 if self._front <= self._rear: 61 self._data += [None]*self._capacity 62 self._capacity *= 2 63 else: 64 self._data = self._data[:self._front] + [None]*self._capacity + self._data[self._front:] 65 self._front, self._capacity = self._front + self._capacity, 2*self._capacity 66 67 def _compact_storage_capacity(self) -> None: 68 """Compact the ca.""" 69 match self._count: 70 case 0: 71 self._capacity, self._front, self._rear, self._data = 2, 0, 1, [None, None] 72 case 1: 73 self._capacity, self._front, self._rear, self._data = 3, 1, 1, [None, self._data[self._front], None] 74 case _: 75 if self._front <= self._rear: 76 self._capacity, self._front, self._rear, self._data = \ 77 self._count+2, 1, self._count, [None] + self._data[self._front:self._rear+1] + [None] 78 else: 79 self._capacity, self._front, self._rear, self._data = \ 80 self._count+2, 1, self._count, [None] + self._data[self._front:] + self._data[:self._rear+1] + [None] 81 82 def __iter__(self) -> Iterator[D]: 83 if self._count > 0: 84 capacity, rear, position, current_state = \ 85 self._capacity, 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._count > 0: 94 capacity, front, position, current_state = \ 95 self._capacity, 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._count > 0 110 111 def __len__(self) -> int: 112 return self._count 113 114 def __getitem__(self, index: int, /) -> D: 115 cnt = self._count 116 if 0 <= index < cnt: 117 return cast(D, self._data[(self._front + index) % self._capacity]) 118 elif -cnt <= index < 0: 119 return cast(D, self._data[(self._front + cnt + index) % self._capacity]) 120 else: 121 if cnt > 0: 122 msg1 = 'Out of bounds: ' 123 msg2 = f'index = {index} not between {-cnt} and {cnt-1} ' 124 msg3 = 'while getting value from a ca.' 125 raise IndexError(msg1 + msg2 + msg3) 126 else: 127 msg0 = 'Trying to get a value from an empty ca.' 128 raise IndexError(msg0) 129 130 def __setitem__(self, index: int, value: D, /) -> None: 131 cnt = self._count 132 if 0 <= index < cnt: 133 self._data[(self._front + index) % self._capacity] = value 134 elif -cnt <= index < 0: 135 self._data[(self._front + cnt + index) % self._capacity] = value 136 else: 137 if cnt > 0: 138 msg1 = 'Out of bounds: ' 139 msg2 = f'index = {index} not between {-cnt} and {cnt-1} ' 140 msg3 = 'while setting value from a ca.' 141 raise IndexError(msg1 + msg2 + msg3) 142 else: 143 msg0 = 'Trying to set a value from an empty ca.' 144 raise IndexError(msg0) 145 146 def __eq__(self, other: object, /) -> bool: 147 if self is other: 148 return True 149 if not isinstance(other, type(self)): 150 return False 151 152 frontL, frontR, \ 153 countL, countR, \ 154 capacityL, capacityR = \ 155 self._front, other._front, \ 156 self._count, other._count, \ 157 self._capacity, other._capacity 158 159 if countL != countR: 160 return False 161 162 for nn in range(countL): 163 if self._data[(frontL+nn)%capacityL] is other._data[(frontR+nn)%capacityR]: 164 continue 165 if self._data[(frontL+nn)%capacityL] != other._data[(frontR+nn)%capacityR]: 166 return False 167 return True 168 169 def pushL(self, *ds: D) -> None: 170 """Push data from the left onto the ca.""" 171 for d in ds: 172 if self._count == self._capacity: 173 self._double_storage_capacity() 174 self._front = (self._front - 1) % self._capacity 175 self._data[self._front], self._count = d, self._count + 1 176 177 def pushR(self, *ds: D) -> None: 178 """Push data from the right onto the ca.""" 179 for d in ds: 180 if self._count == self._capacity: 181 self._double_storage_capacity() 182 self._rear = (self._rear + 1) % self._capacity 183 self._data[self._rear], self._count = d, self._count + 1 184 185 def popL(self) -> D|Never: 186 """Pop one value off the left side of the ca. 187 188 * raises `ValueError` when called on an empty ca 189 """ 190 if self._count > 1: 191 d, self._data[self._front], self._front, self._count = \ 192 self._data[self._front], None, (self._front+1) % self._capacity, self._count - 1 193 elif self._count < 1: 194 msg = 'Method popL called on an empty ca' 195 raise ValueError(msg) 196 else: 197 d, self._data[self._front], self._count, self._front, self._rear = \ 198 self._data[self._front], None, 0, 0, self._capacity - 1 199 return cast(D, d) 200 201 def popR(self) -> D|Never: 202 """Pop one value off the right side of the ca. 203 204 * raises `ValueError` when called on an empty ca 205 """ 206 if self._count > 0: 207 d, self._data[self._rear], self._rear, self._count = \ 208 self._data[self._rear], None, (self._rear - 1) % self._capacity, self._count - 1 209 elif self._count < 1: 210 msg = 'Method popR called on an empty ca' 211 raise ValueError(msg) 212 else: 213 d, self._data[self._front], self._count, self._front, self._rear = \ 214 self._data[self._front], None, 0, 0, self._capacity - 1 215 return cast(D, d) 216 217 def popLD(self, default: D, /) -> D: 218 """Pop one value from left, provide a mandatory default value. 219 220 * safe version of popL 221 * returns a default value in the event the `ca` is empty 222 """ 223 try: 224 return self.popL() 225 except ValueError: 226 return default 227 228 def popRD(self, default: D, /) -> D: 229 """Pop one value from right, provide a mandatory default value. 230 231 * safe version of popR 232 * returns a default value in the event the `ca` is empty 233 """ 234 try: 235 return self.popR() 236 except ValueError: 237 return default 238 239 def popLT(self, max: int) -> tuple[D, ...]: 240 """Pop multiple values from left side of ca. 241 242 * returns the results in a tuple of type `tuple[~D, ...]` 243 * returns an empty tuple if `ca` is empty 244 * pop no more that `max` values 245 * will pop less if `ca` becomes empty 246 """ 247 ds: list[D] = [] 248 249 while max > 0: 250 try: 251 ds.append(self.popL()) 252 except ValueError: 253 break 254 else: 255 max -= 1 256 257 return tuple(ds) 258 259 def popRT(self, max: int) -> tuple[D, ...]: 260 """Pop multiple values from right side of `ca`. 261 262 * returns the results in a tuple of type `tuple[~D, ...]` 263 * returns an empty tuple if `ca` is empty 264 * pop no more that `max` values 265 * will pop less if `ca` becomes empty 266 """ 267 ds: list[D] = [] 268 while max > 0: 269 try: 270 ds.append(self.popR()) 271 except ValueError: 272 break 273 else: 274 max -= 1 275 276 return tuple(ds) 277 278 def map[U](self, f: Callable[[D], U], /) -> ca[U]: 279 """Apply function f over contents, returns new `ca` instance. 280 281 * parameter `f` function of type `f[~D, ~U] -> ca[~U]` 282 * returns a new instance of type `ca[~U]` 283 """ 284 return ca(map(f, self)) 285 286 def foldL[L](self, f: Callable[[L, D], L], /, initial: L|None=None) -> L: 287 """Left fold ca via function and optional initial value. 288 289 * parameter `f` function of type `f[~L, ~D] -> ~L` 290 * the first argument to `f` is for the accumulated value. 291 * parameter `initial` is an optional initial value 292 * returns the reduced value of type `~L` 293 * note that `~L` and `~D` can be the same type 294 * if an initial value is not given then by necessity `~L = ~D` 295 * raises `ValueError` when called on an empty `ca` and `initial` not given 296 """ 297 if self._count == 0: 298 if initial is None: 299 msg = 'Method foldL called on an empty ca without an initial value.' 300 raise ValueError(msg) 301 else: 302 return initial 303 else: 304 if initial is None: 305 acc = cast(L, self[0]) # in this case D = L 306 for idx in range(1, self._count): 307 acc = f(acc, self[idx]) 308 return acc 309 else: 310 acc = initial 311 for d in self: 312 acc = f(acc, d) 313 return acc 314 315 def foldR[R](self, f: Callable[[D, R], R], /, initial: R|None=None) -> R: 316 """Right fold ca via function and optional initial value. 317 318 * parameter `f` function of type `f[~D, ~R] -> ~R` 319 * the second argument to f is for the accumulated value 320 * parameter `initial` is an optional initial value 321 * returns the reduced value of type `~R` 322 * note that `~R` and `~D` can be the same type 323 * if an initial value is not given then by necessity `~R = ~D` 324 * raises `ValueError` when called on an empty `ca` and `initial` not given 325 """ 326 if self._count == 0: 327 if initial is None: 328 msg = 'Method foldR called on an empty ca without an initial value.' 329 raise ValueError(msg) 330 else: 331 return initial 332 else: 333 if initial is None: 334 acc = cast(R, self[-1]) # in this case D = R 335 for idx in range(self._count-2, -1, -1): 336 acc = f(self[idx], acc) 337 return acc 338 else: 339 acc = initial 340 for d in reversed(self): 341 acc = f(d, acc) 342 return acc 343 344 def capacity(self) -> int: 345 """Returns current capacity of the ca.""" 346 return self._capacity 347 348 def empty(self) -> None: 349 """Empty the ca, keep current capacity.""" 350 self._data, self._front, self._rear = [None]*self._capacity, 0, self._capacity-1 351 352 def fractionFilled(self) -> float: 353 """Returns fractional capacity of the ca.""" 354 return self._count/self._capacity 355 356 def resize(self, min_capacity: int=2) -> None: 357 """Compact `ca` and resize to `min_capacity` if necessary. 358 359 * to just compact the `ca`, do not provide a min_capacity 360 """ 361 self._compact_storage_capacity() 362 if min_capacity > self._capacity: 363 self._capacity, self._data = \ 364 min_capacity, self._data + [None]*(min_capacity-self._capacity) 365 if self._count == 0: 366 self._front, self._rear = 0, self._capacity - 1 367 368def CA[U](*ds: U) -> ca[U]: 369 """Function to produce a `ca` array from a variable number of arguments.""" 370 return ca(ds)
class
ca(typing.Generic[D]):
23class ca[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 * makes defensive copies of contents for the purposes of iteration 32 * not sliceable 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 * as Python tuples, lists, and dicts do 36 * raises `IndexError` for out-of-bounds indexing 37 * raises `ValueError` for popping from or folding an empty `ca` 38 * raises `TypeError` if more than 2 arguments are passed to constructor 39 40 """ 41 __slots__ = '_data', '_count', '_capacity', '_front', '_rear' 42 43 def __init__(self, *dss: Iterable[D]) -> None: 44 if len(dss) < 2: 45 self._data: list[D|None] = [None] + \ 46 cast(list[D|None], list(*dss)) + \ 47 [None] 48 else: 49 msg = f'ca expected at most 1 argument, got {len(dss)}' 50 raise TypeError(msg) 51 self._capacity = capacity = len(self._data) 52 self._count = capacity - 2 53 if capacity == 2: 54 self._front = 0 55 self._rear = 1 56 else: 57 self._front = 1 58 self._rear = capacity - 2 59 60 def _double_storage_capacity(self) -> None: 61 if self._front <= self._rear: 62 self._data += [None]*self._capacity 63 self._capacity *= 2 64 else: 65 self._data = self._data[:self._front] + [None]*self._capacity + self._data[self._front:] 66 self._front, self._capacity = self._front + self._capacity, 2*self._capacity 67 68 def _compact_storage_capacity(self) -> None: 69 """Compact the ca.""" 70 match self._count: 71 case 0: 72 self._capacity, self._front, self._rear, self._data = 2, 0, 1, [None, None] 73 case 1: 74 self._capacity, self._front, self._rear, self._data = 3, 1, 1, [None, self._data[self._front], None] 75 case _: 76 if self._front <= self._rear: 77 self._capacity, self._front, self._rear, self._data = \ 78 self._count+2, 1, self._count, [None] + self._data[self._front:self._rear+1] + [None] 79 else: 80 self._capacity, self._front, self._rear, self._data = \ 81 self._count+2, 1, self._count, [None] + self._data[self._front:] + self._data[:self._rear+1] + [None] 82 83 def __iter__(self) -> Iterator[D]: 84 if self._count > 0: 85 capacity, rear, position, current_state = \ 86 self._capacity, 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._count > 0: 95 capacity, front, position, current_state = \ 96 self._capacity, 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._count > 0 111 112 def __len__(self) -> int: 113 return self._count 114 115 def __getitem__(self, index: int, /) -> D: 116 cnt = self._count 117 if 0 <= index < cnt: 118 return cast(D, self._data[(self._front + index) % self._capacity]) 119 elif -cnt <= index < 0: 120 return cast(D, self._data[(self._front + cnt + index) % self._capacity]) 121 else: 122 if cnt > 0: 123 msg1 = 'Out of bounds: ' 124 msg2 = f'index = {index} not between {-cnt} and {cnt-1} ' 125 msg3 = 'while getting value from a ca.' 126 raise IndexError(msg1 + msg2 + msg3) 127 else: 128 msg0 = 'Trying to get a value from an empty ca.' 129 raise IndexError(msg0) 130 131 def __setitem__(self, index: int, value: D, /) -> None: 132 cnt = self._count 133 if 0 <= index < cnt: 134 self._data[(self._front + index) % self._capacity] = value 135 elif -cnt <= index < 0: 136 self._data[(self._front + cnt + index) % self._capacity] = value 137 else: 138 if cnt > 0: 139 msg1 = 'Out of bounds: ' 140 msg2 = f'index = {index} not between {-cnt} and {cnt-1} ' 141 msg3 = 'while setting value from a ca.' 142 raise IndexError(msg1 + msg2 + msg3) 143 else: 144 msg0 = 'Trying to set a value from an empty ca.' 145 raise IndexError(msg0) 146 147 def __eq__(self, other: object, /) -> bool: 148 if self is other: 149 return True 150 if not isinstance(other, type(self)): 151 return False 152 153 frontL, frontR, \ 154 countL, countR, \ 155 capacityL, capacityR = \ 156 self._front, other._front, \ 157 self._count, other._count, \ 158 self._capacity, other._capacity 159 160 if countL != countR: 161 return False 162 163 for nn in range(countL): 164 if self._data[(frontL+nn)%capacityL] is other._data[(frontR+nn)%capacityR]: 165 continue 166 if self._data[(frontL+nn)%capacityL] != other._data[(frontR+nn)%capacityR]: 167 return False 168 return True 169 170 def pushL(self, *ds: D) -> None: 171 """Push data from the left onto the ca.""" 172 for d in ds: 173 if self._count == self._capacity: 174 self._double_storage_capacity() 175 self._front = (self._front - 1) % self._capacity 176 self._data[self._front], self._count = d, self._count + 1 177 178 def pushR(self, *ds: D) -> None: 179 """Push data from the right onto the ca.""" 180 for d in ds: 181 if self._count == self._capacity: 182 self._double_storage_capacity() 183 self._rear = (self._rear + 1) % self._capacity 184 self._data[self._rear], self._count = d, self._count + 1 185 186 def popL(self) -> D|Never: 187 """Pop one value off the left side of the ca. 188 189 * raises `ValueError` when called on an empty ca 190 """ 191 if self._count > 1: 192 d, self._data[self._front], self._front, self._count = \ 193 self._data[self._front], None, (self._front+1) % self._capacity, self._count - 1 194 elif self._count < 1: 195 msg = 'Method popL called on an empty ca' 196 raise ValueError(msg) 197 else: 198 d, self._data[self._front], self._count, self._front, self._rear = \ 199 self._data[self._front], None, 0, 0, self._capacity - 1 200 return cast(D, d) 201 202 def popR(self) -> D|Never: 203 """Pop one value off the right side of the ca. 204 205 * raises `ValueError` when called on an empty ca 206 """ 207 if self._count > 0: 208 d, self._data[self._rear], self._rear, self._count = \ 209 self._data[self._rear], None, (self._rear - 1) % self._capacity, self._count - 1 210 elif self._count < 1: 211 msg = 'Method popR called on an empty ca' 212 raise ValueError(msg) 213 else: 214 d, self._data[self._front], self._count, self._front, self._rear = \ 215 self._data[self._front], None, 0, 0, self._capacity - 1 216 return cast(D, d) 217 218 def popLD(self, default: D, /) -> D: 219 """Pop one value from left, provide a mandatory default value. 220 221 * safe version of popL 222 * returns a default value in the event the `ca` is empty 223 """ 224 try: 225 return self.popL() 226 except ValueError: 227 return default 228 229 def popRD(self, default: D, /) -> D: 230 """Pop one value from right, provide a mandatory default value. 231 232 * safe version of popR 233 * returns a default value in the event the `ca` is empty 234 """ 235 try: 236 return self.popR() 237 except ValueError: 238 return default 239 240 def popLT(self, max: int) -> tuple[D, ...]: 241 """Pop multiple values from left side of ca. 242 243 * returns the results in a tuple of type `tuple[~D, ...]` 244 * returns an empty tuple if `ca` is empty 245 * pop no more that `max` values 246 * will pop less if `ca` becomes empty 247 """ 248 ds: list[D] = [] 249 250 while max > 0: 251 try: 252 ds.append(self.popL()) 253 except ValueError: 254 break 255 else: 256 max -= 1 257 258 return tuple(ds) 259 260 def popRT(self, max: int) -> tuple[D, ...]: 261 """Pop multiple values from right side of `ca`. 262 263 * returns the results in a tuple of type `tuple[~D, ...]` 264 * returns an empty tuple if `ca` is empty 265 * pop no more that `max` values 266 * will pop less if `ca` becomes empty 267 """ 268 ds: list[D] = [] 269 while max > 0: 270 try: 271 ds.append(self.popR()) 272 except ValueError: 273 break 274 else: 275 max -= 1 276 277 return tuple(ds) 278 279 def map[U](self, f: Callable[[D], U], /) -> ca[U]: 280 """Apply function f over contents, returns new `ca` instance. 281 282 * parameter `f` function of type `f[~D, ~U] -> ca[~U]` 283 * returns a new instance of type `ca[~U]` 284 """ 285 return ca(map(f, self)) 286 287 def foldL[L](self, f: Callable[[L, D], L], /, initial: L|None=None) -> L: 288 """Left fold ca via function and optional initial value. 289 290 * parameter `f` function of type `f[~L, ~D] -> ~L` 291 * the first argument to `f` is for the accumulated value. 292 * parameter `initial` is an optional initial value 293 * returns the reduced value of type `~L` 294 * note that `~L` and `~D` can be the same type 295 * if an initial value is not given then by necessity `~L = ~D` 296 * raises `ValueError` when called on an empty `ca` and `initial` not given 297 """ 298 if self._count == 0: 299 if initial is None: 300 msg = 'Method foldL called on an empty ca without an initial value.' 301 raise ValueError(msg) 302 else: 303 return initial 304 else: 305 if initial is None: 306 acc = cast(L, self[0]) # in this case D = L 307 for idx in range(1, self._count): 308 acc = f(acc, self[idx]) 309 return acc 310 else: 311 acc = initial 312 for d in self: 313 acc = f(acc, d) 314 return acc 315 316 def foldR[R](self, f: Callable[[D, R], R], /, initial: R|None=None) -> R: 317 """Right fold ca via function and optional initial value. 318 319 * parameter `f` function of type `f[~D, ~R] -> ~R` 320 * the second argument to f is for the accumulated value 321 * parameter `initial` is an optional initial value 322 * returns the reduced value of type `~R` 323 * note that `~R` and `~D` can be the same type 324 * if an initial value is not given then by necessity `~R = ~D` 325 * raises `ValueError` when called on an empty `ca` and `initial` not given 326 """ 327 if self._count == 0: 328 if initial is None: 329 msg = 'Method foldR called on an empty ca without an initial value.' 330 raise ValueError(msg) 331 else: 332 return initial 333 else: 334 if initial is None: 335 acc = cast(R, self[-1]) # in this case D = R 336 for idx in range(self._count-2, -1, -1): 337 acc = f(self[idx], acc) 338 return acc 339 else: 340 acc = initial 341 for d in reversed(self): 342 acc = f(d, acc) 343 return acc 344 345 def capacity(self) -> int: 346 """Returns current capacity of the ca.""" 347 return self._capacity 348 349 def empty(self) -> None: 350 """Empty the ca, keep current capacity.""" 351 self._data, self._front, self._rear = [None]*self._capacity, 0, self._capacity-1 352 353 def fractionFilled(self) -> float: 354 """Returns fractional capacity of the ca.""" 355 return self._count/self._capacity 356 357 def resize(self, min_capacity: int=2) -> None: 358 """Compact `ca` and resize to `min_capacity` if necessary. 359 360 * to just compact the `ca`, do not provide a min_capacity 361 """ 362 self._compact_storage_capacity() 363 if min_capacity > self._capacity: 364 self._capacity, self._data = \ 365 min_capacity, self._data + [None]*(min_capacity-self._capacity) 366 if self._count == 0: 367 self._front, self._rear = 0, self._capacity - 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
- makes defensive copies of contents for the purposes of iteration
- not sliceable
- in boolean context returns true if not empty, false if empty
- in comparisons compare identity before equality (like Python built-ins do)
- as Python tuples, lists, and dicts do
- raises
IndexError
for out-of-bounds indexing - raises
ValueError
for popping from or folding an emptyca
- raises
TypeError
if more than 2 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] + \ 46 cast(list[D|None], list(*dss)) + \ 47 [None] 48 else: 49 msg = f'ca expected at most 1 argument, got {len(dss)}' 50 raise TypeError(msg) 51 self._capacity = capacity = len(self._data) 52 self._count = capacity - 2 53 if capacity == 2: 54 self._front = 0 55 self._rear = 1 56 else: 57 self._front = 1 58 self._rear = capacity - 2
def
pushL(self, *ds: 'D') -> None:
170 def pushL(self, *ds: D) -> None: 171 """Push data from the left onto the ca.""" 172 for d in ds: 173 if self._count == self._capacity: 174 self._double_storage_capacity() 175 self._front = (self._front - 1) % self._capacity 176 self._data[self._front], self._count = d, self._count + 1
Push data from the left onto the ca.
def
pushR(self, *ds: 'D') -> None:
178 def pushR(self, *ds: D) -> None: 179 """Push data from the right onto the ca.""" 180 for d in ds: 181 if self._count == self._capacity: 182 self._double_storage_capacity() 183 self._rear = (self._rear + 1) % self._capacity 184 self._data[self._rear], self._count = d, self._count + 1
Push data from the right onto the ca.
def
popL(self) -> 'D | Never':
186 def popL(self) -> D|Never: 187 """Pop one value off the left side of the ca. 188 189 * raises `ValueError` when called on an empty ca 190 """ 191 if self._count > 1: 192 d, self._data[self._front], self._front, self._count = \ 193 self._data[self._front], None, (self._front+1) % self._capacity, self._count - 1 194 elif self._count < 1: 195 msg = 'Method popL called on an empty ca' 196 raise ValueError(msg) 197 else: 198 d, self._data[self._front], self._count, self._front, self._rear = \ 199 self._data[self._front], None, 0, 0, self._capacity - 1 200 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':
202 def popR(self) -> D|Never: 203 """Pop one value off the right side of the ca. 204 205 * raises `ValueError` when called on an empty ca 206 """ 207 if self._count > 0: 208 d, self._data[self._rear], self._rear, self._count = \ 209 self._data[self._rear], None, (self._rear - 1) % self._capacity, self._count - 1 210 elif self._count < 1: 211 msg = 'Method popR called on an empty ca' 212 raise ValueError(msg) 213 else: 214 d, self._data[self._front], self._count, self._front, self._rear = \ 215 self._data[self._front], None, 0, 0, self._capacity - 1 216 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':
218 def popLD(self, default: D, /) -> D: 219 """Pop one value from left, provide a mandatory default value. 220 221 * safe version of popL 222 * returns a default value in the event the `ca` is empty 223 """ 224 try: 225 return self.popL() 226 except ValueError: 227 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':
229 def popRD(self, default: D, /) -> D: 230 """Pop one value from right, provide a mandatory default value. 231 232 * safe version of popR 233 * returns a default value in the event the `ca` is empty 234 """ 235 try: 236 return self.popR() 237 except ValueError: 238 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, ...]':
240 def popLT(self, max: int) -> tuple[D, ...]: 241 """Pop multiple values from left side of ca. 242 243 * returns the results in a tuple of type `tuple[~D, ...]` 244 * returns an empty tuple if `ca` is empty 245 * pop no more that `max` values 246 * will pop less if `ca` becomes empty 247 """ 248 ds: list[D] = [] 249 250 while max > 0: 251 try: 252 ds.append(self.popL()) 253 except ValueError: 254 break 255 else: 256 max -= 1 257 258 return tuple(ds)
def
popRT(self, max: int) -> 'tuple[D, ...]':
260 def popRT(self, max: int) -> tuple[D, ...]: 261 """Pop multiple values from right side of `ca`. 262 263 * returns the results in a tuple of type `tuple[~D, ...]` 264 * returns an empty tuple if `ca` is empty 265 * pop no more that `max` values 266 * will pop less if `ca` becomes empty 267 """ 268 ds: list[D] = [] 269 while max > 0: 270 try: 271 ds.append(self.popR()) 272 except ValueError: 273 break 274 else: 275 max -= 1 276 277 return tuple(ds)
def
map(self, f: 'Callable[[D], U]', /) -> 'ca[U]':
279 def map[U](self, f: Callable[[D], U], /) -> ca[U]: 280 """Apply function f over contents, returns new `ca` instance. 281 282 * parameter `f` function of type `f[~D, ~U] -> ca[~U]` 283 * returns a new instance of type `ca[~U]` 284 """ 285 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':
287 def foldL[L](self, f: Callable[[L, D], L], /, initial: L|None=None) -> L: 288 """Left fold ca via function and optional initial value. 289 290 * parameter `f` function of type `f[~L, ~D] -> ~L` 291 * the first argument to `f` is for the accumulated value. 292 * parameter `initial` is an optional initial value 293 * returns the reduced value of type `~L` 294 * note that `~L` and `~D` can be the same type 295 * if an initial value is not given then by necessity `~L = ~D` 296 * raises `ValueError` when called on an empty `ca` and `initial` not given 297 """ 298 if self._count == 0: 299 if initial is None: 300 msg = 'Method foldL called on an empty ca without an initial value.' 301 raise ValueError(msg) 302 else: 303 return initial 304 else: 305 if initial is None: 306 acc = cast(L, self[0]) # in this case D = L 307 for idx in range(1, self._count): 308 acc = f(acc, self[idx]) 309 return acc 310 else: 311 acc = initial 312 for d in self: 313 acc = f(acc, d) 314 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':
316 def foldR[R](self, f: Callable[[D, R], R], /, initial: R|None=None) -> R: 317 """Right fold ca via function and optional initial value. 318 319 * parameter `f` function of type `f[~D, ~R] -> ~R` 320 * the second argument to f is for the accumulated value 321 * parameter `initial` is an optional initial value 322 * returns the reduced value of type `~R` 323 * note that `~R` and `~D` can be the same type 324 * if an initial value is not given then by necessity `~R = ~D` 325 * raises `ValueError` when called on an empty `ca` and `initial` not given 326 """ 327 if self._count == 0: 328 if initial is None: 329 msg = 'Method foldR called on an empty ca without an initial value.' 330 raise ValueError(msg) 331 else: 332 return initial 333 else: 334 if initial is None: 335 acc = cast(R, self[-1]) # in this case D = R 336 for idx in range(self._count-2, -1, -1): 337 acc = f(self[idx], acc) 338 return acc 339 else: 340 acc = initial 341 for d in reversed(self): 342 acc = f(d, acc) 343 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
capacity(self) -> int:
345 def capacity(self) -> int: 346 """Returns current capacity of the ca.""" 347 return self._capacity
Returns current capacity of the ca.
def
empty(self) -> None:
349 def empty(self) -> None: 350 """Empty the ca, keep current capacity.""" 351 self._data, self._front, self._rear = [None]*self._capacity, 0, self._capacity-1
Empty the ca, keep current capacity.
def
fractionFilled(self) -> float:
353 def fractionFilled(self) -> float: 354 """Returns fractional capacity of the ca.""" 355 return self._count/self._capacity
Returns fractional capacity of the ca.
def
resize(self, min_capacity: int = 2) -> None:
357 def resize(self, min_capacity: int=2) -> None: 358 """Compact `ca` and resize to `min_capacity` if necessary. 359 360 * to just compact the `ca`, do not provide a min_capacity 361 """ 362 self._compact_storage_capacity() 363 if min_capacity > self._capacity: 364 self._capacity, self._data = \ 365 min_capacity, self._data + [None]*(min_capacity-self._capacity) 366 if self._count == 0: 367 self._front, self._rear = 0, self._capacity - 1
def
CA(*ds: 'U') -> 'ca[U]':
369def CA[U](*ds: U) -> ca[U]: 370 """Function to produce a `ca` array from a variable number of arguments.""" 371 return ca(ds)
Function to produce a ca
array from a variable number of arguments.