From 56112b1b75b5363898c135e2f80ba5bb44a8b215 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Mon, 2 Jan 2023 01:10:25 +0100
Subject: [PATCH 01/46] Make breaking changes for v0.0.5
> Note that this update introduces breaking changes.
Implement a `Functions` helper class for common procedures such as
- `invert`
- `is_even`
- `is_odd`
- `sign`
The overload in the `Iter` constructor was removed in favor of a static `Iter.range`
method to enforce separation of concerns more strictly. Additional minor changes
were made to pretty much all doc strings to reflect the recent changes that are
to be consolidated for more information. In brief,
- the `chunk_by` method now also provides `eject` as an optional argument
- the `concat` method was renamed to `union`
- `iterable` and `iter` keyword arguments were renamed to `iter_`
- the method signature of the `range` method has changed and added support for
decrements
The following methods are new:
- `duplicates`
- `flatten`
- `linspace`
- `transpose`
---
src/iterfun/iterfun.py | 646 +++++++++++++++++++++++------------------
1 file changed, 367 insertions(+), 279 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 029246f..d1935bb 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -9,50 +9,56 @@
import statistics
import textwrap
from collections import ChainMap, Counter
-from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union, overload
+from decimal import ROUND_HALF_UP, Decimal
+from typing import Any, Callable, Dict, Final, Generator, Iterable, List, Literal, Optional, Tuple, Union, overload
+
+
+class Functions:
+ def invert(x: Union[int, float]) -> Union[int, float]:
+ return -1 * x
+
+ def is_even(x: Union[int, float]) -> bool:
+ return x % 2 == 0
+
+ def is_odd(x: Union[int, float]) -> bool:
+ return x % 2 != 0
+
+ def sign(x: Union[int, float]) -> Literal[-1, 0, 1]:
+ if x < 0:
+ return -1
+ elif x == 0:
+ return 0
+ else:
+ return 1
class Iter:
"""
## Iter
- This class implements an eager set of common algorithms for list and dictionary
- transformations. `self.image` is a public field that we use to temporarily store
- and pass data between method invocations. In addition to that, `self.domain` registers
- the original collection that was available during the initial object instantiation
- and should be regarded as a public constant property.
-
- In Python, an `Iterable` is any data type that implements the `__iter__` method
- (which returns an iterator as the name suggests) or a `__getitem__` method
- suitable for indexed lookup such as `list`, `tuple`, `dict`, or `set`.
+ Implements an eager set of common algorithms for list and dictionary transformations.
+ The image is a public field that is used by this class to temporarily store
+ and pass data between method invocations, while the domain registers the input
+ that was available during the initial object instantiation.
- ## Examples
+ ### Example
```python
- >>> Iter([1, 3]).map(lambda x: 2*x).to_list()
- [2, 4, 6]
- >>> Iter([1, 2, 3]).sum()
- 6
- >>> Iter({'a': 1, 'b': 2}).map(lambda k, v: {k: 2 * v}).to_dict()
- {'b': 4, 'a': 2}
+ >>> Iter.range(1, 3).map(lambda x: 2*x).sum()
+ 12
+ ```
"""
- @overload
- def __init__(self, iter: Iterable, interval: bool=False) -> Iter: ...
-
- @overload
- def __init__(self, iter: List[int, int], interval: bool=True) -> Iter: ...
-
- @overload
- def __init__(self, iter: Tuple[int, int], interval: bool=True) -> Iter: ...
-
- def __init__(self, iter: Iterable | List[int, int] | Tuple[int, int], interval: bool=True) -> Iter:
- self.image = Iter.range(iter) if interval and len(iter) == 2 and all(map(lambda x: isinstance(x, int), iter)) else iter
- self.domain = self.image
+ def __init__(self, iter_: Iterable) -> Iter:
+ """
+ Initialize a new object of this class.
+ """
+ self.domain: Final[Iterable] = iter_
+ self.image: Iterable = iter_
- def all(self, fun: Optional[Callable[[Any], bool]]=None) -> bool:
+ def all(self, fun: Optional[Callable[[Any], bool]] = None) -> bool:
"""
- Return `True` if all elements in `self.image` are truthy, or `True` if
- `fun` is not None and its map truthy for all elements in `self.image`.
+ Return `True` if all elements in the image are truthy, or `True` if
+ `fun` is not None and its map truthy for all elements in the image.
```python
>>> Iter([1, 2, 3]).all()
@@ -67,10 +73,10 @@ def all(self, fun: Optional[Callable[[Any], bool]]=None) -> bool:
"""
return all(self.image) if fun is None else all(map(fun, self.image))
- def any(self, fun: Optional[Callable[[Any], bool]]=None) -> bool:
+ def any(self, fun: Optional[Callable[[Any], bool]] = None) -> bool:
"""
- Return `True` if any elements in `self.image` are truthy, or `True` if
- `fun` is not None and its map truthy for at least on element in `self.image`.
+ Return `True` if any elements in the image are truthy, or `True` if
+ `fun` is not None and its map truthy for at least on element in the image.
```python
>>> Iter([False, False, False]).any()
@@ -85,9 +91,9 @@ def any(self, fun: Optional[Callable[[Any], bool]]=None) -> bool:
def at(self, index: int) -> Any:
"""
- Find the element at the given `index` (zero-based). Raise an `IndexError`
+ Find the element at the given zero-based `index`. Raises an `IndexError`
if `index` is out of bonds. A negative index can be passed, which means
- `self.image` is enumerated once and the index is counted from the end.
+ the image is enumerated once from right to left.
```python
>>> Iter([2, 4, 6]).at(0)
@@ -102,7 +108,7 @@ def at(self, index: int) -> Any:
def avg(self) -> Union[int, float]:
"""
- Return the sample arithmetic mean of `self.image`.
+ Return the sample arithmetic mean of the image.
```python
>>> Iter([0, 10]).avg()
@@ -111,34 +117,39 @@ def avg(self) -> Union[int, float]:
"""
return statistics.mean(self.image)
- def chunk_by(self, fun: Callable[[Any], bool]) -> Iter:
+ def chunk_by(self, fun: Callable[[Any], bool], eject: bool = False) -> Iter:
"""
- Split `self.image` on every element for which `fun` returns a new value.
+ Split the domain on every element for which `fun` returns a new value.
+ Remove any group for which `fun` initially returned `True` if `eject` is
+ enabled.
```python
>>> Iter([1, 2, 2, 3, 4, 4, 6, 7, 7, 7]).chunk_by(lambda x: x % 2 == 1)
[[1], [2, 2], [3], [4, 4, 6], [7, 7, 7]]
+ >>> sequence = ['a', 'b', '1', 'c', 'd', '2', 'e', 'f']
+ >>> Iter(sequence).chunk_by(lambda x: x.isdigit(), eject=True)
+ [['a', 'b'], ['c', 'd'], ['e', 'f']]
```
"""
- self.image = [list(group) for _, group in itertools.groupby(self.image, fun)]
+ tmp = [list(group) for _, group in itertools.groupby(self.image, fun)]
+ self.image = [x for x in tmp if len(x) > 1 and not fun(x[0])] if eject else tmp
return self
- def chunk_every(self, count: int, step: Optional[int]=None, leftover: Optional[List[Any]]=None) -> Iter:
+ def chunk_every(self, count: int, step: Optional[int] = None, leftover: Optional[List[Any]] = None) -> Iter:
"""
Return list of lists containing `count` elements each. `step` is optional
and, if not passed, defaults to `count`, i.e. chunks do not overlap.
```python
- >>> Iter([1, 6]).chunk_every(2)
+ >>> Iter.range(1,6).chunk_every(2)
[[1, 2], [3, 4], [5, 6]]
- >>> Iter([1, 6]).chunk_every(3, 2, [7])
+ >>> Iter.range(1,6).chunk_every(3, 2, [7])
[[1, 2, 3], [3, 4, 5], [5, 6, 7]]
- >>> Iter([1, 4]).chunk_every(3, 3)
+ >>> Iter.range(1, 4).chunk_every(3, 3)
[[1, 2, 3], [4]]
```
"""
- step = step or count
- self.image = [list(self.image)[i:i+count] for i in range(0, len(self.image), step)]
+ self.image = [list(self.image)[i:i+count] for i in range(0, len(self.image), step or count)]
if leftover: self.image[-1].extend(leftover[:len(self.image[-1])])
return self
@@ -147,54 +158,29 @@ def chunk_while(self, acc: List, chunk_fun: Callable, chunk_after: Callable) ->
# https://hexdocs.pm/elixir/1.12/Enum.html#chunk_while/4
raise NotImplementedError()
- def concat(self) -> Iter:
- """
- Given a list of lists, concatenates the list into a single list.
-
- ```python
- >>> Iter([[1, 2, 3], [4, 5, 6]]).concat()
- [1, 2, 4, 5, 6]
- >>> Iter([[1, [2], 3], [4], [5, 6]]).concat()
- [1, [2], 3, 4, 5, 6]
- ```
-
- Concat range-triggering lists or tuples:
-
- ```python
- >>> Iter([[1, 4], [5, 6], [7, 9]]).concat()
- [1, 2, 3, 4, 5, 6, 7, 8, 9]
- >>> Iter([(0, 4), (5, 6), (7, 9)]).concat()
- [1, 2, 3, 8]
- >>> Iter([(0, 4), (3, 7), (6, 10)]).concat()
- [1, 2, 3, 4, 5, 6, 7, 8, 9]
- ```
- """
- self.image = list(itertools.chain(*map(Iter.range, self.image)) if all(map(lambda x: len(x) == 2, self.image)) else itertools.chain(*self.image))
- return self
-
- def count(self, fun: Optional[Callable[[Any], bool]]=None) -> int:
+ def count(self, fun: Optional[Callable[[Any], bool]] = None) -> int:
"""
- Return the size of the `self.image` if `fun` is `None`, else return the
- count of elements in `self.image` for which `fun` returns a truthy value.
+ Return the size of the image if `fun` is `None`, else return the cardinality
+ of the image for which `fun` returns a truthy value.
```python
- >>> Iter([1, 3]).count()
+ >>> Iter([1, 2, 3]).count()
3
- >>> Iter([1, 5]).count(lambda x: x % 2 == 0)
+ >>> Iter.range(1, 5).count(lambda x: x % 2 == 0)
2
```
"""
return len(list(self.image)) if fun is None else len(list(filter(fun, self.image)))
- def count_until(self, limit: int, fun: Optional[Callable[[Any], bool]]=None) -> int:
+ def count_until(self, limit: int, fun: Optional[Callable[[Any], bool]] = None) -> int:
"""
- Count the elements in `self.image` for which `fun` returns a truthy value,
+ Determine the cardinality of the image for which `fun` returns a truthy value,
stopping at `limit`.
```python
- >>> Iter([1, 20]).count_until(5)
+ >>> Iter.range(1, 20).count_until(5)
5
- >>> Iter([1, 20]).count_until(50)
+ >>> Iter.range(1, 20).count_until(50)
20
```
"""
@@ -202,7 +188,7 @@ def count_until(self, limit: int, fun: Optional[Callable[[Any], bool]]=None) ->
def dedup(self) -> Iter:
"""
- Enumerate `self.image`, returning a list where all consecutive duplicated
+ Enumerate the image, returning a list where all consecutive duplicated
elements are collapsed to a single element.
```python
@@ -215,7 +201,7 @@ def dedup(self) -> Iter:
def dedup_by(self, fun: Callable[[Any], bool]):
"""
- Enumerates `self.image`, returning a list where all consecutive duplicated
+ Enumerates the image, returning a list where all consecutive duplicated
elements are collapsed to a single element.
```python
@@ -232,8 +218,8 @@ def dedup_by(self, fun: Callable[[Any], bool]):
def drop(self, amount: int) -> Iter:
"""
- Drop the `amount` of elements from `self.image`. If a negative `amount` is
- given, the `amount` of last values will be dropped. `self.image` will be
+ Drop the `amount` of elements from the image. If a negative `amount` is
+ given, the `amount` of last values will be dropped. The image will be
enumerated once to retrieve the proper index and the remaining calculation
is performed from the end.
@@ -246,23 +232,23 @@ def drop(self, amount: int) -> Iter:
[1, 2]
```
"""
- self.image = list(self.image)
- self.image = self.image[amount:] if amount > 0 else self.image[:len(self.image)+amount]
+ tmp = list(self.image)
+ self.image = tmp[amount:] if amount > 0 else tmp[:len(tmp)+amount]
return self
def drop_every(self, nth: int) -> Iter:
"""
- Return a list of every `nth` element in the `self.image` dropped, starting
- with the first element. The first element is always dropped, unless `nth`
- is `0`. The second argument specifying every nth element must be a non-negative
+ Return a list of every `nth` element in the image dropped, starting with
+ the first element. The first element is always dropped, unless `nth` is `0`.
+ The second argument specifying every nth element must be a non-negative
integer.
```python
- >>> Iter([1, 10]).drop_every(2)
+ >>> Iter.range(1, 10).drop_every(2)
[2, 4, 6, 8, 10]
- >>> Iter([1, 10]).drop_every(0)
+ >>> Iter.range(1, 10).drop_every(0)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
- >>> Iter([1, 3]).drop_every(1)
+ >>> Iter.range(1, 3).drop_every(1)
[]
```
"""
@@ -271,8 +257,7 @@ def drop_every(self, nth: int) -> Iter:
def drop_while(self, fun: Callable[[Any], bool]) -> Iter:
"""
- Drop elements at the beginning of `self.image` while `fun` returns a
- truthy value.
+ Drop elements at the beginning of the image while `fun` returns a truthy value.
```python
>>> Iter([1, 2, 3, 2, 1]).drop_while(lambda x: x < 3)
@@ -282,16 +267,28 @@ def drop_while(self, fun: Callable[[Any], bool]) -> Iter:
self.image = list(itertools.dropwhile(fun, self.image))
return self
+ def duplicates(self) -> Iter:
+ """
+ Return all duplicated occurrences from the image.
+
+ ```python
+ >>> Iter([1, 1, 1, 2, 2, 3, 4, 4]).duplicates()
+ [1, 2, 4]
+ ```
+ """
+ self.image = [item for item, count in Counter(self.image).items() if count > 1]
+ return self
+
def empty(self) -> bool:
"""
- Return `True` if `self.image` is empty, otherwise `False`.
+ Return `True` if the image is empty, otherwise `False`.
```python
>>> Iter([]).empty()
True
- >>> Iter([0, 0]).empty()
- True
- >>> Iter([1, 10]).empty()
+ >>> Iter([0]).empty()
+ False
+ >>> Iter.range(1, 10).empty()
False
```
"""
@@ -299,24 +296,24 @@ def empty(self) -> bool:
def filter(self, fun: Callable[[Any], bool]) -> Iter:
"""
- Filter `self.image`, i.e. return only those elements for which `fun` returns
+ Filter the image, i.e. return only those elements for which `fun` returns
a truthy value.
```python
- >>> Iter([1, 3]).filter(lambda x: x % 2 == 0)
+ >>> Iter.range(1, 3).filter(lambda x: x % 2 == 0)
[2]
```
"""
self.image = list(filter(fun, self.image))
return self
- def find(self, fun: Callable[[Any], bool], default: Optional[Any]=None) -> Optional[Any]:
+ def find(self, fun: Callable[[Any], bool], default: Optional[Any] = None) -> Optional[Any]:
"""
Return the first element for which `fun` returns a truthy value. If no
such element is found, return `default`.
```python
- >>> Iter([2, 4]).find(lambda x: x % 2 == 1)
+ >>> Iter.range(2, 4).find(lambda x: x % 2 == 1)
3
>>> Iter([2, 4, 6]).find(lambda x: x % 2 == 1)
None
@@ -326,9 +323,9 @@ def find(self, fun: Callable[[Any], bool], default: Optional[Any]=None) -> Optio
"""
return next(filter(fun, self.image), default)
- def find_index(self, fun: Callable[[Any], bool], default: Optional[Any]=None) -> Optional[Any]:
+ def find_index(self, fun: Callable[[Any], bool], default: Optional[Any] = None) -> Optional[Any]:
"""
- Similar to `self.find`, but return the index (zero-based) of the element
+ Similar to `self.find`, but return the zero-based index of the element
instead of the element itself.
```python
@@ -341,7 +338,7 @@ def find_index(self, fun: Callable[[Any], bool], default: Optional[Any]=None) ->
found = next(filter(fun, self.image), default)
return self.image.index(found) if found in self.image else default
- def find_value(self, fun: Callable[[Any], bool], default: Optional[Any]=None) -> Optional[Any]:
+ def find_value(self, fun: Callable[[Any], bool], default: Optional[Any] = None) -> Optional[Any]:
"""
Similar to `self.find`, but return the value of the function invocation instead
of the element itself.
@@ -351,7 +348,7 @@ def find_value(self, fun: Callable[[Any], bool], default: Optional[Any]=None) ->
None
>>> Iter([2, 3, 4]).find_value(lambda x: x % 2 == 1)
True
- >>> Iter([1, 3]).find_value(lambda x: isinstance(x, bool), "no bools!")
+ >>> Iter.range(1, 3).find_value(lambda x: isinstance(x, bool), "no bools!")
'no bools!'
```
"""
@@ -360,10 +357,10 @@ def find_value(self, fun: Callable[[Any], bool], default: Optional[Any]=None) ->
def flat_map(self, fun: Callable[[Any], Any]) -> Iter:
"""
- Map the given `fun` over `self.image` and flattens the result.
+ Map the given `fun` over the image and flattens the result.
```python
- >>> Iter([(1, 3), (4, 6)]).flat_map(lambda x: list(range(x[0], x[1] + 1)))
+ >>> Iter([(1, 3), (4, 6)]).flat_map(lambda x: list(range(x[0], x[1]+1))).
[1, 2, 3, 4, 5, 6]
>>> Iter([1, 2, 3]).flat_map(lambda x: [[x]])
[[1], [2], [3]]
@@ -377,9 +374,21 @@ def flat_map_reduce(self, acc: int, fun: Callable[[Any, Any], Any]) -> Iter:
# https://hexdocs.pm/elixir/1.12/Enum.html#flat_map_reduce/3
raise NotImplementedError()
+ def flatten(self) -> Iter:
+ """
+ Flatten the current image.
+
+ ```python
+ >>> Iter([[1, 2], [3, 4], [5], [6, None]]).flatten()
+ [1, 2, 3, 4, 5, 6, None]
+ ```
+ """
+ self.image = list(itertools.chain(*self.image))
+ return self
+
def frequencies(self) -> Iter:
"""
- Return a map with keys as unique elements of `self.image` and values as
+ Return a map with keys as unique elements of the image and values as
the count of every element.
```python
@@ -405,13 +414,13 @@ def frequencies_by(self, key_fun: Callable[[Any], Any]) -> Iter:
self.image = Counter(map(key_fun, self.image))
return self
- def group_by(self, key_fun: Callable[[Any], Any], value_fun: Optional[Callable[[Any], Any]]=None) -> Iter:
+ def group_by(self, key_fun: Callable[[Any], Any], value_fun: Optional[Callable[[Any], Any]] = None) -> Iter:
"""
- Split `self.image` into groups based on `key_fun`.
+ Split the image into groups based on `key_fun`.
The result is a map where each key is given by `key_fun` and each value
is a list of elements given by `value_fun`. The order of elements within
- each list is preserved from `self.image`. However, like all maps, the
+ each list is preserved from the image. However, like all maps, the
resulting map is unordered.
```python
@@ -421,13 +430,13 @@ def group_by(self, key_fun: Callable[[Any], Any], value_fun: Optional[Callable[[
{3: ["a", "c"], 5: ["d"], 7: ["b"]}
```
"""
- value = lambda g: list(g) if value_fun is None else list(map(value_fun, g))
+ def value(g): return list(g) if value_fun is None else list(map(value_fun, g))
self.image = {k: value(g) for k, g in itertools.groupby(sorted(self.image, key=key_fun), key_fun)}
return self
def intersperse(self, separator: Any) -> Iter:
"""
- Intersperses separator between each element of `self.image`.
+ Intersperses separator between each element of the image.
```python
>>> Iter([1, 3]).intersperse(0)
@@ -441,9 +450,9 @@ def intersperse(self, separator: Any) -> Iter:
self.image = list(itertools.islice(itertools.chain.from_iterable(zip(itertools.repeat(separator), self.image)), 1, None))
return self
- def into(self, iter: Iterable) -> Iter:
+ def into(self, iter_: Iterable) -> Iter:
"""
- Insert the given `self.image` into `iter`.
+ Insert the given image into `iter_`.
```python
>>> Iter([1, 2]).into([])
@@ -454,14 +463,14 @@ def into(self, iter: Iterable) -> Iter:
{'a': 1, 'b': 2}
```
"""
- self.image = {**self.image, **iter} if isinstance(iter, Dict) else [*self.image, *iter]
+ self.image = {**self.image, **iter_} if isinstance(iter_, Dict) else [*self.image, *iter_]
return self
- def join(self, joiner: Optional[str]=None) -> str:
+ def join(self, joiner: Optional[str] = None) -> str:
"""
- Join `self.image` into a string using `joiner` as a separator. If `joiner`
+ Join the image into a string using `joiner` as a separator. If `joiner`
is not passed at all, it defaults to an empty string. All elements in
- `self.image` must be convertible to a string, otherwise an error is raised.
+ the image must be convertible to a string, otherwise an error is raised.
```python
>>> Iter([1,5]).join()
@@ -472,24 +481,49 @@ def join(self, joiner: Optional[str]=None) -> str:
"""
return f"{joiner or ''}".join(map(str, self.image))
+ @staticmethod
+ def __round(dec: Decimal, prec: int, rounding: str = ROUND_HALF_UP) -> Decimal:
+ return dec.quantize(Decimal(10)**-prec, rounding)
+
+ @staticmethod
+ def linspace(a: Union[int, float], b: Union[int, float], step: int = 50, prec: int = 28) -> Iter:
+ """
+ Return evenly spaced numbers over a specified closed interval `[a, b]`.
+ Set delta precision by rounding to `prec` decimal places.
+
+ ```python
+ >>> Iter.linspace(1.1, 3.3, step=10, prec=2)
+ [1.1, 1.32, 1.54, 1.76, 1.98, 2.2, 2.42, 2.64, 2.86, 3.08, 3.3]
+ ```
+ """
+ delta = Iter.__round(Decimal(str(abs(a - b) / step)), prec)
+ return Iter([float(Decimal(str(a)) + i * delta) for i in range(step+1)])
+
@overload
def map(self, fun: Callable[[Any], Any]) -> Iter:
"""
Return a list where each element is the result of invoking `fun` on each
- corresponding element of `self.image`. For dictionaries, the function expects
- a key-value pair as arguments.
+ corresponding element of the image.
```python
- >>> Iter([1,3]).map(lambda x: 2 * x)
+ >>> Iter.range(1,3).map(lambda x: 2 * x)
[2, 4, 6]
- >>> Iter({'a': 1, 'b': 2}).map(lambda k, v: {k: -v})
- {'a': -1, 'b': -2}
```
"""
...
@overload
- def map(self, fun: Callable[[Any, Any], Dict]) -> Iter: ...
+ def map(self, fun: Callable[[Any, Any], Dict]) -> Iter:
+ """
+ Return a dictionary where each element is the result of invoking `fun` on each
+ corresponding key-value pair of the image.
+
+ ```python
+ >>> Iter({'a': 1, 'b': 2}).map(lambda k, v: {k: -v})
+ {'a': -1, 'b': -2}
+ ```
+ """
+ ...
def map(self, fun: Callable[[Any], Any]) -> Iter:
self.image = dict(ChainMap(*itertools.starmap(fun, self.image.items()))) if isinstance(self.image, Dict) else list(map(fun, self.image))
@@ -497,16 +531,16 @@ def map(self, fun: Callable[[Any], Any]) -> Iter:
def map_every(self, nth: int, fun: Callable[[Any], Any]) -> Iter:
"""
- Return a list of results of invoking `fun` on every `nth` element of `self.image`,
+ Return a list of results of invoking `fun` on every `nth` element of the image,
starting with the first element. The first element is always passed to the given
function, unless `nth` is `0`.
```python
- >>> Iter([1, 10]).map_every(2, lambda x: x + 1000)
+ >>> Iter.range(1, 10).map_every(2, lambda x: x + 1000)
[1001, 2, 1003, 4, 1005, 6, 1007, 8, 1009, 10]
- >>> Iter([1, 5]).map_every(0, lambda x: x + 1000)
+ >>> Iter.range(1, 5).map_every(0, lambda x: x + 1000)
[1, 2, 3, 4, 5]
- >>> Iter([1, 3]).map_every(1, lambda x: x + 1000)
+ >>> Iter.range(1, 3).map_every(1, lambda x: x + 1000)
[1001, 1002, 1003]
```
"""
@@ -517,26 +551,26 @@ def map_every(self, nth: int, fun: Callable[[Any], Any]) -> Iter:
def map_intersperse(self, separator: Any, fun: Callable[[Any], Any]) -> Iter:
"""
- Map and intersperses `self.image` in one pass.
+ Map and intersperses the image in one pass.
```python
- >>> Iter([1, 3]).map_intersperse(None, lambda x: 2 * x)
+ >>> Iter.range(1, 3).map_intersperse(None, lambda x: 2 * x)
[2, None, 4, None, 6]
```
"""
- self.image = list(itertools.islice(itertools.chain.from_iterable(zip(itertools.repeat(separator), map(fun, self.image))), 1, None))
+ self.image = list(itertools.islice(itertools.chain.from_iterable(zip(itertools.repeat(separator), map(fun, self.image))), 1, None))
return self
- def map_join(self, fun: Callable[[Any], Any], joiner: Optional[str]=None) -> str:
+ def map_join(self, fun: Callable[[Any], Any], joiner: Optional[str] = None) -> str:
"""
- Map and join `self.image` in one pass. If joiner is not passed at all, it
+ Map and join the image in one pass. If joiner is not passed at all, it
defaults to an empty string. All elements returned from invoking `fun` must
be convertible to a string, otherwise an error is raised.
```python
- >>> Iter([1, 3]).map_join(lambda x: 2 * x)
+ >>> Iter.range(1, 3).map_join(lambda x: 2 * x)
'246'
- >>> Iter([1, 3]).map_join(lambda x: 2 * x, " = ")
+ >>> Iter.range(1, 3).map_join(lambda x: 2 * x, " = ")
'2 = 4 = 6'
```
"""
@@ -544,27 +578,26 @@ def map_join(self, fun: Callable[[Any], Any], joiner: Optional[str]=None) -> str
def map_reduce(self, acc: Union[int, float, complex], fun: Callable[[Any], Any], acc_fun: Optional[Callable[[Any, Any], Any]]) -> Iter:
"""
- Invoke the given function to each element in `self.image` to reduce it to
+ Invoke the given function to each element in the image to reduce it to
a single element, while keeping an accumulator. Return a tuple where the
- first element is the mapped `self.image` and the second one is the final
- accumulator.
+ first element is the mapped image and the second one is the final accumulator.
```python
- >>> Iter([1, 3]).map_reduce(0, lambda x: 2 * x, lambda x, acc: x + acc)
+ >>> Iter.range(1, 3).map_reduce(0, lambda x: 2 * x, lambda x, acc: x + acc)
([2, 4, 6], 6)
- >>> Iter([1, 3]).map_reduce(6, lambda x: x * x, operator.sub)
+ >>> Iter.range(1, 3).map_reduce(6, lambda x: x * x, operator.sub)
([1, 4, 9], 0)
```
"""
self.image = (list(map(fun, self.image)), functools.reduce(acc_fun, self.image, acc))
return self
- def max(self, fun: Optional[Callable[[Any], Any]]=None, empty_fallback: Optional[Any]=None) -> Any:
+ def max(self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Optional[Any] = None) -> Any:
"""
- Return the maximal element in `self.image` as calculated by the given `fun`.
+ Return the maximum of the image as determined by the function `fun`.
```python
- >>> Iter([1, 3]).max()
+ >>> Iter.range(1, 3).max()
3
>>> Iter("you shall not pass".split()).max()
'you'
@@ -578,12 +611,12 @@ def max(self, fun: Optional[Callable[[Any], Any]]=None, empty_fallback: Optional
def member(self, element: Any) -> bool:
"""
- Checks if element exists within `self.image`.
+ Checks if element exists within the image.
```python
- >>> Iter([1, 10]).member(5)
+ >>> Iter.range(1, 10).member(5)
True
- >>> Iter([1, 10]).member(5.0)
+ >>> Iter.range(1, 10).member(5.0)
False
>>> Iter([1.0, 2.0, 3.0]).member(2)
True
@@ -595,16 +628,16 @@ def member(self, element: Any) -> bool:
"""
return element in self.image
- def min(self, fun: Optional[Callable[[Any], Any]]=None, empty_fallback: Optional[Any]=None) -> Any:
+ def min(self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Optional[Any] = None) -> Any:
"""
- Return the minimum element in `self.image` as calculated by the given `fun`.
+ Return the minimum of the image as determined by the function `fun`.
```python
- >>> Iter([1, 3]).max()
+ >>> Iter.range(1, 3).min()
1
- >>> Iter("you shall not pass".split()).max()
+ >>> Iter("you shall not pass".split()).min()
'you'
- >>> Iter("you shall not pass".split()).max(len)
+ >>> Iter("you shall not pass".split()).min(len)
'not'
>>> Iter([]).max(empty_fallback='n/a')
'n/a'
@@ -612,9 +645,9 @@ def min(self, fun: Optional[Callable[[Any], Any]]=None, empty_fallback: Optional
"""
return (min(self.image, key=fun) if fun is not None else min(self.image)) if self.image else empty_fallback
- def min_max(self, fun: Optional[Callable[[Any], Any]]=None, empty_fallback: Optional[Any]=None) -> Tuple:
+ def min_max(self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Optional[Any] = None) -> Tuple[Any, Any]:
"""
- Return a tuple with the minimal and the maximal elements in `self.image`.
+ Return a tuple with the minimal and the maximal elements in the image.
```python
>>> Iter([1, 3]).min_max()
@@ -642,12 +675,12 @@ def product(self) -> Union[float, int, complex]:
def random(self) -> Any:
"""
- Return a random element from `self.image`.
+ Return a random element from the image.
```python
- >>> Iter([1, 100]).random()
+ >>> Iter.range(1, 100).random()
42
- >>> Iter([1, 100]).random()
+ >>> Iter.range(1, 100).random()
69
```
"""
@@ -655,50 +688,73 @@ def random(self) -> Any:
@overload
@staticmethod
- def range(bounds: List[int]) -> List[int]:
+ def range(a: int, b: int, step: Optional[int] = 1) -> Iter:
"""
- Return a sequence of integers from start to end.
+ Return a sequence of integers from `a` (inclusive) to `b` (inclusive) by
+ `step`. When `step` is given, it specifies the increment (or decrement).
```python
- >>> Iter.range([1, 5])
+ >>> Iter.range(1, 5)
[1, 2, 3, 4, 5]
- >>> Iter.range((1, 5))
- [2, 3, 4]
+ >>> Iter.range(1, 5, 2)
+ [1, 3, 5]
```
"""
...
@overload
@staticmethod
- def range(bounds: Tuple[int]) -> List[int]: ...
+ def range(a: float, b: float, step: Optional[float] = 0.1) -> Iter:
+ """
+ Return a sequence of floats from `a` (inclusive) to `b` (inclusive) by
+ `step`. When `step` is given, it specifies the increment (or decrement).
+
+ ```python
+ >>> Iter.range(0.1, 1.0)
+ [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
+ >>> Iter.range(0.5, 5.0, 0.5)
+ [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]
+ >>> Iter.range(5.0, 0.5, -0.5)
+ [5.0, 4.5, 4.0, 3.5, 3.0, 2.5, 2.0, 1.5, 1.0, 0.5]
+ ```
+ """
+ ...
+
+ @staticmethod
+ def __range(a: float, b: float, step: float) -> Generator[float]:
+ a2, n2 = Decimal(str(a)), Decimal(str(step))
+ for i in range(int((b - a) / step) + 1):
+ yield float(a2 + (i * n2))
@staticmethod
- def range(bounds: List[int] | Tuple[int]) -> List[int]:
- return list(range(bounds[0], bounds[1]+int(bounds[1]!=0)) if isinstance(bounds, List) and len(bounds) == 2 else range(bounds[0]+1, bounds[1]))
+ def range(a: Union[int, float], b: Union[int, float], step: Optional[int | float] = None) -> Iter:
+ if step == 0: raise ValueError("step must not be zero")
+ tmp = step or 1
+ return Iter(list(range(a, b + Functions.sign(tmp), tmp) if isinstance(a, int) else Iter.__range(a, b, step or 0.1)))
- def reduce(self, fun: Callable[[Any, Any], Any], acc: int=0) -> Any:
+ def reduce(self, fun: Callable[[Any, Any], Any], acc: int = 0) -> Any:
"""
- Invoke `fun` for each element in `self.image` with the accumulator. The
+ Invoke `fun` for each element in the image with the accumulator. The
accumulator defaults to `0` if not otherwise specified. Reduce (sometimes
also called fold) is a basic building block in functional programming.
```python
- >>> Iter([1, 4]).reduce(operator.add)
+ >>> Iter.range(1, 4).reduce(operator.add)
10
- >>> Iter([1, 4]).reduce(lambda x, acc: x * acc, acc=1)
+ >>> Iter.range(1, 4).reduce(lambda x, acc: x * acc, acc=1)
24
```
"""
return functools.reduce(fun, self.image, acc)
- def reduce_while(self, fun: Callable[[Any, Any], Tuple[bool, Any]], acc: int=0) -> Any:
+ def reduce_while(self, fun: Callable[[Any, Any], Tuple[bool, Any]], acc: int = 0) -> Any:
"""
- Reduce `self.image` until `fun` returns `(False, acc)`.
+ Reduce the image until `fun` returns `(False, acc)`.
```python
- >>> Iter([1, 100]).reduce_while(lambda x, acc: (True, acc + x) if x < 5 else (False, acc))
+ >>> Iter.range(1, 100).reduce_while(lambda x, acc: (True, acc + x) if x < 5 else (False, acc))
10
- >>> Iter([1, 100]).reduce_while(lambda x, acc: (True, acc - x) if x % 2 == 0 else (False, acc), acc=2550)
+ >>> Iter.range(1, 100).reduce_while(lambda x, acc: (True, acc - x) if x % 2 == 0 else (False, acc), acc=2550)
0
```
"""
@@ -706,11 +762,11 @@ def reduce_while(self, fun: Callable[[Any, Any], Tuple[bool, Any]], acc: int=0)
def reject(self, fun: Callable[[Any], bool]) -> Iter:
"""
- Return a list of elements in `self.image` excluding those for which the
+ Return a list of elements in the image excluding those for which the
function `fun` returns a truthy value.
```python
- >>> Iter([1, 3]).reject(lambda x: x % 2 == 0)
+ >>> Iter.range(1, 3).reject(lambda x: x % 2 == 0)
[1, 3]
```
"""
@@ -720,60 +776,62 @@ def reject(self, fun: Callable[[Any], bool]) -> Iter:
@overload
def reverse(self) -> Iter:
"""
- Return a list of elements in `self.image` in reverse order.
+ Return a list of elements in the image in reverse order.
```python
- >>> Iter([1, 5]).reverse()
+ >>> Iter.range(1, 5).reverse()
[5, 4, 3, 2, 1]
```
+ """
+ ...
- Reverse the elements in `self.image`, appends the `tail`, and returns it
+ @overload
+ def reverse(self, tail: Optional[List] = None) -> Iter:
+ """
+ Reverse the elements in the image, appends the `tail`, and returns it
as a list.
```python
- >>> Iter([1, 3]).reverse([4, 5, 6])
+ >>> Iter.range(1, 3).reverse([4, 5, 6])
[3, 2, 1, 4, 5, 6]
```
"""
...
- @overload
- def reverse(self, tail: Optional[List]=None) -> Iter: ...
-
- def reverse(self, tail: Optional[List]=None) -> Iter:
+ def reverse(self, tail: Optional[List] = None) -> Iter:
self.image = list(reversed(self.image))
if tail: self.image.extend(tail)
return self
def reverse_slice(self, start_index: int, count: int) -> Iter:
"""
- Reverse `self.image` in the range from initial `start_index` through `count`
- elements. If `count` is greater than the size of the rest of `self.image`,
- then this function will reverse the rest of `self.image`.
+ Reverse the image in the range from initial `start_index` through `count`
+ elements. If `count` is greater than the size of the rest of the image,
+ then this function will reverse the rest of the image.
```python
- >>> Iter([1, 6]).reverse_slice(2, 4)
+ >>> Iter.range(1, 6).reverse_slice(2, 4)
[1, 2, 6, 5, 4, 3]
- >>> Iter([1, 10]).reverse_slice(2, 4)
+ >>> Iter.range(1, 10).reverse_slice(2, 4)
[1, 2, 6, 5, 4, 3, 7, 8, 9, 10]
- >>> Iter([1, 10]).reverse_slice(2, 30)
+ >>> Iter.range(1, 10).reverse_slice(2, 30)
[1, 2, 10, 9, 8, 7, 6, 5, 4, 3]
```
"""
self.image = [*self.image[:start_index], *reversed(self.image[start_index:count+start_index]), *self.image[count+start_index:]]
return self
- def scan(self, fun: Callable[[Any, Any], Any], acc: Optional[int]=None) -> Iter:
+ def scan(self, fun: Callable[[Any, Any], Any], acc: Optional[int] = None) -> Iter:
"""
- Apply the given function to each element in `self.image`, storing the result
+ Apply the given function `fun` to each element in the image, storing the result
in a list and passing it as the accumulator for the next computation. Uses
- the first element in `self.image` as the starting value if `acc` is `None`,
+ the first element in the image as the starting value if `acc` is `None`,
else uses the given `acc` as the starting value.
```python
- >>> Iter([1, 5]).scan(operator.add)
+ >>> Iter.range(1, 5).scan(operator.add)
[1, 3, 6, 10, 15]
- >>> Iter([1, 5]).scan(lambda x, y: x + y, acc=0)
+ >>> Iter.range(1, 5).scan(lambda x, y: x + y, acc=0)
[1, 3, 6, 10, 15]
```
"""
@@ -781,29 +839,26 @@ def scan(self, fun: Callable[[Any, Any], Any], acc: Optional[int]=None) -> Iter:
self.image = list(map(lambda x: x + acc, itertools.accumulate(self.image, fun)))
return self
- @staticmethod
- def shorten(sequence: List, width: int=20) -> str:
+ def shorten(self, width: int = 20) -> str:
"""
Shorten an iterable sequence into an short, human-readable string.
```python
- >>> Iter.shorten(range(1, 6))
+ >>> Iter.range(1, 6).shorten()
'[1, 2, 3, 4, 5]'
- >>> Iter.shorten(range(1, 101), width=50)
+ >>> Iter.range(1, 100).shorten(width=50)
'[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...]'
```
"""
- return textwrap.shorten(str(list(sequence)), width=width, placeholder=' ...]')
+ return textwrap.shorten(str(self.image), width=width, placeholder=' ...]')
def shuffle(self) -> Iter:
"""
- Return a list with the elements of `self.image` shuffled.
+ Return a list with the elements of the image shuffled.
```python
- >>> Iter([1, 3]).shuffle()
- [3, 2, 1]
- >>> Iter([1, 3]).shuffle()
- >>> [2, 1, 3]
+ >>> Iter.range(1, 3).shuffle()
+ [3, 1, 2]
```
"""
random.shuffle(self.image)
@@ -812,45 +867,47 @@ def shuffle(self) -> Iter:
@overload
def slice(self, index: List[int]) -> Iter:
"""
- Return a subset list of `self.image` by `index`.
+ Return a subset list of the image by `index`.
Given an `Iter`, it drops elements before `index[0]` (zero-base),
then it takes elements until element `index[1]` (inclusively). Indexes
- are normalized, meaning that negative indexes will be counted from the end.\
+ are normalized, meaning that negative indexes will be counted from the end.
If `index[1]` is out of bounds, then it is assigned as the index of
the last element.
```python
- >>> Iter([1, 100]).slice([5, 10])
+ >>> Iter.range(1, 100).slice([5, 10])
[6, 7, 8, 9, 10, 11]
- >>> Iter([1, 10]).slice([5, 20])
+ >>> Iter.range(1, 10).slice([5, 20])
[6, 7, 8, 9, 10]
- >>> Iter([1, 30]).slice([-5, -1])
+ >>> Iter.range(1, 30).slice([-5, -1])
[26, 27, 28, 29, 30]
```
+ """
+ ...
- Alternatively, return a subset list of `self.image`, from `index` (zero-based)
- with `amount` number of elements if available. Given `self.image`, it drops
- elements right before element `index`; then, it takes `amount` of elements,
- returning as many elements as possible if there are not enough elements.
+ @overload
+ def slice(self, index: int, amount: Optional[int] = None) -> Iter:
+ """
+ Return a subset list of the image, from `index` (zero-based) with `amount`
+ number of elements if available. Given the image, it drops elements right
+ before element `index`; then, it takes `amount` of elements, returning as
+ many elements as possible if there are not enough elements.
- A negative `index` can be passed, which means `self.image` is enumerated
+ A negative `index` can be passed, which means the image is enumerated
once and the index is counted from the end (for example, `-1` starts slicing
from the last element). It returns `[]` if `amount` is `0` or if `index` is
out of bounds.
```python
- >>> Iter([1, 10]).slice(5, 100)
+ >>> Iter.range(1, 10).slice(5, 100)
[6, 7, 8, 9, 10]
```
"""
...
- @overload
- def slice(self, index: int, amount: Optional[int]=None) -> Iter: ...
-
- def slice(self, index: int | List[int], amount: Optional[int]=None) -> Iter:
+ def slice(self, index: int | List[int], amount: Optional[int] = None) -> Iter:
if isinstance(index, List):
self.image = self.image[index[0]:] if index[1] == -1 else self.image[index[0]:index[1]+1]
else:
@@ -860,7 +917,7 @@ def slice(self, index: int | List[int], amount: Optional[int]=None) -> Iter:
@overload
def slide(self, index: int, insertion_index: int) -> Iter:
"""
- Slide a single or multiple elements given by `index` from `self.image` to
+ Slide a single or multiple elements given by `index` from the image to
`insertion_index`. The semantics of the range to be moved match the semantics
of `self.slice()`.
@@ -896,15 +953,15 @@ def slide(self, index: int | List[int], insertion_index: int) -> Iter:
self.image = list(itertools.chain(p1, p3, p2, p4))
else:
element, ii = self.image.pop(index), insertion_index
- self.image.insert((ii, ii+1)[ii<0], element) if ii!=-1 else self.image.append(element)
+ self.image.insert((ii, ii+1)[ii < 0], element) if ii != -1 else self.image.append(element)
return self
- def sort(self, fun: Optional[Callable[[Any], bool]]=None, descending: bool=False) -> Iter:
+ def sort(self, fun: Optional[Callable[[Any], bool]] = None, descending: bool = False) -> Iter:
"""
- Return a new sorted `self.image`. `fun` specifies a function of one argument
- that is used to extract a comparison key from each element in iterable (for
- example, `key=str.lower`). The `descending` flag can be set to sort `self.image`
- in descending order (ascending by default).
+ Return a new sorted image. `fun` specifies a function of one argument that
+ is used to extract a comparison key from each element in iterable (for example,
+ `key=str.lower`). The `descending` flag can be set to sort the image in
+ descending order (ascending by default).
Use `functools.cmp_to_key()` to convert an old-style cmp function to a key
function.
@@ -919,18 +976,18 @@ def sort(self, fun: Optional[Callable[[Any], bool]]=None, descending: bool=False
def split(self, count: int) -> Iter:
"""
- Split `self.image` into two lists, leaving `count` elements in the first
+ Split the image into two lists, leaving `count` elements in the first
one. If `count` is a negative number, it starts counting from the back to
- the beginning of `self.image`.
+ the beginning of the image.
```python
- >>> Iter([1, 3]).split(2)
+ >>> Iter.range(1, 3).split(2)
[[1,2], [3]]
- >>> Iter([1, 3]).split(10)
+ >>> Iter.range(1, 3).split(10)
[[1, 2, 3], []]
- >>> Iter([1, 3]).split(0)
+ >>> Iter.range(1, 3).split(0)
[[], [1, 2, 3]]
- >>> Iter([1, 3]).split(-1)
+ >>> Iter.range(1, 3).split(-1)
[[1, 2], [3]]
```
"""
@@ -939,18 +996,18 @@ def split(self, count: int) -> Iter:
def split_while(self, fun: Callable[[Any], bool]) -> Iter:
"""
- Split `self.image` in two at the position of the element for which `fun`
+ Split the image in two at the position of the element for which `fun`
returns a falsy value for the first time.
It returns a nested list of length two. The element that triggered the split
is part of the second list.
```python
- >>> Iter([1, 4]).split_while(lambda x: x < 3)
+ >>> Iter.range(1, 4).split_while(lambda x: x < 3)
[[1, 2], [3, 4]]
- >>> Iter([1, 4]).split_while(lambda x: x < 0)
+ >>> Iter.range(1, 4).split_while(lambda x: x < 0)
[[], [1, 2, 3, 4]]
- >>> Iter([1, 4]).split_while(lambda x: x > 0)
+ >>> Iter.range(1, 4).split_while(lambda x: x > 0)
[[1, 2, 3, 4], []]
```
"""
@@ -963,20 +1020,20 @@ def split_while(self, fun: Callable[[Any], bool]) -> Iter:
@overload
def split_with(self, fun: Callable[[Any], bool]) -> Iter:
"""
- Split `self.image` in two lists according to the given function `fun`.
+ Split the image in two lists according to the given function `fun`.
- Split `self.image` in two lists by calling `fun` with each element in
- `self.image` as its only argument. Returns a nested list with the first
- list containing all the elements in `self.image` for which applying `fun`
+ Split the image in two lists by calling `fun` with each element in
+ the image as its only argument. Returns a nested list with the first
+ list containing all the elements in the image for which applying `fun`
returned a truthy value, and a second list with all the elements for which
- applying fun returned a falsy value. The same logic is also applied when
+ applying `fun` returned a falsy value. The same logic is also applied when
a key-value paired lambda expression is passed as an argument, as a consequence
of which the dictionary will be also split into two parts following the
same pattern.
The elements in both the returned lists (or dictionaries) are in the same
- relative order as they were in the original `self.image` (if such iterable
- was ordered, like a list). See the examples below.
+ relative order as they were in the original image (if such iterable was
+ ordered, like a list).
```python
>>> Iter([1, 5]).reverse().split_with(lambda x: x % 2 == 0)
@@ -1005,7 +1062,7 @@ def sum(self) -> Union[int, float, complex, str]:
Return the sum of all elements.
```python
- >>> Iter([1, 100]).sum()
+ >>> Iter.range(1, 100).sum()
5050
```
"""
@@ -1013,17 +1070,17 @@ def sum(self) -> Union[int, float, complex, str]:
def take(self, amount: int) -> Iter:
"""
- Takes an `amount` of elements from the beginning or the end of `self.image`.
+ Takes an `amount` of elements from the beginning or the end of the image.
If a positive `amount` is given, it takes the amount elements from the
- beginning of `self.image`. If a negative `amount` is given, the amount of
- elements will be taken from the end. `self.image` will be enumerated once
+ beginning of the image. If a negative `amount` is given, the amount of
+ elements will be taken from the end. The image will be enumerated once
to retrieve the proper index and the remaining calculation is performed from
the end. If `amount` is `0`, it returns `[]`.
```python
- >>> Iter([1, 3]).take(2)
+ >>> Iter.range(1, 3).take(2)
[1, 2]
- >>> Iter([1, 3]).take(10)
+ >>> Iter.range(1, 3).take(10)
[1, 2, 3]
>>> Iter([1, 2, 3]).take(0)
[]
@@ -1031,22 +1088,22 @@ def take(self, amount: int) -> Iter:
[3]
```
"""
- self.image = list(itertools.islice(self.image, amount) if amount > 0 else itertools.islice(reversed(self.image), abs(amount)))
+ self.image = list(itertools.islice(self.image if amount > 0 else reversed(self.image), abs(amount)))
return self
def take_every(self, nth: int) -> Iter:
"""
- Return a list of every `nth` element in `self.image`, starting with the
+ Return a list of every `nth` element in the image, starting with the
first element. The first element is always included, unless `nth` is `0`.
The second argument specifying every `nth` element must be a non-negative
integer.
```python
- >>> Iter([1, 10]).take_every(2)
+ >>> Iter.range(1, 10).take_every(2)
[1, 3, 5, 7, 9]
- >>> Iter([1, 10]).take_every(0)
+ >>> Iter.range(1, 10).take_every(0)
[]
- >>> Iter([1, 3]).take_every(1)
+ >>> Iter.range(1, 3).take_every(1)
[1, 2, 3]
```
"""
@@ -1055,12 +1112,12 @@ def take_every(self, nth: int) -> Iter:
def take_random(self, count: int) -> Iter:
"""
- Take `count` random elements from `self.image`.
+ Take `count` random elements from the image.
```python
- >>> Iter([1, 10]).take_random(2)
- [3, 1]
- >>> Iter([1, 10]).take_random(2)
+ >>> Iter.range(1, 10).take_random(2)
+ [4, 2]
+ >>> Iter.range(1, 10).take_random(2)
[6, 9]
```
"""
@@ -1069,23 +1126,52 @@ def take_random(self, count: int) -> Iter:
def take_while(self, fun: Callable[[Any], bool]) -> Iter:
"""
- Take the elements from the beginning of `self.image` while `fun` returns
+ Take the elements from the beginning of the image while `fun` returns
a truthy value.
```python
- >>> Iter([1, 3]).take_while(lambda x: x < 3)
+ >>> Iter.range(1, 3).take_while(lambda x: x < 3)
[1, 2]
```
"""
self.image = list(itertools.takewhile(fun, self.image))
return self
- def uniq(self) -> Iter:
+ def transpose(self, fillvalue: Optional[Any] = None) -> Iter:
+ """
+ Transpose the image. When the shorter iterables are exhausted, the `fillvalue`
+ is substituted in their place.
+
+ ```python
+ >>> Iter([['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']]).transpose()
+ [('a', 'd', 'g'), ('b', 'e', 'h'), ('c', 'f', 'i')]
+ >>> Iter([['a', 'b', 'c'], ['d', 'e'], ['g', 'h']]).transpose(fillvalue=False)
+ [('a', 'd', 'g'), ('b', 'e', 'h'), ('c', False, False)]
+ ```
+ """
+ self.image = list(itertools.zip_longest(*self.image, fillvalue=fillvalue))
+ return self
+
+ def union(self, iter_: Iterable = None) -> Iter:
"""
- Enumerates `self.image`, removing all duplicated elements.
+ Given a list of lists or tuples, concatenates all elements into a single list.
```python
- >>> Iter([1, 2, 3, 3, 2, 1]).uniq()
+ >>> Iter([[1, 2, 3], [4, 5, 6]]).union()
+ [1, 2, 4, 5, 6]
+ >>> Iter([[1, [2], 3], [4], [5, 6]]).union()
+ [1, [2], 3, 4, 5, 6]
+ ```
+ """
+ self.image = list(itertools.chain(*self.image))
+ return self
+
+ def unique(self) -> Iter:
+ """
+ Enumerates the image, removing all duplicated elements.
+
+ ```python
+ >>> Iter([1, 2, 3, 3, 2, 1]).unique()
[1, 2, 3]
```
"""
@@ -1094,7 +1180,7 @@ def uniq(self) -> Iter:
def unzip(self) -> Iter:
"""
- Opposite of `self.zip`. Extracts two-element tuples from `self.image` and
+ Opposite of `self.zip`. Extracts two-element tuples from the image and
groups them together.
```python
@@ -1113,13 +1199,13 @@ def unzip(self) -> Iter:
return self
@overload
- def with_index(self, fun_or_offset: Optional[int]=None) -> Iter:
+ def with_index(self, fun_or_offset: Optional[int] = None) -> Iter:
"""
- Return `self.image` with each element wrapped in a tuple alongside its index.
+ Return the image with each element wrapped in a tuple alongside its index.
May receive a function or an integer offset. If an offset is given, it will
index from the given offset instead of from zero. If a function is given,
it will index by invoking the function for each element and index (zero-based)
- of the `self.image`.
+ of the image.
```python
>>> Iter(list("abc")).with_index()
@@ -1135,7 +1221,7 @@ def with_index(self, fun_or_offset: Optional[int]=None) -> Iter:
@overload
def with_index(self, fun_or_offset: Callable[[Any, Any], Any]) -> Iter: ...
- def with_index(self, fun_or_offset: Optional[int] | Callable[[Any, Any], Any]=None) -> Iter:
+ def with_index(self, fun_or_offset: Optional[int] | Callable[[Any, Any], Any] = None) -> Iter:
if isinstance(fun_or_offset, int) or fun_or_offset is None:
offset = 0 if fun_or_offset is None else fun_or_offset
self.image = list(zip(self.image, range(offset, len(self.image)+offset)))
@@ -1143,7 +1229,7 @@ def with_index(self, fun_or_offset: Optional[int] | Callable[[Any, Any], Any]=No
self.image = list(itertools.starmap(fun_or_offset, zip(self.image, range(len(self.image)))))
return self
- def zip(self, *iterables: Iterable) -> Iter:
+ def zip(self, *iter_: Iterable) -> Iter:
"""
Zip corresponding elements from a finite collection of iterables into a
list of tuples. The zipping finishes as soon as any iterable in the given
@@ -1158,13 +1244,13 @@ def zip(self, *iterables: Iterable) -> Iter:
[(1, 'a', "foo"), (2, 'b', "bar"), (3, 'c', "baz")]
```
"""
- self.image = list(zip(self.image, *iterables))
+ self.image = list(zip(self.image, *iter_))
return self
@overload
def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any]) -> Iter:
"""
- Reduce over all of `self.image`, halting as soon as any iterable is empty.
+ Reduce over all of the image, halting as soon as any iterable is empty.
The reducer will receive 2 args: a list of elements (one from each enum) and
the accumulator.
@@ -1172,7 +1258,12 @@ def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any]) -> Iter:
>>> Iter([[1, 1], [2, 2], [3, 3]]).zip_reduce([], lambda x, acc: tuple(x) + (acc,))
[(1, 2, 3), (1, 2, 3)]
```
+ """
+ ...
+ @overload
+ def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Iterable = None, right: Iterable = None) -> Iter:
+ """
Reduce over two iterables halting as soon as either iterable is empty.
```python
@@ -1182,10 +1273,7 @@ def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any]) -> Iter:
"""
...
- @overload
- def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Iterable=None, right: Iterable=None) -> Iter: ...
-
- def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Iterable=None, right: Iterable=None) -> Iter:
+ def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Iterable = None, right: Iterable = None) -> Iter:
if left is not None and right is not None:
# reference implementation:
# https://hexdocs.pm/elixir/1.12/Enum.html#zip_reduce/3
@@ -1217,7 +1305,7 @@ def zip_with(self, fun: Callable[..., Any], *iterable: Iterable) -> Iter:
return self
def __repr__(self) -> str:
- return f"Iter(domain={Iter.shorten(self.domain)},image={Iter.shorten(self.image)})"
+ return f"Iter(domain={Iter(self.domain).shorten()},image={self.shorten()})"
def __str__(self) -> str:
- return Iter.shorten(self.image, width=80)
+ return self.shorten(width=80)
From afdd2e896f728034aa4ef9f948c81442ded0cc27 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Mon, 2 Jan 2023 01:11:25 +0100
Subject: [PATCH 02/46] Add changelog for v0.0.5 (WIP)
---
CHANGELOG.md | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 74eb5fb..234afe5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,34 @@
# Changelog
+## Version 0.0.5 (31 Jan 2023)
+
+> Note that this update introduces breaking changes.
+
+Implement a `Functions` helper class for common procedures such as
+
+- `invert`
+- `is_even`
+- `is_odd`
+- `sign`
+
+The overload in the `Iter` constructor was removed in favor of a static `Iter.range`
+method to enforce separation of concerns more strictly. Additional minor changes
+were made to pretty much all doc strings to reflect the recent changes that are
+to be consolidated for more information. In brief,
+
+- the `chunk_by` method now also provides `eject` as an optional argument
+- the `concat` method was renamed to `union`
+- `iterable` and `iter` keyword arguments were renamed to `iter_`
+- the method signature of the `range` method has changed and added support for
+ decrements
+
+The following methods are new:
+
+- `duplicates`
+- `flatten`
+- `linspace`
+- `transpose`
+
## Version 0.0.4 (17 May 2022)
Refactors code and improves the doc strings here and there. The `Iter.__ctor` method
From a6ca1d23b4ff2446df783be7e3162878cc315e02 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Mon, 2 Jan 2023 01:11:47 +0100
Subject: [PATCH 03/46] Update test suite
---
tests/test_iterfun.py | 263 ++++++++++++++++++++++--------------------
1 file changed, 138 insertions(+), 125 deletions(-)
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index e6f5f65..c0da9d9 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -7,17 +7,6 @@
class TestIter(unittest.TestCase):
- def test_iter_domain(self):
- iter = Iter([1, 4])
- self.assertEqual(iter.domain, iter.image)
- self.assertNotEqual(iter.domain, iter.filter(lambda x: x % 2 == 0).image)
-
- def test_iter_image(self):
- self.assertEqual([1, 2, 3], Iter([1, 2, 3]).image)
- self.assertEqual([1, 2, 3, 4, 5], Iter([1, 5]).image)
- self.assertEqual([1, 5], Iter([1, 5], interval=False).image)
- self.assertEqual([2, 3, 4, 5, 6, 7, 8, 9], Iter((1, 10)).domain)
-
def test_all(self):
self.assertTrue(Iter([1, 2, 3]).all())
self.assertFalse(Iter([1, None, 3]).all())
@@ -47,42 +36,34 @@ def test_avg(self):
self.assertEqual(5, Iter([0, 10]).avg())
def test_chunk_by(self):
- expected = [[1], [2, 2], [3], [4, 4, 6], [7, 7]]
- actual = Iter([1, 2, 2, 3, 4, 4, 6, 7, 7]).chunk_by(lambda x: x % 2 == 1).image
- self.assertEqual(expected, actual)
+ self.assertEqual([[1], [2, 2], [3], [4, 4, 6], [7, 7]], Iter([1, 2, 2, 3, 4, 4, 6, 7, 7]).chunk_by(lambda x: x % 2 == 1).image)
+ self.assertEqual([['a', 'b'], ['c', 'd'], ['e', 'f']], Iter(['a', 'b', '1', 'c', 'd', '2', 'e', 'f']).chunk_by(lambda x: x.isdigit(), eject=True).image)
def test_chunk_every(self):
- self.assertEqual([[1, 2], [3, 4], [5, 6]], Iter([1, 6]).chunk_every(2).image)
- self.assertEqual([[1, 2, 3], [3, 4, 5], [5, 6]], Iter([1, 6]).chunk_every(3, 2).image)
- self.assertEqual([[1, 2, 3], [3, 4, 5], [5, 6, 7]], Iter([1, 6]).chunk_every(3, 2, [7]).image)
- self.assertEqual([[1, 2, 3], [4]], Iter([1, 4]).chunk_every(3, 3, []).image)
- self.assertEqual([[1, 2, 3, 4]], Iter([1, 4]).chunk_every(10).image)
- self.assertEqual([[1, 2], [4, 5]], Iter([1, 5]).chunk_every(2, 3, []).image)
+ self.assertEqual([[1, 2], [3, 4], [5, 6]], Iter.range(1, 6).chunk_every(2).image)
+ self.assertEqual([[1, 2, 3], [3, 4, 5], [5, 6]], Iter.range(1, 6).chunk_every(3, 2).image)
+ self.assertEqual([[1, 2, 3], [3, 4, 5], [5, 6, 7]], Iter.range(1, 6).chunk_every(3, 2, [7]).image)
+ self.assertEqual([[1, 2, 3], [4]], Iter.range(1, 4).chunk_every(3, 3, []).image)
+ self.assertEqual([[1, 2, 3, 4]], Iter.range(1, 4).chunk_every(10).image)
+ self.assertEqual([[1, 2], [4, 5]], Iter.range(1, 5).chunk_every(2, 3, []).image)
@pytest.mark.xfail(raises=NotImplementedError, reason="TODO")
def test_chunk_while(self):
self.assertEqual([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], Iter([1, 10]).chunk_while([], None, None))
- def test_concat(self):
- self.assertEqual([1, 2, 3, 4, 5, 6], Iter([[1, 2, 3], [4, 5, 6]]).concat().image)
- self.assertEqual([1, [2], 3, 4, 5, 6], Iter([[1, [2], 3], [4], [5, 6]]).concat().image)
- self.assertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], Iter([[1, 4], [5, 6], [7, 9]]).concat().image)
- self.assertEqual([1, 2, 3, 8], Iter([(0, 4), (5, 6), (7, 9)]).concat().image)
- self.assertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], Iter([(0, 4), (3, 7), (6, 10)]).concat().image)
-
def test_count(self):
- self.assertEqual(3, Iter([1, 3]).count())
- self.assertEqual(2, Iter([1, 5]).count(lambda x: x % 2 == 0))
+ self.assertEqual(3, Iter([1, 2, 3]).count())
+ self.assertEqual(2, Iter.range(1, 5).count(lambda x: x % 2 == 0))
def test_count_until(self):
- self.assertEqual(5, Iter([1, 20]).count_until(5))
- self.assertEqual(20, Iter([1, 20]).count_until(50))
- self.assertTrue(Iter([1, 10]).count_until(10) == 10)
- self.assertTrue(Iter([1, 12]).count_until(10 + 1) > 10)
- self.assertTrue(Iter([1, 5]).count_until(10) < 10)
- self.assertTrue(Iter([1, 10]).count_until(10 + 1) == 10)
- self.assertEqual(7, Iter([1, 20]).count_until(7, lambda x: x % 2 == 0))
- self.assertTrue(10, Iter([1, 20]).count_until(11, lambda x: x % 2 == 0))
+ self.assertEqual(5, Iter.range(1, 20).count_until(5))
+ self.assertEqual(20, Iter.range(1, 20).count_until(50))
+ self.assertTrue(Iter.range(1, 10).count_until(10) == 10)
+ self.assertTrue(Iter.range(1, 12).count_until(10 + 1) > 10)
+ self.assertTrue(Iter.range(1, 5).count_until(10) < 10)
+ self.assertTrue(Iter.range(1, 10).count_until(10 + 1) == 10)
+ self.assertEqual(7, Iter.range(1, 20).count_until(7, lambda x: x % 2 == 0))
+ self.assertTrue(10, Iter.range(1, 20).count_until(11, lambda x: x % 2 == 0))
def test_dedup(self):
self.assertEqual([1, 2, 3, 2, 1], Iter([1, 2, 3, 3, 2, 1]).dedup().image)
@@ -95,29 +76,32 @@ def test_dedup_by(self):
self.assertEqual([3, 2, 0], Iter([3, 6, 7, 7, 2, 0, 1, 4, 1]).dedup_by(lambda x: x == 2).image)
def test_drop(self):
- self.assertEqual([3], Iter([1, 3]).drop(2).image)
- self.assertEqual([], Iter([1, 3]).drop(10).image)
- self.assertEqual([1, 2, 3], Iter([1, 3]).drop(0).image)
- self.assertEqual([1, 2], Iter([1, 3]).drop(-1).image)
+ self.assertEqual([3], Iter.range(1, 3).drop(2).image)
+ self.assertEqual([], Iter.range(1, 3).drop(10).image)
+ self.assertEqual([1, 2, 3], Iter.range(1, 3).drop(0).image)
+ self.assertEqual([1, 2], Iter.range(1, 3).drop(-1).image)
def test_drop_every(self):
- self.assertEqual([2, 4, 6, 8, 10], Iter([1, 10]).drop_every(2).image)
- self.assertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], Iter([1, 10]).drop_every(0).image)
- self.assertEqual([], Iter([1, 3]).drop_every(1).image)
+ self.assertEqual([2, 4, 6, 8, 10], Iter.range(1, 10).drop_every(2).image)
+ self.assertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], Iter.range(1, 10).drop_every(0).image)
+ self.assertEqual([], Iter.range(1, 3).drop_every(1).image)
def test_drop_while(self):
self.assertEqual([3, 2, 1], Iter([1, 2, 3, 2, 1]).drop_while(lambda x: x < 3).image)
+ def test_duplicates(self):
+ self.assertEqual([1, 2, 4], Iter([1, 1, 1, 2, 2, 3, 4, 4]).duplicates().image)
+
def test_empty(self):
self.assertTrue(Iter([]).empty())
- self.assertTrue(Iter([0, 0]).empty())
- self.assertFalse(Iter([1, 10]).empty())
+ self.assertFalse(Iter([0]).empty())
+ self.assertFalse(Iter.range(1, 10).empty())
def test_filter(self):
- self.assertEqual([2], Iter([1, 3]).filter(lambda x: x % 2 == 0).image)
+ self.assertEqual([2], Iter.range(1, 3).filter(lambda x: x % 2 == 0).image)
def test_find(self):
- self.assertEqual(3, Iter([2, 4]).find(lambda x: x % 2 == 1))
+ self.assertEqual(3, Iter.range(2, 4).find(lambda x: x % 2 == 1))
self.assertEqual(None, Iter([2, 4, 6]).find(lambda x: x % 2 == 1))
self.assertEqual(0, Iter([2, 4, 6]).find(lambda x: x % 2 == 1, default=0))
@@ -129,16 +113,19 @@ def test_find_value(self):
self.assertEqual(None, Iter([2, 4, 6]).find_value(lambda x: x % 2 == 1))
self.assertTrue(Iter([2, 3, 4]).find_value(lambda x: x % 2 == 1))
self.assertTrue(Iter([2, 3, 4]).find_value(lambda x: x % 2 == 1))
- self.assertEqual("no bools!", Iter([1, 3]).find_value(lambda x: isinstance(x, bool), default="no bools!"))
+ self.assertEqual("no bools!", Iter.range(1, 3).find_value(lambda x: isinstance(x, bool), default="no bools!"))
def test_flat_map(self):
- self.assertEqual([1, 2, 3, 4, 5, 6], Iter([(1, 3), (4, 6)], interval=False).flat_map(lambda x: Iter.range(list(x))).image)
+ self.assertEqual([1, 2, 3, 4, 5, 6], Iter([(1, 3), (4, 6)]).flat_map(lambda x: list(range(x[0], x[1]+1))).image)
self.assertEqual([[1], [2], [3]], Iter([1, 2, 3]).flat_map(lambda x: [[x]]).image)
@pytest.mark.xfail(raises=NotImplementedError, reason="TODO")
def test_flat_map_reduce(self):
pass
+ def test_flatten(self):
+ self.assertEqual([1, 2, 3, 4, 5, 6, None], Iter([[1, 2], [3, 4], [5], [6, None]]).flatten().image)
+
def test_frequencies(self):
self.assertEqual({1: 1, 2: 2, 3: 1, 4: 1, 5: 2, 6: 1}, Iter([1, 2, 2, 3, 4, 5, 5, 6]).frequencies().image)
@@ -151,7 +138,7 @@ def test_group_by(self):
self.assertEqual({3: ["a", "c"], 5: ["d"], 7: ["b"]}, Iter(["ant", "buffalo", "cat", "dingo"]).group_by(len, operator.itemgetter(0)).image)
def test_intersperse(self):
- self.assertEqual([1, 0, 2, 0, 3], Iter([1, 3]).intersperse(0).image)
+ self.assertEqual([1, 0, 2, 0, 3], Iter.range(1,3).intersperse(0).image)
self.assertEqual([1], Iter([1]).intersperse(0).image)
self.assertEqual([], Iter([]).intersperse(0).image)
@@ -161,52 +148,55 @@ def test_into(self):
self.assertEqual({'a': 1, 'b': 2}, Iter({'a': 1}).into({'b': 2}).image)
def test_join(self):
- self.assertEqual('12345', Iter([1, 5]).join())
- self.assertEqual('1,2,3,4,5', Iter([1, 5]).join(','))
+ self.assertEqual('12345', Iter.range(1, 5).join())
+ self.assertEqual('1,2,3,4,5', Iter.range(1, 5).join(','))
+
+ def test_linspace(self):
+ self.assertEqual([1.1, 1.32, 1.54, 1.76, 1.98, 2.2, 2.42, 2.64, 2.86, 3.08, 3.3], Iter.linspace(1.1, 3.3, step=10, prec=2).image)
def test_map(self):
- self.assertEqual([2, 4, 6], Iter([1, 3]).map(lambda x: 2 * x).image)
+ self.assertEqual([2, 4, 6], Iter.range(1, 3).map(lambda x: 2 * x).image)
self.assertEqual({'a': -1, 'b': -2}, Iter({'a': 1, 'b': 2}).map(lambda k, v: {k: -v}).image)
self.assertEqual({'a': 2, 'b': 4}, Iter({'a': 1, 'b': 2}).map(lambda k, v: {k: 2 * v}).image)
def test_map_every(self):
- self.assertEqual([1001, 2, 1003, 4, 1005, 6, 1007, 8, 1009, 10], Iter([1, 10]).map_every(2, lambda x: x+1000).image)
- self.assertEqual([1001, 2, 3, 1004, 5, 6, 1007, 8, 9, 1010], Iter([1, 10]).map_every(3, lambda x: x + 1000).image)
- self.assertEqual([1, 2, 3, 4, 5], Iter([1, 5]).map_every(0, lambda x: x + 1000).image)
- self.assertEqual([1001, 1002, 1003], Iter([1, 3]).map_every(1, lambda x: x + 1000).image)
+ self.assertEqual([1001, 2, 1003, 4, 1005, 6, 1007, 8, 1009, 10], Iter.range(1, 10).map_every(2, lambda x: x+1000).image)
+ self.assertEqual([1001, 2, 3, 1004, 5, 6, 1007, 8, 9, 1010], Iter.range(1, 10).map_every(3, lambda x: x + 1000).image)
+ self.assertEqual([1, 2, 3, 4, 5], Iter.range(1, 5).map_every(0, lambda x: x + 1000).image)
+ self.assertEqual([1001, 1002, 1003], Iter.range(1, 3).map_every(1, lambda x: x + 1000).image)
def test_map_intersperse(self):
- self.assertEqual([2, None, 4, None, 6], Iter([1, 3]).map_intersperse(None, lambda x: 2 * x).image)
+ self.assertEqual([2, None, 4, None, 6], Iter.range(1, 3).map_intersperse(None, lambda x: 2 * x).image)
def test_map_join(self):
- self.assertEqual('246', Iter([1, 3]).map_join(lambda x: 2 * x))
- self.assertEqual('2 = 4 = 6', Iter([1, 3]).map_join(lambda x: 2 * x, " = "))
+ self.assertEqual('246', Iter.range(1, 3).map_join(lambda x: 2 * x))
+ self.assertEqual('2 = 4 = 6', Iter.range(1, 3).map_join(lambda x: 2 * x, " = "))
def test_map_reduce(self):
- self.assertEqual(([2, 4, 6], 6), Iter([1, 3]).map_reduce(0, lambda x: 2 * x, operator.add).image)
- self.assertEqual(([1, 4, 9], 0), Iter([1, 3]).map_reduce(6, lambda x: x * x, operator.sub).image)
+ self.assertEqual(([2, 4, 6], 6), Iter.range(1, 3).map_reduce(0, lambda x: 2 * x, operator.add).image)
+ self.assertEqual(([1, 4, 9], 0), Iter.range(1, 3).map_reduce(6, lambda x: x * x, operator.sub).image)
def test_max(self):
- self.assertEqual(3, Iter([1, 3]).max())
+ self.assertEqual(3, Iter.range(1, 3).max())
self.assertEqual('you', Iter("you shall not pass".split()).max())
self.assertEqual('shall', Iter("you shall not pass".split()).max(len))
self.assertEqual('n/a', Iter([]).max(empty_fallback='n/a'))
def test_member(self):
- self.assertTrue(Iter([1, 10]).member(5))
- self.assertTrue(Iter([1, 10]).member(5.0))
+ self.assertTrue(Iter.range(1, 10).member(5))
+ self.assertTrue(Iter.range(1, 10).member(5.0))
self.assertTrue(Iter([1.0, 2.0, 3.0]).member(2))
self.assertTrue(Iter([1.0, 2.0, 3.0]).member(2.000))
self.assertFalse(Iter(['a', 'b', 'c']).member('d'))
- def test_max(self):
+ def test_min(self):
self.assertEqual(1, Iter([1, 3]).min())
self.assertEqual('not', Iter("you shall not pass".split()).min())
self.assertEqual('you', Iter("you shall not pass".split()).min(len))
self.assertEqual('n/a', Iter([]).min(empty_fallback='n/a'))
def test_min_max(self):
- self.assertEqual((1, 3), Iter([1, 3]).min_max())
+ self.assertEqual((1, 3), Iter.range(1, 3).min_max())
self.assertEqual((None, None), Iter([]).min_max(empty_fallback=None))
self.assertEqual(('a', 'aaa'), Iter(["aaa", "a", "bb", "c", "ccc"]).min_max(len))
@@ -215,56 +205,68 @@ def test_product(self):
self.assertEqual(24.0, Iter([2.0, 3.0, 4.0]).product())
def test_random(self):
- numbers = Iter.range([1, 100])
+ numbers = Iter.range(1, 100).image
self.assertIn(Iter(numbers).random(), numbers)
- def test_range(self):
- self.assertEqual([1, 2, 3, 4, 5], Iter.range([1, 5]))
- self.assertEqual([2, 3, 4], Iter.range((1, 5)))
+ def test_range_increments(self):
+ self.assertEqual([1, 2, 3, 4, 5], Iter.range(1, 5).image)
+ self.assertEqual([1, 3, 5, 7, 9], Iter.range(1, 10, 2).image)
+ self.assertEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], Iter.range(0.1, 1.0).image)
+ self.assertEqual([0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0], Iter.range(0.2, 2.0, 0.1).image)
+ self.assertEqual([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0], Iter.range(0.5, 5.0, step=0.5).image)
+
+ def test_range_decrements(self):
+ self.assertEqual([5, 4, 3, 2, 1, 0], Iter.range(5, 0, -1).image)
+ self.assertEqual([0.2, 0.4, 0.6, 0.8, 1.0], Iter.range(0.2, 1.0, 0.2).image)
+ self.assertEqual([5.0, 4.5, 4.0, 3.5, 3.0, 2.5, 2.0, 1.5, 1.0, 0.5], Iter.range(5.0, 0.5, -0.5).image)
+ self.assertEqual([], Iter.range(0.5, 5.0, -0.5).image)
+ self.assertEqual([], Iter.range(0, 5, -1).image)
+ with pytest.raises(ValueError):
+ self.assertEqual([], Iter.range(5, 0, 0).image)
def test_reduce(self):
- self.assertEqual(10, Iter([1, 4]).reduce(operator.add))
- self.assertEqual(24, Iter([1, 4]).reduce(lambda x, acc: x * acc, acc=1))
+ self.assertEqual(10, Iter.range(1, 4).reduce(operator.add))
+ self.assertEqual(24, Iter.range(1, 4).reduce(lambda x, acc: x * acc, acc=1))
def test_reduce_while(self):
- self.assertEqual(10, Iter([1, 100]).reduce_while(lambda x, acc: (True, x + acc) if x < 5 else (False, acc)))
- self.assertEqual(5050, Iter([1, 100]).reduce_while(lambda x, acc: (True, acc + x) if x > 0 else (False, acc)))
- self.assertEqual(0, Iter([1, 100]).reduce_while(lambda x, acc: (True, acc - x) if x % 2 == 0 else (False, acc), acc=2550))
+ self.assertEqual(10, Iter.range(1, 100).reduce_while(lambda x, acc: (True, x + acc) if x < 5 else (False, acc)))
+ self.assertEqual(5050, Iter.range(1, 100).reduce_while(lambda x, acc: (True, acc + x) if x > 0 else (False, acc)))
+ self.assertEqual(0, Iter.range(1, 100).reduce_while(lambda x, acc: (True, acc - x) if x % 2 == 0 else (False, acc), acc=2550))
def test_reject(self):
- self.assertEqual([1, 3], Iter([1, 3]).reject(lambda x: x % 2 == 0).image)
+ self.assertEqual([1, 3], Iter.range(1, 3).reject(lambda x: x % 2 == 0).image)
def test_reverse(self):
- self.assertEqual([5, 4, 3, 2, 1], Iter([1, 5]).reverse().image)
- self.assertEqual([3, 2, 1, 4, 5, 6], Iter([1, 3]).reverse([4, 5, 6]).image)
+ self.assertEqual([5, 4, 3, 2, 1], Iter.range(1, 5).reverse().image)
+ self.assertEqual([3, 2, 1, 4, 5, 6], Iter.range(1, 3).reverse([4, 5, 6]).image)
def test_reverse_slice(self):
- self.assertEqual([1, 2, 6, 5, 4, 3], Iter([1, 6]).reverse_slice(2, 4).image)
- self.assertEqual([1, 2, 6, 5, 4, 3, 7, 8, 9, 10], Iter([1, 10]).reverse_slice(2, 4).image)
- self.assertEqual([1, 2, 10, 9, 8, 7, 6, 5, 4, 3], Iter([1, 10]).reverse_slice(2, 30).image)
+ self.assertEqual([1, 2, 6, 5, 4, 3], Iter.range(1, 6).reverse_slice(2, 4).image)
+ self.assertEqual([1, 2, 6, 5, 4, 3, 7, 8, 9, 10], Iter.range(1, 10).reverse_slice(2, 4).image)
+ self.assertEqual([1, 2, 10, 9, 8, 7, 6, 5, 4, 3], Iter.range(1, 10).reverse_slice(2, 30).image)
def test_scan(self):
- self.assertEqual([1, 3, 6, 10, 15], Iter([1, 5]).scan(operator.add).image)
- self.assertEqual([1, 3, 6, 10, 15], Iter([1, 5]).scan(lambda x, y: x + y, acc=0).image)
- self.assertEqual([2, 4, 7, 11, 16], Iter([1, 5]).scan(operator.add, acc=1).image)
+ self.assertEqual([1, 3, 6, 10, 15], Iter.range(1, 5).scan(operator.add).image)
+ self.assertEqual([1, 3, 6, 10, 15], Iter.range(1, 5).scan(lambda x, y: x + y, acc=0).image)
+ self.assertEqual([2, 4, 7, 11, 16], Iter.range(1, 5).scan(operator.add, acc=1).image)
def test_shorten(self):
- self.assertEqual("[1, 2, 3, 4, 5]", Iter.shorten(range(1, 6)))
- self.assertEqual("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, ...]", Iter.shorten(range(1, 101), width=50))
+ self.assertEqual("[1, 2, 3, 4, 5]", Iter.range(1, 5).shorten())
+ self.assertEqual("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, ...]", Iter.range(1, 100).shorten(width=50))
def test_shuffle(self):
- iter = Iter([1, 10]).shuffle()
- self.assertTrue(iter.all(lambda x: x in Iter.range([1, 10])))
+ iter = Iter.range(1, 10).shuffle()
+ self.assertTrue(iter.all(lambda x: x in Iter.range(1, 10).image))
def test_slice(self):
- self.assertEqual([6, 7, 8, 9, 10, 11], Iter([1, 100]).slice([5, 10]).image)
- self.assertEqual([6, 7, 8, 9, 10], Iter([1, 10]).slice([5, 20]).image)
- self.assertEqual([26, 27, 28, 29, 30], Iter([1, 30]).slice([-5, -1]).image)
- self.assertEqual([7, 8, 9], Iter([1, 10]).slice([-4, -2]).image)
- self.assertEqual([7, 8, 9, 10], Iter([1, 10]).slice([-4, 100]).image)
- self.assertEqual([6, 7, 8, 9, 10], Iter([1, 10]).slice(5, 100).image)
- self.assertEqual([], Iter([1, 10]).slice(10, 5).image)
- self.assertEqual([], Iter([1, 10]).slice(-11, 5).image)
+ self.assertEqual([6, 7, 8, 9, 10, 11], Iter.range(1, 100).slice([5, 10]).image)
+ self.assertEqual([6, 7, 8, 9, 10], Iter.range(1, 10).slice([5, 20]).image)
+ self.assertEqual([26, 27, 28, 29, 30], Iter.range(1, 30).slice([-5, -1]).image)
+ self.assertEqual([7, 8, 9], Iter.range(1, 10).slice([-4, -2]).image)
+ self.assertEqual([7, 8, 9, 10], Iter.range(1, 10).slice([-4, 100]).image)
+ self.assertEqual([6, 7, 8, 9, 10], Iter.range(1, 10).slice(5, 100).image)
+ self.assertEqual([], Iter.range(1, 10).slice(10, 5).image)
+ self.assertEqual([], Iter.range(1, 10).slice(-11, 5).image)
def test_slide(self):
self.assertEqual(['a', 'f', 'b', 'c', 'd', 'e', 'g'], Iter(list("abcdefg")).slide(5, 1).image, msg="Sliding a single element")
@@ -279,47 +281,58 @@ def test_sorted(self):
self.assertEqual([3, 2, 1], Iter([3, 1, 2]).sort(descending=True).image)
def test_split(self):
- self.assertEqual([[1, 2], [3]], Iter([1, 3]).split(2).image)
- self.assertEqual([[1, 2, 3], []], Iter([1, 3]).split(10).image)
- self.assertEqual([[], [1, 2, 3]], Iter([1, 3]).split(0).image)
- self.assertEqual([[1, 2], [3]], Iter([1, 3]).split(-1).image)
- self.assertEqual([[1], [2, 3]], Iter([1, 3]).split(-2).image)
- self.assertEqual([[], [1, 2, 3]], Iter([1, 3]).split(-5).image)
+ self.assertEqual([[1, 2], [3]], Iter.range(1, 3).split(2).image)
+ self.assertEqual([[1, 2, 3], []], Iter.range(1, 3).split(10).image)
+ self.assertEqual([[], [1, 2, 3]], Iter.range(1, 3).split(0).image)
+ self.assertEqual([[1, 2], [3]], Iter.range(1, 3).split(-1).image)
+ self.assertEqual([[1], [2, 3]], Iter.range(1, 3).split(-2).image)
+ self.assertEqual([[], [1, 2, 3]], Iter.range(1, 3).split(-5).image)
def test_split_while(self):
- self.assertEqual([[1, 2], [3, 4]], Iter([1, 4]).split_while(lambda x: x < 3).image)
- self.assertEqual([[], [1, 2, 3, 4]], Iter([1, 4]).split_while(lambda x: x < 0).image)
- self.assertEqual([[1, 2, 3, 4], []], Iter([1, 4]).split_while(lambda x: x > 0).image)
+ self.assertEqual([[1, 2], [3, 4]], Iter.range(1, 4).split_while(lambda x: x < 3).image)
+ self.assertEqual([[], [1, 2, 3, 4]], Iter.range(1, 4).split_while(lambda x: x < 0).image)
+ self.assertEqual([[1, 2, 3, 4], []], Iter.range(1, 4).split_while(lambda x: x > 0).image)
def test_split_with(self):
- self.assertEqual([[4, 2, 0], [5, 3, 1]], Iter([0, 5]).reverse().split_with(lambda x: x % 2 == 0).image)
+ self.assertEqual([[4, 2, 0], [5, 3, 1]], Iter.range(0, 5).reverse().split_with(lambda x: x % 2 == 0).image)
self.assertEqual([{'b': -2, 'd': -3}, {'a': 1, 'c':1}], Iter({'a': 1, 'b': -2, 'c': 1, 'd': -3}).split_with(lambda k, v: v < 0).image)
self.assertEqual([{}, {'a': 1, 'b': -2, 'c': 1, 'd': -3}], Iter({'a': 1, 'b': -2, 'c': 1, 'd': -3}).split_with(lambda k, v: v > 50).image)
self.assertEqual([{}, {}], Iter({}).split_with(lambda k, v: v > 50).image)
def test_sum(self):
- self.assertEqual(5050, Iter([1, 100]).sum())
+ self.assertEqual(5050, Iter.range(1, 100).sum())
def test_take(self):
- self.assertEqual([1, 2], Iter([1, 3]).take(2).image)
- self.assertEqual([1, 2, 3], Iter([1, 3]).take(10).image)
- self.assertEqual([], Iter([1, 3]).take(0).image)
- self.assertEqual([3], Iter([1, 3]).take(-1).image)
+ self.assertEqual([1, 2], Iter.range(1, 3).take(2).image)
+ self.assertEqual([1, 2, 3], Iter.range(1, 3).take(10).image)
+ self.assertEqual([], Iter.range(1, 3).take(0).image)
+ self.assertEqual([3], Iter.range(1, 3).take(-1).image)
def test_take_every(self):
- self.assertEqual([1, 3, 5, 7, 9], Iter([1, 10]).take_every(2).image)
- self.assertEqual([], Iter([1, 10]).take_every(0).image)
- self.assertEqual([1, 2, 3], Iter([1, 3]).take_every(1).image)
+ self.assertEqual([1, 3, 5, 7, 9], Iter.range(1, 10).take_every(2).image)
+ self.assertEqual([], Iter.range(1, 10).take_every(0).image)
+ self.assertEqual([1, 2, 3], Iter.range(1, 3).take_every(1).image)
def test_take_random(self):
- numbers = set(Iter.range([1, 100]))
- self.assertTrue(set(Iter([1, 100]).take_random(2).image).issubset(numbers))
+ numbers = Iter.range(1, 100)
+ self.assertTrue(set(numbers.take_random(2).image).issubset(set(numbers.image)))
def test_take_while(self):
- self.assertEqual([1, 2], Iter([1, 3]).take_while(lambda x: x < 3).image)
+ self.assertEqual([1, 2], Iter.range(1, 3).take_while(lambda x: x < 3).image)
+
+ def test_transpose(self):
+ self.assertEqual([('a', 'd', 'g'), ('b', 'e', 'h'), ('c', 'f', 'i')], Iter([['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']]).transpose().image)
+ self.assertEqual([('a', 'd', 'g'), ('b', 'e', 'h'), ('c', False, False)], Iter([['a', 'b', 'c'], ['d', 'e'], ['g', 'h']]).transpose(fillvalue=False).image)
+
+ def test_union(self):
+ self.assertEqual([1, 2, 3, 4, 5, 6], Iter([[1, 2, 3], [4, 5, 6]]).union().image)
+ self.assertEqual([1, [2], 3, 4, 5, 6], Iter([[1, [2], 3], [4], [5, 6]]).union().image)
+ self.assertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], Iter([[1, 2, 3, 4], [5, 6], [7, 8, 9]]).union().image)
+ self.assertEqual([0, 4, 5, 6, 7, 9], Iter([(0, 4), (5, 6), (7, 9)]).union().image)
+ self.assertEqual([0, 4, 3, 7, 6, 10], Iter([[0, 4], (3, 7), [6, 10]]).union().image)
def test_uniq(self):
- self.assertEqual([1, 2, 3], Iter([1, 2, 3, 3, 2, 1]).uniq().image)
+ self.assertEqual([1, 2, 3], Iter([1, 2, 3, 3, 2, 1]).unique().image)
def test_unzip(self):
self.assertEqual([['a', 'b', 'c'], [1, 2, 3]], Iter({'a': 1, 'b': 2, 'c': 3}).unzip().image)
@@ -332,7 +345,7 @@ def test_with_index(self):
self.assertEqual([(0, 'a'), (1, 'b'), (2, 'c')], Iter(list("abc")).with_index(lambda k, v: (v, k)).image)
def test_zip(self):
- self.assertEqual([(1, 'a', "foo"), (2, 'b', "bar"), (3, 'c', "baz")], Iter([1, 3]).zip(list("abc"), ["foo", "bar", "baz"]).image)
+ self.assertEqual([(1, 'a', "foo"), (2, 'b', "bar"), (3, 'c', "baz")], Iter.range(1, 3).zip(list("abc"), ["foo", "bar", "baz"]).image)
self.assertEqual([('a', 0), ('b', 1), ('c', 2)], Iter(list("abc")).zip(range(3)).image)
self.assertEqual([('a', 0, 'd'), ('b', 1, 'e'), ('c', 2, 'f')], Iter(list("abc")).zip(range(3), list("def")).image)
@@ -347,9 +360,9 @@ def test_zip_with(self):
self.assertEqual([5, 7], Iter([]).zip_with(operator.add, [1, 2], [4, 5, 6, 7]).image)
def test_str(self):
- self.assertEqual("[1, 2, 3, 4, 5]", str(Iter([1, 5])))
- self.assertEqual("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, ...]", str(Iter([1, 100])))
+ self.assertEqual("[1, 2, 3, 4, 5]", str(Iter.range(1, 5)))
+ self.assertEqual("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, ...]", str(Iter.range(1, 100)))
def test_repr(self):
- self.assertEqual("Iter(domain=[1, 2, 3, 4, 5],image=[1, 2, 3, 4, 5])", repr(Iter([1, 5])))
- self.assertEqual("Iter(domain=[1, 2, 3, 4, 5, ...],image=[1, 2, 3, 4, 5, ...])", repr(Iter([1, 50])))
+ self.assertEqual("Iter(domain=[1, 2, 3, 4, 5],image=[1, 2, 3, 4, 5])", repr(Iter.range(1, 5)))
+ self.assertEqual("Iter(domain=[1, 2, 3, 4, 5, ...],image=[1, 2, 3, 4, 5, ...])", repr(Iter.range(1, 50)))
From cd57b24e1fe48214d9e8299b72fc108ae961a6ca Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Mon, 2 Jan 2023 01:12:10 +0100
Subject: [PATCH 04/46] Add more complex test scenarios to test suite as a
proof of concept
---
tests/test_scenarios.py | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
create mode 100644 tests/test_scenarios.py
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py
new file mode 100644
index 0000000..0c0e41c
--- /dev/null
+++ b/tests/test_scenarios.py
@@ -0,0 +1,29 @@
+import unittest
+
+from src.iterfun import Iter
+from src.iterfun import Functions as fun
+
+class TestScenarios(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls) -> None:
+ cls.data2022d1 = [[1000, 2000, 3000], [4000], [5000, 6000], [7000, 8000, 9000], [10000]]
+
+ #region custom scenarios
+
+ def test_scenario1(self):
+ actual = Iter.range(1, 10).filter(fun.is_even).map(fun.invert).sum()
+ self.assertEqual(-30, actual)
+
+ #endregion
+
+ #region advent of code
+
+ def test_year2022_day1_task1(self):
+ actual = Iter(self.data2022d1).map(sum).max()
+ self.assertEqual(24_000, actual)
+
+ def test_year2022_day1_task2(self):
+ actual = Iter(self.data2022d1).map(sum).sort(descending=True).take(3).sum()
+ self.assertEqual(45_000, actual)
+
+ #endregion
From c0091364c614ce4bd5bc328ab19a0b9412e5c60f Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Mon, 2 Jan 2023 01:12:54 +0100
Subject: [PATCH 05/46] Increment version number and add Functions import
---
src/iterfun/__init__.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/iterfun/__init__.py b/src/iterfun/__init__.py
index fe6f874..6da4513 100644
--- a/src/iterfun/__init__.py
+++ b/src/iterfun/__init__.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
-from .iterfun import Iter
+from .iterfun import Functions, Iter
-__version__ = "0.0.4"
+__version__ = "0.0.5"
package_name = "iterfun"
python_major = "3"
python_minor = "7"
From fd5d3b3c8f77c6453eaf99ec4c307eb023b1d9ea Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 14:53:39 +0100
Subject: [PATCH 06/46] Update version matrix in pipeline
---
.github/workflows/python-app.yml | 2 +-
.github/workflows/python-publish.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index 3103ed4..1ded940 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -7,7 +7,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11-dev']
+ python-version: [ '3.8', '3.9', '3.10', '3.11.1', '3.12.0-alpha.3']
os: [ ubuntu-latest, macOS-latest, windows-latest ]
steps:
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
index 0b94ff6..5779739 100644
--- a/.github/workflows/python-publish.yml
+++ b/.github/workflows/python-publish.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
- python-version: [ 3.7 ]
+ python-version: [ 3.8 ]
steps:
- uses: actions/checkout@v2
From e28cfadab07e52f2580df63c30834e91f9c4ba46 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 15:02:11 +0100
Subject: [PATCH 07/46] Update dev dependencies
---
requirements/dev.txt | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 17d0bff..c767eba 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -1,4 +1,4 @@
-pytest==6.2.5
-wheel==0.37.0
-setuptools==58.1.0
-twine==3.7.1
+pytest==7.2.0
+wheel==0.38.4
+setuptools==65.6.3
+twine==4.0.2
From 43b4bd96b6de9501fead9ddcc480a0b2588171dc Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 15:06:27 +0100
Subject: [PATCH 08/46] Edit setup file
* remove redundat lines from project template
* update classifiers
* fix exception message (wrong package name)
---
setup.py | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/setup.py b/setup.py
index 1d47c49..7391bf6 100644
--- a/setup.py
+++ b/setup.py
@@ -14,13 +14,10 @@
python_major = int(re.search(r'python_major = "(.*?)"', lines).group(1))
python_minor = int(re.search(r'python_minor = "(.*?)"', lines).group(1))
-if package_name == 'iterfun':
- print("\033[93mWARNING: You should rename the default package name.\033[0m")
-
try:
assert sys.version_info >= (int(python_major), int(python_minor))
except AssertionError:
- raise RuntimeError("\033[91mWeather requires Python %s.%s+ (You have Python %s)\033[0m" % (python_major, python_minor, sys.version))
+ raise RuntimeError("\033[91m%s requires Python %s.%s+ (You have Python %s)\033[0m" % (package_name, python_major, python_minor, sys.version))
print("reading dependency file")
@@ -67,11 +64,11 @@
'License :: OSI Approved :: MIT License',
'Intended Audience :: Developers',
'Natural Language :: English',
- 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
+ 'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: Implementation :: CPython',
'Operating System :: OS Independent',
'Topic :: Utilities',
From 63c66e33048f5ae37ab218c85fd2c9bb4727815f Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 15:08:35 +0100
Subject: [PATCH 09/46] Bump python_minor to 3.8
---
src/iterfun/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/iterfun/__init__.py b/src/iterfun/__init__.py
index 6da4513..25efa6c 100644
--- a/src/iterfun/__init__.py
+++ b/src/iterfun/__init__.py
@@ -5,4 +5,4 @@
__version__ = "0.0.5"
package_name = "iterfun"
python_major = "3"
-python_minor = "7"
+python_minor = "8"
From 970588dd16f9ff3935defcbc15ee6521d6363359 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 15:17:47 +0100
Subject: [PATCH 10/46] Implement to_list and parameter support for the union
method
---
src/iterfun/iterfun.py | 5 ++++-
tests/test_iterfun.py | 1 +
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index d1935bb..8474bf9 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -1137,6 +1137,9 @@ def take_while(self, fun: Callable[[Any], bool]) -> Iter:
self.image = list(itertools.takewhile(fun, self.image))
return self
+ def to_list(self) -> List[Any]:
+ return list(self.image)
+
def transpose(self, fillvalue: Optional[Any] = None) -> Iter:
"""
Transpose the image. When the shorter iterables are exhausted, the `fillvalue`
@@ -1163,7 +1166,7 @@ def union(self, iter_: Iterable = None) -> Iter:
[1, [2], 3, 4, 5, 6]
```
"""
- self.image = list(itertools.chain(*self.image))
+ self.image = list(itertools.chain(*self.image, *iter_) if iter_ else itertools.chain(*self.image))
return self
def unique(self) -> Iter:
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index c0da9d9..78abdca 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -325,6 +325,7 @@ def test_transpose(self):
self.assertEqual([('a', 'd', 'g'), ('b', 'e', 'h'), ('c', False, False)], Iter([['a', 'b', 'c'], ['d', 'e'], ['g', 'h']]).transpose(fillvalue=False).image)
def test_union(self):
+ self.assertEqual([1, 2, 3, 4, 5, 6], Iter([[1, 2, 3], [4]]).union([[5], [6]]).image)
self.assertEqual([1, 2, 3, 4, 5, 6], Iter([[1, 2, 3], [4, 5, 6]]).union().image)
self.assertEqual([1, [2], 3, 4, 5, 6], Iter([[1, [2], 3], [4], [5, 6]]).union().image)
self.assertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], Iter([[1, 2, 3, 4], [5, 6], [7, 8, 9]]).union().image)
From 6b5320f24421c55d9175cdf3efe33927468784fa Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 17:07:32 +0100
Subject: [PATCH 11/46] Implement intersects method and add doc strings
---
src/iterfun/iterfun.py | 20 +++++++++++++++++++-
tests/test_iterfun.py | 6 ++++++
2 files changed, 25 insertions(+), 1 deletion(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 8474bf9..55ef3c9 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -434,6 +434,20 @@ def value(g): return list(g) if value_fun is None else list(map(value_fun, g))
self.image = {k: value(g) for k, g in itertools.groupby(sorted(self.image, key=key_fun), key_fun)}
return self
+ def intersects(self, iter_: Iterable) -> Iter:
+ """
+ Return the intersection between the image and `iter_`.
+
+ ```python
+ >>> Iter([1, 2, 3, 4]).intersects([5, 6, 1, 2])
+ [1, 2]
+ >>> Iter.range(1, 10).intersects(list("abc")).count() > 0
+ False
+ ```
+ """
+ self.image = list(set(self.image) & set(iter_))
+ return self
+
def intersperse(self, separator: Any) -> Iter:
"""
Intersperses separator between each element of the image.
@@ -1138,6 +1152,9 @@ def take_while(self, fun: Callable[[Any], bool]) -> Iter:
return self
def to_list(self) -> List[Any]:
+ """
+ Return the image as a list object.
+ """
return list(self.image)
def transpose(self, fillvalue: Optional[Any] = None) -> Iter:
@@ -1157,7 +1174,8 @@ def transpose(self, fillvalue: Optional[Any] = None) -> Iter:
def union(self, iter_: Iterable = None) -> Iter:
"""
- Given a list of lists or tuples, concatenates all elements into a single list.
+ Given a list of lists or tuples, concatenate all elements into a single
+ list by performing a union operating.
```python
>>> Iter([[1, 2, 3], [4, 5, 6]]).union()
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index 78abdca..e65b5af 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -137,6 +137,12 @@ def test_group_by(self):
self.assertEqual({3: ["ant", "cat"], 5: ["dingo"], 7: ["buffalo"]}, Iter(["ant", "buffalo", "cat", "dingo"]).group_by(len).image)
self.assertEqual({3: ["a", "c"], 5: ["d"], 7: ["b"]}, Iter(["ant", "buffalo", "cat", "dingo"]).group_by(len, operator.itemgetter(0)).image)
+ def test_intersects(self):
+ self.assertEqual([1, 2], Iter([1, 2, 3, 4]).intersects([5, 6, 1, 2]).image)
+ self.assertEqual([1, 2], Iter([1, 2, 3, 4]).intersects([5, 6, 2, 1]).image)
+ self.assertTrue(Iter.range(1, 10).intersects(range(1, 11)).count() > 0)
+ self.assertFalse(Iter.range(1, 10).intersects(list("abc")).count() > 0)
+
def test_intersperse(self):
self.assertEqual([1, 0, 2, 0, 3], Iter.range(1,3).intersperse(0).image)
self.assertEqual([1], Iter([1]).intersperse(0).image)
From c8630098e3e7584e038925cab8372f0a48e9bff3 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 17:23:03 +0100
Subject: [PATCH 12/46] Implement the (set) difference method
---
src/iterfun/iterfun.py | 20 ++++++++++++++++++--
tests/test_iterfun.py | 4 ++++
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 55ef3c9..912b6d3 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -216,6 +216,21 @@ def dedup_by(self, fun: Callable[[Any], bool]):
self.image = [self.image[0], *[self.image[i] for i in range(1, len(self.image)) if fun(self.image[i-1]) != fun(self.image[i])]]
return self
+ def difference(self, iter_: Iterable) -> Iter:
+ """
+ Return the difference between the image and `iter_`, sorted in ascending
+ order.
+
+ ```python
+ >>> Iter.range(1, 10).difference([4, 3, 2, 10])
+ [1, 5, 6, 7, 8, 9]
+ >>> Iter(list("abc")).difference(range(1, 11))
+ ['a', 'b', 'c']
+ ```
+ """
+ self.image = sorted(set(self.image) - set(iter_))
+ return self
+
def drop(self, amount: int) -> Iter:
"""
Drop the `amount` of elements from the image. If a negative `amount` is
@@ -436,7 +451,8 @@ def value(g): return list(g) if value_fun is None else list(map(value_fun, g))
def intersects(self, iter_: Iterable) -> Iter:
"""
- Return the intersection between the image and `iter_`.
+ Return the intersection between the image and `iter_`, sorted in ascending
+ order.
```python
>>> Iter([1, 2, 3, 4]).intersects([5, 6, 1, 2])
@@ -445,7 +461,7 @@ def intersects(self, iter_: Iterable) -> Iter:
False
```
"""
- self.image = list(set(self.image) & set(iter_))
+ self.image = sorted(set(self.image) & set(iter_))
return self
def intersperse(self, separator: Any) -> Iter:
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index e65b5af..b0c5b08 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -75,6 +75,10 @@ def test_dedup_by(self):
self.assertEqual([3, 2, 4, 1], Iter([3, 6, 7, 7, 2, 0, 1, 4, 1]).dedup_by(lambda x: x > 2).image)
self.assertEqual([3, 2, 0], Iter([3, 6, 7, 7, 2, 0, 1, 4, 1]).dedup_by(lambda x: x == 2).image)
+ def test_difference(self):
+ self.assertEqual([1, 5, 6, 7, 8, 9], Iter.range(1, 10).difference([4, 3, 2, 10]).image)
+ self.assertEqual(['a', 'b', 'c'], Iter(list("abc")).difference(range(1, 11)).image)
+
def test_drop(self):
self.assertEqual([3], Iter.range(1, 3).drop(2).image)
self.assertEqual([], Iter.range(1, 3).drop(10).image)
From 9d7219f73354b6140769c9fdcb3290c2c9094d08 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 17:33:01 +0100
Subject: [PATCH 13/46] Revise the implementation of the union method
* remove the concatenation from the implementation
since this can also be done with a prepended flatten
method, and use the built-in set method for performing
the union of two hashable objects
---
src/iterfun/iterfun.py | 15 +++++++--------
tests/test_iterfun.py | 8 ++------
2 files changed, 9 insertions(+), 14 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 912b6d3..64749c0 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -1188,19 +1188,18 @@ def transpose(self, fillvalue: Optional[Any] = None) -> Iter:
self.image = list(itertools.zip_longest(*self.image, fillvalue=fillvalue))
return self
- def union(self, iter_: Iterable = None) -> Iter:
+ def union(self, iter_: Iterable) -> Iter:
"""
- Given a list of lists or tuples, concatenate all elements into a single
- list by performing a union operating.
+ Return the union between the image and `iter_`, sorted in ascending order.
```python
- >>> Iter([[1, 2, 3], [4, 5, 6]]).union()
- [1, 2, 4, 5, 6]
- >>> Iter([[1, [2], 3], [4], [5, 6]]).union()
- [1, [2], 3, 4, 5, 6]
+ >>> Iter([1, 2, 3, 4]).union([5, 6])
+ [1, 2, 3, 4, 5, 6]
+ >>> Iter([[1, 2], [3]]).flatten().union([4, 5, 6])
+ [1, 2, 3, 4, 5, 6]
```
"""
- self.image = list(itertools.chain(*self.image, *iter_) if iter_ else itertools.chain(*self.image))
+ self.image = sorted(set(self.image) | set(iter_))
return self
def unique(self) -> Iter:
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index b0c5b08..f66e9ba 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -335,12 +335,8 @@ def test_transpose(self):
self.assertEqual([('a', 'd', 'g'), ('b', 'e', 'h'), ('c', False, False)], Iter([['a', 'b', 'c'], ['d', 'e'], ['g', 'h']]).transpose(fillvalue=False).image)
def test_union(self):
- self.assertEqual([1, 2, 3, 4, 5, 6], Iter([[1, 2, 3], [4]]).union([[5], [6]]).image)
- self.assertEqual([1, 2, 3, 4, 5, 6], Iter([[1, 2, 3], [4, 5, 6]]).union().image)
- self.assertEqual([1, [2], 3, 4, 5, 6], Iter([[1, [2], 3], [4], [5, 6]]).union().image)
- self.assertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], Iter([[1, 2, 3, 4], [5, 6], [7, 8, 9]]).union().image)
- self.assertEqual([0, 4, 5, 6, 7, 9], Iter([(0, 4), (5, 6), (7, 9)]).union().image)
- self.assertEqual([0, 4, 3, 7, 6, 10], Iter([[0, 4], (3, 7), [6, 10]]).union().image)
+ self.assertEqual([1, 2, 3, 4, 5, 6], Iter([1, 2, 3, 4]).union([5, 6]).image)
+ self.assertEqual([1, 2, 3, 4, 5, 6], Iter([[1, 2], [3]]).flatten().union([4, 5, 6]).image)
def test_uniq(self):
self.assertEqual([1, 2, 3], Iter([1, 2, 3, 3, 2, 1]).unique().image)
From b95aa9c3f66000e770007cba6af0ed6fe09fcb18 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 18:09:08 +0100
Subject: [PATCH 14/46] Implement is_subset and is_superset and edit doc
strings
* doc strings should start with the 'Test' verb if a function
returns a boolean
* implements is_subset and is_superset based on Python's `set`
collection
---
src/iterfun/iterfun.py | 25 +++++++++++++++++++------
tests/test_iterfun.py | 10 ++++++++++
2 files changed, 29 insertions(+), 6 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 64749c0..64fe9ea 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+import math
import functools
import itertools
import operator
@@ -57,8 +58,8 @@ def __init__(self, iter_: Iterable) -> Iter:
def all(self, fun: Optional[Callable[[Any], bool]] = None) -> bool:
"""
- Return `True` if all elements in the image are truthy, or `True` if
- `fun` is not None and its map truthy for all elements in the image.
+ Test if all elements in the image are truthy, or if `fun` maps to truthy
+ values in all instances.
```python
>>> Iter([1, 2, 3]).all()
@@ -75,8 +76,8 @@ def all(self, fun: Optional[Callable[[Any], bool]] = None) -> bool:
def any(self, fun: Optional[Callable[[Any], bool]] = None) -> bool:
"""
- Return `True` if any elements in the image are truthy, or `True` if
- `fun` is not None and its map truthy for at least on element in the image.
+ Test if any elements in the image are truthy, or if `fun` maps to truthy
+ values in at least once instance.
```python
>>> Iter([False, False, False]).any()
@@ -296,7 +297,7 @@ def duplicates(self) -> Iter:
def empty(self) -> bool:
"""
- Return `True` if the image is empty, otherwise `False`.
+ Test if the image is empty.
```python
>>> Iter([]).empty()
@@ -496,6 +497,18 @@ def into(self, iter_: Iterable) -> Iter:
self.image = {**self.image, **iter_} if isinstance(iter_, Dict) else [*self.image, *iter_]
return self
+ def is_subset(self, iter_: Iterable, proper: bool = False) -> bool:
+ """
+ Test whether every element in `iter_` is in the image.
+ """
+ return set(iter_) < set(self.image) if proper else set(iter_) <= set(self.image)
+
+ def is_superset(self, iter_: Iterable, proper: bool = False) -> bool:
+ """
+ Test whether every element in the image is in `iter_`.
+ """
+ return set(iter_) > set(self.image) if proper else set(iter_) >= set(self.image)
+
def join(self, joiner: Optional[str] = None) -> str:
"""
Join the image into a string using `joiner` as a separator. If `joiner`
@@ -641,7 +654,7 @@ def max(self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Option
def member(self, element: Any) -> bool:
"""
- Checks if element exists within the image.
+ Test if an element exists in the image.
```python
>>> Iter.range(1, 10).member(5)
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index f66e9ba..eefd6a5 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -157,6 +157,16 @@ def test_into(self):
self.assertEqual({'a': 1, 'b': 2}, Iter({'a': 1, 'b': 2}).into({}).image)
self.assertEqual({'a': 1, 'b': 2}, Iter({'a': 1}).into({'b': 2}).image)
+ def test_is_subset(self):
+ self.assertTrue(Iter.range(1, 10).is_subset([1, 2]))
+ self.assertTrue(Iter([1, 2, 3]).is_subset([1, 2, 3]))
+ self.assertFalse(Iter([1, 2, 3]).is_subset([1, 2, 3], proper=True))
+
+ def test_is_superset(self):
+ self.assertTrue(Iter([1, 2]).is_superset(range(1, 11)))
+ self.assertTrue(Iter([1, 2, 3]).is_superset([1, 2, 3]))
+ self.assertFalse(Iter([1, 2, 3]).is_superset([1, 2, 3], proper=True))
+
def test_join(self):
self.assertEqual('12345', Iter.range(1, 5).join())
self.assertEqual('1,2,3,4,5', Iter.range(1, 5).join(','))
From e4211d82373dac1af25a6a906443a3e480d82263 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 18:11:18 +0100
Subject: [PATCH 15/46] Update Functions class
* add doc strings to all methods
* improve implementation of the sign method
---
src/iterfun/iterfun.py | 19 +++++++++++++------
1 file changed, 13 insertions(+), 6 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 64fe9ea..a4371dd 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -16,21 +16,28 @@
class Functions:
def invert(x: Union[int, float]) -> Union[int, float]:
+ """
+ Invert the algebraic sign of `x`.
+ """
return -1 * x
def is_even(x: Union[int, float]) -> bool:
+ """
+ Test if `x` is an even number.
+ """
return x % 2 == 0
def is_odd(x: Union[int, float]) -> bool:
+ """
+ Test if `x` is an odd number.
+ """
return x % 2 != 0
def sign(x: Union[int, float]) -> Literal[-1, 0, 1]:
- if x < 0:
- return -1
- elif x == 0:
- return 0
- else:
- return 1
+ """
+ Extract the sign of a real number.
+ """
+ return x and (-1 if x < 0 else 1)
class Iter:
From eaac35cc89c3be8ec969637a9a3dd53642743868 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 18:14:29 +0100
Subject: [PATCH 16/46] Add examples in doc strings for is_subset and
is_superset
---
src/iterfun/iterfun.py | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index a4371dd..0a0a1da 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -507,12 +507,30 @@ def into(self, iter_: Iterable) -> Iter:
def is_subset(self, iter_: Iterable, proper: bool = False) -> bool:
"""
Test whether every element in `iter_` is in the image.
+
+ ```python
+ >>> Iter.range(1, 10).is_subset([1, 2])
+ True
+ >>> Iter([1, 2, 3]).is_subset([1, 2, 3])
+ True
+ >>> Iter([1, 2, 3]).is_subset([1, 2, 3], proper=True)
+ False
+ ```
"""
return set(iter_) < set(self.image) if proper else set(iter_) <= set(self.image)
def is_superset(self, iter_: Iterable, proper: bool = False) -> bool:
"""
Test whether every element in the image is in `iter_`.
+
+ ```python
+ >>> Iter.range(1, 10).is_subset([1, 2])
+ True
+ >>> Iter([1, 2, 3]).is_subset([1, 2, 3])
+ True
+ >>> Iter([1, 2, 3]).is_subset([1, 2, 3], proper=True)
+ False
+ ```
"""
return set(iter_) > set(self.image) if proper else set(iter_) >= set(self.image)
From 3a205fefedafccde304d73a39ee824b040424583 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 18:29:04 +0100
Subject: [PATCH 17/46] Implement is_disjoint method and improve is_subset and
is_superset
---
src/iterfun/iterfun.py | 18 ++++++++++++++++--
tests/test_iterfun.py | 4 ++++
2 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 0a0a1da..5e7d85e 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -504,6 +504,20 @@ def into(self, iter_: Iterable) -> Iter:
self.image = {**self.image, **iter_} if isinstance(iter_, Dict) else [*self.image, *iter_]
return self
+ def is_disjoint(self, iter_: Iterable) -> bool:
+ """
+ Test whether the `iter_` has no elements in common with the image. Sets
+ are disjoints if and only if their intersection is the empty set.
+
+ ```python
+ >>> Iter.range(1, 10).is_disjoint([11, 12, 13])
+ True
+ >>> Iter([1, 2, 3]).is_disjoint([1, 2, 3])
+ False
+ ```
+ """
+ return set(self.image).isdisjoint(iter_)
+
def is_subset(self, iter_: Iterable, proper: bool = False) -> bool:
"""
Test whether every element in `iter_` is in the image.
@@ -517,7 +531,7 @@ def is_subset(self, iter_: Iterable, proper: bool = False) -> bool:
False
```
"""
- return set(iter_) < set(self.image) if proper else set(iter_) <= set(self.image)
+ return set(iter_) < set(self.image) if proper else set(iter_).issubset(self.image)
def is_superset(self, iter_: Iterable, proper: bool = False) -> bool:
"""
@@ -532,7 +546,7 @@ def is_superset(self, iter_: Iterable, proper: bool = False) -> bool:
False
```
"""
- return set(iter_) > set(self.image) if proper else set(iter_) >= set(self.image)
+ return set(iter_) > set(self.image) if proper else set(iter_).issuperset(self.image)
def join(self, joiner: Optional[str] = None) -> str:
"""
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index eefd6a5..3065271 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -157,6 +157,10 @@ def test_into(self):
self.assertEqual({'a': 1, 'b': 2}, Iter({'a': 1, 'b': 2}).into({}).image)
self.assertEqual({'a': 1, 'b': 2}, Iter({'a': 1}).into({'b': 2}).image)
+ def test_is_disjoint(self):
+ self.assertTrue(Iter.range(1, 10).is_disjoint([11, 12, 13]))
+ self.assertFalse(Iter([1, 2, 3]).is_disjoint([1, 2, 3]))
+
def test_is_subset(self):
self.assertTrue(Iter.range(1, 10).is_subset([1, 2]))
self.assertTrue(Iter([1, 2, 3]).is_subset([1, 2, 3]))
From fc8871e75cfe634370ba211dae8b227559be89af Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 18:42:48 +0100
Subject: [PATCH 18/46] Rename member to is_member
---
src/iterfun/iterfun.py | 39 +++++++++++++++++++--------------------
tests/test_iterfun.py | 10 +++++-----
2 files changed, 24 insertions(+), 25 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 5e7d85e..652302e 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-import math
import functools
import itertools
import operator
@@ -518,6 +517,25 @@ def is_disjoint(self, iter_: Iterable) -> bool:
"""
return set(self.image).isdisjoint(iter_)
+ def is_member(self, element: Any) -> bool:
+ """
+ Test if an element exists in the image.
+
+ ```python
+ >>> Iter.range(1, 10).member(5)
+ True
+ >>> Iter.range(1, 10).member(5.0)
+ False
+ >>> Iter([1.0, 2.0, 3.0]).member(2)
+ True
+ >>> Iter([1.0, 2.0, 3.0]).member(2.000)
+ True
+ >>> Iter(['a', 'b', 'c']).member('d')
+ False
+ ```
+ """
+ return element in self.image
+
def is_subset(self, iter_: Iterable, proper: bool = False) -> bool:
"""
Test whether every element in `iter_` is in the image.
@@ -691,25 +709,6 @@ def max(self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Option
"""
return (max(self.image, key=fun) if fun is not None else max(self.image)) if self.image else empty_fallback
- def member(self, element: Any) -> bool:
- """
- Test if an element exists in the image.
-
- ```python
- >>> Iter.range(1, 10).member(5)
- True
- >>> Iter.range(1, 10).member(5.0)
- False
- >>> Iter([1.0, 2.0, 3.0]).member(2)
- True
- >>> Iter([1.0, 2.0, 3.0]).member(2.000)
- True
- >>> Iter(['a', 'b', 'c']).member('d')
- False
- ```
- """
- return element in self.image
-
def min(self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Optional[Any] = None) -> Any:
"""
Return the minimum of the image as determined by the function `fun`.
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index 3065271..fc3e758 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -207,11 +207,11 @@ def test_max(self):
self.assertEqual('n/a', Iter([]).max(empty_fallback='n/a'))
def test_member(self):
- self.assertTrue(Iter.range(1, 10).member(5))
- self.assertTrue(Iter.range(1, 10).member(5.0))
- self.assertTrue(Iter([1.0, 2.0, 3.0]).member(2))
- self.assertTrue(Iter([1.0, 2.0, 3.0]).member(2.000))
- self.assertFalse(Iter(['a', 'b', 'c']).member('d'))
+ self.assertTrue(Iter.range(1, 10).is_member(5))
+ self.assertTrue(Iter.range(1, 10).is_member(5.0))
+ self.assertTrue(Iter([1.0, 2.0, 3.0]).is_member(2))
+ self.assertTrue(Iter([1.0, 2.0, 3.0]).is_member(2.000))
+ self.assertFalse(Iter(['a', 'b', 'c']).is_member('d'))
def test_min(self):
self.assertEqual(1, Iter([1, 3]).min())
From 9ca13a78a936d089891dae8ee0af6e7b981911f4 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 19:03:33 +0100
Subject: [PATCH 19/46] Improve type hints for zip_reduce
---
src/iterfun/iterfun.py | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 652302e..822ae21 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -1149,6 +1149,17 @@ def sum(self) -> Union[int, float, complex, str]:
"""
return sum(self.image)
+ def symmetric_difference(self, iter_: Iterable) -> Iter:
+ """
+ todo
+
+ ```python
+ >>> # todo
+ ```
+ """
+ self.image = list(set(self.image).symmetric_difference(iter_))
+ return self
+
def take(self, amount: int) -> Iter:
"""
Takes an `amount` of elements from the beginning or the end of the image.
@@ -1349,7 +1360,7 @@ def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any]) -> Iter:
...
@overload
- def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Iterable = None, right: Iterable = None) -> Iter:
+ def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Iterable, right: Iterable) -> Iter:
"""
Reduce over two iterables halting as soon as either iterable is empty.
@@ -1360,7 +1371,7 @@ def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Iterab
"""
...
- def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Iterable = None, right: Iterable = None) -> Iter:
+ def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Optional[Iterable] = None, right: Optional[Iterable] = None) -> Iter:
if left is not None and right is not None:
# reference implementation:
# https://hexdocs.pm/elixir/1.12/Enum.html#zip_reduce/3
From 6474721aa7e069d241af9dbc72f19475ffef91c2 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 20:55:08 +0100
Subject: [PATCH 20/46] Implement the static randint function
---
src/iterfun/iterfun.py | 23 +++++++++++++++++++++++
tests/test_iterfun.py | 14 ++++++++++++++
2 files changed, 37 insertions(+)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 822ae21..79b3d01 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -6,6 +6,7 @@
import itertools
import operator
import random
+import secrets
import statistics
import textwrap
from collections import ChainMap, Counter
@@ -754,6 +755,28 @@ def product(self) -> Union[float, int, complex]:
"""
return functools.reduce(operator.mul, self.image, 1)
+ @staticmethod
+ def __randint(a: int, b: int, size: int, secure: Optional[bool] = False) -> Generator[int]:
+ for _ in range(size):
+ yield secrets.choice(range(a, b+1)) if secure else random.randint(a, b)
+
+ @staticmethod
+ def randint(a: int, b: int, size: int, secure: Optional[bool] = False) -> Iter:
+ """
+ Generate a collection of random integers. Uses a pseudo-random number
+ generator (PRNG) by default, which can be turned off by setting `secure`
+ to `True` which falls back to the cryptographically secure `secrets` module
+ that runs slower in comparison.
+
+ ```python
+ >>> Iter.randint(1, 10, 10)
+ [4, 2, 7, 3, 5, 10, 9, 8, 7, 2]
+ >>> Iter.randint(1, 10_000, size=10, secure=True)
+ [9642, 7179, 1514, 3683, 5117, 9256, 1318, 9880, 5597, 2605]
+ ```
+ """
+ return Iter(Iter.__randint(a, b, size, secure))
+
def random(self) -> Any:
"""
Return a random element from the image.
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index fc3e758..e5ee3bf 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -228,6 +228,20 @@ def test_product(self):
self.assertEqual(24, Iter([2, 3, 4]).product())
self.assertEqual(24.0, Iter([2.0, 3.0, 4.0]).product())
+ def test_randint(self):
+ a, b, size = 1, 100, 1000
+ random_numbers = Iter.randint(a, b, size).to_list()
+ self.assertEqual(size, len(random_numbers))
+ self.assertTrue(min(random_numbers) >= a)
+ self.assertTrue(max(random_numbers) <= b)
+
+ def test_randint_secure(self):
+ a, b, size = 1, 100, 1000
+ random_numbers = Iter.randint(a, b, size, secure=True).to_list()
+ self.assertEqual(size, len(random_numbers))
+ self.assertTrue(min(random_numbers) >= a)
+ self.assertTrue(max(random_numbers) <= b)
+
def test_random(self):
numbers = Iter.range(1, 100).image
self.assertIn(Iter(numbers).random(), numbers)
From 45fe3ae758266a53d0267b2e7c98c171568f537e Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 8 Jan 2023 21:00:45 +0100
Subject: [PATCH 21/46] Implement the symmetric_difference method
---
src/iterfun/iterfun.py | 5 +++--
tests/test_iterfun.py | 3 +++
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 79b3d01..e5c1967 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -1174,10 +1174,11 @@ def sum(self) -> Union[int, float, complex, str]:
def symmetric_difference(self, iter_: Iterable) -> Iter:
"""
- todo
+ Return the symmetric difference between the image and `iter_`.
```python
- >>> # todo
+ >>> Iter.range(1, 10).symmetric_difference(range(5, 16))
+ [1, 2, 3, 4, 11, 12, 13, 14, 15]
```
"""
self.image = list(set(self.image).symmetric_difference(iter_))
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index e5ee3bf..8743bed 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -340,6 +340,9 @@ def test_split_with(self):
def test_sum(self):
self.assertEqual(5050, Iter.range(1, 100).sum())
+ def test_symmetric_difference(self):
+ self.assertEqual([1, 2, 3, 4, 11, 12, 13, 14, 15], Iter.range(1, 10).symmetric_difference(range(5, 16)).image)
+
def test_take(self):
self.assertEqual([1, 2], Iter.range(1, 3).take(2).image)
self.assertEqual([1, 2, 3], Iter.range(1, 3).take(10).image)
From 66a37bf64f752a88e2cd5744f8a796ba63c6c162 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Wed, 11 Jan 2023 14:29:37 +0100
Subject: [PATCH 22/46] Implement iter and next and change implementation of
str
---
src/iterfun/iterfun.py | 19 +++++++++++++++++--
tests/test_iterfun.py | 17 +++++++++++++++--
2 files changed, 32 insertions(+), 4 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index e5c1967..75dda0b 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -62,6 +62,8 @@ def __init__(self, iter_: Iterable) -> Iter:
"""
self.domain: Final[Iterable] = iter_
self.image: Iterable = iter_
+ self.__value = None
+ self.__index = 0
def all(self, fun: Optional[Callable[[Any], bool]] = None) -> bool:
"""
@@ -945,7 +947,7 @@ def scan(self, fun: Callable[[Any, Any], Any], acc: Optional[int] = None) -> Ite
def shorten(self, width: int = 20) -> str:
"""
- Shorten an iterable sequence into an short, human-readable string.
+ Shorten an iterable sequence into a short, human-readable string.
```python
>>> Iter.range(1, 6).shorten()
@@ -1426,8 +1428,21 @@ def zip_with(self, fun: Callable[..., Any], *iterable: Iterable) -> Iter:
self.image = [fun(*args) for args in zip(*iterable)]
return self
+ #region magic methods
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ if self.__index >= len(self.image): raise StopIteration
+ self.__value = self.image[self.__index]
+ self.__index += 1
+ return self.__value
+
def __repr__(self) -> str:
return f"Iter(domain={Iter(self.domain).shorten()},image={self.shorten()})"
def __str__(self) -> str:
- return self.shorten(width=80)
+ return ','.join(map(str, self.image))
+
+ #endregion
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index 8743bed..92bcd5d 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -397,9 +397,22 @@ def test_zip_with(self):
self.assertEqual([5, 11], Iter([]).zip_with(lambda x, y, z: x + y + z, [1, 3], [3, 5], [1, 3]).image)
self.assertEqual([5, 7], Iter([]).zip_with(operator.add, [1, 2], [4, 5, 6, 7]).image)
+ def test_next(self):
+ a, b = 3, 6
+ first = next(Iter.range(a, b))
+ self.assertEqual(3, first)
+ self.assertEqual(4, first+1)
+ self.assertEqual(5, first+2)
+ self.assertEqual(6, first+3)
+
+ def test_iter(self):
+ a, b = 1, 10
+ for i in Iter.range(a, b):
+ self.assertTrue(a <= i and i <= b)
+
def test_str(self):
- self.assertEqual("[1, 2, 3, 4, 5]", str(Iter.range(1, 5)))
- self.assertEqual("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, ...]", str(Iter.range(1, 100)))
+ self.assertEqual("1,2,3,4,5", str(Iter.range(1, 5)))
+ self.assertEqual("1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20", str(Iter.range(1, 20)))
def test_repr(self):
self.assertEqual("Iter(domain=[1, 2, 3, 4, 5],image=[1, 2, 3, 4, 5])", repr(Iter.range(1, 5)))
From 4484b82f7f6ce946e49ea603529b352ff3e2bae4 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Thu, 12 Jan 2023 08:42:29 +0100
Subject: [PATCH 23/46] Improve test_next
---
tests/test_iterfun.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index 92bcd5d..0293ac4 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -399,11 +399,11 @@ def test_zip_with(self):
def test_next(self):
a, b = 3, 6
- first = next(Iter.range(a, b))
- self.assertEqual(3, first)
- self.assertEqual(4, first+1)
- self.assertEqual(5, first+2)
- self.assertEqual(6, first+3)
+ sequence = Iter.range(a, b)
+ self.assertEqual(3, next(sequence))
+ self.assertEqual(4, next(sequence))
+ self.assertEqual(5, next(sequence))
+ self.assertEqual(6, next(sequence))
def test_iter(self):
a, b = 1, 10
From fec4429569af947fa860f6d5a0764b17c36932d0 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Thu, 12 Jan 2023 08:59:41 +0100
Subject: [PATCH 24/46] Implement operator overload for __eq__ and __ne__
---
src/iterfun/iterfun.py | 10 ++++++++--
tests/test_iterfun.py | 20 +++++++++++++++-----
2 files changed, 23 insertions(+), 7 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 75dda0b..d2d67e9 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -1430,10 +1430,16 @@ def zip_with(self, fun: Callable[..., Any], *iterable: Iterable) -> Iter:
#region magic methods
- def __iter__(self):
+ def __eq__(self, other: Iter) -> bool:
+ return self.image == other.image
+
+ def __ne__(self, other: Iter) -> bool:
+ return self.image != other.image
+
+ def __iter__(self) -> Iter:
return self
- def __next__(self):
+ def __next__(self) -> Any:
if self.__index >= len(self.image): raise StopIteration
self.__value = self.image[self.__index]
self.__index += 1
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index 0293ac4..b987472 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -397,6 +397,21 @@ def test_zip_with(self):
self.assertEqual([5, 11], Iter([]).zip_with(lambda x, y, z: x + y + z, [1, 3], [3, 5], [1, 3]).image)
self.assertEqual([5, 7], Iter([]).zip_with(operator.add, [1, 2], [4, 5, 6, 7]).image)
+ def test_eq(self):
+ a = Iter.range(a, b)
+ b = Iter.range(a, b)
+ self.assertEqual(a, b)
+
+ def test_ne(self):
+ a = Iter.range(1, 10)
+ b = Iter.range(5, 12)
+ self.assertNotEqual(a, b)
+
+ def test_iter(self):
+ a, b = 1, 10
+ for i in Iter.range(a, b):
+ self.assertTrue(a <= i and i <= b)
+
def test_next(self):
a, b = 3, 6
sequence = Iter.range(a, b)
@@ -405,11 +420,6 @@ def test_next(self):
self.assertEqual(5, next(sequence))
self.assertEqual(6, next(sequence))
- def test_iter(self):
- a, b = 1, 10
- for i in Iter.range(a, b):
- self.assertTrue(a <= i and i <= b)
-
def test_str(self):
self.assertEqual("1,2,3,4,5", str(Iter.range(1, 5)))
self.assertEqual("1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20", str(Iter.range(1, 20)))
From ac9b0c1b741dd433f7b765ccd59d8d419a7a1673 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Thu, 12 Jan 2023 09:03:21 +0100
Subject: [PATCH 25/46] Fix unit test
---
tests/test_iterfun.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index b987472..b218e8e 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -398,8 +398,8 @@ def test_zip_with(self):
self.assertEqual([5, 7], Iter([]).zip_with(operator.add, [1, 2], [4, 5, 6, 7]).image)
def test_eq(self):
- a = Iter.range(a, b)
- b = Iter.range(a, b)
+ a = Iter.range(1, 10)
+ b = Iter.range(1, 10)
self.assertEqual(a, b)
def test_ne(self):
From de10c777ed91785e71a8d1239724ef29ac0bdf32 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Thu, 12 Jan 2023 21:57:31 +0100
Subject: [PATCH 26/46] Implement cartesian product method and edit typings
* use "T | U" instead of "Union[T, U]" in method signatures
* define Self as explicit type for "self", this type hint
is only available in Python 3.11 which is why we also import
typing_extensions which is a new external dependency
* same goes for the "Annotated" type hint (Python 3.9+)
---
src/iterfun/iterfun.py | 223 ++++++++++++++++++++++-------------------
1 file changed, 121 insertions(+), 102 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index d2d67e9..dd8caeb 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -8,32 +8,37 @@
import random
import secrets
import statistics
+import sys
import textwrap
from collections import ChainMap, Counter
from decimal import ROUND_HALF_UP, Decimal
-from typing import Any, Callable, Dict, Final, Generator, Iterable, List, Literal, Optional, Tuple, Union, overload
+from typing import Any, Callable, Dict, Final, Generator, Iterable, Iterator, List, Literal, Optional, Tuple, overload
+if sys.version_info >= (3, 11):
+ from typing import Annotated, Self
+else:
+ from typing_extensions import Annotated, Self
class Functions:
- def invert(x: Union[int, float]) -> Union[int, float]:
+ def invert(x: int | float) -> (int | float):
"""
Invert the algebraic sign of `x`.
"""
return -1 * x
- def is_even(x: Union[int, float]) -> bool:
+ def is_even(x: int | float) -> bool:
"""
Test if `x` is an even number.
"""
return x % 2 == 0
- def is_odd(x: Union[int, float]) -> bool:
+ def is_odd(x: int | float) -> bool:
"""
Test if `x` is an odd number.
"""
return x % 2 != 0
- def sign(x: Union[int, float]) -> Literal[-1, 0, 1]:
+ def sign(x: int | float) -> Literal[-1, 0, 1]:
"""
Extract the sign of a real number.
"""
@@ -56,7 +61,7 @@ class Iter:
```
"""
- def __init__(self, iter_: Iterable) -> Iter:
+ def __init__(self, iter_: Iterable) -> None:
"""
Initialize a new object of this class.
"""
@@ -65,7 +70,7 @@ def __init__(self, iter_: Iterable) -> Iter:
self.__value = None
self.__index = 0
- def all(self, fun: Optional[Callable[[Any], bool]] = None) -> bool:
+ def all(self: Self, fun: Optional[Callable[[Any], bool]] = None) -> bool:
"""
Test if all elements in the image are truthy, or if `fun` maps to truthy
values in all instances.
@@ -83,7 +88,7 @@ def all(self, fun: Optional[Callable[[Any], bool]] = None) -> bool:
"""
return all(self.image) if fun is None else all(map(fun, self.image))
- def any(self, fun: Optional[Callable[[Any], bool]] = None) -> bool:
+ def any(self: Self, fun: Optional[Callable[[Any], bool]] = None) -> bool:
"""
Test if any elements in the image are truthy, or if `fun` maps to truthy
values in at least once instance.
@@ -99,7 +104,7 @@ def any(self, fun: Optional[Callable[[Any], bool]] = None) -> bool:
"""
return any(self.image) if fun is None else any(map(fun, self.image))
- def at(self, index: int) -> Any:
+ def at(self: Self, index: int) -> Any:
"""
Find the element at the given zero-based `index`. Raises an `IndexError`
if `index` is out of bonds. A negative index can be passed, which means
@@ -116,7 +121,7 @@ def at(self, index: int) -> Any:
"""
return self.image[index]
- def avg(self) -> Union[int, float]:
+ def avg(self: Self) -> (int | float):
"""
Return the sample arithmetic mean of the image.
@@ -127,7 +132,21 @@ def avg(self) -> Union[int, float]:
"""
return statistics.mean(self.image)
- def chunk_by(self, fun: Callable[[Any], bool], eject: bool = False) -> Iter:
+ def cartesian(self: Self, repeat: int = 1) -> Iter:
+ """
+ Return the cartesian product, i.e. `A x B = {(a, b) | a ∈ A ^ b ∈ B}`.
+
+ ```python
+ >>> Iter([(1, 2), ('a', 'b')]).cartesian()
+ [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]
+ >>> Iter([True, False]).cartesian(repeat=2)
+ [(True, True), (True, False), (False, True), (False, False)]
+ ```
+ """
+ nested = any(isinstance(i, Iterable) for i in self.image)
+ return Iter(list(itertools.product(self.image, repeat=repeat) if not nested else itertools.product(*self.image, repeat=repeat)))
+
+ def chunk_by(self: Self, fun: Callable[[Any], bool], eject: bool = False) -> Self:
"""
Split the domain on every element for which `fun` returns a new value.
Remove any group for which `fun` initially returned `True` if `eject` is
@@ -145,7 +164,7 @@ def chunk_by(self, fun: Callable[[Any], bool], eject: bool = False) -> Iter:
self.image = [x for x in tmp if len(x) > 1 and not fun(x[0])] if eject else tmp
return self
- def chunk_every(self, count: int, step: Optional[int] = None, leftover: Optional[List[Any]] = None) -> Iter:
+ def chunk_every(self: Self, count: int, step: Optional[int] = None, leftover: Optional[List[Any]] = None) -> Self:
"""
Return list of lists containing `count` elements each. `step` is optional
and, if not passed, defaults to `count`, i.e. chunks do not overlap.
@@ -163,12 +182,12 @@ def chunk_every(self, count: int, step: Optional[int] = None, leftover: Optional
if leftover: self.image[-1].extend(leftover[:len(self.image[-1])])
return self
- def chunk_while(self, acc: List, chunk_fun: Callable, chunk_after: Callable) -> Iter:
+ def chunk_while(self: Self, acc: List, chunk_fun: Callable, chunk_after: Callable) -> Self:
# reference implementation:
# https://hexdocs.pm/elixir/1.12/Enum.html#chunk_while/4
raise NotImplementedError()
- def count(self, fun: Optional[Callable[[Any], bool]] = None) -> int:
+ def count(self: Self, fun: Optional[Callable[[Any], bool]] = None) -> int:
"""
Return the size of the image if `fun` is `None`, else return the cardinality
of the image for which `fun` returns a truthy value.
@@ -182,7 +201,7 @@ def count(self, fun: Optional[Callable[[Any], bool]] = None) -> int:
"""
return len(list(self.image)) if fun is None else len(list(filter(fun, self.image)))
- def count_until(self, limit: int, fun: Optional[Callable[[Any], bool]] = None) -> int:
+ def count_until(self: Self, limit: int, fun: Optional[Callable[[Any], bool]] = None) -> int:
"""
Determine the cardinality of the image for which `fun` returns a truthy value,
stopping at `limit`.
@@ -196,7 +215,7 @@ def count_until(self, limit: int, fun: Optional[Callable[[Any], bool]] = None) -
"""
return len(list(self.image)[:limit]) if fun is None else len(list(filter(fun, self.image))[:limit])
- def dedup(self) -> Iter:
+ def dedup(self: Self) -> Self:
"""
Enumerate the image, returning a list where all consecutive duplicated
elements are collapsed to a single element.
@@ -209,7 +228,7 @@ def dedup(self) -> Iter:
self.image = list(map(operator.itemgetter(0), itertools.groupby(self.image)))
return self
- def dedup_by(self, fun: Callable[[Any], bool]):
+ def dedup_by(self: Self, fun: Callable[[Any], bool]) -> Self:
"""
Enumerates the image, returning a list where all consecutive duplicated
elements are collapsed to a single element.
@@ -226,7 +245,7 @@ def dedup_by(self, fun: Callable[[Any], bool]):
self.image = [self.image[0], *[self.image[i] for i in range(1, len(self.image)) if fun(self.image[i-1]) != fun(self.image[i])]]
return self
- def difference(self, iter_: Iterable) -> Iter:
+ def difference(self: Self, iter_: Iterable) -> Self:
"""
Return the difference between the image and `iter_`, sorted in ascending
order.
@@ -241,7 +260,7 @@ def difference(self, iter_: Iterable) -> Iter:
self.image = sorted(set(self.image) - set(iter_))
return self
- def drop(self, amount: int) -> Iter:
+ def drop(self: Self, amount: int) -> Self:
"""
Drop the `amount` of elements from the image. If a negative `amount` is
given, the `amount` of last values will be dropped. The image will be
@@ -261,7 +280,7 @@ def drop(self, amount: int) -> Iter:
self.image = tmp[amount:] if amount > 0 else tmp[:len(tmp)+amount]
return self
- def drop_every(self, nth: int) -> Iter:
+ def drop_every(self: Self, nth: int) -> Self:
"""
Return a list of every `nth` element in the image dropped, starting with
the first element. The first element is always dropped, unless `nth` is `0`.
@@ -280,7 +299,7 @@ def drop_every(self, nth: int) -> Iter:
self.image = [] if nth == 1 else [self.image[i] for i in range(int(nth != 0), len(self.image), nth if nth > 1 else 1)]
return self
- def drop_while(self, fun: Callable[[Any], bool]) -> Iter:
+ def drop_while(self: Self, fun: Callable[[Any], bool]) -> Self:
"""
Drop elements at the beginning of the image while `fun` returns a truthy value.
@@ -292,7 +311,7 @@ def drop_while(self, fun: Callable[[Any], bool]) -> Iter:
self.image = list(itertools.dropwhile(fun, self.image))
return self
- def duplicates(self) -> Iter:
+ def duplicates(self: Self) -> Self:
"""
Return all duplicated occurrences from the image.
@@ -304,7 +323,7 @@ def duplicates(self) -> Iter:
self.image = [item for item, count in Counter(self.image).items() if count > 1]
return self
- def empty(self) -> bool:
+ def empty(self: Self) -> bool:
"""
Test if the image is empty.
@@ -319,7 +338,7 @@ def empty(self) -> bool:
"""
return not bool(len(self.image))
- def filter(self, fun: Callable[[Any], bool]) -> Iter:
+ def filter(self: Self, fun: Callable[[Any], bool]) -> Self:
"""
Filter the image, i.e. return only those elements for which `fun` returns
a truthy value.
@@ -332,7 +351,7 @@ def filter(self, fun: Callable[[Any], bool]) -> Iter:
self.image = list(filter(fun, self.image))
return self
- def find(self, fun: Callable[[Any], bool], default: Optional[Any] = None) -> Optional[Any]:
+ def find(self: Self, fun: Callable[[Any], bool], default: Optional[Any] = None) -> Optional[Any]:
"""
Return the first element for which `fun` returns a truthy value. If no
such element is found, return `default`.
@@ -348,7 +367,7 @@ def find(self, fun: Callable[[Any], bool], default: Optional[Any] = None) -> Opt
"""
return next(filter(fun, self.image), default)
- def find_index(self, fun: Callable[[Any], bool], default: Optional[Any] = None) -> Optional[Any]:
+ def find_index(self: Self, fun: Callable[[Any], bool], default: Optional[Any] = None) -> Optional[Any]:
"""
Similar to `self.find`, but return the zero-based index of the element
instead of the element itself.
@@ -363,7 +382,7 @@ def find_index(self, fun: Callable[[Any], bool], default: Optional[Any] = None)
found = next(filter(fun, self.image), default)
return self.image.index(found) if found in self.image else default
- def find_value(self, fun: Callable[[Any], bool], default: Optional[Any] = None) -> Optional[Any]:
+ def find_value(self: Self, fun: Callable[[Any], bool], default: Optional[Any] = None) -> Optional[Any]:
"""
Similar to `self.find`, but return the value of the function invocation instead
of the element itself.
@@ -380,7 +399,7 @@ def find_value(self, fun: Callable[[Any], bool], default: Optional[Any] = None)
found = next(filter(fun, self.image), default)
return fun(found) if found is not default else default
- def flat_map(self, fun: Callable[[Any], Any]) -> Iter:
+ def flat_map(self: Self, fun: Callable[[Any], Any]) -> Self:
"""
Map the given `fun` over the image and flattens the result.
@@ -394,12 +413,12 @@ def flat_map(self, fun: Callable[[Any], Any]) -> Iter:
self.image = list(itertools.chain(*map(fun, self.image)))
return self
- def flat_map_reduce(self, acc: int, fun: Callable[[Any, Any], Any]) -> Iter:
+ def flat_map_reduce(self: Self, acc: int, fun: Callable[[Any, Any], Any]) -> Self:
# reference implementation:
# https://hexdocs.pm/elixir/1.12/Enum.html#flat_map_reduce/3
raise NotImplementedError()
- def flatten(self) -> Iter:
+ def flatten(self: Self) -> Self:
"""
Flatten the current image.
@@ -411,7 +430,7 @@ def flatten(self) -> Iter:
self.image = list(itertools.chain(*self.image))
return self
- def frequencies(self) -> Iter:
+ def frequencies(self: Self) -> Self:
"""
Return a map with keys as unique elements of the image and values as
the count of every element.
@@ -424,7 +443,7 @@ def frequencies(self) -> Iter:
self.image = Counter(self.image)
return self
- def frequencies_by(self, key_fun: Callable[[Any], Any]) -> Iter:
+ def frequencies_by(self: Self, key_fun: Callable[[Any], Any]) -> Self:
"""
Return a map with keys as unique elements given by `key_fun` and values
as the count of every element.
@@ -439,7 +458,7 @@ def frequencies_by(self, key_fun: Callable[[Any], Any]) -> Iter:
self.image = Counter(map(key_fun, self.image))
return self
- def group_by(self, key_fun: Callable[[Any], Any], value_fun: Optional[Callable[[Any], Any]] = None) -> Iter:
+ def group_by(self: Self, key_fun: Callable[[Any], Any], value_fun: Optional[Callable[[Any], Any]] = None) -> Self:
"""
Split the image into groups based on `key_fun`.
@@ -459,7 +478,7 @@ def value(g): return list(g) if value_fun is None else list(map(value_fun, g))
self.image = {k: value(g) for k, g in itertools.groupby(sorted(self.image, key=key_fun), key_fun)}
return self
- def intersects(self, iter_: Iterable) -> Iter:
+ def intersects(self: Self, iter_: Iterable) -> Self:
"""
Return the intersection between the image and `iter_`, sorted in ascending
order.
@@ -474,7 +493,7 @@ def intersects(self, iter_: Iterable) -> Iter:
self.image = sorted(set(self.image) & set(iter_))
return self
- def intersperse(self, separator: Any) -> Iter:
+ def intersperse(self: Self, separator: Any) -> Self:
"""
Intersperses separator between each element of the image.
@@ -490,7 +509,7 @@ def intersperse(self, separator: Any) -> Iter:
self.image = list(itertools.islice(itertools.chain.from_iterable(zip(itertools.repeat(separator), self.image)), 1, None))
return self
- def into(self, iter_: Iterable) -> Iter:
+ def into(self: Self, iter_: Iterable) -> Self:
"""
Insert the given image into `iter_`.
@@ -506,7 +525,7 @@ def into(self, iter_: Iterable) -> Iter:
self.image = {**self.image, **iter_} if isinstance(iter_, Dict) else [*self.image, *iter_]
return self
- def is_disjoint(self, iter_: Iterable) -> bool:
+ def is_disjoint(self: Self, iter_: Iterable) -> bool:
"""
Test whether the `iter_` has no elements in common with the image. Sets
are disjoints if and only if their intersection is the empty set.
@@ -520,7 +539,7 @@ def is_disjoint(self, iter_: Iterable) -> bool:
"""
return set(self.image).isdisjoint(iter_)
- def is_member(self, element: Any) -> bool:
+ def is_member(self: Self, element: Any) -> bool:
"""
Test if an element exists in the image.
@@ -539,7 +558,7 @@ def is_member(self, element: Any) -> bool:
"""
return element in self.image
- def is_subset(self, iter_: Iterable, proper: bool = False) -> bool:
+ def is_subset(self: Self, iter_: Iterable, proper: bool = False) -> bool:
"""
Test whether every element in `iter_` is in the image.
@@ -554,7 +573,7 @@ def is_subset(self, iter_: Iterable, proper: bool = False) -> bool:
"""
return set(iter_) < set(self.image) if proper else set(iter_).issubset(self.image)
- def is_superset(self, iter_: Iterable, proper: bool = False) -> bool:
+ def is_superset(self: Self, iter_: Iterable, proper: bool = False) -> bool:
"""
Test whether every element in the image is in `iter_`.
@@ -569,7 +588,7 @@ def is_superset(self, iter_: Iterable, proper: bool = False) -> bool:
"""
return set(iter_) > set(self.image) if proper else set(iter_).issuperset(self.image)
- def join(self, joiner: Optional[str] = None) -> str:
+ def join(self: Self, joiner: Optional[str] = None) -> str:
"""
Join the image into a string using `joiner` as a separator. If `joiner`
is not passed at all, it defaults to an empty string. All elements in
@@ -589,7 +608,7 @@ def __round(dec: Decimal, prec: int, rounding: str = ROUND_HALF_UP) -> Decimal:
return dec.quantize(Decimal(10)**-prec, rounding)
@staticmethod
- def linspace(a: Union[int, float], b: Union[int, float], step: int = 50, prec: int = 28) -> Iter:
+ def linspace(a: int | float, b: int | float, step: int = 50, prec: int = 28) -> Iter:
"""
Return evenly spaced numbers over a specified closed interval `[a, b]`.
Set delta precision by rounding to `prec` decimal places.
@@ -603,7 +622,7 @@ def linspace(a: Union[int, float], b: Union[int, float], step: int = 50, prec: i
return Iter([float(Decimal(str(a)) + i * delta) for i in range(step+1)])
@overload
- def map(self, fun: Callable[[Any], Any]) -> Iter:
+ def map(self: Self, fun: Callable[[Any], Any]) -> Self:
"""
Return a list where each element is the result of invoking `fun` on each
corresponding element of the image.
@@ -616,7 +635,7 @@ def map(self, fun: Callable[[Any], Any]) -> Iter:
...
@overload
- def map(self, fun: Callable[[Any, Any], Dict]) -> Iter:
+ def map(self: Self, fun: Callable[[Any, Any], Dict]) -> Self:
"""
Return a dictionary where each element is the result of invoking `fun` on each
corresponding key-value pair of the image.
@@ -628,11 +647,11 @@ def map(self, fun: Callable[[Any, Any], Dict]) -> Iter:
"""
...
- def map(self, fun: Callable[[Any], Any]) -> Iter:
+ def map(self: Self, fun: Callable[[Any], Any]) -> Self:
self.image = dict(ChainMap(*itertools.starmap(fun, self.image.items()))) if isinstance(self.image, Dict) else list(map(fun, self.image))
return self
- def map_every(self, nth: int, fun: Callable[[Any], Any]) -> Iter:
+ def map_every(self: Self, nth: int, fun: Callable[[Any], Any]) -> Self:
"""
Return a list of results of invoking `fun` on every `nth` element of the image,
starting with the first element. The first element is always passed to the given
@@ -652,7 +671,7 @@ def map_every(self, nth: int, fun: Callable[[Any], Any]) -> Iter:
self.image[i] = fun(self.image[i])
return self
- def map_intersperse(self, separator: Any, fun: Callable[[Any], Any]) -> Iter:
+ def map_intersperse(self: Self, separator: Any, fun: Callable[[Any], Any]) -> Self:
"""
Map and intersperses the image in one pass.
@@ -664,7 +683,7 @@ def map_intersperse(self, separator: Any, fun: Callable[[Any], Any]) -> Iter:
self.image = list(itertools.islice(itertools.chain.from_iterable(zip(itertools.repeat(separator), map(fun, self.image))), 1, None))
return self
- def map_join(self, fun: Callable[[Any], Any], joiner: Optional[str] = None) -> str:
+ def map_join(self: Self, fun: Callable[[Any], Any], joiner: Optional[str] = None) -> str:
"""
Map and join the image in one pass. If joiner is not passed at all, it
defaults to an empty string. All elements returned from invoking `fun` must
@@ -679,7 +698,7 @@ def map_join(self, fun: Callable[[Any], Any], joiner: Optional[str] = None) -> s
"""
return f"{joiner or ''}".join(map(str, map(fun, self.image)))
- def map_reduce(self, acc: Union[int, float, complex], fun: Callable[[Any], Any], acc_fun: Optional[Callable[[Any, Any], Any]]) -> Iter:
+ def map_reduce(self: Self, acc: int | float | complex, fun: Callable[[Any], Any], acc_fun: Optional[Callable[[Any, Any], Any]]) -> Self:
"""
Invoke the given function to each element in the image to reduce it to
a single element, while keeping an accumulator. Return a tuple where the
@@ -695,7 +714,7 @@ def map_reduce(self, acc: Union[int, float, complex], fun: Callable[[Any], Any],
self.image = (list(map(fun, self.image)), functools.reduce(acc_fun, self.image, acc))
return self
- def max(self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Optional[Any] = None) -> Any:
+ def max(self: Self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Optional[Any] = None) -> Any:
"""
Return the maximum of the image as determined by the function `fun`.
@@ -712,7 +731,7 @@ def max(self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Option
"""
return (max(self.image, key=fun) if fun is not None else max(self.image)) if self.image else empty_fallback
- def min(self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Optional[Any] = None) -> Any:
+ def min(self: Self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Optional[Any] = None) -> Any:
"""
Return the minimum of the image as determined by the function `fun`.
@@ -729,7 +748,7 @@ def min(self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Option
"""
return (min(self.image, key=fun) if fun is not None else min(self.image)) if self.image else empty_fallback
- def min_max(self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Optional[Any] = None) -> Tuple[Any, Any]:
+ def min_max(self: Self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Optional[Any] = None) -> Annotated[Tuple[Any], 2]:
"""
Return a tuple with the minimal and the maximal elements in the image.
@@ -744,7 +763,7 @@ def min_max(self, fun: Optional[Callable[[Any], Any]] = None, empty_fallback: Op
"""
return (self.min(fun, empty_fallback), self.max(fun, empty_fallback))
- def product(self) -> Union[float, int, complex]:
+ def product(self: Self) -> (float | int | complex):
"""
Return the product of all elements.
@@ -779,7 +798,7 @@ def randint(a: int, b: int, size: int, secure: Optional[bool] = False) -> Iter:
"""
return Iter(Iter.__randint(a, b, size, secure))
- def random(self) -> Any:
+ def random(self: Self) -> Any:
"""
Return a random element from the image.
@@ -833,12 +852,12 @@ def __range(a: float, b: float, step: float) -> Generator[float]:
yield float(a2 + (i * n2))
@staticmethod
- def range(a: Union[int, float], b: Union[int, float], step: Optional[int | float] = None) -> Iter:
+ def range(a: int | float, b: int | float, step: Optional[int | float] = None) -> Iter:
if step == 0: raise ValueError("step must not be zero")
tmp = step or 1
return Iter(list(range(a, b + Functions.sign(tmp), tmp) if isinstance(a, int) else Iter.__range(a, b, step or 0.1)))
- def reduce(self, fun: Callable[[Any, Any], Any], acc: int = 0) -> Any:
+ def reduce(self: Self, fun: Callable[[Any, Any], Any], acc: int = 0) -> Any:
"""
Invoke `fun` for each element in the image with the accumulator. The
accumulator defaults to `0` if not otherwise specified. Reduce (sometimes
@@ -853,7 +872,7 @@ def reduce(self, fun: Callable[[Any, Any], Any], acc: int = 0) -> Any:
"""
return functools.reduce(fun, self.image, acc)
- def reduce_while(self, fun: Callable[[Any, Any], Tuple[bool, Any]], acc: int = 0) -> Any:
+ def reduce_while(self: Self, fun: Callable[[Any, Any], Tuple[bool, Any]], acc: int = 0) -> Any:
"""
Reduce the image until `fun` returns `(False, acc)`.
@@ -866,7 +885,7 @@ def reduce_while(self, fun: Callable[[Any, Any], Tuple[bool, Any]], acc: int = 0
"""
return functools.reduce(lambda acc, x: fun(x, acc)[1], filter(lambda x: fun(x, acc)[0], self.image), acc)
- def reject(self, fun: Callable[[Any], bool]) -> Iter:
+ def reject(self: Self, fun: Callable[[Any], bool]) -> Self:
"""
Return a list of elements in the image excluding those for which the
function `fun` returns a truthy value.
@@ -880,7 +899,7 @@ def reject(self, fun: Callable[[Any], bool]) -> Iter:
return self
@overload
- def reverse(self) -> Iter:
+ def reverse(self: Self) -> Self:
"""
Return a list of elements in the image in reverse order.
@@ -892,7 +911,7 @@ def reverse(self) -> Iter:
...
@overload
- def reverse(self, tail: Optional[List] = None) -> Iter:
+ def reverse(self: Self, tail: Optional[List] = None) -> Self:
"""
Reverse the elements in the image, appends the `tail`, and returns it
as a list.
@@ -904,12 +923,12 @@ def reverse(self, tail: Optional[List] = None) -> Iter:
"""
...
- def reverse(self, tail: Optional[List] = None) -> Iter:
+ def reverse(self: Self, tail: Optional[List] = None) -> Self:
self.image = list(reversed(self.image))
if tail: self.image.extend(tail)
return self
- def reverse_slice(self, start_index: int, count: int) -> Iter:
+ def reverse_slice(self: Self, start_index: int, count: int) -> Self:
"""
Reverse the image in the range from initial `start_index` through `count`
elements. If `count` is greater than the size of the rest of the image,
@@ -927,7 +946,7 @@ def reverse_slice(self, start_index: int, count: int) -> Iter:
self.image = [*self.image[:start_index], *reversed(self.image[start_index:count+start_index]), *self.image[count+start_index:]]
return self
- def scan(self, fun: Callable[[Any, Any], Any], acc: Optional[int] = None) -> Iter:
+ def scan(self: Self, fun: Callable[[Any, Any], Any], acc: Optional[int] = None) -> Self:
"""
Apply the given function `fun` to each element in the image, storing the result
in a list and passing it as the accumulator for the next computation. Uses
@@ -945,7 +964,7 @@ def scan(self, fun: Callable[[Any, Any], Any], acc: Optional[int] = None) -> Ite
self.image = list(map(lambda x: x + acc, itertools.accumulate(self.image, fun)))
return self
- def shorten(self, width: int = 20) -> str:
+ def shorten(self: Self, width: int = 20) -> str:
"""
Shorten an iterable sequence into a short, human-readable string.
@@ -958,7 +977,7 @@ def shorten(self, width: int = 20) -> str:
"""
return textwrap.shorten(str(self.image), width=width, placeholder=' ...]')
- def shuffle(self) -> Iter:
+ def shuffle(self: Self) -> Self:
"""
Return a list with the elements of the image shuffled.
@@ -971,7 +990,7 @@ def shuffle(self) -> Iter:
return self
@overload
- def slice(self, index: List[int]) -> Iter:
+ def slice(self: Self, index: List[int]) -> Self:
"""
Return a subset list of the image by `index`.
@@ -994,7 +1013,7 @@ def slice(self, index: List[int]) -> Iter:
...
@overload
- def slice(self, index: int, amount: Optional[int] = None) -> Iter:
+ def slice(self: Self, index: int, amount: Optional[int] = None) -> Self:
"""
Return a subset list of the image, from `index` (zero-based) with `amount`
number of elements if available. Given the image, it drops elements right
@@ -1013,7 +1032,7 @@ def slice(self, index: int, amount: Optional[int] = None) -> Iter:
"""
...
- def slice(self, index: int | List[int], amount: Optional[int] = None) -> Iter:
+ def slice(self: Self, index: int | List[int], amount: Optional[int] = None) -> Self:
if isinstance(index, List):
self.image = self.image[index[0]:] if index[1] == -1 else self.image[index[0]:index[1]+1]
else:
@@ -1021,7 +1040,7 @@ def slice(self, index: int | List[int], amount: Optional[int] = None) -> Iter:
return self
@overload
- def slide(self, index: int, insertion_index: int) -> Iter:
+ def slide(self: Self, index: int, insertion_index: int) -> Self:
"""
Slide a single or multiple elements given by `index` from the image to
`insertion_index`. The semantics of the range to be moved match the semantics
@@ -1041,9 +1060,9 @@ def slide(self, index: int, insertion_index: int) -> Iter:
...
@overload
- def slide(self, index: List[int], insertion_index: int) -> Iter: ...
+ def slide(self: Self, index: List[int], insertion_index: int) -> Self: ...
- def slide(self, index: int | List[int], insertion_index: int) -> Iter:
+ def slide(self: Self, index: int | List[int], insertion_index: int) -> Self:
if isinstance(index, List):
if (max(index) + len(self.image) if max(index) < 0 else max(index)) > insertion_index:
p1 = self.image[:insertion_index]
@@ -1062,7 +1081,7 @@ def slide(self, index: int | List[int], insertion_index: int) -> Iter:
self.image.insert((ii, ii+1)[ii < 0], element) if ii != -1 else self.image.append(element)
return self
- def sort(self, fun: Optional[Callable[[Any], bool]] = None, descending: bool = False) -> Iter:
+ def sort(self: Self, fun: Optional[Callable[[Any], bool]] = None, descending: bool = False) -> Self:
"""
Return a new sorted image. `fun` specifies a function of one argument that
is used to extract a comparison key from each element in iterable (for example,
@@ -1080,7 +1099,7 @@ def sort(self, fun: Optional[Callable[[Any], bool]] = None, descending: bool = F
self.image = sorted(self.image, key=fun, reverse=descending)
return self
- def split(self, count: int) -> Iter:
+ def split(self: Self, count: int) -> Self:
"""
Split the image into two lists, leaving `count` elements in the first
one. If `count` is a negative number, it starts counting from the back to
@@ -1100,7 +1119,7 @@ def split(self, count: int) -> Iter:
self.image = [self.image[:count], self.image[count:]]
return self
- def split_while(self, fun: Callable[[Any], bool]) -> Iter:
+ def split_while(self: Self, fun: Callable[[Any], bool]) -> Self:
"""
Split the image in two at the position of the element for which `fun`
returns a falsy value for the first time.
@@ -1124,7 +1143,7 @@ def split_while(self, fun: Callable[[Any], bool]) -> Iter:
return self
@overload
- def split_with(self, fun: Callable[[Any], bool]) -> Iter:
+ def split_with(self: Self, fun: Callable[[Any], bool]) -> Self:
"""
Split the image in two lists according to the given function `fun`.
@@ -1151,9 +1170,9 @@ def split_with(self, fun: Callable[[Any], bool]) -> Iter:
...
@overload
- def split_with(self, fun: Callable[[Any, Any], bool]) -> Iter: ...
+ def split_with(self: Self, fun: Callable[[Any, Any], bool]) -> Self: ...
- def split_with(self, fun: Callable[[Any], bool] | Callable[[Any, Any], bool]) -> Iter:
+ def split_with(self: Self, fun: Callable[[Any], bool] | Callable[[Any, Any], bool]) -> Self:
if isinstance(self.image, Dict):
f1 = ChainMap(*[{k: v} for k, v in self.image.items() if fun(k, v)])
f2 = ChainMap(*[{k: v} for k, v in self.image.items() if not fun(k, v)])
@@ -1163,7 +1182,7 @@ def split_with(self, fun: Callable[[Any], bool] | Callable[[Any, Any], bool]) ->
self.image = [list(filter(fun, t1)), list(itertools.filterfalse(fun, t2))]
return self
- def sum(self) -> Union[int, float, complex, str]:
+ def sum(self: Self) -> Any:
"""
Return the sum of all elements.
@@ -1174,7 +1193,7 @@ def sum(self) -> Union[int, float, complex, str]:
"""
return sum(self.image)
- def symmetric_difference(self, iter_: Iterable) -> Iter:
+ def symmetric_difference(self: Self, iter_: Iterable) -> Self:
"""
Return the symmetric difference between the image and `iter_`.
@@ -1186,7 +1205,7 @@ def symmetric_difference(self, iter_: Iterable) -> Iter:
self.image = list(set(self.image).symmetric_difference(iter_))
return self
- def take(self, amount: int) -> Iter:
+ def take(self: Self, amount: int) -> Self:
"""
Takes an `amount` of elements from the beginning or the end of the image.
If a positive `amount` is given, it takes the amount elements from the
@@ -1209,7 +1228,7 @@ def take(self, amount: int) -> Iter:
self.image = list(itertools.islice(self.image if amount > 0 else reversed(self.image), abs(amount)))
return self
- def take_every(self, nth: int) -> Iter:
+ def take_every(self: Self, nth: int) -> Self:
"""
Return a list of every `nth` element in the image, starting with the
first element. The first element is always included, unless `nth` is `0`.
@@ -1228,7 +1247,7 @@ def take_every(self, nth: int) -> Iter:
self.image = list(itertools.islice(self.image, 0, len(self.image), nth)) if nth != 0 else []
return self
- def take_random(self, count: int) -> Iter:
+ def take_random(self: Self, count: int) -> Self:
"""
Take `count` random elements from the image.
@@ -1242,7 +1261,7 @@ def take_random(self, count: int) -> Iter:
self.image = random.choices(self.image, k=count)
return self
- def take_while(self, fun: Callable[[Any], bool]) -> Iter:
+ def take_while(self: Self, fun: Callable[[Any], bool]) -> Self:
"""
Take the elements from the beginning of the image while `fun` returns
a truthy value.
@@ -1255,13 +1274,13 @@ def take_while(self, fun: Callable[[Any], bool]) -> Iter:
self.image = list(itertools.takewhile(fun, self.image))
return self
- def to_list(self) -> List[Any]:
+ def to_list(self: Self) -> List[Any]:
"""
Return the image as a list object.
"""
return list(self.image)
- def transpose(self, fillvalue: Optional[Any] = None) -> Iter:
+ def transpose(self: Self, fillvalue: Optional[Any] = None) -> Self:
"""
Transpose the image. When the shorter iterables are exhausted, the `fillvalue`
is substituted in their place.
@@ -1276,7 +1295,7 @@ def transpose(self, fillvalue: Optional[Any] = None) -> Iter:
self.image = list(itertools.zip_longest(*self.image, fillvalue=fillvalue))
return self
- def union(self, iter_: Iterable) -> Iter:
+ def union(self: Self, iter_: Iterable) -> Self:
"""
Return the union between the image and `iter_`, sorted in ascending order.
@@ -1290,7 +1309,7 @@ def union(self, iter_: Iterable) -> Iter:
self.image = sorted(set(self.image) | set(iter_))
return self
- def unique(self) -> Iter:
+ def unique(self: Self) -> Self:
"""
Enumerates the image, removing all duplicated elements.
@@ -1302,7 +1321,7 @@ def unique(self) -> Iter:
self.image = list(Counter(self.image).keys())
return self
- def unzip(self) -> Iter:
+ def unzip(self: Self) -> Self:
"""
Opposite of `self.zip`. Extracts two-element tuples from the image and
groups them together.
@@ -1323,7 +1342,7 @@ def unzip(self) -> Iter:
return self
@overload
- def with_index(self, fun_or_offset: Optional[int] = None) -> Iter:
+ def with_index(self: Self, fun_or_offset: Optional[int] = None) -> Self:
"""
Return the image with each element wrapped in a tuple alongside its index.
May receive a function or an integer offset. If an offset is given, it will
@@ -1343,9 +1362,9 @@ def with_index(self, fun_or_offset: Optional[int] = None) -> Iter:
...
@overload
- def with_index(self, fun_or_offset: Callable[[Any, Any], Any]) -> Iter: ...
+ def with_index(self: Self, fun_or_offset: Callable[[Any, Any], Any]) -> Self: ...
- def with_index(self, fun_or_offset: Optional[int] | Callable[[Any, Any], Any] = None) -> Iter:
+ def with_index(self: Self, fun_or_offset: Optional[int] | Callable[[Any, Any], Any] = None) -> Self:
if isinstance(fun_or_offset, int) or fun_or_offset is None:
offset = 0 if fun_or_offset is None else fun_or_offset
self.image = list(zip(self.image, range(offset, len(self.image)+offset)))
@@ -1353,7 +1372,7 @@ def with_index(self, fun_or_offset: Optional[int] | Callable[[Any, Any], Any] =
self.image = list(itertools.starmap(fun_or_offset, zip(self.image, range(len(self.image)))))
return self
- def zip(self, *iter_: Iterable) -> Iter:
+ def zip(self: Self, *iter_: Tuple[Iterable, ...]) -> Self:
"""
Zip corresponding elements from a finite collection of iterables into a
list of tuples. The zipping finishes as soon as any iterable in the given
@@ -1372,7 +1391,7 @@ def zip(self, *iter_: Iterable) -> Iter:
return self
@overload
- def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any]) -> Iter:
+ def zip_reduce(self: Self, acc: List, reducer: Callable[[Any, Any], Any]) -> Self:
"""
Reduce over all of the image, halting as soon as any iterable is empty.
The reducer will receive 2 args: a list of elements (one from each enum) and
@@ -1386,7 +1405,7 @@ def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any]) -> Iter:
...
@overload
- def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Iterable, right: Iterable) -> Iter:
+ def zip_reduce(self: Self, acc: List, reducer: Callable[[Any, Any], Any], left: Iterable, right: Iterable) -> Self:
"""
Reduce over two iterables halting as soon as either iterable is empty.
@@ -1397,7 +1416,7 @@ def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Iterab
"""
...
- def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Optional[Iterable] = None, right: Optional[Iterable] = None) -> Iter:
+ def zip_reduce(self: Self, acc: List, reducer: Callable[[Any, Any], Any], left: Optional[Iterable] = None, right: Optional[Iterable] = None) -> Self:
if left is not None and right is not None:
# reference implementation:
# https://hexdocs.pm/elixir/1.12/Enum.html#zip_reduce/3
@@ -1406,7 +1425,7 @@ def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Option
self.image = list(functools.reduce(reducer, zip(*self.image), acc))
return self
- def zip_with(self, fun: Callable[..., Any], *iterable: Iterable) -> Iter:
+ def zip_with(self: Self, fun: Callable[..., Any], *iterable: Tuple[Iterable, ...]) -> Self:
"""
Zip corresponding elements from a finite collection of iterables into a list,
transforming them with the `fun` function as it goes. The first element from
@@ -1430,25 +1449,25 @@ def zip_with(self, fun: Callable[..., Any], *iterable: Iterable) -> Iter:
#region magic methods
- def __eq__(self, other: Iter) -> bool:
+ def __eq__(self: Self, other: Iter) -> bool:
return self.image == other.image
- def __ne__(self, other: Iter) -> bool:
+ def __ne__(self: Self, other: Iter) -> bool:
return self.image != other.image
- def __iter__(self) -> Iter:
+ def __iter__(self: Self) -> Iterator[Iter]:
return self
- def __next__(self) -> Any:
+ def __next__(self: Self) -> Any:
if self.__index >= len(self.image): raise StopIteration
self.__value = self.image[self.__index]
self.__index += 1
return self.__value
- def __repr__(self) -> str:
+ def __repr__(self: Self) -> str:
return f"Iter(domain={Iter(self.domain).shorten()},image={self.shorten()})"
- def __str__(self) -> str:
+ def __str__(self: Self) -> str:
return ','.join(map(str, self.image))
#endregion
From 8370600e55007cff3e0e2e95bedd2e85526e83db Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Thu, 12 Jan 2023 22:02:36 +0100
Subject: [PATCH 27/46] Add typing_extensions for advanced type hints as prod
dependency
---
requirements/release.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/requirements/release.txt b/requirements/release.txt
index e69de29..7849d5d 100644
--- a/requirements/release.txt
+++ b/requirements/release.txt
@@ -0,0 +1 @@
+typing_extensions==4.4.0
From 117e48fa9db361791c353fbc5bee47fc8de56006 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Thu, 12 Jan 2023 22:03:10 +0100
Subject: [PATCH 28/46] Add unit tests for cartesian method
---
tests/test_iterfun.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index b218e8e..1c8a876 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -35,6 +35,14 @@ def test_at_throws(self):
def test_avg(self):
self.assertEqual(5, Iter([0, 10]).avg())
+ def test_cartesian(self):
+ expected = [(True, True), (True, False), (False, True), (False, False)]
+ self.assertEqual(expected, Iter([True, False]).cartesian(repeat=2).image)
+
+ domain = [(1, 2), ('a', 'b')]
+ self.assertEqual([(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')], Iter(domain).cartesian().image)
+ self.assertEqual([(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')], Iter(domain).cartesian(repeat=1).image)
+
def test_chunk_by(self):
self.assertEqual([[1], [2, 2], [3], [4, 4, 6], [7, 7]], Iter([1, 2, 2, 3, 4, 4, 6, 7, 7]).chunk_by(lambda x: x % 2 == 1).image)
self.assertEqual([['a', 'b'], ['c', 'd'], ['e', 'f']], Iter(['a', 'b', '1', 'c', 'd', '2', 'e', 'f']).chunk_by(lambda x: x.isdigit(), eject=True).image)
From ce1aa2f2f49d9ed6814c97d5184f70f787f2b532 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Thu, 12 Jan 2023 22:36:41 +0100
Subject: [PATCH 29/46] Improve cartesian method and add more methods for
probability theory
* implement combinations, combinations_with_replacement and permutations
based on the itertools library
* return self in cartesian method instead of creating a new object
---
src/iterfun/iterfun.py | 48 ++++++++++++++++++++++++++++++++++++++++--
1 file changed, 46 insertions(+), 2 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index dd8caeb..9f184fa 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -132,7 +132,7 @@ def avg(self: Self) -> (int | float):
"""
return statistics.mean(self.image)
- def cartesian(self: Self, repeat: int = 1) -> Iter:
+ def cartesian(self: Self, repeat: int = 1) -> Self:
"""
Return the cartesian product, i.e. `A x B = {(a, b) | a ∈ A ^ b ∈ B}`.
@@ -144,7 +144,8 @@ def cartesian(self: Self, repeat: int = 1) -> Iter:
```
"""
nested = any(isinstance(i, Iterable) for i in self.image)
- return Iter(list(itertools.product(self.image, repeat=repeat) if not nested else itertools.product(*self.image, repeat=repeat)))
+ self.image = list(itertools.product(*self.image, repeat=repeat) if nested else itertools.product(self.image, repeat=repeat))
+ return self
def chunk_by(self: Self, fun: Callable[[Any], bool], eject: bool = False) -> Self:
"""
@@ -187,6 +188,35 @@ def chunk_while(self: Self, acc: List, chunk_fun: Callable, chunk_after: Callabl
# https://hexdocs.pm/elixir/1.12/Enum.html#chunk_while/4
raise NotImplementedError()
+ def combinations(self: Self, r: int) -> Self:
+ """
+ Return successive r-length combinations of elements in the iterable.
+
+ ```python
+ >>> Iter([1, 2, 3]).combinations(2)
+ [(1, 2), (1, 3), (2, 3)]
+ >>> Iter.range(1, 3).combinations(r=3)
+ [(1, 2, 3)]
+ ```
+ """
+ self.image = list(itertools.combinations(self.image, r=r))
+ return self
+
+ def combinations_width_replacement(self: Self, r: int) -> Self:
+ """
+ Return successive r-length combinations of elements in the iterable
+ allowing individual elements to have successive repeats.
+
+ ```python
+ >>> Iter([1, 2, 3]).combinations_width_replacement(2)
+ [(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)]
+ >>> Iter.range(1, 3).combinations_width_replacement(r=3)
+ [(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 2, 2), (1, 2, 3), (1, 3, 3), (2, 2, 2), (2, 2, 3), (2, 3, 3), (3, 3, 3)]
+ ```
+ """
+ self.image = list(itertools.combinations_with_replacement(self.image, r=r))
+ return self
+
def count(self: Self, fun: Optional[Callable[[Any], bool]] = None) -> int:
"""
Return the size of the image if `fun` is `None`, else return the cardinality
@@ -763,6 +793,20 @@ def min_max(self: Self, fun: Optional[Callable[[Any], Any]] = None, empty_fallba
"""
return (self.min(fun, empty_fallback), self.max(fun, empty_fallback))
+ def permutations(self: Self, r: Optional[int] = None) -> Self:
+ """
+ Return successive r-length permutations of elements in the iterable.
+
+ ```python
+ >>> Iter([1, 2, 3]).permutations()
+ [(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]
+ >>> Iter.range(1, 3).permutations(r=2)
+ [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
+ ```
+ """
+ self.image = list(itertools.permutations(self.image, r=r))
+ return self
+
def product(self: Self) -> (float | int | complex):
"""
Return the product of all elements.
From 0bfbc8ad5874f4ab572a8ad5b3cd329de01d432b Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Thu, 12 Jan 2023 22:38:41 +0100
Subject: [PATCH 30/46] Increase test coverage
---
tests/test_iterfun.py | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index 1c8a876..33fc94e 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -38,7 +38,6 @@ def test_avg(self):
def test_cartesian(self):
expected = [(True, True), (True, False), (False, True), (False, False)]
self.assertEqual(expected, Iter([True, False]).cartesian(repeat=2).image)
-
domain = [(1, 2), ('a', 'b')]
self.assertEqual([(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')], Iter(domain).cartesian().image)
self.assertEqual([(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')], Iter(domain).cartesian(repeat=1).image)
@@ -59,6 +58,15 @@ def test_chunk_every(self):
def test_chunk_while(self):
self.assertEqual([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], Iter([1, 10]).chunk_while([], None, None))
+ def test_combinations(self):
+ self.assertEqual([(1, 2), (1, 3), (2, 3)], Iter([1, 2, 3]).combinations(2).image)
+ self.assertEqual([(1, 2, 3)], Iter.range(1, 3).combinations(r=3).image)
+
+ def test_combinations_width_replacement(self):
+ self.assertEqual([(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)], Iter([1, 2, 3]).combinations_width_replacement(2).image)
+ expected = [(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 2, 2), (1, 2, 3), (1, 3, 3), (2, 2, 2), (2, 2, 3), (2, 3, 3), (3, 3, 3)]
+ self.assertEqual(expected, Iter.range(1, 3).combinations_width_replacement(r=3).image)
+
def test_count(self):
self.assertEqual(3, Iter([1, 2, 3]).count())
self.assertEqual(2, Iter.range(1, 5).count(lambda x: x % 2 == 0))
@@ -232,6 +240,10 @@ def test_min_max(self):
self.assertEqual((None, None), Iter([]).min_max(empty_fallback=None))
self.assertEqual(('a', 'aaa'), Iter(["aaa", "a", "bb", "c", "ccc"]).min_max(len))
+ def test_permutations(self):
+ self.assertEqual([(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)], Iter([1, 2, 3]).permutations().image)
+ self.assertEqual([(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)], Iter.range(1, 3).permutations(r=2).image)
+
def test_product(self):
self.assertEqual(24, Iter([2, 3, 4]).product())
self.assertEqual(24.0, Iter([2.0, 3.0, 4.0]).product())
From 377b82e286b47f5f78866e7ae548b5175170e0d5 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Thu, 12 Jan 2023 23:07:07 +0100
Subject: [PATCH 31/46] Implement is_prime method, edit doc strings and expand
test suite
---
src/iterfun/iterfun.py | 24 ++++++++++++++++++++++++
tests/test_functions.py | 35 +++++++++++++++++++++++++++++++++++
2 files changed, 59 insertions(+)
create mode 100644 tests/test_functions.py
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 9f184fa..45b31ba 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -4,6 +4,7 @@
import functools
import itertools
+import math
import operator
import random
import secrets
@@ -38,6 +39,21 @@ def is_odd(x: int | float) -> bool:
"""
return x % 2 != 0
+ def is_prime(x: int) -> bool:
+ """
+ Implements the Sieve of Eratosthenes check to test whether the number `x`
+ is prime or not.
+ """
+ if x <= 1: return False
+
+ i = 2
+ while math.pow(i, 2) <= x:
+ if x % i == 0:
+ return False
+ i += 1
+
+ return True
+
def sign(x: int | float) -> Literal[-1, 0, 1]:
"""
Extract the sign of a real number.
@@ -56,8 +72,16 @@ class Iter:
### Example
```python
+ >>> from iterfun import Iter
+ >>> from iterfun import Functions as fun
+ >>>
+ >>> # map over a collection
>>> Iter.range(1, 3).map(lambda x: 2*x).sum()
12
+ >>>
+ >>> # use helper methods from the Functions class
+ >>> Iter.range(10).filter(fun.is_prime).join('-')
+ '2-3-5-7'
```
"""
diff --git a/tests/test_functions.py b/tests/test_functions.py
new file mode 100644
index 0000000..d724914
--- /dev/null
+++ b/tests/test_functions.py
@@ -0,0 +1,35 @@
+import unittest
+
+from src.iterfun import Functions as fun
+
+
+class TestFunctions(unittest.TestCase):
+ def test_is_prime(self):
+ self.assertFalse(fun.is_prime(0))
+ self.assertFalse(fun.is_prime(1))
+ self.assertFalse(fun.is_prime(4))
+ self.assertFalse(fun.is_prime(100))
+ self.assertFalse(fun.is_prime(32472874282))
+ self.assertFalse(fun.is_prime(2032320))
+ self.assertFalse(fun.is_prime(9000600))
+ self.assertTrue(fun.is_prime(2))
+ self.assertTrue(fun.is_prime(3))
+ self.assertTrue(fun.is_prime(5))
+ self.assertTrue(fun.is_prime(7))
+ self.assertTrue(fun.is_prime(43))
+ self.assertTrue(fun.is_prime(59))
+ self.assertTrue(fun.is_prime(83))
+ self.assertTrue(fun.is_prime(97))
+ self.assertTrue(fun.is_prime(2237))
+ self.assertTrue(fun.is_prime(69899))
+ self.assertTrue(fun.is_prime(363047))
+ self.assertTrue(fun.is_prime(659713))
+ self.assertTrue(fun.is_prime(2427281))
+ self.assertTrue(fun.is_prime(81239489))
+ self.assertTrue(fun.is_prime(81247169))
+ self.assertTrue(fun.is_prime(24244807))
+ self.assertTrue(fun.is_prime(3846589051))
+ self.assertTrue(fun.is_prime(5224245449))
+ self.assertTrue(fun.is_prime(798723477701))
+ self.assertTrue(fun.is_prime(100005743))
+
From 4619d06a46166859f657fb0b2f8ba8b19ece9e0b Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Fri, 13 Jan 2023 11:49:10 +0100
Subject: [PATCH 32/46] Add secure parameter to random method
---
src/iterfun/iterfun.py | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 45b31ba..0bddfd5 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -866,9 +866,12 @@ def randint(a: int, b: int, size: int, secure: Optional[bool] = False) -> Iter:
"""
return Iter(Iter.__randint(a, b, size, secure))
- def random(self: Self) -> Any:
+ def random(self: Self, secure: Optional[bool] = False) -> Any:
"""
- Return a random element from the image.
+ Return a random element from the image. Uses a pseudo-random number
+ generator (PRNG) by default, which can be turned off by setting `secure`
+ to `True` which falls back to the cryptographically secure `secrets` module
+ that runs slower in comparison.
```python
>>> Iter.range(1, 100).random()
@@ -877,7 +880,7 @@ def random(self: Self) -> Any:
69
```
"""
- return random.choice(self.image)
+ return secrets.choice(self.image) if secure else random.choice(self.image)
@overload
@staticmethod
From cca44febbda917378e05d4b73b1d6798f93a9c8b Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Fri, 13 Jan 2023 13:41:15 +0100
Subject: [PATCH 33/46] Add file enum and partially implement open method
* implement open method for File.TEXT, File.CSV and File.JSON
(missing case: File.IMG)
* change implementation of __str__ and __repr__
---
src/iterfun/__init__.py | 2 +-
src/iterfun/iterfun.py | 119 ++++++++++++++++++++++++++++++++++++++--
2 files changed, 114 insertions(+), 7 deletions(-)
diff --git a/src/iterfun/__init__.py b/src/iterfun/__init__.py
index 25efa6c..7c9bac0 100644
--- a/src/iterfun/__init__.py
+++ b/src/iterfun/__init__.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
-from .iterfun import Functions, Iter
+from .iterfun import Functions, Iter, File
__version__ = "0.0.5"
package_name = "iterfun"
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 0bddfd5..4aaeaaa 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -2,8 +2,10 @@
from __future__ import annotations
+import csv
import functools
import itertools
+import json
import math
import operator
import random
@@ -11,15 +13,38 @@
import statistics
import sys
import textwrap
+import pprint
from collections import ChainMap, Counter
from decimal import ROUND_HALF_UP, Decimal
-from typing import Any, Callable, Dict, Final, Generator, Iterable, Iterator, List, Literal, Optional, Tuple, overload
+from enum import Enum, unique
+from pathlib import Path
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Final,
+ Generator,
+ Iterable,
+ Iterator,
+ List,
+ Literal,
+ Optional,
+ Tuple,
+ overload
+)
if sys.version_info >= (3, 11):
from typing import Annotated, Self
else:
from typing_extensions import Annotated, Self
+@unique
+class File(Enum):
+ TEXT = 1
+ CSV = 2
+ JSON = 3
+ IMG = 4
+
class Functions:
def invert(x: int | float) -> (int | float):
"""
@@ -210,7 +235,7 @@ def chunk_every(self: Self, count: int, step: Optional[int] = None, leftover: Op
def chunk_while(self: Self, acc: List, chunk_fun: Callable, chunk_after: Callable) -> Self:
# reference implementation:
# https://hexdocs.pm/elixir/1.12/Enum.html#chunk_while/4
- raise NotImplementedError()
+ raise NotImplementedError("TODO")
def combinations(self: Self, r: int) -> Self:
"""
@@ -470,7 +495,7 @@ def flat_map(self: Self, fun: Callable[[Any], Any]) -> Self:
def flat_map_reduce(self: Self, acc: int, fun: Callable[[Any, Any], Any]) -> Self:
# reference implementation:
# https://hexdocs.pm/elixir/1.12/Enum.html#flat_map_reduce/3
- raise NotImplementedError()
+ raise NotImplementedError("TODO")
def flatten(self: Self) -> Self:
"""
@@ -817,6 +842,88 @@ def min_max(self: Self, fun: Optional[Callable[[Any], Any]] = None, empty_fallba
"""
return (self.min(fun, empty_fallback), self.max(fun, empty_fallback))
+ @overload
+ @staticmethod
+ def open(path: str | Path, encoding: str = "utf-8", strip: bool = True, file: File = File.TEXT) -> Iter:
+ """
+ Read a plain text file and store its content as a list in the image. This
+ methods `file` keyword argument defaults to `File.TEXT`. Set `strip` to
+ `False` to disable LF and whitespace trimming (on by default).
+
+ ```python
+ >>> Iter.open("people.txt", encoding="cp1252")
+ ["Stefan", "Monika", "Anneliese"]
+ ```
+ """
+ ...
+
+ @overload
+ @staticmethod
+ def open(path: str | Path, encoding: str = "utf-8", delimiter: str = ",", skip_header: bool = False, file: File = File.CSV) -> Iter:
+ """
+ Deserialize a CSV file and store its content as a nested list in the image.
+ Pass `File.JSON` to the `file` keyword argument to read a file with the
+ `csv` module.
+
+ ```python
+ >>> from iterfun import File
+ >>> Iter.open("data.csv", skip_header=True, file=File.CSV)
+ [['Stefan', '27'], ['Monika', '28'], ['Anneliese', '33']]
+ ```
+ """
+ ...
+
+ @overload
+ @staticmethod
+ def open(path: str | Path, encoding: str = "utf-8", file: File = File.JSON) -> Iter:
+ """
+ Deserialize a JSON file and store its content as a dictionary in the image.
+ Pass `File.JSON` to the `file` keyword argument to read a file with the
+ `json` module.
+
+ ```python
+ >>> from iterfun import File
+ >>> Iter.open("people.json", file=File.JSON)
+ {'People': [{'Name': 'Stefan', 'Age': 27}, {'Name': 'Monika', 'Age': 28}, {'Name': 'Anneliese', 'Age': 33}]}
+ ```
+ """
+ ...
+
+ @overload
+ @staticmethod
+ def open(path: str | Path, file: File = File.IMG) -> Iter:
+ ...
+
+ @staticmethod
+ def open(
+ path: str | Path,
+ encoding: str = "utf-8",
+ strip: bool = True,
+ delimiter: str = ",",
+ skip_header: bool = False,
+ file: File = File.TEXT) -> Iter:
+ """
+ ```python
+ >>> Iter.open("test.txt")
+ ['Hello', 'World']
+ ```
+ """
+ if file == File.CSV:
+ with open(path, mode='r', encoding=encoding) as csv_handler:
+ data = csv.reader(csv_handler, delimiter=delimiter, dialect=csv.excel)
+ if skip_header: next(data)
+ return Iter([row for row in data])
+ elif file == File.JSON:
+ with open(path, mode='r', encoding=encoding) as json_handler:
+ data = json.load(json_handler)
+ return Iter(data)
+ elif file == File.IMG:
+ raise NotImplementedError("TODO")
+ else:
+ with open(path, mode='r', encoding=encoding) as text_handler:
+ data = text_handler.readlines()
+ return Iter([line.rstrip('\n').strip() if strip else line for line in data])
+
def permutations(self: Self, r: Optional[int] = None) -> Self:
"""
Return successive r-length permutations of elements in the iterable.
@@ -1491,7 +1598,7 @@ def zip_reduce(self: Self, acc: List, reducer: Callable[[Any, Any], Any], left:
if left is not None and right is not None:
# reference implementation:
# https://hexdocs.pm/elixir/1.12/Enum.html#zip_reduce/3
- raise NotImplementedError()
+ raise NotImplementedError("TODO")
else:
self.image = list(functools.reduce(reducer, zip(*self.image), acc))
return self
@@ -1536,9 +1643,9 @@ def __next__(self: Self) -> Any:
return self.__value
def __repr__(self: Self) -> str:
- return f"Iter(domain={Iter(self.domain).shorten()},image={self.shorten()})"
+ return f"Iter(image_type={self.image.__class__.__name__},len={len(self.image)})"
def __str__(self: Self) -> str:
- return ','.join(map(str, self.image))
+ return repr(self.image) if isinstance(self.image, Dict) else str(self.image)
#endregion
From da4768e53f64121186694a156c296d58d841c4a3 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Fri, 13 Jan 2023 13:46:09 +0100
Subject: [PATCH 34/46] Rename File.IMG to File.IMAGE and update test suite for
__str__ and __repr__
---
src/iterfun/iterfun.py | 6 +++---
tests/test_iterfun.py | 7 +++----
2 files changed, 6 insertions(+), 7 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 4aaeaaa..6782aed 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -43,7 +43,7 @@ class File(Enum):
TEXT = 1
CSV = 2
JSON = 3
- IMG = 4
+ IMAGE = 4
class Functions:
def invert(x: int | float) -> (int | float):
@@ -891,7 +891,7 @@ def open(path: str | Path, encoding: str = "utf-8", file: File = File.JSON) -> I
@overload
@staticmethod
- def open(path: str | Path, file: File = File.IMG) -> Iter:
+ def open(path: str | Path, file: File = File.IMAGE) -> Iter:
...
@staticmethod
@@ -917,7 +917,7 @@ def open(
with open(path, mode='r', encoding=encoding) as json_handler:
data = json.load(json_handler)
return Iter(data)
- elif file == File.IMG:
+ elif file == File.IMAGE:
raise NotImplementedError("TODO")
else:
with open(path, mode='r', encoding=encoding) as text_handler:
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index 33fc94e..d9fe6f8 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -441,9 +441,8 @@ def test_next(self):
self.assertEqual(6, next(sequence))
def test_str(self):
- self.assertEqual("1,2,3,4,5", str(Iter.range(1, 5)))
- self.assertEqual("1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20", str(Iter.range(1, 20)))
+ self.assertEqual("[1, 2, 3, 4, 5]", str(Iter.range(1, 5)))
+ self.assertEqual("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]", str(Iter.range(1, 20)))
def test_repr(self):
- self.assertEqual("Iter(domain=[1, 2, 3, 4, 5],image=[1, 2, 3, 4, 5])", repr(Iter.range(1, 5)))
- self.assertEqual("Iter(domain=[1, 2, 3, 4, 5, ...],image=[1, 2, 3, 4, 5, ...])", repr(Iter.range(1, 50)))
+ self.assertEqual("Iter(image_type=list,len=5)", repr(Iter.range(1, 5)))
From b8b6c948ea048e29243362e3dfe0c4d16836e8a3 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Fri, 13 Jan 2023 13:48:42 +0100
Subject: [PATCH 35/46] Fix typo in function name
---
src/iterfun/iterfun.py | 2 +-
tests/test_iterfun.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 6782aed..da6bde6 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -251,7 +251,7 @@ def combinations(self: Self, r: int) -> Self:
self.image = list(itertools.combinations(self.image, r=r))
return self
- def combinations_width_replacement(self: Self, r: int) -> Self:
+ def combinations_with_replacement(self: Self, r: int) -> Self:
"""
Return successive r-length combinations of elements in the iterable
allowing individual elements to have successive repeats.
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index d9fe6f8..e38e384 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -63,9 +63,9 @@ def test_combinations(self):
self.assertEqual([(1, 2, 3)], Iter.range(1, 3).combinations(r=3).image)
def test_combinations_width_replacement(self):
- self.assertEqual([(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)], Iter([1, 2, 3]).combinations_width_replacement(2).image)
+ self.assertEqual([(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)], Iter([1, 2, 3]).combinations_with_replacement(2).image)
expected = [(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 2, 2), (1, 2, 3), (1, 3, 3), (2, 2, 2), (2, 2, 3), (2, 3, 3), (3, 3, 3)]
- self.assertEqual(expected, Iter.range(1, 3).combinations_width_replacement(r=3).image)
+ self.assertEqual(expected, Iter.range(1, 3).combinations_with_replacement(r=3).image)
def test_count(self):
self.assertEqual(3, Iter([1, 2, 3]).count())
From db41c5b26a977e3b619b31dc60d628b18f2f3586 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Thu, 19 Jan 2023 23:52:50 +0100
Subject: [PATCH 36/46] Implement save method and refactor code
* implement save method with the image case as TODO
* it's more pythonic to derive the file extension
from the provided file name rather than overloading
the method with an file enum
---
src/iterfun/__init__.py | 2 +-
src/iterfun/iterfun.py | 114 +++++++++++++++++++++++++++-------------
2 files changed, 78 insertions(+), 38 deletions(-)
diff --git a/src/iterfun/__init__.py b/src/iterfun/__init__.py
index 7c9bac0..25efa6c 100644
--- a/src/iterfun/__init__.py
+++ b/src/iterfun/__init__.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
-from .iterfun import Functions, Iter, File
+from .iterfun import Functions, Iter
__version__ = "0.0.5"
package_name = "iterfun"
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index da6bde6..3026f7b 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -8,15 +8,14 @@
import json
import math
import operator
+import os
import random
import secrets
import statistics
import sys
import textwrap
-import pprint
from collections import ChainMap, Counter
from decimal import ROUND_HALF_UP, Decimal
-from enum import Enum, unique
from pathlib import Path
from typing import (
Any,
@@ -38,13 +37,6 @@
else:
from typing_extensions import Annotated, Self
-@unique
-class File(Enum):
- TEXT = 1
- CSV = 2
- JSON = 3
- IMAGE = 4
-
class Functions:
def invert(x: int | float) -> (int | float):
"""
@@ -235,7 +227,7 @@ def chunk_every(self: Self, count: int, step: Optional[int] = None, leftover: Op
def chunk_while(self: Self, acc: List, chunk_fun: Callable, chunk_after: Callable) -> Self:
# reference implementation:
# https://hexdocs.pm/elixir/1.12/Enum.html#chunk_while/4
- raise NotImplementedError("TODO")
+ raise NotImplementedError("To be released in version 0.0.6")
def combinations(self: Self, r: int) -> Self:
"""
@@ -495,7 +487,7 @@ def flat_map(self: Self, fun: Callable[[Any], Any]) -> Self:
def flat_map_reduce(self: Self, acc: int, fun: Callable[[Any, Any], Any]) -> Self:
# reference implementation:
# https://hexdocs.pm/elixir/1.12/Enum.html#flat_map_reduce/3
- raise NotImplementedError("TODO")
+ raise NotImplementedError("To be released in version 0.0.6")
def flatten(self: Self) -> Self:
"""
@@ -844,7 +836,7 @@ def min_max(self: Self, fun: Optional[Callable[[Any], Any]] = None, empty_fallba
@overload
@staticmethod
- def open(path: str | Path, encoding: str = "utf-8", strip: bool = True, file: File = File.TEXT) -> Iter:
+ def open(path: str | Path, encoding: str = "utf-8", strip: bool = True) -> Iter:
"""
Read a plain text file and store its content as a list in the image. This
methods `file` keyword argument defaults to `File.TEXT`. Set `strip` to
@@ -859,15 +851,14 @@ def open(path: str | Path, encoding: str = "utf-8", strip: bool = True, file: Fi
@overload
@staticmethod
- def open(path: str | Path, encoding: str = "utf-8", delimiter: str = ",", skip_header: bool = False, file: File = File.CSV) -> Iter:
+ def open(path: str | Path, encoding: str = "utf-8", delimiter: str = ",", skip_header: bool = False) -> Iter:
"""
Deserialize a CSV file and store its content as a nested list in the image.
Pass `File.JSON` to the `file` keyword argument to read a file with the
`csv` module.
```python
- >>> from iterfun import File
- >>> Iter.open("data.csv", skip_header=True, file=File.CSV)
+ >>> Iter.open("data.csv", skip_header=True)
[['Stefan', '27'], ['Monika', '28'], ['Anneliese', '33']]
```
"""
@@ -875,15 +866,14 @@ def open(path: str | Path, encoding: str = "utf-8", delimiter: str = ",", skip_h
@overload
@staticmethod
- def open(path: str | Path, encoding: str = "utf-8", file: File = File.JSON) -> Iter:
+ def open(path: str | Path, encoding: str = "utf-8") -> Iter:
"""
Deserialize a JSON file and store its content as a dictionary in the image.
Pass `File.JSON` to the `file` keyword argument to read a file with the
`json` module.
```python
- >>> from iterfun import File
- >>> Iter.open("people.json", file=File.JSON)
+ >>> Iter.open("people.json")
{'People': [{'Name': 'Stefan', 'Age': 27}, {'Name': 'Monika', 'Age': 28}, {'Name': 'Anneliese', 'Age': 33}]}
```
"""
@@ -891,34 +881,27 @@ def open(path: str | Path, encoding: str = "utf-8", file: File = File.JSON) -> I
@overload
@staticmethod
- def open(path: str | Path, file: File = File.IMAGE) -> Iter:
+ def open(path: str | Path) -> Iter:
+ """
+ To be released in version 0.0.6
+ """
...
@staticmethod
- def open(
- path: str | Path,
- encoding: str = "utf-8",
- strip: bool = True,
- delimiter: str = ",",
- skip_header: bool = False,
- file: File = File.TEXT) -> Iter:
- """
- ```python
- >>> Iter.open("test.txt")
- ['Hello', 'World']
- ```
- """
- if file == File.CSV:
+ def open(path: str | Path, encoding: str = "utf-8", strip: bool = True, delimiter: str = ",", skip_header: bool = False) -> Iter:
+ extension = Path(path).suffix.lower()
+
+ if extension == ".csv":
with open(path, mode='r', encoding=encoding) as csv_handler:
data = csv.reader(csv_handler, delimiter=delimiter, dialect=csv.excel)
if skip_header: next(data)
return Iter([row for row in data])
- elif file == File.JSON:
+ elif extension == ".json":
with open(path, mode='r', encoding=encoding) as json_handler:
data = json.load(json_handler)
return Iter(data)
- elif file == File.IMAGE:
- raise NotImplementedError("TODO")
+ elif extension in [".png", ".jpg", ".jpeg"]:
+ raise NotImplementedError("To be released in version 0.0.6")
else:
with open(path, mode='r', encoding=encoding) as text_handler:
data = text_handler.readlines()
@@ -1124,6 +1107,63 @@ def reverse_slice(self: Self, start_index: int, count: int) -> Self:
self.image = [*self.image[:start_index], *reversed(self.image[start_index:count+start_index]), *self.image[count+start_index:]]
return self
+ @overload
+ def save(self: Self, path: str | Path, encoding: str = "utf-8", overwrite: bool = False) -> None:
+ """
+ Store all elements in the image with their string representation as a plain text file to disk.
+
+ ```python
+ >>> Iter.range(1, 10).map(lambda x: x**2).save("data.txt")
+ ```
+ """
+ ...
+
+ @overload
+ def save(self: Self, path: str | Path, encoding: str = "utf-8", delimiter: str = ",", overwrite: bool = False) -> None:
+ """
+ Store all elements in the image with their string representation as a CSV file to disk.
+
+ ```python
+ >>> Iter.range(1, 10).chunk_every(2).save("data.csv")
+ ```
+ """
+ ...
+
+ @overload
+ def save(self: Self, path: str | Path, encoding: str = "utf-8", indent: int = 4, sort_keys: bool = False, overwrite: bool = False) -> None:
+ """
+ Store all elements in the image as a JSON file to disk.
+
+ ```python
+ >>> Iter({'a': 1, 'b':2}).map(lambda k, v: {k: -v * 10}).save("data.json", indent=2)
+ ```
+ """
+ ...
+
+ @overload
+ def save(self: Self, path: str | Path, overwrite: bool = False) -> None:
+ """
+ To be released in version 0.0.6
+ """
+ ...
+
+ def save(
+ self: Self, path: str | Path, encoding: str = "utf-8", delimiter: str = ",", indent: int = 4, sort_keys: bool = False, overwrite: bool = False) -> None:
+ extension = Path(path).suffix.lower()
+ if overwrite: os.remove(path)
+
+ with open(path, mode='w', encoding=encoding) as file_handler:
+ if extension == ".csv":
+ writer = csv.writer(file_handler, delimiter=delimiter)
+ writer.writerows(self.image)
+ elif extension == ".json":
+ data = json.dumps(self.image, indent=indent, sort_keys=sort_keys)
+ file_handler.write(data)
+ elif extension in [".png", ".jpg", ".jpeg"]:
+ raise NotImplementedError("To be released in version 0.0.6")
+ else:
+ file_handler.writelines('\n'.join(map(str, self.image)))
+
def scan(self: Self, fun: Callable[[Any, Any], Any], acc: Optional[int] = None) -> Self:
"""
Apply the given function `fun` to each element in the image, storing the result
@@ -1598,7 +1638,7 @@ def zip_reduce(self: Self, acc: List, reducer: Callable[[Any, Any], Any], left:
if left is not None and right is not None:
# reference implementation:
# https://hexdocs.pm/elixir/1.12/Enum.html#zip_reduce/3
- raise NotImplementedError("TODO")
+ raise NotImplementedError("To be released in version 0.0.6")
else:
self.image = list(functools.reduce(reducer, zip(*self.image), acc))
return self
From f66ebb68843d68f83b0021246c5c0785b2db51be Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Fri, 20 Jan 2023 00:30:26 +0100
Subject: [PATCH 37/46] Rename empty to is_empty and implement to_dict
---
src/iterfun/iterfun.py | 39 ++++++++++++++++++++++-----------------
tests/test_iterfun.py | 6 +++---
2 files changed, 25 insertions(+), 20 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 3026f7b..b3c7dd5 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -394,21 +394,6 @@ def duplicates(self: Self) -> Self:
self.image = [item for item, count in Counter(self.image).items() if count > 1]
return self
- def empty(self: Self) -> bool:
- """
- Test if the image is empty.
-
- ```python
- >>> Iter([]).empty()
- True
- >>> Iter([0]).empty()
- False
- >>> Iter.range(1, 10).empty()
- False
- ```
- """
- return not bool(len(self.image))
-
def filter(self: Self, fun: Callable[[Any], bool]) -> Self:
"""
Filter the image, i.e. return only those elements for which `fun` returns
@@ -610,6 +595,21 @@ def is_disjoint(self: Self, iter_: Iterable) -> bool:
"""
return set(self.image).isdisjoint(iter_)
+ def is_empty(self: Self) -> bool:
+ """
+ Test if the image is empty.
+
+ ```python
+ >>> Iter([]).is_empty()
+ True
+ >>> Iter([0]).is_empty()
+ False
+ >>> Iter.range(1, 10).is_empty()
+ False
+ ```
+ """
+ return not bool(len(self.image))
+
def is_member(self: Self, element: Any) -> bool:
"""
Test if an element exists in the image.
@@ -1147,8 +1147,7 @@ def save(self: Self, path: str | Path, overwrite: bool = False) -> None:
"""
...
- def save(
- self: Self, path: str | Path, encoding: str = "utf-8", delimiter: str = ",", indent: int = 4, sort_keys: bool = False, overwrite: bool = False) -> None:
+ def save(self: Self, path: str | Path, encoding: str = "utf-8", delimiter: str = ",", indent: int = 4, sort_keys: bool = False, overwrite: bool = False) -> None:
extension = Path(path).suffix.lower()
if overwrite: os.remove(path)
@@ -1492,6 +1491,12 @@ def take_while(self: Self, fun: Callable[[Any], bool]) -> Self:
self.image = list(itertools.takewhile(fun, self.image))
return self
+ def to_dict(self: Self) -> Dict:
+ """
+ Return the image as a dictionary object.
+ """
+ return dict(self.image)
+
def to_list(self: Self) -> List[Any]:
"""
Return the image as a list object.
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index e38e384..f0d2c0a 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -113,9 +113,9 @@ def test_duplicates(self):
self.assertEqual([1, 2, 4], Iter([1, 1, 1, 2, 2, 3, 4, 4]).duplicates().image)
def test_empty(self):
- self.assertTrue(Iter([]).empty())
- self.assertFalse(Iter([0]).empty())
- self.assertFalse(Iter.range(1, 10).empty())
+ self.assertTrue(Iter([]).is_empty())
+ self.assertFalse(Iter([0]).is_empty())
+ self.assertFalse(Iter.range(1, 10).is_empty())
def test_filter(self):
self.assertEqual([2], Iter.range(1, 3).filter(lambda x: x % 2 == 0).image)
From c4f252bc3eec4fc7599a803644c616c0cf65b798 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Fri, 20 Jan 2023 00:47:57 +0100
Subject: [PATCH 38/46] Update README
---
README.md | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 123 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index a149606..5ee508c 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,126 @@
-Implements an eager iterator class reminiscent of Elixir's `Enum` structure. See
-also the changelog for more details. Note that the interface is not stable; signatures
-may change at any time until further notice.
+Implements an eager iterator class reminiscent of Elixir's `Enum` structure that
+features a series of handy methods for performing common data transformations.
+
+---
+
+## Examples
+
+See also `tests/test_scenarios.py` for more elaborate code snippets.
+
+```python
+from iterfun import Iter
+from iterfun import Functions as fun
+
+primes = Iter.range(1, 10_000).filter(fun.is_prime)
+
+# 1229
+print(primes.count())
+
+primes.save("primes.dat")
+```
+
+---
+
+## Method Reference
+
+This method reference serves as a first point of contact to help you discover
+what this library is capable of. Documentation and small, self-contained examples
+are provided in the doc strings of each method that you can read in the privacy of
+your code editor of choice.
+
+## Functions
+
+- `invert`
+- `is_even`
+- `is_odd`
+- `is_prime`
+- `sign`
+
+## Iter
+
+- `all`
+- `any`
+- `at`
+- `avg`
+- `cartesian`
+- `chunk_by`
+- `chunk_every`
+- `chunk_while`
+- `combinations`
+- `combinations_with_replacement`
+- `count`
+- `count_until`
+- `dedup`
+- `dedup_by`
+- `difference`
+- `drop`
+- `drop_every`
+- `drop_while`
+- `duplicates`
+- `filter`
+- `find`
+- `find_index`
+- `find_value`
+- `flat_map`
+- `flat_map`
+- `flat_map_reduce`
+- `flatten`
+- `frequencies`
+- `group_by`
+- `intersects`
+- `intersperse`
+- `into`
+- `is_disjoint`
+- `is_empty`
+- `is_member`
+- `is_subset`
+- `is_superset`
+- `join`
+- `linspace`
+- `map`
+- `map_every`
+- `map_intersperse`
+- `map_join`
+- `map_reduce`
+- `max`
+- `min`
+- `min_max`
+- `open`
+- `permutations`
+- `product`
+- `randint`
+- `random`
+- `range`
+- `reduce`
+- `reduce_while`
+- `reject`
+- `reverse`
+- `reverse_slice`
+- `save`
+- `scan`
+- `shorten`
+- `shuffle`
+- `slice`
+- `slide`
+- `sort`
+- `split`
+- `split_while`
+- `split_with`
+- `sum`
+- `symmetric_difference`
+- `take`
+- `take_every`
+- `take_random`
+- `take_while`
+- `to_dict`
+- `to_list`
+- `transpose`
+- `union`
+- `unique`
+- `unzip`
+- `with_index`
+- `zip_reduce`
+- `zip_with`
From f8f38bc02b3b95e3c56fe08565fb4c2a54eb47cc Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Fri, 20 Jan 2023 21:25:36 +0100
Subject: [PATCH 39/46] Add license information to the preamble of the source
file
---
src/iterfun/iterfun.py | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index b3c7dd5..da6c49c 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -1,5 +1,25 @@
#!/usr/bin/env python3
+"""
+The MIT License
+Copyright (c) 2023, Stefan Greve
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
from __future__ import annotations
import csv
From f9c38ad11a7bf46053394fb3ef62b2d3e0a437aa Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Fri, 20 Jan 2023 23:10:57 +0100
Subject: [PATCH 40/46] Change implementation of find_index methd
* return a list of all indices that return `True` for `fun`,
and not just the first one
* drop the default parameter and return an empty list if `fun`
always results in `False` for all elements in the image
---
src/iterfun/iterfun.py | 19 ++++++++++---------
tests/test_iterfun.py | 4 ++--
2 files changed, 12 insertions(+), 11 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index da6c49c..08407e3 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -443,20 +443,21 @@ def find(self: Self, fun: Callable[[Any], bool], default: Optional[Any] = None)
"""
return next(filter(fun, self.image), default)
- def find_index(self: Self, fun: Callable[[Any], bool], default: Optional[Any] = None) -> Optional[Any]:
+ def find_index(self: Self, fun: Callable[[Any], bool]) -> Self:
"""
- Similar to `self.find`, but return the zero-based index of the element
- instead of the element itself.
+ Return a list of indices for all occurrences in the image whose filter-
+ value produced by `fun` is equal to `True`, else an empty list.
```python
- >>> Iter([2, 4, 6]).find_index(lambda x: x % 2 == 1)
- None
- >>> Iter([2, 3, 4]).find_index(lambda x: x % 2 == 1)
- 1
+ >>> from iterfun import Functions as fun
+ >>> Iter.range(1, 10).find_index(fun.is_even)
+ [1, 3, 5, 7, 9]
+ >>> Iter([2, 3, 4]).find_index(lambda x: x < 0)
+ []
```
"""
- found = next(filter(fun, self.image), default)
- return self.image.index(found) if found in self.image else default
+ self.image = list(map(operator.itemgetter(0), filter(lambda x: fun(x[1]), enumerate(self.image))))
+ return self
def find_value(self: Self, fun: Callable[[Any], bool], default: Optional[Any] = None) -> Optional[Any]:
"""
diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py
index f0d2c0a..e4dadd6 100644
--- a/tests/test_iterfun.py
+++ b/tests/test_iterfun.py
@@ -126,8 +126,8 @@ def test_find(self):
self.assertEqual(0, Iter([2, 4, 6]).find(lambda x: x % 2 == 1, default=0))
def test_find_index(self):
- self.assertEqual(None, Iter([2, 4, 6]).find_index(lambda x: x % 2 == 1))
- self.assertEqual(1, Iter([2, 3, 4]).find_index(lambda x: x % 2 == 1))
+ self.assertEqual([1, 3, 5, 7, 9], Iter.range(1, 10).find_index(lambda x: x % 2 == 0).image)
+ self.assertEqual([], Iter([2, 3, 4]).find_index(lambda x: x < 0).image)
def test_find_value(self):
self.assertEqual(None, Iter([2, 4, 6]).find_value(lambda x: x % 2 == 1))
From 9e3a6a0f30f574e3bc9dd8ffb774c018dbac10bd Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Fri, 20 Jan 2023 23:13:49 +0100
Subject: [PATCH 41/46] Edit README
* add the solution to a modified leet code challenge
(see also: https://leetcode.com/problems/two-sum)
* add a list of contributors and license information
---
README.md | 43 +++++++++++++++++++++++++++++++++++--------
1 file changed, 35 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index 5ee508c..bedb548 100644
--- a/README.md
+++ b/README.md
@@ -25,18 +25,29 @@ features a series of handy methods for performing common data transformations.
## Examples
-See also `tests/test_scenarios.py` for more elaborate code snippets.
+### Two Sum
+
+Given an array of integers `domain` and an integer `target`, return indices of the
+two numbers such that they add up to target. You may assume that each input would
+have exactly one solution, and you may not use the same element twice. You can
+return the answer in any order.
```python
from iterfun import Iter
-from iterfun import Functions as fun
-primes = Iter.range(1, 10_000).filter(fun.is_prime)
+target = 12
+domain = [45, 26, 5, 41, 58, 97, 82, 9, 79, 22, 3, 74, 70, 84, 17, 79, 41, 96, 13, 89]
+
+pair = Iter(domain) \
+ .filter(lambda x: x < target) \
+ .combinations(2) \
+ .filter(lambda x: sum(x) == target) \
+ .flatten() \
+ .to_list()
-# 1229
-print(primes.count())
-primes.save("primes.dat")
+# [7, 10]
+print(Iter(domain).find_index(lambda x: x in pair).image)
```
---
@@ -48,7 +59,7 @@ what this library is capable of. Documentation and small, self-contained example
are provided in the doc strings of each method that you can read in the privacy of
your code editor of choice.
-## Functions
+### Functions
- `invert`
- `is_even`
@@ -56,7 +67,7 @@ your code editor of choice.
- `is_prime`
- `sign`
-## Iter
+### Iter
- `all`
- `any`
@@ -141,3 +152,19 @@ your code editor of choice.
- `with_index`
- `zip_reduce`
- `zip_with`
+
+---
+
+## Authors
+
+| Name | Mail Address | GitHub Profile |
+|------------------|-------------------------|-----------------------------------------------|
+| Stefan Greve | greve.stefan@outlook.jp | [StefanGreve](https://github.com/StefanGreve) |
+
+See also the list of [contributors](https://github.com/stefangreve/iterfun/contributors)
+who participated in this project.
+
+## License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
+for more details.
From e689ef4f23fdff656e830a8dc00b0ecd7044c9a3 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 22 Jan 2023 19:48:03 +0100
Subject: [PATCH 42/46] Add project logo
---
iterfun.svg | 1 +
1 file changed, 1 insertion(+)
create mode 100644 iterfun.svg
diff --git a/iterfun.svg b/iterfun.svg
new file mode 100644
index 0000000..023674a
--- /dev/null
+++ b/iterfun.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
From 513c4ac98a435750bc52d20382f960a697be27aa Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 22 Jan 2023 22:03:10 +0100
Subject: [PATCH 43/46] Implement the Miller-Rabin primality test
---
src/iterfun/iterfun.py | 54 ++++++++++++++++++++++++++++++++---------
tests/test_functions.py | 33 +++++++++++++++++++++++++
2 files changed, 75 insertions(+), 12 deletions(-)
diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py
index 08407e3..5da9518 100644
--- a/src/iterfun/iterfun.py
+++ b/src/iterfun/iterfun.py
@@ -64,33 +64,63 @@ def invert(x: int | float) -> (int | float):
"""
return -1 * x
- def is_even(x: int | float) -> bool:
+ def is_even(n: int) -> bool:
"""
- Test if `x` is an even number.
+ Test if `n` is an even number.
"""
- return x % 2 == 0
+ return not n & 1
- def is_odd(x: int | float) -> bool:
+ def is_odd(n: int) -> bool:
"""
- Test if `x` is an odd number.
+ Test if `n` is an odd number.
"""
- return x % 2 != 0
+ return bool(n & 1)
- def is_prime(x: int) -> bool:
+ def is_prime(n: int) -> bool:
"""
- Implements the Sieve of Eratosthenes check to test whether the number `x`
- is prime or not.
+ Test whether the number `n` is prime or not.
"""
- if x <= 1: return False
+ if n <= 1: return False
i = 2
- while math.pow(i, 2) <= x:
- if x % i == 0:
+ while i ** 2 <= n:
+ if n % i == 0:
return False
i += 1
return True
+ def __inner_miller_rabin(n: int, r: int) -> bool:
+ m = n - 1 # = (n - 1) / 2^k
+ while not m & 1:
+ m >>= 1
+
+ if pow(r, m, n) == 1:
+ return True
+
+ while m < n - 1:
+ if pow(r, m, n) == n - 1:
+ return True
+ m <<= 1
+
+ return False
+
+ def miller_rabin(n: int, k: int = 40) -> bool:
+ """
+ Test whether a number `n >= 2` is composite, i.e. not prime. Return `True`
+ if `n` passes `k` rounds of the Miller-Rabin primality test and assert that
+ a number `n` might possibly be prime, else return `False` if `n` is proved
+ to be a composite.
+ """
+ if n < 2: raise ValueError()
+ if n <= 3: return True
+
+ for _ in range(k):
+ if not Functions.__inner_miller_rabin(n, r=secrets.choice(range(2, n - 1))):
+ return False
+
+ return True
+
def sign(x: int | float) -> Literal[-1, 0, 1]:
"""
Extract the sign of a real number.
diff --git a/tests/test_functions.py b/tests/test_functions.py
index d724914..df0e974 100644
--- a/tests/test_functions.py
+++ b/tests/test_functions.py
@@ -1,5 +1,7 @@
import unittest
+import pytest
+
from src.iterfun import Functions as fun
@@ -33,3 +35,34 @@ def test_is_prime(self):
self.assertTrue(fun.is_prime(798723477701))
self.assertTrue(fun.is_prime(100005743))
+ def test_miller_rabin_throws(self):
+ with pytest.raises(ValueError):
+ fun.miller_rabin(1)
+
+ def test_miller_rabin(self):
+ self.assertFalse(fun.miller_rabin(4))
+ self.assertFalse(fun.miller_rabin(100))
+ self.assertFalse(fun.miller_rabin(32472874282))
+ self.assertFalse(fun.miller_rabin(2032320))
+ self.assertFalse(fun.miller_rabin(9000600))
+ self.assertTrue(fun.miller_rabin(2))
+ self.assertTrue(fun.miller_rabin(3))
+ self.assertTrue(fun.miller_rabin(5))
+ self.assertTrue(fun.miller_rabin(7))
+ self.assertTrue(fun.miller_rabin(43))
+ self.assertTrue(fun.miller_rabin(59))
+ self.assertTrue(fun.miller_rabin(83))
+ self.assertTrue(fun.miller_rabin(97))
+ self.assertTrue(fun.miller_rabin(2237))
+ self.assertTrue(fun.miller_rabin(69899))
+ self.assertTrue(fun.miller_rabin(363047))
+ self.assertTrue(fun.miller_rabin(659713))
+ self.assertTrue(fun.miller_rabin(2427281))
+ self.assertTrue(fun.miller_rabin(81239489))
+ self.assertTrue(fun.miller_rabin(81247169))
+ self.assertTrue(fun.miller_rabin(24244807))
+ self.assertTrue(fun.miller_rabin(3846589051))
+ self.assertTrue(fun.miller_rabin(5224245449))
+ self.assertTrue(fun.miller_rabin(798723477701))
+ self.assertTrue(fun.miller_rabin(100005743))
+
From b2f06604d0a0f6cee7223c21d043560a3ffe177b Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 22 Jan 2023 22:34:13 +0100
Subject: [PATCH 44/46] Update README: add sub-section about technical
limitations
---
README.md | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index bedb548..65fe536 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,9 @@
+
@@ -25,7 +31,7 @@ features a series of handy methods for performing common data transformations.
## Examples
-### Two Sum
+### Two Sum Problem
Given an array of integers `domain` and an integer `target`, return indices of the
two numbers such that they add up to target. You may assume that each input would
@@ -45,13 +51,24 @@ pair = Iter(domain) \
.flatten() \
.to_list()
-
# [7, 10]
print(Iter(domain).find_index(lambda x: x in pair).image)
```
---
+## Technical Limitations
+
+For reasons of simplicity, the mutable state in `image` is exposed as a list or
+dictionary and not as a generator which is capable of dealing with very large
+sequences. It's an implementation detail that is necessary for some index-based methods,
+though this behavior might change in future versions of this library where applicable.
+
+There is probably room to improve the overall performance and accuracy of this library.
+To this end, future updates will also increase the test coverage in order to consider
+more edge cases and method interoperability. Notice that the terms and conditions
+of the MIT License will always apply.
+
## Method Reference
This method reference serves as a first point of contact to help you discover
@@ -65,6 +82,7 @@ your code editor of choice.
- `is_even`
- `is_odd`
- `is_prime`
+- `miller_rabin`
- `sign`
### Iter
From 1a1d196be2e94e2e3d7acacb97fb60949bdc59ad Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 22 Jan 2023 22:53:44 +0100
Subject: [PATCH 45/46] Update changelog and set release date for v0.0.5 to
2023-01-29
---
CHANGELOG.md | 180 +++++++++++++++++++++++++++++----------------------
1 file changed, 102 insertions(+), 78 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 234afe5..c4d9aa3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
# Changelog
-## Version 0.0.5 (31 Jan 2023)
+## Version 0.0.5 (29 Jan 2023)
> Note that this update introduces breaking changes.
@@ -9,10 +9,11 @@ Implement a `Functions` helper class for common procedures such as
- `invert`
- `is_even`
- `is_odd`
+- `miller_rabin`
- `sign`
The overload in the `Iter` constructor was removed in favor of a static `Iter.range`
-method to enforce separation of concerns more strictly. Additional minor changes
+method to enforce separation of concerns more diligently. Additional minor modifications
were made to pretty much all doc strings to reflect the recent changes that are
to be consolidated for more information. In brief,
@@ -21,13 +22,36 @@ to be consolidated for more information. In brief,
- `iterable` and `iter` keyword arguments were renamed to `iter_`
- the method signature of the `range` method has changed and added support for
decrements
+- the `empty` method was renamed to `is_empty`
The following methods are new:
-- `duplicates`
-- `flatten`
-- `linspace`
-- `transpose`
+- `self.cartesian`
+- `self.combinations`
+- `self.combinations_with_replacement`
+- `self.difference`
+- `self.duplicates`
+- `self.flatten`
+- `self.intersects`
+- `self.is_disjoint`
+- `self.is_subset`
+- `self.is_superset`
+- `Iter.linspace`
+- `Iter.open`
+- `self.permutations`
+- `self.save`
+- `self.symmetric_difference`
+- `Iter.randint`
+- `self.to_dict`
+- `self.to_list`
+- `self.transpose`
+- `self.__eq__`
+- `self.__ne__`
+- `self.__iter__`
+- `self.__next__`
+
+Starting with version `0.0.5`, this library now also pulls in `typing_extensions`
+as an external dependency.
## Version 0.0.4 (17 May 2022)
@@ -36,8 +60,8 @@ was removed in favor of `Iter.range` for implementing the `Iter.__init__` overlo
Additionally, the following methods have been removed as well:
-- `self.each`
-- `self.fetch`
+- `self.each`
+- `self.fetch`
Turns out they are not that useful in Python after all.
@@ -51,76 +75,76 @@ all at once. Perhaps there will be a `Stream`-like structure in the future that'
compatible with `Iter`, though it remains to be seen whether such a thing will be
implemented in the future.
-- [x] `self.__init__`
-- [x] `self.all`
-- [x] `self.any`
-- [x] `self.at`
-- [x] `self.avg`
-- [x] `self.chunk_by`
-- [x] `self.chunk_every`
-- [ ] `self.chunk_while`
-- [x] `self.concat`
-- [x] `self.count`
-- [x] `self.count_until`
-- [x] `self.dedup`
-- [x] `self.dedup_by`
-- [x] `self.drop`
-- [x] `self.drop_every`
-- [x] `self.drop_while`
-- [x] `self.each`
-- [x] `self.empty`
-- [x] `self.fetch`
-- [x] `self.filter`
-- [x] `self.find`
-- [x] `self.find_index`
-- [x] `self.find_value`
-- [x] `self.flat_map`
-- [ ] `self.flat_map_reduce`
-- [x] `self.frequencies`
-- [x] `self.frequencies_by`
-- [x] `self.group_by`
-- [x] `self.intersperse`
-- [x] `self.into`
-- [x] `self.join`
-- [x] `self.map`
-- [x] `self.map_every`
-- [x] `self.map_intersperse`
-- [x] `self.map_join`
-- [x] `self.map_reduce`
-- [x] `self.max`
-- [x] `self.member`
-- [x] `self.min`
-- [x] `self.min_max`
-- [x] `self.product`
-- [x] `self.random`
-- [x] `Iter.range`
-- [x] `self.reduce`
-- [x] `self.reduce_while`
-- [x] `self.reject`
-- [x] `self.reverse`
-- [x] `self.reverse_slice`
-- [x] `self.scan`
-- [x] `Iter.shorten`
-- [x] `self.shuffle`
-- [x] `self.slice`
-- [x] `self.slide`
-- [x] `self.sort`
-- [x] `self.split`
-- [x] `self.split_while`
-- [x] `self.split_with`
-- [x] `self.sum`
-- [x] `self.take`
-- [x] `self.take_every`
-- [x] `self.take_random`
-- [x] `self.take_while`
-- [x] `self.uniq`
-- [x] `self.unzip`
-- [x] `self.with_index`
-- [x] `self.zip`
-- [ ] `self.zip_reduce`
-- [ ] `self.zip_with`
-- [x] `self.__repr__`
-- [x] `self.__str__`
+- [x] `self.__init__`
+- [x] `self.all`
+- [x] `self.any`
+- [x] `self.at`
+- [x] `self.avg`
+- [x] `self.chunk_by`
+- [x] `self.chunk_every`
+- [ ] `self.chunk_while`
+- [x] `self.concat`
+- [x] `self.count`
+- [x] `self.count_until`
+- [x] `self.dedup`
+- [x] `self.dedup_by`
+- [x] `self.drop`
+- [x] `self.drop_every`
+- [x] `self.drop_while`
+- [x] `self.each`
+- [x] `self.empty`
+- [x] `self.fetch`
+- [x] `self.filter`
+- [x] `self.find`
+- [x] `self.find_index`
+- [x] `self.find_value`
+- [x] `self.flat_map`
+- [ ] `self.flat_map_reduce`
+- [x] `self.frequencies`
+- [x] `self.frequencies_by`
+- [x] `self.group_by`
+- [x] `self.intersperse`
+- [x] `self.into`
+- [x] `self.join`
+- [x] `self.map`
+- [x] `self.map_every`
+- [x] `self.map_intersperse`
+- [x] `self.map_join`
+- [x] `self.map_reduce`
+- [x] `self.max`
+- [x] `self.member`
+- [x] `self.min`
+- [x] `self.min_max`
+- [x] `self.product`
+- [x] `self.random`
+- [x] `Iter.range`
+- [x] `self.reduce`
+- [x] `self.reduce_while`
+- [x] `self.reject`
+- [x] `self.reverse`
+- [x] `self.reverse_slice`
+- [x] `self.scan`
+- [x] `Iter.shorten`
+- [x] `self.shuffle`
+- [x] `self.slice`
+- [x] `self.slide`
+- [x] `self.sort`
+- [x] `self.split`
+- [x] `self.split_while`
+- [x] `self.split_with`
+- [x] `self.sum`
+- [x] `self.take`
+- [x] `self.take_every`
+- [x] `self.take_random`
+- [x] `self.take_while`
+- [x] `self.uniq`
+- [x] `self.unzip`
+- [x] `self.with_index`
+- [x] `self.zip`
+- [ ] `self.zip_reduce`
+- [ ] `self.zip_with`
+- [x] `self.__repr__`
+- [x] `self.__str__`
The static non-standard functions `Iter.range` and `Iter.shorten` have been added
for convenience purposes only. The method signature `uniq_by` does not appear in
From aa9ea45655ef6df328ee8b539fec72bb473a4727 Mon Sep 17 00:00:00 2001
From: StefanGreve
Date: Sun, 29 Jan 2023 14:20:55 +0100
Subject: [PATCH 46/46] Update README and CHANGELOG prior to release
---
CHANGELOG.md | 3 ++-
README.md | 17 +++++++++++++++--
2 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c4d9aa3..6b5b61b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -51,7 +51,8 @@ The following methods are new:
- `self.__next__`
Starting with version `0.0.5`, this library now also pulls in `typing_extensions`
-as an external dependency.
+as an external dependency. Support for Python 3.7 has been dropped which makes
+version 3.8 the new baseline in this project.
## Version 0.0.4 (17 May 2022)
diff --git a/README.md b/README.md
index 65fe536..4d5e55f 100644
--- a/README.md
+++ b/README.md
@@ -24,8 +24,21 @@
-Implements an eager iterator class reminiscent of Elixir's `Enum` structure that
-features a series of handy methods for performing common data transformations.
+## About
+
+IterFun implements an eager iterator class reminiscent of Elixir's `Enum` structure
+that features a series of handy methods for performing common data transformations.
+
+It's a continuously evolving project specifically developed to meet my personal
+needs, but I have decided to put it out in the open in case someone else find it
+as useful as me. Contributions in form of pull requests or suggestions are always
+welcome. If you need a more mature library for your project, consider one of these
+alternatives instead:
+
+- [`toolz`](https://github.com/pytoolz/toolz)
+- [`fn.py`](https://github.com/kachayev/fn.py)
+- [`more-itertools`](https://github.com/more-itertools/more-itertools)
+- [`Pipe`](https://github.com/JulienPalard/Pipe)
---