pythonic-fp-circulararray 5.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ # Copyright 2024-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
+ """Package implementing stateful circular array data structures.
16
+
17
+ - O(1) pops and pushes either end
18
+ - O(1) indexing and size determination
19
+ - O(1) size determination
20
+
21
+ +-------------------------------------+-------------------------------------------+
22
+ | Module | Description |
23
+ +=====================================+===========================================+
24
+ | **pythonic_fp.circulararray.auto** | Variable storage capacity circular array. |
25
+ +-------------------------------------+-------------------------------------------+
26
+ | **pythonic_fp.circulararray.fixed** | Fixed storage capacity circular array. |
27
+ +-------------------------------------+-------------------------------------------+
28
+
29
+ """
30
+
31
+ __author__ = 'Geoffrey R. Scheller'
32
+ __copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
33
+ __license__ = 'Apache License 2.0'
@@ -0,0 +1,631 @@
1
+ # Copyright 2023-202 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
+ """Circular array with variable storage capacity.
16
+
17
+ - O(1) pops either end
18
+ - O(1) amortized pushes either end
19
+ - O(1) indexing, fully supports slicing
20
+ - auto-resizing more storage capacity when necessary, manually compatible
21
+ - iterable, can safely mutate while iterators continue iterating over previous state
22
+ - comparisons compare identity before equality, like builtins
23
+ - in boolean context, falsy when empty, otherwise truthy
24
+ - factory function ca produces a variable storage capacity circular array from its arguments
25
+
26
+ """
27
+ from collections.abc import Callable, Iterable, Iterator
28
+ from typing import cast, Never, overload, TypeVar
29
+
30
+ __all__ = ['CA', 'ca']
31
+
32
+ I = TypeVar('I')
33
+ T = TypeVar('T')
34
+
35
+
36
+ class CA[I]():
37
+
38
+ L = TypeVar('L')
39
+ R = TypeVar('R')
40
+ U = TypeVar('U')
41
+
42
+ __slots__ = '_items', '_cnt', '_cap', '_front', '_rear'
43
+
44
+ def __init__(
45
+ self,
46
+ items: Iterable[I] | None = None
47
+ ) -> None:
48
+ """Basically a list that can be grown from both ends
49
+ in O(1) time and space complexity.
50
+
51
+ :param items: optional iterable to initial populate circular array
52
+ :raises TypeError: if items is not Iterable
53
+
54
+ """
55
+ if items is None:
56
+ self._items: list[I | None] = [None, None]
57
+ else:
58
+ self._items = [None] + list(items) + [None]
59
+ self._cap = cap = len(self._items)
60
+ self._cnt = cap - 2
61
+ if cap == 2:
62
+ self._front = 0
63
+ self._rear = 1
64
+ else:
65
+ self._front = 1
66
+ self._rear = cap - 2
67
+
68
+ def _double_storage_capacity(self) -> None:
69
+ if self._front <= self._rear:
70
+ (
71
+ self._items,
72
+ self._cap,
73
+ ) = (
74
+ self._items + [None] * self._cap,
75
+ self._cap * 2,
76
+ )
77
+ else:
78
+ (
79
+ self._items,
80
+ self._front,
81
+ self._cap,
82
+ ) = (
83
+ self._items[: self._front] + [None]*self._cap + self._items[self._front:],
84
+ self._front + self._cap,
85
+ 2*self._cap,
86
+ )
87
+
88
+ def _compact_storage_capacity(self) -> None:
89
+ match self._cnt:
90
+ case 0:
91
+ (
92
+ self._cap,
93
+ self._front,
94
+ self._rear,
95
+ self._items,
96
+ ) = (
97
+ 2,
98
+ 0,
99
+ 1,
100
+ [None, None],
101
+ )
102
+ case 1:
103
+ (
104
+ self._cap,
105
+ self._front,
106
+ self._rear,
107
+ self._items,
108
+ ) = (
109
+ 3,
110
+ 1,
111
+ 1,
112
+ [None, self._items[self._front], None],
113
+ )
114
+ case _:
115
+ if self._front <= self._rear:
116
+ (
117
+ self._cap,
118
+ self._front,
119
+ self._rear,
120
+ self._items,
121
+ ) = (
122
+ self._cnt + 2,
123
+ 1,
124
+ self._cnt,
125
+ [None] + self._items[self._front : self._rear + 1] + [None],
126
+ )
127
+ else:
128
+ (
129
+ self._cap,
130
+ self._front,
131
+ self._rear,
132
+ self._items,
133
+ ) = (
134
+ self._cnt + 2,
135
+ 1,
136
+ self._cnt,
137
+ [None] + self._items[self._front :] + self._items[: self._rear + 1] + [None],
138
+ )
139
+
140
+ def __iter__(self) -> Iterator[I]:
141
+ if self._cnt > 0:
142
+ (
143
+ capacity,
144
+ rear,
145
+ position,
146
+ current_state,
147
+ ) = (
148
+ self._cap,
149
+ self._rear,
150
+ self._front,
151
+ self._items.copy(),
152
+ )
153
+
154
+ while position != rear:
155
+ yield cast(I, current_state[position])
156
+ position = (position + 1) % capacity
157
+ yield cast(I, current_state[position])
158
+
159
+ def __reversed__(self) -> Iterator[I]:
160
+ if self._cnt > 0:
161
+ (
162
+ capacity,
163
+ front,
164
+ position,
165
+ current_state,
166
+ ) = (
167
+ self._cap,
168
+ self._front,
169
+ self._rear,
170
+ self._items.copy(),
171
+ )
172
+
173
+ while position != front:
174
+ yield cast(I, current_state[position])
175
+ position = (position - 1) % capacity
176
+ yield cast(I, current_state[position])
177
+
178
+ def __repr__(self) -> str:
179
+ return 'ca(' + ', '.join(map(repr, self)) + ')'
180
+
181
+ def __str__(self) -> str:
182
+ return '(|' + ', '.join(map(str, self)) + '|)'
183
+
184
+ def __bool__(self) -> bool:
185
+ return self._cnt > 0
186
+
187
+ def __len__(self) -> int:
188
+ return self._cnt
189
+
190
+ @overload
191
+ def __getitem__(self, idx: int) -> I: ...
192
+ @overload
193
+ def __getitem__(self, idx: slice) -> "CA[I]": ...
194
+
195
+ def __getitem__(self, idx: int | slice) -> I | "CA[I]":
196
+ if isinstance(idx, slice):
197
+ return CA(list(self)[idx])
198
+
199
+ cnt = self._cnt
200
+ if 0 <= idx < cnt:
201
+ return cast(I, self._items[(self._front + idx) % self._cap])
202
+
203
+ if -cnt <= idx < 0:
204
+ return cast(I, self._items[(self._front + cnt + idx) % self._cap])
205
+
206
+ if cnt == 0:
207
+ msg0 = 'Trying to get a value from an empty CA.'
208
+ raise IndexError(msg0)
209
+
210
+ msg1 = 'Out of bounds: '
211
+ msg2 = f'index = {idx} not between {-cnt} and {cnt - 1} '
212
+ msg3 = 'while getting value from a CA.'
213
+ raise IndexError(msg1 + msg2 + msg3)
214
+
215
+ @overload
216
+ def __setitem__(self, idx: int, vals: I) -> None: ...
217
+ @overload
218
+ def __setitem__(self, idx: slice, vals: Iterable[I]) -> None: ...
219
+
220
+ def __setitem__(self, idx: int | slice, vals: I | Iterable[I]) -> None:
221
+ if isinstance(idx, slice):
222
+ if isinstance(vals, Iterable):
223
+ item_list = list(self)
224
+ item_list[idx] = vals
225
+ _ca = CA(item_list)
226
+ (
227
+ self._items,
228
+ self._cnt,
229
+ self._cap,
230
+ self._front,
231
+ self._rear,
232
+ ) = (
233
+ _ca._items,
234
+ _ca._cnt,
235
+ _ca._cap,
236
+ _ca._front,
237
+ _ca._rear,
238
+ )
239
+ return
240
+
241
+ msg = 'must assign iterable to extended slice'
242
+ raise TypeError(msg)
243
+
244
+ cnt = self._cnt
245
+ if 0 <= idx < cnt:
246
+ self._items[(self._front + idx) % self._cap] = cast(I, vals)
247
+ elif -cnt <= idx < 0:
248
+ self._items[(self._front + cnt + idx) % self._cap] = cast(I, vals)
249
+ else:
250
+ if cnt < 1:
251
+ msg0 = 'Trying to index into an empty CA.'
252
+ raise IndexError(msg0)
253
+ msg1 = 'Out of bounds: '
254
+ msg2 = f'index = {idx} not between {-cnt} and {cnt - 1} '
255
+ msg3 = 'while setting value from a CA.'
256
+ raise IndexError(msg1 + msg2 + msg3)
257
+
258
+ @overload
259
+ def __delitem__(self, idx: int) -> None: ...
260
+ @overload
261
+ def __delitem__(self, idx: slice) -> None: ...
262
+
263
+ def __delitem__(self, idx: int | slice) -> None:
264
+ item_list = list(self)
265
+ del item_list[idx]
266
+ _ca = CA(item_list)
267
+ (
268
+ self._items,
269
+ self._cnt,
270
+ self._cap,
271
+ self._front,
272
+ self._rear,
273
+ ) = (
274
+ _ca._items,
275
+ _ca._cnt,
276
+ _ca._cap,
277
+ _ca._front,
278
+ _ca._rear,
279
+ )
280
+ del _ca
281
+
282
+ def __eq__(self, other: object) -> bool:
283
+ if self is other:
284
+ return True
285
+ if not isinstance(other, type(self)):
286
+ return False
287
+
288
+ (
289
+ front1,
290
+ count1,
291
+ capacity1,
292
+ front2,
293
+ count2,
294
+ capacity2,
295
+ ) = (
296
+ self._front,
297
+ self._cnt,
298
+ self._cap,
299
+ other._front,
300
+ other._cnt,
301
+ other._cap,
302
+ )
303
+
304
+ if count1 != count2:
305
+ return False
306
+
307
+ for nn in range(count1):
308
+ if self._items[(front1 + nn) % capacity1] is other._items[(front2 + nn) % capacity2]:
309
+ continue
310
+ if self._items[(front1 + nn) % capacity1] != other._items[(front2 + nn) % capacity2]:
311
+ return False
312
+ return True
313
+
314
+ def pushl(self, *items: I) -> None:
315
+ """Push items onto left side (front) of circular array.
316
+
317
+ :param items: items pushed onto circular array from left
318
+
319
+ """
320
+ for item in items:
321
+ if self._cnt == self._cap:
322
+ self._double_storage_capacity()
323
+ (
324
+ self._front,
325
+ self._items[self._front],
326
+ self._cnt,
327
+ ) = (
328
+ (self._front - 1) % self._cap,
329
+ item,
330
+ self._cnt + 1,
331
+ )
332
+
333
+ def pushr(self, *items: I) -> None:
334
+ """Push items onto right side (rear) of circular array.
335
+
336
+ :param items: items pushed onto circular array from right
337
+
338
+ """
339
+ for item in items:
340
+ if self._cnt == self._cap:
341
+ self._double_storage_capacity()
342
+ (
343
+ self._rear,
344
+ self._items[self._rear],
345
+ self._cnt,
346
+ ) = (
347
+ (self._rear + 1) % self._cap,
348
+ item,
349
+ self._cnt + 1,
350
+ )
351
+
352
+ def popl(self) -> I | Never:
353
+ """Pop item off left side (front) of circular array.
354
+
355
+ :return: item popped from left side of circular array
356
+ :raises ValueError: when called on an empty circular array
357
+
358
+ """
359
+ if self._cnt > 1:
360
+ (
361
+ d,
362
+ self._items[self._front],
363
+ self._front,
364
+ self._cnt,
365
+ ) = (
366
+ self._items[self._front],
367
+ None,
368
+ (self._front + 1) % self._cap,
369
+ self._cnt - 1,
370
+ )
371
+ elif self._cnt == 1:
372
+ (
373
+ d,
374
+ self._items[self._front],
375
+ self._cnt,
376
+ self._front,
377
+ self._rear,
378
+ ) = (
379
+ self._items[self._front],
380
+ None,
381
+ 0,
382
+ 0,
383
+ self._cap - 1,
384
+ )
385
+ else:
386
+ msg = 'Method popl called on an empty CA'
387
+ raise ValueError(msg)
388
+ return cast(I, d)
389
+
390
+ def popr(self) -> I | Never:
391
+ """Pop item off right side (rear) of circular array.
392
+
393
+ :return: item popped from right side of circular array
394
+ :raises ValueError: when called on an empty circular array
395
+
396
+ """
397
+ if self._cnt > 1:
398
+ (
399
+ d,
400
+ self._items[self._rear],
401
+ self._rear,
402
+ self._cnt,
403
+ ) = (
404
+ self._items[self._rear],
405
+ None,
406
+ (self._rear - 1) % self._cap,
407
+ self._cnt - 1,
408
+ )
409
+ elif self._cnt == 1:
410
+ (
411
+ d,
412
+ self._items[self._front],
413
+ self._cnt,
414
+ self._front,
415
+ self._rear,
416
+ ) = (
417
+ self._items[self._front],
418
+ None,
419
+ 0,
420
+ 0,
421
+ self._cap - 1,
422
+ )
423
+ else:
424
+ msg = 'Method popr called on an empty CA'
425
+ raise ValueError(msg)
426
+ return cast(I, d)
427
+
428
+ def popld(self, default: I) -> I:
429
+ """Pop one item from left side of the circular array, provide
430
+ a mandatory default value. "Safe" version of popl.
431
+
432
+ :param default: item returned if circular array is empty
433
+ :return: item popped from left side or default item if empty
434
+
435
+ """
436
+ try:
437
+ return self.popl()
438
+ except ValueError:
439
+ return default
440
+
441
+ def poprd(self, default: I) -> I:
442
+ """Pop one item from right side of the circular array, provide
443
+ a mandatory default value. "Safe" version of popr.
444
+
445
+ :param default: item returned if circular array is empty
446
+ :return: item popped from right side or default item if empty
447
+
448
+ """
449
+ try:
450
+ return self.popr()
451
+ except ValueError:
452
+ return default
453
+
454
+ def poplt(self, maximum: int) -> tuple[I, ...]:
455
+ """Pop multiple items from left side of circular array.
456
+
457
+ :param maximum: maximum number of items to pop, may pop less if not enough items
458
+ :return: items in the order popped, left to right
459
+
460
+ """
461
+ item_list: list[I] = []
462
+
463
+ while maximum > 0:
464
+ try:
465
+ item_list.append(self.popl())
466
+ except ValueError:
467
+ break
468
+ else:
469
+ maximum -= 1
470
+
471
+ return tuple(item_list)
472
+
473
+ def poprt(self, maximum: int) -> tuple[I, ...]:
474
+ """Pop multiple items from right side of circular array.
475
+
476
+ :param maximum: maximum number of items to pop, may pop less if not enough items
477
+ :return: items in the order popped, right to left
478
+
479
+ """
480
+ item_list: list[I] = []
481
+ while maximum > 0:
482
+ try:
483
+ item_list.append(self.popr())
484
+ except ValueError:
485
+ break
486
+ else:
487
+ maximum -= 1
488
+ return tuple(item_list)
489
+
490
+ def rotl(self, n: int = 1) -> None:
491
+ """Rotate items left.
492
+
493
+ :param n: number of times to shift elements to the left
494
+
495
+ """
496
+ if self._cnt < 2:
497
+ return
498
+ for _ in range(n, 0, -1):
499
+ self.pushr(self.popl())
500
+
501
+ def rotr(self, n: int = 1) -> None:
502
+ """Rotate items right.
503
+
504
+ :param n: number of times to shift elements to the right
505
+
506
+ """
507
+ if self._cnt < 2:
508
+ return
509
+ for _ in range(n, 0, -1):
510
+ self.pushl(self.popr())
511
+
512
+ def map[U](self, f: Callable[[I], U]) -> "CA[U]":
513
+ """Apply function f over the circular array's contents,
514
+
515
+ :param f: callable from type I to type U
516
+ :return: new circular array instance
517
+
518
+ """
519
+ return CA(map(f, self))
520
+
521
+ def foldl[L](self, f: Callable[[L, I], L], start: L | None = None) -> L | Never:
522
+ """Fold left with a function and optional starting item.
523
+
524
+ :param f: first argument to f is for the accumulator
525
+ :param start: optional starting item
526
+ :return: reduced value produced by the left fold
527
+ :raises ValueError: when circular array empty and no starting item given
528
+
529
+ """
530
+ if self._cnt == 0:
531
+ if start is None:
532
+ msg = 'Method foldl called on an empty CA without a start item.'
533
+ raise ValueError(msg)
534
+ return start
535
+
536
+ if start is None:
537
+ acc = cast(L, self[0]) # in this case D = L
538
+ for idx in range(1, self._cnt):
539
+ acc = f(acc, self[idx])
540
+ return acc
541
+
542
+ acc = start
543
+ for d in self:
544
+ acc = f(acc, d)
545
+ return acc
546
+
547
+ def foldr[R](self, f: Callable[[I, R], R], start: R | None = None) -> R | Never:
548
+ """Fold right with a function and an optional starting item.
549
+
550
+ :param f: second argument to f is for the accumulator
551
+ :param start: optional starting item
552
+ :return: reduced value produced by the right fold
553
+ :raises ValueError: when circular array empty and no starting item given
554
+
555
+ """
556
+ if self._cnt == 0:
557
+ if start is None:
558
+ msg = 'Method foldr called on empty CA without initial value.'
559
+ raise ValueError(msg)
560
+ return start
561
+
562
+ if start is None:
563
+ acc = cast(R, self[-1]) # in this case D = R
564
+ for idx in range(self._cnt - 2, -1, -1):
565
+ acc = f(self[idx], acc)
566
+ return acc
567
+
568
+ acc = start
569
+ for d in reversed(self):
570
+ acc = f(d, acc)
571
+ return acc
572
+
573
+ def capacity(self) -> int:
574
+ """Return current storage capacity of the circular array.
575
+
576
+ :return: current storage capacity
577
+
578
+ """
579
+ return self._cap
580
+
581
+ def empty(self) -> None:
582
+ """Empty the circular array, keep current storage capacity."""
583
+ (
584
+ self._items,
585
+ self._front,
586
+ self._rear,
587
+ self._cnt,
588
+ ) = (
589
+ [None] * self._cap,
590
+ 0,
591
+ self._cap - 1,
592
+ 0,
593
+ )
594
+
595
+ def fraction_filled(self) -> float:
596
+ """Find fraction of the storage capacity which is filled.
597
+
598
+ :return: the ratio count/capacity
599
+
600
+ """
601
+ return self._cnt / self._cap
602
+
603
+ def resize(self, minimum_capacity: int = 2) -> None:
604
+ """Compact circular array and, if necessary, resize to a minimum
605
+ storage capacity. To just compact the circular array, do not
606
+ provide ``minimum_capacity``.
607
+
608
+ :param minimum_capacity: minimum storage capacity to compact the circular array
609
+
610
+ """
611
+ self._compact_storage_capacity()
612
+ if (min_cap := minimum_capacity) > self._cap:
613
+ (
614
+ self._cap,
615
+ self._items,
616
+ ) = (
617
+ min_cap,
618
+ self._items + [None] * (min_cap - self._cap),
619
+ )
620
+ if self._cnt == 0:
621
+ self._front, self._rear = 0, self._cap - 1
622
+
623
+
624
+ def ca[T](*items: T) -> CA[T]:
625
+ """Produce circular array from a variable number of arguments.
626
+
627
+ :param items: initial items for a new circular array
628
+ :return: new variable storage capacity circular array
629
+
630
+ """
631
+ return CA(items)