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,103 @@
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
+ from collections.abc import Callable, Iterable, Iterator
16
+ from typing import Never, overload, TypeVar
17
+
18
+ __all__ = ['CA', 'ca']
19
+
20
+ I = TypeVar('I')
21
+ T = TypeVar('T')
22
+
23
+
24
+ class CA[I]():
25
+
26
+ L = TypeVar('L')
27
+ R = TypeVar('R')
28
+ U = TypeVar('U')
29
+
30
+ def __init__(
31
+ self,
32
+ items: Iterable[I] | None = None
33
+ ) -> None: ...
34
+
35
+ def _double_storage_capacity(self) -> None: ...
36
+
37
+ def _compact_storage_capacity(self) -> None: ...
38
+
39
+ def __iter__(self) -> Iterator[I]: ...
40
+
41
+ def __reversed__(self) -> Iterator[I]: ...
42
+
43
+ def __repr__(self) -> str: ...
44
+
45
+ def __str__(self) -> str: ...
46
+
47
+ def __bool__(self) -> bool: ...
48
+
49
+ def __len__(self) -> int: ...
50
+
51
+ @overload
52
+ def __getitem__(self, idx: int) -> I: ...
53
+ @overload
54
+ def __getitem__(self, idx: slice) -> CA[I]: ...
55
+
56
+ @overload
57
+ def __setitem__(self, idx: int, vals: I) -> None: ...
58
+ @overload
59
+ def __setitem__(self, idx: slice, vals: Iterable[I]) -> None: ...
60
+
61
+ @overload
62
+ def __delitem__(self, idx: int) -> None: ...
63
+ @overload
64
+ def __delitem__(self, idx: slice) -> None: ...
65
+
66
+ def __eq__(self, other: object) -> bool: ...
67
+
68
+ def pushl(self, *items: I) -> None: ...
69
+
70
+ def pushr(self, *items: I) -> None: ...
71
+
72
+ def popl(self) -> I | Never: ...
73
+
74
+ def popr(self) -> I | Never: ...
75
+
76
+ def popld(self, default: I) -> I: ...
77
+
78
+ def poprd(self, default: I) -> I: ...
79
+
80
+ def poplt(self, maximum: int) -> tuple[I, ...]: ...
81
+
82
+ def poprt(self, maximum: int) -> tuple[I, ...]: ...
83
+
84
+ def rotl(self, n: int = 1) -> None: ...
85
+
86
+ def rotr(self, n: int = 1) -> None: ...
87
+
88
+ def map[U](self, f: Callable[[I], U]) -> CA[U]: ...
89
+
90
+ def foldl[L](self, f: Callable[[L, I], L], start: L | None = None) -> L | Never: ...
91
+
92
+ def foldr[R](self, f: Callable[[I, R], R], start: R | None = None) -> R | Never: ...
93
+
94
+ def capacity(self) -> int: ...
95
+
96
+ def empty(self) -> None: ...
97
+
98
+ def fraction_filled(self) -> float: ...
99
+
100
+ def resize(self, minimum_capacity: int = 2) -> None: ...
101
+
102
+
103
+ def ca[T](*items: T) -> CA[T]: ...
@@ -0,0 +1,511 @@
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
+ """Circular array with fixed storage capacity.
16
+
17
+ - O(1) pops and pushes either end
18
+ - O(1) indexing, does not support slicing
19
+ - fixed total storage capacity
20
+ - iterable, can safely mutate while iterators continue iterating over previous state
21
+ - comparisons compare identity before equality, like builtins
22
+ - in boolean context, falsy when either empty or full, otherwise truthy
23
+ - factory function caf produces a fixed storage capacity circular array from its arguments
24
+
25
+ """
26
+ from collections.abc import Callable, Iterable, Iterator
27
+ from typing import cast, Final, Never, TypeVar
28
+
29
+ __all__ = ['CAF', 'caf']
30
+
31
+ I = TypeVar('I')
32
+ T = TypeVar('T')
33
+
34
+
35
+ class CAF[I]():
36
+
37
+ L = TypeVar('L')
38
+ R = TypeVar('R')
39
+ U = TypeVar('U')
40
+
41
+ __slots__ = '_items', '_cnt', '_cap', '_front', '_rear'
42
+
43
+ def __init__(
44
+ self,
45
+ items: Iterable[I] | None = None,
46
+ capacity: int = 2
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
+ :param capacity: fixed storage capacity of circular array
53
+ :raises TypeError: if items is not Iterable
54
+
55
+ """
56
+ capacity = max(2, capacity)
57
+ if items is None:
58
+ self._items: list[I | None] = [None]*capacity
59
+ count = 0
60
+ else:
61
+ dlist: list[I | None] = list(items)
62
+ count = len(dlist)
63
+ capacity = max(count, capacity)
64
+ self._items = dlist + [None]*(capacity - count)
65
+ self._cap: Final[int] = capacity
66
+ self._cnt = count
67
+ if count == 0:
68
+ self._front = 0
69
+ self._rear = capacity - 1
70
+ else:
71
+ self._front = 0
72
+ self._rear = count - 1
73
+
74
+ def __iter__(self) -> Iterator[I]:
75
+ if self._cnt > 0:
76
+ (
77
+ capacity,
78
+ rear,
79
+ position,
80
+ current_state,
81
+ ) = (
82
+ self._cap,
83
+ self._rear,
84
+ self._front,
85
+ self._items.copy(),
86
+ )
87
+
88
+ while position != rear:
89
+ yield cast(I, current_state[position])
90
+ position = (position + 1) % capacity
91
+ yield cast(I, current_state[position])
92
+
93
+ def __reversed__(self) -> Iterator[I]:
94
+ if self._cnt > 0:
95
+ (
96
+ capacity,
97
+ front,
98
+ position,
99
+ current_state,
100
+ ) = (
101
+ self._cap,
102
+ self._front,
103
+ self._rear,
104
+ self._items.copy(),
105
+ )
106
+
107
+ while position != front:
108
+ yield cast(I, current_state[position])
109
+ position = (position - 1) % capacity
110
+ yield cast(I, current_state[position])
111
+
112
+ def __repr__(self) -> str:
113
+ return 'caf(' + ', '.join(map(repr, self)) + ')'
114
+
115
+ def __str__(self) -> str:
116
+ return '(|' + ', '.join(map(str, self)) + '|)'
117
+
118
+ def __bool__(self) -> bool:
119
+ return 0 < self._cnt < self._cap
120
+
121
+ def __len__(self) -> int:
122
+ return self._cnt
123
+
124
+ def __getitem__(self, idx: int) -> I:
125
+ cnt = self._cnt
126
+ if 0 <= idx < cnt:
127
+ return cast(I, self._items[(self._front + idx) % self._cap])
128
+
129
+ if -cnt <= idx < 0:
130
+ return cast(I, self._items[(self._front + cnt + idx) % self._cap])
131
+
132
+ if cnt == 0:
133
+ msg0 = 'Trying to get a value from an empty CAF.'
134
+ raise IndexError(msg0)
135
+
136
+ msg1 = 'Out of bounds: '
137
+ msg2 = f'index = {idx} not between {-cnt} and {cnt - 1} '
138
+ msg3 = 'while getting value from a CAF.'
139
+ raise IndexError(msg1 + msg2 + msg3)
140
+
141
+ def __setitem__(self, idx: int, val: I) -> None:
142
+ cnt = self._cnt
143
+ if 0 <= idx < cnt:
144
+ self._items[(self._front + idx) % self._cap] = val
145
+ elif -cnt <= idx < 0:
146
+ self._items[(self._front + cnt + idx) % self._cap] = val
147
+ else:
148
+ if cnt < 1:
149
+ msg0 = 'Trying to index into an empty CAF.'
150
+ raise IndexError(msg0)
151
+ msg1 = 'Out of bounds: '
152
+ msg2 = f'index = {idx} not between {-cnt} and {cnt - 1} '
153
+ msg3 = 'while setting value from a CAF.'
154
+ raise IndexError(msg1 + msg2 + msg3)
155
+
156
+ def __delitem__(self, idx: int) -> None:
157
+ item_list = list(self)
158
+ del item_list[idx]
159
+ _ca = CAF(item_list, self._cap)
160
+ (
161
+ self._items,
162
+ self._cnt,
163
+ self._front,
164
+ self._rear,
165
+ ) = (
166
+ _ca._items,
167
+ _ca._cnt,
168
+ _ca._front,
169
+ _ca._rear,
170
+ )
171
+ del _ca
172
+
173
+ def __eq__(self, other: object) -> bool:
174
+ if self is other:
175
+ return True
176
+ if not isinstance(other, type(self)):
177
+ return False
178
+
179
+ (
180
+ front1,
181
+ count1,
182
+ capacity1,
183
+ front2,
184
+ count2,
185
+ capacity2,
186
+ ) = (
187
+ self._front,
188
+ self._cnt,
189
+ self._cap,
190
+ other._front,
191
+ other._cnt,
192
+ other._cap,
193
+ )
194
+
195
+ if count1 != count2:
196
+ return False
197
+
198
+ for nn in range(count1):
199
+ if self._items[(front1 + nn) % capacity1] is other._items[(front2 + nn) % capacity2]:
200
+ continue
201
+ if self._items[(front1 + nn) % capacity1] != other._items[(front2 + nn) % capacity2]:
202
+ return False
203
+ return True
204
+
205
+ def pushl(self, item: I) -> None:
206
+ """Push single item onto left side (front) of circular array.
207
+
208
+ :param item: single item pushed onto circular array from left
209
+ :raises ValueError: when called on a full CAF
210
+
211
+ """
212
+ if self._cnt == self._cap:
213
+ msg = 'Method pushl called on a full CAF'
214
+ raise ValueError(msg)
215
+
216
+ (
217
+ self._front,
218
+ self._items[self._front],
219
+ self._cnt,
220
+ ) = (
221
+ (self._front - 1) % self._cap,
222
+ item,
223
+ self._cnt + 1,
224
+ )
225
+
226
+ def pushr(self, item: I) -> None:
227
+ """Push a single item onto right side (rear) of circular array.
228
+
229
+ :param item: single item pushed onto circular array from right
230
+ :raises ValueError: when called on a full fixed storage capacity circular array
231
+
232
+ """
233
+ if self._cnt == self._cap:
234
+ msg = 'Method pushr called on a full CAF'
235
+ raise ValueError(msg)
236
+
237
+ (
238
+ self._rear,
239
+ self._items[self._rear],
240
+ self._cnt,
241
+ ) = (
242
+ (self._rear + 1) % self._cap,
243
+ item,
244
+ self._cnt + 1,
245
+ )
246
+
247
+ def popl(self) -> I | Never:
248
+ """Pop item off left side (front) of circular array.
249
+
250
+ :return: item popped from left side of circular array
251
+ :raises ValueError: when called on an empty circular array
252
+
253
+ """
254
+ if self._cnt > 1:
255
+ (
256
+ d,
257
+ self._items[self._front],
258
+ self._front,
259
+ self._cnt,
260
+ ) = (
261
+ self._items[self._front],
262
+ None,
263
+ (self._front + 1) % self._cap,
264
+ self._cnt - 1,
265
+ )
266
+ elif self._cnt == 1:
267
+ (
268
+ d,
269
+ self._items[self._front],
270
+ self._cnt,
271
+ self._front,
272
+ self._rear,
273
+ ) = (
274
+ self._items[self._front],
275
+ None,
276
+ 0,
277
+ 0,
278
+ self._cap - 1,
279
+ )
280
+ else:
281
+ msg = 'Method popl called on an empty CAF'
282
+ raise ValueError(msg)
283
+ return cast(I, d)
284
+
285
+ def popr(self) -> I | Never:
286
+ """Pop item off right side (rear) of circular array.
287
+
288
+ :return: item popped from right side of circular array
289
+ :raises ValueError: when called on an empty circular array
290
+
291
+ """
292
+ if self._cnt > 1:
293
+ (
294
+ d,
295
+ self._items[self._rear],
296
+ self._rear,
297
+ self._cnt,
298
+ ) = (
299
+ self._items[self._rear],
300
+ None,
301
+ (self._rear - 1) % self._cap,
302
+ self._cnt - 1,
303
+ )
304
+ elif self._cnt == 1:
305
+ (
306
+ d,
307
+ self._items[self._front],
308
+ self._cnt,
309
+ self._front,
310
+ self._rear,
311
+ ) = (
312
+ self._items[self._front],
313
+ None,
314
+ 0,
315
+ 0,
316
+ self._cap - 1,
317
+ )
318
+ else:
319
+ msg = 'Method popr called on an empty CAF'
320
+ raise ValueError(msg)
321
+ return cast(I, d)
322
+
323
+ def popld(self, default: I) -> I:
324
+ """Pop one item from left side of the circular array, provide
325
+ a mandatory default value. "Safe" version of popl.
326
+
327
+
328
+ Pop one item from left side of the circular array, provide
329
+ a mandatory default value. "Safe" version of popl.
330
+
331
+ :param default: item returned if circular array is empty
332
+ :return: item popped from left side or default item if empty
333
+
334
+ """
335
+ try:
336
+ return self.popl()
337
+ except ValueError:
338
+ return default
339
+
340
+ def poprd(self, default: I) -> I:
341
+ """Pop one item from right side of the circular array, provide
342
+ a mandatory default value. "Safe" version of popr.
343
+
344
+ :param default: item returned if circular array is empty
345
+ :return: item popped from right side or default item if empty
346
+
347
+ """
348
+ try:
349
+ return self.popr()
350
+ except ValueError:
351
+ return default
352
+
353
+ def poplt(self, maximum: int) -> tuple[I, ...]:
354
+ """Pop multiple items from left side of circular array.
355
+
356
+ :param maximum: maximum number of items to pop, may pop less if not enough items
357
+ :return: items in the order popped, left to right
358
+
359
+ """
360
+ item_list: list[I] = []
361
+
362
+ while maximum > 0:
363
+ try:
364
+ item_list.append(self.popl())
365
+ except ValueError:
366
+ break
367
+ else:
368
+ maximum -= 1
369
+
370
+ return tuple(item_list)
371
+
372
+ def poprt(self, maximum: int) -> tuple[I, ...]:
373
+ """Pop multiple items from right side of circular array.
374
+
375
+ :param maximum: maximum number of items to pop, may pop less if not enough items
376
+ :return: items in the order popped, right to left
377
+
378
+ """
379
+ item_list: list[I] = []
380
+ while maximum > 0:
381
+ try:
382
+ item_list.append(self.popr())
383
+ except ValueError:
384
+ break
385
+ else:
386
+ maximum -= 1
387
+ return tuple(item_list)
388
+
389
+ def rotl(self, n: int = 1) -> None:
390
+ """Rotate items left.
391
+
392
+ :param n: number of times to shift elements to the left
393
+
394
+ """
395
+ if self._cnt < 2:
396
+ return
397
+ for _ in range(n, 0, -1):
398
+ self.pushr(self.popl())
399
+
400
+ def rotr(self, n: int = 1) -> None:
401
+ """Rotate items right.
402
+
403
+ :param n: number of times to shift elements to the right
404
+
405
+ """
406
+ if self._cnt < 2:
407
+ return
408
+ for _ in range(n, 0, -1):
409
+ self.pushl(self.popr())
410
+
411
+ def map[U](self, f: Callable[[I], U]) -> "CAF[U]":
412
+ """Apply function f over the circular array's contents,
413
+
414
+ :param f: callable from type I to type U
415
+ :return: new fixed circular array instance
416
+
417
+ """
418
+ return CAF(map(f, self), self._cap)
419
+
420
+ def foldl[L](self, f: Callable[[L, I], L], start: L | None = None) -> L | Never:
421
+ """Fold left with a function and optional stating item.
422
+
423
+ :param f: first argument to f is for the accumulator
424
+ :param start: optional starting item
425
+ :return: reduced value produced by the left fold
426
+ :raises ValueError: when circular array empty and no starting item given
427
+
428
+ """
429
+ if self._cnt == 0:
430
+ if start is None:
431
+ msg = 'Method foldl called on an empty CAF without a start item.'
432
+ raise ValueError(msg)
433
+ return start
434
+
435
+ if start is None:
436
+ acc = cast(L, self[0]) # in this case D = L
437
+ for idx in range(1, self._cnt):
438
+ acc = f(acc, self[idx])
439
+ return acc
440
+
441
+ acc = start
442
+ for d in self:
443
+ acc = f(acc, d)
444
+ return acc
445
+
446
+ def foldr[R](self, f: Callable[[I, R], R], start: R | None = None) -> R | Never:
447
+ """Fold right with a function and an optional starting item.
448
+
449
+ :param f: second argument to f is for the accumulator
450
+ :param start: optional starting item
451
+ :return: reduced value produced by the right fold
452
+ :raises ValueError: when circular array empty and no starting item given
453
+
454
+ """
455
+ if self._cnt == 0:
456
+ if start is None:
457
+ msg = 'Method foldr called on empty CAF without initial value.'
458
+ raise ValueError(msg)
459
+ return start
460
+
461
+ if start is None:
462
+ acc = cast(R, self[-1]) # in this case D = R
463
+ for idx in range(self._cnt - 2, -1, -1):
464
+ acc = f(self[idx], acc)
465
+ return acc
466
+
467
+ acc = start
468
+ for d in reversed(self):
469
+ acc = f(d, acc)
470
+ return acc
471
+
472
+ def capacity(self) -> int:
473
+ """Return fixed storage capacity of the circular array.
474
+
475
+ :return: fixed storage capacity
476
+
477
+ """
478
+ return self._cap
479
+
480
+ def empty(self) -> None:
481
+ """Empty the circular array."""
482
+ (
483
+ self._items,
484
+ self._front,
485
+ self._rear,
486
+ self._cnt,
487
+ ) = (
488
+ [None] * self._cap,
489
+ 0,
490
+ self._cap - 1,
491
+ 0,
492
+ )
493
+
494
+ def fraction_filled(self) -> float:
495
+ """Find fraction of the storage capacity which is filled.
496
+
497
+ :return: the ratio count/capacity
498
+
499
+ """
500
+ return self._cnt / self._cap
501
+
502
+
503
+ def caf[T](*items: T, capacity: int = 2) -> CAF[T]:
504
+ """Produce a circular array from a variable number of arguments.
505
+
506
+ :param items: initial items for a new circular array
507
+ :param capacity: the minimum storage capacity to set
508
+ :return: new fixed storage capacity circular array
509
+
510
+ """
511
+ return CAF(items, capacity = capacity)
@@ -0,0 +1,89 @@
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
+ from collections.abc import Callable, Iterable, Iterator
16
+ from typing import Never, TypeVar
17
+
18
+ __all__ = ['CAF', 'caf']
19
+
20
+ I = TypeVar('I')
21
+ T = TypeVar('T')
22
+
23
+
24
+ class CAF[I]():
25
+
26
+ L = TypeVar('L')
27
+ R = TypeVar('R')
28
+ U = TypeVar('U')
29
+
30
+ def __init__(
31
+ self,
32
+ items: Iterable[I] | None = None,
33
+ capacity: int = 2
34
+ ) -> None: ...
35
+
36
+ def __iter__(self) -> Iterator[I]: ...
37
+
38
+ def __reversed__(self) -> Iterator[I]: ...
39
+
40
+ def __repr__(self) -> str: ...
41
+
42
+ def __str__(self) -> str: ...
43
+
44
+ def __bool__(self) -> bool: ...
45
+
46
+ def __len__(self) -> int: ...
47
+
48
+ def __getitem__(self, idx: int) -> I: ...
49
+
50
+ def __setitem__(self, idx: int, val: I) -> None: ...
51
+
52
+ def __delitem__(self, idx: int) -> None: ...
53
+
54
+ def __eq__(self, other: object) -> bool: ...
55
+
56
+ def pushl(self, item: I) -> None: ...
57
+
58
+ def pushr(self, item: I) -> None: ...
59
+
60
+ def popl(self) -> I | Never: ...
61
+
62
+ def popr(self) -> I | Never: ...
63
+
64
+ def popld(self, default: I) -> I: ...
65
+
66
+ def poprd(self, default: I) -> I: ...
67
+
68
+ def poplt(self, maximum: int) -> tuple[I, ...]: ...
69
+
70
+ def poprt(self, maximum: int) -> tuple[I, ...]: ...
71
+
72
+ def rotl(self, n: int = 1) -> None: ...
73
+
74
+ def rotr(self, n: int = 1) -> None: ...
75
+
76
+ def map[U](self, f: Callable[[I], U]) -> CAF[U]: ...
77
+
78
+ def foldl[L](self, f: Callable[[L, I], L], start: L | None = None) -> L | Never: ...
79
+
80
+ def foldr[R](self, f: Callable[[I, R], R], start: R | None = None) -> R | Never: ...
81
+
82
+ def capacity(self) -> int: ...
83
+
84
+ def empty(self) -> None: ...
85
+
86
+ def fraction_filled(self) -> float: ...
87
+
88
+
89
+ def caf[T](*items: T, capacity: int = 2) -> CAF[T]: ...
File without changes