From 9e1fef27915dc7d8c852f141886c8d1813971fbf Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 17 Dec 2021 09:24:07 +0100 Subject: [PATCH 01/30] Add Python 3.11-dev to version matrix --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index f04b5f2..3103ed4 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' ] + python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11-dev'] os: [ ubuntu-latest, macOS-latest, windows-latest ] steps: From bfe1443072e36f7606cbfe3d7c937a33e74d8f56 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 17 Dec 2021 09:24:25 +0100 Subject: [PATCH 02/30] Edit description --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 292884e..27e6299 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,5 @@

-Implements an iterator interface reminscent of languages such as Elixier of F#. +Implements an iterator interface reminiscent of functional programming languages +such as Elixier. From d20dbe60eeb3a7630cbe135604c4485359e6b623 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 17 Dec 2021 09:25:29 +0100 Subject: [PATCH 03/30] Edit description and list of classifiers --- setup.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 5a13144..6ae183f 100644 --- a/setup.py +++ b/setup.py @@ -42,13 +42,13 @@ author_email="greve.stefan@outlook.jp", name=package_name, version=version, - description="Implements an iterator interface reminscent of languages such as Elixier of F#.", + description="Implements an iterator interface reminiscent of functional programming languages such as Elixier.", long_description=long_description, long_description_content_type='text/markdown', license='MIT', url="https://github.com/StefanGreve/iterfun", project_urls={ - 'Documentation': "https://github.com/StefanGreve/iterfun/blob/master/README.md", #TODO: Add proper documention + 'Documentation': "https://github.com/StefanGreve/iterfun/blob/master/README.md", 'Source Code': "https://github.com/StefanGreve/iterfun", 'Bug Reports': "https://github.com/StefanGreve/iterfun/issues", 'Changelog': "https://github.com/StefanGreve/iterfun/blob/master/CHANGELOG.md" @@ -63,7 +63,7 @@ package_dir={'': 'src'}, packages=find_packages(where='src'), classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', 'Natural Language :: English', @@ -71,6 +71,8 @@ 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: Implementation :: CPython', 'Operating System :: OS Independent', 'Topic :: Utilities', ], From 719bb63e397b900311635d7558ed3c170d2f5111 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 17 Dec 2021 09:26:12 +0100 Subject: [PATCH 04/30] Implement all, any, at, avg (chunk_by WIP) --- src/iterfun/iterfun.py | 118 +++++++++++++++++++++++++++++++++++++++-- tests/test_iterfun.py | 45 ++++++++++++++-- 2 files changed, 157 insertions(+), 6 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 4bf6d01..69eced3 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -1,6 +1,118 @@ #!/usr/bin/env python3 +from __future__ import annotations + +import functools +import itertools +import operator +import random +import statistics +from collections import Counter +from typing import Any, Callable, Dict, Iterable, List, Set, Tuple, Union, overload + + class Iter: - @staticmethod - def hello(): - return "Hello, World!" + """ + Provides a set of algorithms to work with iterable collections. In Python, + an iterable is any data type that implements the `__iter__` method which + returns an iterator, or alternatively, a `__getitem__` method suitable for + indexed lookup such as `list`, `tuple`, `dict`, or `set`. + + ```python + >>> Iter(range(1,4)).map(lambda x: 2*x) + [2, 4, 6] + >>> Iter(range(1, 4)).sum() + 6 + >>> Iter({'a': 1, 'b': 2}).map(lambda k,v: {k, 2 * v}) + {'a': 2, 'b': 4} + ``` + """ + def __init__(self, iter: Iterable) -> Iter: + self.domain = list(iter) + self.image = iter + + def all(self, fun: Callable=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`. + + ```python + >>> Iter([1, 2, 3]).all() + True + >>> Iter([1, None, 3]).all() + False + >>> Iter([]).all() + True + >>> Iter([1,2,3]).all(lambda x: x % 2 == 0) + False + ``` + """ + self.image = all(self.image) if fun is None else all(map(fun, self.image)) + return self.image + + def any(self, fun: Callable=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`. + + ```python + >>> Iter([False, False, False]).any() + False + >>> Iter([False, True, False]).any() + True + >>> Iter([2,4,6]).any(lambda x: x % 2 == 1) + False + ``` + """ + self.image = any(self.image) if fun is None else any(map(fun, self.image)) + return self.image + + def at(self, index: int) -> Any: + """ + Find the element at the given `index` (zero-based). Raise an `IndexError` + if `index` is out of bonds. A negative index can be passed, which means the + enumerable is enumerated once and the index is counted from the end. + + ```python + >>> Iter([2,4,6]).at(0) + 2 + >>> Iter([2,4,6]).at(-1) + 6 + >>> Iter([2,4,6]).at(4) + IndexError: list index out of range + ``` + """ + self.image = list(self.image)[index] + return self.image + + def avg(self) -> Union[int, float]: + """ + Return the sample arithmetic mean of `self.image`. + + ```python + >>> Iter(range(11)).avg() + 5 + ``` + """ + self.image = statistics.mean(self.image) + return self.image + + def chunk_by(self, by: Union[int, Callable]) -> Iter: + """ + Split `self.image` on every element for which `by` returns a new value + if `by` is a predicate, else split `self.image` into sublists of size `by`. + + ```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]] + >>> Iter([1, 2, 2, 3, 4, 4, 6, 7, 7]).chunk_by(3) + [[1, 2, 2], [3, 4, 4], [6, 7, 7], [7]] + """ + if isinstance(by, int): + self.image = [list(self.image)[i:i+by] for i in range(0, len(self.image), by)] + return self.image + else: + raise NotImplementedError() + + def __str__(self) -> str: + return f"[{', '.join(map(str, self.iter))}]" if isinstance(self.image, Iterable) else self.image diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index 0889402..c163ed5 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -1,9 +1,48 @@ import unittest +import pytest from src.iterfun import Iter -class TestIterator(unittest.TestCase): +class TestIter(unittest.TestCase): + #region unit tests - def test_hello(self): - self.assertEqual("Hello, World!", Iter.hello()) + def test_all(self): + self.assertTrue(Iter([1, 2, 3]).all()) + self.assertFalse(Iter([1, None, 3]).all()) + self.assertTrue(Iter([]).all()) + self.assertTrue(Iter([2,4,6]).all(lambda x: x % 2 == 0)) + self.assertFalse(Iter([1,2,3]).all(lambda x: x % 2 == 0)) + self.assertTrue(Iter([]).all(lambda: None)) + def test_any(self): + self.assertFalse(Iter([False, False, False]).any()) + self.assertTrue(Iter([False, True, False]).any()) + self.assertFalse(Iter([]).any()) + self.assertFalse(Iter([2,4,6]).any(lambda x: x % 2 == 1)) + self.assertTrue(Iter([1,2,3]).any(lambda x: x % 2 == 1)) + self.assertFalse(Iter([]).any(lambda: None)) + + def test_at(self): + self.assertEqual(2, Iter([2,4,6]).at(0)) + self.assertEqual(6, Iter([2,4,6]).at(2)) + self.assertEqual(6, Iter([2,4,6]).at(-1)) + + def test_at_throws(self): + with pytest.raises(IndexError): + self.assertEqual(None, Iter([2,4,6]).at(4)) + + def test_avg(self): + self.assertEqual(5, Iter(range(11)).avg()) + + @pytest.mark.xfail(raises=NotImplementedError) + 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) + self.assertEqual(expected, actual) + print(f"{actual=}") + + #endregion + + #region integration tests + + #endregion From 82c83c7dc5d1f4e0cdc5b68a354f7322d399d62e Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sun, 19 Dec 2021 23:40:25 +0100 Subject: [PATCH 05/30] Implement chunk_every, concat and count --- src/iterfun/iterfun.py | 81 ++++++++++++++++++++++++++++++++++-------- tests/test_iterfun.py | 27 ++++++++++++-- 2 files changed, 91 insertions(+), 17 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 69eced3..28549c0 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -8,7 +8,7 @@ import random import statistics from collections import Counter -from typing import Any, Callable, Dict, Iterable, List, Set, Tuple, Union, overload +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union, overload class Iter: @@ -31,7 +31,7 @@ def __init__(self, iter: Iterable) -> Iter: self.domain = list(iter) self.image = iter - def all(self, fun: Callable=None) -> bool: + def all(self, fun: Optional[Callable]=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`. @@ -50,7 +50,7 @@ def all(self, fun: Callable=None) -> bool: self.image = all(self.image) if fun is None else all(map(fun, self.image)) return self.image - def any(self, fun: Callable=None) -> bool: + def any(self, fun: Optional[Callable]=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`. @@ -97,22 +97,75 @@ def avg(self) -> Union[int, float]: self.image = statistics.mean(self.image) return self.image - def chunk_by(self, by: Union[int, Callable]) -> Iter: + def chunk_by(self, fun: Callable) -> Iter: """ - Split `self.image` on every element for which `by` returns a new value - if `by` is a predicate, else split `self.image` into sublists of size `by`. + Split `self.image` on every element for which `fun` returns a new value. ```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]] - >>> Iter([1, 2, 2, 3, 4, 4, 6, 7, 7]).chunk_by(3) - [[1, 2, 2], [3, 4, 4], [6, 7, 7], [7]] - """ - if isinstance(by, int): - self.image = [list(self.image)[i:i+by] for i in range(0, len(self.image), by)] - return self.image - else: - raise NotImplementedError() + ``` + """ + self.image = [list(group) for _, group in itertools.groupby(self.image, fun)] + return self + + 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(range(1, 7)).chunk_every(2) + [[1, 2], [3, 4], [5, 6]] + >>> Iter(range(1, 7)).chunk_every(3, 2, [7]) + [[1, 2, 3], [3, 4, 5], [5, 6, 7]] + >>> 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), count-(count-step))] + 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: + raise NotImplementedError() + + @overload + @staticmethod + def concat(iter: List[Any]) -> Iter: ... + + @overload + @staticmethod + def concat(*iter: Tuple[int, int]) -> Iter: ... + + @staticmethod + def concat(*iter: List[Any] | Tuple[int, int]) -> Iter: + """ + Given a list of lists, concatenates the list into a single list. + + ```python + >>> Iter.concat([[1, [2], 3], [4], [5, 6]]) + [1, [2], 3, 4, 5, 6] + >>> Iter.concat((1, 3), (4, 6)) + [1, 2, 3, 4, 5, 6] + ``` + """ + return Iter(list(itertools.chain(*(iter[0] if isinstance(iter[0], List) else [range(t[0], t[1]+1) for t in iter])))) + + def count(self, fun: Optional[Callable]=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. + + ```python + >>> Iter(range(1,4)).count() + 3 + >>> Iter(range(1, 6).count(lambda x: x % 2 == 0)) + 2 + ``` + """ + return len(self.image) if fun is None else len(list(filter(fun, self.image))) def __str__(self) -> str: return f"[{', '.join(map(str, self.iter))}]" if isinstance(self.image, Iterable) else self.image diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index c163ed5..2ab432d 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -1,3 +1,4 @@ +from itertools import accumulate import unittest import pytest @@ -34,12 +35,32 @@ def test_at_throws(self): def test_avg(self): self.assertEqual(5, Iter(range(11)).avg()) - @pytest.mark.xfail(raises=NotImplementedError) 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) + actual = Iter([1, 2, 2, 3, 4, 4, 6, 7, 7]).chunk_by(lambda x: x % 2 == 1).image self.assertEqual(expected, actual) - print(f"{actual=}") + + def test_chunk_every(self): + self.assertEqual([[1, 2], [3, 4], [5, 6]], Iter(range(1, 7)).chunk_every(2).image) + self.assertEqual([[1, 2, 3], [3, 4, 5], [5, 6]], Iter(range(1,7)).chunk_every(3, 2).image) + self.assertEqual([[1, 2, 3], [3, 4, 5], [5, 6, 7]], Iter(range(1, 7)).chunk_every(3, 2, [7]).image) + self.assertEqual([[1, 2, 3], [4]], Iter(range(1, 5)).chunk_every(3, 3, []).image) + self.assertEqual([[1, 2, 3, 4]], Iter(range(1,5)).chunk_every(10).image) + self.assertEqual([[1, 2], [4, 5]], Iter(range(1, 6)).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(range(1, 11)).chunk_while([], None, None)) + + def test_concant(self): + self.assertEqual([1, [2], 3, 4, 5, 6], Iter.concat([[1, [2], 3], [4], [5, 6]]).image) + self.assertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], Iter.concat((1, 3), (4, 6), (7, 9)).image) + self.assertEqual([1, 2, 3, 4, 5, 6], Iter.concat([[1, 2, 3], [4, 5, 6]]).image) + self.assertEqual([1, 2, 3, 4, 5, 6], Iter.concat((1, 3), (4, 6)).image) + + def test_count(self): + self.assertEqual(3, Iter(range(1, 4)).count()) + self.assertEqual(2, Iter(range(1, 6)).count(lambda x: x % 2 == 0)) #endregion From 45c2b35998352709460ef2b5595c81fb40edffe9 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sun, 19 Dec 2021 23:40:37 +0100 Subject: [PATCH 06/30] Fix typo --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 27e6299..b230bb4 100644 --- a/README.md +++ b/README.md @@ -19,4 +19,4 @@

