Skip to content

Commit 1bc6df8

Browse files
committed
add chunk()
1 parent c2a3f14 commit 1bc6df8

File tree

6 files changed

+118
-4
lines changed

6 files changed

+118
-4
lines changed

RunTest.ps1

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
coverage run -m pytest
1+
coverage run -m pytest @args
22
if ($?)
33
{
44
coverage report -m

doc/api/types_linq.enumerable.rst

+29
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,35 @@ Example
506506
507507
----
508508

509+
instancemethod ``chunk(size)``
510+
--------------------------------
511+
512+
Parameters
513+
- `size` (``int``)
514+
515+
Returns
516+
- ``Enumerable[MutableSequence[TSource_co]]``
517+
518+
Splits the elements of a sequence into chunks of size at most the provided size. Raises
519+
`InvalidOperationError` if `size` is less than 1.
520+
521+
Example
522+
.. code-block:: python
523+
524+
>>> def source(i):
525+
... while True:
526+
... yield i
527+
... i *= 3
528+
529+
>>> en = Enumerable(source(1)).chunk(4).take(3)
530+
>>> for chunk in en:
531+
... print(chunk)
532+
[1, 3, 9, 27]
533+
[81, 243, 729, 2187]
534+
[6561, 19683, 59049, 177147]
535+
536+
----
537+
509538
instancemethod ``concat(second)``
510539
-----------------------------------
511540

doc/api_spec.py

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class ClassSpec(TypedDict):
5252
'average',
5353
'average2',
5454
'cast',
55+
'chunk',
5556
'concat',
5657
'contains',
5758
'count',

tests/test_usage.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,48 @@ def test_average2_overload2(self):
212212
assert avg == 5.8
213213

214214

215+
class TestChunkMethod:
216+
def test_infinite_exact(self):
217+
def source(i):
218+
while True:
219+
yield i
220+
i *= 3
221+
en = Enumerable(source(1)).chunk(4).take(3)
222+
assert en.to_list() == [
223+
[1, 3, 9, 27], [81, 243, 729, 2187], [6561, 19683, 59049, 177147],
224+
]
225+
226+
def test_sized_last_not_full(self):
227+
ints = [1, 3, 9, 27, 81, 243, 729]
228+
en = Enumerable(ints).chunk(3)
229+
assert en.to_list() == [[1, 3, 9], [27, 81, 243], [729]]
230+
231+
def test_elements_less_than_size(self):
232+
ints = [1, 3, 9, 27]
233+
en = Enumerable(ints).chunk(5)
234+
assert en.to_list() == [[1, 3, 9, 27]]
235+
236+
def test_empty(self):
237+
en = Enumerable([])
238+
assert en.chunk(3).to_list() == []
239+
240+
def test_mutate_source_before_iteration(self):
241+
ints = [1, 3, 9, 27, 81, 243, 729]
242+
en = Enumerable(ints).chunk(2)
243+
it = iter(en)
244+
assert next(it) == [1, 3]
245+
ints.remove(27)
246+
assert next(it) == [9, 81]
247+
assert next(it) == [243, 729]
248+
249+
def test_invalid_size(self):
250+
en = Enumerable([0])
251+
with pytest.raises(InvalidOperationError):
252+
en.chunk(0)
253+
with pytest.raises(InvalidOperationError):
254+
en.chunk(-1)
255+
256+
215257
class TestConcatMethod:
216258
def test_concat(self):
217259
en1 = Enumerable([1, 2, 3])
@@ -371,7 +413,8 @@ def test_fallback(self):
371413
en3[1]
372414

373415
def test_not_fallback_and_with_default(self):
374-
en = Enumerable([])
416+
en = Enumerable([1])
417+
assert en[0, 'haha'] == 1
375418
assert en[1, 'haha'] == 'haha'
376419

377420

types_linq/enumerable.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from __future__ import annotations
2-
from typing import Any, Callable, Container, Deque, Dict, Iterable, Iterator, List, NoReturn, Optional, Reversible, Sequence, Set, Sized, TYPE_CHECKING, Tuple, Type, Generic, Union
2+
from typing import Any, Callable, Container, Deque, Dict, Iterable, Iterator, List, MutableSequence, NoReturn, Optional, Reversible, Sequence, Set, Sized, TYPE_CHECKING, Tuple, Type, Generic, Union
33

44
if TYPE_CHECKING:
55
from .lookup import Lookup
@@ -255,6 +255,25 @@ def average2(self, *args):
255255
def cast(self, _: Type[TResult]) -> Enumerable[TResult]:
256256
return self # type: ignore
257257

258+
def chunk(self, size: int) -> Enumerable[MutableSequence[TSource_co]]:
259+
if size < 1:
260+
raise InvalidOperationError('size must be greater than 0')
261+
def inner():
262+
lst: List[Any] = [1] * size
263+
i = 0
264+
for elem in self:
265+
lst[i] = elem
266+
if i == size - 1:
267+
yield lst
268+
i = 0
269+
lst = [1] * size
270+
else:
271+
i += 1
272+
if i > 0:
273+
del lst[i:]
274+
yield lst
275+
return Enumerable(inner)
276+
258277
def concat(self, second: Iterable[TSource_co]) -> Enumerable[TSource_co]:
259278
def inner():
260279
yield from self

types_linq/enumerable.pyi

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Callable, Dict, Generic, Iterable, Iterator, List, NoReturn, Optional, Sequence, Set, Tuple, Type, Union, overload
1+
from typing import Any, Callable, Dict, Generic, Iterable, Iterator, List, MutableSequence, NoReturn, Optional, Sequence, Set, Tuple, Type, Union, overload
22

33
from .lookup import Lookup
44
from .grouping import Grouping
@@ -389,6 +389,28 @@ class Enumerable(Sequence[TSource_co], Generic[TSource_co]):
389389
same_query: Enumerable[int] = query.cast(int)
390390
'''
391391

392+
# nonstanard: .NET returns fix-size arrays as elements instead
393+
def chunk(self, size: int) -> Enumerable[MutableSequence[TSource_co]]:
394+
'''
395+
Splits the elements of a sequence into chunks of size at most the provided size. Raises
396+
`InvalidOperationError` if `size` is less than 1.
397+
398+
Example
399+
.. code-block:: python
400+
401+
>>> def source(i):
402+
... while True:
403+
... yield i
404+
... i *= 3
405+
406+
>>> en = Enumerable(source(1)).chunk(4).take(3)
407+
>>> for chunk in en:
408+
... print(chunk)
409+
[1, 3, 9, 27]
410+
[81, 243, 729, 2187]
411+
[6561, 19683, 59049, 177147]
412+
'''
413+
392414
def concat(self, second: Iterable[TSource_co]) -> Enumerable[TSource_co]:
393415
'''
394416
Concatenates two sequences.

0 commit comments

Comments
 (0)