Implements an iterator interface reminiscent of functional programming languages -such as Elixier. +such as Elixir. diff --git a/setup.py b/setup.py index 6ae183f..aae3a5c 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ author_email="greve.stefan@outlook.jp", name=package_name, version=version, - description="Implements an iterator interface reminiscent of functional programming languages such as Elixier.", + description="Implements an iterator interface reminiscent of functional programming languages such as Elixir.", long_description=long_description, long_description_content_type='text/markdown', license='MIT', From eec3989523818245faef16e390b13befdb3560d4 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Mon, 20 Dec 2021 21:44:23 +0100 Subject: [PATCH 07/30] Implements a series of new functions. Note: Unchecked functions currently raise an NotImplementedError. - [x] overload __init__ with tuple range initialization - [x] implement count_until - [x] implement dedup - [ ] add dedup_by method signature - [x] implement drop - [x] implement drop_every - [x] implement drop_while - [x] implement each - [x] implement empty - [ ] add fetch method signature - [x] implement filter - [x] implement find - [x] implement find_index - [x] implement find_value - [x] implement flat_map - [ ] add flat_map_reduce method signature --- src/iterfun/iterfun.py | 225 +++++++++++++++++++++++++++++++++++++++-- tests/test_iterfun.py | 104 ++++++++++++++++--- 2 files changed, 307 insertions(+), 22 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 28549c0..46c553e 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -13,23 +13,36 @@ class Iter: """ - Provides a set of algorithms to work with iterable collections. In Python, + Define a set of algorithms to work with iterable collections. In Python, an iterable is any data type that implements the `__iter__` method which returns an iterator, or alternatively, a `__getitem__` method suitable for indexed lookup such as `list`, `tuple`, `dict`, or `set`. ```python - >>> Iter(range(1,4)).map(lambda x: 2*x) + >>> Iter(1, 3).map(lambda x: 2*x) [2, 4, 6] - >>> Iter(range(1, 4)).sum() + >>> Iter([1, 2, 3]).sum() 6 - >>> Iter({'a': 1, 'b': 2}).map(lambda k,v: {k, 2 * v}) + >>> Iter({'a': 1, 'b': 2}).map(lambda k,v: {k, 2 * v} {'a': 2, 'b': 4} ``` """ - def __init__(self, iter: Iterable) -> Iter: - self.domain = list(iter) - self.image = iter + + @overload + def __init__(self, iter: Iterable) -> Iter: ... + + @overload + def __init__(self, *iter: int) -> Iter: ... + + def __init__(self, *iter: Iterable | int) -> Iter: + self.domain = iter[0] + self.image = Iter.__ctor(iter) + + @staticmethod + def __ctor(iter: Iterable | int) -> List: + if isinstance(iter, Tuple) and len(iter) == 2: + return list(range(iter[0], iter[1]+1)) if iter[1] != 0 else [] + return iter[0] def all(self, fun: Optional[Callable]=None) -> bool: """ @@ -161,11 +174,205 @@ def count(self, fun: Optional[Callable]=None) -> int: ```python >>> Iter(range(1,4)).count() 3 - >>> Iter(range(1, 6).count(lambda x: x % 2 == 0)) + >>> Iter(range(1, 6)).count(lambda x: x % 2 == 0) 2 ``` """ - return len(self.image) if fun is None else len(list(filter(fun, self.image))) + 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]=None) -> int: + """ + Count the elements in `self.image` for which `fun` returns a truthy value, + stopping at `limit`. + + ```python + >>> Iter(range(1, 21)).count_until(5) + 5 + >>> Iter(range(1, 21)).count_until(50) + 20 + ``` + """ + return len(list(self.image)[:limit]) if fun is None else len(list(filter(fun, self.image))[:limit]) + + + def dedup(self) -> Iter: + """ + Enumerates `self.image`, returning a list where all consecutive duplicated + elements are collapsed to a single element. + + ```python + >>> Iter([1, 2, 3, 3, 2, 1]).dedup() + [1, 2, 3, 2, 1] + ``` + """ + self.image = [group[0] for group in itertools.groupby(self.image)] + return self + + def dedup_by(self, fun: Callable): + raise NotImplementedError() + + def drop(self, amount: int) -> Iter: + """ + ```python + >>> Iter([1, 2, 3]).drop(2) + [3] + >>> Iter([1, 2, 3]).drop(10) + [] + >>> Iter([1, 2, 3]).drop(-1) + [1, 2] + ``` + """ + self.image = list(self.image) + self.image = self.image[amount:] if amount > 0 else self.image[:len(self.image)+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 integer. + + ```python + >>> Iter(1, 10).drop_every(2) + [2, 4, 6, 8, 10] + >>> Iter(1, 10).drop_every(0) + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + >>> Iter(1, 3).drop_every(1) + [] + ``` + """ + 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) -> Iter: + """ + Drop elements at the beginning of the enumerable while `fun` returns a + truthy value. + + ```python + >>> Iter([1, 2, 3, 2, 1]).drop_while(lambda x: x < 3) + [3, 2, 1] + ``` + """ + self.image = self.image[self.image.index(list(itertools.filterfalse(fun, self.image))[0]):] + return self + + def each(self, fun: Callable) -> bool: + """ + Invoke the given `fun` for each element in the `self.image`, then return + `True`. + >>> Iter(1, 3).each(print) + 1 + 2 + 3 + """ + list(map(fun, self.image)) + return True + + def empty(self) -> bool: + """ + Return `True` if `self.image` is empty, otherwise `False`. + + ```python + >>> Iter([]).empty() + True + >>> Iter(0, 0).empty() + True + >>> Iter(1, 10).empty() + False + ``` + """ + return not bool(len(self.image)) + + def fetch(self, index: int) -> bool: + raise NotImplementedError() + + def filter(self, fun: Callable) -> Iter: + """ + Filter `self.image`, i.e. return only those elements for which `fun` returns + a truthy value. + + ```python + >>> Iter(1, 3).filter(lambda x: x % 2 == 0) + [2] + ``` + """ + self.image = list(filter(fun, self.image)) + return self + + def find(self, fun: Callable, 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) + 3 + >>> Iter([2, 4, 6]).find(lambda x: x % 2 == 1) + None + >>> Iter([2, 4, 6]).find(lambda x: x % 2 == 1, default=0) + 0 + ``` + """ + return next(filter(fun, self.image), default) + + def find_index(self, fun: Callable, default: Optional[Any]=None) -> Optional[Any]: + """ + Similar to `self.find`, but return the index (zero-based) of the element + instead of the element itself. + + ```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 + ``` + """ + 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, default: Optional[Any]=None) -> Optional[Any]: + """ + Similar to `self.find`, but return the value of the function invocation instead + of the element itself. + + ```python + >>> Iter([2, 4, 6]).find_value(lambda x: x % 2 == 1) + None + >>> Iter([2, 3, 4]).filter(lambda x: x > 2).find_value(lambda x: x * x) + 9 + >>> Iter(1, 3).find_value(lambda x: isinstance(x, bool), "no bools!") + 'no bools!' + ``` + """ + found = next(filter(fun, self.image), default) + return fun(found) if found is not default else default + + def flat_map(self, fun: Callable) -> Iter: + """ + Map the given `fun` over `self.image` and flattens the result. + + ```python + >>> 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]] + ``` + """ + self.image = list(itertools.chain(*map(fun, self.image))) + return self + + def flat_map_reduce(self, fun: Callable[[Any, Any], Any], acc: Any) -> Iter: + """ + Map and reduce an `self.image`, flattening the given results (only one + level deep). It expects an accumulator and a function that receives each + enumerable element, and must return a... + + ```python + >>> #example + ``` + """ + raise NotImplementedError() def __str__(self) -> str: return f"[{', '.join(map(str, self.iter))}]" if isinstance(self.image, Iterable) else self.image diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index 2ab432d..d77354f 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -1,4 +1,3 @@ -from itertools import accumulate import unittest import pytest @@ -7,33 +6,38 @@ class TestIter(unittest.TestCase): #region unit tests + def test_iter(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, 2, 3, 4, 5, 6, 7, 8, 9, 10], Iter(1, 10).image) + def test_all(self): self.assertTrue(Iter([1, 2, 3]).all()) self.assertFalse(Iter([1, None, 3]).all()) self.assertTrue(Iter([]).all()) - self.assertTrue(Iter([2,4,6]).all(lambda x: x % 2 == 0)) - self.assertFalse(Iter([1,2,3]).all(lambda x: x % 2 == 0)) + self.assertTrue(Iter([2, 4, 6]).all(lambda x: x % 2 == 0)) + self.assertFalse(Iter([1, 2, 3]).all(lambda x: x % 2 == 0)) self.assertTrue(Iter([]).all(lambda: None)) def test_any(self): self.assertFalse(Iter([False, False, False]).any()) self.assertTrue(Iter([False, True, False]).any()) self.assertFalse(Iter([]).any()) - self.assertFalse(Iter([2,4,6]).any(lambda x: x % 2 == 1)) - self.assertTrue(Iter([1,2,3]).any(lambda x: x % 2 == 1)) + self.assertFalse(Iter([2, 4, 6]).any(lambda x: x % 2 == 1)) + self.assertTrue(Iter([1, 2, 3]).any(lambda x: x % 2 == 1)) self.assertFalse(Iter([]).any(lambda: None)) def test_at(self): - self.assertEqual(2, Iter([2,4,6]).at(0)) - self.assertEqual(6, Iter([2,4,6]).at(2)) - self.assertEqual(6, Iter([2,4,6]).at(-1)) + self.assertEqual(2, Iter([2, 4, 6]).at(0)) + self.assertEqual(6, Iter([2, 4, 6]).at(2)) + self.assertEqual(6, Iter([2, 4, 6]).at(-1)) def test_at_throws(self): with pytest.raises(IndexError): - self.assertEqual(None, Iter([2,4,6]).at(4)) + self.assertEqual(None, Iter([2, 4, 6]).at(4)) def test_avg(self): - self.assertEqual(5, Iter(range(11)).avg()) + self.assertEqual(5, Iter(0, 10).avg()) def test_chunk_by(self): expected = [[1], [2, 2], [3], [4, 4, 6], [7, 7]] @@ -59,11 +63,85 @@ def test_concant(self): self.assertEqual([1, 2, 3, 4, 5, 6], Iter.concat((1, 3), (4, 6)).image) def test_count(self): - self.assertEqual(3, Iter(range(1, 4)).count()) - self.assertEqual(2, Iter(range(1, 6)).count(lambda x: x % 2 == 0)) + self.assertEqual(3, Iter(1, 3).count()) + self.assertEqual(2, Iter(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)) + + def test_dedup(self): + self.assertEqual([1, 2, 3, 2, 1], Iter([1, 2, 3, 3, 2, 1]).dedup().image) + + @pytest.mark.xfail(raises=NotImplementedError, reason="TODO") + def test_dedup_by(self): + self.assertEqual([5, 1, 3, 2], Iter([5, 1, 2, 3, 2, 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) + + 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) + + 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_each(self): + self.assertTrue(Iter(["some", "example"]).each(print)) + + def test_empty(self): + self.assertTrue(Iter([]).empty()) + self.assertTrue(Iter(0, 0).empty()) + self.assertFalse(Iter(1, 10).empty()) + + @pytest.mark.xfail(raises=NotImplementedError, reason="TODO") + def test_fetch(self): + self.assertTrue(Iter([2, 4, 6]).fetch(0).image) + self.assertTrue(Iter([2, 4, 6]).fetch(-3).image) + self.assertTrue(Iter([2, 4, 6]).fetch(2).image) + self.assertFalse(Iter([2, 4, 6]).fetch(4).image) + + def test_filter(self): + self.assertEqual([2], Iter(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(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)) + + 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)) + + 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.assertEqual(9, Iter([2, 3, 4]).filter(lambda x: x > 2).find_value(lambda x: x * x)) + self.assertEqual("no bools!", Iter(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)]).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): + n = 3 + fun = lambda x, acc: [[x], acc+1] if acc < n else acc + self.assertEqual([[1, 2, 3], 3], Iter(1, 100).flat_map_reduce(fun, acc=0).image) #endregion - #region integration tests + #region leet code tests #endregion From 9ca9ef0389f34a26ebf5d111df03dcaa3ba98c73 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Tue, 21 Dec 2021 14:23:17 +0100 Subject: [PATCH 08/30] Implement and overload static range function --- src/iterfun/iterfun.py | 24 +++++++++++++++++++++++- tests/test_iterfun.py | 8 ++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 46c553e..a8c4482 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -259,7 +259,7 @@ def drop_while(self, fun: Callable) -> Iter: def each(self, fun: Callable) -> bool: """ - Invoke the given `fun` for each element in the `self.image`, then return + Invoke the given `fun` for each element in `self.image`, then return `True`. >>> Iter(1, 3).each(print) 1 @@ -374,5 +374,27 @@ def flat_map_reduce(self, fun: Callable[[Any, Any], Any], acc: Any) -> Iter: """ raise NotImplementedError() + @overload + @staticmethod + def range(lim: List[int, int]) -> List[int]: ... + + @overload + @staticmethod + def range(lim: Tuple[int, int]) -> List[int]: ... + + @staticmethod + def range(lim: List[int, int] | Tuple[int, int]) -> List[int]: + """ + Return a sequence of integers from start to end. + + ```python + >>> Iter.range([1, 5]) + [1, 2, 3, 4, 5] + >>> Iter.range((1, 5)) + [2, 3, 4] + ``` + """ + return list(range(lim[0], lim[1]+1) if isinstance(lim, List) else range(lim[0]+1, lim[1])) + def __str__(self) -> str: return f"[{', '.join(map(str, self.iter))}]" if isinstance(self.image, Iterable) else self.image diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index d77354f..7983a7a 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -6,7 +6,7 @@ class TestIter(unittest.TestCase): #region unit tests - def test_iter(self): + def test_iter2(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, 2, 3, 4, 5, 6, 7, 8, 9, 10], Iter(1, 10).image) @@ -131,7 +131,7 @@ def test_find_value(self): self.assertEqual("no bools!", Iter(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)]).flat_map(lambda x: list(range(x[0], x[1]+1))).image) + self.assertEqual([1, 2, 3, 4, 5, 6], Iter([(1, 3), (4, 6)]).flat_map(lambda x: Iter.range(list(x))).image) self.assertEqual([[1], [2], [3]], Iter([1, 2, 3]).flat_map(lambda x: [[x]]).image) @pytest.mark.xfail(raises=NotImplementedError, reason="TODO") @@ -140,6 +140,10 @@ def test_flat_map_reduce(self): fun = lambda x, acc: [[x], acc+1] if acc < n else acc self.assertEqual([[1, 2, 3], 3], Iter(1, 100).flat_map_reduce(fun, acc=0).image) + def test_range(self): + self.assertEqual([1, 2, 3, 4, 5], Iter.range([1, 5])) + self.assertEqual([2, 3, 4], Iter.range((1, 5))) + #endregion #region leet code tests From 2ad06d1abe28b1182f016a51320f6fba29ada7c2 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 22 Dec 2021 00:39:57 +0100 Subject: [PATCH 09/30] Overload constructor (similar to Iter.range) --- src/iterfun/iterfun.py | 23 +++++++------ tests/test_iterfun.py | 73 +++++++++++++++++++++--------------------- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index a8c4482..4b26ded 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -19,7 +19,7 @@ class Iter: indexed lookup such as `list`, `tuple`, `dict`, or `set`. ```python - >>> Iter(1, 3).map(lambda x: 2*x) + >>> Iter([1, 3]).map(lambda x: 2*x) [2, 4, 6] >>> Iter([1, 2, 3]).sum() 6 @@ -29,20 +29,23 @@ class Iter: """ @overload - def __init__(self, iter: Iterable) -> Iter: ... + def __init__(self, iter: Iterable, interval: bool=False) -> Iter: ... @overload - def __init__(self, *iter: int) -> Iter: ... + def __init__(self, iter: List[int, int], interval: bool=True) -> Iter: ... - def __init__(self, *iter: Iterable | int) -> Iter: - self.domain = iter[0] - self.image = Iter.__ctor(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.domain = iter + self.image = Iter.__ctor(iter) if interval and len(iter) == 2 else iter @staticmethod - def __ctor(iter: Iterable | int) -> List: - if isinstance(iter, Tuple) and len(iter) == 2: - return list(range(iter[0], iter[1]+1)) if iter[1] != 0 else [] - return iter[0] + def __ctor(iter: Iterable | List[int, int] | Tuple[int, int]) -> List: + if (isinstance(iter, Tuple) or isinstance(iter, List)): + return Iter.range(iter) if iter[1] != 0 else [] + return iter def all(self, fun: Optional[Callable]=None) -> bool: """ diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index 7983a7a..00ea4ae 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -6,10 +6,11 @@ class TestIter(unittest.TestCase): #region unit tests - def test_iter2(self): + def test_iter(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, 2, 3, 4, 5, 6, 7, 8, 9, 10], Iter(1, 10).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)).image) def test_all(self): self.assertTrue(Iter([1, 2, 3]).all()) @@ -37,7 +38,7 @@ def test_at_throws(self): self.assertEqual(None, Iter([2, 4, 6]).at(4)) def test_avg(self): - self.assertEqual(5, Iter(0, 10).avg()) + self.assertEqual(5, Iter([0, 10]).avg()) def test_chunk_by(self): expected = [[1], [2, 2], [3], [4, 4, 6], [7, 7]] @@ -45,16 +46,16 @@ def test_chunk_by(self): self.assertEqual(expected, actual) def test_chunk_every(self): - self.assertEqual([[1, 2], [3, 4], [5, 6]], Iter(range(1, 7)).chunk_every(2).image) - self.assertEqual([[1, 2, 3], [3, 4, 5], [5, 6]], Iter(range(1,7)).chunk_every(3, 2).image) - self.assertEqual([[1, 2, 3], [3, 4, 5], [5, 6, 7]], Iter(range(1, 7)).chunk_every(3, 2, [7]).image) - self.assertEqual([[1, 2, 3], [4]], Iter(range(1, 5)).chunk_every(3, 3, []).image) - self.assertEqual([[1, 2, 3, 4]], Iter(range(1,5)).chunk_every(10).image) - self.assertEqual([[1, 2], [4, 5]], Iter(range(1, 6)).chunk_every(2, 3, []).image) + 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) @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(range(1, 11)).chunk_while([], None, None)) + self.assertEqual([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], Iter([1, 10]).chunk_while([], None, None)) def test_concant(self): self.assertEqual([1, [2], 3, 4, 5, 6], Iter.concat([[1, [2], 3], [4], [5, 6]]).image) @@ -63,18 +64,18 @@ def test_concant(self): self.assertEqual([1, 2, 3, 4, 5, 6], Iter.concat((1, 3), (4, 6)).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, 3]).count()) + self.assertEqual(2, Iter([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([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)) def test_dedup(self): self.assertEqual([1, 2, 3, 2, 1], Iter([1, 2, 3, 3, 2, 1]).dedup().image) @@ -84,26 +85,26 @@ def test_dedup_by(self): self.assertEqual([5, 1, 3, 2], Iter([5, 1, 2, 3, 2, 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([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) 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([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) 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_each(self): - self.assertTrue(Iter(["some", "example"]).each(print)) + self.assertTrue(Iter(["some", "example"], interval=False).each(print)) def test_empty(self): self.assertTrue(Iter([]).empty()) - self.assertTrue(Iter(0, 0).empty()) - self.assertFalse(Iter(1, 10).empty()) + self.assertTrue(Iter([0, 0]).empty()) + self.assertFalse(Iter([1, 10]).empty()) @pytest.mark.xfail(raises=NotImplementedError, reason="TODO") def test_fetch(self): @@ -113,10 +114,10 @@ def test_fetch(self): self.assertFalse(Iter([2, 4, 6]).fetch(4).image) def test_filter(self): - self.assertEqual([2], Iter(1, 3).filter(lambda x: x % 2 == 0).image) + self.assertEqual([2], Iter([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([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)) @@ -128,17 +129,17 @@ 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.assertEqual(9, Iter([2, 3, 4]).filter(lambda x: x > 2).find_value(lambda x: x * x)) - self.assertEqual("no bools!", Iter(1, 3).find_value(lambda x: isinstance(x, bool), default="no bools!")) + self.assertEqual("no bools!", Iter([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)]).flat_map(lambda x: Iter.range(list(x))).image) + 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]], Iter([1, 2, 3]).flat_map(lambda x: [[x]]).image) @pytest.mark.xfail(raises=NotImplementedError, reason="TODO") def test_flat_map_reduce(self): n = 3 fun = lambda x, acc: [[x], acc+1] if acc < n else acc - self.assertEqual([[1, 2, 3], 3], Iter(1, 100).flat_map_reduce(fun, acc=0).image) + self.assertEqual([[1, 2, 3], 3], Iter([1, 100]).flat_map_reduce(fun, acc=0).image) def test_range(self): self.assertEqual([1, 2, 3, 4, 5], Iter.range([1, 5])) From b67b2b4ed1194a7c9e88fb8cd2e660b1b43bcf7e Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 22 Dec 2021 01:54:37 +0100 Subject: [PATCH 10/30] Implement and test five new methods. - [x] Implements frequencies - [x] Implements frequencies_by - [x] Implements group_by - [x] Implements intersperse - [x] Implements into --- src/iterfun/iterfun.py | 79 ++++++++++++++++++++++++++++++++++++++++++ tests/test_iterfun.py | 21 +++++++++++ 2 files changed, 100 insertions(+) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 4b26ded..d8e4f1d 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -377,6 +377,85 @@ def flat_map_reduce(self, fun: Callable[[Any, Any], Any], acc: Any) -> Iter: """ raise NotImplementedError() + def frequencies(self) -> Iter: + """ + Return a map with keys as unique elements of `self.image` and values as + the count of every element. + + ```python + >>> Iter([1, 2, 2, 3, 4, 5, 5, 6]).frequencies() + {1: 1, 2: 2, 3: 1, 4: 1, 5: 2, 6: 1} + ``` + """ + self.image = Counter(self.image) + return self + + def frequencies_by(self, key_fun: Callable) -> Iter: + """ + Return a map with keys as unique elements given by `key_fun` and values + as the count of every element. + + ```python + >>> Iter(["aa", "aA", "bb", "cc"]).frequencies_by(lambda s: s.lower()) + {"aa": 2, "bb": 1, "cc": 1} + >>> Iter(["aaa", "aA", "bbb", "cc", "c"]).frequencies_by(len) + {3: 2, 2: 2, 1: 1} + ``` + """ + self.image = Counter(map(key_fun, self.image)) + return self + + def group_by(self, key_fun: Callable, value_fun: Optional[Callable]=None) -> Iter: + """ + Split `self.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 + resulting map is unordered. + + ```python + >>> Iter(["ant", "buffalo", "cat", "dingo"]).group_by(len) + {3: ["ant", "cat"], 5: ["dingo"], 7: ["buffalo"]} + >>> Iter(["ant", "buffalo", "cat", "dingo"]).group_by(len, lambda s: s[0]) + {3: ["a", "c"], 5: ["d"], 7: ["b"]} + ``` + """ + value = lambda g: 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, seperator: Any) -> Iter: + """ + Intersperses separator between each element of `self.image`. + + ```python + >>> Iter([1, 3]).intersperse(0) + [1, 0, 2, 0, 3, 0] + >>> Iter([1]).intersperse(0) + [1] + >>> Iter([]).intersperse(0) + [] + ``` + """ + self.image = list(itertools.islice(itertools.chain.from_iterable(zip(itertools.repeat(seperator), self.image)), 1, None)) + if len(self.image) > 1: self.image.append(seperator) + return self + + def into(self, iter: Iterable) -> Iter: + """ + ```python + >>> Iter([1, 2]).into([]) + [1, 2] + >>> Iter({'a': 1, 'b': 2}).into({}) + {'a': 1, 'b': 2} + >>> Iter({'a': 1}).into({'b': 2}) + {'a': 1, 'b': 2} + ``` + """ + self.image = {**self.image, **iter} if isinstance(iter, Dict) else [*self.image, *iter] + return self + @overload @staticmethod def range(lim: List[int, int]) -> List[int]: ... diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index 00ea4ae..87bae7f 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -141,6 +141,27 @@ def test_flat_map_reduce(self): fun = lambda x, acc: [[x], acc+1] if acc < n else acc self.assertEqual([[1, 2, 3], 3], Iter([1, 100]).flat_map_reduce(fun, acc=0).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) + + def test_frequencies_by(self): + self.assertEqual({"aa": 2, "bb": 1, "cc": 1}, Iter(["aa", "aA", "bb", "cc"]).frequencies_by(lambda s: s.lower()).image) + self.assertEqual({3: 2, 2: 2, 1: 1}, Iter(["aaa", "aA", "bbb", "cc", "c"]).frequencies_by(len).image) + + 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, lambda s: s[0]).image) + + def test_intersperse(self): + self.assertEqual([1, 0, 2, 0, 3, 0], Iter([1, 3]).intersperse(0).image) + self.assertEqual([1], Iter([1]).intersperse(0).image) + self.assertEqual([], Iter([]).intersperse(0).image) + + def test_into(self): + self.assertEqual([1, 2], Iter([1, 2]).into([]).image) + 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_range(self): self.assertEqual([1, 2, 3, 4, 5], Iter.range([1, 5])) self.assertEqual([2, 3, 4], Iter.range((1, 5))) From 6ae5d9e46cabadea1d68e4e3ecbf709417bd795d Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 22 Dec 2021 14:08:36 +0100 Subject: [PATCH 11/30] Implement join, map and map_every + edit doc strings --- src/iterfun/iterfun.py | 115 +++++++++++++++++++++++++++++++---------- tests/test_iterfun.py | 15 ++++++ 2 files changed, 102 insertions(+), 28 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index d8e4f1d..59ca96e 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -7,7 +7,7 @@ import operator import random import statistics -from collections import Counter +from collections import Counter, ChainMap from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union, overload @@ -23,7 +23,7 @@ class Iter: [2, 4, 6] >>> Iter([1, 2, 3]).sum() 6 - >>> Iter({'a': 1, 'b': 2}).map(lambda k,v: {k, 2 * v} + >>> Iter({'a': 1, 'b': 2}).map(lambda k, v: {k: 2 * v} {'a': 2, 'b': 4} ``` """ @@ -59,7 +59,7 @@ def all(self, fun: Optional[Callable]=None) -> bool: False >>> Iter([]).all() True - >>> Iter([1,2,3]).all(lambda x: x % 2 == 0) + >>> Iter([1, 2, 3]).all(lambda x: x % 2 == 0) False ``` """ @@ -76,7 +76,7 @@ def any(self, fun: Optional[Callable]=None) -> bool: False >>> Iter([False, True, False]).any() True - >>> Iter([2,4,6]).any(lambda x: x % 2 == 1) + >>> Iter([2, 4, 6]).any(lambda x: x % 2 == 1) False ``` """ @@ -90,11 +90,11 @@ def at(self, index: int) -> Any: enumerable is enumerated once and the index is counted from the end. ```python - >>> Iter([2,4,6]).at(0) + >>> Iter([2, 4, 6]).at(0) 2 - >>> Iter([2,4,6]).at(-1) + >>> Iter([2, 4, 6]).at(-1) 6 - >>> Iter([2,4,6]).at(4) + >>> Iter([2, 4, 6]).at(4) IndexError: list index out of range ``` """ @@ -106,7 +106,7 @@ def avg(self) -> Union[int, float]: Return the sample arithmetic mean of `self.image`. ```python - >>> Iter(range(11)).avg() + >>> Iter([0, 10]).avg() 5 ``` """ @@ -131,11 +131,11 @@ def chunk_every(self, count: int, step: Optional[int]=None, leftover: Optional[L and, if not passed, defaults to `count`, i.e. chunks do not overlap. ```python - >>> Iter(range(1, 7)).chunk_every(2) + >>> Iter([1, 6]).chunk_every(2) [[1, 2], [3, 4], [5, 6]] - >>> Iter(range(1, 7)).chunk_every(3, 2, [7]) + >>> Iter([1, 6]).chunk_every(3, 2, [7]) [[1, 2, 3], [3, 4, 5], [5, 6, 7]] - >>> Iter(range(1, 4)).chunk_every(3, 3) + >>> Iter([1, 4]).chunk_every(3, 3) [[1, 2, 3], [4]] ``` """ @@ -175,9 +175,9 @@ def count(self, fun: Optional[Callable]=None) -> int: count of elements in `self.image` for which `fun` returns a truthy value. ```python - >>> Iter(range(1,4)).count() + >>> Iter([1, 3]).count() 3 - >>> Iter(range(1, 6)).count(lambda x: x % 2 == 0) + >>> Iter([1, 5]).count(lambda x: x % 2 == 0) 2 ``` """ @@ -189,9 +189,9 @@ def count_until(self, limit: int, fun: Optional[Callable]=None) -> int: stopping at `limit`. ```python - >>> Iter(range(1, 21)).count_until(5) + >>> Iter([1, 20]).count_until(5) 5 - >>> Iter(range(1, 21)).count_until(50) + >>> Iter([1, 20]).count_until(50) 20 ``` """ @@ -200,7 +200,7 @@ def count_until(self, limit: int, fun: Optional[Callable]=None) -> int: def dedup(self) -> Iter: """ - Enumerates `self.image`, returning a list where all consecutive duplicated + Enumerate `self.image`, returning a list where all consecutive duplicated elements are collapsed to a single element. ```python @@ -216,6 +216,11 @@ def dedup_by(self, fun: Callable): 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 + enumerated once to retrieve the proper index and the remaining calculation + is performed from the end. + ```python >>> Iter([1, 2, 3]).drop(2) [3] @@ -232,15 +237,16 @@ def drop(self, amount: int) -> Iter: 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 integer. + 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([1, 10]).drop_every(2) [2, 4, 6, 8, 10] - >>> Iter(1, 10).drop_every(0) + >>> Iter([1, 10]).drop_every(0) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - >>> Iter(1, 3).drop_every(1) + >>> Iter([1, 3]).drop_every(1) [] ``` """ @@ -264,7 +270,7 @@ def each(self, fun: Callable) -> bool: """ Invoke the given `fun` for each element in `self.image`, then return `True`. - >>> Iter(1, 3).each(print) + >>> Iter([1, 3]).each(print) 1 2 3 @@ -279,9 +285,9 @@ def empty(self) -> bool: ```python >>> Iter([]).empty() True - >>> Iter(0, 0).empty() + >>> Iter([0, 0]).empty() True - >>> Iter(1, 10).empty() + >>> Iter([1, 10]).empty() False ``` """ @@ -296,7 +302,7 @@ def filter(self, fun: Callable) -> Iter: a truthy value. ```python - >>> Iter(1, 3).filter(lambda x: x % 2 == 0) + >>> Iter([1, 3]).filter(lambda x: x % 2 == 0) [2] ``` """ @@ -309,7 +315,7 @@ def find(self, fun: Callable, default: Optional[Any]=None) -> Optional[Any]: such element is found, return `default`. ```python - >>> Iter(2, 4).find(lambda x: x % 2 == 1) + >>> Iter([2, 4]).find(lambda x: x % 2 == 1) 3 >>> Iter([2, 4, 6]).find(lambda x: x % 2 == 1) None @@ -344,7 +350,7 @@ def find_value(self, fun: Callable, default: Optional[Any]=None) -> Optional[Any None >>> Iter([2, 3, 4]).filter(lambda x: x > 2).find_value(lambda x: x * x) 9 - >>> Iter(1, 3).find_value(lambda x: isinstance(x, bool), "no bools!") + >>> Iter([1, 3]).find_value(lambda x: isinstance(x, bool), "no bools!") 'no bools!' ``` """ @@ -356,7 +362,7 @@ def flat_map(self, fun: Callable) -> Iter: Map the given `fun` over `self.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]] @@ -444,6 +450,8 @@ def intersperse(self, seperator: Any) -> Iter: def into(self, iter: Iterable) -> Iter: """ + Insert the given `self.image` into `iter`. + ```python >>> Iter([1, 2]).into([]) [1, 2] @@ -456,6 +464,57 @@ def into(self, iter: Iterable) -> Iter: self.image = {**self.image, **iter} if isinstance(iter, Dict) else [*self.image, *iter] return self + def join(self, joiner: Optional[str]=None) -> str: + """ + Join `self.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. + + ```python + >>> Iter([1,5]).join() + '12345' + >>> Iter[[1,5]].join(',') + '1,2,3,4,5' + ``` + """ + return f"{joiner or ''}".join(map(str, self.image)) + + def map(self, fun: Callable) -> 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. + + ```python + >>> Iter([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} + ``` + """ + 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) -> Iter: + """ + Return a list of results of invoking `fun` on every `nth` element of `self.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) + [1001, 2, 1003, 4, 1005, 6, 1007, 8, 1009, 10] + >>> Iter([1, 5]).map_every(0, lambda x: x + 1000) + [1, 2, 3, 4, 5] + >>> Iter([1, 3]).map_every(1, lambda x: x + 1000) + [1001, 1002, 1003] + ``` + """ + if nth != 0: + for i in range(0, len(self.image), nth): + self.image[i] = fun(self.image[i]) + return self + @overload @staticmethod def range(lim: List[int, int]) -> List[int]: ... diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index 87bae7f..59703c0 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -162,6 +162,21 @@ 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_join(self): + self.assertEqual('12345', Iter([1,5]).join()) + self.assertEqual('1,2,3,4,5', Iter([1,5]).join(',')) + + def test_map(self): + self.assertEqual([2, 4, 6], Iter([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) + def test_range(self): self.assertEqual([1, 2, 3, 4, 5], Iter.range([1, 5])) self.assertEqual([2, 3, 4], Iter.range((1, 5))) From 805a6515e0ee2423927f15d9e695b1db3ca4ff1c Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 22 Dec 2021 23:31:03 +0100 Subject: [PATCH 12/30] Implement ten new methods in the Iter class. - [x] Improves map_intersperse - [x] Implements map_join - [x] Implements map_reduce - [x] Implements max (max_by in Elixir) - [x] Implements member - [x] Implements min (min_by in Elixir) - [x] Implements min_max - [x] Implements product - [x] Implements random - [x] Implements reduce - [x] Implements reduce_while Remarks: There's doesn't seem to be a sorter parameter for the max in min built-in functions in Python, so for the moment I have resorted to rename max_by to max to stay close to Python's built-ins in this instance. The random function essentially just invokes random.choice from the standard library which is sufficiently random for the time being. As for the product function, it's equivalent for x_1 * x_2 * x_k * ... * x_b for k in range [a, b], which is different from itertools.product which yields the cartesian product of two sets. The member function uses the operator; for dictionary specifically this means it checks keys for memberships. --- src/iterfun/iterfun.py | 174 ++++++++++++++++++++++++++++++++++++++++- tests/test_iterfun.py | 54 ++++++++++++- 2 files changed, 223 insertions(+), 5 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 59ca96e..95dff23 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -431,21 +431,20 @@ def group_by(self, key_fun: Callable, value_fun: Optional[Callable]=None) -> Ite self.image = {k: value(g) for k, g in itertools.groupby(sorted(self.image, key=key_fun), key_fun)} return self - def intersperse(self, seperator: Any) -> Iter: + def intersperse(self, separator: Any) -> Iter: """ Intersperses separator between each element of `self.image`. ```python >>> Iter([1, 3]).intersperse(0) - [1, 0, 2, 0, 3, 0] + [1, 0, 2, 0, 3] >>> Iter([1]).intersperse(0) [1] >>> Iter([]).intersperse(0) [] ``` """ - self.image = list(itertools.islice(itertools.chain.from_iterable(zip(itertools.repeat(seperator), self.image)), 1, None)) - if len(self.image) > 1: self.image.append(seperator) + 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: @@ -515,6 +514,173 @@ def map_every(self, nth: int, fun: Callable) -> Iter: self.image[i] = fun(self.image[i]) return self + def map_intersperse(self, separator: Any, fun: Callable) -> Iter: + """ + Map and intersperses `self.image` in one pass. + + ```python + >>> Iter([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)) + return self + + def map_join(self, fun: Callable, joiner: Optional[str]=None) -> str: + """ + Map and join `self.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) + '246' + >>> Iter([1, 3]).map_join(lambda x: 2 * x, " = ") + '2 = 4 = 6' + ``` + """ + return f"{joiner or ''}".join(map(str, map(fun, self.image))) + + def map_reduce(self, acc: Union[int, float, complex], fun: Callable, acc_fun: Optional[Callable]) -> Iter: + """ + Invoke the given function to each element in `self.image` to reduce it to + a single element, while keeping an accumulator. Return a tuple where the + first element is the mapped enumerable and the second one is the final + accumulator. + + ```python + >>> Iter([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, lambda x, acc: x - acc) + ([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]=None, empty_fallback: Optional[Any]=None) -> Any: + """ + Return the maximal element in `self.image` as calculated by the given `fun`. + + ```python + >>> Iter([1, 3]).max() + 3 + >>> Iter("you shall not pass".split()).max() + 'you' + >>> Iter("you shall not pass".split()).max(len) + 'shall' + >>> Iter([]).max(empty_fallback='n/a') + 'n/a' + ``` + """ + return max(self.image, key=fun) if len(self.image) else empty_fallback + + def member(self, element: Any) -> bool: + """ + Checks if element exists within `self.image`. + + ```python + >>> Iter([1, 10]).member(5) + True + >>> Iter([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]=None, empty_fallback: Optional[Any]=None) -> Any: + """ + Return the minimum element in `self.image` as calculated by the given `fun`. + + ```python + >>> Iter([1, 3]).max() + 1 + >>> Iter("you shall not pass".split()).max() + 'you' + >>> Iter("you shall not pass".split()).max(len) + 'not' + >>> Iter([]).max(empty_fallback='n/a') + 'n/a' + ``` + """ + return min(self.image, key=fun) if len(self.image) else empty_fallback + + def min_max(self, fun: Optional[Callable]=None, empty_fallback: Optional[Any]=None) -> Tuple: + """ + Return a tuple with the minimal and the maximal elements in `self.image`. + + ```python + >>> Iter([1, 3]).min_max() + (1, 3) + >>> Iter([]).min_max(empty_fallback=None) + (None, None) + >>> Iter(["aaa", "a", "bb", "c", "ccc"]).min_max(len) + ('a', 'aaa') + ``` + """ + return (self.min(fun, empty_fallback), self.max(fun, empty_fallback)) + + def product(self) -> Iter: + """ + Return the product of all elements. + + ```python + >>> Iter([2, 3, 4]).product() + 24 + >>> Iter([2.0, 3.0, 4.0]).product() + 24.0 + ``` + """ + return functools.reduce(operator.mul, self.image, 1) + + def random(self) -> Any: + """ + Return a random element from `self.image`. + + ```python + >>> Iter([1, 100]).random() + 42 + >>> Iter([1, 100]).random() + 69 + ``` + """ + return random.choice(self.image) + + def reduce(self, fun: Callable, acc: Optional[Any]=None) -> Any: + """ + Invoke `fun` for each element in `self.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(lambda x, acc: x + acc) + 10 + >>> Iter([1, 4]).reduce(lambda x, acc: x * acc, acc=1) + 24 + ``` + """ + return functools.reduce(fun, self.image, acc or 0) + + def reduce_while(self, fun: Callable, acc: Optional[Any]=None) -> Any: + """ + Reduce `self.image` until `fun` returns `(False, acc)`. + + ```python + >>> Iter([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) + 0 + ``` + """ + acc = acc or 0 + return functools.reduce(lambda acc, x: fun(x, acc)[1], filter(lambda x: fun(x, acc)[0], self.image), acc) + @overload @staticmethod def range(lim: List[int, int]) -> List[int]: ... diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index 59703c0..dff08b6 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -153,7 +153,7 @@ def test_group_by(self): self.assertEqual({3: ["a", "c"], 5: ["d"], 7: ["b"]}, Iter(["ant", "buffalo", "cat", "dingo"]).group_by(len, lambda s: s[0]).image) def test_intersperse(self): - self.assertEqual([1, 0, 2, 0, 3, 0], Iter([1, 3]).intersperse(0).image) + self.assertEqual([1, 0, 2, 0, 3], Iter([1, 3]).intersperse(0).image) self.assertEqual([1], Iter([1]).intersperse(0).image) self.assertEqual([], Iter([]).intersperse(0).image) @@ -177,6 +177,58 @@ def test_map_every(self): 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) + def test_map_intersperse(self): + self.assertEqual([2, None, 4, None, 6], Iter([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, " = ")) + + def test_map_reduce(self): + self.assertEqual(([2, 4, 6], 6), Iter([1, 3]).map_reduce(0, lambda x: 2 * x, lambda x, acc: x + acc).image) + self.assertEqual(([1, 4, 9], 0), Iter([1, 3]).map_reduce(6, lambda x: x * x, lambda x, acc: x - acc).image) + + def test_max(self): + self.assertEqual(3, Iter([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([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): + 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((None, None), Iter([]).min_max(empty_fallback=None)) + self.assertEqual(('a', 'aaa'), Iter(["aaa", "a", "bb", "c", "ccc"]).min_max(len)) + + 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_random(self): + numbers = Iter.range([1, 100]) + self.assertIn(Iter(numbers).random(), numbers) + + def test_reduce(self): + self.assertEqual(10, Iter([1, 4]).reduce(lambda x, acc: x + acc)) + self.assertEqual(24, Iter([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)) + def test_range(self): self.assertEqual([1, 2, 3, 4, 5], Iter.range([1, 5])) self.assertEqual([2, 3, 4], Iter.range((1, 5))) From 005d808da30b63e3828702f9fa7944fa71bf7311 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 22 Dec 2021 23:46:51 +0100 Subject: [PATCH 13/30] Remove len method call for non-zero check --- src/iterfun/iterfun.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 95dff23..97db670 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -573,7 +573,7 @@ def max(self, fun: Optional[Callable]=None, empty_fallback: Optional[Any]=None) 'n/a' ``` """ - return max(self.image, key=fun) if len(self.image) else empty_fallback + return max(self.image, key=fun) if self.image else empty_fallback def member(self, element: Any) -> bool: """ @@ -609,7 +609,7 @@ def min(self, fun: Optional[Callable]=None, empty_fallback: Optional[Any]=None) 'n/a' ``` """ - return min(self.image, key=fun) if len(self.image) else empty_fallback + return min(self.image, key=fun) if self.image else empty_fallback def min_max(self, fun: Optional[Callable]=None, empty_fallback: Optional[Any]=None) -> Tuple: """ From 7a60fe152916f5010fc609eeb43ab3665d1160a7 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 22 Dec 2021 23:55:24 +0100 Subject: [PATCH 14/30] Add fix for Python 3.7: key cannot be None Albeit this changed in Python 3.8, we will add an additional fun is not None check for 3.7 support. This version of Python is scheduled to reach EOL on 27 Jun 2023. --- src/iterfun/iterfun.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 97db670..778aa8b 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -573,7 +573,7 @@ def max(self, fun: Optional[Callable]=None, empty_fallback: Optional[Any]=None) 'n/a' ``` """ - return max(self.image, key=fun) if self.image else empty_fallback + 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: """ @@ -609,7 +609,7 @@ def min(self, fun: Optional[Callable]=None, empty_fallback: Optional[Any]=None) 'n/a' ``` """ - return min(self.image, key=fun) if self.image else empty_fallback + 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]=None, empty_fallback: Optional[Any]=None) -> Tuple: """ From 2374af826d50ef09277ad2f3b7fcd530a23dd668 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Thu, 23 Dec 2021 00:36:36 +0100 Subject: [PATCH 15/30] Improve type hints for callable parameters and change example in find_value method (doc strings and unit test) --- src/iterfun/iterfun.py | 62 +++++++++++++++++++++++------------------- tests/test_iterfun.py | 4 +-- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 778aa8b..d45a5e7 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -47,7 +47,7 @@ def __ctor(iter: Iterable | List[int, int] | Tuple[int, int]) -> List: return Iter.range(iter) if iter[1] != 0 else [] return iter - def all(self, fun: Optional[Callable]=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`. @@ -66,7 +66,7 @@ def all(self, fun: Optional[Callable]=None) -> bool: self.image = all(self.image) if fun is None else all(map(fun, self.image)) return self.image - def any(self, fun: Optional[Callable]=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`. @@ -113,7 +113,7 @@ def avg(self) -> Union[int, float]: self.image = statistics.mean(self.image) return self.image - def chunk_by(self, fun: Callable) -> Iter: + def chunk_by(self, fun: Callable[[Any], bool]) -> Iter: """ Split `self.image` on every element for which `fun` returns a new value. @@ -169,7 +169,7 @@ def concat(*iter: List[Any] | Tuple[int, int]) -> Iter: """ return Iter(list(itertools.chain(*(iter[0] if isinstance(iter[0], List) else [range(t[0], t[1]+1) for t in iter])))) - def count(self, fun: Optional[Callable]=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. @@ -183,7 +183,7 @@ def count(self, fun: Optional[Callable]=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]=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, stopping at `limit`. @@ -211,7 +211,7 @@ def dedup(self) -> Iter: self.image = [group[0] for group in itertools.groupby(self.image)] return self - def dedup_by(self, fun: Callable): + def dedup_by(self, fun: Callable[[Any], bool]): raise NotImplementedError() def drop(self, amount: int) -> Iter: @@ -253,7 +253,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) -> Iter: + def drop_while(self, fun: Callable[[Any], bool]) -> Iter: """ Drop elements at the beginning of the enumerable while `fun` returns a truthy value. @@ -266,7 +266,7 @@ def drop_while(self, fun: Callable) -> Iter: self.image = self.image[self.image.index(list(itertools.filterfalse(fun, self.image))[0]):] return self - def each(self, fun: Callable) -> bool: + def each(self, fun: Callable[[Any], Any]) -> bool: """ Invoke the given `fun` for each element in `self.image`, then return `True`. @@ -296,7 +296,7 @@ def empty(self) -> bool: def fetch(self, index: int) -> bool: raise NotImplementedError() - def filter(self, fun: Callable) -> Iter: + def filter(self, fun: Callable[[Any], bool]) -> Iter: """ Filter `self.image`, i.e. return only those elements for which `fun` returns a truthy value. @@ -309,7 +309,7 @@ def filter(self, fun: Callable) -> Iter: self.image = list(filter(fun, self.image)) return self - def find(self, fun: Callable, 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`. @@ -325,7 +325,7 @@ def find(self, fun: Callable, default: Optional[Any]=None) -> Optional[Any]: """ return next(filter(fun, self.image), default) - def find_index(self, fun: Callable, 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 instead of the element itself. @@ -340,7 +340,7 @@ def find_index(self, fun: Callable, default: Optional[Any]=None) -> Optional[Any 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, 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. @@ -348,8 +348,8 @@ def find_value(self, fun: Callable, default: Optional[Any]=None) -> Optional[Any ```python >>> Iter([2, 4, 6]).find_value(lambda x: x % 2 == 1) None - >>> Iter([2, 3, 4]).filter(lambda x: x > 2).find_value(lambda x: x * x) - 9 + >>> Iter([2, 3, 4]).find_value(lambda x: x % 2 == 1) + True >>> Iter([1, 3]).find_value(lambda x: isinstance(x, bool), "no bools!") 'no bools!' ``` @@ -357,7 +357,7 @@ def find_value(self, fun: Callable, default: Optional[Any]=None) -> Optional[Any found = next(filter(fun, self.image), default) return fun(found) if found is not default else default - def flat_map(self, fun: Callable) -> Iter: + def flat_map(self, fun: Callable[[Any], Any]) -> Iter: """ Map the given `fun` over `self.image` and flattens the result. @@ -396,13 +396,13 @@ def frequencies(self) -> Iter: self.image = Counter(self.image) return self - def frequencies_by(self, key_fun: Callable) -> Iter: + def frequencies_by(self, key_fun: Callable[[Any], Any]) -> Iter: """ Return a map with keys as unique elements given by `key_fun` and values as the count of every element. ```python - >>> Iter(["aa", "aA", "bb", "cc"]).frequencies_by(lambda s: s.lower()) + >>> Iter(["aa", "aA", "bb", "cc"]).frequencies_by(str.lower) {"aa": 2, "bb": 1, "cc": 1} >>> Iter(["aaa", "aA", "bbb", "cc", "c"]).frequencies_by(len) {3: 2, 2: 2, 1: 1} @@ -411,7 +411,7 @@ def frequencies_by(self, key_fun: Callable) -> Iter: self.image = Counter(map(key_fun, self.image)) return self - def group_by(self, key_fun: Callable, value_fun: Optional[Callable]=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`. @@ -478,7 +478,13 @@ def join(self, joiner: Optional[str]=None) -> str: """ return f"{joiner or ''}".join(map(str, self.image)) - def map(self, fun: Callable) -> Iter: + @overload + def map(self, fun: Callable[[Any], Any]) -> Iter: ... + + @overload + def map(self, fun: Callable[[Any, Any], Dict]) -> Iter: ... + + 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 @@ -494,7 +500,7 @@ def map(self, fun: Callable) -> Iter: 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) -> 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`, starting with the first element. The first element is always passed to the given @@ -514,7 +520,7 @@ def map_every(self, nth: int, fun: Callable) -> Iter: self.image[i] = fun(self.image[i]) return self - def map_intersperse(self, separator: Any, fun: Callable) -> Iter: + def map_intersperse(self, separator: Any, fun: Callable[[Any], Any]) -> Iter: """ Map and intersperses `self.image` in one pass. @@ -526,7 +532,7 @@ def map_intersperse(self, separator: Any, fun: Callable) -> 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, 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 defaults to an empty string. All elements returned from invoking `fun` must @@ -541,7 +547,7 @@ def map_join(self, fun: Callable, joiner: Optional[str]=None) -> str: """ return f"{joiner or ''}".join(map(str, map(fun, self.image))) - def map_reduce(self, acc: Union[int, float, complex], fun: Callable, acc_fun: Optional[Callable]) -> Iter: + 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 a single element, while keeping an accumulator. Return a tuple where the @@ -558,7 +564,7 @@ def map_reduce(self, acc: Union[int, float, complex], fun: Callable, acc_fun: Op self.image = (list(map(fun, self.image)), functools.reduce(acc_fun, self.image, acc)) return self - def max(self, fun: Optional[Callable]=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`. @@ -594,7 +600,7 @@ def member(self, element: Any) -> bool: """ return element in self.image - def min(self, fun: Optional[Callable]=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`. @@ -611,7 +617,7 @@ def min(self, fun: Optional[Callable]=None, empty_fallback: Optional[Any]=None) """ 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]=None, empty_fallback: Optional[Any]=None) -> Tuple: + def min_max(self, fun: Optional[Callable[[Any], Any]]=None, empty_fallback: Optional[Any]=None) -> Tuple: """ Return a tuple with the minimal and the maximal elements in `self.image`. @@ -652,7 +658,7 @@ def random(self) -> Any: """ return random.choice(self.image) - def reduce(self, fun: Callable, acc: Optional[Any]=None) -> Any: + def reduce(self, fun: Callable[[Any, Any], Any], acc: Optional[Any]=None) -> Any: """ Invoke `fun` for each element in `self.image` with the accumulator. The accumulator defaults to `0` if not otherwise specified. Reduce (sometimes @@ -667,7 +673,7 @@ def reduce(self, fun: Callable, acc: Optional[Any]=None) -> Any: """ return functools.reduce(fun, self.image, acc or 0) - def reduce_while(self, fun: Callable, acc: Optional[Any]=None) -> Any: + def reduce_while(self, fun: Callable[[Any, Any], Tuple[bool, Any]], acc: Optional[Any]=None) -> Any: """ Reduce `self.image` until `fun` returns `(False, acc)`. diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index dff08b6..aab63c7 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -128,7 +128,7 @@ def test_find_index(self): 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.assertEqual(9, Iter([2, 3, 4]).filter(lambda x: x > 2).find_value(lambda x: x * x)) + 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!")) def test_flat_map(self): @@ -145,7 +145,7 @@ 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) def test_frequencies_by(self): - self.assertEqual({"aa": 2, "bb": 1, "cc": 1}, Iter(["aa", "aA", "bb", "cc"]).frequencies_by(lambda s: s.lower()).image) + self.assertEqual({"aa": 2, "bb": 1, "cc": 1}, Iter(["aa", "aA", "bb", "cc"]).frequencies_by(str.lower).image) self.assertEqual({3: 2, 2: 2, 1: 1}, Iter(["aaa", "aA", "bbb", "cc", "c"]).frequencies_by(len).image) def test_group_by(self): From 2cbd675db9712ea1384e5f9bc990f1b456f83a70 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Thu, 23 Dec 2021 14:04:44 +0100 Subject: [PATCH 16/30] Implement five new methods and improve test suite - [x] Implements reject - [x] Implements reverse - [x] Implements reverse_slice - [x] Implements scan - [x] Implements shuffle Some example now also incorporate the operator module in place of lambda expression. --- src/iterfun/iterfun.py | 126 ++++++++++++++++++++++++++++++++++------- tests/test_iterfun.py | 43 ++++++++++---- 2 files changed, 138 insertions(+), 31 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index d45a5e7..66ba190 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -371,7 +371,7 @@ 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, fun: Callable[[Any, Any], Any], acc: Any) -> Iter: + def flat_map_reduce(self, fun: Callable[[Any, Any], Any], acc: int=1) -> Iter: """ Map and reduce an `self.image`, flattening the given results (only one level deep). It expects an accumulator and a function that receives each @@ -423,7 +423,7 @@ def group_by(self, key_fun: Callable[[Any], Any], value_fun: Optional[Callable[[ ```python >>> Iter(["ant", "buffalo", "cat", "dingo"]).group_by(len) {3: ["ant", "cat"], 5: ["dingo"], 7: ["buffalo"]} - >>> Iter(["ant", "buffalo", "cat", "dingo"]).group_by(len, lambda s: s[0]) + >>> Iter(["ant", "buffalo", "cat", "dingo"]).group_by(len, operator.itemgetter(0)) {3: ["a", "c"], 5: ["d"], 7: ["b"]} ``` """ @@ -557,7 +557,7 @@ def map_reduce(self, acc: Union[int, float, complex], fun: Callable[[Any], Any], ```python >>> Iter([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, lambda x, acc: x - acc) + >>> Iter([1, 3]).map_reduce(6, lambda x: x * x, operator.sub) ([1, 4, 9], 0) ``` """ @@ -658,22 +658,44 @@ def random(self) -> Any: """ return random.choice(self.image) - def reduce(self, fun: Callable[[Any, Any], Any], acc: Optional[Any]=None) -> Any: + @overload + @staticmethod + def range(lim: List[int, int]) -> List[int]: ... + + @overload + @staticmethod + def range(lim: Tuple[int, int]) -> List[int]: ... + + @staticmethod + def range(lim: List[int, int] | Tuple[int, int]) -> List[int]: + """ + Return a sequence of integers from start to end. + + ```python + >>> Iter.range([1, 5]) + [1, 2, 3, 4, 5] + >>> Iter.range((1, 5)) + [2, 3, 4] + ``` + """ + return list(range(lim[0], lim[1]+1) if isinstance(lim, List) else range(lim[0]+1, lim[1])) + + def reduce(self, fun: Callable[[Any, Any], Any], acc: int=0) -> Any: """ Invoke `fun` for each element in `self.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(lambda x, acc: x + acc) + >>> Iter([1, 4]).reduce(operator.add) 10 >>> Iter([1, 4]).reduce(lambda x, acc: x * acc, acc=1) 24 ``` """ - return functools.reduce(fun, self.image, acc or 0) + return functools.reduce(fun, self.image, acc) - def reduce_while(self, fun: Callable[[Any, Any], Tuple[bool, Any]], acc: Optional[Any]=None) -> Any: + def reduce_while(self, fun: Callable[[Any, Any], Tuple[bool, Any]], acc: int=0) -> Any: """ Reduce `self.image` until `fun` returns `(False, acc)`. @@ -684,30 +706,92 @@ def reduce_while(self, fun: Callable[[Any, Any], Tuple[bool, Any]], acc: Optiona 0 ``` """ - acc = acc or 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: + """ + Return a list of elements in `self.image` excluding those for which the + function `fun` returns a truthy value. + + ```python + >>> Iter([1, 3]).reject(lambda x: x % 2 == 0) + [1, 3] + ``` + """ + self.image = list(itertools.filterfalse(fun, self.image)) + return self + @overload - @staticmethod - def range(lim: List[int, int]) -> List[int]: ... + def reverse(self) -> Iter: ... @overload - @staticmethod - def range(lim: Tuple[int, int]) -> List[int]: ... + def reverse(self, tail: Optional[List]=None) -> Iter: ... - @staticmethod - def range(lim: List[int, int] | Tuple[int, int]) -> List[int]: + def reverse(self, tail: Optional[List]=None) -> Iter: """ - Return a sequence of integers from start to end. + Return a list of elements in `self.image` in reverse order. ```python - >>> Iter.range([1, 5]) - [1, 2, 3, 4, 5] - >>> Iter.range((1, 5)) - [2, 3, 4] + >>> Iter([1, 5]).reverse() + [5, 4, 3, 2, 1] ``` """ - return list(range(lim[0], lim[1]+1) if isinstance(lim, List) else range(lim[0]+1, lim[1])) + 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`. + + ```python + >>> Iter([1, 6]).reverse_slice(2, 4) + [1, 2, 6, 5, 4, 3] + >>> Iter([1, 10]).reverse_slice(2, 4) + [1, 2, 6, 5, 4, 3, 7, 8, 9, 10] + >>> Iter([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: + """ + Apply the given function to each element in `self.image`, storing the result + in a list and passing it as the accumulator for the next computation. Uses + the first element in the enumerable as the starting value if `acc` is `None`, + else uses the given `acc` as the starting value. + + ```python + >>> Iter([1, 5]).scan(operator.add) + [1, 3, 6, 10, 15] + >>> Iter([1, 5]).scan(lambda x, y: x + y, acc=0) + [1, 3, 6, 10, 15] + ``` + """ + acc = acc if acc is not None else self.image[0] + self.image = list(itertools.accumulate(self.image[acc==self.image[0]:], fun, initial=acc))[acc!=self.image[0]:] + return self + + def shuffle(self) -> Iter: + """ + Return a list with the elements of `self.image` shuffled. + + ```python + >>> Iter([1, 3]).shuffle() + [3, 2, 1] + >>> Iter([1, 3]).shuffle() + >>> [2, 1, 3] + ``` + """ + random.shuffle(self.image) + return self def __str__(self) -> str: - return f"[{', '.join(map(str, self.iter))}]" if isinstance(self.image, Iterable) else self.image + return f"[{', '.join(map(str, self.image))}]" if isinstance(self.image, Iterable) else self.image + + def __repr__(self) -> str: + raise NotImplementedError() diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index aab63c7..aeac765 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -1,8 +1,11 @@ +import operator import unittest + import pytest from src.iterfun import Iter + class TestIter(unittest.TestCase): #region unit tests @@ -150,7 +153,7 @@ def test_frequencies_by(self): 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, lambda s: s[0]).image) + 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) @@ -163,11 +166,11 @@ 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([1, 5]).join()) + self.assertEqual('1,2,3,4,5', Iter([1, 5]).join(',')) def test_map(self): - self.assertEqual([2, 4, 6], Iter([1,3]).map(lambda x: 2*x).image) + self.assertEqual([2, 4, 6], Iter([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) @@ -185,8 +188,8 @@ def test_map_join(self): self.assertEqual('2 = 4 = 6', Iter([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, lambda x, acc: x + acc).image) - self.assertEqual(([1, 4, 9], 0), Iter([1, 3]).map_reduce(6, lambda x: x * x, lambda x, acc: x - acc).image) + 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) def test_max(self): self.assertEqual(3, Iter([1, 3]).max()) @@ -220,8 +223,12 @@ def test_random(self): numbers = Iter.range([1, 100]) 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_reduce(self): - self.assertEqual(10, Iter([1, 4]).reduce(lambda x, acc: x + acc)) + self.assertEqual(10, Iter([1, 4]).reduce(operator.add)) self.assertEqual(24, Iter([1, 4]).reduce(lambda x, acc: x * acc, acc=1)) def test_reduce_while(self): @@ -229,9 +236,25 @@ def test_reduce_while(self): 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)) - 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_reject(self): + self.assertEqual([1, 3], Iter([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) + + 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) + + 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) + + def test_shuffle(self): + ... + #endregion From ab2b2bf4023b58f60ea9f0fddb1d230d420809c9 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Thu, 23 Dec 2021 20:32:02 +0100 Subject: [PATCH 17/30] Implement slice and slice methods with overloads --- src/iterfun/iterfun.py | 154 +++++++++++++++++++++++++++++++++-------- tests/test_iterfun.py | 22 +++++- 2 files changed, 146 insertions(+), 30 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 66ba190..8d1ece0 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -149,14 +149,7 @@ def chunk_while(self, acc: List, chunk_fun: Callable, chunk_after: Callable) -> @overload @staticmethod - def concat(iter: List[Any]) -> Iter: ... - - @overload - @staticmethod - def concat(*iter: Tuple[int, int]) -> Iter: ... - - @staticmethod - def concat(*iter: List[Any] | Tuple[int, int]) -> Iter: + def concat(iter: List[Any]) -> Iter: """ Given a list of lists, concatenates the list into a single list. @@ -167,6 +160,14 @@ def concat(*iter: List[Any] | Tuple[int, int]) -> Iter: [1, 2, 3, 4, 5, 6] ``` """ + ... + + @overload + @staticmethod + def concat(*iter: Tuple[int, int]) -> Iter: ... + + @staticmethod + def concat(*iter: List[Any] | Tuple[int, int]) -> Iter: return Iter(list(itertools.chain(*(iter[0] if isinstance(iter[0], List) else [range(t[0], t[1]+1) for t in iter])))) def count(self, fun: Optional[Callable[[Any], bool]]=None) -> int: @@ -479,11 +480,6 @@ def join(self, joiner: Optional[str]=None) -> str: return f"{joiner or ''}".join(map(str, self.image)) @overload - def map(self, fun: Callable[[Any], Any]) -> Iter: ... - - @overload - def map(self, fun: Callable[[Any, Any], Dict]) -> Iter: ... - def map(self, fun: Callable[[Any], Any]) -> Iter: """ Return a list where each element is the result of invoking `fun` on each @@ -497,6 +493,12 @@ def map(self, fun: Callable[[Any], Any]) -> Iter: {'a': -1, 'b': -2} ``` """ + ... + + @overload + def map(self, fun: Callable[[Any, Any], Dict]) -> Iter: ... + + 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)) return self @@ -660,14 +662,7 @@ def random(self) -> Any: @overload @staticmethod - def range(lim: List[int, int]) -> List[int]: ... - - @overload - @staticmethod - def range(lim: Tuple[int, int]) -> List[int]: ... - - @staticmethod - def range(lim: List[int, int] | Tuple[int, int]) -> List[int]: + def range(bounds: List[int, int]) -> List[int]: """ Return a sequence of integers from start to end. @@ -678,7 +673,15 @@ def range(lim: List[int, int] | Tuple[int, int]) -> List[int]: [2, 3, 4] ``` """ - return list(range(lim[0], lim[1]+1) if isinstance(lim, List) else range(lim[0]+1, lim[1])) + ... + + @overload + @staticmethod + def range(bounds: Tuple[int, int]) -> List[int]: ... + + @staticmethod + def range(bounds: List[int, int] | Tuple[int, int]) -> List[int]: + return list(range(bounds[0], bounds[1]+1) if isinstance(bounds, List) else range(bounds[0]+1, bounds[1])) def reduce(self, fun: Callable[[Any, Any], Any], acc: int=0) -> Any: """ @@ -722,12 +725,7 @@ def reject(self, fun: Callable[[Any], bool]) -> Iter: return self @overload - def reverse(self) -> Iter: ... - - @overload - def reverse(self, tail: Optional[List]=None) -> Iter: ... - - def reverse(self, tail: Optional[List]=None) -> Iter: + def reverse(self) -> Iter: """ Return a list of elements in `self.image` in reverse order. @@ -735,7 +733,21 @@ def reverse(self, tail: Optional[List]=None) -> Iter: >>> Iter([1, 5]).reverse() [5, 4, 3, 2, 1] ``` + + Reverse the elements in `self.image`, appends the `tail`, and returns it + as a list. + + ```python + >>> Iter([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: self.image = list(reversed(self.image)) if tail: self.image.extend(tail) return self @@ -790,6 +802,92 @@ def shuffle(self) -> Iter: random.shuffle(self.image) return self + @overload + def slice(self, index: List[int]) -> Iter: + """ + Return a subset list of `self.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.\ + + 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]) + [6, 7, 8, 9, 10, 11] + >>> Iter([1, 10]).slice([5, 20]) + [6, 7, 8, 9, 10] + >>> Iter([1, 30]).slice([-5, -1]) + [26, 27, 28, 29, 30] + ``` + + Alternatively, return a subset list of the given enumerable, 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. + + A negative `index` can be passed, which means `self.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) + [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: + if isinstance(index, List): + self.image = self.image[index[0]:] if index[1] == -1 else self.image[index[0]:index[1]+1] + else: + self.image = self.image[index:amount] if abs(index) <= len(self.image) else [] + return self + + @overload + def slide(self, index: int, insertion_index: int) -> Iter: + """ + Slide a single or multiple elements given by `index` from `self.image` to + `insertion_index`. The semantics of the range to be moved match the semantics + of `self.slice()`. + + ```python + >>> Iter(list("abcdefg")).slide(5, 1) + ['a', 'f', 'b', 'c', 'd', 'e', 'g'] + ``` + """ + ... + + @overload + def slide(self, index: List[int], insertion_index: int) -> Iter: ... + + def slide(self, index: int | List[int], insertion_index: int) -> Iter: + if isinstance(index, List): + if (max(index) + len(self.image) if max(index) < 0 else max(index)) > insertion_index: + # sliding backwards + p1 = self.image[:insertion_index] + p3 = self.image[index[0]:index[1]+1] + p2 = self.image[insertion_index:index[0]] + p4 = self.image[index[1]+1:] + self.image = list(itertools.chain(p1, p3, p2, p4)) + else: + # sliding forwards + p1 = self.image[:index[0]] + p2 = self.image[index[0]:index[1]+1] + p3 = self.image[index[1]+1:insertion_index+1] + p4 = self.image[insertion_index+1:] + 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) + return self + def __str__(self) -> str: return f"[{', '.join(map(str, self.image))}]" if isinstance(self.image, Iterable) else self.image diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index aeac765..d6c56d7 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -253,8 +253,26 @@ def test_scan(self): self.assertEqual([1, 3, 6, 10, 15], Iter([1, 5]).scan(lambda x, y: x + y, acc=0).image) def test_shuffle(self): - ... - + iter = Iter([1, 10]).shuffle() + self.assertTrue(iter.all(lambda x: x in Iter.range([1, 10]))) + + 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) + + 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") + self.assertEqual(['a', 'd', 'e', 'f', 'b', 'c', 'g'], Iter(list("abcdefg")).slide([3, 5], 1).image, msg="Slide a range of elements backward") + self.assertEqual(['a', 'e', 'f', 'b', 'c', 'd', 'g'], Iter(list("abcdefg")).slide([1, 3], 5).image, msg="Slide a range of elements forward") + self.assertEqual(['a', 'd', 'e', 'f', 'b', 'c', 'g'], Iter(list("abcdefg")).slide([-4, -2], 1).image, msg="Slide with negative indices (counting from the end)") + self.assertEqual(['a', 'b', 'c', 'e', 'f', 'g', 'd'], Iter(list("abcdefg")).slide(3, -1).image, msg="Slide with negative indices (counting from the end)") + self.assertEqual(['a', 'b', 'c', 'e', 'f', 'd', 'g'], Iter(list("abcdefg")).slide(3, -2).image, msg="Slide with negative indices (counting from the end)") #endregion From bef7b0b4a644124e1265bf2a0903cd1845cc87d7 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 24 Dec 2021 02:19:25 +0100 Subject: [PATCH 18/30] Fix and refactor scan method for Python 3.7 The optional accumulator parameter was introduced in 3.8, so this workaround replicates this option with an additional map invocation. --- src/iterfun/iterfun.py | 4 ++-- tests/test_iterfun.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 8d1ece0..b0a55ad 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -784,8 +784,8 @@ def scan(self, fun: Callable[[Any, Any], Any], acc: Optional[int]=None) -> Iter: [1, 3, 6, 10, 15] ``` """ - acc = acc if acc is not None else self.image[0] - self.image = list(itertools.accumulate(self.image[acc==self.image[0]:], fun, initial=acc))[acc!=self.image[0]:] + acc = acc if acc is not None else 0 + self.image = list(map(lambda x: x + acc, itertools.accumulate(self.image, fun))) return self def shuffle(self) -> Iter: diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index d6c56d7..9d1f537 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -251,6 +251,7 @@ def test_reverse_slice(self): 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) def test_shuffle(self): iter = Iter([1, 10]).shuffle() From 3e9f947b4393698b7146ee6a1fb9dbf990257a2f Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 24 Dec 2021 02:23:11 +0100 Subject: [PATCH 19/30] Refactor drop_while method by using itertools --- src/iterfun/iterfun.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index b0a55ad..e44edf2 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -264,7 +264,7 @@ def drop_while(self, fun: Callable[[Any], bool]) -> Iter: [3, 2, 1] ``` """ - self.image = self.image[self.image.index(list(itertools.filterfalse(fun, self.image))[0]):] + self.image = list(itertools.dropwhile(fun, self.image)) return self def each(self, fun: Callable[[Any], Any]) -> bool: From d8684c45a29d9da6af07cd2580dc7d66a73d1382 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 24 Dec 2021 22:44:12 +0100 Subject: [PATCH 20/30] Implement thirteen new methods and add zip_reduce signature - [x] Implements sort - [x] Implements split - [x] Implements split_with - [x] Implements split_while - [x] Implements sum - [x] Implements take - [x] Implements take_every - [x] Implements take_random - [x] Implements take_while - [x] Implements uniq - [x] Implements unzip - [x] Implements with_index - [x] Implements zip - [ ] Implements zip_reduce --- src/iterfun/iterfun.py | 325 +++++++++++++++++++++++++++++++++++++++-- tests/test_iterfun.py | 66 +++++++++ 2 files changed, 377 insertions(+), 14 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index e44edf2..c73ecdf 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -86,8 +86,8 @@ 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` - if `index` is out of bonds. A negative index can be passed, which means the - enumerable is enumerated once and the index is counted from the end. + 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. ```python >>> Iter([2, 4, 6]).at(0) @@ -256,7 +256,7 @@ def drop_every(self, nth: int) -> Iter: def drop_while(self, fun: Callable[[Any], bool]) -> Iter: """ - Drop elements at the beginning of the enumerable while `fun` returns a + Drop elements at the beginning of `self.image` while `fun` returns a truthy value. ```python @@ -376,7 +376,7 @@ def flat_map_reduce(self, fun: Callable[[Any, Any], Any], acc: int=1) -> Iter: """ Map and reduce an `self.image`, flattening the given results (only one level deep). It expects an accumulator and a function that receives each - enumerable element, and must return a... + iterable element, and must return a... ```python >>> #example @@ -553,7 +553,7 @@ def map_reduce(self, acc: Union[int, float, complex], fun: Callable[[Any], Any], """ Invoke the given function to each element in `self.image` to reduce it to a single element, while keeping an accumulator. Return a tuple where the - first element is the mapped enumerable and the second one is the final + first element is the mapped `self.image` and the second one is the final accumulator. ```python @@ -634,7 +634,7 @@ def min_max(self, fun: Optional[Callable[[Any], Any]]=None, empty_fallback: Opti """ return (self.min(fun, empty_fallback), self.max(fun, empty_fallback)) - def product(self) -> Iter: + def product(self) -> Union[float, int, complex]: """ Return the product of all elements. @@ -774,7 +774,7 @@ 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 in a list and passing it as the accumulator for the next computation. Uses - the first element in the enumerable as the starting value if `acc` is `None`, + the first element in `self.image` as the starting value if `acc` is `None`, else uses the given `acc` as the starting value. ```python @@ -823,10 +823,10 @@ def slice(self, index: List[int]) -> Iter: [26, 27, 28, 29, 30] ``` - Alternatively, return a subset list of the given enumerable, 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. + 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. A negative `index` can be passed, which means `self.image` is enumerated once and the index is counted from the end (for example, `-1` starts slicing @@ -860,6 +860,12 @@ def slide(self, index: int, insertion_index: int) -> Iter: ```python >>> Iter(list("abcdefg")).slide(5, 1) ['a', 'f', 'b', 'c', 'd', 'e', 'g'] + >>> Iter(list("abcdefg")).slide([3, 5], 1) # slide backwards + ['a', 'd', 'e', 'f', 'b', 'c', 'g'] + >>> Iter(list("abcdefg")).slide([1, 3], 5) # slide forwards + ['a', 'e', 'f', 'b', 'c', 'd', 'g'] + >>> Iter(list("abcdefg")).slide(3, -1) + ['a', 'b', 'c', 'e', 'f', 'g', 'd'] ``` """ ... @@ -870,14 +876,14 @@ def slide(self, index: List[int], insertion_index: int) -> Iter: ... def slide(self, index: int | List[int], insertion_index: int) -> Iter: if isinstance(index, List): if (max(index) + len(self.image) if max(index) < 0 else max(index)) > insertion_index: - # sliding backwards + # slide backwards p1 = self.image[:insertion_index] p3 = self.image[index[0]:index[1]+1] p2 = self.image[insertion_index:index[0]] p4 = self.image[index[1]+1:] self.image = list(itertools.chain(p1, p3, p2, p4)) else: - # sliding forwards + # slide forwards p1 = self.image[:index[0]] p2 = self.image[index[0]:index[1]+1] p3 = self.image[index[1]+1:insertion_index+1] @@ -888,8 +894,299 @@ 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: + """ + 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). + + Use `functools.cmp_to_key()` to convert an old-style cmp function to a key + function. + + ```python + >>> Iter([3, 1, 2]).sort() + [1, 2, 3] + ``` + """ + self.image = sorted(self.image, key=fun, reverse=descending) + return self + + def split(self, count: int) -> Iter: + """ + Split `self.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`. + + ```python + >>> Iter([1, 3]).split(2) + [[1,2], [3]] + >>> Iter([1, 3]).split(10) + [[1, 2, 3], []] + >>> Iter([1, 3]).split(0) + [[], [1, 2, 3]] + >>> Iter([1, 3]).split(-1) + [[1, 2], [3]] + ``` + """ + self.image = [self.image[:count], self.image[count:]] + return self + + def split_while(self, fun: Callable[[Any], bool]) -> Iter: + """ + Split `self.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) + [[1, 2], [3, 4]] + >>> Iter([1, 4]).split_while(lambda x: x < 0) + [[], [1, 2, 3, 4]] + >>> Iter([1, 4]).split_while(lambda x: x > 0) + [[1, 2, 3, 4], []] + ``` + """ + default = len(self.image) + 1 + element = next(itertools.filterfalse(fun, self.image), default) + count = self.image.index(element) if element in self.image else default + self.image = [self.image[:count], self.image[count:]] + return self + + @overload + def split_with(self, fun: Callable[[Any], bool]) -> Iter: + """ + Split `self.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` + 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 + 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. + + ```python + >>> Iter([1, 5]).reverse().split_with(lambda x: x % 2 == 0) + [[4, 2, 0], [5, 3, 1]] + >>> Iter({'a': 1, 'b': -2, 'c': 1, 'd': -3}).split_with(lambda k, v: v < 0) + [{'b': -2, 'd': -3}, {'a': 1, 'c':1}] + ``` + """ + ... + + @overload + def split_with(self, fun: Callable[[Any, Any], bool]) -> Iter: ... + + def split_with(self, fun: Callable[[Any], bool] | Callable[[Any, Any], bool]) -> Iter: + 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)]) + self.image = [dict(f1), dict(f2)] + else: + t1, t2 = itertools.tee(self.image) + self.image = [list(filter(fun, t1)), list(itertools.filterfalse(fun, t2))] + return self + + def sum(self) -> Union[int, float, complex, str]: + """ + Return the sum of all elements. + + ```python + >>> Iter([1, 100]).sum() + 5050 + ``` + """ + return sum(self.image) + + def take(self, amount: int) -> Iter: + """ + Takes an `amount` of elements from the beginning or the end of `self.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 + 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) + [1, 2] + >>> Iter([1, 3]).take(10) + [1, 2, 3] + >>> Iter([1, 2, 3]).take(0) + [] + >>> Iter([1, 2, 3]).take(-1) + [3] + ``` + """ + self.image = list(itertools.islice(self.image, amount) if amount > 0 else itertools.islice(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 + 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) + [1, 3, 5, 7, 9] + >>> Iter([1, 10]).take_every(0) + [] + >>> Iter([1, 3]).take_every(1) + [1, 2, 3] + ``` + """ + 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: + """ + Take `count` random elements from `self.image`. + + ```python + >>> Iter([1, 10]).take_random(2) + [3, 1] + >>> Iter([1, 10]).take_random(2) + [6, 9] + ``` + """ + self.image = random.choices(self.image, k=count) + return self + + def take_while(self, fun: Callable[[Any], bool]) -> Iter: + """ + Take the elements from the beginning of `self.image` while `fun` returns + a truthy value. + + ```python + >>> Iter([1, 3]).take_while(lambda x: x < 3) + [1, 2] + ``` + """ + self.image = list(itertools.takewhile(fun, self.image)) + return self + + def uniq(self) -> Iter: + """ + Enumerates `self.image`, removing all duplicated elements. + + ```python + >>> Iter([1, 2, 3, 3, 2, 1]).uniq() + [1, 2, 3] + ``` + """ + self.image = list(Counter(self.image).keys()) + return self + + def unzip(self) -> Iter: + """ + Opposite of `self.zip`. Extracts two-element tuples from `self.image` and + groups them together. + + ```python + >>> Iter({'a': 1, 'b': 2, 'c': 3}).unzip() + [['a', 'b', 'c'], [1, 2, 3]] + >>> Iter([('a', 1), ('b', 2), ('c', 3)]).unzip() + [['a', 'b', 'c'], [1, 2, 3]] + >>> Iter([['a', 1], ['b', 2], ['c', 3]]).unzip() + [['a', 'b', 'c'], [1, 2, 3]] + ``` + """ + if isinstance(self.image, Dict): + self.image = [list(self.image.keys()), list(self.image.values())] + else: + self.image = [list(map(operator.itemgetter(0), self.image)), list(map(operator.itemgetter(1), self.image))] + return self + + @overload + def with_index(self, fun_or_offset: Optional[int]=None) -> Iter: + """ + Return `self.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`. + + ```python + >>> Iter(list("abc")).with_index() + [('a', 0), ('b', 1), ('c', 2)] + >>> Iter(list("abc")).with_index(2) + [('a', 2), ('b', 3), ('c', 4)] + >>> Iter(list("abc")).with_index(lambda k, v: (v, k)) + [(0, 'a'), (1, 'b'), (2, 'c')] + ``` + """ + ... + + @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: + 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))) + else: + self.image = list(itertools.starmap(fun_or_offset, zip(self.image, range(len(self.image))))) + return self + + def zip(self, *iterables: 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 + collection completes. + + ```python + >>> Iter(list("abc")).zip(range(3)) + [('a', 0), ('b', 1), ('c', 2)] + >>> Iter(list("abc")).zip(range(3), list("def")) + [('a', 0, 'd'), ('b', 1, 'e'), ('c', 2, 'f')] + >>> Iter([1, 3]).zip(list("abc"), ["foo", "bar", "baz"]) + [(1, 'a', "foo"), (2, 'b', "bar"), (3, 'c', "baz")] + ``` + """ + self.image = list(zip(self.image, *iterables)) + 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. + The reducer will receive 2 args: a list of elements (one from each enum) and + the accumulator. + + ```python + >>> # TODO + ``` + + Reduce over two iterables halting as soon as either iterable is empty. + + ```python + >>> # TODO + ``` + """ + ... + + @overload + def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: List=None, right: List=None) -> Iter: ... + + def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: List=None, right: List=None) -> Iter: + if left is not None and right is not None: + raise NotImplementedError() + else: + raise NotImplementedError() + return self + def __str__(self) -> str: - return f"[{', '.join(map(str, self.image))}]" if isinstance(self.image, Iterable) else self.image + return str(self.image) def __repr__(self) -> str: raise NotImplementedError() diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index 9d1f537..44f9d7a 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -275,6 +275,72 @@ def test_slide(self): self.assertEqual(['a', 'b', 'c', 'e', 'f', 'g', 'd'], Iter(list("abcdefg")).slide(3, -1).image, msg="Slide with negative indices (counting from the end)") self.assertEqual(['a', 'b', 'c', 'e', 'f', 'd', 'g'], Iter(list("abcdefg")).slide(3, -2).image, msg="Slide with negative indices (counting from the end)") + def test_sorted(self): + self.assertEqual([1, 2, 3], Iter([3, 1, 2]).sort().image) + 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) + + 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) + + 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([{'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()) + + 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) + + 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) + + def test_take_random(self): + numbers = set(Iter.range([1, 100])) + self.assertTrue(set(Iter([1, 100]).take_random(2).image).issubset(numbers)) + + def test_take_while(self): + self.assertEqual([1, 2], Iter([1, 3]).take_while(lambda x: x < 3).image) + + def test_uniq(self): + self.assertEqual([1, 2, 3], Iter([1, 2, 3, 3, 2, 1]).uniq().image) + + def test_unzip(self): + self.assertEqual([['a', 'b', 'c'], [1, 2, 3]], Iter({'a': 1, 'b': 2, 'c': 3}).unzip().image) + self.assertEqual([['a', 'b', 'c'], [1, 2, 3]], Iter([('a', 1), ('b', 2), ('c', 3)]).unzip().image) + self.assertEqual([['a', 'b', 'c'], [1, 2, 3]], Iter([['a', 1], ['b', 2], ['c', 3]]).unzip().image) + + def test_with_index(self): + self.assertEqual([('a', 0), ('b', 1), ('c', 2)], Iter(list("abc")).with_index().image) + self.assertEqual([('a', 2), ('b', 3), ('c', 4)], Iter(list("abc")).with_index(2).image) + 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([('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) + + @pytest.mark.xfail(raises=NotImplementedError, reason='TODO') + def test_zip_reduce(self): + self.assertEqual([(1, 2, 3), (1, 2, 3)], Iter([[1, 1], [2, 2], [3, 3]]).zip_reduce([], lambda x, acc: x).image) + #endregion #region leet code tests From 8fc5219a1cdd49cf23ec1d7877d4e466081b82c6 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 25 Dec 2021 01:29:16 +0100 Subject: [PATCH 21/30] Implement zip_reduce (partially), shorten, str and repr --- src/iterfun/iterfun.py | 33 +++++++++++++++++++++++++-------- tests/test_iterfun.py | 17 +++++++++++++++-- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index c73ecdf..18ecb68 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -7,7 +7,8 @@ import operator import random import statistics -from collections import Counter, ChainMap +import textwrap +from collections import ChainMap, Counter from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union, overload @@ -788,6 +789,20 @@ 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: + """ + Shorten an iterable sequence into an short, human-readable string. + + ```python + >>> Iter.shorten(range(1, 6)) + '[1, 2, 3, 4, 5]' + >>> Iter.shorten(range(1, 101), width=50) + '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...]' + ``` + """ + return textwrap.shorten(str(list(sequence)), width=width, placeholder=' ...]') + def shuffle(self) -> Iter: """ Return a list with the elements of `self.image` shuffled. @@ -1164,29 +1179,31 @@ def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any]) -> Iter: the accumulator. ```python - >>> # TODO + >>> Iter([[1, 1], [2, 2], [3, 3]]).zip_reduce([], lambda x, acc: tuple(x) + (acc,)) + [(1, 2, 3), (1, 2, 3)] ``` Reduce over two iterables halting as soon as either iterable is empty. ```python - >>> # TODO + >>> Iter([]).zip_reduce([5, 6], lambda x, acc: tuple(x) + (acc,), [1, 2], {'a': 3, 'b': 4}) + [(1, {'a': 3}, 5), (2, {'b': 4}, 6)] ``` """ ... @overload - def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: List=None, right: List=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: List=None, right: List=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: raise NotImplementedError() else: - raise NotImplementedError() + self.image = list(functools.reduce(reducer, zip(*self.image), acc)) return self def __str__(self) -> str: - return str(self.image) + return Iter.shorten(self.image, width=50) def __repr__(self) -> str: - raise NotImplementedError() + return f"Iter(domain={Iter.shorten(self.domain)},image={Iter.shorten(self.image)})" diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index 44f9d7a..8563e13 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -253,6 +253,10 @@ def test_scan(self): 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) + 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)) + def test_shuffle(self): iter = Iter([1, 10]).shuffle() self.assertTrue(iter.all(lambda x: x in Iter.range([1, 10]))) @@ -337,9 +341,18 @@ def test_zip(self): 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) - @pytest.mark.xfail(raises=NotImplementedError, reason='TODO') + @pytest.mark.xfail(raises=NotImplementedError,reason='TODO') def test_zip_reduce(self): - self.assertEqual([(1, 2, 3), (1, 2, 3)], Iter([[1, 1], [2, 2], [3, 3]]).zip_reduce([], lambda x, acc: x).image) + self.assertEqual([(1, 2, 3), (1, 2, 3)], Iter([[1, 1], [2, 2], [3, 3]]).zip_reduce([], lambda x, acc: tuple(x) + (acc,)).image) + self.assertEqual([(1, {'a': 3}, 5), (2, {'b': 4}, 6)], Iter([]).zip_reduce([5, 6], lambda x, acc: tuple(x) + (acc,), [1, 2], {'a': 3, 'b': 4}).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, ...]", str(Iter([1, 100]))) + + def test_repr(self): + self.assertEqual("Iter(domain=[1, 5],image=[1, 2, 3, 4, 5])", repr(Iter([1, 5]))) + self.assertEqual("Iter(domain=[1, 50],image=[1, 2, 3, 4, 5, ...])", repr(Iter([1, 50]))) #endregion From 1699f27d72e5c536f5365fb7deb5cace6c9ab835 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 25 Dec 2021 14:47:57 +0100 Subject: [PATCH 22/30] Modify domain in ctor and implement zip_with 1. self.domain now receives ranges (if any) and not just its bounds as a list 2. implements zip_with; however this does not work with iterables in self.image yet. Lists to be zipped and mapped have to be passed directly to zip_with in iterable as arguments. --- src/iterfun/iterfun.py | 24 +++++++++++++++++++++++- tests/test_iterfun.py | 24 +++++++++++++----------- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 18ecb68..4432259 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -39,8 +39,8 @@ def __init__(self, iter: List[int, int], interval: bool=True) -> Iter: ... 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.domain = iter self.image = Iter.__ctor(iter) if interval and len(iter) == 2 else iter + self.domain = self.image @staticmethod def __ctor(iter: Iterable | List[int, int] | Tuple[int, int]) -> List: @@ -1202,6 +1202,28 @@ def zip_reduce(self, acc: List, reducer: Callable[[Any, Any], Any], left: Iterab self.image = list(functools.reduce(reducer, zip(*self.image), acc)) return self + def zip_with(self, fun: Callable[..., Any], *iterable: Iterable) -> Iter: + """ + 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 + each of the lists in iterables will be put into a list which is then passed + to the 1-arity `fun` function. Then, the second elements from each of the + iterables are put into a list and passed to `fun`, and so on until any one + of the lists in `iterables` runs out of elements. Returns a list with all + the results of calling `fun`. + + ```python + >>> Iter([]).zip_with(operator.add, [1, 2, 3], [4, 5, 6]) + [5, 7, 9] + >>> Iter([1, 1]).zip_with(operator.add, [1, 2], [3, 4, 6, 7]).image + [5, 7] + >>> Iter([]).zip_with(lambda x, y, z: x + y + z, [1, 3], [3, 5], [1, 3]) + [5, 11] + ``` + """ + self.image = [fun(*args) for args in zip(*iterable)] + return self + def __str__(self) -> str: return Iter.shorten(self.image, width=50) diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index 8563e13..f8c00a6 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -7,13 +7,16 @@ class TestIter(unittest.TestCase): - #region unit tests + 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(self): + 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)).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()) @@ -346,16 +349,15 @@ def test_zip_reduce(self): self.assertEqual([(1, 2, 3), (1, 2, 3)], Iter([[1, 1], [2, 2], [3, 3]]).zip_reduce([], lambda x, acc: tuple(x) + (acc,)).image) self.assertEqual([(1, {'a': 3}, 5), (2, {'b': 4}, 6)], Iter([]).zip_reduce([5, 6], lambda x, acc: tuple(x) + (acc,), [1, 2], {'a': 3, 'b': 4}).image) + def test_zip_with(self): + self.assertEqual([5, 7, 9], Iter([]).zip_with(operator.add, [1, 2, 3], [4, 5, 6]).image) + 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_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, ...]", str(Iter([1, 100]))) def test_repr(self): - self.assertEqual("Iter(domain=[1, 5],image=[1, 2, 3, 4, 5])", repr(Iter([1, 5]))) - self.assertEqual("Iter(domain=[1, 50],image=[1, 2, 3, 4, 5, ...])", repr(Iter([1, 50]))) - - #endregion - - #region leet code tests - - #endregion + 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]))) From c771b5543307200ded92501d44ae8d3f0162f207 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 25 Dec 2021 16:35:17 +0100 Subject: [PATCH 23/30] Make concat non-static and tighten ctor's interval restriction --- src/iterfun/iterfun.py | 35 ++++++++++++++++++++--------------- tests/test_iterfun.py | 11 ++++++----- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 4432259..9054c3e 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -44,7 +44,7 @@ def __init__(self, iter: Iterable | List[int, int] | Tuple[int, int], interval: @staticmethod def __ctor(iter: Iterable | List[int, int] | Tuple[int, int]) -> List: - if (isinstance(iter, Tuple) or isinstance(iter, List)): + if (isinstance(iter, Tuple) or isinstance(iter, List)) and (isinstance(iter[0], int) and isinstance(iter[1], int)): return Iter.range(iter) if iter[1] != 0 else [] return iter @@ -148,28 +148,33 @@ def chunk_every(self, count: int, step: Optional[int]=None, leftover: Optional[L def chunk_while(self, acc: List, chunk_fun: Callable, chunk_after: Callable) -> Iter: raise NotImplementedError() - @overload - @staticmethod - def concat(iter: List[Any]) -> Iter: + def concat(self) -> Iter: """ Given a list of lists, concatenates the list into a single list. ```python - >>> Iter.concat([[1, [2], 3], [4], [5, 6]]) + >>> 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] - >>> Iter.concat((1, 3), (4, 6)) - [1, 2, 3, 4, 5, 6] ``` - """ - ... - @overload - @staticmethod - def concat(*iter: Tuple[int, int]) -> Iter: ... + mode with ranges: - @staticmethod - def concat(*iter: List[Any] | Tuple[int, int]) -> Iter: - return Iter(list(itertools.chain(*(iter[0] if isinstance(iter[0], List) else [range(t[0], t[1]+1) for t in iter])))) + ```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] + ``` + """ + if all(map(lambda x: len(x) == 2, self.image)): + self.image = list(itertools.chain(*map(Iter.range, self.image))) + else: + self.image = list(itertools.chain(*self.image)) + return self def count(self, fun: Optional[Callable[[Any], bool]]=None) -> int: """ diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index f8c00a6..b6d0d89 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -63,11 +63,12 @@ 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_concant(self): - self.assertEqual([1, [2], 3, 4, 5, 6], Iter.concat([[1, [2], 3], [4], [5, 6]]).image) - self.assertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], Iter.concat((1, 3), (4, 6), (7, 9)).image) - self.assertEqual([1, 2, 3, 4, 5, 6], Iter.concat([[1, 2, 3], [4, 5, 6]]).image) - self.assertEqual([1, 2, 3, 4, 5, 6], Iter.concat((1, 3), (4, 6)).image) + 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()) From 4d76a29c0417160ae230d2b0521441cad6ac5eaf Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 25 Dec 2021 22:10:39 +0100 Subject: [PATCH 24/30] Improve implementation of dedup --- src/iterfun/iterfun.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 9054c3e..f53ff51 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -204,7 +204,6 @@ 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: """ Enumerate `self.image`, returning a list where all consecutive duplicated @@ -215,7 +214,7 @@ def dedup(self) -> Iter: [1, 2, 3, 2, 1] ``` """ - self.image = [group[0] for group in itertools.groupby(self.image)] + self.image = list(map(operator.itemgetter(0), itertools.groupby(self.image))) return self def dedup_by(self, fun: Callable[[Any], bool]): From d08b1854e827b9840655a00abc33867f3a9ea406 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 25 Dec 2021 22:47:51 +0100 Subject: [PATCH 25/30] Implement dedup_by and change width in str to 80 --- src/iterfun/iterfun.py | 18 ++++++++++++++++-- tests/test_iterfun.py | 7 +++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index f53ff51..b6a49c8 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -218,7 +218,21 @@ def dedup(self) -> Iter: return self def dedup_by(self, fun: Callable[[Any], bool]): - raise NotImplementedError() + """ + Enumerates `self.image`, returning a list where all consecutive duplicated + elements are collapsed to a single element. + + ```python + >>> Iter([5, 1, 2, 3, 2, 1]).dedup_by(lambda x: x > 2) + [5, 1, 3, 2] + >>> Iter([0, 4, 9, 1, 2, 0, 3, 4, 9]).dedup_by(lambda x: x < 2) + [0, 4, 1, 2, 0, 3] + >>> Iter([3, 6, 7, 7, 2, 0, 1, 4, 1]).dedup_by(lambda x: x == 2) + [3, 2, 0] + ``` + """ + 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 drop(self, amount: int) -> Iter: """ @@ -1229,7 +1243,7 @@ def zip_with(self, fun: Callable[..., Any], *iterable: Iterable) -> Iter: return self def __str__(self) -> str: - return Iter.shorten(self.image, width=50) + return Iter.shorten(self.image, width=80) def __repr__(self) -> str: return f"Iter(domain={Iter.shorten(self.domain)},image={Iter.shorten(self.image)})" diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index b6d0d89..6a9c59c 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -87,9 +87,12 @@ def test_count_until(self): def test_dedup(self): self.assertEqual([1, 2, 3, 2, 1], Iter([1, 2, 3, 3, 2, 1]).dedup().image) - @pytest.mark.xfail(raises=NotImplementedError, reason="TODO") def test_dedup_by(self): self.assertEqual([5, 1, 3, 2], Iter([5, 1, 2, 3, 2, 1]).dedup_by(lambda x: x > 2).image) + self.assertEqual([0, 3, 2], Iter([0, 1, 2, 3, 2, 1]).dedup_by(lambda x: x > 2).image) + self.assertEqual([0, 4, 1, 2, 0, 3], Iter([0, 4, 9, 1, 2, 0, 3, 4, 9]).dedup_by(lambda x: x < 2).image) + 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_drop(self): self.assertEqual([3], Iter([1, 3]).drop(2).image) @@ -357,7 +360,7 @@ def test_zip_with(self): 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, ...]", str(Iter([1, 100]))) + 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]))) def test_repr(self): self.assertEqual("Iter(domain=[1, 2, 3, 4, 5],image=[1, 2, 3, 4, 5])", repr(Iter([1, 5]))) From 47bd21e6a01984f763fc06e15b835aa539d637d3 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 25 Dec 2021 23:00:13 +0100 Subject: [PATCH 26/30] Remove list conversion in at and implement fetch --- src/iterfun/iterfun.py | 21 ++++++++++++++++++--- tests/test_iterfun.py | 9 ++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index b6a49c8..9da9666 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -99,7 +99,7 @@ def at(self, index: int) -> Any: IndexError: list index out of range ``` """ - self.image = list(self.image)[index] + self.image = self.image[index] return self.image def avg(self) -> Union[int, float]: @@ -313,8 +313,23 @@ def empty(self) -> bool: """ return not bool(len(self.image)) - def fetch(self, index: int) -> bool: - raise NotImplementedError() + def fetch(self, index: int) -> Dict[str, Any]: + """ + Find the element at the given index (zero-based). Return `{'ok': element}` + if found, otherwise `{'error': None}`. + + ```python + >>> Iter([2, 4, 6]).fetch(0) + {'ok': 2} + >>> Iter([2, 4, 6]).fetch(-3) + {'ok': 2} + >>> Iter([2, 4, 6]).fetch(2) + {'ok': 6} + >>> Iter([2, 4, 6]).fetch(4) + {'error': None} + ``` + """ + return {'ok': self.image[index]} if index < len(self.image) else {'error': None} def filter(self, fun: Callable[[Any], bool]) -> Iter: """ diff --git a/tests/test_iterfun.py b/tests/test_iterfun.py index 6a9c59c..b6f7fdd 100644 --- a/tests/test_iterfun.py +++ b/tests/test_iterfun.py @@ -116,12 +116,11 @@ def test_empty(self): self.assertTrue(Iter([0, 0]).empty()) self.assertFalse(Iter([1, 10]).empty()) - @pytest.mark.xfail(raises=NotImplementedError, reason="TODO") def test_fetch(self): - self.assertTrue(Iter([2, 4, 6]).fetch(0).image) - self.assertTrue(Iter([2, 4, 6]).fetch(-3).image) - self.assertTrue(Iter([2, 4, 6]).fetch(2).image) - self.assertFalse(Iter([2, 4, 6]).fetch(4).image) + self.assertEqual({'ok': 2}, Iter([2, 4, 6]).fetch(0)) + self.assertEqual({'ok': 2}, Iter([2, 4, 6]).fetch(-3)) + self.assertEqual({'ok': 6}, Iter([2, 4, 6]).fetch(2)) + self.assertEqual({'error': None}, Iter([2, 4, 6]).fetch(4)) def test_filter(self): self.assertEqual([2], Iter([1, 3]).filter(lambda x: x % 2 == 0).image) From 6102a8114979100e9de9fc9827f1b89a3baef4aa Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 25 Dec 2021 23:39:21 +0100 Subject: [PATCH 27/30] Tidy up formatting and placement and remove unused imports --- src/iterfun/iterfun.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/iterfun/iterfun.py b/src/iterfun/iterfun.py index 9da9666..25936d5 100644 --- a/src/iterfun/iterfun.py +++ b/src/iterfun/iterfun.py @@ -9,7 +9,7 @@ import statistics import textwrap from collections import ChainMap, Counter -from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union, overload +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union, overload class Iter: @@ -1257,8 +1257,8 @@ def zip_with(self, fun: Callable[..., Any], *iterable: Iterable) -> Iter: self.image = [fun(*args) for args in zip(*iterable)] return self - def __str__(self) -> str: - return Iter.shorten(self.image, width=80) - def __repr__(self) -> str: return f"Iter(domain={Iter.shorten(self.domain)},image={Iter.shorten(self.image)})" + + def __str__(self) -> str: + return Iter.shorten(self.image, width=80) From 677fb1b65cfd804630085701909a9cf7915d8bb5 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 25 Dec 2021 23:39:48 +0100 Subject: [PATCH 28/30] Increment version number --- 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 9ad524a..0b3735b 100644 --- a/src/iterfun/__init__.py +++ b/src/iterfun/__init__.py @@ -2,7 +2,7 @@ from .iterfun import Iter -__version__ = "0.0.2" +__version__ = "0.0.3" package_name = "iterfun" python_major = "3" python_minor = "7" From ef52c7beeb8917be8175a15f17d097f1700aaf5b Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 25 Dec 2021 23:43:06 +0100 Subject: [PATCH 29/30] Update description --- README.md | 5 +++-- setup.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b230bb4..a149606 100644 --- a/README.md +++ b/README.md @@ -18,5 +18,6 @@

-Implements an iterator interface reminiscent of functional programming languages -such as Elixir. +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. diff --git a/setup.py b/setup.py index aae3a5c..1d47c49 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ author_email="greve.stefan@outlook.jp", name=package_name, version=version, - description="Implements an iterator interface reminiscent of functional programming languages such as Elixir.", + description="Implements an eager iterator class reminiscent of Elixir's Enum structure.", long_description=long_description, long_description_content_type='text/markdown', license='MIT', From 2d4d5ddb692e7470cff34e1446423ffe04b9e0c0 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 25 Dec 2021 23:43:49 +0100 Subject: [PATCH 30/30] Add changelog for v0.0.3 --- CHANGELOG.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af722f2..68a9253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,90 @@ # Changelog +## Version 0.0.3 (25 Dec 2021) + +See below for a break down of the development status of all methods provided by +the `Iter` class. Some methods also provide overloads via `typing`. Support for +working with dictionaries is rather scarce. Like `Enum` in Elixir, this is an eager +implementation meaning that generators are always converted into lists, thus consumed +all at once. Perhaps there will be a `Stream`-like structure in the future that's +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__` + +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 +this list yet; it may be added in future versions of this library. + ## Version 0.0.2 (16 Dec 2021) Finalize package structure for development and setup release pipeline.